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
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.
AtLeastTwo(over, mask)
dataclass
#
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
#
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).
Contiguous(mask, values)
dataclass
#
Curved(x, y, over, curvature)
dataclass
#
DimensionComparisonNode(name, op, value)
dataclass
#
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.
Divide(numerator, divisor)
dataclass
#
Bases: Expression
Quotient numerator / divisor. The divisor must be variable-free.
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
#
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:
|
variable_types |
Every domain declared.
TYPE:
|
sos_types |
The order of each special-ordered set declared. |
shapes |
Every expression node kind that appears.
TYPE:
|
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.
Increasing(parameter, over)
dataclass
#
LastOf(block, mask)
dataclass
#
LookupComparisonNode(name, over, op, value)
dataclass
#
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.
LookupDefinedNode(name, over)
dataclass
#
LookupPairComparisonNode(name, other, over, op)
dataclass
#
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:
|
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
#
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.
Negate(operand)
dataclass
#
Bases: Expression
operand
instance-attribute
#
ObjectiveDeclaration(sense, expression)
dataclass
#
Parameter(name)
dataclass
#
Bases: Expression
A parameter reference — contributes to the constant part.
name
instance-attribute
#
ParameterComparisonNode(name, op, value, dims)
dataclass
#
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.
ParameterDefinedNode(name, dims)
dataclass
#
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:
|
method |
How the weights are restricted.
TYPE:
|
breakpoints |
The links' values parameters, in link order. |
checks |
What the block assumes of the numbers, each carrying its own subjects, for the consumer holding them to check. |
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.
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
#
separability
cached
property
#
Every axis, to what building it a window at a time asks and what it would break.
The locality :doc:the ceiling </about/ceiling> argues in — pointwise,
bounded halo, global — asked about the axes rather than about the
operators, so a driver may know before it cuts a horizon whether every
row it builds is complete inside some window.
A reduction means opposite things by position, which is the whole of the care: in a constraint a sum over the axis ties every window to every other, and in the objective it is additively separable, an objective being a sum already.
Every declared dimension has an entry, an axis nothing mentions being
trivially windowable. Walked once and held, like :attr:footprint and
for the same reason — a program cannot change after construction — and
answering for every axis costs what answering for one did, every
construct that ties an axis naming the axis it ties (#248).
sos = MappingProxyType({})
class-attribute
instance-attribute
#
variables
instance-attribute
#
dimension(name)
#
parameter(name)
#
Reach(label, name, kind)
dataclass
#
One read along an axis whose distance only data can say.
| ATTRIBUTE | DESCRIPTION |
|---|---|
label |
The declaration reading, as the lowering's messages label it.
TYPE:
|
name |
The parameter or lookup that says how far.
TYPE:
|
kind |
An
TYPE:
|
Region(when, value)
dataclass
#
Separability(dimension, ahead, coupled, undecided, restarts)
dataclass
#
What building one dimension a window at a time asks of a driver, and what it would break.
A rolling-horizon or myopic driver cuts an axis into windows and builds each on its own. What the program can say is whether every row it builds is then complete inside some window: how far a row reads ahead along the axis, and which declarations tie the axis together so that no window holds them. It cannot say whether the windowed answer is the one a whole-horizon solve would give — a store carried over one row windows cleanly, and a rolling solve of it is still a different answer — which is the driver's design and not the model's.
What a row reads behind is not reported. A window starts where the driver puts it, and what its first rows meet there is the edge policy: the opening state a rolling horizon seeds, and the driver's to carry.
| ATTRIBUTE | DESCRIPTION |
|---|---|
dimension |
The axis asked about.
TYPE:
|
ahead |
Coordinates a window must see after its last row for every row
it builds to be complete — what a negative
TYPE:
|
coupled |
Each declaration that ties the axis together, to what ties it and the one modelling change that would not: a sum over the axis in a constraint, a grouping that consumes it, a wrapped translation, a set. No window satisfies these, and no rewrite here would keep the model's meaning, so the remedy is named rather than applied. |
undecided |
Each read along the axis whose reach only data can say —
a named offset, a partition whose groups a window may cut, a read
through a lookup at a coordinate the data chooses.
:meth: |
restarts |
Each declaration counting a position along the axis, which a window restarts at its first row. Whether that is wanted — a seed once per window, or once per horizon — is the modeller's, so it is reported rather than refused. |
ahead
instance-attribute
#
coupled
instance-attribute
#
dimension
instance-attribute
#
restarts
instance-attribute
#
undecided
instance-attribute
#
windowable
property
#
Whether every row builds complete inside a window looking :attr:ahead past its last row.
False while a reach is :attr:undecided, which a driver holding
the data may resolve; :attr:restarts do not count against it.
resolved(least)
#
The same verdict with each named offset folded into :attr:ahead.
A driver holding the data reads the least value of each parameter an
:attr:undecided reach names and hands it here, so the rule that
turns a value into a reach — a negative offset reads ahead by that
much, a positive one reads behind and asks nothing — has one home.
| PARAMETER | DESCRIPTION |
|---|---|
least
|
Parameter name to the least of its values. A reach through a lookup — a partition, a coordinate — cannot be folded this way and stays undecided, as does a parameter left out. |
| RAISES | DESCRIPTION |
|---|---|
KeyError
|
A name no undecided reach along this axis waits on. |
Source code in src/math_spec/program.py
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.
Sum(operand, over)
dataclass
#
Bases: Expression
Sum operand over the named dims, removing them from the result.
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.
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
#
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.
carries_variable(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
children(expression)
#
The sub-expressions of expression — what every walk recurses through.
Source code in src/math_spec/program.py
divisor_parameters(*expressions)
#
Every parameter named anywhere in a divisor under 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
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
parameters_of(*expressions)
#
Every parameter named anywhere under expressions.
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
variables_of(*expressions)
#
Every variable named anywhere under expressions.
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.