Operators#
An operator sums an expression over a dimension, or moves its values along one.
The set is closed: the four below, and
dual in a reported expression, are all
of them, and a file cannot add an operator. Write a composition of them as a macro.
| Operator | Result |
|---|---|
sum(array) |
Every dimension that array carries collapses. The result is a scalar |
sum(array, over=dim) |
dim collapses. array must carry dim |
sum(array, over=[a, …]) |
Each dimension in the list collapses. array must carry each one, and the list names each one once |
sum(array, over=dim, by=relation[c]) |
dim collapses, and the result is grouped by the relation's column c. The other key columns are joined on, so the array carries them and the result keeps them |
sum(array, over=[dim, …], by=relation[c, …]) |
The same with several dimensions and several columns of one relation. The call joins on them together, and groups by their product |
at(array, by=relation[c]) |
array read at the value of the relation's column c, once per key of the relation. The result gains the key |
shift(array, along=dim, offset=n) |
The value n positions earlier along dim. The vacated edge is absent |
shift(array, along=dim, offset=n, edge='wrap') |
The value n positions earlier, counted cyclically, so nothing is vacated |
shift(array, along=dim, offset=n, edge=v) |
The value n positions earlier, with the number v standing where the edge was vacated |
shift(array, along=dim, offset=p, edge=…) |
p is an integer parameter, so each entity is reached by its own offset |
shift(array, along=dim, offset=n, within=relation[c]) |
The translation steps inside each group that the relation's column c makes. Neighbours, edges and a wrap all belong to that group |
sum_back(array, along=dim, window=n) |
The sum of the last n positions along dim, ending at the position being written |
sum_back(array, along=dim, window=p) |
p is an integer parameter, so each entity gets its own window length |
sum_back(array, along=dim, window=p, edge='wrap') |
The window reaches around the axis, instead of stopping short at its start |
sum_back(array, along=dim, window=n, within=relation[c]) |
The window stays inside each group that the relation's column c makes |
array is any expression with the right dimension set, a parameter or a
variable. Every operator as math shows how each row
prints.
sum#
sum(x) on a scalar is an error. sum(x, over=[a, b]) is
sum(sum(x, over=a), over=b), and prints as one sum over both sets. A nodal
balance is one sum(by=) per kind of component:
dimensions:
bus: { dtype: str }
generator: { dtype: str }
line: { dtype: str }
relations:
gen_bus: { key: generator, values: bus }
line_from: { key: line, values: bus }
line_to: { key: line, values: bus }
parameters:
load: { dims: [bus] }
variables:
p: { dims: [generator] }
f: { dims: [line] }
constraints:
nodal_balance:
dims: [bus]
expression: >-
sum(p, over=generator, by=gen_bus[bus])
+ sum(f, over=line, by=line_to[bus])
- sum(f, over=line, by=line_from[bus])
== load
How a relation is used says what a call through a relation reads and carries.
at#
at joins the relation with no group-by, so it reads one coarse value once for
each fine label that points at it (lookups). One
decision per bus, read by every line that touches the bus, is
at(decision, by=line_bus[bus]).
sum_back#
sum_back sums the last window positions along a dimension. Use it for a
minimum up time, a rolling budget or a delivery horizon. The dimension
stays in the result, and a window of 1 gives x itself.
dimensions:
unit: { dtype: str }
hour: { dtype: int, ordered: true }
parameters:
min_up: { dims: [unit], dtype: int }
variables:
started: { dims: [unit, hour], domain: binary }
on: { dims: [unit, hour], domain: binary }
constraints:
stays_up_its_own_time:
dims: [unit, hour]
expression: sum_back(started, along=hour, window=min_up) <= on
objective: { sense: minimize, expression: sum(on) }
A window given as a parameter is dtype: int, and does not vary along the
dimension being summed.
edge= takes 'wrap' or nothing, and a number is a load error. Without
edge=, a window that reaches past the start of the dimension is short,
and no row is lost.
within= takes a partition.
shift#
shift counts positions in the order of an ordered
dimension. edge= says what stands where nothing moved in.
dimensions:
snapshot: { dtype: int, ordered: true }
storage: { dtype: str }
parameters:
eta: { dims: [storage] }
variables:
soc: { dims: [snapshot, storage] }
charge: { dims: [snapshot, storage] }
discharge: { dims: [snapshot, storage] }
constraints:
storage_balance:
dims: [snapshot, storage]
expression: soc == shift(soc, along=snapshot, offset=1, edge='wrap') + charge * eta - discharge
edge='wrap' makes the store cyclic: the first snapshot reads the last. Without
edge=, the row that the vacated coordinate would feed is not built, so state
the initial condition in an entry of its own (a rule that differs by
regime).
Two rules hold for edge=:
- Over a variable, the only numeric edge is
0. - A bare
shiftover an expression with no variable is a load error, and the error names the rewrites:edge='wrap',edge=0, oredge=0together with awherethat excludes the vacated coordinate.
Translation within groups#
within= takes a partition, and the neighbour of a
coordinate is the one before it in its own group, such as a season:
dimensions:
snapshot: { dtype: int, ordered: true }
season: { dtype: str }
relations:
season_of: { key: snapshot, values: season }
parameters:
inflow: { dims: [snapshot] }
variables:
soc: { dims: [snapshot], bounds: { lower: 0 } }
constraints:
season_balance:
dims: [snapshot]
expression: soc == shift(soc, along=snapshot, offset=1, edge='wrap', within=season_of[season]) + inflow
objective: { sense: minimize, expression: sum(soc) }
Every edge= setting then applies one group at a time, and a coordinate in no
group is dropped under every edge=.
A parameter as offset#
Use a parameter as offset for a construction lead time, a transit time, or any delay the data carries as a column:
dimensions:
technology: { dtype: str }
month: { dtype: int, ordered: true }
parameters:
lead: { dims: [technology], dtype: int }
demand: { dims: [technology, month] }
variables:
order:
dims: [technology, month]
bounds: { lower: 0 }
constraints:
arrives_after_its_lead:
dims: [technology, month]
expression: shift(order, along=month, offset=lead, edge=0) >= demand
objective: { sense: minimize, expression: sum(order) }
Each of these is a load error:
- The parameter is not
dtype: int. - The parameter varies along the dimension being translated.
- The parameter varies over a dimension the shift cannot read. The shift
reads the dimensions of the shifted expression, and the dimension a
within=column groups into:offset=leadwithlead: {dims: [period]}underwithin=period_of[period]gives one lag per period.
Put the sign in the data: offset=-lead is refused.
Every operator as math#
tools/spec_math.py generates each row from one spec in
examples/operators/,
and the typesetter prints it. The specs themselves are on
One construct per spec.
| Operator | Renders as |
|---|---|
sum(array) |
\(\sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} \le \mathrm{budget}\) |
sum(array, over=dim) |
\(\sum_{g \in \mathcal{G}} p_{t,g} \le \mathrm{limit}_{t} \qquad \forall\, t \in \mathcal{T}\) |
sum(array, over=[a, …]) |
\(\sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g,c} \le \mathrm{limit}_{c} \qquad \forall\, c \in \mathcal{C}\) |
sum(array, over=dim, by=relation[c]) |
\(\sum_{g \in \mathcal{G} \,:\, \mathrm{gen\_bus}(g) = b} p_{t,g} \le \mathrm{limit}_{t,b} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B}\) |
sum(array, over=dim, by=relation[c]), joining on the rest of the key |
\(\sum_{g \in \mathcal{G} \,:\, \mathrm{zone\_of}(g,\ e) = z} p_{g,e} \ge \mathrm{demand}_{z,e} \qquad \forall\, z \in \mathcal{Z},\ e \in \mathcal{E}\) |
sum(array, over=[dim, …], by=relation[c, …]) |
\(\sum_{g \in \mathcal{G},\ e \in \mathcal{E} \,:\, \mathrm{slot\_of.bus}(g,\ e) = b \wedge \mathrm{slot\_of.technology}(g,\ e) = t} p_{g,e} \le \mathrm{cap}_{b,t} \qquad \forall\, b \in \mathcal{B},\ t \in \mathcal{T}\) |
at(array, by=relation[c]) |
\(p_{t} \le \mathrm{cap}_{\mathrm{period\_of}(t)} \qquad \forall\, t \in \mathcal{T}\) |
at(array, by=relation[c]), two columns over one dimension |
\(f_{l} \le \mathrm{cap}_{\mathrm{ends.bus0}(l)} \qquad \forall\, l \in \mathcal{L}\) |
shift(array, along=dim, offset=n) |
\(p_{t} \le p_{t - 1} \qquad \forall\, t \in \mathcal{T}\) |
shift(array, along=dim, offset=n, edge='wrap') |
\(p_{t} \le p_{t \ominus 1} \qquad \forall\, t \in \mathcal{T}\) |
shift(array, along=dim, offset=n, edge=v) |
\(p_{t} \le p_{t \boxminus_{0} 1} \qquad \forall\, t \in \mathcal{T}\) |
shift(array, along=dim, offset=p, edge=…) |
\(\mathit{order}_{t,m \boxminus_{0} \mathrm{lead}} \ge \mathrm{demand}_{t,m} \qquad \forall\, t \in \mathcal{T},\ m \in \mathcal{M}\) |
shift(array, along=dim, offset=n, within=relation[c]) |
\(p_{t} \le p_{t \ominus^{\mathrm{season\_of}(t)} 1} \qquad \forall\, t \in \mathcal{T}\) |
sum_back(array, along=dim, window=n) |
\(\sum_{h' \in \mathcal{H} \,:\, 0 \le h - h' < 3} \mathit{started}_{u,h'} \le \mathit{on}_{u,h} \qquad \forall\, u \in \mathcal{U},\ h \in \mathcal{H}\) |
sum_back(array, along=dim, window=p) |
\(\sum_{h' \in \mathcal{H} \,:\, 0 \le h - h' < \mathrm{min\_up}} \mathit{started}_{u,h'} \le \mathit{on}_{u,h} \qquad \forall\, u \in \mathcal{U},\ h \in \mathcal{H}\) |
sum_back(array, along=dim, window=p, edge='wrap') |
\(\sum_{h' \in \mathcal{H} \,:\, 0 \le h \ominus h' < \mathrm{min\_up}} \mathit{started}_{u,h'} \le \mathit{on}_{u,h} \qquad \forall\, u \in \mathcal{U},\ h \in \mathcal{H}\) |
sum_back(array, along=dim, window=n, within=relation[c]) |
\(\sum_{h' \in \mathcal{H} \,:\, 0 \le h -^{\mathrm{day\_of}(h)} h' < 3} \mathit{started}_{u,h'} \le \mathit{on}_{u,h} \qquad \forall\, u \in \mathcal{U},\ h \in \mathcal{H}\) |
dual(constraint) |
\(\mathit{price}_{t} = \lambda_{\mathrm{balance},t} \qquad \forall\, t \in \mathcal{T}\) |
Regenerate with pixi run python -m tools.spec_math.