math_spec.model
The YAML surface's types — every block a file may contain, rooted at :class:Spec.
Nothing here has seen data.
Curvature = Literal['convex', 'concave', 'either']
module-attribute
#
Expression = Annotated[str, BeforeValidator(_number_is_an_expression, json_schema_input_type=str | float)]
module-attribute
#
FORMULATIONS = ('piecewise', 'sos')
module-attribute
#
Formulation = Literal['piecewise', 'sos']
module-attribute
#
NUMERIC_DTYPES = frozenset({'float', 'int'})
module-attribute
#
PIECEWISE_METHODS = {'adjacency': 'a binary per segment, and a row making the two nonzero weights neighbours', 'sos2': 'the same weights, restricted by a set the solver branches on (the sos rules)', 'convex': 'nothing — the weights range over the hull, which is a pure LP', 'lp': 'no weights at all — one row per segment line, plus the two rows holding the domain'}
module-attribute
#
SOS_TYPES = frozenset(get_args(SosType))
module-attribute
#
SUPPORTED_VERSIONS = (0,)
module-attribute
#
AssumptionBlock
#
Bases: _StrictBlock
What the model assumes of its data: a predicate every coordinate it is checked at has to satisfy.
Written in YAML as a bare where string, or as a mapping once it carries a
where: or a description:, and serialised back to whichever form it
was written in::
assumptions:
efficiency_is_a_fraction: "efficiency > 0 AND efficiency <= 1"
bounds_do_not_cross:
holds: "p_min <= p_max"
where: "p_min"
description: a unit with no minimum is unconstrained below
The language decides nothing about the numbers, so the consumer binding the data checks it, and refuses the data where it does not hold.
BoundsBlock
#
Bases: _StrictBlock
Variable bounds — each side is a finite number, a parameter name, or None where it is open.
An omitted bound leaves the variable unbounded on that side, not
implicitly non-negative. An infinity is refused: an open side is null,
and the other infinity leaves no value at all.
ConstraintBlock
#
DimensionBlock
#
Bases: _StrictBlock
A declared dimension, and the dtype its coordinates must be.
A dimension is an axis and nothing else: it declares that the axis exists
and what its coordinates are typed as, never which coordinates there are —
those are data, and arrive at bind time. The maps its members carry — a
generator's bus, a snapshot's period — are top-level relations:
(:class:RelationBlock), keyed by their own name.
ExpressionBlock
#
Bases: _StrictBlock
A named quantity: one arithmetic expression, referenced by the math or read back after a solve.
Written in YAML as a bare string, or as a mapping once it carries a
description: — and serialised back to whichever form it was written in,
so a round trip through :meth:Spec.to_yaml reproduces the file::
expressions:
total_generation: sum(p, over=generator)
emissions:
expression: sum(p * rate, over=generator)
description: CO2 released, the quantity the cap bounds
A quantity whose value varies by region is written as cases: over a
declared dims:, with an otherwise: for the rest — see the
language reference.
ExpressionCase
#
Bases: _StrictBlock
One region of a named expression: the value, and when it is the value.
Every case says where it applies. The value wherever none of them does is
the block's otherwise:, which is written outside cases: because it
is not a region like these — it is what is left::
cases:
opening: { when: "position(snapshot) == 0", expression: p_max }
otherwise: 0
MacroBlock
#
Bases: _StrictBlock
A parameterised expression template, defined in the YAML itself.
Language, not code: formals (args positional, kwargs keyword)
shadow model names inside the template, and every call site expands in
the syntax tree before resolution reads the expression.
ObjectiveBlock
#
ParameterBlock
#
PiecewiseBlock
#
Bases: _StrictBlock
N expressions jointly pinned to a breakpoint-indexed piecewise curve.
Mirrors linopy.Spec.add_piecewise_formulation. Each link is
[expression, values_parameter] or [expression, values_parameter,
sign]: expression is any affine expression string, values_parameter
names a parameter carrying the over dim, and sign bounds the link by
the curve instead of pinning it (at most one non-"==", and only with
exactly two links).
activity = None
class-attribute
instance-attribute
#
curve
property
#
The two links as (x, y), the bounded one last.
Two-link blocks only.
description = None
class-attribute
instance-attribute
#
links
instance-attribute
#
method = 'adjacency'
class-attribute
instance-attribute
#
nominated
property
#
The block's own values parameter points: names, so the mask is derived from it — or None.
over
instance-attribute
#
points = None
class-attribute
instance-attribute
#
PiecewiseLink
#
Bases: _StrictBlock
One link of a piecewise block: an expression pinned to a values curve.
Written in YAML as [expression, values] or [expression, values,
sign] and serialised back to exactly that form, so a round trip through
:meth:Spec.to_yaml reproduces the file.
RelationBlock
#
Bases: _StrictBlock
A named relation between dimensions: the columns a row is keyed by, and the columns that key determines.
Each side is a dimension, a list of them, or a mapping of column name to
dimension where two columns share one. key: is the claim the language
checks at bind: one row per key tuple, so every values: column is a
function of it. A relation with no values: is bare — every column is
in its key, a row is its own identity, and nothing reads it::
relations:
gen_bus: {key: generator, values: bus}
gen_bt: {key: [generator], values: [bus, technology]}
zone_of: {key: [generator, period], values: zone}
ends: {key: line, values: {bus0: bus, bus1: bus}}
connection: {key: [generator, bus]}
An operator reads the table in the direction the call names
(over=, into=), joining on the other key columns; the
declaration fixes no direction. The map itself is data, and arrives at bind
time under the relation's name, one column per role.
description = None
class-attribute
instance-attribute
#
dims
property
#
key
instance-attribute
#
key_roles
property
#
The key roles, however key: was written.
pairs
property
#
(role, dimension) per column, the key's columns first.
The program calls the same thing :attr:~math_spec.program.RelationDeclaration.columns;
here the table has no field of its own, being what the two sides make.
roles
property
#
value_roles
property
#
The roles the key determines; empty for a bare relation.
values = None
class-attribute
instance-attribute
#
SosBlock
#
Bases: _StrictBlock
A special-ordered set over one dimension of one variable.
One set per coordinate of the variable's dims minus along; the
members are the variable's existing coordinates along along, in that
dimension's declared order.
type: 1 admits at most one nonzero member, type: 2 at most two,
and those two consecutive. A consumer with the concept takes the set as
one; :meth:Spec.expand states it as binaries instead, and the rows it
writes multiply by the member's own bounds, which is why a member
needs both.
Spec
#
Bases: _StrictBlock
The declared math — one YAML file, or one dict, validated. Nothing here has seen data.
A Spec that exists has passed the whole language: constructing one by
any route — to_spec, :meth:model_validate, the constructor — runs
every load-time check, expression pass included, and raises
:class:~math_spec.errors.LanguageError on a model the language refuses.
Holding one is the proof, so nothing downstream checks it again.
The API is the eleven declaration sections plus version and
description, three ways back out — :meth:to_dict for the model as
data, :meth:to_yaml for the file a reviewer reads, :meth:expand for the
same math with its formulations written out — and :attr:program, the
model typed, which every reader after load walks. Everything else on this
class is pydantic's, not a contract this package keeps.
assumptions = {}
class-attribute
instance-attribute
#
constraints = {}
class-attribute
instance-attribute
#
description = None
class-attribute
instance-attribute
#
dimensions = {}
class-attribute
instance-attribute
#
expressions = {}
class-attribute
instance-attribute
#
macros = {}
class-attribute
instance-attribute
#
objective = None
class-attribute
instance-attribute
#
parameters = {}
class-attribute
instance-attribute
#
piecewise = {}
class-attribute
instance-attribute
#
program
cached
property
#
This model typed, section for section — what every reader after load walks.
Computing it is the expression pass, so a model the language refuses
raises here; loading forces it, so every ask on a model in hand is the
one object. It mirrors the model: a piecewise: block still in it is
a curve under program.piecewise and a sos: block a set under
program.sos, and :meth:expand is what writes either out as rows,
so a consumer building rows reads spec.expand(...).program and
refuses a block it does not take.
relations = {}
class-attribute
instance-attribute
#
sos = {}
class-attribute
instance-attribute
#
variables = {}
class-attribute
instance-attribute
#
version = 0
class-attribute
instance-attribute
#
expand(*kinds)
#
This model with its formulations written out as plain variables and constraints.
A formulation states rows rather than being one — piecewise: states
a curve, sos: states which members of a family may be nonzero — and
expanding one writes those rows under names prefixed with the block's
own, then drops the block. The math is the same afterwards, and so is
the data that binds it: neither a set nor a curve emits a parameter,
and a curve's rows sit on where predicates over the file's own.
| PARAMETER | DESCRIPTION |
|---|---|
kinds
|
Which formulations to write out —
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Spec
|
The model those blocks wrote out, or this one where it declares |
Spec
|
none of them. It is a model like any other: :meth: |
Spec
|
it, and the file binds the same data as the one it came from. |
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
kinds names something that is not a formulation. |
Source code in src/math_spec/model.py
model_validate(obj, *, strict=None, extra=None, from_attributes=None, context=None, by_alias=None, by_name=None)
classmethod
#
Validate a mapping, raising this package's exception tree rather than pydantic's.
__init__ is not wrapped the same way, because defining one makes
pydantic run every after-validator twice.
Source code in src/math_spec/model.py
to_dict()
#
to_yaml()
#
The file a reviewer reads — including for a model that never had one.
VariableBlock
#
Bases: _StrictBlock
A declared decision variable.
absence = 'undefined'
class-attribute
instance-attribute
#
bounds = BoundsBlock()
class-attribute
instance-attribute
#
description = None
class-attribute
instance-attribute
#
dims
instance-attribute
#
domain = 'continuous'
class-attribute
instance-attribute
#
where = None
class-attribute
instance-attribute
#
side_columns(written)
#
(role, dimension) per column of one side of a relation, in written order.
A bare name or a list names each column after the dimension it is over; a mapping names the roles, which is what two columns over one dimension need.