math_spec.model
The YAML surface's types — every block a file may contain, rooted at :class:Model.
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.
:class:Model is the first of the three stages the pipeline names — what a
file declares, before plan.Program (what it lowers to) and an engine
(what a build holds). Nothing here has seen data.
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
#
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 sink 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.
Buildable
#
Bases: Model
A model with nothing left to expand — what rows are built from.
A :class:Model is the file as written, and a file may carry a
piecewise: block whose variables and constraints do not exist until
:func:~math_spec.piecewise.expand_piecewise emits them. This is the
model after that pass, and it guarantees the one thing a builder needs:
variables: and constraints: hold the whole model, so the rows built
from it are the rows the file asked for.
Taking one is how a consumer says it builds rather than reads, and
passing a :class:Model where one is wanted is a type error rather than a
model quietly missing declarations. The subtyping runs the other way for
the same reason: whatever reads the file as written — the curve masks
:mod:math_spec.sources derives from piecewise: — takes a :class:Model
and accepts either.
The guarantee is about declarations, and deliberately says nothing about
the expression strings inside them: macros: and expressions: are
substituted per read, because an expression is needed only when someone
reads it where the set of declarations is needed before anything can be.
ConstraintBlock
#
DimensionBlock
#
Bases: _StrictBlock
A declared dimension with optional dtype and values.
A dimension is an axis and nothing else. The maps its members carry — a
generator's bus, a snapshot's period — are top-level lookups:
(:class:LookupBlock), keyed by their own name.
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:Model.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
The description matters more here than anywhere else: a named expression is
expanded away before the typeset walk, so its whole surface is
result.expression(name) after a solve — a name arriving in a summary
with nothing else to say what it counts.
cases = {}
class-attribute
instance-attribute
#
description = None
class-attribute
instance-attribute
#
expression = None
class-attribute
instance-attribute
#
foreach = None
class-attribute
instance-attribute
#
referenced_dims
property
#
foreach where there is one — only a cased expression declares it.
ExpressionCase
#
Bases: _StrictBlock
One case of a named expression: the value, and where it is the value.
when rather than where: a case selects which value a coordinate
takes, and creates no absence and deletes no row — which is what where
means on every other block (:doc:absence </reference/language/absence>).
LookupBlock
#
Bases: _StrictBlock
A named single-valued map out of a dimension (the declaration rules).
Two kinds, told apart by which field is set:
-
into:names the dimension the values are labels of — the groupable kind, whatsum(by=)lands terms on andat(by=)reads through, checked for containment once data is bound rather than joined blind::lookups: bus_of: {over: generator, into: bus} send: {over: line, into: bus}
-
dtype:declares an inline label space — the selection-only kind, owning its values and targeting nothing, so no axis exists for terms to land on. Grouping into one is refused with the promotion rewrite (:func:math_spec.resolution._ungroupable)::lookups: period: {over: snapshot, dtype: int}
values: gives the map in the file — {label of over: value} — for a
relation small enough to read, the way a dimension's own values: does.
A label it omits is unmapped, which is the partial case a lookup already
allows. Without it the map is supplied at bind time under the lookup's own
source key, as a (over, label space) relation of the rows it has (the
data-binding rules). One of the two, and never neither.
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.
Model
#
Bases: _StrictBlock
The declared math — one YAML file, or one dict, validated.
First of the three stages the pipeline names: Model is what a file
says, plan.Program what it lowers to, an engine what a build holds.
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
lps.load_model, 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 Model 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
#
declared_maps(dimension)
#
The lookups over dimension whose map the file declares, by name.
A map is not the dimension. It is a partial relation over one, free
to omit labels and written in whatever key order someone typed, so it
supplies values and never the label set or its order — those come from
dimensions.<d>.values or from the caller, and a label no map
mentions is a label with a null lookup, not a label that does not exist.
| RETURNS | DESCRIPTION |
|---|---|
dict[str, dict[Any, Any]]
|
|
dict[str, dict[Any, Any]]
|
map over dimension. |
Source code in src/math_spec/model.py
label_space(lookup)
#
What lookup's values are labels of, named as a supplied relation names it.
The groupable kind lands its values in the dimension it targets, so that dimension names them; the label-space kind owns its values and no dimension holds them, so the lookup does. One rule — the space the values live in — and the two kinds need no branch at the caller.
Source code in src/math_spec/model.py
labels_of(dimension)
#
The label-space lookups over dimension — selection only, never an axis.
model_validate(*args, **kwargs)
classmethod
#
Validate a mapping — see :func:_in_our_tree for what it raises.
model_validate_json(*args, **kwargs)
classmethod
#
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.
Hard rule 5 is that the model is the file you review and diff; a model a framework emitted as a dict has no such file. 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
ObjectiveBlock
#
ParameterBlock
#
PiecewiseBlock
#
Bases: _StrictBlock
N expressions jointly pinned to a breakpoint-indexed piecewise curve.
Mirrors linopy.Model.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
#
convex
property
#
Whether this block relaxes to the hull, which needs no binaries.
The curvature guard and the expansion both ask this rather than comparing against the method name, since what they act on is the absence of a restriction and not which word was written.
curvature_required
property
#
The curvature this block's method is only exact for, if any.
'either' is the hull's condition — it cuts corners on a mixed
curve and nothing else. lp states one side of the curve and its
sign says which, so the opposite bend is silently wrong rather than
merely loose.
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:Model.to_yaml reproduces the file.
SosBlock
#
Bases: _StrictBlock
A special-ordered set over one dimension of one variable.
Mirrors linopy.Model.add_sos_constraints, whose decomposition this
copies: a variable, the dimension the set runs along, the type, and the
optional big-M a reformulating sink caps its linking rows with. One set
per coordinate of the variable's foreach minus over; the members
are the variable's existing coordinates along over, and their order
is that dimension's declared one — what shift walks.
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 a sink can read off A: it is a set, carried to the sink that
has the concept and reformulated for the sink that does not.
VariableBlock
#
Bases: _StrictBlock
A declared decision variable.