Every construct, as math¶
Typesetting prints a model the way a paper prints it. This page prints all of it: every construct the language has, beside the math the typesetter gives it, so the notation can be read as the one system it has to be — two constructs that mean different things looking different, a symbol introduced where it is defined and used where it is meant.
It is generated by uv run python -m tools.notation, almost all of it from one
model:
tests/golden/model.yaml,
which is not a sensible optimisation problem and is not trying to be: it is the
one file that carries every construct at once, and three checks in
tests/test_typeset.py hold it to the language — every operator a format
spells, every node kind the parsers produce, every line of the walk. So every
here is asserted rather than promised, and a construct added to the language
arrives on this page or CI goes red. The curves are the exception, one real
model per method:, for the reason the section gives.
Two things this page is not. It is not the operator reference — what each operator does is Operators, which renders the same math one row per call shape. And it is not a tutorial: the models on the gallery pages are the ones written to be read.
The symbols are the derived ones, taken with no symbol table, because that is what a model prints with no setup — \(\mathit{load}_{t}\) rather than \(\ell_t\). A symbol table replaces them wholesale and changes nothing else on this page.
The legend¶
A dimension, a lookup and a parameter declare no equation; what they print is the legend every model opens with.
dimensions:
snapshot: {dtype: int}
generator: {dtype: str}
bus: {dtype: str}
zone: {dtype: str}
season: {dtype: str}
lookups:
gen_bus: {over: generator, into: bus}
zone_of: {over: bus, into: zone}
area_of: {over: bus, into: zone} # a second map into the same set, to compare against
season_of: {over: snapshot, into: season}
tech: {over: generator, dtype: str} # no `into`: a label space, which the legend words differently
parameters:
p_max: {dims: [generator]}
p_min: {dims: [generator]}
cost: {dims: [generator]}
load: {dims: [snapshot, bus]}
is_flexible: {dims: [generator], dtype: bool}
zone_cap: {dims: [zone]}
min_up: {dims: [generator], dtype: int}
lead: {dims: [generator], dtype: int}
budget: {dims: []} # scalar: the legend says so rather than printing an empty product
Sets¶
| Symbol | Meaning |
|---|---|
| \(\mathcal{T}\) | index \(t\) --- snapshot with \(\mathrm{season\_of}: \mathcal{T} \to \mathcal{S}\) |
| \(\mathcal{G}\) | index \(g\) --- generator with \(\mathrm{gen\_bus}: \mathcal{G} \to \mathcal{B}\) carrying label \(\mathrm{tech}\) |
| \(\mathcal{B}\) | index \(b\) --- bus with \(\mathrm{zone\_of}: \mathcal{B} \to \mathcal{Z},\enspace \mathrm{area\_of}: \mathcal{B} \to \mathcal{Z}\) |
| \(\mathcal{Z}\) | index \(z\) --- zone |
| \(\mathcal{S}\) | index \(s\) --- season |
Parameters¶
| Symbol | Meaning |
|---|---|
| \(p^{\mathrm{max}}\) | p_max over \(\mathcal{G}\) |
| \(p^{\mathrm{min}}\) | p_min over \(\mathcal{G}\) |
| \(\mathit{cost}\) | cost over \(\mathcal{G}\) |
| \(\mathit{load}\) | load over \(\mathcal{T} \times \mathcal{B}\) |
| \(\mathit{is\_flexible}\) | is_flexible over \(\mathcal{G}\) |
| \(\mathit{zone}^{\mathrm{cap}}\) | zone_cap over \(\mathcal{Z}\) |
| \(\mathit{min\_up}\) | min_up over \(\mathcal{G}\) |
| \(\mathit{lead}\) | lead over \(\mathcal{G}\) |
| \(\mathit{budget}\) | budget (scalar) |
Variables¶
| Symbol | Meaning |
|---|---|
| \(p\) | p over \(\mathcal{T} \times \mathcal{G}\) |
| \(\mathit{spill}\) | spill over \(\mathcal{T}\) |
| \(\mathit{slack}\) | slack over \(\mathcal{T}\) |
| \(\mathit{theta}\) | theta over \(\mathcal{B}\) |
| \(\mathit{on}\) | on over \(\mathcal{T} \times \mathcal{G}\) |
| \(\mathit{units}\) | units over \(\mathcal{G}\) |
| \(\mathit{spare}\) | spare over \(\mathcal{G}\) |
| \(\mathit{reserve}\) | reserve (scalar) |
| \(\mathit{headroom}\) | headroom (scalar) |
| \(\mathit{void}\) | void over \(\mathcal{B}\) |
| \(\mathit{weight}\) | weight over \(\mathcal{T} \times \mathcal{G}\) |
\(t \ominus k\) denotes cyclic translation: index \(t-k\) taken modulo the size of the dimension (roll). Plain \(t-k\) (shift) has no wraparound --- terms translated past the edge are simply absent.
\(t \boxminus_{v} k\) denotes translation with \(v\) standing where index \(t-k\) leaves the dimension (shift(edge=v)), so the row at that boundary is built and carries \(v\) rather than being dropped.
The objective¶
objective¶
a sense, the reduction a declaration implies, and three groups: two terms sharing dims, then two subtracted scalars
Constraints¶
balance¶
sum over a lookup
ramp¶
roll (cyclic) and shift (acyclic) in one equation
ramp:
foreach: [snapshot, generator]
expression: p - shift(p, over=snapshot, offset=1, edge='wrap') <= shift(p, over=snapshot, offset=1) + p_max
edges¶
the two translations ramp leaves out: a fill, and forwards
edges:
foreach: [snapshot, generator]
expression: >-
shift(p, over=snapshot, offset=1, edge=0)
<= shift(p, over=snapshot, offset=-1, edge=0) + p_max
ahead¶
the cyclic translation forwards, which is a fourth symbol again
ahead:
foreach: [snapshot, generator]
expression: p <= shift(p, over=snapshot, offset=-1, edge='wrap')
composed¶
two steps of one policy are one step; a zero step is none at all
composed:
foreach: [snapshot, generator]
expression: shift(shift(p, over=snapshot, offset=1), over=snapshot, offset=1) <= shift(p_max, over=generator, offset=0)
uncomposed¶
a named offset under a numbered one stays two steps, not their sum
uncomposed:
foreach: [snapshot, generator]
expression: shift(shift(p, over=snapshot, offset=lead), over=snapshot, offset=1) <= p_max
crossed¶
two dimensions translated at one leaf, each with its own policy
crossed:
foreach: [snapshot, generator]
expression: shift(shift(p, over=snapshot, offset=1, edge='wrap'), over=generator, offset=-1) <= p_max
lead_time¶
an offset the data carries, so it prints as a symbol rather than a number
lead_time:
foreach: [snapshot, generator]
expression: shift(p, over=snapshot, offset=lead, edge=0) <= p_max
in_season¶
a translation partitioned by a lookup: the group rides on the operator
in_season:
foreach: [snapshot, generator]
expression: p <= shift(p, over=snapshot, offset=1, edge='wrap', by=season_of)
window¶
a trailing window of fixed width
history¶
the same window, its width in the data and its edge wrapped
history:
foreach: [snapshot, generator]
expression: sum_back(on, over=snapshot, within=min_up, edge='wrap') <= units
pullback¶
at(), which re-indexes through a lookup instead of an offset
arithmetic¶
division, unary minus, nested reduction, bracketing
arithmetic:
# No `**`: the walk renders it, but `lower_program` rejects it, so a model
# using it would not be a model. Format.power stays exercised by unit
# tests rather than from here.
foreach: [snapshot]
expression: sum(p / 2 + -cost, over=generator) >= -sum(p, over=generator) * 3
scalar¶
a parameter over nothing, and a mask that is a bare parameter
running¶
a mask on a variable's existence, and one on a dimension's label
first¶
a position in a dimension, and the same position within a group
first:
foreach: [snapshot, generator]
where: "snapshot == index(snapshot, 0) OR snapshot == index(snapshot, 0, by=season_of)"
expression: on == 1
northern¶
a lookup compared to a label, to another lookup, and to nothing
northern:
foreach: [snapshot, bus]
where: "zone_of == 'north' AND zone_of != area_of AND zone_of"
expression: slack <= load
always¶
the two constant masks, which are a quantifier and no equation
never¶
the other constant mask
Variable domains¶
p¶
both bounds, and a where with all three connectives
p:
foreach: [snapshot, generator]
where: "p_max > 0 AND NOT is_flexible OR p_min > 0"
bounds: {lower: p_min, upper: p_max}
spill¶
lower only
slack¶
upper only
theta¶
unbounded
on¶
a binary domain, which is a set rather than a pair of bounds
units¶
an integer domain, which is both: bounds, and where the values live
spare¶
integer with neither bound: the domain is the whole line
reserve¶
an empty foreach: a scalar declaration, whose line carries no quantifier
headroom¶
scalar too, but masked, so the condition stands with no set beside it
void¶
a bound on the wrong side of the line: the one way infinity prints
weight¶
the family a sos runs along
Curves, as what they expand to¶
A curve is sugar: what prints is the formulation it expands to, which is the math the solver receives. One row per method:, each from the model named under it, so the symbols in this section are that model's.
economies_of_scale¶
method: adjacency --- a binary per segment, and a row making the two nonzero weights neighbours, in examples/ports/transport_pwl.yaml.
cost_curve¶
method: sos2 --- the same weights, restricted by a set the sink branches on (the sos rules), in examples/sos.yaml.
cost_curve¶
method: convex --- nothing — the weights range over the hull, which is a pure LP, in examples/piecewise.yaml.
Sets carried to the solver¶
adjacent¶
at most two adjacent members nonzero, one set per snapshot