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 pixi run python -m tools.notation, almost all of it from one
model:
tests/typesetting/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/typesetting/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 under
examples/ 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 }
technology: { dtype: str }
lookups:
gen_bus: { over: generator, into: bus }
gen_tech: { over: generator, into: technology } # a second map out of `generator`, to group through both at once
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] }
tech_cap: { dims: [bus, technology] }
min_up: { dims: [generator], dtype: int }
eta: { dims: [generator] } # a Greek name that is *given*, so the rule wins and it prints as the word
lead: { dims: [generator], dtype: int }
budget: { dims: [] } # scalar: the legend says so rather than printing an empty product
growth: { dims: [] } # the base of a power; the exponent is `lead`, a column
Sets#
| Symbol | Meaning |
|---|---|
| \(\mathcal{T}\) | index \(t\) — snapshot (int coordinates) with \(\mathrm{season\_of}: \mathcal{T} \to \mathcal{S}\) |
| \(\mathcal{G}\) | index \(g\) — generator with \(\mathrm{gen\_bus}: \mathcal{G} \to \mathcal{B},\enspace \mathrm{gen\_tech}: \mathcal{G} \to \mathcal{E}\) 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 |
| \(\mathcal{E}\) | index \(e\) — technology |
Parameters#
| Symbol | Meaning |
|---|---|
| \(\mathrm{p}^{\mathrm{max}}\) | p_max over \(\mathcal{G}\) |
| \(\mathrm{p}^{\mathrm{min}}\) | p_min over \(\mathcal{G}\) |
| \(\mathrm{cost}\) | cost over \(\mathcal{G}\) |
| \(\mathrm{load}\) | load over \(\mathcal{T} \times \mathcal{B}\) |
| \(\mathrm{is\_flexible}\) | is_flexible over \(\mathcal{G}\) |
| \(\mathrm{zone\_cap}\) | zone_cap over \(\mathcal{Z}\) |
| \(\mathrm{tech\_cap}\) | tech_cap over \(\mathcal{B} \times \mathcal{E}\) |
| \(\mathrm{min\_up}\) | min_up over \(\mathcal{G}\) |
| \(\mathrm{eta}\) | eta over \(\mathcal{G}\) |
| \(\mathrm{lead}\) | lead over \(\mathcal{G}\) |
| \(\mathrm{budget}\) | budget (scalar) |
| \(\mathrm{growth}\) | growth (scalar) |
Variables#
| Symbol | Meaning |
|---|---|
| \(p\) | p over \(\mathcal{T} \times \mathcal{G}\) |
| \(\mathit{spill}\) | spill over \(\mathcal{T}\) |
| \(\mathit{slack}\) | slack over \(\mathcal{T}\) |
| \(\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{weight}\) | weight over \(\mathcal{T} \times \mathcal{G}\) |
Upright is what the model is given — a parameter such as \(\mathrm{p}^{\mathrm{max}}\), a coordinate map, a label — and italic is what the solver chooses, such as \(p\). An index is italic too, being what a quantifier chooses, and a set is script.
\(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.
\(t \ominus^{\mathrm{lookup}(t)} k\) denotes a translation counted inside the group a lookup puts \(t\) in (shift(by=lookup)), so a term never crosses out of its own group. The two modifiers take different slots — the group above, the fill below — so \(t \boxminus_{v}^{\mathrm{lookup}(t)} k\) is both at once.
\(\mathrm{pos}(t)\) denotes where index \(t\) sits along its dimension's own order — the order shift walks, not the order labels sort in — counted from \(0\). The index itself stays the coordinate, so \(t\) compares against labels and \(\mathrm{pos}(t)\) against positions.
\(\mathrm{pos}_{\mathrm{lookup}(t)}(t)\) counts within the group a lookup puts \(t\) in: the subscript names the map, \(\mathcal{T}_{\mathrm{lookup}(t)}\) is the group it lands in, and that group has a first position of its own.
\(\lvert \mathcal{T} \rvert\) denotes the size of the set being counted along, and a position counted from the end prints against it — \(\lvert \mathcal{T} \rvert - 1\) is the last position, one less than the size because the first is \(0\).
The objective#
objective#
a sense, a product of two variables, a power over two parameters, a power of one of those, and the summations a scalar objective spells out beside two scalar terms
sense: maximize
expression: sum(p * cost) + sum(p * p * cost) + sum(p * cost * growth ** lead) + sum(p * (growth ** lead) ** 2) + sum(p * p_max) - reserve + -headroom
Constraints#
starts#
names the cased expression: its symbol prints here, its block once below
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, edge=0), 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)
held_in_season#
the same group, with a fill: each season's opening row is kept and given a zero
held_in_season:
foreach: [snapshot, generator]
expression: p <= shift(p, over=snapshot, offset=1, edge=0, 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
seasonal_window#
a window partitioned by a lookup: the group rides on the operator
seasonal_window:
foreach: [snapshot, generator]
expression: sum_back(on, over=snapshot, within=3, by=season_of) <= units
pullback#
at(), which re-indexes through a lookup instead of an offset
grouped_twice#
one grouping through two maps: the domain carries both conditions
grouped_twice:
foreach: [snapshot, bus, technology]
expression: sum(p, by=[gen_bus, gen_tech]) <= tech_cap
pulled_back_twice#
its adjoint, reading one slot through a pair of labels
arithmetic#
division, both unary signs, a sign beside a sign, floats with and without an exponent, bracketing
arithmetic:
foreach: [snapshot]
expression: >-
sum(p / 2 + -cost - -1e-5 * p + 2.5e-7 * cost + 0.5 * p, over=generator)
>= -sum(+p, over=generator) * -3
total#
a sum naming no dim, whose domain is the one place the dims it took are said
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: "position(snapshot) == 0 OR position(snapshot, by=season_of) == 0"
expression: on == 1
last#
the same two counted from the end, which print against a size rather than as themselves
last:
foreach: [snapshot, generator]
where: "position(snapshot) == -1 OR position(snapshot, by=season_of) == -1"
expression: on == 0
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
efficiency#
a Greek-named parameter, which is given — so the convention wins and it prints as the word
ceiling#
the infinity literal, which is the one way infinity prints
always#
a mask that is only the constant true, which the language says is no mask at all — so none prints
redundant#
the same constant inside a mask, where it is what the file says and prints
never#
the other constant mask, which says the rows are none and is worth seeing
Definitions#
lcoe#
nothing in the math reads it, so it prints under its own name rather than inlining — and its divisor may carry a variable
marginal_price#
the row dual of a constraint, the one builtin only an entry the math never reads may call
startup_cost#
a quantity defined by region: no two cases overlap, and otherwise is the rest
startup_cost:
foreach: [snapshot, generator]
cases:
opening: { when: "position(snapshot) == 0", expression: cost }
winter: { when: "position(snapshot) > 0 and season_of == 'winter'", expression: cost * 2 }
otherwise: 0
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
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.
Rendered with the sidecar symbol table examples/symbols/transport_pwl.yaml, which is what the weights print as:
notation: latex
names:
economies_of_scale_lam: "\\lambda"
economies_of_scale_seg: "\\delta"
bp_x: "\\mathrm{x}"
bp_y: "\\mathrm{y}"
cost_curve#
method: sos2 — the same weights, restricted by a set the solver branches on (the sos rules), in examples/sos.yaml.
Rendered with the sidecar symbol table examples/symbols/sos.yaml, which is what the weights print as:
cost_curve#
method: convex — nothing — the weights range over the hull, which is a pure LP, in examples/piecewise.yaml.
Rendered with the sidecar symbol table examples/symbols/piecewise.yaml, which is what the weights print as:
cost_curve#
method: lp — no weights at all — one row per segment line, plus the two rows holding the domain, in examples/piecewise_lp.yaml.
Rendered with the sidecar symbol table examples/symbols/piecewise_lp.yaml, which is what the weights print as:
Sets carried to the solver#
adjacent#
at most two adjacent members nonzero, one set per snapshot