Skip to content

math_spec.program

The program: what a file declares, with names resolved and shapes fixed.

The second public state, and the one a consumer reads. A :class:Program is every declaration a file makes and no data at all; :func:~math_spec.lowering.to_program is the only thing that builds one, so nothing here re-checks a hand-built one.

Node and declaration classes are matched with isinstance. The rules a node's structure does not show are :func:children and :func:fan_in; the questions over the walk are :func:walk and the filters beside it. A resolved where arrives as a :class:Mask. Frozen dataclasses only — no execution logic, and nothing imported from a consumer. How a consumer reads one: docs/reference/language/reading.md.

Check = Increasing | Curved | AtLeastTwo | Contiguous module-attribute #

ConnectiveWhereNode = NotNode | AndNode | OrNode module-attribute #

ConstraintSense = ComparisonOperator module-attribute #

Derivation = MaskOf | FirstOf | LastOf module-attribute #

DimensionDtype = _model.DimensionDtype module-attribute #

ExpressionNode = Constant | Parameter | Variable | Negate | Add | Multiply | Power | Divide | Sum | GroupSum | At | Translate | Window | Cases module-attribute #

FanIn = Literal['one-to-one', 'many-to-one', 'one-to-many'] module-attribute #

ObjectiveSense = Literal['minimize', 'maximize'] module-attribute #

ParameterDtype = _model.ParameterDtype module-attribute #

PredicateOperator = Literal['<=', '>=', '==', '!=', '<', '>'] module-attribute #

QUADRATIC_POSITIONS = frozenset(get_args(QuadraticPosition)) module-attribute #

QuadraticPosition = Literal['objective', 'constraint'] module-attribute #

TypedPredicateNode = ParameterComparisonNode | ParameterDefinedNode | VariableDefinedNode | DimensionComparisonNode | DimensionPositionNode | LookupComparisonNode | LookupPairComparisonNode | LookupDefinedNode module-attribute #

VariableAbsence = _model.VariableAbsence module-attribute #

VariableType = _model.VariableDomain module-attribute #

WhereNode = BooleanLiteralNode | DimensionPositionNode | ParameterDefinedNode | VariableDefinedNode | ParameterComparisonNode | DimensionComparisonNode | LookupComparisonNode | LookupPairComparisonNode | LookupDefinedNode | NotNode | AndNode | OrNode module-attribute #

Add(left, right) dataclass #

Bases: Expression

left instance-attribute #

right instance-attribute #

AndNode(left, right) dataclass #

left instance-attribute #

right instance-attribute #

At(operand, over, coordinate, into) dataclass #

Bases: Expression

Read operand through a lookup — the adjoint of :class:GroupSum.

Same mapping table, walked the other way: GroupSum consumes over and produces into, this consumes into and produces over. The join fans out, many over labels sharing one into tuple.

coordinate instance-attribute #

into instance-attribute #

operand instance-attribute #

over instance-attribute #

AtLeastTwo(over, mask) dataclass #

Each curve has at least two breakpoints — every position along over, or those mask admits.

mask instance-attribute #

over instance-attribute #

BooleanLiteralNode(value) dataclass #

value instance-attribute #

Cases(regions) dataclass #

Bases: Expression

A value defined by region — exactly one region applies at each coordinate.

The regions are disjoint and total, so a consumer adds them rather than ranking them. Not a shape operator: every region spans the dims the expression does.

regions instance-attribute #

Constant(value) dataclass #

Bases: Expression

A scalar constant.

value instance-attribute #

ConstraintDeclaration(dims, lhs, sense, rhs, where=None) dataclass #

lhs sense rhs for each coord combination of dims.

Either side may carry variables and constants alike; which side a consumer gathers them onto is its own arrangement and not stated here. where masks out coord combinations (row absence, like variables).

dims instance-attribute #

lhs instance-attribute #

rhs instance-attribute #

sense instance-attribute #

where = None class-attribute instance-attribute #

Contiguous(mask, values) dataclass #

mask admits one consecutive run of at least one breakpoint per curve.

mask instance-attribute #

values instance-attribute #

Curved(x, y, over, curvature) dataclass #

y over x bends, along over, the way curvature says.

That is the shape the method is exact for. either is the hull's weaker condition: any single bend, so only a mixed curve fails it.

curvature instance-attribute #

over instance-attribute #

x instance-attribute #

y instance-attribute #

DimensionComparisonNode(name, op, value) dataclass #

Compare a dimension's own coordinates against a literal.

name instance-attribute #

op instance-attribute #

value instance-attribute #

DimensionDeclaration(lookups=(), dtype='str') dataclass #

A dimension and the lookups its labels carry, of both kinds.

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

lookups = () class-attribute instance-attribute #

maps property #

Every map over the dimension, targeted and label-space alike.

What binding needs a relation for: both kinds are read by a where and both arrive the same way, and only the targeted ones have a label set to be checked against.

targets property #

Each targeted map over the dimension, to the dimension its values are labels of.

The question every consumer of a by= asks, and asked here so it has one answer: an operator grouping through a lookup names the target as the dim it lands on, and a partition array is named for it so an amount declared over the group's own dim can be read through it.

DimensionPositionNode(name, op, position, by=None) dataclass #

Compare where a row sits along a dimension against a position — position(snapshot) == 0.

Both sides are integers, negative counting from the end. With by the position is counted within each group the lookup makes.

by = None class-attribute instance-attribute #

name instance-attribute #

op instance-attribute #

position instance-attribute #

Divide(numerator, divisor) dataclass #

Bases: Expression

Quotient numerator / divisor. The divisor must be variable-free.

divisor instance-attribute #

numerator instance-attribute #

Expression() dataclass #

Base class for expressions over variables and parameters.

Affine everywhere but where math_spec.degree admits a :class:Multiply of two variable-carrying operands; no node records which position that is.

FirstOf(block, mask) dataclass #

A bool parameter marking, per curve, the first breakpoint mask admits.

block instance-attribute #

mask instance-attribute #

Footprint(quadratic, variable_types, sos_types, shapes) dataclass #

Which of the language's constructs one program uses.

A subset, never the whole: an empty field says this program does not use the construct.

ATTRIBUTE DESCRIPTION
quadratic

Each position a product of two variable-carrying operands stands in; empty is affine throughout.

TYPE: frozenset[QuadraticPosition]

variable_types

Every domain declared.

TYPE: frozenset[VariableType]

sos_types

The order of each special-ordered set declared.

TYPE: frozenset[Literal[1, 2]]

shapes

Every expression node kind that appears.

TYPE: frozenset[type[ExpressionNode]]

quadratic instance-attribute #

shapes instance-attribute #

sos_types instance-attribute #

variable_types instance-attribute #

GroupSum(operand, over, coordinate, into) dataclass #

Bases: Expression

Sum operand through coordinates declared on dim over.

coordinate names lookups carried by dim over whose values are labels of the matching dim in into; the result replaces over with all of them. The two tuples are the same length and their order pairs them: several coordinates are one grouping into a product of targets, consumed in a single join.

coordinate instance-attribute #

into instance-attribute #

operand instance-attribute #

over instance-attribute #

Increasing(parameter, over) dataclass #

parameter is strictly increasing along over within each curve — the x-axis a method sorts by.

over instance-attribute #

parameter instance-attribute #

LastOf(block, mask) dataclass #

Its sibling for the last breakpoint.

block instance-attribute #

mask instance-attribute #

LookupComparisonNode(name, over, op, value) dataclass #

Compare a lookup's values against a literal — period_of == 2030.

over is the dimension the lookup maps out of.

name instance-attribute #

op instance-attribute #

over instance-attribute #

value instance-attribute #

LookupDeclaration #

Bases: NamedTuple

One declared lookup over a dimension, of either kind.

Exactly one of target and dtype is set. A targeted lookup's values are labels of target, checked for containment once the dim tables exist — which keeps a mistyped label from silently dropping its terms in the join that places them — and it is what sum(by=) lands terms on. A label space owns its values, typed by dtype the way a dimension's labels are: it is read for selection and rendering, and resolution refuses to group into one, so no expression node reaches it.

dtype = None class-attribute instance-attribute #

name instance-attribute #

target instance-attribute #

LookupDefinedNode(name, over) dataclass #

True where the named lookup has a value — a null says the label belongs to no group.

name instance-attribute #

over instance-attribute #

LookupPairComparisonNode(name, other, over, op) dataclass #

Compare two lookups over one dimension — from != to, row by row on that dimension's table.

name instance-attribute #

op instance-attribute #

other instance-attribute #

over instance-attribute #

Mask(root) dataclass #

A resolved where and the questions the language answers about it.

root is the predicate a consumer dispatches on with isinstance; every question below is derived from it. Construction folds, so a boolean literal stands at the root or nowhere in it, and refuses an unresolved tree.

ATTRIBUTE DESCRIPTION
root

The resolved predicate the mask restricts rows by, folded.

TYPE: WhereNode

atoms cached property #

The mask's leaves, connectives removed — the one walk the other questions read.

Held rather than re-walked: construction takes this walk anyway, to refuse an unresolved tree, and a mask cannot change afterwards.

conjuncts property #

The predicates the mask joins with AND — its AND spine flattened, stopping at an OR or a NOT.

dims property #

The dims the mask is read at — the union of what each leaf carries.

Empty for a mask over nothing but literals. Read off the leaves, which resolution stamped with their declarations' dims, so a predicate built from resolved pieces answers exactly as a declaration's own does.

names_read property #

The parameters, lookups and variables the mask names.

root instance-attribute #

MaskOf(block, values) dataclass #

A bool parameter true wherever values has a row.

The mask a points: naming one of the block's own breakpoints derives: the curve runs as far as its values do. values is the name the file wrote, so a refusal about the mask can say it.

block instance-attribute #

values instance-attribute #

Multiply(left, right) dataclass #

Bases: Expression

Product of two operands.

Affine where at least one factor is variable-free; degree 2 where neither is, which math_spec.degree admits in a :data:QuadraticPosition alone.

left instance-attribute #

right instance-attribute #

Negate(operand) dataclass #

Bases: Expression

operand instance-attribute #

NotNode(operand) dataclass #

operand instance-attribute #

ObjectiveDeclaration(sense, expression) dataclass #

Objective — scalar, every reduction in it one the file wrote.

expression instance-attribute #

sense instance-attribute #

OrNode(left, right) dataclass #

left instance-attribute #

right instance-attribute #

Parameter(name) dataclass #

Bases: Expression

A parameter reference — contributes to the constant part.

name instance-attribute #

ParameterComparisonNode(name, op, value, dims) dataclass #

Compare a parameter against a literal, element-wise.

dims instance-attribute #

name instance-attribute #

op instance-attribute #

value instance-attribute #

ParameterDeclaration(dims, dtype='float', derivation=None) dataclass #

Shape declaration; data is bound at execution time by name.

dtype is what the declaration claims the values are, and a consumer binding data refuses a column that is not it — so the declaration is what is read, rather than whatever the column happens to hold.

derivation = None class-attribute instance-attribute #

dims instance-attribute #

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

ParameterDefinedNode(name, dims) dataclass #

True wherever the named parameter is non-null and finite.

dims is the parameter's own, copied off the declaration during resolution; every leaf below that names a declaration carries its dims (or over) the same way.

dims instance-attribute #

name instance-attribute #

PiecewiseDeclaration(over, method, breakpoints, checks) dataclass #

A piecewise: block, kept as the facts a consumer binding its data reads.

The expansion lowered the links into constraints and emitted the parameters it needs — each of those says how it is filled, on its own :attr:ParameterDeclaration.derivation. What is left here is the curve and what the block assumes of it.

ATTRIBUTE DESCRIPTION
over

The breakpoint dimension.

TYPE: str

method

How the weights are restricted.

TYPE: PiecewiseMethod

breakpoints

The links' values parameters, in link order.

TYPE: tuple[str, ...]

checks

What the block assumes of the numbers, each carrying its own subjects, for the consumer holding them to check.

TYPE: tuple[Check, ...]

breakpoints instance-attribute #

checks instance-attribute #

method instance-attribute #

over instance-attribute #

Power(base, exponent) dataclass #

Bases: Expression

base ** exponent, both variable-free.

Degree 0 in variables wherever it appears, so no consumer has to ask what position it stands in: the language refuses a variable anywhere under it (math_spec.degree), which is what lets this fold to one number per coordinate like any other parameter arithmetic.

base instance-attribute #

exponent instance-attribute #

Program(*, parameters, variables, constraints, objective, dimensions=MappingProxyType({}), sos=MappingProxyType({}), piecewise=MappingProxyType({}), named_expressions=MappingProxyType({})) dataclass #

A complete declarative description of a mathematical program, with no data in it.

Every group of declarations is keyed by the name the file wrote, in the order it wrote them, and is read-only: the mappings are wrapped at construction, so a consumer cannot rewrite what another consumer reads. A whole program is not hashable — the declarations and expression nodes inside it are, which is what dedup and memoisation ask for.

constraints instance-attribute #

dimensions = MappingProxyType({}) class-attribute instance-attribute #

expressions property #

Every expression a row is built from — the objective and both sides of each constraint.

A :attr:named_expressions entry builds no row and is not among them.

footprint cached property #

Which constructs this program uses — walked once, then held.

lookups property #

Every lookup in the program, targeted and label-space alike, with the dimension it is over.

named_expressions = MappingProxyType({}) class-attribute instance-attribute #

objective instance-attribute #

parameters instance-attribute #

piecewise = MappingProxyType({}) class-attribute instance-attribute #

sos = MappingProxyType({}) class-attribute instance-attribute #

variables instance-attribute #

dimension(name) #

Source code in src/math_spec/program.py
def dimension(self, name: str) -> DimensionDeclaration:
    return _declared(self.dimensions, name, 'dimension')

parameter(name) #

Source code in src/math_spec/program.py
def parameter(self, name: str) -> ParameterDeclaration:
    return _declared(self.parameters, name, 'parameter')

variable(name) #

Source code in src/math_spec/program.py
def variable(self, name: str) -> VariableDeclaration:
    return _declared(self.variables, name, 'variable')

Region(when, value) dataclass #

One region of a :class:Cases: where it applies, and the value there.

when is stated on every region; the one the file wrote as otherwise: carries the negation of the others.

value instance-attribute #

when instance-attribute #

SosDeclaration(variable, over, sos_type, big_m=None) dataclass #

One special-ordered set per coordinate of the variable's foreach minus over.

The only declaration that adds neither a column nor a row: it names columns a consumer already has and says what may be nonzero among them. Which dims those are is the variable's own foreach and is read from it: a copy here would be a second home for a fact (:meth:Program.variable).

big_m caps the linking coefficient a consumer without the concept reformulates with, and is None where the variable's own upper bound is the only cap.

big_m = None class-attribute instance-attribute #

over instance-attribute #

sos_type instance-attribute #

variable instance-attribute #

Sum(operand, over) dataclass #

Bases: Expression

Sum operand over the named dims, removing them from the result.

operand instance-attribute #

over instance-attribute #

Translate(operand, dimension, offset, wrap, fill=None, partition=None) dataclass #

Bases: Expression

Re-index along one dimension: the result at t is operand at t - offset.

wrap is edge='wrap' in the file: periodic, and stated on every node. fill is what an acyclic shift leaves behind: None leaves the vacated positions absent, so the row drops; a number makes them present and contribute it. Always None under wrap.

offset is an integer, or the name of an integer parameter that does not depend on dimension and carries its sign in the values.

partition names a lookup over dimension, and the translation then happens inside each group it makes: the neighbour is the one before in the same group, the edge is the group's, and a wrap closes each group onto itself. A coordinate the lookup sends nowhere reaches nothing.

dimension instance-attribute #

fill = None class-attribute instance-attribute #

offset instance-attribute #

operand instance-attribute #

partition = None class-attribute instance-attribute #

wrap instance-attribute #

Variable(name) dataclass #

Bases: Expression

A variable reference — one term per existing variable row.

name instance-attribute #

VariableDeclaration(dims, where=None, lower=(lambda: Constant(float('-inf')))(), upper=(lambda: Constant(float('inf')))(), variable_type='continuous', absence='undefined') dataclass #

absence = 'undefined' class-attribute instance-attribute #

dims instance-attribute #

lower = field(default_factory=lambda: Constant(float('-inf'))) class-attribute instance-attribute #

upper = field(default_factory=lambda: Constant(float('inf'))) class-attribute instance-attribute #

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

where = None class-attribute instance-attribute #

VariableDefinedNode(name, dims) dataclass #

True at the coordinates where the named variable exists.

dims instance-attribute #

name instance-attribute #

Window(operand, dimension, width, wrap, partition=None) dataclass #

Bases: Expression

Sum operand over a trailing window along one dimension.

The result at t is the sum of the operand at every position from t - width + 1 through t, so a width of 1 is the operand itself. The dimension survives: this replicates terms onto the positions that can see them rather than reducing anything away.

width is a whole number, or the name of an integer parameter when the window differs per entity — a minimum up time, a rolling budget, a delivery horizon. A named width may not depend on the dimension being summed over.

wrap says whether the window reaches around the start of the axis instead of stopping short at it, and is stated on every node.

partition names a lookup over that dimension, and the window then stops at each group's edge. Positions are counted inside the group, so a coordinate the lookup places nowhere reaches nothing — not even itself.

dimension instance-attribute #

operand instance-attribute #

partition = None class-attribute instance-attribute #

width instance-attribute #

wrap instance-attribute #

carries_variable(expression) #

Whether a variable appears anywhere under expression.

Source code in src/math_spec/program.py
def carries_variable(expression: ExpressionNode) -> bool:
    """Whether a variable appears anywhere under *expression*."""
    return any(isinstance(node, Variable) for node in walk(expression))

check_message(block, pw, check) #

The sentence a consumer raises when the data bound to block fails check.

The language's own wording, so every consumer refuses in the same words; a consumer appends what it saw.

Source code in src/math_spec/program.py
def check_message(block: str, pw: PiecewiseDeclaration, check: Check) -> str:
    """The sentence a consumer raises when the data bound to *block* fails *check*.

    The language's own wording, so every consumer refuses in the same words;
    a consumer appends what it saw.
    """
    ctx = f"piecewise '{block}'"
    match check:
        case Increasing(parameter, over):
            return (
                f"{ctx}: method: {pw.method} requires strictly increasing breakpoints in '{parameter}' along '{over}'"
            )
        case Curved(x, y, over, curvature):
            shape = 'a single bend' if curvature == 'either' else f'a {curvature} curve'
            return (
                f"{ctx}: method: {pw.method} is exact only for {shape}, and '{y}' over '{x}' along "
                f"'{over}' is not one, so the answer is wrong rather than loose. Use method: adjacency "
                f'or sos2, which take a curve of any shape.'
            )
        case AtLeastTwo():
            return (
                f'{ctx}: method: lp needs at least two breakpoints per curve — the method *is* its segment '
                f'lines, so a curve with no segment states nothing and leaves the bounded link on its own '
                f'bound. Use method: adjacency, sos2 or convex, which pin it to the points it does have.'
            )
        case Contiguous(mask, values):
            return (
                f"{ctx}: points: '{values if values is not None else mask}' must mark a consecutive run of at "
                f'least one breakpoint per curve — the chord row joins a breakpoint to the one before it, and '
                f"the domain rows sit on the curve's own first and last."
            )
        case _:
            assert_never(check)

children(expression) #

The sub-expressions of expression — what every walk recurses through.

Source code in src/math_spec/program.py
def children(expression: ExpressionNode) -> tuple[ExpressionNode, ...]:
    """The sub-expressions of *expression* — what every walk recurses through."""
    if isinstance(expression, Negate):
        return (expression.operand,)
    if isinstance(expression, (Add, Multiply)):
        return (expression.left, expression.right)
    if isinstance(expression, Divide):
        return (expression.numerator, expression.divisor)
    if isinstance(expression, (Sum, GroupSum, At, Translate, Window)):
        return (expression.operand,)
    if isinstance(expression, Cases):
        return tuple(region.value for region in expression.regions)
    return ()

divisor_parameters(*expressions) #

Every parameter named anywhere in a divisor under expressions.

Source code in src/math_spec/program.py
def divisor_parameters(*expressions: ExpressionNode) -> frozenset[str]:
    """Every parameter named anywhere in a divisor under *expressions*."""
    return frozenset().union(*(parameters_of(q.divisor) for q in quotients(*expressions)))

fan_in(expression) #

How expression's output rows relate to its input slots.

For the absence rules, both classes other than 'one-to-one' sum several input slots into an output row.

Source code in src/math_spec/program.py
def fan_in(expression: ExpressionNode) -> FanIn:
    """How *expression*'s output rows relate to its input slots.

    For the absence rules, both classes other than ``'one-to-one'`` sum
    several input slots into an output row.
    """
    if isinstance(expression, (Sum, GroupSum)):
        return 'many-to-one'
    if isinstance(expression, Window):
        return 'one-to-many'
    if isinstance(
        expression,
        (Constant, Parameter, Variable, Negate, Add, Multiply, Power, Divide, At, Translate, Cases),
    ):
        return 'one-to-one'
    assert_never(expression)

is_quadratic(expression) #

Whether expression contains a product of two variable-carrying operands.

A structural question over the program, and unrelated consumers ask it — what a solver must support, which declarations to build last, whether this form can be represented at all — so it is answered once here beside the other walks rather than once per consumer in its own terms.

Whether a degree may be written is the language's verdict, and this is not a second opinion on it: by the time a program exists the question is which shape the expression has, and the program is what is in hand to answer it.

Source code in src/math_spec/program.py
def is_quadratic(expression: ExpressionNode) -> bool:
    """Whether *expression* contains a product of two variable-carrying operands.

    A structural question over the program, and unrelated consumers ask it —
    what a solver must support, which declarations to build last, whether this
    form can be represented at all — so it is answered once here beside the
    other walks rather than once per consumer in its own terms.

    Whether a degree *may be written* is the language's verdict, and this is
    not a second opinion on it: by the time a program exists the question is
    which shape the expression has, and the program is what is in hand to
    answer it.
    """
    return any(
        isinstance(node, Multiply) and all(carries_variable(side) for side in (node.left, node.right))
        for node in walk(expression)
    )

parameters_of(*expressions) #

Every parameter named anywhere under expressions.

Source code in src/math_spec/program.py
def parameters_of(*expressions: ExpressionNode) -> frozenset[str]:
    """Every parameter named anywhere under *expressions*."""
    return frozenset(node.name for node in walk(*expressions) if isinstance(node, Parameter))

quotients(*expressions) #

Every division under expressions, each kept whole.

The divisor and the numerator answer different questions and one consumer needs them paired: a divisor is judged against the rows the declaration builds narrowed by the variables in its own numerator, which the flat :func:divisor_parameters cannot say.

Source code in src/math_spec/program.py
def quotients(*expressions: ExpressionNode) -> tuple[Divide, ...]:
    """Every division under *expressions*, each kept whole.

    The divisor and the numerator answer different questions and one consumer
    needs them paired: a divisor is judged against the rows the declaration
    builds *narrowed by the variables in its own numerator*, which the flat
    :func:`divisor_parameters` cannot say.
    """
    return tuple(node for node in walk(*expressions) if isinstance(node, Divide))

variables_of(*expressions) #

Every variable named anywhere under expressions.

Source code in src/math_spec/program.py
def variables_of(*expressions: ExpressionNode) -> frozenset[str]:
    """Every variable named anywhere under *expressions*."""
    return frozenset(node.name for node in walk(*expressions) if isinstance(node, Variable))

walk(*expressions) #

Every node under expressions, each expression itself included, parents first.

The traversal every question about a program is a filter of — which names it mentions, whether a variable stands under it, which divisions it contains. One generator rather than that five-line recursion once per question: how a program is traversed is one fact, so a node kind :func:children learns to descend into reaches every caller at once rather than the callers that remembered.

Source code in src/math_spec/program.py
def walk(*expressions: ExpressionNode) -> Iterator[ExpressionNode]:
    """Every node under *expressions*, each expression itself included, parents first.

    The traversal every *question* about a program is a filter of — which names
    it mentions, whether a variable stands under it, which divisions it
    contains. One generator rather than that five-line recursion once per
    question: how a program is traversed is one fact, so a node kind
    :func:`children` learns to descend into reaches every caller at once
    rather than the callers that remembered.
    """
    for expression in expressions:
        yield expression
        yield from walk(*children(expression))