Skip to content

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 #

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 = 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.

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 #

expression instance-attribute #

foreach 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 lookups: (:class:LookupBlock), 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, 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.

cases = {} class-attribute instance-attribute #

description = None class-attribute instance-attribute #

expression = None class-attribute instance-attribute #

foreach = 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 #

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.

description = None class-attribute instance-attribute #

dtype = None class-attribute instance-attribute #

into = None class-attribute instance-attribute #

over 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: _StrictBlock

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).

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 #

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 #

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.

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.

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.

Source code in src/math_spec/model.py
def labels_of(self, dimension: str) -> dict[str, LookupBlock]:
    """The label-space lookups over *dimension* — selection only, never an axis."""
    return {n: lk for n, lk in self.lookups.items() if lk.over == dimension and lk.into is None}

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

targeted_of(dimension) #

The groupable lookups over dimension: name -> the dim they map into.

Source code in src/math_spec/model.py
def targeted_of(self, dimension: str) -> dict[str, str]:
    """The groupable lookups over *dimension*: name -> the dim they map into."""
    return {n: lk.into for n, lk in self.lookups.items() if lk.over == dimension and lk.into is not None}

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.

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
def to_yaml(self) -> str:
    """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.
    """
    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 #

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.

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:'."