Skip to content

math_spec.model

The YAML surface's types — every block a file may contain, rooted at :class:Spec.

Nothing here has seen data.

CURVATURES = frozenset(get_args(Curvature)) module-attribute #

Coverage = Literal['total', 'masked'] module-attribute #

Curvature = Literal['convex', 'concave', 'either'] module-attribute #

DIMENSION_DTYPES = frozenset(get_args(DimensionDtype)) module-attribute #

DeclaredDtype = ParameterDtype | 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 = ComparisonOperator 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.

An omitted bound leaves the variable unbounded on that side, not implicitly non-negative.

lower = float('-inf') class-attribute instance-attribute #

upper = float('inf') class-attribute instance-attribute #

ConstraintBlock #

Bases: _StrictBlock

A declared constraint: one rule, over one frame.

description = None class-attribute instance-attribute #

dims instance-attribute #

expression instance-attribute #

where = None class-attribute instance-attribute #

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.

description = None class-attribute instance-attribute #

dtype = 'str' class-attribute instance-attribute #

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.

block instance-attribute #

ends = None class-attribute instance-attribute #

points = None class-attribute instance-attribute #

starts = None class-attribute instance-attribute #

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.

cases = {} class-attribute instance-attribute #

description = None class-attribute instance-attribute #

dims = None class-attribute instance-attribute #

expression = None class-attribute instance-attribute #

otherwise = None class-attribute instance-attribute #

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

expression instance-attribute #

when instance-attribute #

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.

args = [] class-attribute instance-attribute #

description = None class-attribute instance-attribute #

kwargs = [] class-attribute instance-attribute #

template instance-attribute #

ObjectiveBlock #

Bases: _StrictBlock

A declared objective function.

description = None class-attribute instance-attribute #

expression instance-attribute #

sense = 'minimize' class-attribute instance-attribute #

ParameterBlock #

Bases: _CoveredBlock

A declared parameter with dims and dtype.

description = None class-attribute instance-attribute #

dims instance-attribute #

dtype = 'float' class-attribute instance-attribute #

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 #

consumes property #

The parameters the block reads: each link's values, and the points: mask.

The block owns the shape of every one of them, which is why :meth:Spec._piecewise_parameters_declare_no_coverage refuses coverage: there and lowering reports none.

curve property #

The two links as (x, y), the bounded one last.

Two-link blocks only.

description = None class-attribute instance-attribute #

method = 'adjacency' class-attribute instance-attribute #

over instance-attribute #

points = None class-attribute instance-attribute #

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.

expression instance-attribute #

sign = '==' class-attribute instance-attribute #

values instance-attribute #

RelationBlock #

Bases: _CoveredBlock

A named relation between dimensions, and the key it is single-valued per.

columns: is the table's columns — a list of dimensions, or a mapping of column name to dimension where two columns share one. key: names the columns each row is identified by, and is the claim the language checks at bind: one row per key tuple, so the other columns are a function of it. Without a key the table is a bare relation::

relations:
  gen_bus: {columns: [generator, bus], key: generator}
  zone_of: {columns: [generator, period, zone], key: [generator, period]}
  rep_of: {columns: {snapshot: snapshot, rep: snapshot}, key: snapshot}
  connection: {columns: [entity, bus]}

An operator walks 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.

columns instance-attribute #

description = None class-attribute instance-attribute #

dims property #

key = None class-attribute instance-attribute #

keys property #

The key roles, however key: was written; empty for a bare relation.

pairs property #

(role, dimension) per column in declared order — a list names each role after its dimension.

The program calls the same thing :attr:~math_spec.program.RelationDeclaration.columns; here that name belongs to the field, which is what the file wrote.

roles property #

values property #

The roles the key determines; empty where there is no key.

SosBlock #

Bases: _StrictBlock

A special-ordered set over one dimension of one variable.

One set per coordinate of the variable's dims 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.

big_m = None class-attribute instance-attribute #

description = None class-attribute instance-attribute #

over instance-attribute #

type instance-attribute #

variable instance-attribute #

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, expansion and 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 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. Everything else on this class is pydantic's, not a contract this package keeps.

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 #

relations = {} class-attribute instance-attribute #

sos = {} class-attribute instance-attribute #

variables = {} class-attribute instance-attribute #

version = 0 class-attribute instance-attribute #

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
@classmethod
@override
def model_validate(cls, *args: Any, **kwargs: Any) -> Self:
    """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.
    """
    try:
        return super().model_validate(*args, **kwargs)
    except ValidationError as exc:
        raise schema_error(exc) from None

relations_of(dimension) #

The relations with a column over dimension, by name.

Source code in src/math_spec/model.py
def relations_of(self, dimension: str) -> dict[str, RelationBlock]:
    """The relations with a column over *dimension*, by name."""
    return {n: lk for n, lk in self.relations.items() if dimension in lk.dims}

to_dict() #

The model as plain data. to_spec(m.to_dict()) reproduces it.

Source code in src/math_spec/model.py
def to_dict(self) -> dict[str, Any]:
    """The model as plain data. ``to_spec(m.to_dict())`` reproduces it."""
    return self.model_dump()

to_yaml() #

The file a reviewer reads — including for a model that never had one.

Source code in src/math_spec/model.py
def to_yaml(self) -> str:
    """The file a reviewer reads — including for a model that never had one."""
    import yaml

    return yaml.safe_dump(self.to_dict(), sort_keys=False, allow_unicode=True)

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 #

undeclared_dimension(kind, name, dimension) #

The one wording for a declaration naming a dimension the file does not declare.

Source code in src/math_spec/model.py
def undeclared_dimension(kind: str, name: str, dimension: str) -> str:
    """The one wording for a declaration naming a dimension the file does not declare."""
    return f"{kind} '{name}' references undeclared dimension '{dimension}'. Declare it under 'dimensions:'."