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 below are derived from the names in the file, which is what a model prints with no setup, so you see \(\mathrm{load}_{t}\) rather than \(\ell_t\). A symbol table replaces every symbol, and changes nothing else on this page.
The legend#
A dimension, a relation 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 }
bp: { dtype: int } # the breakpoints every curve below runs through
relations:
gen_bus: { key: generator, values: bus }
zone_of: { key: bus, values: zone }
area_of: { key: bus, values: zone } # a second map into the same set, to compare against
season_of: { key: snapshot, values: season }
gen_zone: { key: [generator, snapshot], values: zone } # a map keyed by two dimensions: a call consumes one and joins on the other
rep_of: { key: snapshot, values: { rep: snapshot } } # a map into its own dimension: the representative snapshot
connection: { key: [generator, bus] } # a bare relation, with no value columns: many-to-many, read only by sum with both ends named
gen_bt: { key: generator, values: [bus, technology] } # one table with two value columns, read to both at once
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
bp_x: { dims: [generator, bp] } # the x-axis of every curve below, and what a derived mask is read from
bp_y: { dims: [generator, bp] }
bp_heat: { dims: [generator, bp] }
bp_run: { dims: [generator, bp], dtype: bool } # how far each curve runs, so a block has a mask to print
Sets#
| Symbol | Meaning |
|---|---|
| \(\mathcal{T}\) | index \(t\) — snapshot (int coordinates) with \(\mathrm{season\_of}: \mathcal{T} \to \mathcal{S},\ \mathrm{gen\_zone}: \mathcal{G} \times \mathcal{T} \to \mathcal{Z},\ \mathrm{rep\_of}: \mathcal{T} \to \mathcal{T}\) |
| \(\mathcal{G}\) | index \(g\) — generator with \(\mathrm{gen\_bus}: \mathcal{G} \to \mathcal{B},\ \mathrm{gen\_zone}: \mathcal{G} \times \mathcal{T} \to \mathcal{Z},\ \mathrm{connection} \subseteq \mathcal{G} \times \mathcal{B},\ \mathrm{gen\_bt}: \mathcal{G} \to \mathcal{B} \times \mathcal{E}\) |
| \(\mathcal{B}\) | index \(b\) — bus with \(\mathrm{gen\_bus}: \mathcal{G} \to \mathcal{B},\ \mathrm{zone\_of}: \mathcal{B} \to \mathcal{Z},\ \mathrm{area\_of}: \mathcal{B} \to \mathcal{Z},\ \mathrm{connection} \subseteq \mathcal{G} \times \mathcal{B},\ \mathrm{gen\_bt}: \mathcal{G} \to \mathcal{B} \times \mathcal{E}\) |
| \(\mathcal{Z}\) | index \(z\) — zone with \(\mathrm{zone\_of}: \mathcal{B} \to \mathcal{Z},\ \mathrm{area\_of}: \mathcal{B} \to \mathcal{Z},\ \mathrm{gen\_zone}: \mathcal{G} \times \mathcal{T} \to \mathcal{Z}\) |
| \(\mathcal{S}\) | index \(s\) — season with \(\mathrm{season\_of}: \mathcal{T} \to \mathcal{S}\) |
| \(\mathcal{E}\) | index \(e\) — technology with \(\mathrm{gen\_bt}: \mathcal{G} \to \mathcal{B} \times \mathcal{E}\) |
| \(\mathcal{A}\) | index \(a\) — bp |
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) |
| \(\mathrm{bp\_x}\) | bp_x over \(\mathcal{G} \times \mathcal{A}\) |
| \(\mathrm{bp\_y}\) | bp_y over \(\mathcal{G} \times \mathcal{A}\) |
| \(\mathrm{bp\_heat}\) | bp_heat over \(\mathcal{G} \times \mathcal{A}\) |
| \(\mathrm{bp\_run}\) | bp_run over \(\mathcal{G} \times \mathcal{A}\) |
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{fuel}\) | fuel over \(\mathcal{T} \times \mathcal{G}\) |
| \(\mathit{heat}\) | heat over \(\mathcal{T} \times \mathcal{G}\) |
| \(\mathit{op\_cost}\) | op_cost over \(\mathcal{T} \times \mathcal{G}\) |
| \(\mathit{warm}\) | warm over \(\mathcal{T} \times \mathcal{G}\) |
Definitions#
| Symbol | Meaning |
|---|---|
| \(\mathrm{spend}^{\mathrm{cap}}\) | spend_cap over \(\mathcal{G}\) |
| \(\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}\) |
| \(\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{relation}(t)} k\) denotes a translation counted inside the group a relation puts \(t\) in (shift(by=relation)), 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{relation}(t)} k\) is both at once.
\(\mathrm{pos}(t)\) denotes where index \(t\) sits along its dimension's own order — the order shift steps along, 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{relation}(t)}(t)\) counts within the group a relation puts \(t\) in: the subscript names the map, \(\mathcal{T}_{\mathrm{relation}(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 relation
balance:
dims: [snapshot, bus]
expression: sum(p, by=gen_bus, over=generator, into=bus) + spill - slack == load
ramp#
roll (cyclic) and shift (acyclic) in one equation
ramp:
dims: [snapshot, generator]
expression: p - shift(p, along=snapshot, offset=1, edge='wrap') <= shift(p, along=snapshot, offset=1) + p_max
edges#
the two translations ramp leaves out: a fill, and forwards
edges:
dims: [snapshot, generator]
expression: >-
shift(p, along=snapshot, offset=1, edge=0)
<= shift(p, along=snapshot, offset=-1, edge=0) + p_max
ahead#
the cyclic translation forwards, which is a fourth symbol again
ahead:
dims: [snapshot, generator]
expression: p <= shift(p, along=snapshot, offset=-1, edge='wrap')
composed#
two steps of one policy are one step; a zero step is none at all
composed:
dims: [snapshot, generator]
expression: shift(shift(p, along=snapshot, offset=1), along=snapshot, offset=1) <= shift(p_max, along=generator, offset=0)
uncomposed#
a named offset under a numbered one stays two steps, not their sum
uncomposed:
dims: [snapshot, generator]
expression: shift(shift(p, along=snapshot, offset=lead, edge=0), along=snapshot, offset=1) <= p_max
crossed#
two dimensions translated at one leaf, each with its own policy
crossed:
dims: [snapshot, generator]
expression: shift(shift(p, along=snapshot, offset=1, edge='wrap'), along=generator, offset=-1) <= p_max
lead_time#
an offset the data carries, so it prints as a symbol rather than a number
lead_time:
dims: [snapshot, generator]
expression: shift(p, along=snapshot, offset=lead, edge=0) <= p_max
in_season#
a translation partitioned by a relation: the group rides on the operator
in_season:
dims: [snapshot, generator]
expression: p <= shift(p, along=snapshot, offset=1, edge='wrap', by=season_of, within=season)
held_in_season#
the same group, with a fill: each season's opening row is kept and given a zero
held_in_season:
dims: [snapshot, generator]
expression: p <= shift(p, along=snapshot, offset=1, edge=0, by=season_of, within=season)
window#
a trailing window of fixed width
history#
the same window, its width in the data and its edge wrapped
history:
dims: [snapshot, generator]
expression: sum_back(on, along=snapshot, window=min_up, edge='wrap') <= units
seasonal_window#
a window partitioned by a relation: the group rides on the operator
seasonal_window:
dims: [snapshot, generator]
expression: sum_back(on, along=snapshot, window=3, by=season_of, within=season) <= units
pullback#
at(), which re-indexes through a relation instead of an offset
grouped_once#
one table read to two value columns: the domain carries a condition per column
grouped_once:
dims: [snapshot, bus, technology]
expression: sum(p, by=gen_bt, into=[bus, technology], over=generator) <= tech_cap
pulled_back_once#
its adjoint, reading one slot through two columns of one table
pulled_back_once:
dims: [generator]
expression: units <= at(tech_cap, by=gen_bt, over=[bus, technology], into=generator)
within_bus#
a partition grouped by one named value column of a two-value table, and a position within both
within_bus:
dims: [generator]
where: "position(generator, by=gen_bt, within=[bus, technology]) == 0"
expression: units <= shift(units, along=generator, offset=1, edge=0, by=gen_bt, within=bus)
relational#
a sum through a bare relation: the domain is a row of the relation rather than a function's value
relational:
dims: [snapshot, bus]
expression: sum(p, by=connection, over=generator, into=bus) <= load
connected#
a bare relation as a where: the row of the frame has to be a member of the relation
representative#
a map into its own dimension, read both ways: the frame is unchanged and the index is primed
representative:
dims: [snapshot]
expression: sum(spill, by=rep_of, over=snapshot, into=rep) <= at(spill, by=rep_of, over=rep, into=snapshot)
zonal#
a grouping through a two-key map, consuming one key: the condition reads the other, and the row keeps it
zonal:
dims: [snapshot, zone]
expression: sum(p, by=gen_zone, over=generator, into=zone) <= zone_cap
zonal_history#
the same table consuming its other key
zonal_history:
dims: [generator, zone]
expression: sum(p, by=gen_zone, over=snapshot, into=zone) <= zone_cap
zonal_membership#
the same table read between its two key columns: no value column is read, so the domain asks only that the row is there
zonal_membership:
dims: [snapshot]
expression: sum(units, by=gen_zone, over=generator, into=snapshot) <= budget
zonal_pullback#
its adjoint, reading the slot the row's own snapshot puts the generator in
zonal_pullback:
dims: [snapshot, generator]
where: "gen_zone == 'north' AND position(generator, by=gen_zone, within=zone) == 0"
expression: p <= at(spill * zone_cap, by=gen_zone, into=generator, over=zone)
arithmetic#
division, both unary signs, a sign beside a sign, floats with and without an exponent, bracketing
arithmetic:
dims: [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:
dims: [snapshot, generator]
where: "position(snapshot) == 0 OR position(snapshot, by=season_of, within=season) == 0"
expression: on == 1
last#
the same two counted from the end, which print against a size rather than as themselves
last:
dims: [snapshot, generator]
where: "position(snapshot) == -1 OR position(snapshot, by=season_of, within=season) == -1"
expression: on == 0
northern#
a relation compared to a label, to another relation, and to nothing
northern:
dims: [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
margin#
a mask comparing two expressions, which prints as the arithmetic it is
ramped#
a translation under a comparison names its edge, a pullback reads through a relation, and the position keeps the vacated row out
ramped:
dims: [snapshot, bus]
where: "load - shift(load, along=snapshot, offset=1, edge=0) <= at(zone_cap, by=zone_of, over=zone, into=bus) AND position(snapshot) > 0"
expression: slack <= load
covered#
a reduction on a side of a scalar mask, so nothing is left to quantify
counted#
a count of the coordinates a predicate admits, which reduces one dim away
counted_here#
the same count along a dim the frame carries, so the set takes a primed dummy
counted_here:
dims: [bus, technology]
where: "count(tech_cap > 0, over=technology) >= 2"
expression: theta <= tech_cap
run_start#
a predicate read one coordinate back, which is false where the translation vacates
run_start:
dims: [snapshot, bus]
where: "load AND NOT shift(load, along=snapshot, offset=1)"
expression: slack <= load
zoned#
a predicate read through a relation: a bus is held only where its zone has a cap at all
zoned:
dims: [bus]
where: "at(zone_cap, by=zone_of, over=zone, into=bus)"
expression: theta <= budget
capped#
an expressions: entry on a side, read by the name the file gave it
capped:
dims: [snapshot, generator]
where: "spend_cap > 0 OR NOT is_flexible"
expression: p <= p_max
Definitions#
spend_cap#
a data-only entry, so a where may compare it
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
startup_cost#
a quantity defined by region: no two cases overlap, and otherwise is the rest
startup_cost:
dims: [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:
dims: [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 dims: 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
fuel#
a curve's second axis
heat#
its third, so one curve ties three expressions
op_cost#
bounded by a curve rather than pinned to it
warm#
a gate not every unit has, so the curve it gates is ungated where it does not exist
Curves#
A curve prints as the curve it states, over the frame the block builds one per coordinate of, and its expansion prints the rows that curve stands for. 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 breakpoints print as:
notation: latex
names:
economies_of_scale_lam: "\\lambda"
economies_of_scale_seg: "\\delta"
bp_x: "\\mathrm{x}"
bp_y: "\\mathrm{y}"
Written out by spec.expand():
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 breakpoints print as:
Written out by spec.expand():
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 breakpoints print as:
Written out by spec.expand():
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 breakpoints print as:
Written out by spec.expand():
Sets carried to the solver#
A set prints beside the variable it restricts, because it restricts that variable rather than adding a row of its own. Under it are the rows it is written out as.
adjacent#
at most two adjacent members nonzero, one set per snapshot
Written out by spec.expand():
What the data has to satisfy#
bounds_do_not_cross#
two parameters, which is arithmetic like any other
efficiency_is_a_fraction#
a connective, so the line has no relation to align on
lead_times_are_short#
one parameter against a literal
zones_agree#
two maps into one set, compared row by row
budget_covers_the_peak#
a reduction on a side, leaving nothing to quantify
ramps_are_gentle#
a translation inside arithmetic, and a position keeping the vacated row out
ramps_are_gentle:
holds: "load - shift(load, along=snapshot, offset=1, edge=0) <= budget"
where: "position(snapshot) > 0"
flexible_units_have_headroom#
a bare bool parameter as the where
northern_demand_is_real#
a relation comparison as the where, over a frame two dims wide