Skip to content

Program API#

This page documents every name that mathspec.program exports: the declarations, the expression and predicate nodes, and the reports a program answers. Reading a spec and its program says how they fit together.

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

The second public state, and the one a consumer reads. A Program is the file typed, section for section: every declaration it makes, with names resolved, shapes fixed and every rule decidable without data checked, and no data at all. Lowering, as a Spec loads, 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 is children; the questions over the walk are walk_regions, walk and the filters beside them. A resolved where arrives as a Mask. Frozen dataclasses only — no execution logic, and nothing imported from a consumer. How a consumer reads one: docs/reference/reading.md.

Connective = Not | And | Or module-attribute #

ConstraintSense = ComparisonOperator module-attribute #

DeclaredDtype = ParameterDtype | DimensionDtype module-attribute #

DimensionDtype = Literal['float', 'int', 'str', 'datetime'] module-attribute #

Expression = Constant | Parameter | Variable | Dual | Negate | Add | Multiply | Power | Divide | Sum | GroupSum | Pullback | Translate | WindowSum | Cases | Named module-attribute #

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

ParameterDtype = Literal['float', 'int', 'bool', 'str'] module-attribute #

PiecewiseMethod = Literal['adjacency', 'sos2', 'convex', 'lp'] module-attribute #

Predicate = BooleanLiteral | TypedPredicate | Connective module-attribute #

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

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

SosType = Literal[1, 2] module-attribute #

TypedPredicate = ParameterComparison | ExpressionComparison | ParameterDefined | VariableDefined | DimensionComparison | DimensionPosition | RelationComparison | RelationPairComparison | RelationDefined | CountComparison | TranslatedPredicate | PulledBackPredicate module-attribute #

VariableAbsence = Literal['undefined', 'zero'] module-attribute #

VariableDomain = Literal['continuous', 'integer', 'binary'] module-attribute #

Add(left, right) dataclass #

left instance-attribute #

right instance-attribute #

And(left, right) dataclass #

left instance-attribute #

right instance-attribute #

Assumption(predicate, where=None, description=None) dataclass #

A predicate the file states of its data, under the name it wrote in assumptions:.

predicate is true at every coordinate of its frame — the product of every dim the two masks name — that where admits, a missing row reading as false as it does in any mask. Nothing here is decidable at load: both sides are the data's, which is why the consumer attaching it checks.

description = None class-attribute instance-attribute #

predicate instance-attribute #

where = None class-attribute instance-attribute #

BooleanLiteral(value) dataclass #

value instance-attribute #

Cases(regions) dataclass #

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 #

A scalar constant.

value instance-attribute #

ConstraintDeclaration(dims, lhs, sense, rhs, where=None, description=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).

description = None class-attribute instance-attribute #

dims instance-attribute #

lhs instance-attribute #

rhs instance-attribute #

sense instance-attribute #

where = None class-attribute instance-attribute #

CountComparison(predicate, over, op, value, dims) dataclass #

How many coordinates predicate admits along over, against a literal — count(points, over=bp) >= 2.

The count is one number per coordinate of dims, which is every dim predicate reads minus over, so a claim about each curve is written without saying "each curve". A predicate a leaf reads arrives as a Mask, where a connective's operand is a bare Predicate: a walk recurses through the second and stops at the first.

dims instance-attribute #

op instance-attribute #

over instance-attribute #

predicate instance-attribute #

value instance-attribute #

DimensionComparison(name, op, value) dataclass #

Compare a dimension's own coordinates against a literal.

name instance-attribute #

op instance-attribute #

value instance-attribute #

DimensionDeclaration(dtype='str', description=None) dataclass #

A dimension, as the file declares it.

description = None class-attribute instance-attribute #

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

DimensionPosition(name, op, position, partition=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 a partition the position is counted within each group the relation makes (Partition), whose joined columns' dimensions the frame carries.

name instance-attribute #

op instance-attribute #

partition = None class-attribute instance-attribute #

position instance-attribute #

Direction(name, relation, consumed, produced, joined) dataclass #

One relation as one call reads it — which columns are consumed, which produced, which joined on.

The declaration fixes no direction; the call does, and this is the one it named. name is the relation's, as Program.relations keys it. consumed, produced and joined are roles — column names of relation, which maps every role to its dimension and names the key. joined is the key roles the call did not name (every role, for a bare relation): the join keys on them, and a value role left unnamed is not read.

consumed instance-attribute #

consumed_dims property #

joined instance-attribute #

joined_dims property #

name instance-attribute #

produced instance-attribute #

produced_dims property #

relation instance-attribute #

dim(role) #

The dimension role ranges over.

Source code in src/mathspec/program.py
def dim(self, role: str) -> str:
    """The dimension *role* ranges over."""
    return self.relation.dim(role)

Divide(numerator, divisor) dataclass #

Quotient numerator / divisor, the divisor variable-free wherever the math reads it (mathspec.degree).

divisor instance-attribute #

numerator instance-attribute #

Dual(constraint) dataclass #

A constraint's dual — its shadow price, read after the solve.

Stands only under an ExpressionDeclaration the math never reads: the loader refuses dual() anywhere a solver ingests. One value per coordinate of the named constraint's own dims frame: the leaf reshapes nothing, like a parameter.

constraint instance-attribute #

ExpressionComparison(left, op, right, dims) dataclass #

Compare two variable-free expressions, coordinate by coordinate — p_min <= 0.5 * p_max.

dims is every dim either side carries. A side whose value is absent at a coordinate — a parameter row missing, a translation that vacated it — makes the comparison false there, as a null does in every other comparison; under a summing operator the absent term is one fewer.

dims instance-attribute #

left instance-attribute #

op instance-attribute #

right instance-attribute #

ExpressionDeclaration(expression, dims, in_math, description=None) dataclass #

A named quantity — one the math reads, or one only read back after a solve.

in_math where the objective or a constraint reads it, directly or through another entry or a macro; its body then stands inside Program.roots and is held to the degree rules where it is read. Otherwise nothing a solver sees contains it: it is a reported quantity, its body held to no degree, the one place a Dual may stand. A bound and a where name no entry, so neither decides this.

description = None class-attribute instance-attribute #

dims instance-attribute #

expression instance-attribute #

in_math instance-attribute #

Footprint(quadratic, domains, sos_types, kinds) 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]

domains

Every domain declared.

TYPE: frozenset[VariableDomain]

sos_types

The order of each special-ordered set declared.

TYPE: frozenset[SosType]

kinds

Every expression node kind that appears.

TYPE: frozenset[type[Expression]]

domains instance-attribute #

kinds instance-attribute #

quadratic instance-attribute #

sos_types instance-attribute #

GroupSum(operand, direction) dataclass #

Sum operand through a relation: the dims direction consumes go, the dims it produces arrive, the dims it joins on stay.

The join keys on the consumed columns and every joined column, and the operand carries every dim consumed or joined on.

direction instance-attribute #

operand instance-attribute #

One link of a piecewise: block: an expression tied to the breakpoints a values parameter holds.

sign is '==' where the link is pinned to the curve, and one side of it where the link is bounded by the curve instead.

expression instance-attribute #

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

values 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: Predicate

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, relations and variables the mask names.

root instance-attribute #

Multiply(left, right) dataclass #

Product of two operands.

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

left instance-attribute #

right instance-attribute #

Named(name, body) dataclass #

A use of an expressions: entry, standing where its name was written, with the entry's body under it.

Its value is its body's: a consumer building rows steps through it, as children does. It is kept as a node rather than written in so the typesetter can print the symbol where the name stood and define it once. Every use of one entry holds the one node resolution built for it, whose body is the ExpressionDeclaration.expression of that entry.

body instance-attribute #

name instance-attribute #

Negate(operand) dataclass #

operand instance-attribute #

Not(operand) dataclass #

operand instance-attribute #

ObjectiveDeclaration(sense, expression, description=None) dataclass #

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

description = None class-attribute instance-attribute #

expression instance-attribute #

sense instance-attribute #

Or(left, right) dataclass #

left instance-attribute #

right instance-attribute #

Parameter(name) dataclass #

A parameter reference — contributes to the constant part.

name instance-attribute #

ParameterComparison(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', description=None) dataclass #

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

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

description = None class-attribute instance-attribute #

dims instance-attribute #

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

ParameterDefined(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 #

Partition(name, relation, along, group, joined) dataclass #

One relation as a partition steps along it — the key column stepped along, the group columns, and the key columns joined on.

name is the relation's, as Program.relations keys it. along, group and joined are roles — column names of relation, which maps every role to its dimension and names the key. along is the one key column over the dimension stepped along, and the frame keeps it. group is the value columns within= named, read at the row's key. joined is the other key columns, whose dimensions the frame carries. Nothing is consumed and nothing is produced: the frame does not change.

along instance-attribute #

along_dim property #

group instance-attribute #

joined instance-attribute #

joined_dims property #

name instance-attribute #

relation instance-attribute #

dim(role) #

The dimension role ranges over.

Source code in src/mathspec/program.py
def dim(self, role: str) -> str:
    """The dimension *role* ranges over."""
    return self.relation.dim(role)

PiecewiseDeclaration(over, links, method, frame, activity=None, points=None, description=None) dataclass #

A piecewise: block as the curve it states, which expand writes out as rows.

A program of a spec that still declares one carries it here, typed; a program of the expanded spec carries the rows instead, under Program.variables and Program.constraints, and what the method assumes of the breakpoints under Program.assumptions. A consumer building rows takes the expanded spec.

ATTRIBUTE DESCRIPTION
over

The breakpoint dimension.

TYPE: str

links

The links, in the order the file wrote them.

TYPE: tuple[Link, ...]

method

How the weights are restricted.

TYPE: PiecewiseMethod

activity

The binary the weights sum to, or None where they sum to 1.

TYPE: str | None

points

The parameter saying how far each curve runs, or None.

TYPE: str | None

frame

The dimensions the block builds one curve per coordinate of, in declaration order.

TYPE: tuple[str, ...]

description

What the file wrote under description:, or None.

TYPE: str | None

activity = None class-attribute instance-attribute #

curve property #

The two links as (x, y), the bounded one last. Two-link blocks only.

description = None class-attribute instance-attribute #

frame instance-attribute #

method 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 #

Power(base, exponent) dataclass #

base ** exponent, both variable-free wherever the math reads it.

The language refuses a variable anywhere under it (mathspec.degree), so in the program a solver sees it is degree 0 and folds to one number per coordinate like any other parameter arithmetic.

base instance-attribute #

exponent instance-attribute #

Program(*, parameters, variables, constraints, objective, dimensions=Sealed({}), relations=Sealed({}), sos=Sealed({}), piecewise=Sealed({}), assumptions=Sealed({}), expressions=Sealed({}), description=None) 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.

assumptions = Sealed({}) class-attribute instance-attribute #

constraints instance-attribute #

description = None class-attribute instance-attribute #

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

expressions = Sealed({}) class-attribute instance-attribute #

footprint cached property #

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

It answers for the rows this program holds. A curve still under piecewise is not counted, so a sos2 curve adds no set order here; ask the program of spec.expand('piecewise') for its rows.

objective instance-attribute #

parameters instance-attribute #

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

relations = Sealed({}) class-attribute instance-attribute #

roots property #

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

An expressions entry builds no row and is not among them. Nor is a curve still under piecewise: it is not a row until expand writes it out, and its rows are in the program of the expansion.

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

It answers for the rows this program holds, as footprint does. A curve still under piecewise ties nothing here, although its rows sum over its breakpoint dimension; ask the program of spec.expand('piecewise').

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

variables instance-attribute #

relations_of(dimension) #

The relations with a column over dimension, by name.

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

Pullback(operand, direction) dataclass #

Read operand through a relation — the adjoint of GroupSum.

The dims direction consumes go and the dims it produces arrive, one value per coordinate because the read takes value columns at a key the result fixes, which the loader checks. The join fans out, many produced tuples sharing one consumed tuple — at each coordinate of the joined columns, which the operand carries and the result keeps.

direction instance-attribute #

operand instance-attribute #

PulledBackPredicate(operand, direction, dims) dataclass #

operand read through a relation — at(has_curve, by=converter_of, over=converter, into=flow).

True at a coordinate where the relation has a row and operand holds at the coordinate that row reads. False where the relation has no row, which is what a missing row already means in a mask. The dims direction consumes go and the dims it produces arrive, as Pullback's do.

dims instance-attribute #

direction instance-attribute #

operand instance-attribute #

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: str

name

The parameter or relation that says how far.

TYPE: str

kind

An offset is a parameter's values, which Separability.resolved folds in; a partition and a coordinate are a relation's groups, which it does not.

TYPE: Literal['offset', 'partition', 'coordinate']

kind instance-attribute #

label instance-attribute #

name instance-attribute #

Region(when, value) dataclass #

One region of a 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 #

RelationComparison(name, column, op, value, dims=()) dataclass #

Compare one value column of a keyed relation against a literal — period_of == 2030.

column is the role read, and dims the dimensions of the key columns: the leaf is read at them, one value per coordinate.

column instance-attribute #

dims = () class-attribute instance-attribute #

name instance-attribute #

op instance-attribute #

value instance-attribute #

RelationDeclaration(columns, key, description=None) dataclass #

One declared relation: a table over its columns, single-valued per key.

columns maps each role to its dimension in the order the table carries them, the key's roles first; key is the roles a row is identified by, and values the rest — every role is a key role for a bare relation, which is one with no value columns. Every value is checked when the data is attached to be a label of its column's dimension, and the table to have one row per key tuple — which keeps a mistyped label from silently dropping its terms in the join that places them, and is what lets at read one value.

columns instance-attribute #

description = None class-attribute instance-attribute #

dims property #

key instance-attribute #

roles property #

values property #

The roles the key determines.

dim(role) #

Source code in src/mathspec/program.py
def dim(self, role: str) -> str:
    return self._dim_of[role]

RelationDefined(name, dims=()) dataclass #

True where the relation has a row at the frame's coordinates.

dims is what the frame supplies: the key's dimensions, whose row is then the one the key finds — every column's for a bare relation, where a row is the whole tuple.

dims = () class-attribute instance-attribute #

name instance-attribute #

RelationPairComparison(name, column, other, other_column, op, dims=()) dataclass #

Compare a value column of one keyed relation with one of another — from_bus != to_bus — row by row on the key.

Both keys are over the same dims, and the two columns are over one dimension, so a match is possible at all.

column instance-attribute #

dims = () class-attribute instance-attribute #

name instance-attribute #

op instance-attribute #

other instance-attribute #

other_column instance-attribute #

Separability(dimension, ahead, coupled, undecided, restarts, linking_rows, linking_columns) 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, and a decomposition cuts the same axis and solves each piece on its own. What the program can say is whether every row it builds is then complete inside one window: how far a row reads ahead along the axis, which declarations tie the axis together so that no window holds them, and — the same fact read as a set — which rows and columns are left over as the border every window shares. 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 spec'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: str

ahead

Coordinates a window must see after its last row for every row it builds to be complete — what a negative shift reads. 0 is pointwise; a shift of -2 is 2.

TYPE: int

coupled

Each declaration that ties the axis together, to what ties it and the one change to the spec 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 spec's meaning, so the remedy is named rather than applied.

TYPE: Mapping[str, str]

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 relation at a coordinate the data chooses. resolved folds a parameter's values in.

TYPE: tuple[Reach, ...]

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 for the spec's author to decide, so it is reported rather than refused.

TYPE: Mapping[str, str]

linking_rows

Each constraint no one window holds whole, in declaration order: one the axis does not index, whose row stands in every window, and one coupled names. A reach the data decides is not one, so a row waiting on undecided may span two windows.

TYPE: tuple[str, ...]

linking_columns

Each variable the axis does not index, in declaration order, whose column every window reads. A decomposition calls a window a block, and with linking_rows this is the border of a bordered block-diagonal form cut along the axis, whole where nothing is undecided and no set runs through it. A set couples the axis without building a row, so it stands in neither field. The form is exactly that where ahead is 0: a positive lookahead is neighbouring blocks overlapping by that much.

TYPE: tuple[str, ...]

ahead instance-attribute #

coupled instance-attribute #

dimension instance-attribute #

linking_columns instance-attribute #

linking_rows instance-attribute #

restarts instance-attribute #

undecided instance-attribute #

windowable property #

Whether every row builds complete inside a window looking ahead past its last row.

False while a reach is undecided, which a driver holding the data may resolve; restarts do not count against it.

resolved(least) #

The same verdict with each named offset folded into ahead.

A driver holding the data reads the least value of each parameter an 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 relation — a partition, a coordinate — cannot be folded this way and stays undecided, as does a parameter left out.

TYPE: Mapping[str, int]

RAISES DESCRIPTION
KeyError

A name no undecided reach along this axis waits on.

Source code in src/mathspec/program.py
def resolved(self, least: Mapping[str, int]) -> Separability:
    """The same verdict with each named offset folded into [`ahead`][].

    A driver holding the data reads the least value of each parameter an
    [`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.

    Args:
        least: Parameter name to the least of its values. A reach through
            a relation — a partition, a coordinate — cannot be folded this
            way and stays undecided, as does a parameter left out.

    Raises:
        KeyError: A name no undecided reach along this axis waits on.
    """
    waiting = {reach.name for reach in self.undecided if reach.kind == 'offset'}
    for name in least:
        if name not in waiting:
            raise KeyError(
                f"'{name}' is not a parameter an undecided reach along '{self.dimension}' waits on. "
                + did_you_mean(name, sorted(waiting))
            )
    folded = {reach for reach in self.undecided if reach.kind == 'offset' and reach.name in least}
    ahead = max([self.ahead, *(-least[reach.name] for reach in folded)])
    return replace(self, ahead=ahead, undecided=tuple(r for r in self.undecided if r not in folded))

SosDeclaration(variable, along, sos_type, description=None) dataclass #

One special-ordered set per coordinate of the variable's dims minus along.

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 dims and is read from it: a copy here would be a second home for a fact (Program.variables).

along instance-attribute #

description = None class-attribute instance-attribute #

sos_type instance-attribute #

variable instance-attribute #

Sum(operand, over) dataclass #

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

operand instance-attribute #

over instance-attribute #

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

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 along and carries its sign in the values.

partition is a relation with a key column over along (Partition), and the translation then happens inside each group its within= columns make: 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 relation sends nowhere reaches nothing.

along instance-attribute #

fill = None class-attribute instance-attribute #

offset instance-attribute #

operand instance-attribute #

partition = None class-attribute instance-attribute #

wrap instance-attribute #

TranslatedPredicate(operand, along, offset, dims) dataclass #

operand read at a neighbouring coordinate — shift(points, along=bp, offset=1).

False where the translation vacates, and there is no edge= to state. The arithmetic translation needs one because no number is neutral and inventing one changes the answer; false is what a missing row already means in a mask, so the predicate form has the value the language already gives it.

along instance-attribute #

dims instance-attribute #

offset instance-attribute #

operand instance-attribute #

Variable(name) dataclass #

A variable reference — one term per existing variable row.

name instance-attribute #

VariableDeclaration(dims, where=None, lower=None, upper=None, domain='continuous', absence='undefined', description=None) dataclass #

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

description = None class-attribute instance-attribute #

dims instance-attribute #

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

lower = None class-attribute instance-attribute #

upper = None class-attribute instance-attribute #

where = None class-attribute instance-attribute #

VariableDefined(name, dims) dataclass #

True at the coordinates where the named variable exists.

dims instance-attribute #

name instance-attribute #

WindowSum(operand, along, width, wrap, partition=None) dataclass #

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 relation over that dimension, and the window then stops at each group's edge. Positions are counted inside the group, so a coordinate the relation places nowhere reaches nothing — not even itself.

along instance-attribute #

operand instance-attribute #

partition = None class-attribute instance-attribute #

width instance-attribute #

wrap instance-attribute #

assumption_message(name, assumption) #

The sentence a consumer raises when the data attached to assumption, called name, fails it.

The language's own wording, so every consumer refuses in the same words; a consumer appends the coordinates it saw. Where the file wrote a description:, or a piecewise: method implied one, it trails the sentence: the names say which columns are wrong, and the description says why the rule is there.

Source code in src/mathspec/program.py
def assumption_message(name: str, assumption: Assumption) -> str:
    """The sentence a consumer raises when the data attached to *assumption*, called *name*, fails it.

    The language's own wording, so every consumer refuses in the same words;
    a consumer appends the coordinates it saw. Where the file wrote a
    ``description:``, or a ``piecewise:`` method implied one, it trails the
    sentence: the names say which columns are wrong, and the description says
    why the rule is there.
    """
    read = ', '.join(f"'{n}'" for n in sorted(assumption.predicate.names_read))
    sentence = f"assumption '{name}' does not hold for the data attached to {read}"
    return f'{sentence} — {assumption.description}' if assumption.description else sentence

carries_variable(expression) #

Whether a variable appears anywhere under expression.

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

children(expression) #

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

Source code in src/mathspec/program.py
def children(expression: Expression) -> tuple[Expression, ...]:
    """The sub-expressions of *expression* — what every walk recurses through."""
    if isinstance(expression, Named):
        return (expression.body,)
    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, Power):
        return (expression.base, expression.exponent)
    if isinstance(expression, (Sum, GroupSum, Pullback, Translate, WindowSum)):
        return (expression.operand,)
    if isinstance(expression, Cases):
        return tuple(region.value for region in expression.regions)
    if isinstance(expression, (Constant, Parameter, Variable, Dual)):
        return ()
    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/mathspec/program.py
def is_quadratic(expression: Expression) -> 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/mathspec/program.py
def parameters_of(*expressions: Expression) -> frozenset[str]:
    """Every parameter named anywhere under *expressions*."""
    return frozenset(node.name for node in walk(*expressions) if isinstance(node, Parameter))

variables_of(*expressions) #

Every variable named anywhere under expressions.

Source code in src/mathspec/program.py
def variables_of(*expressions: Expression) -> 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.

walk_regions with the regions dropped, for the questions that do not ask where a node stands.

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

    [`walk_regions`][] with the regions dropped, for the questions that do
    not ask where a node stands.
    """
    return (node for node, _ in walk_regions(*expressions))

walk_regions(*expressions) #

Every node under expressions, each with the regions it stands inside, outermost 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, which rows a piece owes data at. One generator rather than that five-line recursion once per question: how a program is traversed is one fact, so a node kind children learns to descend into reaches every caller at once rather than the callers that remembered.

The regions are the when of every Cases region the node's value stands under, the outermost first, which is the order the masks conjoin in. A node outside any cases: block carries the empty tuple, and a Cases node carries only the regions above it, not its own. The tuple rather than one conjoined mask: what a consumer does with the regions is its own, and the conjunction is one & away.

Source code in src/mathspec/program.py
def walk_regions(*expressions: Expression) -> Iterator[tuple[Expression, tuple[Mask, ...]]]:
    """Every node under *expressions*, each with the regions it stands inside, outermost 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, which rows a piece owes data at. One generator rather than that
    five-line recursion once per question: how a program is traversed is one
    fact, so a node kind [`children`][] learns to descend into reaches every
    caller at once rather than the callers that remembered.

    The regions are the ``when`` of every [`Cases`][] region the node's
    value stands under, the outermost first, which is the order the masks
    conjoin in. A node outside any ``cases:`` block carries the empty tuple,
    and a ``Cases`` node carries only the regions above it, not its own. The
    tuple rather than one conjoined mask: what a consumer does with the
    regions is its own, and the conjunction is one ``&`` away.
    """
    yield from _walk_regions(expressions, ())

where_children(where) #

The predicates under where — a connective's operands, and nothing under a leaf.

What every walk over a predicate recurses through, as children is for an expression. A leaf has nothing under it whether or not it is resolved, so the grammar measures its own output with this too.

Source code in src/mathspec/program.py
def where_children(where: Predicate) -> tuple[Predicate, ...]:
    """The predicates under *where* — a connective's operands, and nothing under a leaf.

    What every walk over a predicate recurses through, as [`children`][] is
    for an expression. A leaf has nothing under it whether or not it is
    resolved, so the grammar measures its own output with this too.
    """
    if isinstance(where, Not):
        return (where.operand,)
    if isinstance(where, (And, Or)):
        return (where.left, where.right)
    return ()