Skip to content

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

sense: maximize
expression: p * cost + p * p_max - reserve + -headroom
\[\max \sum_{t \in \mathcal{T},\enspace g \in \mathcal{G}} \left( p_{t,g} \cdot \mathit{cost}_{g} + p_{t,g} \cdot p^{\mathrm{max}}_{g} \right) - \mathit{reserve} - \mathit{headroom}\]

Constraints

balance

sum over a lookup

balance:
  foreach: [snapshot, bus]
  expression: sum(p, by=gen_bus) + spill - slack == load
\[\sum_{g \in \mathcal{G} \thinspace:\thinspace \mathrm{gen\_bus}(g) = b} p_{t,g} + \mathit{spill}_{t} - \mathit{slack}_{t} = \mathit{load}_{t,b} \qquad \forall\thinspace t \in \mathcal{T},\enspace b \in \mathcal{B}\]

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
\[p_{t,g} - p_{t \ominus 1,g} \le p_{t - 1,g} + p^{\mathrm{max}}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}\]

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
\[p_{t \boxminus_{0} 1,g} \le p_{t \boxplus_{0} 1,g} + p^{\mathrm{max}}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}\]

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')
\[p_{t,g} \le p_{t \oplus 1,g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}\]

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)
\[p_{t - 2,g} \le p^{\mathrm{max}}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}\]

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
\[p_{\left( t - 1 \right) - \mathit{lead},g} \le p^{\mathrm{max}}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}\]

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
\[p_{t \ominus 1,g + 1} \le p^{\mathrm{max}}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}\]

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
\[p_{t \boxminus_{0} \mathit{lead},g} \le p^{\mathrm{max}}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}\]

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)
\[p_{t,g} \le p_{t \ominus_{\mathrm{season\_of}(t)} 1,g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}\]

window

a trailing window of fixed width

window:
  foreach: [snapshot, generator]
  expression: sum_back(on, over=snapshot, within=3) <= units
\[\sum_{t' \in \mathcal{T} \thinspace:\thinspace 0 \le t - t' < 3} \mathit{on}_{t',g} \le \mathit{units}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}\]

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
\[\sum_{t' \in \mathcal{T} \thinspace:\thinspace 0 \le t \ominus t' < \mathit{min\_up}} \mathit{on}_{t',g} \le \mathit{units}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}\]

pullback

at(), which re-indexes through a lookup instead of an offset

pullback:
  foreach: [snapshot, bus]
  expression: spill <= at(zone_cap, by=zone_of)
\[\mathit{spill}_{t} \le \mathit{zone}^{\mathrm{cap}}_{\mathrm{zone\_of}(b)} \qquad \forall\thinspace t \in \mathcal{T},\enspace b \in \mathcal{B}\]

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
\[\sum_{g \in \mathcal{G}} \left( \frac{p_{t,g}}{2} + -\mathit{cost}_{g} \right) \ge \left( -\left( \sum_{g \in \mathcal{G}} p_{t,g} \right) \right) \cdot 3 \qquad \forall\thinspace t \in \mathcal{T}\]

scalar

a parameter over nothing, and a mask that is a bare parameter

scalar:
  foreach: [generator]
  where: "cost"
  expression: units <= budget
\[\mathit{units}_{g} \le \mathit{budget} \qquad \forall\thinspace g \in \mathcal{G} \thinspace:\thinspace \mathit{cost}_{g} \text{ is defined}\]

running

a mask on a variable's existence, and one on a dimension's label

running:
  foreach: [snapshot, bus]
  where: "theta AND snapshot >= 3"
  expression: theta <= load
\[\mathit{theta}_{b} \le \mathit{load}_{t,b} \qquad \forall\thinspace t \in \mathcal{T},\enspace b \in \mathcal{B} \thinspace:\thinspace \mathit{theta}_{b} \text{ exists} \wedge t \ge 3\]

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
\[\mathit{on}_{t,g} = 1 \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G} \thinspace:\thinspace \left( t = \mathrm{index}(\mathcal{T}, 0) \vee t = \mathrm{index}(\mathcal{T}, 0, \mathrm{season\_of}(t)) \right)\]

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
\[\mathit{slack}_{t} \le \mathit{load}_{t,b} \qquad \forall\thinspace t \in \mathcal{T},\enspace b \in \mathcal{B} \thinspace:\thinspace \mathrm{zone\_of}(b) = \text{north} \wedge \mathrm{zone\_of}(b) \neq \mathrm{area\_of}(b) \wedge \mathrm{zone\_of}(b) \text{ is defined}\]

always

the two constant masks, which are a quantifier and no equation

always:
  foreach: [snapshot]
  where: "true"
  expression: spill >= 0
\[\mathit{spill}_{t} \ge 0 \qquad \forall\thinspace t \in \mathcal{T} \thinspace:\thinspace \top\]

never

the other constant mask

never:
  foreach: [snapshot]
  where: "false"
  expression: slack >= 0
\[\mathit{slack}_{t} \ge 0 \qquad \forall\thinspace t \in \mathcal{T} \thinspace:\thinspace \bot\]

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}
\[p^{\mathrm{min}}_{g} \le p_{t,g} \le p^{\mathrm{max}}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G} \thinspace:\thinspace \left( p^{\mathrm{max}}_{g} > 0 \wedge \neg \mathit{is\_flexible}_{g} \vee p^{\mathrm{min}}_{g} > 0 \right)\]

spill

lower only

spill:
  foreach: [snapshot]
  bounds: {lower: 0}
\[\mathit{spill}_{t} \ge 0 \qquad \forall\thinspace t \in \mathcal{T}\]

slack

upper only

slack:
  foreach: [snapshot]
  bounds: {upper: 100}
\[\mathit{slack}_{t} \le 100 \qquad \forall\thinspace t \in \mathcal{T}\]

theta

unbounded

theta:
  foreach: [bus]
\[\mathit{theta}_{b} \in \mathbb{R} \qquad \forall\thinspace b \in \mathcal{B}\]

on

a binary domain, which is a set rather than a pair of bounds

on:
  foreach: [snapshot, generator]
  domain: binary
\[\mathit{on}_{t,g} \in \{0, 1\} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}\]

units

an integer domain, which is both: bounds, and where the values live

units:
  foreach: [generator]
  domain: integer
  bounds: {lower: 0, upper: 10}
\[0 \le \mathit{units}_{g} \le 10, \mathit{units}_{g} \in \mathbb{Z} \qquad \forall\thinspace g \in \mathcal{G}\]

spare

integer with neither bound: the domain is the whole line

spare:
  foreach: [generator]
  domain: integer
\[\mathit{spare}_{g} \in \mathbb{Z} \qquad \forall\thinspace g \in \mathcal{G}\]

reserve

an empty foreach: a scalar declaration, whose line carries no quantifier

reserve:
  foreach: []
  bounds: {lower: 0}
\[\mathit{reserve} \ge 0\]

headroom

scalar too, but masked, so the condition stands with no set beside it

headroom:
  foreach: []
  where: "budget"
  bounds: {lower: 0}
\[\mathit{headroom} \ge 0 \qquad \text{where } \mathit{budget} \text{ is defined}\]

void

a bound on the wrong side of the line: the one way infinity prints

void:
  foreach: [bus]
  bounds: {lower: .inf, upper: -.inf}
\[\infty \le \mathit{void}_{b} \le -\infty \qquad \forall\thinspace b \in \mathcal{B}\]

weight

the family a sos runs along

weight:
  foreach: [snapshot, generator]
  bounds: {lower: 0, upper: 1}
\[0 \le \mathit{weight}_{t,g} \le 1 \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}\]

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.

economies_of_scale:
  over: bp
  links:
    - [shipment, bp_x]
    - [scaled, bp_y]
\[\sum_{b \in \mathcal{B}} \mathit{economies\_of\_scale\_lam}_{p,m,b} = 1 \qquad \forall\thinspace p \in \mathcal{P},\enspace m \in \mathcal{M}\]
\[\mathit{shipment}_{p,m} = \sum_{b \in \mathcal{B}} \mathit{economies\_of\_scale\_lam}_{p,m,b} \cdot \mathit{bp}^{\mathrm{x}}_{b} \qquad \forall\thinspace p \in \mathcal{P},\enspace m \in \mathcal{M}\]
\[\mathit{scaled}_{p,m} = \sum_{b \in \mathcal{B}} \mathit{economies\_of\_scale\_lam}_{p,m,b} \cdot \mathit{bp}^{\mathrm{y}}_{b} \qquad \forall\thinspace p \in \mathcal{P},\enspace m \in \mathcal{M}\]
\[\sum_{b \in \mathcal{B}} \mathit{economies\_of\_scale\_seg}_{p,m,b} = 1 \qquad \forall\thinspace p \in \mathcal{P},\enspace m \in \mathcal{M}\]
\[\mathit{economies\_of\_scale\_lam}_{p,m,b} \le \mathit{economies\_of\_scale\_seg}_{p,m,b} + \mathit{economies\_of\_scale\_seg}_{p,m,b \boxminus_{0} 1} \qquad \forall\thinspace p \in \mathcal{P},\enspace m \in \mathcal{M},\enspace b \in \mathcal{B}\]
\[0 \le \mathit{economies\_of\_scale\_lam}_{p,m,b} \le 1 \qquad \forall\thinspace p \in \mathcal{P},\enspace m \in \mathcal{M},\enspace b \in \mathcal{B}\]
\[\mathit{economies\_of\_scale\_seg}_{p,m,b} \in \{0, 1\} \qquad \forall\thinspace p \in \mathcal{P},\enspace m \in \mathcal{M},\enspace b \in \mathcal{B}\]

cost_curve

method: sos2 --- the same weights, restricted by a set the sink branches on (the sos rules), in examples/sos.yaml.

cost_curve:
  over: bp
  links:
    - [p, bp_x]
    - [op_cost, bp_y]
  method: sos2
\[\sum_{b \in \mathcal{B}} \mathit{cost\_curve\_lam}_{t,g,b} = 1 \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}\]
\[p_{t,g} = \sum_{b \in \mathcal{B}} \mathit{cost\_curve\_lam}_{t,g,b} \cdot \mathit{bp}^{\mathrm{x}}_{g,b} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}\]
\[\mathit{op\_cost}_{t,g} = \sum_{b \in \mathcal{B}} \mathit{cost\_curve\_lam}_{t,g,b} \cdot \mathit{bp}^{\mathrm{y}}_{g,b} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}\]
\[0 \le \mathit{cost\_curve\_lam}_{t,g,b} \le 1 \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G},\enspace b \in \mathcal{B}\]
\[\left( \mathit{cost\_curve\_lam}_{t,g,b} \right)_{b \in \mathcal{B}} \in \mathrm{SOS}2 \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}\]

cost_curve

method: convex --- nothing — the weights range over the hull, which is a pure LP, in examples/piecewise.yaml.

cost_curve:
  over: bp
  links:
    - [p, bp_x]
    - [op_cost, bp_y]
  method: convex
\[\sum_{b \in \mathcal{B}} \mathit{cost\_curve\_lam}_{t,g,b} = 1 \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}\]
\[p_{t,g} = \sum_{b \in \mathcal{B}} \mathit{cost\_curve\_lam}_{t,g,b} \cdot \mathit{bp}^{\mathrm{x}}_{g,b} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}\]
\[\mathit{op\_cost}_{t,g} = \sum_{b \in \mathcal{B}} \mathit{cost\_curve\_lam}_{t,g,b} \cdot \mathit{bp}^{\mathrm{y}}_{g,b} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}\]
\[0 \le \mathit{cost\_curve\_lam}_{t,g,b} \le 1 \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G},\enspace b \in \mathcal{B}\]

Sets carried to the solver

adjacent

at most two adjacent members nonzero, one set per snapshot

adjacent:
  variable: weight
  over: generator
  type: 2
\[\left( \mathit{weight}_{t,g} \right)_{g \in \mathcal{G}} \in \mathrm{SOS}2 \qquad \forall\thinspace t \in \mathcal{T}\]