diff --git a/.prettierignore b/.prettierignore index 300de51b..56297914 100644 --- a/.prettierignore +++ b/.prettierignore @@ -29,3 +29,16 @@ docs/examples/operators.md # by the hook and the generator's output has never been what the file holds — # which is why nothing could compare them. docs/reference/notation.md + +# `tools/spec_math.py` writes the operator table on this page, and it is the +# third page in the same trap — the committed block has been prettier's shape +# and never the generator's since the day it was written. The prose and the +# hand-written tables above the markers give up formatting with it, which is +# the price of the file being the unit `.prettierignore` works in. +# +# `tools/home_math.py` is the other way out of this and needs no entry: it +# emits the blank lines around its markers that prettier wants, so `--check` +# and the formatter agree on `docs/index.md` and `README.md`. Padding a table +# is a harder shape to match than a blank line, which is why these three take +# the generator-wins route instead. +docs/reference/language/operators.md diff --git a/docs/examples/dispatch.md b/docs/examples/dispatch.md index 997178d0..6c385ea3 100644 --- a/docs/examples/dispatch.md +++ b/docs/examples/dispatch.md @@ -58,9 +58,9 @@ Least-cost dispatch of a generator fleet against an hourly load. | Symbol | Meaning | |---|---| -| $p^{\mathrm{max}}$ | `p_max` over $\mathcal{G}$ — installed capacity | -| $\mathit{load}$ | `load` over $\mathcal{T}$ — demand to be met | -| $\mathit{cost}$ | `cost` over $\mathcal{G}$ — marginal cost | +| $\mathrm{p}^{\mathrm{max}}$ | `p_max` over $\mathcal{G}$ — installed capacity | +| $\mathrm{load}$ | `load` over $\mathcal{T}$ — demand to be met | +| $\mathrm{cost}$ | `cost` over $\mathcal{G}$ — marginal cost | #### Variables @@ -68,21 +68,23 @@ Least-cost dispatch of a generator fleet against an hourly load. |---|---| | $p$ | `p` over $\mathcal{T} \times \mathcal{G}$ — output of a generator in a snapshot | +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. + #### Objective -$$\min \sum_{t \in \mathcal{T},\enspace g \in \mathcal{G}} p_{t,g} \cdot \mathit{cost}_{g}$$ +$$\min \sum_{t \in \mathcal{T},\enspace g \in \mathcal{G}} p_{t,g} \cdot \mathrm{cost}_{g}$$ #### Subject to **`power_balance`** -$$\sum_{g \in \mathcal{G}} p_{t,g} = \mathit{load}_{t} \qquad \forall\thinspace t \in \mathcal{T}$$ +$$\sum_{g \in \mathcal{G}} p_{t,g} = \mathrm{load}_{t} \qquad \forall\thinspace t \in \mathcal{T}$$ #### Variable domains **`p`** -$$0 \le p_{t,g} \le p^{\mathrm{max}}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G} \thinspace:\thinspace p^{\mathrm{max}}_{g} > 0$$ +$$0 \le p_{t,g} \le \mathrm{p}^{\mathrm{max}}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G} \thinspace:\thinspace \mathrm{p}^{\mathrm{max}}_{g} > 0$$ Regenerate with `pixi run python -m tools.gallery`. diff --git a/docs/examples/operators.md b/docs/examples/operators.md index 0b953aff..a0bdfb56 100644 --- a/docs/examples/operators.md +++ b/docs/examples/operators.md @@ -42,7 +42,7 @@ constraints: objective: { sense: minimize, expression: sum(p) } ``` -$\sum_{t \in \mathcal{T},\enspace g \in \mathcal{G}} p_{t,g} \le \mathit{budget}$ +$\sum_{t \in \mathcal{T},\enspace g \in \mathcal{G}} p_{t,g} \le \mathrm{budget}$ ### `sum(array, over=dim)` @@ -71,7 +71,7 @@ constraints: objective: { sense: minimize, expression: sum(p) } ``` -$\sum_{g \in \mathcal{G}} p_{t,g} \le \mathit{limit}_{t} \qquad \forall\thinspace t \in \mathcal{T}$ +$\sum_{g \in \mathcal{G}} p_{t,g} \le \mathrm{limit}_{t} \qquad \forall\thinspace t \in \mathcal{T}$ ### `sum(array, by=lookup)` @@ -107,7 +107,7 @@ constraints: objective: { sense: minimize, expression: sum(p) } ``` -$\sum_{g \in \mathcal{G} \thinspace:\thinspace \mathrm{gen\_bus}(g) = b} p_{t,g} \le \mathit{limit}_{t,b} \qquad \forall\thinspace t \in \mathcal{T},\enspace b \in \mathcal{B}$ +$\sum_{g \in \mathcal{G} \thinspace:\thinspace \mathrm{gen\_bus}(g) = b} p_{t,g} \le \mathrm{limit}_{t,b} \qquad \forall\thinspace t \in \mathcal{T},\enspace b \in \mathcal{B}$ ### `sum(array, by=[lookup, …])` @@ -145,7 +145,7 @@ constraints: objective: { sense: minimize, expression: sum(p) } ``` -$\sum_{g \in \mathcal{G} \thinspace:\thinspace \mathrm{gen\_bus}(g) = b \wedge \mathrm{gen\_tech}(g) = e} p_{t,g} \le \mathit{limit}_{t,b,e} \qquad \forall\thinspace t \in \mathcal{T},\enspace b \in \mathcal{B},\enspace e \in \mathcal{E}$ +$\sum_{g \in \mathcal{G} \thinspace:\thinspace \mathrm{gen\_bus}(g) = b \wedge \mathrm{gen\_tech}(g) = e} p_{t,g} \le \mathrm{limit}_{t,b,e} \qquad \forall\thinspace t \in \mathcal{T},\enspace b \in \mathcal{B},\enspace e \in \mathcal{E}$ ### `at(array, by=lookup)` @@ -179,7 +179,7 @@ constraints: objective: { sense: minimize, expression: sum(p) } ``` -$p_{t} \le \mathit{cap}_{\mathrm{period\_of}(t)} \qquad \forall\thinspace t \in \mathcal{T}$ +$p_{t} \le \mathrm{cap}_{\mathrm{period\_of}(t)} \qquad \forall\thinspace t \in \mathcal{T}$ ### `shift(array, over=dim, offset=n)` @@ -293,7 +293,7 @@ constraints: objective: { sense: minimize, expression: sum(order) } ``` -$\mathit{order}_{t,m \boxminus_{0} \mathit{lead}} \ge \mathit{demand}_{t,m} \qquad \forall\thinspace t \in \mathcal{T},\enspace m \in \mathcal{M}$ +$\mathit{order}_{t,m \boxminus_{0} \mathrm{lead}} \ge \mathrm{demand}_{t,m} \qquad \forall\thinspace t \in \mathcal{T},\enspace m \in \mathcal{M}$ ### `shift(array, over=dim, offset=n, by=lookup)` @@ -324,7 +324,7 @@ constraints: objective: { sense: minimize, expression: sum(p) } ``` -$p_{t} \le p_{t \ominus_{\mathrm{season\_of}(t)} 1} \qquad \forall\thinspace t \in \mathcal{T}$ +$p_{t} \le p_{t \ominus^{\mathrm{season\_of}(t)} 1} \qquad \forall\thinspace t \in \mathcal{T}$ ### `sum_back(array, over=dim, within=n)` @@ -392,7 +392,7 @@ constraints: objective: { sense: minimize, expression: sum(on) } ``` -$\sum_{h' \in \mathcal{H} \thinspace:\thinspace 0 \le h - h' < \mathit{min\_up}} \mathit{started}_{u,h'} \le \mathit{on}_{u,h} \qquad \forall\thinspace u \in \mathcal{U},\enspace h \in \mathcal{H}$ +$\sum_{h' \in \mathcal{H} \thinspace:\thinspace 0 \le h - h' < \mathrm{min\_up}} \mathit{started}_{u,h'} \le \mathit{on}_{u,h} \qquad \forall\thinspace u \in \mathcal{U},\enspace h \in \mathcal{H}$ ### `sum_back(array, over=dim, within=p, edge='wrap')` @@ -426,7 +426,7 @@ constraints: objective: { sense: minimize, expression: sum(on) } ``` -$\sum_{h' \in \mathcal{H} \thinspace:\thinspace 0 \le h \ominus h' < \mathit{min\_up}} \mathit{started}_{u,h'} \le \mathit{on}_{u,h} \qquad \forall\thinspace u \in \mathcal{U},\enspace h \in \mathcal{H}$ +$\sum_{h' \in \mathcal{H} \thinspace:\thinspace 0 \le h \ominus h' < \mathrm{min\_up}} \mathit{started}_{u,h'} \le \mathit{on}_{u,h} \qquad \forall\thinspace u \in \mathcal{U},\enspace h \in \mathcal{H}$ Regenerate with `pixi run python -m tools.gallery`. diff --git a/docs/reference/language/operators.md b/docs/reference/language/operators.md index 2a877686..aa07dddd 100644 --- a/docs/reference/language/operators.md +++ b/docs/reference/language/operators.md @@ -345,27 +345,27 @@ language is rendered the same way, on one page: [Every construct, as math](../notation.md). - -| Operator | Renders as | -| -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `sum(array)` | $\sum_{t \in \mathcal{T},\enspace g \in \mathcal{G}} p_{t,g} \le \mathit{budget}$ | -| `sum(array, over=dim)` | $\sum_{g \in \mathcal{G}} p_{t,g} \le \mathit{limit}_{t} \qquad \forall\thinspace t \in \mathcal{T}$ | -| `sum(array, by=lookup)` | $\sum_{g \in \mathcal{G} \thinspace:\thinspace \mathrm{gen\_bus}(g) = b} p_{t,g} \le \mathit{limit}_{t,b} \qquad \forall\thinspace t \in \mathcal{T},\enspace b \in \mathcal{B}$ | -| `sum(array, by=[lookup, …])` | $\sum_{g \in \mathcal{G} \thinspace:\thinspace \mathrm{gen\_bus}(g) = b \wedge \mathrm{gen\_tech}(g) = e} p_{t,g} \le \mathit{limit}_{t,b,e} \qquad \forall\thinspace t \in \mathcal{T},\enspace b \in \mathcal{B},\enspace e \in \mathcal{E}$ | -| `at(array, by=lookup)` | $p_{t} \le \mathit{cap}_{\mathrm{period\_of}(t)} \qquad \forall\thinspace t \in \mathcal{T}$ | -| `shift(array, over=dim, offset=n)` | $p_{t} \le p_{t - 1} \qquad \forall\thinspace t \in \mathcal{T}$ | -| `shift(array, over=dim, offset=n, edge='wrap')` | $p_{t} \le p_{t \ominus 1} \qquad \forall\thinspace t \in \mathcal{T}$ | -| `shift(array, over=dim, offset=n, edge=v)` | $p_{t} \le p_{t \boxminus_{0} 1} \qquad \forall\thinspace t \in \mathcal{T}$ | -| `shift(array, over=dim, offset=p, edge=…)` | $\mathit{order}_{t,m \boxminus_{0} \mathit{lead}} \ge \mathit{demand}_{t,m} \qquad \forall\thinspace t \in \mathcal{T},\enspace m \in \mathcal{M}$ | -| `shift(array, over=dim, offset=n, by=lookup)` | $p_{t} \le p_{t \ominus_{\mathrm{season\_of}(t)} 1} \qquad \forall\thinspace t \in \mathcal{T}$ | -| `sum_back(array, over=dim, within=n)` | $\sum_{h' \in \mathcal{H} \thinspace:\thinspace 0 \le h - h' < 3} \mathit{started}_{u,h'} \le \mathit{on}_{u,h} \qquad \forall\thinspace u \in \mathcal{U},\enspace h \in \mathcal{H}$ | -| `sum_back(array, over=dim, within=p)` | $\sum_{h' \in \mathcal{H} \thinspace:\thinspace 0 \le h - h' < \mathit{min\_up}} \mathit{started}_{u,h'} \le \mathit{on}_{u,h} \qquad \forall\thinspace u \in \mathcal{U},\enspace h \in \mathcal{H}$ | -| `sum_back(array, over=dim, within=p, edge='wrap')` | $\sum_{h' \in \mathcal{H} \thinspace:\thinspace 0 \le h \ominus h' < \mathit{min\_up}} \mathit{started}_{u,h'} \le \mathit{on}_{u,h} \qquad \forall\thinspace u \in \mathcal{U},\enspace h \in \mathcal{H}$ | +| Operator | Renders as | +|---|---| +| `sum(array)` | $\sum_{t \in \mathcal{T},\enspace 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\thinspace t \in \mathcal{T}$ | +| `sum(array, by=lookup)` | $\sum_{g \in \mathcal{G} \thinspace:\thinspace \mathrm{gen\_bus}(g) = b} p_{t,g} \le \mathrm{limit}_{t,b} \qquad \forall\thinspace t \in \mathcal{T},\enspace b \in \mathcal{B}$ | +| `sum(array, by=[lookup, …])` | $\sum_{g \in \mathcal{G} \thinspace:\thinspace \mathrm{gen\_bus}(g) = b \wedge \mathrm{gen\_tech}(g) = e} p_{t,g} \le \mathrm{limit}_{t,b,e} \qquad \forall\thinspace t \in \mathcal{T},\enspace b \in \mathcal{B},\enspace e \in \mathcal{E}$ | +| `at(array, by=lookup)` | $p_{t} \le \mathrm{cap}_{\mathrm{period\_of}(t)} \qquad \forall\thinspace t \in \mathcal{T}$ | +| `shift(array, over=dim, offset=n)` | $p_{t} \le p_{t - 1} \qquad \forall\thinspace t \in \mathcal{T}$ | +| `shift(array, over=dim, offset=n, edge='wrap')` | $p_{t} \le p_{t \ominus 1} \qquad \forall\thinspace t \in \mathcal{T}$ | +| `shift(array, over=dim, offset=n, edge=v)` | $p_{t} \le p_{t \boxminus_{0} 1} \qquad \forall\thinspace t \in \mathcal{T}$ | +| `shift(array, over=dim, offset=p, edge=…)` | $\mathit{order}_{t,m \boxminus_{0} \mathrm{lead}} \ge \mathrm{demand}_{t,m} \qquad \forall\thinspace t \in \mathcal{T},\enspace m \in \mathcal{M}$ | +| `shift(array, over=dim, offset=n, by=lookup)` | $p_{t} \le p_{t \ominus^{\mathrm{season\_of}(t)} 1} \qquad \forall\thinspace t \in \mathcal{T}$ | +| `sum_back(array, over=dim, within=n)` | $\sum_{h' \in \mathcal{H} \thinspace:\thinspace 0 \le h - h' < 3} \mathit{started}_{u,h'} \le \mathit{on}_{u,h} \qquad \forall\thinspace u \in \mathcal{U},\enspace h \in \mathcal{H}$ | +| `sum_back(array, over=dim, within=p)` | $\sum_{h' \in \mathcal{H} \thinspace:\thinspace 0 \le h - h' < \mathrm{min\_up}} \mathit{started}_{u,h'} \le \mathit{on}_{u,h} \qquad \forall\thinspace u \in \mathcal{U},\enspace h \in \mathcal{H}$ | +| `sum_back(array, over=dim, within=p, edge='wrap')` | $\sum_{h' \in \mathcal{H} \thinspace:\thinspace 0 \le h \ominus h' < \mathrm{min\_up}} \mathit{started}_{u,h'} \le \mathit{on}_{u,h} \qquad \forall\thinspace u \in \mathcal{U},\enspace h \in \mathcal{H}$ | $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. Regenerate with `pixi run python -m tools.spec_math`. diff --git a/docs/reference/notation.md b/docs/reference/notation.md index 2400c306..db12de1c 100644 --- a/docs/reference/notation.md +++ b/docs/reference/notation.md @@ -63,6 +63,7 @@ parameters: 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 @@ -83,17 +84,18 @@ parameters: | Symbol | Meaning | |---|---| -| $p^{\mathrm{max}}$ | `p_max` over $\mathcal{G}$ | -| $p^{\mathrm{min}}$ | `p_min` over $\mathcal{G}$ | -| $\mathit{cost}$ | `cost` over $\mathcal{G}$ | -| $\mathit{load}$ | `load` over $\mathcal{T} \times \mathcal{B}$ | -| $\mathit{is\_flexible}$ | `is_flexible` over $\mathcal{G}$ | -| $\mathit{zone}^{\mathrm{cap}}$ | `zone_cap` over $\mathcal{Z}$ | -| $\mathit{tech\_cap}$ | `tech_cap` over $\mathcal{B} \times \mathcal{E}$ | -| $\mathit{min\_up}$ | `min_up` over $\mathcal{G}$ | -| $\mathit{lead}$ | `lead` over $\mathcal{G}$ | -| $\mathit{budget}$ | `budget` (scalar) | -| $\mathit{growth}$ | `growth` (scalar) | +| $\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 @@ -102,7 +104,7 @@ parameters: | $p$ | `p` over $\mathcal{T} \times \mathcal{G}$ | | $\mathit{spill}$ | `spill` over $\mathcal{T}$ | | $\mathit{slack}$ | `slack` over $\mathcal{T}$ | -| $\mathit{theta}$ | `theta` over $\mathcal{B}$ | +| $\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}$ | @@ -111,10 +113,14 @@ parameters: | $\mathit{void}$ | `void` over $\mathcal{B}$ | | $\mathit{weight}$ | `weight` over $\mathcal{T} \times \mathcal{G}$ | +Upright is what the model is given — a parameter such as $\mathrm{p}^{\mathrm{max}}$, a coordinate map, a label — and italic is what the solver chooses, such as $p$. An index is italic too, being what a quantifier chooses, and a set is script. + $t \ominus k$ denotes cyclic translation: index $t-k$ taken modulo the size of the dimension (`roll`). Plain $t-k$ (`shift`) has no wraparound — terms translated past the edge are simply absent. $t \boxminus_{v} k$ denotes translation with $v$ standing where index $t-k$ leaves the dimension (`shift(edge=v)`), so the row at that boundary is built and carries $v$ rather than being dropped. +$t \ominus^{\mathrm{lookup}(t)} k$ denotes a translation counted inside the group a lookup puts $t$ in (`shift(by=lookup)`), so a term never crosses out of its own group. The two modifiers take different slots — the group above, the fill below — so $t \boxminus_{v}^{\mathrm{lookup}(t)} k$ is both at once. + ### The objective #### `objective` @@ -126,7 +132,7 @@ sense: maximize expression: sum(p * cost) + sum(p * p * cost) + sum(p * cost * growth ** lead) + sum(p * p_max) - reserve + -headroom ``` -$$\max \sum_{t \in \mathcal{T},\enspace g \in \mathcal{G}} p_{t,g} \cdot \mathit{cost}_{g} + \sum_{t \in \mathcal{T},\enspace g \in \mathcal{G}} p_{t,g} \cdot p_{t,g} \cdot \mathit{cost}_{g} + \sum_{t \in \mathcal{T},\enspace g \in \mathcal{G}} p_{t,g} \cdot \mathit{cost}_{g} \cdot \mathit{growth}^{\mathit{lead}_{g}} + \sum_{t \in \mathcal{T},\enspace g \in \mathcal{G}} p_{t,g} \cdot p^{\mathrm{max}}_{g} - \mathit{reserve} + -\mathit{headroom}$$ +$$\max \sum_{t \in \mathcal{T},\enspace g \in \mathcal{G}} p_{t,g} \cdot \mathrm{cost}_{g} + \sum_{t \in \mathcal{T},\enspace g \in \mathcal{G}} p_{t,g} \cdot p_{t,g} \cdot \mathrm{cost}_{g} + \sum_{t \in \mathcal{T},\enspace g \in \mathcal{G}} p_{t,g} \cdot \mathrm{cost}_{g} \cdot \mathrm{growth}^{\mathrm{lead}_{g}} + \sum_{t \in \mathcal{T},\enspace g \in \mathcal{G}} p_{t,g} \cdot \mathrm{p}^{\mathrm{max}}_{g} - \mathit{reserve} - \mathit{headroom}$$ ### Constraints @@ -140,7 +146,7 @@ balance: expression: sum(p, by=gen_bus) + spill - slack == load ``` -$$\sum_{g \in \mathcal{G} \thinspace:\thinspace \mathrm{gen\_bus}(g) = b} p_{t,g} + \mathit{spill}_{t} - \mathit{slack}_{t} = \mathit{load}_{t,b} \qquad \forall\thinspace t \in \mathcal{T},\enspace b \in \mathcal{B}$$ +$$\sum_{g \in \mathcal{G} \thinspace:\thinspace \mathrm{gen\_bus}(g) = b} p_{t,g} + \mathit{spill}_{t} - \mathit{slack}_{t} = \mathrm{load}_{t,b} \qquad \forall\thinspace t \in \mathcal{T},\enspace b \in \mathcal{B}$$ #### `ramp` @@ -152,7 +158,7 @@ ramp: expression: p - shift(p, over=snapshot, offset=1, edge='wrap') <= shift(p, over=snapshot, offset=1) + p_max ``` -$$p_{t,g} - p_{t \ominus 1,g} \le p_{t - 1,g} + p^{\mathrm{max}}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ +$$p_{t,g} - p_{t \ominus 1,g} \le p_{t - 1,g} + \mathrm{p}^{\mathrm{max}}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ #### `edges` @@ -166,7 +172,7 @@ edges: <= shift(p, over=snapshot, offset=-1, edge=0) + p_max ``` -$$p_{t \boxminus_{0} 1,g} \le p_{t \boxplus_{0} 1,g} + p^{\mathrm{max}}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ +$$p_{t \boxminus_{0} 1,g} \le p_{t \boxplus_{0} 1,g} + \mathrm{p}^{\mathrm{max}}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ #### `ahead` @@ -190,7 +196,7 @@ composed: expression: shift(shift(p, over=snapshot, offset=1), over=snapshot, offset=1) <= shift(p_max, over=generator, offset=0) ``` -$$p_{t - 2,g} \le p^{\mathrm{max}}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ +$$p_{t - 2,g} \le \mathrm{p}^{\mathrm{max}}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ #### `uncomposed` @@ -202,7 +208,7 @@ uncomposed: expression: shift(shift(p, over=snapshot, offset=lead), over=snapshot, offset=1) <= p_max ``` -$$p_{\left( t - 1 \right) - \mathit{lead},g} \le p^{\mathrm{max}}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ +$$p_{\left( t - 1 \right) - \mathrm{lead},g} \le \mathrm{p}^{\mathrm{max}}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ #### `crossed` @@ -214,7 +220,7 @@ crossed: expression: shift(shift(p, over=snapshot, offset=1, edge='wrap'), over=generator, offset=-1) <= p_max ``` -$$p_{t \ominus 1,g + 1} \le p^{\mathrm{max}}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ +$$p_{t \ominus 1,g + 1} \le \mathrm{p}^{\mathrm{max}}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ #### `lead_time` @@ -226,7 +232,7 @@ lead_time: expression: shift(p, over=snapshot, offset=lead, edge=0) <= p_max ``` -$$p_{t \boxminus_{0} \mathit{lead},g} \le p^{\mathrm{max}}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ +$$p_{t \boxminus_{0} \mathrm{lead},g} \le \mathrm{p}^{\mathrm{max}}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ #### `in_season` @@ -238,7 +244,7 @@ in_season: expression: p <= shift(p, over=snapshot, offset=1, edge='wrap', by=season_of) ``` -$$p_{t,g} \le p_{t \ominus_{\mathrm{season\_of}(t)} 1,g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ +$$p_{t,g} \le p_{t \ominus^{\mathrm{season\_of}(t)} 1,g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ #### `held_in_season` @@ -250,7 +256,7 @@ held_in_season: expression: p <= shift(p, over=snapshot, offset=1, edge=0, by=season_of) ``` -$$p_{t,g} \le p_{t \boxminus_{0,\mathrm{season\_of}(t)} 1,g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ +$$p_{t,g} \le p_{t \boxminus_{0}^{\mathrm{season\_of}(t)} 1,g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ #### `window` @@ -274,7 +280,7 @@ history: expression: sum_back(on, over=snapshot, within=min_up, edge='wrap') <= units ``` -$$\sum_{t' \in \mathcal{T} \thinspace:\thinspace 0 \le t \ominus t' < \mathit{min\_up}} \mathit{on}_{t',g} \le \mathit{units}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ +$$\sum_{t' \in \mathcal{T} \thinspace:\thinspace 0 \le t \ominus t' < \mathrm{min\_up}} \mathit{on}_{t',g} \le \mathit{units}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ #### `pullback` @@ -286,7 +292,7 @@ pullback: expression: spill <= at(zone_cap, by=zone_of) ``` -$$\mathit{spill}_{t} \le \mathit{zone}^{\mathrm{cap}}_{\mathrm{zone\_of}(b)} \qquad \forall\thinspace t \in \mathcal{T},\enspace b \in \mathcal{B}$$ +$$\mathit{spill}_{t} \le \mathrm{zone\_cap}_{\mathrm{zone\_of}(b)} \qquad \forall\thinspace t \in \mathcal{T},\enspace b \in \mathcal{B}$$ #### `grouped_twice` @@ -298,7 +304,7 @@ grouped_twice: expression: sum(p, by=[gen_bus, gen_tech]) <= tech_cap ``` -$$\sum_{g \in \mathcal{G} \thinspace:\thinspace \mathrm{gen\_bus}(g) = b \wedge \mathrm{gen\_tech}(g) = e} p_{t,g} \le \mathit{tech\_cap}_{b,e} \qquad \forall\thinspace t \in \mathcal{T},\enspace b \in \mathcal{B},\enspace e \in \mathcal{E}$$ +$$\sum_{g \in \mathcal{G} \thinspace:\thinspace \mathrm{gen\_bus}(g) = b \wedge \mathrm{gen\_tech}(g) = e} p_{t,g} \le \mathrm{tech\_cap}_{b,e} \qquad \forall\thinspace t \in \mathcal{T},\enspace b \in \mathcal{B},\enspace e \in \mathcal{E}$$ #### `pulled_back_twice` @@ -310,11 +316,11 @@ pulled_back_twice: expression: units <= at(tech_cap, by=[gen_bus, gen_tech]) ``` -$$\mathit{units}_{g} \le \mathit{tech\_cap}_{\mathrm{gen\_bus}(g),\mathrm{gen\_tech}(g)} \qquad \forall\thinspace g \in \mathcal{G}$$ +$$\mathit{units}_{g} \le \mathrm{tech\_cap}_{\mathrm{gen\_bus}(g),\mathrm{gen\_tech}(g)} \qquad \forall\thinspace g \in \mathcal{G}$$ #### `arithmetic` -division, unary minus, nested reduction, bracketing +division, both unary signs, nested reduction, bracketing ```yaml arithmetic: @@ -322,10 +328,10 @@ arithmetic: # using it would not be a model. Format.power stays exercised by unit # tests rather than from here. foreach: [snapshot] - expression: sum(p / 2 + -cost, over=generator) >= -sum(p, over=generator) * 3 + expression: sum(p / 2 + -cost, over=generator) >= -sum(+p, over=generator) * 3 ``` -$$\sum_{g \in \mathcal{G}} \left( \frac{p_{t,g}}{2} + -\mathit{cost}_{g} \right) \ge \left( -\left( \sum_{g \in \mathcal{G}} p_{t,g} \right) \right) \cdot 3 \qquad \forall\thinspace t \in \mathcal{T}$$ +$$\sum_{g \in \mathcal{G}} \left( \frac{p_{t,g}}{2} - \mathrm{cost}_{g} \right) \ge -\left( \sum_{g \in \mathcal{G}} p_{t,g} \right) \cdot 3 \qquad \forall\thinspace t \in \mathcal{T}$$ #### `total` @@ -337,7 +343,7 @@ total: expression: sum(p) <= budget ``` -$$\sum_{t \in \mathcal{T},\enspace g \in \mathcal{G}} p_{t,g} \le \mathit{budget}$$ +$$\sum_{t \in \mathcal{T},\enspace g \in \mathcal{G}} p_{t,g} \le \mathrm{budget}$$ #### `scalar` @@ -350,7 +356,7 @@ scalar: expression: units <= budget ``` -$$\mathit{units}_{g} \le \mathit{budget} \qquad \forall\thinspace g \in \mathcal{G} \thinspace:\thinspace \mathit{cost}_{g} \text{ is defined}$$ +$$\mathit{units}_{g} \le \mathrm{budget} \qquad \forall\thinspace g \in \mathcal{G} \thinspace:\thinspace \mathrm{cost}_{g} \text{ is defined}$$ #### `running` @@ -363,7 +369,7 @@ running: expression: theta <= load ``` -$$\mathit{theta}_{b} \le \mathit{load}_{t,b} \qquad \forall\thinspace t \in \mathcal{T},\enspace b \in \mathcal{B} \thinspace:\thinspace \mathit{theta}_{b} \text{ exists} \wedge t \ge 3$$ +$$\theta_{b} \le \mathrm{load}_{t,b} \qquad \forall\thinspace t \in \mathcal{T},\enspace b \in \mathcal{B} \thinspace:\thinspace \theta_{b} \text{ exists} \wedge t \ge 3$$ #### `first` @@ -389,11 +395,23 @@ northern: expression: slack <= load ``` -$$\mathit{slack}_{t} \le \mathit{load}_{t,b} \qquad \forall\thinspace t \in \mathcal{T},\enspace b \in \mathcal{B} \thinspace:\thinspace \mathrm{zone\_of}(b) = \text{north} \wedge \mathrm{zone\_of}(b) \neq \mathrm{area\_of}(b) \wedge \mathrm{zone\_of}(b) \text{ is defined}$$ +$$\mathit{slack}_{t} \le \mathrm{load}_{t,b} \qquad \forall\thinspace t \in \mathcal{T},\enspace b \in \mathcal{B} \thinspace:\thinspace \mathrm{zone\_of}(b) = \text{north} \wedge \mathrm{zone\_of}(b) \neq \mathrm{area\_of}(b) \wedge \mathrm{zone\_of}(b) \text{ is defined}$$ + +#### `efficiency` + +a Greek-named parameter, which is given — so the convention wins and it prints as the word + +```yaml +efficiency: + foreach: [snapshot, generator] + expression: p <= eta * p_max +``` + +$$p_{t,g} \le \mathrm{eta}_{g} \cdot \mathrm{p}^{\mathrm{max}}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ #### `always` -the two constant masks, which are a quantifier and no equation +a mask that is only the constant true, which the language says is no mask at all — so none prints ```yaml always: @@ -402,11 +420,24 @@ always: expression: spill >= 0 ``` -$$\mathit{spill}_{t} \ge 0 \qquad \forall\thinspace t \in \mathcal{T} \thinspace:\thinspace \top$$ +$$\mathit{spill}_{t} \ge 0 \qquad \forall\thinspace t \in \mathcal{T}$$ + +#### `redundant` + +the same constant *inside* a mask, where it is what the file says and prints + +```yaml +redundant: + foreach: [snapshot] + where: "True AND spill" + expression: spill >= 0 +``` + +$$\mathit{spill}_{t} \ge 0 \qquad \forall\thinspace t \in \mathcal{T} \thinspace:\thinspace \top \wedge \mathit{spill}_{t} \text{ exists}$$ #### `never` -the other constant mask +the other constant mask, which says the rows are none and is worth seeing ```yaml never: @@ -430,7 +461,7 @@ p: bounds: { lower: p_min, upper: p_max } ``` -$$p^{\mathrm{min}}_{g} \le p_{t,g} \le p^{\mathrm{max}}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G} \thinspace:\thinspace \left( p^{\mathrm{max}}_{g} > 0 \wedge \neg \mathit{is\_flexible}_{g} \vee p^{\mathrm{min}}_{g} > 0 \right)$$ +$$\mathrm{p}^{\mathrm{min}}_{g} \le p_{t,g} \le \mathrm{p}^{\mathrm{max}}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G} \thinspace:\thinspace \left( \mathrm{p}^{\mathrm{max}}_{g} > 0 \wedge \neg \mathrm{is\_flexible}_{g} \vee \mathrm{p}^{\mathrm{min}}_{g} > 0 \right)$$ #### `spill` @@ -465,7 +496,7 @@ theta: foreach: [bus] ``` -$$\mathit{theta}_{b} \in \mathbb{R} \qquad \forall\thinspace b \in \mathcal{B}$$ +$$\theta_{b} \in \mathbb{R} \qquad \forall\thinspace b \in \mathcal{B}$$ #### `on` @@ -527,7 +558,7 @@ headroom: bounds: { lower: 0 } ``` -$$\mathit{headroom} \ge 0 \qquad \text{where } \mathit{budget} \text{ is defined}$$ +$$\mathit{headroom} \ge 0 \qquad \text{where } \mathrm{budget} \text{ is defined}$$ #### `void` @@ -561,6 +592,18 @@ A curve is sugar: what prints is the formulation it expands to, which is the mat **`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: + +```yaml +notation: latex + +names: + economies_of_scale_lam: "\\lambda" + economies_of_scale_seg: "\\delta" + bp_x: "\\mathrm{x}" + bp_y: "\\mathrm{y}" +``` + ```yaml economies_of_scale: over: bp @@ -569,24 +612,35 @@ economies_of_scale: - [scaled, bp_y] ``` -$$\sum_{b \in \mathcal{B}} \mathit{economies\_of\_scale\_lam}_{p,m,b} = 1 \qquad \forall\thinspace p \in \mathcal{P},\enspace m \in \mathcal{M}$$ +$$\sum_{b \in \mathcal{B}} \lambda_{p,m,b} = 1 \qquad \forall\thinspace p \in \mathcal{P},\enspace m \in \mathcal{M}$$ -$$\mathit{shipment}_{p,m} = \sum_{b \in \mathcal{B}} \mathit{economies\_of\_scale\_lam}_{p,m,b} \cdot \mathit{bp}^{\mathrm{x}}_{b} \qquad \forall\thinspace p \in \mathcal{P},\enspace m \in \mathcal{M}$$ +$$\mathit{shipment}_{p,m} = \sum_{b \in \mathcal{B}} \lambda_{p,m,b} \cdot \mathrm{x}_{b} \qquad \forall\thinspace p \in \mathcal{P},\enspace m \in \mathcal{M}$$ -$$\mathit{scaled}_{p,m} = \sum_{b \in \mathcal{B}} \mathit{economies\_of\_scale\_lam}_{p,m,b} \cdot \mathit{bp}^{\mathrm{y}}_{b} \qquad \forall\thinspace p \in \mathcal{P},\enspace m \in \mathcal{M}$$ +$$\mathit{scaled}_{p,m} = \sum_{b \in \mathcal{B}} \lambda_{p,m,b} \cdot \mathrm{y}_{b} \qquad \forall\thinspace p \in \mathcal{P},\enspace m \in \mathcal{M}$$ -$$\sum_{b \in \mathcal{B}} \mathit{economies\_of\_scale\_seg}_{p,m,b} = 1 \qquad \forall\thinspace p \in \mathcal{P},\enspace m \in \mathcal{M}$$ +$$\sum_{b \in \mathcal{B}} \delta_{p,m,b} = 1 \qquad \forall\thinspace p \in \mathcal{P},\enspace m \in \mathcal{M}$$ -$$\mathit{economies\_of\_scale\_lam}_{p,m,b} \le \mathit{economies\_of\_scale\_seg}_{p,m,b} + \mathit{economies\_of\_scale\_seg}_{p,m,b \boxminus_{0} 1} \qquad \forall\thinspace p \in \mathcal{P},\enspace m \in \mathcal{M},\enspace b \in \mathcal{B}$$ +$$\lambda_{p,m,b} \le \delta_{p,m,b} + \delta_{p,m,b \boxminus_{0} 1} \qquad \forall\thinspace p \in \mathcal{P},\enspace m \in \mathcal{M},\enspace b \in \mathcal{B}$$ -$$0 \le \mathit{economies\_of\_scale\_lam}_{p,m,b} \le 1 \qquad \forall\thinspace p \in \mathcal{P},\enspace m \in \mathcal{M},\enspace b \in \mathcal{B}$$ +$$0 \le \lambda_{p,m,b} \le 1 \qquad \forall\thinspace p \in \mathcal{P},\enspace m \in \mathcal{M},\enspace b \in \mathcal{B}$$ -$$\mathit{economies\_of\_scale\_seg}_{p,m,b} \in \{0, 1\} \qquad \forall\thinspace p \in \mathcal{P},\enspace m \in \mathcal{M},\enspace b \in \mathcal{B}$$ +$$\delta_{p,m,b} \in \{0, 1\} \qquad \forall\thinspace p \in \mathcal{P},\enspace m \in \mathcal{M},\enspace b \in \mathcal{B}$$ #### `cost_curve` **`method: sos2`** — the same weights, restricted by a set the sink 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: + +```yaml +notation: latex + +names: + cost_curve_lam: "\\lambda" + bp_x: "\\mathrm{x}" + bp_y: "\\mathrm{y}" +``` + ```yaml cost_curve: over: bp @@ -596,20 +650,31 @@ cost_curve: method: sos2 ``` -$$\sum_{b \in \mathcal{B}} \mathit{cost\_curve\_lam}_{t,g,b} = 1 \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ +$$\sum_{b \in \mathcal{B}} \lambda_{t,g,b} = 1 \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ -$$p_{t,g} = \sum_{b \in \mathcal{B}} \mathit{cost\_curve\_lam}_{t,g,b} \cdot \mathit{bp}^{\mathrm{x}}_{g,b} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ +$$p_{t,g} = \sum_{b \in \mathcal{B}} \lambda_{t,g,b} \cdot \mathrm{x}_{g,b} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ -$$\mathit{op\_cost}_{t,g} = \sum_{b \in \mathcal{B}} \mathit{cost\_curve\_lam}_{t,g,b} \cdot \mathit{bp}^{\mathrm{y}}_{g,b} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ +$$\mathit{op\_cost}_{t,g} = \sum_{b \in \mathcal{B}} \lambda_{t,g,b} \cdot \mathrm{y}_{g,b} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ -$$0 \le \mathit{cost\_curve\_lam}_{t,g,b} \le 1 \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G},\enspace b \in \mathcal{B}$$ +$$0 \le \lambda_{t,g,b} \le 1 \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G},\enspace b \in \mathcal{B}$$ -$$\left( \mathit{cost\_curve\_lam}_{t,g,b} \right)_{b \in \mathcal{B}} \in \mathrm{SOS}2 \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ +$$\left( \lambda_{t,g,b} \right)_{b \in \mathcal{B}} \in \mathrm{SOS}2 \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ #### `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: + +```yaml +notation: latex + +names: + cost_curve_lam: "\\lambda" + bp_x: "\\mathrm{x}" + bp_y: "\\mathrm{y}" +``` + ```yaml cost_curve: over: bp @@ -619,18 +684,28 @@ cost_curve: method: convex ``` -$$\sum_{b \in \mathcal{B}} \mathit{cost\_curve\_lam}_{t,g,b} = 1 \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ +$$\sum_{b \in \mathcal{B}} \lambda_{t,g,b} = 1 \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ -$$p_{t,g} = \sum_{b \in \mathcal{B}} \mathit{cost\_curve\_lam}_{t,g,b} \cdot \mathit{bp}^{\mathrm{x}}_{g,b} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ +$$p_{t,g} = \sum_{b \in \mathcal{B}} \lambda_{t,g,b} \cdot \mathrm{x}_{g,b} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ -$$\mathit{op\_cost}_{t,g} = \sum_{b \in \mathcal{B}} \mathit{cost\_curve\_lam}_{t,g,b} \cdot \mathit{bp}^{\mathrm{y}}_{g,b} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ +$$\mathit{op\_cost}_{t,g} = \sum_{b \in \mathcal{B}} \lambda_{t,g,b} \cdot \mathrm{y}_{g,b} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ -$$0 \le \mathit{cost\_curve\_lam}_{t,g,b} \le 1 \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G},\enspace b \in \mathcal{B}$$ +$$0 \le \lambda_{t,g,b} \le 1 \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G},\enspace b \in \mathcal{B}$$ #### `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: + +```yaml +notation: latex + +names: + bp_x: "\\mathrm{x}" + bp_y: "\\mathrm{y}" +``` + ```yaml cost_curve: over: bp @@ -640,11 +715,11 @@ cost_curve: method: lp ``` -$$\mathit{op\_cost}_{t,g} \cdot \left( \mathit{bp}^{\mathrm{x}}_{g,b} - \mathit{bp}^{\mathrm{x}}_{g,b \boxminus_{0} 1} \right) \ge \left( \mathit{bp}^{\mathrm{y}}_{g,b} - \mathit{bp}^{\mathrm{y}}_{g,b \boxminus_{0} 1} \right) \cdot \left( p_{t,g} - \mathit{bp}^{\mathrm{x}}_{g,b} \right) + \mathit{bp}^{\mathrm{y}}_{g,b} \cdot \left( \mathit{bp}^{\mathrm{x}}_{g,b} - \mathit{bp}^{\mathrm{x}}_{g,b \boxminus_{0} 1} \right) \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G},\enspace b \in \mathcal{B} \thinspace:\thinspace b \neq \mathrm{index}(\mathcal{B}, 0)$$ +$$\mathit{op\_cost}_{t,g} \cdot \left( \mathrm{x}_{g,b} - \mathrm{x}_{g,b \boxminus_{0} 1} \right) \ge \left( \mathrm{y}_{g,b} - \mathrm{y}_{g,b \boxminus_{0} 1} \right) \cdot \left( p_{t,g} - \mathrm{x}_{g,b} \right) + \mathrm{y}_{g,b} \cdot \left( \mathrm{x}_{g,b} - \mathrm{x}_{g,b \boxminus_{0} 1} \right) \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G},\enspace b \in \mathcal{B} \thinspace:\thinspace b \neq \mathrm{index}(\mathcal{B}, 0)$$ -$$p_{t,g} \ge \mathit{bp}^{\mathrm{x}}_{g,b} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G},\enspace b \in \mathcal{B} \thinspace:\thinspace b = \mathrm{index}(\mathcal{B}, 0)$$ +$$p_{t,g} \ge \mathrm{x}_{g,b} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G},\enspace b \in \mathcal{B} \thinspace:\thinspace b = \mathrm{index}(\mathcal{B}, 0)$$ -$$p_{t,g} \le \mathit{bp}^{\mathrm{x}}_{g,b} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G},\enspace b \in \mathcal{B} \thinspace:\thinspace b = \mathrm{index}(\mathcal{B}, -1)$$ +$$p_{t,g} \le \mathrm{x}_{g,b} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G},\enspace b \in \mathcal{B} \thinspace:\thinspace b = \mathrm{index}(\mathcal{B}, -1)$$ ### Sets carried to the solver diff --git a/examples/symbols/dispatch.yaml b/examples/symbols/dispatch.yaml index 8e1e56ac..0c046f9f 100644 --- a/examples/symbols/dispatch.yaml +++ b/examples/symbols/dispatch.yaml @@ -5,6 +5,15 @@ # The sidecar symbol table for `examples/dispatch.yaml` — not a model, so # nothing here is checked against the schema. `tools/render_tex.py` picks it up # by filename, and the homepage prints the same model with and without it. +# +# `c`, `\ell` and `\bar p` name three *parameters* and all three are italic, which +# is not what the derivation would print: upright is what the model is given. +# That is the point of a table rather than a hole in the rule. Entries are +# printed verbatim and a reader's notation is theirs to choose — the same +# property that lets an author whose preamble loads `upgreek` write `\upeta` +# for a parameter the derivation has to spell out. The convention note is +# suppressed wherever a table has taken over the symbols it would quote, so a +# page never states a rule its own symbols are not keeping. notation: latex dimensions: diff --git a/examples/symbols/piecewise.yaml b/examples/symbols/piecewise.yaml new file mode 100644 index 00000000..de0cfcce --- /dev/null +++ b/examples/symbols/piecewise.yaml @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +# The sidecar symbol table for `examples/piecewise.yaml`. The weights a curve +# expands to are named after the block that declared them, which is right in +# the file and unreadable in an equation that names one six times: papers write +# the convex-combination weight as lambda, so this says so out loud rather than +# the typesetter renaming anything behind the reader's back. +notation: latex + +names: + cost_curve_lam: "\\lambda" + bp_x: "\\mathrm{x}" + bp_y: "\\mathrm{y}" diff --git a/examples/symbols/piecewise_lp.yaml b/examples/symbols/piecewise_lp.yaml new file mode 100644 index 00000000..4a1362b5 --- /dev/null +++ b/examples/symbols/piecewise_lp.yaml @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +notation: latex + +names: + bp_x: "\\mathrm{x}" + bp_y: "\\mathrm{y}" diff --git a/examples/symbols/sos.yaml b/examples/symbols/sos.yaml new file mode 100644 index 00000000..4bf58603 --- /dev/null +++ b/examples/symbols/sos.yaml @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +# The sidecar symbol table for `examples/sos.yaml`. The weights a curve +# expands to are named after the block that declared them, which is right in +# the file and unreadable in an equation that names one six times: papers write +# the convex-combination weight as lambda, so this says so out loud rather than +# the typesetter renaming anything behind the reader's back. +notation: latex + +names: + cost_curve_lam: "\\lambda" + bp_x: "\\mathrm{x}" + bp_y: "\\mathrm{y}" diff --git a/examples/symbols/transport_pwl.yaml b/examples/symbols/transport_pwl.yaml new file mode 100644 index 00000000..c420239c --- /dev/null +++ b/examples/symbols/transport_pwl.yaml @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +# The sidecar symbol table for `examples/ports/transport_pwl.yaml`. The +# adjacency method expands to two families — the convex-combination weights and +# the binary that says which segment is live — and names both after the block, +# which no equation can carry. Lambda and delta are what the piecewise-linear +# literature calls them. +notation: latex + +names: + economies_of_scale_lam: "\\lambda" + economies_of_scale_seg: "\\delta" + bp_x: "\\mathrm{x}" + bp_y: "\\mathrm{y}" diff --git a/src/math_spec/typeset/__init__.py b/src/math_spec/typeset/__init__.py index c756a5fb..bff51c9e 100644 --- a/src/math_spec/typeset/__init__.py +++ b/src/math_spec/typeset/__init__.py @@ -112,6 +112,7 @@ def typeset( blocks = [fmt.note(fmt.escape(schema.description))] if schema.description else [] if legend: blocks += [fmt.glossary(group.title, group.entries) for group in walk.glossaries()] + blocks += [fmt.note(text) for text in walk.convention_notes()] blocks += [fmt.note(text) for text in walk.translation_notes()] return fmt.document([*blocks, *rendered], standalone=standalone) diff --git a/src/math_spec/typeset/format.py b/src/math_spec/typeset/format.py index f3302644..844568e0 100644 --- a/src/math_spec/typeset/format.py +++ b/src/math_spec/typeset/format.py @@ -149,6 +149,15 @@ def script(self, letter: str) -> str: """A set symbol.""" ... + def greek(self, name: str) -> str: + """A name that *is* a Greek letter, set as the letter. + + The walk decides which names those are; a format only spells one. + Lower-case names only — every one of them has a letter in both + notations, which the capitals do not. + """ + ... + def prose(self, text: str) -> str: """Words inside math.""" ... diff --git a/src/math_spec/typeset/latex.py b/src/math_spec/typeset/latex.py index ecc26605..6e281890 100644 --- a/src/math_spec/typeset/latex.py +++ b/src/math_spec/typeset/latex.py @@ -95,6 +95,9 @@ def upright(self, name: str) -> str: def script(self, letter: str) -> str: return rf'\mathcal{{{letter}}}' + def greek(self, name: str) -> str: + return f'\\{name}' + def prose(self, text: str) -> str: return rf'\text{{{_escape(text)}}}' diff --git a/src/math_spec/typeset/markdown.py b/src/math_spec/typeset/markdown.py index cdeb6afa..1bb6a8cf 100644 --- a/src/math_spec/typeset/markdown.py +++ b/src/math_spec/typeset/markdown.py @@ -65,6 +65,9 @@ def upright(self, name: str) -> str: def script(self, letter: str) -> str: return _LATEX.script(letter) + def greek(self, name: str) -> str: + return _LATEX.greek(name) + def prose(self, text: str) -> str: return _LATEX.prose(text) diff --git a/src/math_spec/typeset/symbols.py b/src/math_spec/typeset/symbols.py index 9ae0766d..48ba994a 100644 --- a/src/math_spec/typeset/symbols.py +++ b/src/math_spec/typeset/symbols.py @@ -38,27 +38,72 @@ _INDEX_ALIASES = {'snapshot': 't', 'snapshots': 't', 'time': 't', 'timestep': 't', 'timesteps': 't'} -def _word(name: str, fmt: Format) -> str: - """One name as one symbol: a letter stays a letter, a word is set italic.""" +#: Names that are a Greek letter written out. A variable called ``theta`` +#: printed as the italic word *theta* is the one derived symbol a paper would +#: never accept, and the fix is the same shape as ``_INDEX_ALIASES``: a small +#: curated map, with ``--symbols`` for anything it does not know. Lower case +#: only — every one of these has a letter in LaTeX and in Typst, which the +#: capitals do not, and ``omicron`` is left out because LaTeX spells it ``o``. +_GREEK = frozenset( + { + 'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'zeta', 'eta', 'theta', + 'iota', 'kappa', 'lambda', 'mu', 'nu', 'xi', 'rho', 'sigma', 'tau', + 'upsilon', 'phi', 'chi', 'psi', 'omega', + } +) # fmt: skip + + +def _word(name: str, fmt: Format, *, given: bool) -> str: + r"""One name as one symbol, upright where it is *given*. + + **Upright is what the data supplies; italic is what the solver chooses.** + Which of the two a symbol is, is the distinction a linear model cannot + afford to leave to a legend — quote one equation on a slide and a reader + has to know which side of it the solver is on — and it completes a system + the page was already three-quarters of the way through: script for index + sets, upright for the maps and qualifiers a model is handed, italic for + quantities. The cut this adds is *inside* italic. + + A name that *is* a Greek letter is set as the letter **where it is + chosen**, which is what the author meant by writing it out. Where it is + given, the rule wins and the name prints upright as the word: LaTeX has no + upright lower-case Greek without ``upgreek``, and taking that dependency + would break two things this repository holds — a preamble installable from + a two-package TeX, and `to_markdown` output that renders the same on GitHub + as on the docs site, GitHub's MathJax being unconfigurable. An italic + ``\eta`` that might be either is worse than an upright ``\mathrm{eta}`` + that is one; a table entry is how an author whose own preamble loads + ``upgreek`` writes ``\upeta`` instead. + """ + if given: + return fmt.upright(name) + if name in _GREEK: + return fmt.greek(name) return name if len(name) == 1 else fmt.italic(name) -def _derive_name_symbol(name: str, declared: frozenset[str], fmt: Format) -> str: +def _derive_name_symbol(name: str, declared: frozenset[str], fmt: Format, *, given: bool = False) -> str: r"""``p`` → ``p``; ``load`` → ``\mathit{load}``; ``p_max`` → ``p^{\mathrm{max}}``. An underscore is a **qualifier** only when what precedes it is a symbol in - its own right — a single letter (``p_max``) or another declared name - (``soc_max``). Everywhere else it is word separation, where splitting - produces nonsense: ``marginal_cost`` is not *marginal* raised to *cost*. + its own right — a single letter (``p_max``), a Greek letter written out + (``theta_max``), or another declared **quantity** (``soc_max``). + Everywhere else it is word separation, where splitting produces nonsense: + ``marginal_cost`` is not *marginal* raised to *cost*. + + A quantity, not a dimension: ``zone_cap`` is a capacity *indexed by* zone, + not a zone qualified by cap, and reading the axis as the head made a + parameter's symbol depend on whether some unrelated dimension happened to + share its prefix. The fallback therefore prints the name as written, underscore and all, which is plain rather than beautiful; ``--symbols`` is what makes it pretty. A qualifier lands in the superscript, the subscript slot being spoken for by the dimensions. """ head, _, tail = name.partition('_') - if tail and (len(head) == 1 or head in declared): - return fmt.superscript(_word(head, fmt), fmt.upright(tail.replace('_', ','))) - return _word(name, fmt) + if tail and (len(head) == 1 or head in _GREEK or head in declared): + return fmt.superscript(_word(head, fmt, given=given), fmt.upright(tail.replace('_', ','))) + return _word(name, fmt, given=given) class Symbols: @@ -71,6 +116,11 @@ class Symbols: single-letter name symbols are kept off the index letters, a ``\mathit{load}`` never colliding with a ``t``. + Which now means **variables**, since a parameter is upright: a dimension + may take ``p`` beside a parameter ``p``, because ``\mathrm{p}`` and ``p`` + are not the same symbol on the page. The guard shrank to exactly the + collisions that are still collisions. + Raises: SchemaError: If *table* is written in a notation *fmt* does not read. """ @@ -82,10 +132,21 @@ def __init__(self, schema: Buildable, fmt: Format, table: SymbolTable) -> None: f'and nothing translates between notations — write a {fmt.notation} table.' ) raise SchemaError(msg) - declared = frozenset({*schema.dimensions, *schema.parameters, *schema.variables}) - + # quantities only — see `_derive_name_symbol` for why an axis is not a + # head a qualifier may hang off + declared = frozenset({*schema.parameters, *schema.variables}) + + #: Names whose symbol came from the table rather than the derivation. + #: The convention note quotes only the others: a table is printed + #: verbatim and is the author's to write, so a symbol it supplies is + #: not one the note governs — the homepage's own table maps two + #: parameters to italic symbols, and the note quoting one of those + #: contradicted itself on the page. + self.overridden = frozenset(table.names) & {*schema.parameters, *schema.variables} self.name: dict[str, str] = { - name: table.names[name] if name in table.names else _derive_name_symbol(name, declared, fmt) + name: table.names[name] + if name in table.names + else _derive_name_symbol(name, declared, fmt, given=name in schema.parameters) for name in (*schema.parameters, *schema.variables) } spoken_for = {s for s in self.name.values() if len(s) == 1} diff --git a/src/math_spec/typeset/typst.py b/src/math_spec/typeset/typst.py index 4b618e45..70a51cb5 100644 --- a/src/math_spec/typeset/typst.py +++ b/src/math_spec/typeset/typst.py @@ -105,6 +105,9 @@ def upright(self, name: str) -> str: def script(self, letter: str) -> str: return f'cal({letter})' + def greek(self, name: str) -> str: + return name + def prose(self, text: str) -> str: return f'upright({_quote(text)})' diff --git a/src/math_spec/typeset/walk.py b/src/math_spec/typeset/walk.py index 75e9b1ec..cc159461 100644 --- a/src/math_spec/typeset/walk.py +++ b/src/math_spec/typeset/walk.py @@ -213,8 +213,9 @@ def indexed(self, symbol: str, dims: list[str]) -> str: class Walk: """Walks a validated schema, emitting :class:`Line`s in one format. - Stateful only in what it has *noticed* — which edge policies appeared, - which the legend needs to explain the symbols they print. + Stateful only in what it has *noticed* — which edge policies appeared and + whether a translation was counted inside a group, which the legend needs + to explain the symbols they print. """ def __init__(self, schema: Buildable, namespace: Namespace, symbols: Symbols, fmt: Format) -> None: @@ -223,6 +224,7 @@ def __init__(self, schema: Buildable, namespace: Namespace, symbols: Symbols, fm self.symbols = symbols self.format = fmt self.policies: set[str] = set() + self.grouped = False def op(self, name: str) -> str: return self.format.operators[name] @@ -230,16 +232,23 @@ def op(self, name: str) -> str: def translation(self, step: _Step, group: str = '') -> str: """The operator for one translation, carrying its fill and its group. - Both ride the operator, so a call with both writes *one* subscript - group: two subscripts on one symbol is a TeX error rather than a - rendering, and the equation carrying it stopped compiling (#1165). + Both ride the operator, and they take **different slots**: the fill + below, the partition above. Sharing one subscript is what a single + symbol allows — two subscripts is a TeX error rather than a rendering + (#1165) — but comma-joined there, ``0,season_of(t)`` said nothing about + which of the two was the value standing at the boundary and which was + the group the translation stays inside. """ backward, forward = _TRANSLATIONS[step.policy] # a named offset is always backward: `by=-p` is refused, so the sign is # in the data and the operator cannot read it off the call operator = self.op(backward if isinstance(step.by, str) or step.by > 0 else forward) - indices = [index for index in (step.fill, group) if index] - return self.format.subscript(operator, indices) if indices else operator + if step.fill: + operator = self.format.subscript(operator, [step.fill]) + if not group: + return operator + self.grouped = True + return self.format.superscript(operator, group) def context(self, ceiling: int = 1) -> _Context: return _Context(self, ceiling=ceiling) @@ -275,8 +284,13 @@ def _arithmetic(self, node: ArithmeticNode, ctx: _Context) -> tuple[str, int]: return ctx.indexed(self.symbols.name[node.name], list(self.schema.variables[node.name].foreach)), _ATOM if isinstance(node, UnaryOperatorNode): - operand = self.arithmetic(node.operand, ctx, need=2) - return (f'{self.op("minus")}{operand}' if node.op == '-' else operand), 1 + text, precedence = self._arithmetic(node.operand, ctx) + operand = self.format.parenthesise(text) if precedence < 2 else text + if node.op != '-': + return operand, precedence + # the operand's own brackets delimit the negation, so `-(Σ x) · 3` + # needs no second pair around the whole of it + return f'{self.op("minus")}{operand}', 2 if precedence < 2 else 1 if isinstance(node, BinaryOperatorNode): return self._binary(node, ctx) @@ -317,9 +331,15 @@ def _binary(self, node: BinaryOperatorNode, ctx: _Context) -> tuple[str, int]: return self.format.superscript(base, self.arithmetic(node.right, ctx)), _PRECEDENCE['**'] precedence = _PRECEDENCE[node.op] left = self.arithmetic(node.left, ctx, need=precedence) - right = self.arithmetic(node.right, ctx, need=precedence + (1 if node.op == '-' else 0)) + # `a + -b` is a spelling nobody uses: a negation under a `+` is the + # subtraction it already means, and folding it also hands the right + # operand subtraction's bracket rule, which is the one it needs + operand, op = node.right, node.op + if op == '+' and isinstance(operand, UnaryOperatorNode) and operand.op == '-': + operand, op = operand.operand, '-' + right = self.arithmetic(operand, ctx, need=_PRECEDENCE[op] + (1 if op == '-' else 0)) names = {'*': 'cdot', '+': 'plus', '-': 'minus'} - return self.format.joined([left, right], self.op(names[node.op])), precedence + return self.format.joined([left, right], self.op(names[op])), precedence def _call(self, node: FunctionCallNode, ctx: _Context) -> tuple[str, int]: """Render an operator: a translation at the leaves, or a summation. @@ -516,7 +536,16 @@ def position(self, dimension: str, at: int, grouping: str | None = None) -> str: return self.format.apply(self.format.upright('index'), ', '.join(parts)) def conjoined(self, ctx: _Context, *nodes: WhereNode | None) -> str: - parts = [self.where(n, ctx, need=1) for n in nodes if n is not None] + r"""The mask on a quantifier, as one condition. + + A mask that is *only* ``True`` prints nothing: the language says it is + the same as no ``where`` at all, so a quantifier reading ``: \top`` + would put a condition on the page that reads as one and is not. Nested + it still prints — ``\top \wedge x`` is what the file says, and + simplifying a mask is resolution's job rather than the typesetter's. + """ + kept = [n for n in nodes if n is not None and not (isinstance(n, BooleanLiteralNode) and n.value)] + parts = [self.where(n, ctx, need=1) for n in kept] return self.format.joined(parts, self.op('and')) if parts else '' def quantifier(self, dims: list[str], condition: str) -> str: @@ -694,6 +723,32 @@ def _coords(self, dim: str) -> str: clauses.append(f' carrying label{plural} {self.format.math(named)}') return ''.join(clauses) + def convention_notes(self) -> list[str]: + """What the two faces mean, said once, with the model's own symbols. + + A nomenclature table already splits the legend into parameters and + variables, and that is the convention a paper in this field uses. It is + a *lookup* rather than a reading, though: quote one equation on a slide + and the distinction is gone. So the symbols carry it, and this is where + the page says which face is which. + + Only where the model has both, and only quoting symbols the + *derivation* produced: a table is printed verbatim and is the author's + to write, so a symbol it supplies is not one this note governs. + """ + derived = [ + next((n for n in names if n not in self.symbols.overridden), None) + for names in (self.schema.parameters, self.schema.variables) + ] + if not all(derived): + return [] + given, chosen = (self.format.math(self.symbols.name[n]) for n in derived if n is not None) + return [ + f'Upright is what the model is given {self.format.dash} a parameter such as {given}, a coordinate ' + f'map, a label {self.format.dash} and italic is what the solver chooses, such as {chosen}. ' + f'An index is italic too, being what a quantifier chooses, and a set is script.' + ] + def translation_notes(self) -> list[str]: """A sentence for each translation symbol the model actually printed. @@ -717,4 +772,18 @@ def translation_notes(self) -> list[str]: f'{self.format.math("t-k")} leaves the dimension ({self.format.mono("shift(edge=v)")}), so the row ' f'at that boundary is built and carries {self.format.math("v")} rather than being dropped.' ) + if self.grouped: + applied = self.format.apply(self.format.upright('lookup'), 't') + counted = self.format.math(f't {self.format.superscript(self.op("cyclic_minus"), applied)} k') + note = ( + f'{counted} denotes a translation counted inside the group a lookup puts {self.format.math("t")} ' + f'in ({self.format.mono("shift(by=lookup)")}), so a term never crosses out of its own group.' + ) + if 'edge' in self.policies: + both = self.format.superscript(self.format.subscript(self.op('edge_minus'), ['v']), applied) + note += ( + f' The two modifiers take different slots {self.format.dash} the group above, the fill ' + f'below {self.format.dash} so {self.format.math(f"t {both} k")} is both at once.' + ) + notes.append(note) return notes diff --git a/tests/test_docs.py b/tests/test_docs.py index c1a34e4c..e51894bd 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -4,44 +4,73 @@ """The generated half of the documentation, held to what generates it. -`docs/examples/` shows each model beside the math the typesetter prints from -it. Written by hand, that math would be a claim nothing checks — on a site -whose subject is the math a file means, and in a repository that owns the -renderer which would have caught it. So the block is generated, and this is -what makes "generated" true of the committed file rather than of a script -nobody runs. +The site shows models beside the math the typesetter prints from them. Written +by hand, that math would be a claim nothing checks — on a site whose subject is +the math a file means, and in a repository that owns the renderer which would +have caught it. So the blocks are generated, and this is what makes +"generated" true of the committed files rather than of scripts nobody runs. + +Three of the five pages below were stale at some point with the whole suite +green, which is why the last test in this module exists: a tool that can tell +a page has drifted is one a test has to *ask*. """ from __future__ import annotations +from functools import partial +from pathlib import Path +from typing import TYPE_CHECKING + import pytest from math_spec.model import PIECEWISE_METHODS -from tools import gallery, notation +from tools import gallery, home_math, notation, spec_math + +if TYPE_CHECKING: + from collections.abc import Callable + +ROOT = Path(__file__).resolve().parent.parent + +#: Every committed page a generator writes: how to re-render it, and which +#: tool rewrites it. Adding a generator means adding a row here, which +#: `test_every_generator_is_asked` is what says out loud. +GENERATED: list[tuple[str, Path, Callable[[str], str], str]] = [ + *( + (f'gallery:{name}', gallery.PAGES / name, partial(gallery.rendered, name), 'gallery') + for name in gallery.pages() + ), + ('notation', notation.PAGE, notation.rendered_page, 'notation'), + ('operators', spec_math.PAGE, spec_math.rendered, 'spec_math'), + ('home:index', home_math.PAGE, home_math.rendered_page, 'home_math'), + ('home:readme', home_math.README, home_math.rendered_readme, 'home_math'), +] -@pytest.mark.parametrize('page', gallery.pages()) -def test_the_gallery_math_is_current(page: str): - path = gallery.PAGES / page +@pytest.mark.parametrize( + ('path', 'render', 'tool'), + [row[1:] for row in GENERATED], + ids=[row[0] for row in GENERATED], +) +def test_the_generated_page_is_current(path: Path, render: Callable[[str], str], tool: str): text = path.read_text() - assert gallery.rendered(page, text) == text, ( - f'docs/examples/{page} no longer matches the model it shows — run `pixi run python -m tools.gallery`' + assert render(text) == text, ( + f'{path.relative_to(ROOT)} no longer matches what it is generated from — run `pixi run python -m tools.{tool}`' ) -def test_the_notation_page_is_current(): - """The same claim, for the page that shows every construct at once. +def test_every_generator_is_asked(): + """A tool that can say a page is stale needs a test that asks it. - It went unmade for longer, and cost more: four of the models the page - renders from were left behind when the language was extracted, so - `tools/notation.py` raised `FileNotFoundError` on the curve section and - the committed page kept showing math no model here produced. Nothing - failed, because nothing ran it. + Three of the pages above were stale at some point with the suite green: the + notation page for the whole life of the extraction, because the models it + renders stayed behind and nothing ran `--check` (#41); then the homepage and + the operator reference, under a change to how a parameter prints. Each time + the tool knew and nothing asked it, so the next generator lands with its row + or this fails — which is cheaper than a reader finding the drift. """ - text = notation.PAGE.read_text() - assert notation.rendered_page(text) == text, ( - 'docs/reference/notation.md no longer matches the fixture it is generated from — ' - 'run `pixi run python -m tools.notation`' + detects_drift = {path.stem for path in (ROOT / 'tools').glob('*.py') if '--check' in path.read_text()} + assert detects_drift == {tool for *_, tool in GENERATED}, ( + 'a tool that can detect a stale page has no row in GENERATED, or a row names a tool that cannot' ) diff --git a/tests/typeset/golden/latex.out b/tests/typeset/golden/latex.out index f0bbee8b..ddaf395f 100644 --- a/tests/typeset/golden/latex.out +++ b/tests/typeset/golden/latex.out @@ -19,17 +19,18 @@ \paragraph{Parameters} \begin{description} -\item[$p^{\mathrm{max}}$] \texttt{p\_max} over $\mathcal{G}$ -\item[$p^{\mathrm{min}}$] \texttt{p\_min} over $\mathcal{G}$ -\item[$\mathit{cost}$] \texttt{cost} over $\mathcal{G}$ -\item[$\mathit{load}$] \texttt{load} over $\mathcal{T} \times \mathcal{B}$ -\item[$\mathit{is\_flexible}$] \texttt{is\_flexible} over $\mathcal{G}$ -\item[$\mathit{zone}^{\mathrm{cap}}$] \texttt{zone\_cap} over $\mathcal{Z}$ -\item[$\mathit{tech\_cap}$] \texttt{tech\_cap} over $\mathcal{B} \times \mathcal{E}$ -\item[$\mathit{min\_up}$] \texttt{min\_up} over $\mathcal{G}$ -\item[$\mathit{lead}$] \texttt{lead} over $\mathcal{G}$ -\item[$\mathit{budget}$] \texttt{budget} (scalar) -\item[$\mathit{growth}$] \texttt{growth} (scalar) +\item[$\mathrm{p}^{\mathrm{max}}$] \texttt{p\_max} over $\mathcal{G}$ +\item[$\mathrm{p}^{\mathrm{min}}$] \texttt{p\_min} over $\mathcal{G}$ +\item[$\mathrm{cost}$] \texttt{cost} over $\mathcal{G}$ +\item[$\mathrm{load}$] \texttt{load} over $\mathcal{T} \times \mathcal{B}$ +\item[$\mathrm{is\_flexible}$] \texttt{is\_flexible} over $\mathcal{G}$ +\item[$\mathrm{zone\_cap}$] \texttt{zone\_cap} over $\mathcal{Z}$ +\item[$\mathrm{tech\_cap}$] \texttt{tech\_cap} over $\mathcal{B} \times \mathcal{E}$ +\item[$\mathrm{min\_up}$] \texttt{min\_up} over $\mathcal{G}$ +\item[$\mathrm{eta}$] \texttt{eta} over $\mathcal{G}$ +\item[$\mathrm{lead}$] \texttt{lead} over $\mathcal{G}$ +\item[$\mathrm{budget}$] \texttt{budget} (scalar) +\item[$\mathrm{growth}$] \texttt{growth} (scalar) \end{description} \paragraph{Variables} @@ -37,7 +38,7 @@ \item[$p$] \texttt{p} over $\mathcal{T} \times \mathcal{G}$ \item[$\mathit{spill}$] \texttt{spill} over $\mathcal{T}$ \item[$\mathit{slack}$] \texttt{slack} over $\mathcal{T}$ -\item[$\mathit{theta}$] \texttt{theta} over $\mathcal{B}$ +\item[$\theta$] \texttt{theta} over $\mathcal{B}$ \item[$\mathit{on}$] \texttt{on} over $\mathcal{T} \times \mathcal{G}$ \item[$\mathit{units}$] \texttt{units} over $\mathcal{G}$ \item[$\mathit{spare}$] \texttt{spare} over $\mathcal{G}$ @@ -47,53 +48,59 @@ \item[$\mathit{weight}$] \texttt{weight} over $\mathcal{T} \times \mathcal{G}$ \end{description} +\noindent 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. + \noindent $t \ominus k$ denotes cyclic translation: index $t-k$ taken modulo the size of the dimension (\texttt{roll}). Plain $t-k$ (\texttt{shift}) has no wraparound --- terms translated past the edge are simply absent. \noindent $t \boxminus_{v} k$ denotes translation with $v$ standing where index $t-k$ leaves the dimension (\texttt{shift(edge=v)}), so the row at that boundary is built and carries $v$ rather than being dropped. +\noindent $t \ominus^{\mathrm{lookup}(t)} k$ denotes a translation counted inside the group a lookup puts $t$ in (\texttt{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. + \paragraph{Objective} \begin{align} - && \max & \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} \cdot \mathit{cost}_{g} + \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} \cdot p_{t,g} \cdot \mathit{cost}_{g} + \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} \cdot \mathit{cost}_{g} \cdot \mathit{growth}^{\mathit{lead}_{g}} + \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} \cdot p^{\mathrm{max}}_{g} - \mathit{reserve} + -\mathit{headroom} + && \max & \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} \cdot \mathrm{cost}_{g} + \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} \cdot p_{t,g} \cdot \mathrm{cost}_{g} + \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} \cdot \mathrm{cost}_{g} \cdot \mathrm{growth}^{\mathrm{lead}_{g}} + \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} \cdot \mathrm{p}^{\mathrm{max}}_{g} - \mathit{reserve} - \mathit{headroom} \end{align} \paragraph{Subject to} \begin{align} -\text{balance} && \sum_{g \in \mathcal{G} \,:\, \mathrm{gen\_bus}(g) = b} p_{t,g} + \mathit{spill}_{t} - \mathit{slack}_{t} & = \mathit{load}_{t,b} && \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \\ -\text{ramp} && p_{t,g} - p_{t \ominus 1,g} & \le p_{t - 1,g} + p^{\mathrm{max}}_{g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ -\text{edges} && p_{t \boxminus_{0} 1,g} & \le p_{t \boxplus_{0} 1,g} + p^{\mathrm{max}}_{g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ +\text{balance} && \sum_{g \in \mathcal{G} \,:\, \mathrm{gen\_bus}(g) = b} p_{t,g} + \mathit{spill}_{t} - \mathit{slack}_{t} & = \mathrm{load}_{t,b} && \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \\ +\text{ramp} && p_{t,g} - p_{t \ominus 1,g} & \le p_{t - 1,g} + \mathrm{p}^{\mathrm{max}}_{g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ +\text{edges} && p_{t \boxminus_{0} 1,g} & \le p_{t \boxplus_{0} 1,g} + \mathrm{p}^{\mathrm{max}}_{g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ \text{ahead} && p_{t,g} & \le p_{t \oplus 1,g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ -\text{composed} && p_{t - 2,g} & \le p^{\mathrm{max}}_{g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ -\text{uncomposed} && p_{\left( t - 1 \right) - \mathit{lead},g} & \le p^{\mathrm{max}}_{g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ -\text{crossed} && p_{t \ominus 1,g + 1} & \le p^{\mathrm{max}}_{g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ -\text{lead\_time} && p_{t \boxminus_{0} \mathit{lead},g} & \le p^{\mathrm{max}}_{g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ -\text{in\_season} && p_{t,g} & \le p_{t \ominus_{\mathrm{season\_of}(t)} 1,g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ -\text{held\_in\_season} && p_{t,g} & \le p_{t \boxminus_{0,\mathrm{season\_of}(t)} 1,g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ +\text{composed} && p_{t - 2,g} & \le \mathrm{p}^{\mathrm{max}}_{g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ +\text{uncomposed} && p_{\left( t - 1 \right) - \mathrm{lead},g} & \le \mathrm{p}^{\mathrm{max}}_{g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ +\text{crossed} && p_{t \ominus 1,g + 1} & \le \mathrm{p}^{\mathrm{max}}_{g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ +\text{lead\_time} && p_{t \boxminus_{0} \mathrm{lead},g} & \le \mathrm{p}^{\mathrm{max}}_{g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ +\text{in\_season} && p_{t,g} & \le p_{t \ominus^{\mathrm{season\_of}(t)} 1,g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ +\text{held\_in\_season} && p_{t,g} & \le p_{t \boxminus_{0}^{\mathrm{season\_of}(t)} 1,g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ \text{window} && \sum_{t' \in \mathcal{T} \,:\, 0 \le t - t' < 3} \mathit{on}_{t',g} & \le \mathit{units}_{g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ -\text{history} && \sum_{t' \in \mathcal{T} \,:\, 0 \le t \ominus t' < \mathit{min\_up}} \mathit{on}_{t',g} & \le \mathit{units}_{g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ -\text{pullback} && \mathit{spill}_{t} & \le \mathit{zone}^{\mathrm{cap}}_{\mathrm{zone\_of}(b)} && \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \\ -\text{grouped\_twice} && \sum_{g \in \mathcal{G} \,:\, \mathrm{gen\_bus}(g) = b \wedge \mathrm{gen\_tech}(g) = e} p_{t,g} & \le \mathit{tech\_cap}_{b,e} && \forall\, t \in \mathcal{T},\ b \in \mathcal{B},\ e \in \mathcal{E} \\ -\text{pulled\_back\_twice} && \mathit{units}_{g} & \le \mathit{tech\_cap}_{\mathrm{gen\_bus}(g),\mathrm{gen\_tech}(g)} && \forall\, g \in \mathcal{G} \\ -\text{arithmetic} && \sum_{g \in \mathcal{G}} \left( \frac{p_{t,g}}{2} + -\mathit{cost}_{g} \right) & \ge \left( -\left( \sum_{g \in \mathcal{G}} p_{t,g} \right) \right) \cdot 3 && \forall\, t \in \mathcal{T} \\ -\text{total} && \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} & \le \mathit{budget} \\ -\text{scalar} && \mathit{units}_{g} & \le \mathit{budget} && \forall\, g \in \mathcal{G} \,:\, \mathit{cost}_{g} \text{ is defined} \\ -\text{running} && \mathit{theta}_{b} & \le \mathit{load}_{t,b} && \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \,:\, \mathit{theta}_{b} \text{ exists} \wedge t \ge 3 \\ +\text{history} && \sum_{t' \in \mathcal{T} \,:\, 0 \le t \ominus t' < \mathrm{min\_up}} \mathit{on}_{t',g} & \le \mathit{units}_{g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ +\text{pullback} && \mathit{spill}_{t} & \le \mathrm{zone\_cap}_{\mathrm{zone\_of}(b)} && \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \\ +\text{grouped\_twice} && \sum_{g \in \mathcal{G} \,:\, \mathrm{gen\_bus}(g) = b \wedge \mathrm{gen\_tech}(g) = e} p_{t,g} & \le \mathrm{tech\_cap}_{b,e} && \forall\, t \in \mathcal{T},\ b \in \mathcal{B},\ e \in \mathcal{E} \\ +\text{pulled\_back\_twice} && \mathit{units}_{g} & \le \mathrm{tech\_cap}_{\mathrm{gen\_bus}(g),\mathrm{gen\_tech}(g)} && \forall\, g \in \mathcal{G} \\ +\text{arithmetic} && \sum_{g \in \mathcal{G}} \left( \frac{p_{t,g}}{2} - \mathrm{cost}_{g} \right) & \ge -\left( \sum_{g \in \mathcal{G}} p_{t,g} \right) \cdot 3 && \forall\, t \in \mathcal{T} \\ +\text{total} && \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} & \le \mathrm{budget} \\ +\text{scalar} && \mathit{units}_{g} & \le \mathrm{budget} && \forall\, g \in \mathcal{G} \,:\, \mathrm{cost}_{g} \text{ is defined} \\ +\text{running} && \theta_{b} & \le \mathrm{load}_{t,b} && \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \,:\, \theta_{b} \text{ exists} \wedge t \ge 3 \\ \text{first} && \mathit{on}_{t,g} & = 1 && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \left( t = \mathrm{index}(\mathcal{T}, 0) \vee t = \mathrm{index}(\mathcal{T}, 0, \mathrm{season\_of}(t)) \right) \\ -\text{northern} && \mathit{slack}_{t} & \le \mathit{load}_{t,b} && \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \,:\, \mathrm{zone\_of}(b) = \text{north} \wedge \mathrm{zone\_of}(b) \neq \mathrm{area\_of}(b) \wedge \mathrm{zone\_of}(b) \text{ is defined} \\ -\text{always} && \mathit{spill}_{t} & \ge 0 && \forall\, t \in \mathcal{T} \,:\, \top \\ +\text{northern} && \mathit{slack}_{t} & \le \mathrm{load}_{t,b} && \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \,:\, \mathrm{zone\_of}(b) = \text{north} \wedge \mathrm{zone\_of}(b) \neq \mathrm{area\_of}(b) \wedge \mathrm{zone\_of}(b) \text{ is defined} \\ +\text{efficiency} && p_{t,g} & \le \mathrm{eta}_{g} \cdot \mathrm{p}^{\mathrm{max}}_{g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ +\text{always} && \mathit{spill}_{t} & \ge 0 && \forall\, t \in \mathcal{T} \\ +\text{redundant} && \mathit{spill}_{t} & \ge 0 && \forall\, t \in \mathcal{T} \,:\, \top \wedge \mathit{spill}_{t} \text{ exists} \\ \text{never} && \mathit{slack}_{t} & \ge 0 && \forall\, t \in \mathcal{T} \,:\, \bot \end{align} \paragraph{Variable domains} \begin{align} -\text{p} && p^{\mathrm{min}}_{g} \le p_{t,g} & \le p^{\mathrm{max}}_{g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \left( p^{\mathrm{max}}_{g} > 0 \wedge \neg \mathit{is\_flexible}_{g} \vee p^{\mathrm{min}}_{g} > 0 \right) \\ +\text{p} && \mathrm{p}^{\mathrm{min}}_{g} \le p_{t,g} & \le \mathrm{p}^{\mathrm{max}}_{g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \left( \mathrm{p}^{\mathrm{max}}_{g} > 0 \wedge \neg \mathrm{is\_flexible}_{g} \vee \mathrm{p}^{\mathrm{min}}_{g} > 0 \right) \\ \text{spill} && \mathit{spill}_{t} & \ge 0 && \forall\, t \in \mathcal{T} \\ \text{slack} && \mathit{slack}_{t} & \le 100 && \forall\, t \in \mathcal{T} \\ -\text{theta} && \mathit{theta}_{b} & \in \mathbb{R} && \forall\, b \in \mathcal{B} \\ +\text{theta} && \theta_{b} & \in \mathbb{R} && \forall\, b \in \mathcal{B} \\ \text{on} && \mathit{on}_{t,g} & \in \{0, 1\} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ \text{units} && 0 \le \mathit{units}_{g} & \le 10, \mathit{units}_{g} \in \mathbb{Z} && \forall\, g \in \mathcal{G} \\ \text{spare} && \mathit{spare}_{g} & \in \mathbb{Z} && \forall\, g \in \mathcal{G} \\ \text{reserve} && \mathit{reserve} & \ge 0 \\ -\text{headroom} && \mathit{headroom} & \ge 0 && \text{where } \mathit{budget} \text{ is defined} \\ +\text{headroom} && \mathit{headroom} & \ge 0 && \text{where } \mathrm{budget} \text{ is defined} \\ \text{void} && \infty \le \mathit{void}_{b} & \le -\infty && \forall\, b \in \mathcal{B} \\ \text{weight} && 0 \le \mathit{weight}_{t,g} & \le 1 && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ \text{weight sos} && \left( \mathit{weight}_{t,g} \right)_{g \in \mathcal{G}} & \in \mathrm{SOS}2 && \forall\, t \in \mathcal{T} diff --git a/tests/typeset/golden/markdown.out b/tests/typeset/golden/markdown.out index a5009f64..c3334bab 100644 --- a/tests/typeset/golden/markdown.out +++ b/tests/typeset/golden/markdown.out @@ -17,17 +17,18 @@ every character a notation escapes, set as text: link_to, 100% & #1 costs $5 {ne | Symbol | Meaning | |---|---| -| $p^{\mathrm{max}}$ | `p_max` over $\mathcal{G}$ | -| $p^{\mathrm{min}}$ | `p_min` over $\mathcal{G}$ | -| $\mathit{cost}$ | `cost` over $\mathcal{G}$ | -| $\mathit{load}$ | `load` over $\mathcal{T} \times \mathcal{B}$ | -| $\mathit{is\_flexible}$ | `is_flexible` over $\mathcal{G}$ | -| $\mathit{zone}^{\mathrm{cap}}$ | `zone_cap` over $\mathcal{Z}$ | -| $\mathit{tech\_cap}$ | `tech_cap` over $\mathcal{B} \times \mathcal{E}$ | -| $\mathit{min\_up}$ | `min_up` over $\mathcal{G}$ | -| $\mathit{lead}$ | `lead` over $\mathcal{G}$ | -| $\mathit{budget}$ | `budget` (scalar) | -| $\mathit{growth}$ | `growth` (scalar) | +| $\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 @@ -36,7 +37,7 @@ every character a notation escapes, set as text: link_to, 100% & #1 costs $5 {ne | $p$ | `p` over $\mathcal{T} \times \mathcal{G}$ | | $\mathit{spill}$ | `spill` over $\mathcal{T}$ | | $\mathit{slack}$ | `slack` over $\mathcal{T}$ | -| $\mathit{theta}$ | `theta` over $\mathcal{B}$ | +| $\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}$ | @@ -45,27 +46,31 @@ every character a notation escapes, set as text: link_to, 100% & #1 costs $5 {ne | $\mathit{void}$ | `void` over $\mathcal{B}$ | | $\mathit{weight}$ | `weight` over $\mathcal{T} \times \mathcal{G}$ | +Upright is what the model is given — a parameter such as $\mathrm{p}^{\mathrm{max}}$, a coordinate map, a label — and italic is what the solver chooses, such as $p$. An index is italic too, being what a quantifier chooses, and a set is script. + $t \ominus k$ denotes cyclic translation: index $t-k$ taken modulo the size of the dimension (`roll`). Plain $t-k$ (`shift`) has no wraparound — terms translated past the edge are simply absent. $t \boxminus_{v} k$ denotes translation with $v$ standing where index $t-k$ leaves the dimension (`shift(edge=v)`), so the row at that boundary is built and carries $v$ rather than being dropped. +$t \ominus^{\mathrm{lookup}(t)} k$ denotes a translation counted inside the group a lookup puts $t$ in (`shift(by=lookup)`), so a term never crosses out of its own group. The two modifiers take different slots — the group above, the fill below — so $t \boxminus_{v}^{\mathrm{lookup}(t)} k$ is both at once. + #### Objective -$$\max \sum_{t \in \mathcal{T},\enspace g \in \mathcal{G}} p_{t,g} \cdot \mathit{cost}_{g} + \sum_{t \in \mathcal{T},\enspace g \in \mathcal{G}} p_{t,g} \cdot p_{t,g} \cdot \mathit{cost}_{g} + \sum_{t \in \mathcal{T},\enspace g \in \mathcal{G}} p_{t,g} \cdot \mathit{cost}_{g} \cdot \mathit{growth}^{\mathit{lead}_{g}} + \sum_{t \in \mathcal{T},\enspace g \in \mathcal{G}} p_{t,g} \cdot p^{\mathrm{max}}_{g} - \mathit{reserve} + -\mathit{headroom}$$ +$$\max \sum_{t \in \mathcal{T},\enspace g \in \mathcal{G}} p_{t,g} \cdot \mathrm{cost}_{g} + \sum_{t \in \mathcal{T},\enspace g \in \mathcal{G}} p_{t,g} \cdot p_{t,g} \cdot \mathrm{cost}_{g} + \sum_{t \in \mathcal{T},\enspace g \in \mathcal{G}} p_{t,g} \cdot \mathrm{cost}_{g} \cdot \mathrm{growth}^{\mathrm{lead}_{g}} + \sum_{t \in \mathcal{T},\enspace g \in \mathcal{G}} p_{t,g} \cdot \mathrm{p}^{\mathrm{max}}_{g} - \mathit{reserve} - \mathit{headroom}$$ #### Subject to **`balance`** -$$\sum_{g \in \mathcal{G} \thinspace:\thinspace \mathrm{gen\_bus}(g) = b} p_{t,g} + \mathit{spill}_{t} - \mathit{slack}_{t} = \mathit{load}_{t,b} \qquad \forall\thinspace t \in \mathcal{T},\enspace b \in \mathcal{B}$$ +$$\sum_{g \in \mathcal{G} \thinspace:\thinspace \mathrm{gen\_bus}(g) = b} p_{t,g} + \mathit{spill}_{t} - \mathit{slack}_{t} = \mathrm{load}_{t,b} \qquad \forall\thinspace t \in \mathcal{T},\enspace b \in \mathcal{B}$$ **`ramp`** -$$p_{t,g} - p_{t \ominus 1,g} \le p_{t - 1,g} + p^{\mathrm{max}}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ +$$p_{t,g} - p_{t \ominus 1,g} \le p_{t - 1,g} + \mathrm{p}^{\mathrm{max}}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ **`edges`** -$$p_{t \boxminus_{0} 1,g} \le p_{t \boxplus_{0} 1,g} + p^{\mathrm{max}}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ +$$p_{t \boxminus_{0} 1,g} \le p_{t \boxplus_{0} 1,g} + \mathrm{p}^{\mathrm{max}}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ **`ahead`** @@ -73,27 +78,27 @@ $$p_{t,g} \le p_{t \oplus 1,g} \qquad \forall\thinspace t \in \mathcal{T},\enspa **`composed`** -$$p_{t - 2,g} \le p^{\mathrm{max}}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ +$$p_{t - 2,g} \le \mathrm{p}^{\mathrm{max}}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ **`uncomposed`** -$$p_{\left( t - 1 \right) - \mathit{lead},g} \le p^{\mathrm{max}}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ +$$p_{\left( t - 1 \right) - \mathrm{lead},g} \le \mathrm{p}^{\mathrm{max}}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ **`crossed`** -$$p_{t \ominus 1,g + 1} \le p^{\mathrm{max}}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ +$$p_{t \ominus 1,g + 1} \le \mathrm{p}^{\mathrm{max}}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ **`lead_time`** -$$p_{t \boxminus_{0} \mathit{lead},g} \le p^{\mathrm{max}}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ +$$p_{t \boxminus_{0} \mathrm{lead},g} \le \mathrm{p}^{\mathrm{max}}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ **`in_season`** -$$p_{t,g} \le p_{t \ominus_{\mathrm{season\_of}(t)} 1,g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ +$$p_{t,g} \le p_{t \ominus^{\mathrm{season\_of}(t)} 1,g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ **`held_in_season`** -$$p_{t,g} \le p_{t \boxminus_{0,\mathrm{season\_of}(t)} 1,g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ +$$p_{t,g} \le p_{t \boxminus_{0}^{\mathrm{season\_of}(t)} 1,g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ **`window`** @@ -101,35 +106,35 @@ $$\sum_{t' \in \mathcal{T} \thinspace:\thinspace 0 \le t - t' < 3} \mathit{on}_{ **`history`** -$$\sum_{t' \in \mathcal{T} \thinspace:\thinspace 0 \le t \ominus t' < \mathit{min\_up}} \mathit{on}_{t',g} \le \mathit{units}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ +$$\sum_{t' \in \mathcal{T} \thinspace:\thinspace 0 \le t \ominus t' < \mathrm{min\_up}} \mathit{on}_{t',g} \le \mathit{units}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ **`pullback`** -$$\mathit{spill}_{t} \le \mathit{zone}^{\mathrm{cap}}_{\mathrm{zone\_of}(b)} \qquad \forall\thinspace t \in \mathcal{T},\enspace b \in \mathcal{B}$$ +$$\mathit{spill}_{t} \le \mathrm{zone\_cap}_{\mathrm{zone\_of}(b)} \qquad \forall\thinspace t \in \mathcal{T},\enspace b \in \mathcal{B}$$ **`grouped_twice`** -$$\sum_{g \in \mathcal{G} \thinspace:\thinspace \mathrm{gen\_bus}(g) = b \wedge \mathrm{gen\_tech}(g) = e} p_{t,g} \le \mathit{tech\_cap}_{b,e} \qquad \forall\thinspace t \in \mathcal{T},\enspace b \in \mathcal{B},\enspace e \in \mathcal{E}$$ +$$\sum_{g \in \mathcal{G} \thinspace:\thinspace \mathrm{gen\_bus}(g) = b \wedge \mathrm{gen\_tech}(g) = e} p_{t,g} \le \mathrm{tech\_cap}_{b,e} \qquad \forall\thinspace t \in \mathcal{T},\enspace b \in \mathcal{B},\enspace e \in \mathcal{E}$$ **`pulled_back_twice`** -$$\mathit{units}_{g} \le \mathit{tech\_cap}_{\mathrm{gen\_bus}(g),\mathrm{gen\_tech}(g)} \qquad \forall\thinspace g \in \mathcal{G}$$ +$$\mathit{units}_{g} \le \mathrm{tech\_cap}_{\mathrm{gen\_bus}(g),\mathrm{gen\_tech}(g)} \qquad \forall\thinspace g \in \mathcal{G}$$ **`arithmetic`** -$$\sum_{g \in \mathcal{G}} \left( \frac{p_{t,g}}{2} + -\mathit{cost}_{g} \right) \ge \left( -\left( \sum_{g \in \mathcal{G}} p_{t,g} \right) \right) \cdot 3 \qquad \forall\thinspace t \in \mathcal{T}$$ +$$\sum_{g \in \mathcal{G}} \left( \frac{p_{t,g}}{2} - \mathrm{cost}_{g} \right) \ge -\left( \sum_{g \in \mathcal{G}} p_{t,g} \right) \cdot 3 \qquad \forall\thinspace t \in \mathcal{T}$$ **`total`** -$$\sum_{t \in \mathcal{T},\enspace g \in \mathcal{G}} p_{t,g} \le \mathit{budget}$$ +$$\sum_{t \in \mathcal{T},\enspace g \in \mathcal{G}} p_{t,g} \le \mathrm{budget}$$ **`scalar`** -$$\mathit{units}_{g} \le \mathit{budget} \qquad \forall\thinspace g \in \mathcal{G} \thinspace:\thinspace \mathit{cost}_{g} \text{ is defined}$$ +$$\mathit{units}_{g} \le \mathrm{budget} \qquad \forall\thinspace g \in \mathcal{G} \thinspace:\thinspace \mathrm{cost}_{g} \text{ is defined}$$ **`running`** -$$\mathit{theta}_{b} \le \mathit{load}_{t,b} \qquad \forall\thinspace t \in \mathcal{T},\enspace b \in \mathcal{B} \thinspace:\thinspace \mathit{theta}_{b} \text{ exists} \wedge t \ge 3$$ +$$\theta_{b} \le \mathrm{load}_{t,b} \qquad \forall\thinspace t \in \mathcal{T},\enspace b \in \mathcal{B} \thinspace:\thinspace \theta_{b} \text{ exists} \wedge t \ge 3$$ **`first`** @@ -137,11 +142,19 @@ $$\mathit{on}_{t,g} = 1 \qquad \forall\thinspace t \in \mathcal{T},\enspace g \i **`northern`** -$$\mathit{slack}_{t} \le \mathit{load}_{t,b} \qquad \forall\thinspace t \in \mathcal{T},\enspace b \in \mathcal{B} \thinspace:\thinspace \mathrm{zone\_of}(b) = \text{north} \wedge \mathrm{zone\_of}(b) \neq \mathrm{area\_of}(b) \wedge \mathrm{zone\_of}(b) \text{ is defined}$$ +$$\mathit{slack}_{t} \le \mathrm{load}_{t,b} \qquad \forall\thinspace t \in \mathcal{T},\enspace b \in \mathcal{B} \thinspace:\thinspace \mathrm{zone\_of}(b) = \text{north} \wedge \mathrm{zone\_of}(b) \neq \mathrm{area\_of}(b) \wedge \mathrm{zone\_of}(b) \text{ is defined}$$ + +**`efficiency`** + +$$p_{t,g} \le \mathrm{eta}_{g} \cdot \mathrm{p}^{\mathrm{max}}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$ **`always`** -$$\mathit{spill}_{t} \ge 0 \qquad \forall\thinspace t \in \mathcal{T} \thinspace:\thinspace \top$$ +$$\mathit{spill}_{t} \ge 0 \qquad \forall\thinspace t \in \mathcal{T}$$ + +**`redundant`** + +$$\mathit{spill}_{t} \ge 0 \qquad \forall\thinspace t \in \mathcal{T} \thinspace:\thinspace \top \wedge \mathit{spill}_{t} \text{ exists}$$ **`never`** @@ -151,7 +164,7 @@ $$\mathit{slack}_{t} \ge 0 \qquad \forall\thinspace t \in \mathcal{T} \thinspace **`p`** -$$p^{\mathrm{min}}_{g} \le p_{t,g} \le p^{\mathrm{max}}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G} \thinspace:\thinspace \left( p^{\mathrm{max}}_{g} > 0 \wedge \neg \mathit{is\_flexible}_{g} \vee p^{\mathrm{min}}_{g} > 0 \right)$$ +$$\mathrm{p}^{\mathrm{min}}_{g} \le p_{t,g} \le \mathrm{p}^{\mathrm{max}}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G} \thinspace:\thinspace \left( \mathrm{p}^{\mathrm{max}}_{g} > 0 \wedge \neg \mathrm{is\_flexible}_{g} \vee \mathrm{p}^{\mathrm{min}}_{g} > 0 \right)$$ **`spill`** @@ -163,7 +176,7 @@ $$\mathit{slack}_{t} \le 100 \qquad \forall\thinspace t \in \mathcal{T}$$ **`theta`** -$$\mathit{theta}_{b} \in \mathbb{R} \qquad \forall\thinspace b \in \mathcal{B}$$ +$$\theta_{b} \in \mathbb{R} \qquad \forall\thinspace b \in \mathcal{B}$$ **`on`** @@ -183,7 +196,7 @@ $$\mathit{reserve} \ge 0$$ **`headroom`** -$$\mathit{headroom} \ge 0 \qquad \text{where } \mathit{budget} \text{ is defined}$$ +$$\mathit{headroom} \ge 0 \qquad \text{where } \mathrm{budget} \text{ is defined}$$ **`void`** diff --git a/tests/typeset/golden/model.yaml b/tests/typeset/golden/model.yaml index b70d5653..477f2862 100644 --- a/tests/typeset/golden/model.yaml +++ b/tests/typeset/golden/model.yaml @@ -37,6 +37,7 @@ parameters: 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 @@ -132,12 +133,12 @@ constraints: pulled_back_twice: # its adjoint, reading one slot through a pair of labels foreach: [generator] expression: units <= at(tech_cap, by=[gen_bus, gen_tech]) - arithmetic: # division, unary minus, nested reduction, bracketing + arithmetic: # division, both unary signs, nested reduction, bracketing # No `**`: the walk renders it, but `lower_program` rejects it, so a model # using it would not be a model. Format.power stays exercised by unit # tests rather than from here. foreach: [snapshot] - expression: sum(p / 2 + -cost, over=generator) >= -sum(p, over=generator) * 3 + expression: sum(p / 2 + -cost, 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 foreach: [] expression: sum(p) <= budget @@ -157,11 +158,18 @@ constraints: foreach: [snapshot, bus] where: "zone_of == 'north' AND zone_of != area_of AND zone_of" expression: slack <= load - always: # the two constant masks, which are a quantifier and no equation + efficiency: # a Greek-named parameter, which is given — so the convention wins and it prints as the word + foreach: [snapshot, generator] + expression: p <= eta * p_max + always: # a mask that is only the constant true, which the language says is no mask at all — so none prints foreach: [snapshot] where: "true" expression: spill >= 0 - never: # the other constant mask + redundant: # the same constant *inside* a mask, where it is what the file says and prints + foreach: [snapshot] + where: "True AND spill" + expression: spill >= 0 + never: # the other constant mask, which says the rows are none and is worth seeing foreach: [snapshot] where: "false" expression: slack >= 0 diff --git a/tests/typeset/golden/typst.out b/tests/typeset/golden/typst.out index cade2a0f..ca55d692 100644 --- a/tests/typeset/golden/typst.out +++ b/tests/typeset/golden/typst.out @@ -12,23 +12,24 @@ every character a notation escapes, set as text: link\_to, 100% & \#1 costs \$5 / $cal(E)$: index $e$ --- `technology` == Parameters -/ $p^(upright("max"))$: `p_max` over $cal(G)$ -/ $p^(upright("min"))$: `p_min` over $cal(G)$ -/ $italic("cost")$: `cost` over $cal(G)$ -/ $italic("load")$: `load` over $cal(T) times cal(B)$ -/ $italic("is_flexible")$: `is_flexible` over $cal(G)$ -/ $italic("zone")^(upright("cap"))$: `zone_cap` over $cal(Z)$ -/ $italic("tech_cap")$: `tech_cap` over $cal(B) times cal(E)$ -/ $italic("min_up")$: `min_up` over $cal(G)$ -/ $italic("lead")$: `lead` over $cal(G)$ -/ $italic("budget")$: `budget` (scalar) -/ $italic("growth")$: `growth` (scalar) +/ $upright("p")^(upright("max"))$: `p_max` over $cal(G)$ +/ $upright("p")^(upright("min"))$: `p_min` over $cal(G)$ +/ $upright("cost")$: `cost` over $cal(G)$ +/ $upright("load")$: `load` over $cal(T) times cal(B)$ +/ $upright("is_flexible")$: `is_flexible` over $cal(G)$ +/ $upright("zone_cap")$: `zone_cap` over $cal(Z)$ +/ $upright("tech_cap")$: `tech_cap` over $cal(B) times cal(E)$ +/ $upright("min_up")$: `min_up` over $cal(G)$ +/ $upright("eta")$: `eta` over $cal(G)$ +/ $upright("lead")$: `lead` over $cal(G)$ +/ $upright("budget")$: `budget` (scalar) +/ $upright("growth")$: `growth` (scalar) == Variables / $p$: `p` over $cal(T) times cal(G)$ / $italic("spill")$: `spill` over $cal(T)$ / $italic("slack")$: `slack` over $cal(T)$ -/ $italic("theta")$: `theta` over $cal(B)$ +/ $theta$: `theta` over $cal(B)$ / $italic("on")$: `on` over $cal(T) times cal(G)$ / $italic("units")$: `units` over $cal(G)$ / $italic("spare")$: `spare` over $cal(G)$ @@ -37,51 +38,57 @@ every character a notation escapes, set as text: link\_to, 100% & \#1 costs \$5 / $italic("void")$: `void` over $cal(B)$ / $italic("weight")$: `weight` over $cal(T) times cal(G)$ +Upright is what the model is given --- a parameter such as $upright("p")^(upright("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 minus.o 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 minus.square_(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 minus.o^(upright("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 minus.square_(v)^(upright("lookup")(t)) k$ is both at once. + == Objective #set math.equation(numbering: "(1)") -$ & max & sum_(t in cal(T), g in cal(G)) p_(t,g) dot italic("cost")_(g) + sum_(t in cal(T), g in cal(G)) p_(t,g) dot p_(t,g) dot italic("cost")_(g) + sum_(t in cal(T), g in cal(G)) p_(t,g) dot italic("cost")_(g) dot italic("growth")^(italic("lead")_(g)) + sum_(t in cal(T), g in cal(G)) p_(t,g) dot p^(upright("max"))_(g) - italic("reserve") + -italic("headroom") $ +$ & max & sum_(t in cal(T), g in cal(G)) p_(t,g) dot upright("cost")_(g) + sum_(t in cal(T), g in cal(G)) p_(t,g) dot p_(t,g) dot upright("cost")_(g) + sum_(t in cal(T), g in cal(G)) p_(t,g) dot upright("cost")_(g) dot upright("growth")^(upright("lead")_(g)) + sum_(t in cal(T), g in cal(G)) p_(t,g) dot upright("p")^(upright("max"))_(g) - italic("reserve") - italic("headroom") $ == Subject to #set math.equation(numbering: "(1)") -$ upright("balance") & sum_(g in cal(G) colon upright("gen_bus")(g) = b) p_(t,g) + italic("spill")_(t) - italic("slack")_(t) & = italic("load")_(t,b) & forall t in cal(T), b in cal(B) \ - upright("ramp") & p_(t,g) - p_(t minus.o 1,g) & <= p_(t - 1,g) + p^(upright("max"))_(g) & forall t in cal(T), g in cal(G) \ - upright("edges") & p_(t minus.square_(0) 1,g) & <= p_(t plus.square_(0) 1,g) + p^(upright("max"))_(g) & forall t in cal(T), g in cal(G) \ +$ upright("balance") & sum_(g in cal(G) colon upright("gen_bus")(g) = b) p_(t,g) + italic("spill")_(t) - italic("slack")_(t) & = upright("load")_(t,b) & forall t in cal(T), b in cal(B) \ + upright("ramp") & p_(t,g) - p_(t minus.o 1,g) & <= p_(t - 1,g) + upright("p")^(upright("max"))_(g) & forall t in cal(T), g in cal(G) \ + upright("edges") & p_(t minus.square_(0) 1,g) & <= p_(t plus.square_(0) 1,g) + upright("p")^(upright("max"))_(g) & forall t in cal(T), g in cal(G) \ upright("ahead") & p_(t,g) & <= p_(t plus.o 1,g) & forall t in cal(T), g in cal(G) \ - upright("composed") & p_(t - 2,g) & <= p^(upright("max"))_(g) & forall t in cal(T), g in cal(G) \ - upright("uncomposed") & p_((t - 1) - italic("lead"),g) & <= p^(upright("max"))_(g) & forall t in cal(T), g in cal(G) \ - upright("crossed") & p_(t minus.o 1,g + 1) & <= p^(upright("max"))_(g) & forall t in cal(T), g in cal(G) \ - upright("lead_time") & p_(t minus.square_(0) italic("lead"),g) & <= p^(upright("max"))_(g) & forall t in cal(T), g in cal(G) \ - upright("in_season") & p_(t,g) & <= p_(t minus.o_(upright("season_of")(t)) 1,g) & forall t in cal(T), g in cal(G) \ - upright("held_in_season") & p_(t,g) & <= p_(t minus.square_(0,upright("season_of")(t)) 1,g) & forall t in cal(T), g in cal(G) \ + upright("composed") & p_(t - 2,g) & <= upright("p")^(upright("max"))_(g) & forall t in cal(T), g in cal(G) \ + upright("uncomposed") & p_((t - 1) - upright("lead"),g) & <= upright("p")^(upright("max"))_(g) & forall t in cal(T), g in cal(G) \ + upright("crossed") & p_(t minus.o 1,g + 1) & <= upright("p")^(upright("max"))_(g) & forall t in cal(T), g in cal(G) \ + upright("lead_time") & p_(t minus.square_(0) upright("lead"),g) & <= upright("p")^(upright("max"))_(g) & forall t in cal(T), g in cal(G) \ + upright("in_season") & p_(t,g) & <= p_(t minus.o^(upright("season_of")(t)) 1,g) & forall t in cal(T), g in cal(G) \ + upright("held_in_season") & p_(t,g) & <= p_(t minus.square_(0)^(upright("season_of")(t)) 1,g) & forall t in cal(T), g in cal(G) \ upright("window") & sum_(t' in cal(T) colon 0 <= t - t' < 3) italic("on")_(t',g) & <= italic("units")_(g) & forall t in cal(T), g in cal(G) \ - upright("history") & sum_(t' in cal(T) colon 0 <= t minus.o t' < italic("min_up")) italic("on")_(t',g) & <= italic("units")_(g) & forall t in cal(T), g in cal(G) \ - upright("pullback") & italic("spill")_(t) & <= italic("zone")^(upright("cap"))_(upright("zone_of")(b)) & forall t in cal(T), b in cal(B) \ - upright("grouped_twice") & sum_(g in cal(G) colon upright("gen_bus")(g) = b and upright("gen_tech")(g) = e) p_(t,g) & <= italic("tech_cap")_(b,e) & forall t in cal(T), b in cal(B), e in cal(E) \ - upright("pulled_back_twice") & italic("units")_(g) & <= italic("tech_cap")_(upright("gen_bus")(g),upright("gen_tech")(g)) & forall g in cal(G) \ - upright("arithmetic") & sum_(g in cal(G)) (frac(p_(t,g), 2) + -italic("cost")_(g)) & >= (-(sum_(g in cal(G)) p_(t,g))) dot 3 & forall t in cal(T) \ - upright("total") & sum_(t in cal(T), g in cal(G)) p_(t,g) & <= italic("budget") \ - upright("scalar") & italic("units")_(g) & <= italic("budget") & forall g in cal(G) colon italic("cost")_(g) upright(" is defined") \ - upright("running") & italic("theta")_(b) & <= italic("load")_(t,b) & forall t in cal(T), b in cal(B) colon italic("theta")_(b) upright(" exists") and t >= 3 \ + upright("history") & sum_(t' in cal(T) colon 0 <= t minus.o t' < upright("min_up")) italic("on")_(t',g) & <= italic("units")_(g) & forall t in cal(T), g in cal(G) \ + upright("pullback") & italic("spill")_(t) & <= upright("zone_cap")_(upright("zone_of")(b)) & forall t in cal(T), b in cal(B) \ + upright("grouped_twice") & sum_(g in cal(G) colon upright("gen_bus")(g) = b and upright("gen_tech")(g) = e) p_(t,g) & <= upright("tech_cap")_(b,e) & forall t in cal(T), b in cal(B), e in cal(E) \ + upright("pulled_back_twice") & italic("units")_(g) & <= upright("tech_cap")_(upright("gen_bus")(g),upright("gen_tech")(g)) & forall g in cal(G) \ + upright("arithmetic") & sum_(g in cal(G)) (frac(p_(t,g), 2) - upright("cost")_(g)) & >= -(sum_(g in cal(G)) p_(t,g)) dot 3 & forall t in cal(T) \ + upright("total") & sum_(t in cal(T), g in cal(G)) p_(t,g) & <= upright("budget") \ + upright("scalar") & italic("units")_(g) & <= upright("budget") & forall g in cal(G) colon upright("cost")_(g) upright(" is defined") \ + upright("running") & theta_(b) & <= upright("load")_(t,b) & forall t in cal(T), b in cal(B) colon theta_(b) upright(" exists") and t >= 3 \ upright("first") & italic("on")_(t,g) & = 1 & forall t in cal(T), g in cal(G) colon (t = upright("index")(cal(T), 0) or t = upright("index")(cal(T), 0, upright("season_of")(t))) \ - upright("northern") & italic("slack")_(t) & <= italic("load")_(t,b) & forall t in cal(T), b in cal(B) colon upright("zone_of")(b) = upright("north") and upright("zone_of")(b) != upright("area_of")(b) and upright("zone_of")(b) upright(" is defined") \ - upright("always") & italic("spill")_(t) & >= 0 & forall t in cal(T) colon top \ + upright("northern") & italic("slack")_(t) & <= upright("load")_(t,b) & forall t in cal(T), b in cal(B) colon upright("zone_of")(b) = upright("north") and upright("zone_of")(b) != upright("area_of")(b) and upright("zone_of")(b) upright(" is defined") \ + upright("efficiency") & p_(t,g) & <= upright("eta")_(g) dot upright("p")^(upright("max"))_(g) & forall t in cal(T), g in cal(G) \ + upright("always") & italic("spill")_(t) & >= 0 & forall t in cal(T) \ + upright("redundant") & italic("spill")_(t) & >= 0 & forall t in cal(T) colon top and italic("spill")_(t) upright(" exists") \ upright("never") & italic("slack")_(t) & >= 0 & forall t in cal(T) colon bot $ == Variable domains #set math.equation(numbering: "(1)") -$ upright("p") & p^(upright("min"))_(g) <= p_(t,g) & <= p^(upright("max"))_(g) & forall t in cal(T), g in cal(G) colon (p^(upright("max"))_(g) > 0 and not italic("is_flexible")_(g) or p^(upright("min"))_(g) > 0) \ +$ upright("p") & upright("p")^(upright("min"))_(g) <= p_(t,g) & <= upright("p")^(upright("max"))_(g) & forall t in cal(T), g in cal(G) colon (upright("p")^(upright("max"))_(g) > 0 and not upright("is_flexible")_(g) or upright("p")^(upright("min"))_(g) > 0) \ upright("spill") & italic("spill")_(t) & >= 0 & forall t in cal(T) \ upright("slack") & italic("slack")_(t) & <= 100 & forall t in cal(T) \ - upright("theta") & italic("theta")_(b) & in RR & forall b in cal(B) \ + upright("theta") & theta_(b) & in RR & forall b in cal(B) \ upright("on") & italic("on")_(t,g) & in {0, 1} & forall t in cal(T), g in cal(G) \ upright("units") & 0 <= italic("units")_(g) & <= 10, italic("units")_(g) in ZZ & forall g in cal(G) \ upright("spare") & italic("spare")_(g) & in ZZ & forall g in cal(G) \ upright("reserve") & italic("reserve") & >= 0 \ - upright("headroom") & italic("headroom") & >= 0 & upright("where ") italic("budget") upright(" is defined") \ + upright("headroom") & italic("headroom") & >= 0 & upright("where ") upright("budget") upright(" is defined") \ upright("void") & infinity <= italic("void")_(b) & <= -infinity & forall b in cal(B) \ upright("weight") & 0 <= italic("weight")_(t,g) & <= 1 & forall t in cal(T), g in cal(G) \ upright("weight sos") & (italic("weight")_(t,g))_(g in cal(G)) & in upright("SOS")2 & forall t in cal(T) $ diff --git a/tests/typeset/test_typeset.py b/tests/typeset/test_typeset.py index 61fa467e..fc7b7b00 100644 --- a/tests/typeset/test_typeset.py +++ b/tests/typeset/test_typeset.py @@ -167,7 +167,7 @@ def test_a_negated_boolean_mask_negates_the_predicate_alone(fmt: Format): grouping to the one the model builds. """ text = typeset(_masked('bool'), fmt, legend=False) - negated = f'{fmt.operators["not"]} {fmt.subscript(fmt.italic("flag"), ["g"])}' + negated = f'{fmt.operators["not"]} {fmt.subscript(fmt.upright("flag"), ["g"])}' assert negated in text, 'the negation has to land on the predicate itself' assert f'{negated} {fmt.prose(" is defined")}' not in text, ( 'the negation must not scope over prose it cannot bracket' @@ -234,14 +234,15 @@ def storage(edge: str) -> dict[str, object]: @EVERY_FORMAT -def test_a_fill_and_a_group_share_the_operators_one_subscript(fmt: Format): - """Both policies subscript the operator, so a call carrying both writes one - subscript group rather than two. - - Not a matter of taste: `\\boxminus_{0}_{season_of(t)}` is a *Double - subscript* error, so the page stopped compiling at the equation — and the - two policies are independent, so nothing else in the model has to be - wrong for a model to reach it. +def test_a_fill_and_a_group_take_the_operators_two_slots(fmt: Format): + """The fill subscripts the operator; the group superscripts it. + + One slot each, so neither `\\boxminus_{0}_{season_of(t)}` — a *Double + subscript* error that stopped the page compiling — nor + `\\boxminus_{0,season_of(t)}`, which compiles and leaves a reader to guess + which of the two is the value standing at the boundary and which is the + group the translation stays inside. The two policies are independent, so + nothing else has to be wrong for a model to reach this. """ model = { 'dimensions': {'snapshot': {'dtype': 'int'}, 'season': {'dtype': 'str'}}, @@ -256,10 +257,11 @@ def test_a_fill_and_a_group_share_the_operators_one_subscript(fmt: Format): 'objective': {'sense': 'minimize', 'expression': 'sum(p)'}, } text = typeset(model, fmt, legend=False) - opened = fmt.subscript(fmt.operators['edge_minus'], ['0', 'GROUP']).split('GROUP')[0] - assert opened in text, 'the fill and the group do not share the one subscript the operator has' - assert fmt.subscript(fmt.operators['edge_minus'], ['0']) not in text, ( - 'the fill closed its own subscript and the group opened a second one' + group = fmt.apply(fmt.upright('season_of'), 't') + filled = fmt.subscript(fmt.operators['edge_minus'], ['0']) + assert fmt.superscript(filled, group) in text, 'the fill and the group are not in their own slots' + assert fmt.subscript(fmt.operators['edge_minus'], ['0', group]) not in text, ( + 'the fill and the group are sharing one subscript again' ) @@ -342,6 +344,29 @@ def test_translations_that_disagree_at_the_edge_do_not_merge(fmt: Format): ) +@EVERY_FORMAT +def test_a_negation_under_a_plus_is_the_subtraction_it_means(fmt: Format): + """`a + -b` is a spelling nobody uses, and the walk was printing it.""" + model = override(DISPATCH, **{'objective.expression': 'sum(p) + -sum(p)'}) + text = typeset(model, fmt) + assert f'{fmt.operators["plus"]} {fmt.operators["minus"]}' not in text + assert fmt.operators['minus'] in text, 'the subtraction it folded into should still print' + + +@EVERY_FORMAT +def test_a_mask_that_is_only_true_prints_no_condition(fmt: Format): + """The language says `True` is the same as no `where`, so a `\\top` on the + quantifier would put a condition on the page that reads as one and is not. + + Nested it still prints: `\\top \\wedge x` is what the file says, and simplifying a + mask belongs to resolution rather than to the typesetter. + """ + always = override(DISPATCH, **{'constraints.power_balance.where': 'True'}) + assert fmt.operators['true'] not in typeset(always, fmt) + nested = override(DISPATCH, **{'constraints.power_balance.where': 'True AND load > 0'}) + assert fmt.operators['true'] in typeset(nested, fmt) + + @EVERY_FORMAT def test_the_legend_explains_wraparound_only_when_it_is_used(fmt: Format): rolled = { @@ -401,6 +426,9 @@ def test_an_invalid_model_fails_the_same_way_check_does(fmt: Format): pytest.param('soc_max', r'\mathit{soc}^{\mathrm{max}}', id='declared-head-so-the-tail-is-a-qualifier'), pytest.param('marginal_cost', r'\mathit{marginal\_cost}', id='neither-so-it-stays-one-word'), pytest.param('shut_down', r'\mathit{shut\_down}', id='neither-even-when-the-tail-reads-like-a-qualifier'), + pytest.param('theta', r'\theta', id='a-name-that-is-a-greek-letter-is-the-letter'), + pytest.param('theta_max', r'\theta^{\mathrm{max}}', id='and-is-a-head-a-qualifier-may-hang-off'), + pytest.param('thetas', r'\mathit{thetas}', id='but-only-when-the-whole-name-is-the-letter'), ], ) def test_an_underscore_is_only_a_qualifier_when_its_head_is_a_symbol(name: str, expected: str): @@ -409,6 +437,96 @@ def test_an_underscore_is_only_a_qualifier_when_its_head_is_a_symbol(name: str, assert _derive_name_symbol(name, frozenset({'p', 'soc'}), LATEX) == expected +@pytest.mark.parametrize( + ('name', 'expected'), + [ + pytest.param('cost', r'\mathrm{cost}', id='a-word'), + pytest.param('p', r'\mathrm{p}', id='and-a-single-letter-too'), + pytest.param('p_max', r'\mathrm{p}^{\mathrm{max}}', id='the-head-of-a-qualifier-with-it'), + pytest.param('eta', r'\mathrm{eta}', id='and-a-greek-name-the-rule-beating-the-letter'), + ], +) +def test_a_given_quantity_is_upright(name: str, expected: str): + r"""Upright is what the data supplies, and it admits no exception. + + Not for single letters — `p^max` beside a variable `p` is exactly the pair + a reader has to be able to tell apart — and not for a Greek name, where + LaTeX has no upright lower-case form without `upgreek`, and taking that + dependency would cost both a two-package preamble and markdown that + renders the same on GitHub as on the docs site. An italic `\eta` that + might be either is worse than an upright `\mathrm{eta}` that is one. + """ + assert _derive_name_symbol(name, frozenset({'p', 'soc'}), LATEX, given=True) == expected + + +@EVERY_FORMAT +def test_a_name_that_is_a_greek_letter_prints_as_the_letter(fmt: Format): + """A variable called `theta` set as the italic word *theta* is the one + derived symbol no paper would accept.""" + model = override(DISPATCH, **{'variables.theta': {'foreach': ['snapshot']}}) + assert fmt.greek('theta') in typeset(model, fmt) + + +@EVERY_FORMAT +def test_a_parameter_is_upright_and_a_variable_is_italic(fmt: Format): + """The one distinction a reader of a linear model cannot afford to guess. + + A nomenclature table already splits the legend, and that is the convention + the field uses — but it is a lookup rather than a reading, and an equation + quoted on its own takes the legend with it. So the symbols carry it: + upright is what the model is given, italic is what the solver chooses. + """ + text = typeset(DISPATCH, fmt, legend=False) + assert fmt.subscript(fmt.upright('load'), ['t']) in text, 'a parameter is given, so it is upright' + assert fmt.subscript(fmt.italic('load'), ['t']) not in text + assert fmt.subscript('p', ['t', 'g']) in text, 'a variable is chosen, so it stays italic' + + +@EVERY_FORMAT +def test_the_legend_states_the_convention_only_where_it_draws_it(fmt: Format): + """A note explaining a contrast the page does not draw is a dead end, the + same reason the translation notes are gated on their symbol printing.""" + assert 'Upright is what the model is given' in typeset(DISPATCH, fmt) + parameterless = { + 'dimensions': {'snapshot': {'dtype': 'int'}}, + 'variables': {'p': {'foreach': ['snapshot'], 'bounds': {'lower': 0}}}, + 'objective': {'sense': 'minimize', 'expression': 'sum(p)'}, + } + assert 'Upright is what the model is given' not in typeset(parameterless, fmt) + + +@EVERY_FORMAT +def test_the_convention_note_quotes_only_what_the_derivation_chose(fmt: Format): + """A table is printed verbatim and is the author's to write, so a symbol it + supplies is not one the note governs. + + `examples/symbols/dispatch.yaml` maps three parameters to italic symbols, + and the homepage renders through it — so the note quoting one of those said + "a parameter such as $\\bar p$" under a sentence claiming a parameter is + upright, contradicting itself on the page a reader arrives at first. + """ + table = {'notation': fmt.notation, 'names': {'load': 'x', 'cost': 'c', 'p_max': 'm'}} + assert 'Upright is what the model is given' not in typeset(DISPATCH, fmt, symbols=table) + assert 'Upright is what the model is given' in typeset(DISPATCH, fmt), 'derived, so the note applies' + + +@EVERY_FORMAT +def test_a_dimension_is_not_a_head_a_qualifier_hangs_off(fmt: Format): + """`zone_cap` is a capacity *indexed by* zone, not a zone qualified by cap. + + Reading the axis as the head also made a parameter's symbol depend on + whether some unrelated dimension happened to share its prefix: declare a + dimension named `tech` and `tech_cap` silently re-rendered. + """ + model = override( + DISPATCH, + **{'dimensions.zone': {'dtype': 'str'}, 'parameters.zone_cap': {'dims': ['zone']}}, + ) + text = typeset(model, fmt) + assert fmt.upright('zone_cap') in text + assert fmt.superscript(fmt.upright('zone'), fmt.upright('cap')) not in text + + @EVERY_FORMAT def test_the_objective_shows_the_summations_the_file_wrote(fmt: Format): """One summation per ``sum`` in the expression, over the dims it took. @@ -451,17 +569,17 @@ def test_a_subtracted_summation_keeps_the_sign_outside_it(fmt: Format): 'fragment', [ pytest.param('p_{t,g}', id='symbols-follow-the-names-variable'), - pytest.param(r'\mathit{load}_{t}', id='symbols-follow-the-names-parameter'), - pytest.param(r'p^{\mathrm{max}}_{g}', id='symbols-follow-the-names-qualifier'), + pytest.param(r'\mathrm{load}_{t}', id='symbols-follow-the-names-parameter'), + pytest.param(r'\mathrm{p}^{\mathrm{max}}_{g}', id='symbols-follow-the-names-qualifier'), pytest.param( - r'\sum_{g \in \mathcal{G}} p_{t,g} & = \mathit{load}_{t}', + r'\sum_{g \in \mathcal{G}} p_{t,g} & = \mathrm{load}_{t}', id='sum-binds-the-dimension-it-reduces', ), pytest.param( - r'\sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} \cdot \mathit{cost}_{g}', + r'\sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} \cdot \mathrm{cost}_{g}', id='a-sum-naming-no-dim-puts-them-all-in-its-domain', ), - pytest.param(r'0 \le p_{t,g} & \le p^{\mathrm{max}}_{g}', id='bounds-become-a-domain-line'), + pytest.param(r'0 \le p_{t,g} & \le \mathrm{p}^{\mathrm{max}}_{g}', id='bounds-become-a-domain-line'), pytest.param(r'\text{power\_balance}', id='names-are-escaped-in-text-mode'), ], ) @@ -547,7 +665,7 @@ def test_typst_uses_its_own_grouping_and_set_notation(): typ = to_typst(DISPATCH, legend=False) assert 'p_(t,g)' in typ assert 'sum_(g in cal(G))' in typ - assert 'italic("load")_(t)' in typ + assert 'upright("load")_(t)' in typ def test_typst_sum_renders_the_coordinate_map(): @@ -905,7 +1023,7 @@ def test_the_table_overrides_and_the_rest_is_still_derived(): tex = to_latex(WITH_MARGINAL_COST, symbols=SYMBOLS, legend=False) assert r'\pi_{t,u}' in tex, 'both the symbol and its subscripts were overridden' assert r'c^{\mathrm{marg}}_{u}' in tex - assert r'\mathit{load}_{t}' in tex, 'untouched, so still derived' + assert r'\mathrm{load}_{t}' in tex, 'untouched, so still derived' assert r'u \in \mathcal{U}' in tex diff --git a/tools/notation.py b/tools/notation.py index 86c0cb9f..7f4b3892 100644 --- a/tools/notation.py +++ b/tools/notation.py @@ -233,7 +233,8 @@ def _curves() -> list[str]: """One row per ``method:``, each captioned with what that method restricts.""" rows = [] for method, source in PIECEWISE.items(): - printed = equations(to_markdown(source, numbered=False)) + table = _symbols(source) + printed = equations(to_markdown(source, symbols=table, numbered=False)) found = [block for block in declarations(source.read_text())['piecewise'] if _method(block) == method] assert found, f'{source.name} declares no piecewise block with method: {method}' for block in found: @@ -241,10 +242,35 @@ def _curves() -> list[str]: caption = ( f'**`method: {method}`** \N{EM DASH} {PIECEWISE_METHODS[method]}, in `{source.relative_to(ROOT)}`.' ) - rows.append(row.replace('\n\n', f'\n\n{caption}\n\n', 1)) + rows.append(row.replace('\n\n', f'\n\n{caption}\n\n{_table_shown(table)}', 1)) return rows +def _symbols(source: Path) -> Path | None: + """The sidecar symbol table for *source*, by the filename convention + ``tools/render_tex.py`` already uses.""" + table = ROOT / 'examples' / 'symbols' / f'{source.stem}.yaml' + return table if table.exists() else None + + +def _table_shown(table: Path | None) -> str: + """The symbol table, printed beside the math it renamed. + + A curve expands to weights named after the block that declared them, which + an equation naming one six times cannot carry. Renaming them in the + typesetter would be a symbol a reader could not trace back to the file, so + the rename is a **declaration** — the same ``--symbols`` sidecar any reader + may write — and the page shows it rather than performing it. + """ + if table is None: + return '' + body = re.sub(r'\A(?:#[^\n]*\n|\n)+', '', table.read_text()).strip() + return ( + f'Rendered with the sidecar symbol table `{table.relative_to(ROOT)}`, ' + f'which is what the weights print as:\n\n```yaml\n{body}\n```\n\n' + ) + + def _method(block: Declaration) -> str: """The block's ``method:``, or the default the language gives it.""" return block.field('method') or 'adjacency'