Skip to content

Program API#

mathspec.program contains what a file means. It has the typed entries, the expression and condition nodes, and the reports that a program has. It also has each warning that advice returns (an Advice). This page lists every name that the module exports, in alphabetical order.

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 entry 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 entry 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, and what advice says about a program as Advice. Frozen dataclasses only — no execution logic, and nothing imported from a consumer. How a consumer reads one: docs/reference/reading.md.

AdviceKind = Literal['never-an-axis', 'given', 'unbounded'] module-attribute #

The checks that an Advice comes from. The set is closed, so a tool that filters on it can list every value.

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

Condition = BooleanLiteral | TypedCondition | Connective | ConditionReference module-attribute #

The union of every condition node in a program. The parser's Unresolved* nodes are not part of it, because mathspec replaces them at load.

Connective = Not | And | Or module-attribute #

The boolean connectives. With ConditionReference, they are the only condition nodes that contain other condition nodes, so a walk over a condition recurses only into these.

ConstraintSense = Sense module-attribute #

DeclaredDtype = ParameterDtype | DimensionDtype module-attribute #

The dtype of a name that a comparison in a condition tests. A parameter and a dimension declare it, and a relation column has the dtype of its dimension.

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

The dtypes a dimension may declare, which set what its labels are. Only a dimension may declare datetime.

Expression = Constant | ParameterReference | VariableReference | Dual | Negate | Add | Multiply | Power | Divide | Sum | Join | Translate | WindowSum | Cases | ExpressionReference module-attribute #

The union of every expression node, which a walk over an expression takes. The set is closed, so a walk can end in assert_never. Every node has dims, the dimensions it is indexed over. Lowering sorts them by name, and a node built by hand has the dims it was given. An ExpressionReference has the dims of its body. Program.roots, a bound and a named expression that is in_math are affine, except that at a QuadraticPosition a Multiply may have a variable on both sides.

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

The directions an objective may declare.

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

The dtypes a parameter may declare, which set the dtype of its data column. Only a parameter may declare bool.

ParameterMissing = ParameterReading | MissingBy module-attribute #

The values of missing: on a parameter: one reading for every missing row, or a MissingBy.

ParameterReading = RelationMissing | bool | float module-attribute #

What a missing row of a parameter reads as: a value of RelationMissing, or the value that the row takes.

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

The methods a piecewise: entry may declare to restrict its interpolation weights. PIECEWISE_METHODS lists what each method adds.

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

Where the math may contain a product of two variables: the objective or a constraint. A bound and a piecewise: link are affine.

RelationMissing = Literal['refused', 'absent'] module-attribute #

The values of missing: on a relation. Under refused, a tool refuses data that leaves out a combination of the key. Under absent, that combination has no row.

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

The types of special ordered set: 1 or 2.

TypedCondition = ParameterComparison | ExpressionComparison | ParameterDefined | VariableDefined | ConstraintDefined | DimensionComparison | DimensionPosition | RelationComparison | RelationPairComparison | RelationDefined | CountComparison | TranslatedCondition | JoinedCondition module-attribute #

The condition nodes that refer to an entry whose kind mathspec knows at load.

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

The domains a variable may declare.

VariableMissing = Literal['absent'] | float module-attribute #

The values of missing: on a variable. Under absent, the default, a term that contains the variable has no row where its where is false. A number is the value of the variable there, so the term takes that number and keeps its row.

Add(left, right, *, dims) dataclass #

dims = field(kw_only=True) class-attribute instance-attribute #

left instance-attribute #

right instance-attribute #

Advice(kind, subject, text) dataclass #

One thing the language advises about a file it accepts.

Never an error: each is what a half-written spec looks like too. A consumer prints it, or filters on kind and subject; the text is the language's, so no consumer writes its own.

ATTRIBUTE DESCRIPTION
kind

The pass that said it.

TYPE: AdviceKind

subject

The entry it is about — a dimension name, a variable name.

TYPE: str

text

The sentence, naming the rewrite.

TYPE: str

kind instance-attribute #

subject instance-attribute #

text instance-attribute #

And(left, right) dataclass #

left instance-attribute #

right instance-attribute #

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

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

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

The description: that the file sets, or the sentence that a piecewise: method implies. A tool quotes it in a refusal, after the names of the columns, to say why the rule exists.

holds instance-attribute #

where = None class-attribute instance-attribute #

Axis(dimension, column=None) dataclass #

A position in a frame: the dimension whose labels it runs over, and the relation column it stands for.

A declared dimension is the axis Axis(dimension). A Join opens Axis(dimension, column) for each column it drops, so a column dropped and a column added over one dimension are two axes of one frame. Two axes are equal where both fields are, so a frame holds a dimension's own axis at most once however many join axes run over the same labels.

column = None class-attribute instance-attribute #

dimension instance-attribute #

BooleanLiteral(value) dataclass #

value instance-attribute #

Cases(regions, *, dims) 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.

dims = field(kw_only=True) class-attribute instance-attribute #

regions instance-attribute #

Column(relation, name) dataclass #

One column of a relation: the relation's name, and the column's name in its entry.

name instance-attribute #

relation instance-attribute #

ConditionReference(name, body) dataclass #

A use of a conditions: entry, standing where its name was written, with the entry's condition under it.

Its truth is its body's: a consumer steps through it, as condition_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, as ExpressionReference does for an expression.

body instance-attribute #

name instance-attribute #

Constant(value, *, dims) dataclass #

A scalar constant.

dims = field(kw_only=True) class-attribute instance-attribute #

value instance-attribute #

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

ConstraintDefined(name, dims) dataclass #

True at the coordinates where the named constraint has a row. Only an assumption reads one.

A row exists where the constraint's where holds and every term outside a sum is present, which the data alone decides, before any solve.

dims instance-attribute #

name instance-attribute #

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

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

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

condition instance-attribute #

dims instance-attribute #

op instance-attribute #

over instance-attribute #

value instance-attribute #

Dimension(dtype='str', one_of=None, ordered=False, description=None) dataclass #

A dimension, as the file declares it.

description = None class-attribute instance-attribute #

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

The dtype of the labels, as the file declares it. A tool checks the column of labels against it, as it checks a value column against Parameter.dtype. It is str when the file lists the labels under one_of.

one_of = None class-attribute instance-attribute #

The labels that the file lists under one_of:, sorted by name, or None when it lists none. With data attached, a tool refuses any other label, in the table of the dimension and in every relation column over it.

ordered = False class-attribute instance-attribute #

Whether the order of the labels is part of the spec. When it is, a tool keeps the labels in the order the data gives them.

DimensionComparison(name, op, value) dataclass #

Compare a dimension's own coordinates against a literal.

name instance-attribute #

op instance-attribute #

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

Divide(numerator, divisor, *, dims) dataclass #

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

dims = field(kw_only=True) class-attribute instance-attribute #

divisor instance-attribute #

numerator instance-attribute #

Dual(constraint, *, dims) dataclass #

A constraint's dual — the rate the optimal objective rises with the constraint's right side, read after the solve.

Of lhs <= rhs it is the rate in d of lhs <= rhs + d, for every comparator and under either sense. Stands only under an NamedExpression 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 #

dims = field(kw_only=True) class-attribute 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 #

ExpressionReference(name, body, *, dims) 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 NamedExpression.expression of that entry.

body instance-attribute #

dims = field(kw_only=True) class-attribute instance-attribute #

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

Gate(variable, read, exists=None) dataclass #

The variable that switches a piecewise: entry's curves on, as each curve reads it.

variable is the variable the file named: a binary, or an integer under method: incremental, which turns the curve off at 0 and, from 1 up, moves it by its point at the first breakpoint once per unit above 1. read is the gate at one curve: the variable on the entry's own frame, or the variable read through a relation, as at(..., by=) reads it. exists is where a curve reads the gate, and the curve is ungated elsewhere, its weights summing to 1; it is None where every curve reads it. A masked variable exists only on its mask, and a relation declared missing: absent may have no row for a curve, so both leave curves ungated; missing: 0 reads the variable as 0 off its mask instead, which pins those curves off. Under the relation's missing: refused every curve has a row.

exists = None class-attribute instance-attribute #

read instance-attribute #

variable instance-attribute #

Given(dims, description=None) dataclass #

A column or a row family this program reads and does not build.

The frame is the whole entry. A consumer looks the name up in the model this one is layered onto, checks the frame against what it finds, and refuses a name the host does not provide.

description = None class-attribute instance-attribute #

dims instance-attribute #

GivenParameter(dims, dtype='float', description=None) dataclass #

Data this program reads and another file declares: the frame the dim rules read, and the dtype a where compares against.

What a missing row means is the declaring file's missing:, so it is not here.

description = None class-attribute instance-attribute #

dims instance-attribute #

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

GivenTargets(parameters=Frozen({}), variables=Frozen({}), constraints=Frozen({}), expressions=Frozen({}), conditions=Frozen({})) dataclass #

What a program reads and does not build, by kind.

Every group is empty in a program built from one whole spec. Every group is frozen at construction, like every group of Program.

conditions = Frozen({}) class-attribute instance-attribute #

The named conditions the host model defines, by name. In a condition, one is data: a ParameterDefined of a boolean over its dimensions.

constraints = Frozen({}) class-attribute instance-attribute #

The constraints the host model builds, by name. A tool reads them back after the solver runs.

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

The named expressions the host model defines, by name.

parameters = Frozen({}) class-attribute instance-attribute #

The parameters another file declares, by name, with the dtype that a comparison in a condition tests.

variables = Frozen({}) class-attribute instance-attribute #

The variables the host model provides, by name.

Join(operand, columns, *, dims) dataclass #

Join operand to a relation on the columns columns joins on, one row per matching row of the relation.

The operand carries every dim joined on. The result has the operand's dims, less the dims joined on, plus the dims grouped by, plus one axis per column joined on and not grouped by (JoinColumns.axes), which stands for the relation's column and runs over its dimension. So a column dropped and a column added over one dimension stay two axes. A Sum over those axes is the group-by that follows the join, which is how sum(x, over=a, by=subject[b]) lowers. Where the grouped columns hold the relation's whole key, they determine every other column, the join opens no axis, and the bare Join is at(x, by=subject[a]): one value per row of the result, fanned out where several key tuples share the values joined on.

An axis is never a dim: a join that opens axes states the dims of the sum that closes them, and that sum removes only the dims its plain axes name.

columns instance-attribute #

dims = field(kw_only=True) class-attribute instance-attribute #

operand instance-attribute #

JoinColumns(name, relation, joined, grouped) dataclass #

One relation as one call joins it: the columns joined on, and the columns grouped by.

The entry fixes no direction; the call does, and this is the one it named. name is the relation's, as Program.relations keys it. joined and grouped are roles, which are column names of relation. relation maps every role to its dimension and names the key. joined is every column the join matches the operand on: the columns that leave the frame, and every key column the call does not name. grouped is every column of the relation the result keeps: the columns that arrive, and the same unnamed key columns. Both are in the order of the relation's columns: the key's first, each side sorted by role. A column in neither is not read, so a relation may gain a value column without changing what a call means.

A column both joined on and grouped by stays in the frame. One joined on and not grouped by is dropped, and one grouped by and not joined on is added, so the frame after the call is the operand's dims, less the dims joined on, plus the dims grouped by.

added property #

The roles grouped by and not joined on: what the call brings into the frame.

added_dims property #

axes property #

The axis the join opens for each column it drops, empty where each group is one row.

Each runs over the column's dimension and stands for the column, so it never equals that dimension's own axis. A column the grouped columns determine opens none: in a lookup they hold the key, and the key determines every column.

dropped property #

The roles joined on and not grouped by: what the call sums away or looks up.

dropped_dims property #

grouped instance-attribute #

grouped_dims property #

joined instance-attribute #

joined_dims property #

kept property #

The roles both joined on and grouped by: the key columns the call did not name.

name instance-attribute #

one_row_per_group property #

Whether the grouped columns hold the relation's whole key, so each group is one row: a lookup, not a sum.

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)

JoinedCondition(operand, columns, dims) dataclass #

operand read through a relation, written at(has_curve, by=converter_of[converter]): a lookup Join of a condition.

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 columns joins on go and the dims it groups by arrive, as a lookup Join's do.

columns instance-attribute #

dims instance-attribute #

operand instance-attribute #

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

name is the link's key in the entry, and <entry>_<name> is the row the expansion writes for it. sign is '==' where the link is pinned to the curve, and one side of it where the link is bounded by the curve instead. A link that walks a map reads the curve's weights through it, as at(..., by=) reads an array: by is the subject[map] the file wrote, and walk the join it resolves to. dims is the row the link builds: the entry's frame, or, for a walked link, that frame with the dims the join drops replaced by the ones it adds. reads says whether that row reads the entry's where through the relation, which it does when the mask carries every dim the join drops or keeps.

by = None class-attribute instance-attribute #

dims instance-attribute #

expression instance-attribute #

name instance-attribute #

reads = False class-attribute instance-attribute #

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

values instance-attribute #

walk = None class-attribute instance-attribute #

walks property #

Whether the link reads the curve's weights through a relation, rather than on the entry's own frame.

Mask(root) dataclass #

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

root is the condition 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 condition the mask restricts rows by, folded.

TYPE: Condition

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 conditions 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, sorted by name — the union of what each leaf carries.

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

names_read property #

The parameters, relations and variables the mask names.

root instance-attribute #

within(relations) #

The mask read inside the rows of relations, where each of them, read at the mask's own coordinates, is true.

A relation read anywhere else, as through shift or at, is left as it is.

Source code in src/mathspec/program.py
def within(self, relations: Iterable[str]) -> Mask:
    """The mask read inside the rows of *relations*, where each of them, read at the mask's own coordinates, is true.

    A relation read anywhere else, as through ``shift`` or ``at``, is left as it is.
    """
    return Mask(_within(self.root, frozenset(relations)))

MissingBy(by, column, fill, otherwise='refused') dataclass #

A missing: that reads a missing row by the label a map gives its coordinate.

At a coordinate with no row, the parameter reads as the fill of the label that the map column of the subject by gives that coordinate, and as otherwise where fill names no such label.

by instance-attribute #

The subject whose map gives each coordinate its label. Its own columns are over dimensions of the parameter.

column instance-attribute #

The map of by that gives the label.

fill instance-attribute #

The reading of a missing row, per label of column.

otherwise = 'refused' class-attribute instance-attribute #

The reading of a missing row at a label that fill does not name, or where column gives no label.

readings property #

Every reading a missing row may take, by label, otherwise last.

Multiply(left, right, *, dims) 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.

dims = field(kw_only=True) class-attribute instance-attribute #

left instance-attribute #

right instance-attribute #

NamedCondition(condition, dims, description=None) dataclass #

A named condition — a conditions: entry, read wherever a where, a when or a holds names it.

It builds nothing of its own: every use stands as a ConditionReference with this condition under it, so a consumer reading a condition never looks the name up here.

condition instance-attribute #

description = None class-attribute instance-attribute #

dims instance-attribute #

The dimensions of the condition, sorted by name.

NamedExpression(expression, dims, in_math, description=None, adds_to=None) dataclass #

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

in_math where the objective, a constraint or a term of a given expression reads it, directly or through another entry or a macro, and where it is such a term itself; 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.

adds_to = None class-attribute instance-attribute #

The sum this entry adds to as a term, or None.

description = None class-attribute instance-attribute #

dims instance-attribute #

The dimensions of the entry: the dims: that an entry with cases declares, or the dimensions of the body of a plain entry.

expression instance-attribute #

in_math instance-attribute #

Negate(operand, *, dims) dataclass #

dims = field(kw_only=True) class-attribute instance-attribute #

operand instance-attribute #

Not(operand) dataclass #

operand instance-attribute #

Objective(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(dims, dtype='float', missing='refused', description=None, where=None, in_=()) dataclass #

The shape of a column; data is attached at execution time by name.

dtype is what the entry claims the values are, and a consumer attaching data refuses a column that is not it — so the entry is what is read, rather than whatever the column happens to hold. It refuses a null or NaN value too: a coordinate with no value has no row, and inf is a value.

description = None class-attribute instance-attribute #

dims instance-attribute #

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

in_ = () class-attribute instance-attribute #

The relations of the parameter's index set, written in:, sorted by name. A tool attaching the data refuses a row at a key combination one of them has no row for, before where drops anything and before missing fills anything. A value column of the relation plays no part.

missing = 'refused' class-attribute instance-attribute #

What a missing row means where the parameter has coordinates, or the value it takes wherever an expression uses a value. A bare numeric name in a where is still true only where the data has a row.

readings property #

Every reading a missing row may take: the one missing names, or each of a MissingBy.

where = None class-attribute instance-attribute #

Where the parameter has coordinates, or None when it has every coordinate of its dims. It holds each relation in in_ and the file's where:. Where it is false, the parameter is absent and a tool drops its rows of data there. missing applies where it is true.

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 #

ParameterDefined(name, dims) dataclass #

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

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

dims instance-attribute #

name instance-attribute #

ParameterReference(name, *, dims) dataclass #

A parameter reference — contributes to the constant part.

dims = field(kw_only=True) class-attribute instance-attribute #

name instance-attribute #

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

One relation as a partition steps along it: the key column it steps along, the columns it partitions by, and the key columns it joins on.

name is the relation's, as Program.relations keys it. along, grouped and joined are roles, which are column names of relation. relation 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. grouped is the value columns within= named, read at the row's key: the partition's group. joined is the other key columns, whose dimensions the frame carries. No column is dropped and none is added: the frame does not change.

along instance-attribute #

along_dim property #

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

Piecewise(along, links, method, frame, where=None, activity=None, description=None) dataclass #

A piecewise: entry 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
along

The dimension each curve runs along.

TYPE: str

links

The links, in the order the file wrote them, each with the row it builds.

TYPE: tuple[Link, ...]

method

How the weights are restricted.

TYPE: PiecewiseMethod

frame

The dims: the entry builds one curve per coordinate of, in the order the file wrote them.

TYPE: tuple[str, ...]

where

Which coordinates of frame have a curve — and, where it reads along too, how far each runs — or None where every coordinate has a whole curve.

TYPE: Mask | None

activity

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

TYPE: Gate | None

description

What the file wrote under description:, or None.

TYPE: str | None

activity = None class-attribute instance-attribute #

along instance-attribute #

curve property #

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

description = None class-attribute instance-attribute #

frame instance-attribute #

method instance-attribute #

ragged property #

Whether where reads along, and so says how far each curve runs rather than only which exist.

where = None class-attribute instance-attribute #

Power(base, exponent, *, dims) 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 #

dims = field(kw_only=True) class-attribute instance-attribute #

exponent instance-attribute #

Program(*, parameters, variables, constraints, objective, dimensions=Frozen({}), relations=Frozen({}), sos=Frozen({}), piecewise=Frozen({}), assumptions=Frozen({}), expressions=Frozen({}), conditions=Frozen({}), given=GivenTargets(), description=None) dataclass #

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

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

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

What the data must satisfy, by the name that a tool quotes when it refuses data: each assumptions: entry of the file, then what the method of each piecewise: entry assumes of its breakpoints. mathspec checks none of them at load. A tool checks each when it attaches data, and refuses with assumption_message.

conditions = Frozen({}) class-attribute instance-attribute #

The conditions: that the file declares. Each use in the program is a ConditionReference, so a tool that builds rows needs nothing from this mapping.

constraints instance-attribute #

description = None class-attribute instance-attribute #

The description: of the file as a whole.

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

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

The expressions: that the file declares, each with whether the math uses it. None of them has a row of its own, and where the math uses one, it contains an ExpressionReference. mathspec compiles every named expression, so it refuses at load a file whose named expression is outside the language, even one the math does not use.

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.

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

What this program uses and does not build (GivenTargets), for the host model to provide.

objective instance-attribute #

The objective, or None when the file declares none. A solver then only tests whether the constraints can be met.

parameters instance-attribute #

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

Each piecewise: entry of the spec, as the curve it states. It is empty for the program of a spec whose curves are expanded.

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

Each table the data supplies, by its subject's name: every relation the file declares, and every dimension it declares maps of, each with its maps as value columns.

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 = Frozen({}) class-attribute instance-attribute #

variables instance-attribute #

relations_of(dimension) #

The tables with a column over dimension, by name: a relation over it, or a subject with a map into it.

Source code in src/mathspec/program.py
def relations_of(self, dimension: str) -> Mapping[str, Relation]:
    """The tables with a column over *dimension*, by name: a relation over it, or a subject with a map into it."""
    return Frozen({n: lk for n, lk in self.relations.items() if dimension in lk.dims})

Reach(label, name, kind) dataclass #

One read along an axis whose distance only data can say.

ATTRIBUTE DESCRIPTION
label

The entry reading, as the lowering's messages label it.

TYPE: str

name

The parameter, the relation, or the subject[map] whose data 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 #

Relation(columns, key, missing='refused', description=None, partial=()) dataclass #

One table the data supplies: a relation's rows, or a dimension's labels, with a column per map of the subject.

columns maps each role to its dimension, the key's roles first and each side sorted by role; key is the roles a row is identified by: a relation's own columns, or the dimension for a dimension's table. Its values are the subject's maps, each named after the map, and none for a relation with no maps. 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. Program.relations keys a table by its subject's name, so a dimension's table shares the dimension's name.

columns instance-attribute #

description = None class-attribute instance-attribute #

dims property #

Every dimension a column is over, once, sorted by name.

key instance-attribute #

missing = 'refused' class-attribute instance-attribute #

Whether every combination of the key needs a row: refused or absent.

partial = () class-attribute instance-attribute #

The value roles a row may leave empty: the maps under missing: absent, sorted by role.

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]

in_order(roles) #

roles as columns orders them: the key's first, each side sorted by role.

Source code in src/mathspec/program.py
def in_order(self, roles: Iterable[str]) -> tuple[str, ...]:
    """*roles* as [`columns`][] orders them: the key's first, each side sorted by role."""
    given = tuple(roles)
    return (*sorted(r for r in given if r in self.key), *sorted(r for r in given if r not in self.key))

missing_at(roles) #

absent where a read of roles may find nothing there: the table may lack the row, or a map its label.

Source code in src/mathspec/program.py
def missing_at(self, roles: Iterable[str]) -> RelationMissing:
    """``absent`` where a read of *roles* may find nothing there: the table may lack the row, or a map its label."""
    return 'absent' if self.missing == 'absent' or set(roles) & set(self.partial) else 'refused'

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

Compare one map of a subject against a literal — snapshot[period] == 2030.

column is the map read, and dims the dimensions of the subject's own columns: the leaf is read at them, one label per coordinate.

column instance-attribute #

dims = () class-attribute instance-attribute #

name instance-attribute #

op instance-attribute #

value instance-attribute #

RelationDefined(name, dims=(), column=None) dataclass #

True where the table has a row at the frame's coordinates, and with column set, a label in that map.

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. A bare relation name in a condition sets no column; subject[map] alone sets it, as outage[line] does.

column = None class-attribute instance-attribute #

dims = () class-attribute instance-attribute #

name instance-attribute #

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

Compare a map of one subject with a map of another, or of the same — line[bus0] != line[bus1] — row by row.

Both subjects are over the same dims, and the two maps refer to 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 entries 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 entry 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 sums it away, 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 entry 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 entry 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 entry 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 that 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))

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

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

The only entry 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, *, dims) dataclass #

Sum operand over the axes in over, removing them from the result.

An axis in over is a dimension's own, or one a Join under it opens (JoinColumns.axes): sum(x, over=a, by=subject[b]) lowers to a Sum over a Join, over the axis of each column the join does not group by. This node is the group-by that follows the join.

dims = field(kw_only=True) class-attribute instance-attribute #

operand instance-attribute #

over instance-attribute #

Translate(operand, along, offset, wrap, fill=None, partition=None, *, dims) 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 #

dims = field(kw_only=True) class-attribute instance-attribute #

fill = None class-attribute instance-attribute #

offset instance-attribute #

operand instance-attribute #

partition = None class-attribute instance-attribute #

wrap instance-attribute #

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

operand read offset coordinates back along one dimension — shift(points, along=bp, offset=1).

A negative offset reads forward. Where the shift reads past the edge, the condition is false, as a missing row is false in any condition, so the file states no edge=.

along instance-attribute #

dims instance-attribute #

offset instance-attribute #

operand instance-attribute #

Variable(dims, where=None, lower=None, upper=None, domain='continuous', missing='absent', description=None, in_=()) dataclass #

description = None class-attribute instance-attribute #

dims instance-attribute #

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

in_ = () class-attribute instance-attribute #

The relations of the variable's index set, written in:, sorted by name.

lower = None class-attribute instance-attribute #

A number, a parameter, or None when that side is open. An engine decides how an open side enters the model.

missing = 'absent' class-attribute instance-attribute #

upper = None class-attribute instance-attribute #

As lower, for the upper side.

where = None class-attribute instance-attribute #

Where the variable exists, or None when it exists at every coordinate of its dims. It holds each relation in in_ and the file's where:.

VariableDefined(name, dims) dataclass #

True at the coordinates where the named variable exists.

dims instance-attribute #

name instance-attribute #

VariableReference(name, *, dims) dataclass #

A variable reference — one term per existing variable row.

dims = field(kw_only=True) class-attribute instance-attribute #

name instance-attribute #

WindowSum(operand, along, width, wrap, partition=None, *, dims) 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 #

dims = field(kw_only=True) class-attribute 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.holds.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, VariableReference) 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, ExpressionReference):
        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, Join, Translate, WindowSum)):
        return (expression.operand,)
    if isinstance(expression, Cases):
        return tuple(region.value for region in expression.regions)
    if isinstance(expression, (Constant, ParameterReference, VariableReference, Dual)):
        return ()
    assert_never(expression)

condition_children(condition) #

The conditions under condition — a connective's operands, a named condition's body, and nothing under a leaf.

What every walk over a condition 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 condition_children(condition: Condition) -> tuple[Condition, ...]:
    """The conditions under *condition* — a connective's operands, a named condition's body, and nothing under a leaf.

    What every walk over a condition 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(condition, Not):
        return (condition.operand,)
    if isinstance(condition, (And, Or)):
        return (condition.left, condition.right)
    if isinstance(condition, ConditionReference):
        return (condition.body,)
    return ()

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 entries 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 entries 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, ParameterReference))

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, VariableReference))

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: 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:`` 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, ())

See also