Skip to content

Spec API#

This page documents every name that mathspec.spec exports: Spec, the blocks its sections hold, and the operator names an expression may call. to_spec returns a Spec. Reading a spec and its program says how a spec and its program fit together.

The file: what it says, as every block it may contain, rooted at Spec.

The first public state. A Spec holds one file's sections as the blocks below, and BUILTIN_NAMES is the closed set of operators an expression in one may call. Nothing here has seen data; what the file means is its program.

BUILTIN_NAMES = frozenset(BUILTINS) module-attribute #

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 spec 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 attaching the data checks it, and refuses the data where it does not hold.

description = None class-attribute instance-attribute #

holds instance-attribute #

where = None class-attribute instance-attribute #

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.

lower = None class-attribute instance-attribute #

upper = None 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, the dtype its coordinates must be, and whether their order means anything.

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 when the data is attached. The maps its members carry — a generator's bus, a snapshot's period — are top-level relations: (RelationBlock), keyed by their own name.

description = None class-attribute instance-attribute #

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

ordered = False 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 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

dims: declares the frame the quantity is read over. A plain entry may leave it out, and its body then decides the frame; a body that carries a dimension the frame does not name is refused, and one that carries fewer is constant along the rest. 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.

adds_to: adds this entry as a term to the sum it names, a given: expressions: entry of this file: a write, where the given entry is the read. merge adds every term to that name by name, after the body a file defines where one does. The term is read over at most the frame the given entry states, and does not read the name it adds to.

adds_to = None class-attribute instance-attribute #

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 #

GivenBlock #

Bases: _StrictBlock

What this file reads and does not build, by kind. Closed at the five kinds.

constraints = {} class-attribute instance-attribute #

expressions = {} class-attribute instance-attribute #

masks = {} class-attribute instance-attribute #

parameters = {} class-attribute instance-attribute #

variables = {} class-attribute instance-attribute #

GivenConstraintBlock #

Bases: _StrictBlock

A row family this file reads the dual of and another model builds.

The frame says how many duals there are and what indexes them, which is what dual() needs. There is no expression:, because nothing here builds a row.

description = None class-attribute instance-attribute #

dims instance-attribute #

GivenExpressionBlock #

Bases: _StrictBlock

A named expression this file reads and another file defines.

The frame is all this file states. This file reads the name as a quantity over that frame, affine in the columns, as it reads a given variable: the body is the definer's, and the composed spec holds the body to the rules of every place this file reads it.

A named expression of this file may add to the name with adds_to:. merge then writes every term the files add after the body a file defines, or as the whole body where no file defines it. The file itself reads the name as the whole sum, alone and composed.

description = None class-attribute instance-attribute #

dims instance-attribute #

GivenMaskBlock #

Bases: _StrictBlock

A mask this file reads and another file defines.

The frame is all this file states, and it names the dims the definer's predicate reads. A where reads the name as data over that frame, true or false at each coordinate, since a mask reads nothing a solve decides: the predicate is the definer's.

description = None class-attribute instance-attribute #

dims instance-attribute #

GivenParameterBlock #

Bases: _StrictBlock

Data this file reads and another file declares.

It says the frame and the dtype of a ParameterBlock, which are what this file reads: a where compares against the dtype, and the dim rules read the frame. What a missing row means is the declaring file's missing:.

description = None class-attribute instance-attribute #

dims instance-attribute #

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

GivenVariableBlock #

Bases: _StrictBlock

A column this file reads and another file introduces.

The frame and the domain are all this file states. The file that introduces the column owns its bounds and its mask.

description = None class-attribute instance-attribute #

dims instance-attribute #

domain = 'continuous' class-attribute instance-attribute #

MacroBlock #

Bases: _StrictBlock

A parameterised expression template, defined in the YAML itself.

Language, not code: formals (args positional, kwargs keyword) shadow the spec's names inside the template, and every call site expands in the syntax tree before resolution reads the expression.

args = [] class-attribute instance-attribute #

description = None class-attribute instance-attribute #

kwargs = [] class-attribute instance-attribute #

template instance-attribute #

MaskBlock #

Bases: _StrictBlock

A named predicate: one where string, read wherever a where:, a when: or a holds: names it.

Written in YAML as a bare where string, or as a mapping once it carries a description:, and serialised back to whichever form it was written in::

masks:
  committable: "Generator_committable AND Generator_active"
  stands:
    where: build_year <= period_year AND period_year < build_year + lifetime
    description: the generator stands in this period

A bare mask name in a where string stands for its predicate, inside count, shift and at too. The mask's frame is the dims its predicate reads, so it declares none. A mask is a predicate, so it is never a value in an expression.

description = None class-attribute instance-attribute #

where 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, and what a missing row of its data means.

description = None class-attribute instance-attribute #

dims instance-attribute #

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

missing = 'refused' 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.

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 #

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 #

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 Spec.to_yaml reproduces the file.

expression instance-attribute #

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

values instance-attribute #

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 when the data is attached: 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]}

A sum joins the table on the columns over= names and every other key column, and groups by the columns by=relation[...] names; a lookup joins on the columns it names. The declaration fixes no direction, and a column is named after its own dimension or after none, so a name in a call reads the same as a column and as a dimension. The map itself is data, and arrives with the rest of it, 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.

missing = Field(default=None, json_schema_extra={'default': 'refused'}) class-attribute instance-attribute #

pairs property #

(role, dimension) per column, the key's columns first.

The program calls the same thing 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; 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.

along instance-attribute #

description = None class-attribute 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, model_validate, the constructor — runs every load-time check, expression pass included, and raises LanguageError on a spec the language refuses. Holding one is the proof, so nothing downstream checks it again.

The API is the thirteen declaration sections plus version and description, three ways back out — to_dict for the spec as data, to_yaml for the file a reviewer reads, expand for the spec with its formulations written out as plain rows — and program, the spec 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 #

given = GivenBlock() class-attribute instance-attribute #

macros = {} class-attribute instance-attribute #

masks = {} class-attribute instance-attribute #

objective = None class-attribute instance-attribute #

parameters = {} class-attribute instance-attribute #

piecewise = {} class-attribute instance-attribute #

program cached property #

This spec typed, section for section — what every reader after load walks.

Computing it is the expression pass, so a spec the language refuses raises here; loading forces it, so every ask on a spec in hand is the one object. It mirrors the spec: a piecewise: block still in it is a curve under program.piecewise and a sos: block a set under program.sos, and 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 spec 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. Expanding one writes those rows under names prefixed with the block's own, and drops the block. The result is a different spec: it declares more variables and constraints, so it does not compare equal to this one. It declares the same dimensions and parameters, so the same data attaches to both. Nothing is cached, so a second call builds the expansion again.

PARAMETER DESCRIPTION
kinds

Which formulations to write out — 'piecewise', 'sos', or none of them for every one. They go in that order whatever order they are asked in, because a method: sos2 curve emits a set and no set emits a curve.

TYPE: Formulation DEFAULT: ()

RETURNS DESCRIPTION
Spec

The spec with those blocks written out, or this same object where

Spec

it declares none of them, so an expansion asked for the same kinds

Spec

again returns itself. It is a spec like any other: to_yaml

Spec

writes it, and program holds its rows.

RAISES DESCRIPTION
ValueError

kinds names something that is not a formulation.

Source code in src/mathspec/spec.py
def expand(self, *kinds: Formulation) -> Spec:
    """This spec 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.
    Expanding one writes those rows under names prefixed with the block's
    own, and drops the block. The result is a different spec: it declares
    more variables and constraints, so it does not compare equal to this
    one. It declares the same dimensions and parameters, so the same data
    attaches to both. Nothing is cached, so a second call builds the
    expansion again.

    Args:
        kinds: Which formulations to write out — ``'piecewise'``,
            ``'sos'``, or none of them for every one. They go in that
            order whatever order they are asked in, because a
            ``method: sos2`` curve emits a set and no set emits a curve.

    Returns:
        The spec with those blocks written out, or this same object where
        it declares none of them, so an expansion asked for the same kinds
        again returns itself. It is a spec like any other: [`to_yaml`][]
        writes it, and [`program`][] holds its rows.

    Raises:
        ValueError: *kinds* names something that is not a formulation.
    """
    wanted = _formulations(kinds)
    from mathspec.piecewise import expand_piecewise
    from mathspec.sos import expand_sets

    expanded = expand_piecewise(self) if 'piecewise' in wanted else self
    if 'sos' in wanted and expanded.sos:
        expanded = expand_sets(expanded)
    return expanded

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/mathspec/spec.py
@classmethod
@override
def model_validate(
    cls,
    obj: object,
    *,
    strict: bool | None = None,
    extra: ExtraValues | None = None,
    from_attributes: bool | None = None,
    context: object = None,
    by_alias: bool | None = None,
    by_name: bool | None = None,
) -> 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(
            obj,
            strict=strict,
            extra=extra,
            from_attributes=from_attributes,
            context=context,
            by_alias=by_alias,
            by_name=by_name,
        )
    except ValidationError as exc:
        raise schema_error(exc) from None

to_dict() #

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

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

to_yaml(*, canonical=False) #

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

PARAMETER DESCRIPTION
canonical

Write the normal form instead: declarations sorted by name, every expression printed from its parsed tree, one term of a sum per line. Two files that state the same spec write the same text, so what a diff shows is a difference in the spec. The normal form loads to the same spec and not to an equal Spec, a reprinted expression being a different string.

TYPE: bool DEFAULT: False

Source code in src/mathspec/spec.py
def to_yaml(self, *, canonical: bool = False) -> str:
    """The file a reviewer reads — including for a spec that never had one.

    Args:
        canonical: Write the normal form instead: declarations sorted by
            name, every expression printed from its parsed tree, one term
            of a sum per line. Two files that state the same spec write
            the same text, so what a diff shows is a difference in the
            spec. The normal form loads to the same spec and not to an
            equal [`Spec`][mathspec.spec.Spec], a reprinted expression
            being a different string.
    """
    import yaml

    if canonical:
        from mathspec.canonical import canonical_yaml

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

VariableBlock #

Bases: _StrictBlock

A declared decision variable.

bounds = BoundsBlock() class-attribute instance-attribute #

description = None class-attribute instance-attribute #

dims instance-attribute #

domain = 'continuous' class-attribute instance-attribute #

missing = 'absent' 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.

Source code in src/mathspec/spec.py
def side_columns(written: str | list[str] | dict[str, str] | None) -> tuple[tuple[str, str], ...]:
    """``(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.
    """
    if written is None:
        return ()
    if isinstance(written, dict):
        return tuple(written.items())
    return tuple((d, d) for d in ((written,) if isinstance(written, str) else written))