Every construct, as math#
Every construct the language has is printed here, beside the math the typesetter gives it. Look up the notation for one construct, or read the whole notation at once: two constructs that mean different things print differently, and every symbol below appears first in the legend that defines it.
The page is generated by pixi run python -m tools.notation, almost all of it
from one model,
tests/typesetting/golden/model.yaml.
That model is not a sensible optimisation problem. It is the one file that
carries every construct at once, and tests in tests/typesetting/ hold it to
the language: every operator a format spells, every kind of node the parsers
produce, and every line of the code that walks them. So a construct added to the
language either arrives on this page, or CI fails. The curves are the exception,
and use one real model per method:.
For what an operator does, read Operators, which prints the same math with one row per call. For models written to be read, start with the examples.
The symbols below are derived from the names in the file, which is what a model prints with no setup, so you see \(\mathit{load}_{t}\) rather than \(\ell_t\). A symbol table replaces every symbol, 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 }
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},\ \mathrm{gen\_tech}: \mathcal{G} \to \mathcal{E}\) |
| \(\mathcal{B}\) | index \(b\) — bus with \(\mathrm{zone\_of}: \mathcal{B} \to \mathcal{Z},\ \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}\) |
| \(\mathit{imported}\) | imported over \(\mathcal{T} \times \mathcal{Z}\) — energy brought into a zone, built by the model this file is laid over |
Definitions#
| Symbol | Meaning |
|---|---|
| \(\mathit{spend}\) | spend over \(\mathcal{T}\) — what a snapshot's dispatch costs |
| \(\mathit{lcoe}\) | lcoe (scalar) |
| \(\mathit{marginal\_price}\) | marginal_price over \(\mathcal{T} \times \mathcal{B}\) |
| \(\mathit{congestion\_rent}\) | congestion_rent over \(\mathcal{T} \times \mathcal{Z}\) |
| \(\mathit{net\_import}\) | net_import over \(\mathcal{T}\) |
| \(\mathrm{startup\_cost}\) | startup_cost over \(\mathcal{T} \times \mathcal{G}\) — what starting a unit in this snapshot costs, which the horizon's edge changes |
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#
budgeted#
names the plain expression: its symbol prints here, its definition once below
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#
spend#
a plain named expression: its symbol prints where it is used, its body once as a definition
lcoe#
nothing in the math reads it, so 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
congestion_rent#
the dual of a row family this file is given rather than builds
net_import#
a given column read like any other
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