math_spec.model
The YAML surface's types — every block a file may contain, rooted at :class:Spec.
A block per declaration kind, and one strict base: an unrecognised key is an
error naming the near miss rather than a shrug, because a dropped bounds:
leaves a variable unbounded and says nothing.
Nothing here has seen data.
CURVATURES = frozenset(get_args(Curvature))
module-attribute
#
Curvature = Literal['convex', 'concave', 'either']
module-attribute
#
DIMENSION_DTYPES = frozenset(get_args(DimensionDtype))
module-attribute
#
DimensionDtype = Literal['float', 'int', 'str', 'datetime']
module-attribute
#
Expression = Annotated[str, BeforeValidator(_number_is_an_expression, json_schema_input_type=str | float)]
module-attribute
#
LinkSign = Literal['==', '<=', '>=']
module-attribute
#
NUMERIC_DTYPES = frozenset({'float', 'int'})
module-attribute
#
ObjectiveSense = Literal['minimize', 'maximize']
module-attribute
#
PARAMETER_DTYPES = frozenset(get_args(ParameterDtype))
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
#
ParameterDtype = Literal['float', 'int', 'bool', 'str']
module-attribute
#
PiecewiseMethod = Literal['adjacency', 'sos2', 'convex', 'lp']
module-attribute
#
SOS_TYPES = frozenset(get_args(SosType))
module-attribute
#
SUPPORTED_VERSIONS = (0,)
module-attribute
#
SosType = Literal[1, 2]
module-attribute
#
VARIABLE_ABSENCE = frozenset(get_args(VariableAbsence))
module-attribute
#
VARIABLE_DOMAINS = frozenset(get_args(VariableDomain))
module-attribute
#
VariableAbsence = Literal['undefined', 'zero']
module-attribute
#
VariableDomain = Literal['continuous', 'integer', 'binary']
module-attribute
#
BoundsBlock
#
Bases: _StrictBlock
Variable bounds — each side is a number or parameter name.
linopy's defaults (add_variables(lower=-inf, upper=inf)): omitting a
bound leaves the variable unbounded on that side, not implicitly
non-negative. Non-negativity is a real constraint, so the file says it.
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 lookups:
(:class:LookupBlock), keyed by their own name.
ExpandedPiecewise
#
Bases: _StrictBlock
A piecewise: block after expansion: the block, and the parameters it emitted.
points is the mask the weights carry — the file's own parameter, or
the one derived from a values parameter; starts and ends are the
edge flags an lp block under a mask sits its domain rows on.
ExpressionBlock
#
Bases: _StrictBlock
A named quantity: one arithmetic expression, readable 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:
instead — one case per region over a declared foreach:, no two of them
claiming one coordinate, and an otherwise: for the rest::
previous_status:
foreach: [snapshot, generator]
cases:
always_on: { when: "not committable", expression: 1 }
boundary: { when: "committable and position(snapshot) == 0", expression: status_initial }
otherwise: shift(status, over=snapshot, offset=1)
So the constraint that needs it names it, rather than being forked into one copy per regime.
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
LookupBlock
#
Bases: _StrictBlock
A named single-valued map out of a dimension (the declaration rules).
Exactly one of into: (a groupable map onto that dimension, what
sum(by=) lands terms on) or dtype: (its own label space, selection
only)::
lookups:
bus_of: {over: generator, into: bus}
period: {over: snapshot, dtype: int}
The map itself is data, and arrives at bind time under the lookup's name.
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 into
core AST before either backend sees 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).
over names the breakpoint dimension; method is which of
:data:PIECEWISE_METHODS restricts the weights; activity names what the weights sum
to — 1 where the block is unconditional, and a binary where a curve applies
only when something runs, which pins the formulation to 0 when it is 0; points names a
boolean parameter saying how far each curve runs, for a model whose curves
are not all the same length. Expanded before building into plain variables
and constraints — see math_spec.piecewise.
activity = None
class-attribute
instance-attribute
#
curve
property
#
The two links as (x, y), the bounded one last.
Only a two-link block has a curve to speak of, and only a bounded link
can be the wrong way round in links: — so this is what reads the
pair anywhere the y side is the one being stated.
description = None
class-attribute
instance-attribute
#
links
instance-attribute
#
method = 'adjacency'
class-attribute
instance-attribute
#
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.
SosBlock
#
Bases: _StrictBlock
A special-ordered set over one dimension of one variable.
One set per coordinate of the variable's foreach minus over; the
members are the variable's existing coordinates along over, in that
dimension's declared order, and big_m is the optional cap a consumer
that reformulates the set puts on its linking rows.
type: 1 admits at most one nonzero member, type: 2 at most two,
and those two consecutive. Unlike every other block this one declares no
math to read off A: it is a set, carried to a consumer that has the
concept and reformulated for one that does not.
Spec
#
Bases: _StrictBlock
The declared math — one YAML file, or one dict, validated. Nothing here has seen data.
The API is the ten declaration sections plus version and
description, and two ways back out: :meth:to_dict for the model as
data, :meth:to_yaml for the file a reviewer reads. In goes through
to_spec, which raises
:class:~math_spec.errors.LanguageError on a model the language refuses.
Everything else on this class is pydantic's, not a contract this package
keeps — model_json_schema() describes the shape pydantic validates
rather than the language (checked in for editors as
schema/math_spec.schema.json), and model_construct() skips validation
entirely, so a Spec is valid when it was built the normal way.
constraints = {}
class-attribute
instance-attribute
#
description = None
class-attribute
instance-attribute
#
dimensions = {}
class-attribute
instance-attribute
#
expressions = {}
class-attribute
instance-attribute
#
lookups = {}
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
#
sos = {}
class-attribute
instance-attribute
#
variables = {}
class-attribute
instance-attribute
#
version = 0
class-attribute
instance-attribute
#
labels_of(dimension)
#
The label-space lookups over dimension — selection only, never an axis.
model_validate(*args, **kwargs)
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
targeted_of(dimension)
#
The groupable lookups over dimension: name -> the dim they map into.
to_dict()
#
to_yaml()
#
The file a reviewer reads — including for a model that never had one.
Generated rather than authored, so length costs a reader nothing and being unambiguous saves them knowing this package's defaults at all.
Source code in src/math_spec/model.py
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
#
domain = 'continuous'
class-attribute
instance-attribute
#
foreach
instance-attribute
#
where = None
class-attribute
instance-attribute
#
undeclared_dimension(kind, name, dimension)
#
The one wording for a declaration naming a dimension the file does not declare.