Skip to content

Every construct, as math#

This page lists each construct of the language beside the math that the typesetter prints for it. Each section has the math of one kind of construct, and the legend lists the symbol of each dimension, relation and parameter.

Each construct is printed as its YAML and then its equation. Most of the YAML comes from one test spec, tests/typesetting/golden/model.yaml, which contains every construct and is not a meaningful optimisation problem. The curves come from the example specs named in each section.

The typesetter derives each symbol from the name in the file, so the symbol is \(\mathrm{load}_{t}\) rather than \(\ell_t\). A symbol table sets other symbols, and the typesetter changes nothing else.

Legend#

A dimension, a relation, a map and a parameter declare no equation; what they print is the legend every spec opens with.

dimensions:
  snapshot: { dtype: int, ordered: true }
  generator: { dtype: str, ordered: true }
  bus: { dtype: str }
  zone: { dtype: str, one_of: [north, south] } # the labels a condition may name: the legend lists them
  season: { dtype: str }
  technology: { dtype: str }
  bp: { dtype: int, ordered: true } # the breakpoints every curve below runs through

relations:
  gen_zone: { dims: [generator, snapshot] } # a relation over two dimensions, with a map: a call sums one away and joins on the other
  connection: { dims: [generator, bus], missing: absent } # a relation with no map: many-to-many, read only by sum with both ends named
  bus_zone: { dims: [bus, zone], missing: absent } # the pairs that exist

maps:
  generator: { bus: bus, technology: technology } # two maps of one subject, read to both at once
  bus:
    zone: zone
    area: { refers_to: zone, missing: absent } # a second map into the same set, to compare against; a bus may be in no area
  snapshot:
    season: season
    rep: snapshot # a map into its own dimension: the representative snapshot
  gen_zone: { zone: zone }

parameters:
  p_max: { dims: [generator], missing: .inf } # a value: the legend says what a missing row reads as
  p_min: { dims: [generator] }
  cost: { dims: [generator], missing: 0 } # a reading: the legend names it
  load: { dims: [snapshot, bus], missing: { by: "bus[zone]", fill: { north: 0, south: absent } } } # a reading per label of a map: the legend lists each, then otherwise
  is_flexible: { dims: [generator], dtype: bool, missing: false }
  zone_cap: { dims: [zone] }
  tech_cap: { dims: [bus, technology], missing: absent }
  link_eff: { dims: [generator, bus], in: [connection], missing: 1 } # over the pairs a relation lists; a `where:` may not test them
  zone_share: { dims: [bus, zone], in: [bus_zone], missing: 1 } # over the pairs that exist: data refused outside them, with no `where:` of its own
  area_weight: { dims: [bus], where: "bus[area]" } # a map's label as a condition: the buses that have an area
  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], missing: 0 } # the x-axis of every curve below, and what a derived mask is read from
  bp_y: { dims: [generator, bp], missing: 0 }
  bp_heat: { dims: [generator, bp] }
  bp_run: { dims: [generator, bp], dtype: bool } # how far each curve runs, so a piecewise entry has a mask to print

Sets#

Symbol Meaning
\(\mathcal{B}\) index \(b\) — bp
\(\mathcal{U}\) index \(u\) — bus with \(\mathrm{area}: \mathcal{U} \to \mathcal{Z},\ \mathrm{zone}: \mathcal{U} \to \mathcal{Z},\ \mathrm{bus\_zone} \subseteq \mathcal{U} \times \mathcal{Z},\ \mathrm{connection} \subseteq \mathcal{U} \times \mathcal{G},\ \mathrm{bus}: \mathcal{G} \to \mathcal{U}\), bus_zone is absent where the data has no row, connection is absent where the data has no row, bus[area] is absent where the data gives no label
\(\mathcal{G}\) index \(g\) — generator with \(\mathrm{connection} \subseteq \mathcal{U} \times \mathcal{G},\ \mathrm{gen\_zone} \subseteq \mathcal{G} \times \mathcal{T},\ \mathrm{zone}: \mathrm{gen\_zone} \to \mathcal{Z},\ \mathrm{bus}: \mathcal{G} \to \mathcal{U},\ \mathrm{technology}: \mathcal{G} \to \mathcal{E}\), connection is absent where the data has no row
\(\mathcal{S}\) index \(s\) — season with \(\mathrm{season}: \mathcal{T} \to \mathcal{S}\)
\(\mathcal{T}\) index \(t\) — snapshot (int coordinates) with \(\mathrm{gen\_zone} \subseteq \mathcal{G} \times \mathcal{T},\ \mathrm{zone}: \mathrm{gen\_zone} \to \mathcal{Z},\ \mathrm{rep}: \mathcal{T} \to \mathcal{T},\ \mathrm{season}: \mathcal{T} \to \mathcal{S}\)
\(\mathcal{E}\) index \(e\) — technology with \(\mathrm{technology}: \mathcal{G} \to \mathcal{E}\)
\(\mathcal{Z}\) index \(z\) — zone (labels north, south) with \(\mathrm{area}: \mathcal{U} \to \mathcal{Z},\ \mathrm{zone}: \mathcal{U} \to \mathcal{Z},\ \mathrm{bus\_zone} \subseteq \mathcal{U} \times \mathcal{Z},\ \mathrm{zone}: \mathrm{gen\_zone} \to \mathcal{Z}\), bus_zone is absent where the data has no row, bus[area] is absent where the data gives no label

Parameters#

Symbol Meaning
\(\mathrm{area\_weight}\) area_weight over \(\mathcal{U}\), only where \(\mathrm{area}(u) \text{ is defined}\)
\(\mathrm{bp\_heat}\) bp_heat over \(\mathcal{B} \times \mathcal{G}\)
\(\mathrm{bp\_run}\) bp_run over \(\mathcal{B} \times \mathcal{G}\)
\(\mathrm{bp\_x}\) bp_x over \(\mathcal{B} \times \mathcal{G}\), \(0\) where the data has no row
\(\mathrm{bp\_y}\) bp_y over \(\mathcal{B} \times \mathcal{G}\), \(0\) where the data has no row
\(\mathrm{budget}\) budget (scalar)
\(\mathrm{cost}\) cost over \(\mathcal{G}\), \(0\) where the data has no row
\(\mathrm{eta}\) eta over \(\mathcal{G}\)
\(\mathrm{growth}\) growth (scalar)
\(\mathrm{is\_flexible}\) is_flexible over \(\mathcal{G}\), false where the data has no row
\(\mathrm{lead}\) lead over \(\mathcal{G}\)
\(\mathrm{link\_eff}\) link_eff over \(\left( u,\ g \right) \in \mathrm{connection}\), \(1\) where the data has no row
\(\mathrm{load}\) load over \(\mathcal{U} \times \mathcal{T}\), where the data has no row: \(0\) where bus[zone] is north, absent where bus[zone] is south, refused otherwise
\(\mathrm{min\_up}\) min_up over \(\mathcal{G}\)
\(\mathrm{p}^{\mathrm{max}}\) p_max over \(\mathcal{G}\), \(\infty\) where the data has no row
\(\mathrm{p}^{\mathrm{min}}\) p_min over \(\mathcal{G}\)
\(\mathrm{tech\_cap}\) tech_cap over \(\mathcal{U} \times \mathcal{E}\), absent where the data has no row
\(\mathrm{zone\_cap}\) zone_cap over \(\mathcal{Z}\)
\(\mathrm{zone\_share}\) zone_share over \(\left( u,\ z \right) \in \mathrm{bus\_zone}\), \(1\) where the data has no row

Conditions#

Symbol Meaning
\(\mathrm{dispatchable}\) dispatchable over \(\mathcal{G}\) — a unit that dispatches and cannot be turned down
\(\mathrm{priced\_dispatch}\) priced_dispatch over \(\mathcal{G}\)

Variables#

Symbol Meaning
\(\mathit{flow}\) flow over \(\left( u,\ g \right) \in \mathrm{connection},\ t \in \mathcal{T}\)
\(\mathit{fuel}\) fuel over \(\mathcal{G} \times \mathcal{T}\)
\(\mathit{headroom}\) headroom (scalar), \(0\) where the mask leaves it out
\(\mathit{heat}\) heat over \(\mathcal{G} \times \mathcal{T}\)
\(\mathit{on}\) on over \(\mathcal{G} \times \mathcal{T}\)
\(\mathit{op\_cost}\) op_cost over \(\mathcal{G} \times \mathcal{T}\)
\(p\) p over \(\mathcal{G} \times \mathcal{T}\)
\(\mathit{reserve}\) reserve (scalar)
\(\mathit{slack}\) slack over \(\mathcal{T}\)
\(\mathit{spare}\) spare over \(\mathcal{G}\)
\(\mathit{spill}\) spill over \(\mathcal{T}\)
\(\theta\) theta over \(\mathcal{U}\)
\(\mathit{units}\) units over \(\mathcal{G}\)
\(\mathit{warm}\) warm over \(\mathcal{G} \times \mathcal{T}\)
\(\mathit{weight}\) weight over \(\mathcal{G} \times \mathcal{T}\)

Definitions#

Symbol Meaning
\(\mathit{lcoe}\) lcoe (scalar)
\(\mathit{marginal\_price}\) marginal_price over \(\mathcal{U} \times \mathcal{T}\)
\(\mathit{net}\) net over \(\mathcal{T}\) — what a snapshot spills, less what it lacks
\(\mathrm{rating}\) rating over \(\mathcal{G} \times \mathcal{T}\)
\(\mathit{spend}\) spend over \(\mathcal{T}\) — what a snapshot's dispatch costs
\(\mathrm{spend}^{\mathrm{cap}}\) spend_cap over \(\mathcal{G}\)
\(\mathrm{startup\_cost}\) startup_cost over \(\mathcal{G} \times \mathcal{T}\) — what starting a unit in this snapshot costs, which the horizon's edge changes

Notation#

Symbol Meaning
\(t \ominus k\) edge='wrap' — \(t-k\), cyclic
\(t \boxminus_{v} k\) edge=v — \(t-k\), and \(v\) past the edge
\(t \ominus^{\mathrm{map}(t)} k\) within=subject[map] — \(t-k\) inside the group of \(t\)
\(\mathrm{pos}(t)\) position() — the position of \(t\), counted from \(0\); \(t\) is the label
\(\mathrm{pos}_{\mathrm{map}(t)}(t)\) position(within=subject[map]) — the position of \(t\) inside its group
\(\lvert \mathcal{T} \rvert\) the size of \(\mathcal{T}\); \(\lvert \mathcal{T} \rvert - 1\) is the last position

Variable domains#

Lower and upper bounds#

both bounds, and a where with all three connectives

variables:
  p:
    dims: [snapshot, generator]
    where: "p_max > 0 AND NOT is_flexible OR p_min > 0"
    bounds: { lower: p_min, upper: p_max }
\[ \mathrm{p}^{\mathrm{min}}_{g} \le p_{g,t} \le \mathrm{p}^{\mathrm{max}}_{g} \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \,:\, \mathrm{p}^{\mathrm{max}}_{g} > 0 \wedge \neg \mathrm{is\_flexible}_{g} \vee \mathrm{p}^{\mathrm{min}}_{g} > 0 \]

Lower bound only#

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

Upper bound only#

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

Unbounded variable#

variables:
  theta:
    dims: [bus]
\[ \theta_{u} \in \mathbb{R} \qquad \forall\, u \in \mathcal{U} \]

Binary domain#

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

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

Bounded integer domain#

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

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

Unbounded integer domain#

integer with neither bound: the domain is the whole line

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

Scalar variable#

an empty dims: a scalar entry, whose line carries no quantifier

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

Scalar variable with a condition#

scalar too, but masked, so the condition stands with no set beside it; 0, so the legend names the value

variables:
  headroom:
    dims: []
    where: "budget"
    missing: 0
    bounds: { lower: 0 }
\[ \mathit{headroom} \ge 0 \qquad \text{where } \mathrm{budget} \text{ is defined} \]

Variable over the rows of a relation#

a domain of the pairs connection lists, at every snapshot, and a where: inside it

variables:
  flow:
    dims: [snapshot, generator, bus]
    in: [connection]
    where: "p_max > 0"
    bounds: { lower: 0 }
\[ \mathit{flow}_{u,g,t} \ge 0 \qquad \forall\, \left( u,\ g \right) \in \mathrm{connection},\ t \in \mathcal{T} \,:\, \mathrm{p}^{\mathrm{max}}_{g} > 0 \]

Variable in a special ordered set#

the family a sos runs along

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

Second axis of a curve#

a curve's second axis

variables:
  fuel:
    dims: [snapshot, generator]
    bounds: { lower: 0 }
\[ \mathit{fuel}_{g,t} \ge 0 \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \]

Third axis of a curve#

its third, so one curve ties three expressions

variables:
  heat:
    dims: [snapshot, generator]
    bounds: { lower: 0 }
\[ \mathit{heat}_{g,t} \ge 0 \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \]

Variable bounded by a curve#

bounded by a curve rather than pinned to it

variables:
  op_cost:
    dims: [snapshot, generator]
    bounds: { lower: 0 }
\[ \mathit{op\_cost}_{g,t} \ge 0 \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \]

Binary variable with a condition#

a gate not every unit has, so the curve it gates is ungated where it does not exist

variables:
  warm:
    dims: [snapshot, generator]
    domain: binary
    where: "is_flexible"
\[ \mathit{warm}_{g,t} \in \{0, 1\} \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \,:\, \mathrm{is\_flexible}_{g} \]

Objective#

Products and powers in the 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

objective:
  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
\[ \max \sum_{g \in \mathcal{G},\ t \in \mathcal{T}} p_{g,t} \cdot \mathrm{cost}_{g} + \sum_{g \in \mathcal{G},\ t \in \mathcal{T}} p_{g,t} \cdot p_{g,t} \cdot \mathrm{cost}_{g} + \sum_{g \in \mathcal{G},\ t \in \mathcal{T}} p_{g,t} \cdot \mathrm{cost}_{g} \cdot \mathrm{growth}^{\mathrm{lead}_{g}} + \sum_{g \in \mathcal{G},\ t \in \mathcal{T}} p_{g,t} \cdot \left( \mathrm{growth}^{\mathrm{lead}_{g}} \right)^{2} + \sum_{g \in \mathcal{G},\ t \in \mathcal{T}} p_{g,t} \cdot \mathrm{p}^{\mathrm{max}}_{g} - \mathit{reserve} - \mathit{headroom} \]

Relations and maps#

Sum through a map#

constraints:
  balance:
    dims: [snapshot, bus]
    expression: sum(p, over=generator, by=generator[bus]) + spill - slack == load
\[ \sum_{g \in \mathcal{G} \,:\, \mathrm{bus}(g) = u} p_{g,t} + \mathit{spill}_{t} - \mathit{slack}_{t} = \mathrm{load}_{u,t} \qquad \forall\, u \in \mathcal{U},\ t \in \mathcal{T} \]

Sum over every dimension#

a sum naming no dim, whose domain is the one place the dims it took are said

constraints:
  total:
    dims: []
    expression: sum(p) <= budget
\[ \sum_{g \in \mathcal{G},\ t \in \mathcal{T}} p_{g,t} \le \mathrm{budget} \]

at through a map#

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

constraints:
  lookup:
    dims: [snapshot, bus]
    expression: spill <= at(zone_cap, by=bus[zone])
\[ \mathit{spill}_{t} \le \mathrm{zone\_cap}_{\mathrm{zone}(u)} \qquad \forall\, u \in \mathcal{U},\ t \in \mathcal{T} \]

Sum into two maps#

one table read to two value columns: the domain carries a condition per column

constraints:
  grouped_once:
    dims: [snapshot, bus, technology]
    expression: sum(p, over=generator, by=generator[bus, technology]) <= tech_cap
\[ \sum_{g \in \mathcal{G} \,:\, \mathrm{bus}(g) = u \wedge \mathrm{technology}(g) = e} p_{g,t} \le \mathrm{tech\_cap}_{u,e} \qquad \forall\, u \in \mathcal{U},\ t \in \mathcal{T},\ e \in \mathcal{E} \]

at through two maps#

the same table joined the other way, reading one slot through two columns

constraints:
  looked_up_once:
    dims: [generator]
    expression: units <= at(tech_cap, by=generator[bus, technology])
\[ \mathit{units}_{g} \le \mathrm{tech\_cap}_{\mathrm{bus}(g),\mathrm{technology}(g)} \qquad \forall\, g \in \mathcal{G} \]

Shift within one map#

a partition grouped by one named value column of a two-value table, and a position within both

constraints:
  within_bus:
    dims: [generator]
    where: "position(generator, within=generator[bus, technology]) == 0"
    expression: units <= shift(units, along=generator, offset=1, edge=0, within=generator[bus])
\[ \mathit{units}_{g} \le \mathit{units}_{g \boxminus_{0}^{\mathrm{bus}(g)} 1} \qquad \forall\, g \in \mathcal{G} \,:\, \mathrm{pos}_{\left( \mathrm{bus}(g),\ \mathrm{technology}(g) \right)}(g) = 0 \]

Sum through a relation#

a sum through a relation with no map: the domain is a row of the relation rather than a label

constraints:
  relational:
    dims: [snapshot, bus]
    expression: sum(p, over=generator, by=connection[bus]) <= load
\[ \sum_{g \in \mathcal{G} \,:\, \left( u,\ g \right) \in \mathrm{connection}} p_{g,t} \le \mathrm{load}_{u,t} \qquad \forall\, u \in \mathcal{U},\ t \in \mathcal{T} \]

Relation as a condition#

a relation as a where: the row of the frame has to be a member of the relation

constraints:
  connected:
    dims: [snapshot, generator, bus]
    where: "connection"
    expression: p <= load
\[ p_{g,t} \le \mathrm{load}_{u,t} \qquad \forall\, u \in \mathcal{U},\ g \in \mathcal{G},\ t \in \mathcal{T} \,:\, \left( u,\ g \right) \in \mathrm{connection} \]

Map into its own dimension#

a map into its own dimension, read both ways: the frame is unchanged and the index is primed

constraints:
  representative:
    dims: [snapshot]
    expression: sum(spill, over=snapshot, by=snapshot[rep]) <= at(spill, by=snapshot[rep])
\[ \sum_{t' \in \mathcal{T} \,:\, \mathrm{rep}(t') = t} \mathit{spill}_{t'} \le \mathit{spill}_{\mathrm{rep}(t)} \qquad \forall\, t \in \mathcal{T} \]

Sum through a map of a relation#

a grouping through a two-key map, summing one key away: the condition reads the other, and the row keeps it

constraints:
  zonal:
    dims: [snapshot, zone]
    expression: sum(p, over=generator, by=gen_zone[zone]) <= zone_cap
\[ \sum_{g \in \mathcal{G} \,:\, \mathrm{zone}(g,\ t) = z} p_{g,t} \le \mathrm{zone\_cap}_{z} \qquad \forall\, t \in \mathcal{T},\ z \in \mathcal{Z} \]

Sum over the other column of a relation#

the same table summing its other key away

constraints:
  zonal_history:
    dims: [generator, zone]
    expression: sum(p, over=snapshot, by=gen_zone[zone]) <= zone_cap
\[ \sum_{t \in \mathcal{T} \,:\, \mathrm{zone}(g,\ t) = z} p_{g,t} \le \mathrm{zone\_cap}_{z} \qquad \forall\, g \in \mathcal{G},\ z \in \mathcal{Z} \]

Sum between the two columns of a relation#

the same relation read between its own two columns: no map is read, so the domain asks only that the row is there

constraints:
  zonal_membership:
    dims: [snapshot]
    expression: sum(units, over=generator, by=gen_zone[snapshot]) <= budget
\[ \sum_{g \in \mathcal{G} \,:\, \left( g,\ t \right) \in \mathrm{gen\_zone}} \mathit{units}_{g} \le \mathrm{budget} \qquad \forall\, t \in \mathcal{T} \]

at through a map of a relation#

the same table joined the other way, reading the slot the row's own snapshot puts the generator in

constraints:
  zonal_lookup:
    dims: [snapshot, generator]
    where: "gen_zone[zone] == 'north' AND position(generator, within=gen_zone[zone]) == 0"
    expression: p <= at(spill * zone_cap, by=gen_zone[zone])
\[ p_{g,t} \le \mathit{spill}_{t} \cdot \mathrm{zone\_cap}_{\mathrm{zone}(g,\ t)} \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \,:\, \mathrm{zone}(g,\ t) = \text{'}\mathrm{north}\text{'} \wedge \mathrm{pos}_{\mathrm{zone}(g,\ t)}(g) = 0 \]

Arithmetic and literals#

Signs, division and number literals#

division, both unary signs, a sign beside a sign, floats with and without an exponent, bracketing

constraints:
  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
\[ \sum_{g \in \mathcal{G}} \left( \frac{p_{g,t}}{2} - \mathrm{cost}_{g} + 10^{-5} \cdot p_{g,t} + 2.5 \times 10^{-7} \cdot \mathrm{cost}_{g} + 0.5 \cdot p_{g,t} \right) \ge -\left( \sum_{g \in \mathcal{G}} p_{g,t} \right) \cdot \left( -3 \right) \qquad \forall\, t \in \mathcal{T} \]

Greek parameter name#

a Greek-named parameter, which is given — so the convention wins and it prints as the word

constraints:
  efficiency:
    dims: [snapshot, generator]
    expression: p <= eta * p_max
\[ p_{g,t} \le \mathrm{eta}_{g} \cdot \mathrm{p}^{\mathrm{max}}_{g} \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \]

Infinity literal#

the infinity literal, which is the one way infinity prints

constraints:
  ceiling:
    dims: [bus]
    expression: theta <= inf
\[ \theta_{u} \le \infty \qquad \forall\, u \in \mathcal{U} \]

Named expressions#

Plain expression in a constraint#

names the plain expression: its symbol prints here, its definition once below

constraints:
  budgeted:
    dims: [snapshot]
    expression: spend <= budget
\[ \mathit{spend}_{t} \le \mathrm{budget} \qquad \forall\, t \in \mathcal{T} \]

Signed sum substituted into a plus#

names the signed sum on the right of a plus: inlined, its minus prints as a subtraction

constraints:
  netted:
    dims: [snapshot, bus]
    expression: sum(p, over=generator, by=generator[bus]) + net == load
\[ \sum_{g \in \mathcal{G} \,:\, \mathrm{bus}(g) = u} p_{g,t} + \mathit{net}_{t} = \mathrm{load}_{u,t} \qquad \forall\, u \in \mathcal{U},\ t \in \mathcal{T} \]

Cased expression in a constraint#

names the cased expression: its symbol prints here, its block once below

constraints:
  starts:
    dims: [snapshot, generator]
    expression: p <= startup_cost
\[ p_{g,t} \le \mathrm{startup\_cost}_{g,t} \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \]

Expression with a declared frame in a constraint#

names the expression with declared dims, read in the row's own order

constraints:
  under_rating:
    dims: [snapshot, generator]
    expression: p <= rating
\[ p_{g,t} \le \mathrm{rating}_{g,t} \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \]

Plain named expression#

a plain named expression: its symbol prints where it is used, its body once as a definition

expressions:
  spend:
    expression: sum(p * cost, over=generator)
\[ \mathit{spend}_{t} = \sum_{g \in \mathcal{G}} p_{g,t} \cdot \mathrm{cost}_{g} \qquad \forall\, t \in \mathcal{T} \]

Named expression with a declared frame#

dims declared, each one the expression is indexed over

expressions:
  rating:
    dims: [generator, snapshot]
    expression: eta * p_max * sum(load, over=bus)
\[ \mathrm{rating}_{g,t} = \mathrm{eta}_{g} \cdot \mathrm{p}^{\mathrm{max}}_{g} \cdot \left( \sum_{u \in \mathcal{U}} \mathrm{load}_{u,t} \right) \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \]

Plain expression that is a signed sum#

a plain expression that opens with a minus

expressions:
  net:
    expression: -spill + slack
\[ \mathit{net}_{t} = -\mathit{spill}_{t} + \mathit{slack}_{t} \qquad \forall\, t \in \mathcal{T} \]

Expression defined by cases#

a quantity defined by region: no two cases overlap, and otherwise is the rest

expressions:
  startup_cost:
    dims: [snapshot, generator]
    cases:
      opening: { when: "position(snapshot) == 0", expression: cost }
      winter: { when: "position(snapshot) > 0 and snapshot[season] == 'winter'", expression: cost * 2 }
    otherwise: 0
\[ \mathrm{startup\_cost}_{g,t} = \begin{cases} \mathrm{cost}_{g} & \text{if } \mathrm{pos}(t) = 0 \\ \mathrm{cost}_{g} \cdot 2 & \text{if } \mathrm{pos}(t) > 0 \wedge \mathrm{season}(t) = \text{'}\mathrm{winter}\text{'} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \]

Data-only expression#

a data-only entry, so a where may compare it

expressions:
  spend_cap: cost * 2
\[ \mathrm{spend}^{\mathrm{cap}}_{g} = \mathrm{cost}_{g} \cdot 2 \qquad \forall\, g \in \mathcal{G} \]

Named expression in a condition#

an expressions: entry on a side, read by the name the file gave it

constraints:
  capped:
    dims: [snapshot, generator]
    where: "spend_cap > 0 OR NOT is_flexible"
    expression: p <= p_max
\[ p_{g,t} \le \mathrm{p}^{\mathrm{max}}_{g} \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \,:\, \mathrm{spend}^{\mathrm{cap}}_{g} > 0 \vee \neg \mathrm{is\_flexible}_{g} \]

Reported expression#

nothing in the math reads it, so its divisor may carry a variable

expressions:
  lcoe: sum(p * cost) / sum(p)
\[ \mathit{lcoe} = \frac{\sum_{g \in \mathcal{G},\ t \in \mathcal{T}} p_{g,t} \cdot \mathrm{cost}_{g}}{\sum_{g \in \mathcal{G},\ t \in \mathcal{T}} p_{g,t}} \]

Dual of a constraint#

the row dual of a constraint, the one builtin only an entry the math never reads may call

expressions:
  marginal_price: dual(balance)
\[ \mathit{marginal\_price}_{u,t} = \lambda_{\mathrm{balance},u,t} \qquad \forall\, u \in \mathcal{U},\ t \in \mathcal{T} \]

Named conditions#

Named condition#

a named condition with its words: a use prints the symbol over the dims its condition reads, the condition prints once

conditions:
  dispatchable:
    condition: "p_max > 0 AND NOT is_flexible"
\[ \mathrm{dispatchable}_{g} \iff \mathrm{p}^{\mathrm{max}}_{g} > 0 \wedge \neg \mathrm{is\_flexible}_{g} \qquad \forall\, g \in \mathcal{G} \]

Condition that reads a condition#

the one-line form, reading another mask and a data-only entry

conditions:
  priced_dispatch: "dispatchable AND spend_cap > 0"
\[ \mathrm{priced\_dispatch}_{g} \iff \mathrm{dispatchable}_{g} \wedge \mathrm{spend}^{\mathrm{cap}}_{g} > 0 \qquad \forall\, g \in \mathcal{G} \]

Named condition in a condition#

a mask as the where, beside a condition of the constraint's own

constraints:
  masked:
    dims: [snapshot, generator]
    where: "priced_dispatch AND position(snapshot) > 0"
    expression: p <= p_max
\[ p_{g,t} \le \mathrm{p}^{\mathrm{max}}_{g} \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \,:\, \mathrm{priced\_dispatch}_{g} \wedge \mathrm{pos}(t) > 0 \]

Shifts#

Cyclic and acyclic shift#

roll (cyclic) and shift (acyclic) in one equation

constraints:
  ramp:
    dims: [snapshot, generator]
    expression: p - shift(p, along=snapshot, offset=1, edge='wrap') <= shift(p, along=snapshot, offset=1) + p_max
\[ p_{g,t} - p_{g,t \ominus 1} \le p_{g,t - 1} + \mathrm{p}^{\mathrm{max}}_{g} \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \]

Filled and forward shifts#

the two translations ramp leaves out: a fill, and forwards

constraints:
  edges:
    dims: [snapshot, generator]
    expression: >-
      shift(p, along=snapshot, offset=1, edge=0)
      <= shift(p, along=snapshot, offset=-1, edge=0) + p_max
\[ p_{g,t \boxminus_{0} 1} \le p_{g,t \boxplus_{0} 1} + \mathrm{p}^{\mathrm{max}}_{g} \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \]

Cyclic forward shift#

the cyclic translation forwards, which is a fourth symbol again

constraints:
  ahead:
    dims: [snapshot, generator]
    expression: p <= shift(p, along=snapshot, offset=-1, edge='wrap')
\[ p_{g,t} \le p_{g,t \oplus 1} \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \]

Composed shifts#

two steps of one policy are one step; a zero step is none at all

constraints:
  composed:
    dims: [snapshot, generator]
    expression: shift(shift(p, along=snapshot, offset=1), along=snapshot, offset=1) <= shift(p_max, along=generator, offset=0)
\[ p_{g,t - 2} \le \mathrm{p}^{\mathrm{max}}_{g} \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \]

Nested shifts that do not compose#

a named offset under a numbered one stays two steps, not their sum

constraints:
  uncomposed:
    dims: [snapshot, generator]
    expression: shift(shift(p, along=snapshot, offset=lead, edge=0), along=snapshot, offset=1) <= p_max
\[ p_{g,\left( t - 1 \right) \boxminus_{0} \mathrm{lead}} \le \mathrm{p}^{\mathrm{max}}_{g} \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \]

Shift along two dimensions#

two dimensions translated at one leaf, each with its own policy

constraints:
  crossed:
    dims: [snapshot, generator]
    expression: shift(shift(p, along=snapshot, offset=1, edge='wrap'), along=generator, offset=-1) <= p_max
\[ p_{g + 1,t \ominus 1} \le \mathrm{p}^{\mathrm{max}}_{g} \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \]

Shift by a parameter offset#

an offset the data carries, so it prints as a symbol rather than a number

constraints:
  lead_time:
    dims: [snapshot, generator]
    expression: shift(p, along=snapshot, offset=lead, edge=0) <= p_max
\[ p_{g,t \boxminus_{0} \mathrm{lead}} \le \mathrm{p}^{\mathrm{max}}_{g} \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \]

Cyclic shift within a group#

a translation partitioned by a map: the group rides on the operator

constraints:
  in_season:
    dims: [snapshot, generator]
    expression: p <= shift(p, along=snapshot, offset=1, edge='wrap', within=snapshot[season])
\[ p_{g,t} \le p_{g,t \ominus^{\mathrm{season}(t)} 1} \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \]

Filled shift within a group#

the same group, with a fill: each season's opening row is kept and given a zero

constraints:
  held_in_season:
    dims: [snapshot, generator]
    expression: p <= shift(p, along=snapshot, offset=1, edge=0, within=snapshot[season])
\[ p_{g,t} \le p_{g,t \boxminus_{0}^{\mathrm{season}(t)} 1} \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \]

Trailing windows#

Window of fixed width#

a trailing window of fixed width

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

Window with a parameter width#

the same window, its width in the data and its edge wrapped

constraints:
  history:
    dims: [snapshot, generator]
    expression: sum_back(on, along=snapshot, window=min_up, edge='wrap') <= units
\[ \sum_{t' \in \mathcal{T} \,:\, 0 \le t \ominus t' < \mathrm{min\_up}} \mathit{on}_{g,t'} \le \mathit{units}_{g} \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \]

Window within a group#

a window partitioned by a map: the group rides on the operator

constraints:
  seasonal_window:
    dims: [snapshot, generator]
    expression: sum_back(on, along=snapshot, window=3, within=snapshot[season]) <= units
\[ \sum_{t' \in \mathcal{T} \,:\, 0 \le t -^{\mathrm{season}(t)} t' < 3} \mathit{on}_{g,t'} \le \mathit{units}_{g} \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \]

Piecewise curves#

A curve prints as the curve it states, over the frame the entry builds one per coordinate of, and its expansion prints the rows that curve stands for. One row per method:, each from the spec named under it, so the symbols in this section are that spec's.

Adjacency method#

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}"
piecewise:
  economies_of_scale:
    along: bp
    dims: [plant, market]
    links:
      shipment: [shipment, bp_x]
      scaled: [scaled, bp_y]
\[ \left( \mathit{shipment}_{m,p},\ \mathit{scaled}_{m,p} \right) \in \mathrm{pwl}_{b \in \mathcal{B}}(\mathrm{x}_{b},\ \mathrm{y}_{b}) \qquad \forall\, m \in \mathcal{M},\ p \in \mathcal{P} \]

Written out by spec.expand():

\[ 0 \le \lambda_{b,m,p} \le 1 \qquad \forall\, b \in \mathcal{B},\ m \in \mathcal{M},\ p \in \mathcal{P} \]
\[ \delta_{b,m,p} \in \{0, 1\} \qquad \forall\, b \in \mathcal{B},\ m \in \mathcal{M},\ p \in \mathcal{P} \]
\[ \lambda_{b,m,p} \le \delta_{b,m,p} + \delta_{b \boxminus_{0} 1,m,p} \qquad \forall\, b \in \mathcal{B},\ m \in \mathcal{M},\ p \in \mathcal{P} \]
\[ \sum_{b \in \mathcal{B}} \lambda_{b,m,p} = 1 \qquad \forall\, m \in \mathcal{M},\ p \in \mathcal{P} \]
\[ \sum_{b \in \mathcal{B}} \delta_{b,m,p} \le 1 \qquad \forall\, m \in \mathcal{M},\ p \in \mathcal{P} \]
\[ \mathit{scaled}_{m,p} = \sum_{b \in \mathcal{B}} \lambda_{b,m,p} \cdot \mathrm{y}_{b} \qquad \forall\, m \in \mathcal{M},\ p \in \mathcal{P} \]
\[ \mathit{shipment}_{m,p} = \sum_{b \in \mathcal{B}} \lambda_{b,m,p} \cdot \mathrm{x}_{b} \qquad \forall\, m \in \mathcal{M},\ p \in \mathcal{P} \]
\[ \mathrm{x}_{b} \text{ is defined} \wedge \mathrm{y}_{b} \text{ is defined} \qquad \forall\, b \in \mathcal{B} \]

SOS2 method#

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:

notation: latex

names:
  cost_curve_lam: "\\lambda"
  bp_x: "\\mathrm{x}"
  bp_y: "\\mathrm{y}"
piecewise:
  cost_curve:
    along: bp
    dims: [snapshot, generator]
    links:
      dispatch: [dispatch, bp_x]
      op_cost: [op_cost, bp_y]
    method: sos2
\[ \left( \mathit{dispatch}_{g,t},\ \mathit{op\_cost}_{g,t} \right) \in \mathrm{pwl}_{b \in \mathcal{B}}(\mathrm{x}_{b,g},\ \mathrm{y}_{b,g}) \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \]

Written out by spec.expand():

\[ 0 \le \lambda_{b,g,t} \le 1 \qquad \forall\, b \in \mathcal{B},\ g \in \mathcal{G},\ t \in \mathcal{T} \]
\[ \sum_{b \in \mathcal{B}} \lambda_{b,g,t} = 1 \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \]
\[ \mathit{dispatch}_{g,t} = \sum_{b \in \mathcal{B}} \lambda_{b,g,t} \cdot \mathrm{x}_{b,g} \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \]
\[ \mathit{op\_cost}_{g,t} = \sum_{b \in \mathcal{B}} \lambda_{b,g,t} \cdot \mathrm{y}_{b,g} \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \]
\[ \left( \lambda_{b,g,t} \right)_{b \in \mathcal{B}} \in \mathrm{SOS}2 \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \]
\[ \mathrm{x}_{b,g} \text{ is defined} \wedge \mathrm{y}_{b,g} \text{ is defined} \qquad \forall\, b \in \mathcal{B},\ g \in \mathcal{G} \]

Incremental method#

method: incremental — a filled fraction per segment, each filled only once the one before it is full, ordered by a binary, in examples/piecewise_incremental.yaml.

piecewise:
  cost_curve:
    along: bp
    dims: [snapshot, generator]
    where: on_curve
    method: incremental
    activity: status
    links:
      p: [p, p_bp]
      cost: [cost, cost_bp, ">="]
\[ \mathit{cost}_{g,t} \ge \begin{cases} \mathit{status}_{g,t} & \text{if } \mathrm{committable}_{g} \\ 1 & \text{otherwise} \end{cases} \cdot \mathrm{pwl}_{b \in \mathcal{B} \,:\, \mathrm{on\_curve}_{b,g}}(\mathrm{p}^{\mathrm{bp}}_{b,g},\ \mathrm{cost}^{\mathrm{bp}}_{b,g})(p_{g,t}) \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \]

Written out by spec.expand():

\[ 0 \le \mathit{cost}^{\mathrm{curve,delta}}_{b,g,t} \le 1 \qquad \forall\, b \in \mathcal{B},\ g \in \mathcal{G},\ t \in \mathcal{T} \,:\, \mathrm{on\_curve}_{b,g} \wedge \mathrm{on\_curve}_{b + 1,g} \]
\[ \mathit{cost}^{\mathrm{curve,order}}_{b,g,t} \in \{0, 1\} \qquad \forall\, b \in \mathcal{B},\ g \in \mathcal{G},\ t \in \mathcal{T} \,:\, \mathrm{on\_curve}_{b,g} \wedge \mathrm{on\_curve}_{b + 1,g} \]
\[ \mathit{cost}^{\mathrm{curve,delta}}_{b,g,t} \le \mathit{status}_{g,t} \qquad \forall\, b \in \mathcal{B},\ g \in \mathcal{G},\ t \in \mathcal{T} \]
\[ \mathit{cost}^{\mathrm{curve,order}}_{b,g,t} \le \mathit{cost}^{\mathrm{curve,delta}}_{b - 1,g,t} \qquad \forall\, b \in \mathcal{B},\ g \in \mathcal{G},\ t \in \mathcal{T} \]
\[ \mathit{cost}_{g,t} \ge \mathrm{cost}^{\mathrm{bp}}_{b,g} \cdot \mathit{status}_{g,t} + \sum_{b' \in \mathcal{B}} \mathit{cost}^{\mathrm{curve,delta}}_{b',g,t} \cdot \left( \mathrm{cost}^{\mathrm{bp}}_{b' \boxplus_{0} 1,g} - \mathrm{cost}^{\mathrm{bp}}_{b',g} \right) \qquad \forall\, b \in \mathcal{B},\ g \in \mathcal{G},\ t \in \mathcal{T} \,:\, \mathrm{on\_curve}_{b,g} \wedge \neg \mathrm{on\_curve}_{b - 1,g} \wedge \mathit{status}_{g,t} \text{ exists} \]
\[ \mathit{cost}_{g,t} \ge \mathrm{cost}^{\mathrm{bp}}_{b,g} + \sum_{b' \in \mathcal{B}} \mathit{cost}^{\mathrm{curve,delta}}_{b',g,t} \cdot \left( \mathrm{cost}^{\mathrm{bp}}_{b' \boxplus_{0} 1,g} - \mathrm{cost}^{\mathrm{bp}}_{b',g} \right) \qquad \forall\, b \in \mathcal{B},\ g \in \mathcal{G},\ t \in \mathcal{T} \,:\, \mathrm{on\_curve}_{b,g} \wedge \neg \mathrm{on\_curve}_{b - 1,g} \wedge \neg \left( \mathit{status}_{g,t} \text{ exists} \right) \]
\[ \mathit{cost}^{\mathrm{curve,delta}}_{b,g,t} \le \mathit{cost}^{\mathrm{curve,order}}_{b,g,t} \qquad \forall\, b \in \mathcal{B},\ g \in \mathcal{G},\ t \in \mathcal{T} \]
\[ \mathit{cost}^{\mathrm{curve,delta}}_{b,g,t} \le \mathit{cost}^{\mathrm{curve,delta}}_{b - 1,g,t} \qquad \forall\, b \in \mathcal{B},\ g \in \mathcal{G},\ t \in \mathcal{T} \]
\[ p_{g,t} = \mathrm{p}^{\mathrm{bp}}_{b,g} \cdot \mathit{status}_{g,t} + \sum_{b' \in \mathcal{B}} \mathit{cost}^{\mathrm{curve,delta}}_{b',g,t} \cdot \left( \mathrm{p}^{\mathrm{bp}}_{b' \boxplus_{0} 1,g} - \mathrm{p}^{\mathrm{bp}}_{b',g} \right) \qquad \forall\, b \in \mathcal{B},\ g \in \mathcal{G},\ t \in \mathcal{T} \,:\, \mathrm{on\_curve}_{b,g} \wedge \neg \mathrm{on\_curve}_{b - 1,g} \wedge \mathit{status}_{g,t} \text{ exists} \]
\[ p_{g,t} = \mathrm{p}^{\mathrm{bp}}_{b,g} + \sum_{b' \in \mathcal{B}} \mathit{cost}^{\mathrm{curve,delta}}_{b',g,t} \cdot \left( \mathrm{p}^{\mathrm{bp}}_{b' \boxplus_{0} 1,g} - \mathrm{p}^{\mathrm{bp}}_{b',g} \right) \qquad \forall\, b \in \mathcal{B},\ g \in \mathcal{G},\ t \in \mathcal{T} \,:\, \mathrm{on\_curve}_{b,g} \wedge \neg \mathrm{on\_curve}_{b - 1,g} \wedge \neg \left( \mathit{status}_{g,t} \text{ exists} \right) \]
\[ \mathrm{p}^{\mathrm{bp}}_{b,g} \text{ is defined} \wedge \mathrm{cost}^{\mathrm{bp}}_{b,g} \text{ is defined} \qquad \forall\, b \in \mathcal{B},\ g \in \mathcal{G} \,:\, \mathrm{on\_curve}_{b,g} \]
\[ \lvert \{ b \in \mathcal{B} \,:\, \mathrm{on\_curve}_{b,g} \wedge \neg \mathrm{on\_curve}_{b - 1,g} \} \rvert = 1 \qquad \forall\, g \in \mathcal{G} \,:\, \lvert \{ b \in \mathcal{B} \,:\, \mathrm{on\_curve}_{b,g} \} \rvert > 0 \]

Convex method#

method: convex — nothing, so 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:

notation: latex

names:
  cost_curve_lam: "\\lambda"
  bp_x: "\\mathrm{x}"
  bp_y: "\\mathrm{y}"
piecewise:
  cost_curve:
    along: bp
    dims: [snapshot, generator]
    links:
      dispatch: [dispatch, bp_x]
      op_cost: [op_cost, bp_y]
    method: convex
\[ \left( \mathit{dispatch}_{g,t},\ \mathit{op\_cost}_{g,t} \right) \in \mathrm{conv}_{b \in \mathcal{B}}(\mathrm{x}_{b,g},\ \mathrm{y}_{b,g}) \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \]

Written out by spec.expand():

\[ 0 \le \lambda_{b,g,t} \le 1 \qquad \forall\, b \in \mathcal{B},\ g \in \mathcal{G},\ t \in \mathcal{T} \]
\[ \sum_{b \in \mathcal{B}} \lambda_{b,g,t} = 1 \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \]
\[ \mathit{dispatch}_{g,t} = \sum_{b \in \mathcal{B}} \lambda_{b,g,t} \cdot \mathrm{x}_{b,g} \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \]
\[ \mathit{op\_cost}_{g,t} = \sum_{b \in \mathcal{B}} \lambda_{b,g,t} \cdot \mathrm{y}_{b,g} \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \]
\[ \mathrm{x}_{b,g} \text{ is defined} \wedge \mathrm{y}_{b,g} \text{ is defined} \qquad \forall\, b \in \mathcal{B},\ g \in \mathcal{G} \]
\[ \lvert \{ b \in \mathcal{B} \,:\, \left( \mathrm{y}_{b,g} - \mathrm{y}_{b \boxminus_{0} 1,g} \right) \cdot \left( \mathrm{x}_{b \boxplus_{0} 1,g} - \mathrm{x}_{b,g} \right) > \left( \mathrm{y}_{b \boxplus_{0} 1,g} - \mathrm{y}_{b,g} \right) \cdot \left( \mathrm{x}_{b,g} - \mathrm{x}_{b \boxminus_{0} 1,g} \right) \wedge \mathrm{pos}(b) > 0 \wedge \mathrm{pos}(b) \neq \lvert \mathcal{B} \rvert - 1 \} \rvert = 0 \vee \lvert \{ b \in \mathcal{B} \,:\, \left( \mathrm{y}_{b,g} - \mathrm{y}_{b \boxminus_{0} 1,g} \right) \cdot \left( \mathrm{x}_{b \boxplus_{0} 1,g} - \mathrm{x}_{b,g} \right) < \left( \mathrm{y}_{b \boxplus_{0} 1,g} - \mathrm{y}_{b,g} \right) \cdot \left( \mathrm{x}_{b,g} - \mathrm{x}_{b \boxminus_{0} 1,g} \right) \wedge \mathrm{pos}(b) > 0 \wedge \mathrm{pos}(b) \neq \lvert \mathcal{B} \rvert - 1 \} \rvert = 0 \qquad \forall\, g \in \mathcal{G} \]
\[ \mathrm{x}_{b \boxminus_{0} 1,g} < \mathrm{x}_{b,g} \qquad \forall\, b \in \mathcal{B},\ g \in \mathcal{G} \,:\, \mathrm{pos}(b) > 0 \]

LP method#

method: lp — no weights, but 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:

notation: latex

names:
  bp_x: "\\mathrm{x}"
  bp_y: "\\mathrm{y}"
piecewise:
  cost_curve:
    along: bp
    dims: [snapshot, generator]
    links:
      dispatch: [dispatch, bp_x]
      op_cost: [op_cost, bp_y, ">="]
    method: lp
\[ \mathit{op\_cost}_{g,t} \ge \mathrm{pwl}_{b \in \mathcal{B}}(\mathrm{x}_{b,g},\ \mathrm{y}_{b,g})(\mathit{dispatch}_{g,t}) \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \]

Written out by spec.expand():

\[ \mathit{op\_cost}_{g,t} \cdot \left( \mathrm{x}_{b,g} - \mathrm{x}_{b \boxminus_{0} 1,g} \right) \ge \left( \mathrm{y}_{b,g} - \mathrm{y}_{b \boxminus_{0} 1,g} \right) \cdot \left( \mathit{dispatch}_{g,t} - \mathrm{x}_{b,g} \right) + \mathrm{y}_{b,g} \cdot \left( \mathrm{x}_{b,g} - \mathrm{x}_{b \boxminus_{0} 1,g} \right) \qquad \forall\, b \in \mathcal{B},\ g \in \mathcal{G},\ t \in \mathcal{T} \,:\, \mathrm{pos}(b) > 0 \]
\[ \mathit{dispatch}_{g,t} \le \mathrm{x}_{b,g} \qquad \forall\, b \in \mathcal{B},\ g \in \mathcal{G},\ t \in \mathcal{T} \,:\, \mathrm{pos}(b) = \lvert \mathcal{B} \rvert - 1 \]
\[ \mathit{dispatch}_{g,t} \ge \mathrm{x}_{b,g} \qquad \forall\, b \in \mathcal{B},\ g \in \mathcal{G},\ t \in \mathcal{T} \,:\, \mathrm{pos}(b) = 0 \]
\[ \lvert \{ b \in \mathcal{B} \,:\, \mathrm{x}_{b,g} \text{ is defined} \} \rvert \ge 2 \qquad \forall\, g \in \mathcal{G} \]
\[ \mathrm{x}_{b,g} \text{ is defined} \wedge \mathrm{y}_{b,g} \text{ is defined} \qquad \forall\, b \in \mathcal{B},\ g \in \mathcal{G} \]
\[ \left( \mathrm{y}_{b,g} - \mathrm{y}_{b \boxminus_{0} 1,g} \right) \cdot \left( \mathrm{x}_{b \boxplus_{0} 1,g} - \mathrm{x}_{b,g} \right) \le \left( \mathrm{y}_{b \boxplus_{0} 1,g} - \mathrm{y}_{b,g} \right) \cdot \left( \mathrm{x}_{b,g} - \mathrm{x}_{b \boxminus_{0} 1,g} \right) \qquad \forall\, b \in \mathcal{B},\ g \in \mathcal{G} \,:\, \mathrm{pos}(b) > 0 \wedge \mathrm{pos}(b) \neq \lvert \mathcal{B} \rvert - 1 \]
\[ \mathrm{x}_{b \boxminus_{0} 1,g} < \mathrm{x}_{b,g} \qquad \forall\, b \in \mathcal{B},\ g \in \mathcal{G} \,:\, \mathrm{pos}(b) > 0 \]

Special ordered sets#

A set prints under Subject to, after the constraints and before the curves, because a solver holds it as a constraint. Under it are the rows it is written out as.

Special ordered set of type 2#

at most two adjacent members nonzero, one set per snapshot

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

Written out by spec.expand():

\[ \mathit{adjacent\_seg}_{g,t} \in \{0, 1\} \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \]
\[ \mathit{weight}_{g,t} \le \mathit{adjacent\_seg}_{g,t} + \mathit{adjacent\_seg}_{g \boxminus_{0} 1,t} \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \]
\[ \sum_{g \in \mathcal{G}} \mathit{adjacent\_seg}_{g,t} \le 1 \qquad \forall\, t \in \mathcal{T} \]

Assumptions#

Two parameters compared#

two parameters, which is arithmetic like any other

assumptions:
  bounds_do_not_cross: "p_min <= p_max"
\[ \mathrm{p}^{\mathrm{min}}_{g} \le \mathrm{p}^{\mathrm{max}}_{g} \qquad \forall\, g \in \mathcal{G} \]

Connective in an assumption#

a connective, so the line has no relation to align on

assumptions:
  efficiency_is_a_fraction: "eta > 0 AND eta <= 1"
\[ \mathrm{eta}_{g} > 0 \wedge \mathrm{eta}_{g} \le 1 \qquad \forall\, g \in \mathcal{G} \]

Parameter compared to a literal#

one parameter against a literal

assumptions:
  lead_times_are_short: "lead <= 3"
\[ \mathrm{lead}_{g} \le 3 \qquad \forall\, g \in \mathcal{G} \]

Two maps compared#

two maps into one set, compared row by row

assumptions:
  zones_agree: "bus[zone] == bus[area]"
\[ \mathrm{zone}(u) = \mathrm{area}(u) \qquad \forall\, u \in \mathcal{U} \]

Reduction in an assumption#

a reduction on a side, leaving nothing to quantify

assumptions:
  budget_covers_the_peak: "sum(p_max, over=generator) >= budget"
\[ \sum_{g \in \mathcal{G}} \mathrm{p}^{\mathrm{max}}_{g} \ge \mathrm{budget} \]

Shift in an assumption#

a translation inside arithmetic, and a position keeping the vacated row out

assumptions:
  ramps_are_gentle:
    holds: "load - shift(load, along=snapshot, offset=1, edge=0) <= budget"
    where: "position(snapshot) > 0"
\[ \mathrm{load}_{u,t} - \mathrm{load}_{u,t \boxminus_{0} 1} \le \mathrm{budget} \qquad \forall\, u \in \mathcal{U},\ t \in \mathcal{T} \,:\, \mathrm{pos}(t) > 0 \]

Assumption with a boolean condition#

a bare bool parameter as the where

assumptions:
  flexible_units_have_headroom:
    holds: "p_min < p_max"
    where: "is_flexible"
\[ \mathrm{p}^{\mathrm{min}}_{g} < \mathrm{p}^{\mathrm{max}}_{g} \qquad \forall\, g \in \mathcal{G} \,:\, \mathrm{is\_flexible}_{g} \]

Assumption with a map condition#

a map comparison as the where, over a frame two dims wide

assumptions:
  northern_demand_is_real:
    holds: "load >= 0"
    where: "bus[zone] == 'north'"
\[ \mathrm{load}_{u,t} \ge 0 \qquad \forall\, u \in \mathcal{U},\ t \in \mathcal{T} \,:\, \mathrm{zone}(u) = \text{'}\mathrm{north}\text{'} \]

Assumption on the rows of a constraint and a variable#

a constraint's row where a variable exists: decided at load from their where:, then checked on the data

assumptions:
  every_dispatch_has_a_start_row:
    holds: "starts"
    where: "p"
\[ \mathrm{starts}_{g,t} \text{ has a row} \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \,:\, p_{g,t} \text{ exists} \]

Where conditions#

Parameter as a condition#

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

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

Variable and label conditions#

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

constraints:
  running:
    dims: [snapshot, bus]
    where: "theta AND snapshot >= 3"
    expression: theta <= load
\[ \theta_{u} \le \mathrm{load}_{u,t} \qquad \forall\, u \in \mathcal{U},\ t \in \mathcal{T} \,:\, \theta_{u} \text{ exists} \wedge t \ge 3 \]

Position in a dimension#

a position in a dimension, and the same position within a group

constraints:
  first:
    dims: [snapshot, generator]
    where: "position(snapshot) == 0 OR position(snapshot, within=snapshot[season]) == 0"
    expression: on == 1
\[ \mathit{on}_{g,t} = 1 \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \,:\, \mathrm{pos}(t) = 0 \vee \mathrm{pos}_{\mathrm{season}(t)}(t) = 0 \]

Position counted from the end#

the same two counted from the end, which print against a size rather than as themselves

constraints:
  last:
    dims: [snapshot, generator]
    where: "position(snapshot) == -1 OR position(snapshot, within=snapshot[season]) == -1"
    expression: on == 0
\[ \mathit{on}_{g,t} = 0 \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \,:\, \mathrm{pos}(t) = \lvert \mathcal{T} \rvert - 1 \vee \mathrm{pos}_{\mathrm{season}(t)}(t) = \lvert \mathcal{T}_{\mathrm{season}(t)} \rvert - 1 \]

Map compared to a label#

a map compared to a label, to another map, and tested for a label

constraints:
  northern:
    dims: [snapshot, bus]
    where: "bus[zone] == 'north' AND bus[zone] != bus[area] AND bus[area]"
    expression: slack <= load
\[ \mathit{slack}_{t} \le \mathrm{load}_{u,t} \qquad \forall\, u \in \mathcal{U},\ t \in \mathcal{T} \,:\, \mathrm{zone}(u) = \text{'}\mathrm{north}\text{'} \wedge \mathrm{zone}(u) \neq \mathrm{area}(u) \wedge \mathrm{area}(u) \text{ is defined} \]

Constant true condition#

a mask that is only the constant true, which the language says is no mask at all — so none prints

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

Constant true inside a condition#

the same constant inside a mask, where it is what the file says and prints

constraints:
  redundant:
    dims: [snapshot]
    where: "True AND spill"
    expression: spill >= 0
\[ \mathit{spill}_{t} \ge 0 \qquad \forall\, t \in \mathcal{T} \,:\, \mathit{spill}_{t} \text{ exists} \]

Constant false condition#

the other constant mask, which says the rows are none and is worth seeing

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

Comparison of two expressions#

a mask comparing two expressions, which prints as the arithmetic it is

constraints:
  margin:
    dims: [snapshot, generator]
    where: "p_max - p_min > cost / 2"
    expression: p <= p_max
\[ p_{g,t} \le \mathrm{p}^{\mathrm{max}}_{g} \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \,:\, \mathrm{p}^{\mathrm{max}}_{g} - \mathrm{p}^{\mathrm{min}}_{g} > \frac{\mathrm{cost}_{g}}{2} \]

Shift and lookup in a condition#

a translation under a comparison names its edge, a lookup reads through a map, and the position keeps the vacated row out

constraints:
  ramped:
    dims: [snapshot, bus]
    where: "load - shift(load, along=snapshot, offset=1, edge=0) <= at(zone_cap, by=bus[zone]) AND position(snapshot) > 0"
    expression: slack <= load
\[ \mathit{slack}_{t} \le \mathrm{load}_{u,t} \qquad \forall\, u \in \mathcal{U},\ t \in \mathcal{T} \,:\, \mathrm{load}_{u,t} - \mathrm{load}_{u,t \boxminus_{0} 1} \le \mathrm{zone\_cap}_{\mathrm{zone}(u)} \wedge \mathrm{pos}(t) > 0 \]

Reduction in a scalar condition#

a reduction on a side of a scalar mask, so nothing is left to quantify

constraints:
  covered:
    dims: []
    where: "sum(p_max, over=generator) >= budget"
    expression: sum(p) <= budget
\[ \sum_{g \in \mathcal{G},\ t \in \mathcal{T}} p_{g,t} \le \mathrm{budget} \qquad \text{where } \sum_{g \in \mathcal{G}} \mathrm{p}^{\mathrm{max}}_{g} \ge \mathrm{budget} \]

Count over a dimension#

a count of the coordinates a condition admits, which reduces one dim away

constraints:
  counted:
    dims: [bus]
    where: "count(tech_cap > 0, over=technology) >= 2"
    expression: theta <= budget
\[ \theta_{u} \le \mathrm{budget} \qquad \forall\, u \in \mathcal{U} \,:\, \lvert \{ e \in \mathcal{E} \,:\, \mathrm{tech\_cap}_{u,e} > 0 \} \rvert \ge 2 \]

Count along a dimension of the frame#

the same count along a dim the frame carries, so the set takes a primed dummy

constraints:
  counted_here:
    dims: [bus, technology]
    where: "count(tech_cap > 0, over=technology) >= 2"
    expression: theta <= tech_cap
\[ \theta_{u} \le \mathrm{tech\_cap}_{u,e} \qquad \forall\, u \in \mathcal{U},\ e \in \mathcal{E} \,:\, \lvert \{ e' \in \mathcal{E} \,:\, \mathrm{tech\_cap}_{u,e'} > 0 \} \rvert \ge 2 \]

Condition at the previous coordinate#

a condition read one coordinate back, which is false where the translation vacates

constraints:
  run_start:
    dims: [snapshot, bus]
    where: "load AND NOT shift(load, along=snapshot, offset=1)"
    expression: slack <= load
\[ \mathit{slack}_{t} \le \mathrm{load}_{u,t} \qquad \forall\, u \in \mathcal{U},\ t \in \mathcal{T} \,:\, \mathrm{load}_{u,t} \text{ is defined} \wedge \neg \left( \mathrm{load}_{u,t - 1} \text{ is defined} \right) \]

Condition through a map#

a condition read through a map: a bus is held only where its zone has a cap at all

constraints:
  zoned:
    dims: [bus]
    where: "at(zone_cap, by=bus[zone])"
    expression: theta <= budget
\[ \theta_{u} \le \mathrm{budget} \qquad \forall\, u \in \mathcal{U} \,:\, \mathrm{zone\_cap}_{\mathrm{zone}(u)} \text{ is defined} \]

See also