diff --git a/.prettierignore b/.prettierignore
index 6618d943..66add10f 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -47,9 +47,10 @@ docs/reference/notation.md
# 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.
+# `tools/home_math.py` needs no entry, and takes both of the other ways out:
+# it emits the blank lines around its markers that prettier wants, and the
+# block of tables it writes into `README.md` sits inside a
+# `` range there. A range is what a file whose
+# generated part is a block among hand-written prose wants — the pages above
+# are generated nearly end to end, so listing them is the shorter answer.
docs/reference/language/operators.md
diff --git a/README.md b/README.md
index 4aea960d..953aa399 100644
--- a/README.md
+++ b/README.md
@@ -20,9 +20,9 @@ with no data and no solver.**
A math-spec file declares four things: the axes the model runs over, such as
`snapshot` and `generator`; the data it expects, such as `load` and `cost`; the
-decisions the solver makes, such as `dispatch`; and the rules those decisions obey, such
-as `sum(dispatch, consume=generator) == load`. The file [below](#example) is a complete
-model.
+decisions the solver makes, such as `dispatch`; and the rules those decisions
+obey, such as `sum(dispatch, over=generator) == load`. The file
+[below](#example) is a complete model.
math-spec reads that file, checks everything that can be checked without data,
and hands the result on: to an engine that builds and solves the model, or to the
@@ -79,69 +79,213 @@ dimensions:
generator: { description: generating units }
parameters:
- p_max: { dims: [generator], description: installed capacity }
+ capacity: { dims: [generator], description: installed capacity }
load: { dims: [snapshot], description: demand to be met }
cost: { dims: [generator], description: marginal cost }
variables:
- p:
+ dispatch:
description: output of a generator in a snapshot
dims: [snapshot, generator]
- where: "p_max > 0"
- bounds: { lower: 0, upper: p_max }
+ where: "capacity > 0"
+ bounds: { lower: 0, upper: capacity }
constraints:
power_balance:
dims: [snapshot]
- expression: sum(p, over=generator) == load
+ expression: sum(dispatch, over=generator) == load
objective:
sense: minimize
- expression: sum(p * cost)
+ expression: sum(dispatch * cost)
```
-That file is a complete model. Nothing outside it changes what it means, and
-everything about it that can be wrong is wrong at load:
+That file is a complete model. Nothing outside it changes what it means.
-
+### The math it prints
-```python
-import math_spec as ms
+Here is that model as math, printed from the file above and nothing else. No
+data, no solver, and no second copy of the equations to keep in step. Markdown
+is one of three formats, so GitHub renders it here.
-spec = ms.to_spec('dispatch.yaml') # schema, names, dims, degree — all checked here
-sorted(spec.variables) # ['dispatch']
+
+
+
-program = ms.to_program(spec) # curves expanded, names typed, operators resolved to nodes
-sorted(program.constraints) # ['power_balance']
+Least-cost dispatch of a generator fleet against an hourly load.
+
+#### Objective
+
+```math
+\min \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} \mathit{dispatch}_{t,g} \cdot \mathrm{cost}_{g}
```
-Neither needs data or a solver, so a repository of models compiles in CI with
-nothing bound to any of them. **A `Spec` holds the file as written, and a
-`Program` holds the model it builds**, with every macro expanded and every curve
-turned into its variables and constraints. An engine reads the second.
+#### Subject to
-
+**`power_balance`**
-[Reading a loaded model](docs/reference/language/reading.md) says what a tool
-gets from each.
+```math
+\sum_{g \in \mathcal{G}} \mathit{dispatch}_{t,g} = \mathrm{load}_{t} \qquad \forall\, t \in \mathcal{T}
+```
+
+#### Variable domains
+
+**`dispatch`**
+
+```math
+0 \le \mathit{dispatch}_{t,g} \le \mathrm{capacity}_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{capacity}_{g} > 0
+```
+
+
+The whole document: a symbol table, and the legend it prints
-The same `spec` prints as math. It is read and checked once, then printed three
-ways:
+Least-cost dispatch of a generator fleet against an hourly load.
+
+#### Sets
+
+| Symbol | Meaning |
+|---|---|
+| $`\mathcal{S}`$ | index $`s`$ — `snapshot` — dispatch periods |
+| $`\mathcal{G}`$ | index $`g`$ — `generator` — generating units |
+
+#### Parameters
+
+| Symbol | Meaning |
+|---|---|
+| $`\bar p`$ | `capacity` over $`\mathcal{G}`$ — installed capacity |
+| $`\ell`$ | `load` over $`\mathcal{S}`$ — demand to be met |
+| $`c`$ | `cost` over $`\mathcal{G}`$ — marginal cost |
+
+#### Variables
+
+| Symbol | Meaning |
+|---|---|
+| $`\mathit{dispatch}`$ | `dispatch` over $`\mathcal{S} \times \mathcal{G}`$ — output of a generator in a snapshot |
+
+#### Objective
+
+```math
+\min \sum_{s \in \mathcal{S},\ g \in \mathcal{G}} \mathit{dispatch}_{s,g} \cdot c_{g}
+```
+
+#### Subject to
+
+**`power_balance`**
+
+```math
+\sum_{g \in \mathcal{G}} \mathit{dispatch}_{s,g} = \ell_{s} \qquad \forall\, s \in \mathcal{S}
+```
+
+#### Variable domains
+
+**`dispatch`**
+
+```math
+0 \le \mathit{dispatch}_{s,g} \le \bar p_{g} \qquad \forall\, s \in \mathcal{S},\ g \in \mathcal{G} \,:\, \bar p_{g} > 0
+```
+
+
+
+
+The same document as LaTeX
+
+```latex
+\noindent Least-cost dispatch of a generator fleet against an hourly load.
+
+\paragraph{Sets}
+\begin{description}
+\item[{$\mathcal{S}$}] index $s$ --- \texttt{snapshot} --- dispatch periods
+\item[{$\mathcal{G}$}] index $g$ --- \texttt{generator} --- generating units
+\end{description}
+
+\paragraph{Parameters}
+\begin{description}
+\item[{$\bar p$}] \texttt{capacity} over $\mathcal{G}$ --- installed capacity
+\item[{$\ell$}] \texttt{load} over $\mathcal{S}$ --- demand to be met
+\item[{$c$}] \texttt{cost} over $\mathcal{G}$ --- marginal cost
+\end{description}
+
+\paragraph{Variables}
+\begin{description}
+\item[{$\mathit{dispatch}$}] \texttt{dispatch} over $\mathcal{S} \times \mathcal{G}$ --- output of a generator in a snapshot
+\end{description}
+
+\paragraph{Objective}
+\begin{align*}
+ && \min & \sum_{s \in \mathcal{S},\ g \in \mathcal{G}} \mathit{dispatch}_{s,g} \cdot c_{g}
+\end{align*}
+
+\paragraph{Subject to}
+\begin{align*}
+\text{power\_balance} && \sum_{g \in \mathcal{G}} \mathit{dispatch}_{s,g} & = \ell_{s} && \forall\, s \in \mathcal{S}
+\end{align*}
+
+\paragraph{Variable domains}
+\begin{align*}
+\text{dispatch} && 0 \le \mathit{dispatch}_{s,g} & \le \bar p_{g} && \forall\, s \in \mathcal{S},\ g \in \mathcal{G} \,:\, \bar p_{g} > 0
+\end{align*}
+```
+
+
+
+
+The same document as Typst, printed with no symbol table
+
+```typst
+Least-cost dispatch of a generator fleet against an hourly load.
+
+== Sets
+/ $cal(T)$: index $t$ --- `snapshot` --- dispatch periods
+/ $cal(G)$: index $g$ --- `generator` --- generating units
+
+== Parameters
+/ $upright("capacity")$: `capacity` over $cal(G)$ --- installed capacity
+/ $upright("load")$: `load` over $cal(T)$ --- demand to be met
+/ $upright("cost")$: `cost` over $cal(G)$ --- marginal cost
+
+== Variables
+/ $italic("dispatch")$: `dispatch` over $cal(T) times cal(G)$ --- output of a generator in a snapshot
+
+Upright is what the model is given --- a parameter such as $upright("capacity")$, a coordinate map, a label --- and italic is what the solver chooses, such as $italic("dispatch")$. An index is italic too, being what a quantifier chooses, and a set is script.
+
+== Objective
+$ & min & sum_(t in cal(T), g in cal(G)) italic("dispatch")_(t,g) dot upright("cost")_(g) $
+
+== Subject to
+$ upright("power_balance") & sum_(g in cal(G)) italic("dispatch")_(t,g) & = upright("load")_(t) & forall t in cal(T) $
+
+== Variable domains
+$ upright("dispatch") & 0 <= italic("dispatch")_(t,g) & <= upright("capacity")_(g) & forall t in cal(T), g in cal(G) colon upright("capacity")_(g) > 0 $
+```
+
+
+
+
+
+
+Each format is one call, and the file is read and checked once:
```python
-symbols = 'dispatch.symbols.yaml' # optional: a dict, a path, or a SymbolTable
+import math_spec as ms
-ms.to_latex(spec, symbols=symbols) # amsmath align
+spec = ms.to_spec('dispatch.yaml')
+
+ms.to_markdown(spec) # renders as-is on GitHub, as above
+ms.to_latex(spec) # amsmath align
ms.to_typst(spec) # compiles without a TeX toolchain
-ms.to_markdown(spec) # renders as-is on GitHub
```
-Drop the symbol table, and the same model prints as $\mathit{load}_t$ and
-$dispatch^{\mathrm{max}}_g$, with no setup. Every spelling in a table is printed as
-written, a key naming nothing in the model is an error, and nothing in a table
-changes what the file means.
+Those symbols are the file's own names: `load` prints as $`\mathrm{load}_t`$,
+and `capacity` as $`\mathrm{capacity}_g`$. Nothing had to be set up for
+that. Pass `symbols='dispatch.symbols.yaml'` and the typesetter prints
+$`\ell_t`$ and $`\bar p_g`$ instead, above a legend that defines them. The
+first folded block shows it. The table can be a dict, a `SymbolTable`, or a
+path to YAML. A key that names nothing in the model is an error, and nothing
+in a table changes what the file means.
Or from a shell, beside `pdflatex` in a Makefile:
@@ -151,6 +295,32 @@ python -m math_spec typst dispatch.yaml --standalone -o dispatch.typ
python -m math_spec markdown dispatch.yaml
```
+### `Spec` and `Program`
+
+
+
+Whatever is wrong with a model is wrong when it loads, not when it solves:
+
+```python
+import math_spec as ms
+
+spec = ms.to_spec('dispatch.yaml') # schema, names, dimensions, degree: all checked here
+sorted(spec.variables) # ['dispatch']
+
+program = ms.to_program(spec) # curves expanded, names typed, operators resolved to nodes
+sorted(program.constraints) # ['power_balance']
+```
+
+Neither needs data or a solver, so a repository of models compiles in CI with
+nothing bound to any of them. **A `Spec` holds the file as written, and a
+`Program` holds the model it builds**, with every macro expanded and every curve
+turned into its variables and constraints. An engine reads the `Program`.
+
+
+
+[Reading a loaded model](docs/reference/language/reading.md) says what a tool
+gets from each.
+
## Why
- **Declarative math.** A file is readable without knowing any implementation,
@@ -211,7 +381,7 @@ through are a dependency rather than one engine's internals. The keys themselves
which are YAML math, a block per component, `dims:` and a `where:` string,
come from [Calliope](https://github.com/calliope-project/calliope).
[linopy](https://github.com/PyPSA/linopy) supplies the vocabulary that
-`sum(consume=)` and the dimension rules are named against. Issue numbers in these
+`sum(over=)` and the dimension rules are named against. Issue numbers in these
pages point at lpspec, where the arguments happened.
## Status
diff --git a/docs/examples/commitment.md b/docs/examples/commitment.md
index 23dc4d08..915ad9c2 100644
--- a/docs/examples/commitment.md
+++ b/docs/examples/commitment.md
@@ -32,18 +32,18 @@ dimensions:
parameters:
committable: { dims: [generator], dtype: bool, description: whether the unit may be switched off }
status_initial: { dims: [generator], description: whether the unit was running before the horizon }
- p_max: { dims: [generator], description: installed capacity }
- p_min: { dims: [generator], description: output floor while running }
+ capacity: { dims: [generator], description: installed capacity }
+ min_output: { dims: [generator], description: output floor while running }
ramp_limit: { dims: [generator], description: how far output may move between snapshots while running }
start_up_limit: { dims: [generator], description: how far it may move in the snapshot it starts in }
load: { dims: [snapshot], description: demand to be met }
cost: { dims: [generator], description: marginal cost }
variables:
- p:
+ dispatch:
description: output of a generator in a snapshot
dims: [snapshot, generator]
- bounds: { lower: 0, upper: p_max }
+ bounds: { lower: 0, upper: capacity }
status:
description: whether the unit is running in a snapshot
dims: [snapshot, generator]
@@ -65,27 +65,27 @@ expressions:
constraints:
power_balance:
dims: [snapshot]
- expression: sum(p, over=generator) == load
+ expression: sum(dispatch, over=generator) == load
upper:
description: a unit that is not running produces nothing
dims: [snapshot, generator]
- expression: p <= status * p_max
+ expression: dispatch <= status * capacity
lower:
description: and one that is running produces at least its floor
dims: [snapshot, generator]
- expression: p >= status * p_min
+ expression: dispatch >= status * min_output
ramp_up:
description: >-
one inequality for both regimes — a unit already running is held to
`ramp_limit`, a unit starting up to `start_up_limit`.
dims: [snapshot, generator]
expression: >-
- p - shift(p, along=snapshot, offset=1, edge=0)
+ dispatch - shift(dispatch, along=snapshot, offset=1, edge=0)
<= ramp_limit * previous_status + start_up_limit * (1 - previous_status)
objective:
sense: minimize
- expression: sum(p * cost)
+ expression: sum(dispatch * cost)
```
Unit commitment with a start-up ramp, the formulation `cases:` exists for. The state a unit carries into a snapshot has three regimes — a unit that is never off, the first snapshot, and every later one — and writing them at the constraint would fork `ramp_up` three ways. With the regimes named once, the inequality is written once.
@@ -103,8 +103,8 @@ Unit commitment with a start-up ramp, the formulation `cases:` exists for. The s
|---|---|
| $`\mathrm{committable}`$ | `committable` over $`\mathcal{G}`$ — whether the unit may be switched off |
| $`\mathrm{status}^{\mathrm{initial}}`$ | `status_initial` over $`\mathcal{G}`$ — whether the unit was running before the horizon |
-| $`\mathrm{p}^{\mathrm{max}}`$ | `p_max` over $`\mathcal{G}`$ — installed capacity |
-| $`\mathrm{p}^{\mathrm{min}}`$ | `p_min` over $`\mathcal{G}`$ — output floor while running |
+| $`\mathrm{capacity}`$ | `capacity` over $`\mathcal{G}`$ — installed capacity |
+| $`\mathrm{min\_output}`$ | `min_output` over $`\mathcal{G}`$ — output floor while running |
| $`\mathrm{ramp\_limit}`$ | `ramp_limit` over $`\mathcal{G}`$ — how far output may move between snapshots while running |
| $`\mathrm{start\_up\_limit}`$ | `start_up_limit` over $`\mathcal{G}`$ — how far it may move in the snapshot it starts in |
| $`\mathrm{load}`$ | `load` over $`\mathcal{T}`$ — demand to be met |
@@ -114,7 +114,7 @@ Unit commitment with a start-up ramp, the formulation `cases:` exists for. The s
| Symbol | Meaning |
|---|---|
-| $`p`$ | `p` over $`\mathcal{T} \times \mathcal{G}`$ — output of a generator in a snapshot |
+| $`\mathit{dispatch}`$ | `dispatch` over $`\mathcal{T} \times \mathcal{G}`$ — output of a generator in a snapshot |
| $`\mathit{status}`$ | `status` over $`\mathcal{T} \times \mathcal{G}`$ — whether the unit is running in a snapshot |
#### Definitions
@@ -123,7 +123,7 @@ Unit commitment with a start-up ramp, the formulation `cases:` exists for. The s
|---|---|
| $`\mathit{previous\_status}`$ | `previous_status` over $`\mathcal{T} \times \mathcal{G}`$ — the commitment state a unit carries into a snapshot |
-Upright is what the model is given — a parameter such as $`\mathrm{committable}`$, 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.
+Upright is what the model is given — a parameter such as $`\mathrm{committable}`$, a coordinate map, a label — and italic is what the solver chooses, such as $`\mathit{dispatch}`$. An index is italic too, being what a quantifier chooses, and a set is script.
$`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.
@@ -132,7 +132,7 @@ $`\mathrm{pos}(t)`$ denotes where index $`t`$ sits along its dimension's own ord
#### Objective
```math
-\min \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} \cdot \mathrm{cost}_{g}
+\min \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} \mathit{dispatch}_{t,g} \cdot \mathrm{cost}_{g}
```
#### Subject to
@@ -140,25 +140,25 @@ $`\mathrm{pos}(t)`$ denotes where index $`t`$ sits along its dimension's own ord
**`power_balance`**
```math
-\sum_{g \in \mathcal{G}} p_{t,g} = \mathrm{load}_{t} \qquad \forall\, t \in \mathcal{T}
+\sum_{g \in \mathcal{G}} \mathit{dispatch}_{t,g} = \mathrm{load}_{t} \qquad \forall\, t \in \mathcal{T}
```
**`upper`**
```math
-p_{t,g} \le \mathit{status}_{t,g} \cdot \mathrm{p}^{\mathrm{max}}_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G}
+\mathit{dispatch}_{t,g} \le \mathit{status}_{t,g} \cdot \mathrm{capacity}_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G}
```
**`lower`**
```math
-p_{t,g} \ge \mathit{status}_{t,g} \cdot \mathrm{p}^{\mathrm{min}}_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G}
+\mathit{dispatch}_{t,g} \ge \mathit{status}_{t,g} \cdot \mathrm{min\_output}_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G}
```
**`ramp_up`**
```math
-p_{t,g} - p_{t \boxminus_{0} 1,g} \le \mathrm{ramp\_limit}_{g} \cdot \mathit{previous\_status}_{t,g} + \mathrm{start\_up\_limit}_{g} \cdot \left( 1 - \mathit{previous\_status}_{t,g} \right) \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G}
+\mathit{dispatch}_{t,g} - \mathit{dispatch}_{t \boxminus_{0} 1,g} \le \mathrm{ramp\_limit}_{g} \cdot \mathit{previous\_status}_{t,g} + \mathrm{start\_up\_limit}_{g} \cdot \left( 1 - \mathit{previous\_status}_{t,g} \right) \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G}
```
#### Definitions
@@ -171,10 +171,10 @@ p_{t,g} - p_{t \boxminus_{0} 1,g} \le \mathrm{ramp\_limit}_{g} \cdot \mathit{pre
#### Variable domains
-**`p`**
+**`dispatch`**
```math
-0 \le p_{t,g} \le \mathrm{p}^{\mathrm{max}}_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G}
+0 \le \mathit{dispatch}_{t,g} \le \mathrm{capacity}_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G}
```
**`status`**
diff --git a/docs/examples/dispatch.md b/docs/examples/dispatch.md
index 0ba0f360..72a08743 100644
--- a/docs/examples/dispatch.md
+++ b/docs/examples/dispatch.md
@@ -10,9 +10,9 @@ load to meet, and a cost to minimise. It is the model on the
[home page](../index.md) and in the README, and the one the language reference
varies when it needs a base to change one thing in.
-The `where:` on `p` deletes the rows where a generator has no capacity, so
+The `where:` on `dispatch` deletes the rows where a generator has no capacity, so
[absence](../reference/language/absence.md) is declared in the file rather than
-checked at run time. `sum(p, over=generator)` names the dimension it reduces, so
+checked at run time. `sum(dispatch, over=generator)` names the dimension it reduces, so
the constraint's `dims` is what remains.
@@ -24,25 +24,25 @@ dimensions:
generator: { description: generating units }
parameters:
- p_max: { dims: [generator], description: installed capacity }
+ capacity: { dims: [generator], description: installed capacity }
load: { dims: [snapshot], description: demand to be met }
cost: { dims: [generator], description: marginal cost }
variables:
- p:
+ dispatch:
description: output of a generator in a snapshot
dims: [snapshot, generator]
- where: "p_max > 0"
- bounds: { lower: 0, upper: p_max }
+ where: "capacity > 0"
+ bounds: { lower: 0, upper: capacity }
constraints:
power_balance:
dims: [snapshot]
- expression: sum(p, over=generator) == load
+ expression: sum(dispatch, over=generator) == load
objective:
sense: minimize
- expression: sum(p * cost)
+ expression: sum(dispatch * cost)
```
Least-cost dispatch of a generator fleet against an hourly load.
@@ -58,7 +58,7 @@ Least-cost dispatch of a generator fleet against an hourly load.
| Symbol | Meaning |
|---|---|
-| $`\mathrm{p}^{\mathrm{max}}`$ | `p_max` over $`\mathcal{G}`$ — installed capacity |
+| $`\mathrm{capacity}`$ | `capacity` over $`\mathcal{G}`$ — installed capacity |
| $`\mathrm{load}`$ | `load` over $`\mathcal{T}`$ — demand to be met |
| $`\mathrm{cost}`$ | `cost` over $`\mathcal{G}`$ — marginal cost |
@@ -66,14 +66,14 @@ Least-cost dispatch of a generator fleet against an hourly load.
| Symbol | Meaning |
|---|---|
-| $`p`$ | `p` over $`\mathcal{T} \times \mathcal{G}`$ — output of a generator in a snapshot |
+| $`\mathit{dispatch}`$ | `dispatch` 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.
+Upright is what the model is given — a parameter such as $`\mathrm{capacity}`$, a coordinate map, a label — and italic is what the solver chooses, such as $`\mathit{dispatch}`$. An index is italic too, being what a quantifier chooses, and a set is script.
#### Objective
```math
-\min \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} \cdot \mathrm{cost}_{g}
+\min \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} \mathit{dispatch}_{t,g} \cdot \mathrm{cost}_{g}
```
#### Subject to
@@ -81,15 +81,15 @@ Upright is what the model is given — a parameter such as $`\mathrm{p}^{\mathrm
**`power_balance`**
```math
-\sum_{g \in \mathcal{G}} p_{t,g} = \mathrm{load}_{t} \qquad \forall\, t \in \mathcal{T}
+\sum_{g \in \mathcal{G}} \mathit{dispatch}_{t,g} = \mathrm{load}_{t} \qquad \forall\, t \in \mathcal{T}
```
#### Variable domains
-**`p`**
+**`dispatch`**
```math
-0 \le p_{t,g} \le \mathrm{p}^{\mathrm{max}}_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{p}^{\mathrm{max}}_{g} > 0
+0 \le \mathit{dispatch}_{t,g} \le \mathrm{capacity}_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{capacity}_{g} > 0
```
diff --git a/docs/howto/print.md b/docs/howto/print.md
index b3b4e59c..f898a47b 100644
--- a/docs/howto/print.md
+++ b/docs/howto/print.md
@@ -17,7 +17,7 @@ keep the document current as the file changes.
2. **Give the symbols their conventional spelling** with a symbol table beside
the model, `model.symbols.yaml`. Without one, `load` prints as
- $\mathit{load}_t$; with one it prints as whatever you write:
+ $\mathrm{load}_t$; with one it prints as whatever you write:
@@ -31,7 +31,7 @@ keep the document current as the file changes.
names:
cost: c
load: "\\ell"
- p_max: "\\bar p"
+ capacity: "\\bar p"
```
A key naming nothing in the model is an error, so a table cannot drift
diff --git a/docs/howto/regimes.md b/docs/howto/regimes.md
index ffeebbd2..eb6c4e1a 100644
--- a/docs/howto/regimes.md
+++ b/docs/howto/regimes.md
@@ -27,26 +27,26 @@ the recipe needs no second model file.
generator: { dtype: str }
parameters:
- p_max: { dims: [generator] }
- p_min: { dims: [generator] }
+ capacity: { dims: [generator] }
+ min_output: { dims: [generator] }
committable: { dims: [generator], dtype: bool }
variables:
- p: { dims: [snapshot, generator], bounds: { lower: 0, upper: p_max } }
+ dispatch: { dims: [snapshot, generator], bounds: { lower: 0, upper: capacity } }
on: { dims: [snapshot, generator], where: committable, domain: binary }
constraints:
floor_committed:
dims: [snapshot, generator]
where: committable
- expression: p >= p_min * on
+ expression: dispatch >= min_output * on
ceiling_committed:
dims: [snapshot, generator]
where: committable
- expression: p <= p_max * on
+ expression: dispatch <= capacity * on
```
- Here a non-committable generator is bounded by `p_max` alone, through the
+ Here a non-committable generator is bounded by `capacity` alone, through the
variable's `bounds:`. Where the other regime has a rule of its own, write
it as a third block under `where: "NOT committable"`.
@@ -59,11 +59,11 @@ the recipe needs no second model file.
generator: { dtype: str }
parameters:
- p_max: { dims: [generator] }
+ capacity: { dims: [generator] }
committable: { dims: [generator], dtype: bool }
variables:
- p: { dims: [snapshot, generator], bounds: { lower: 0 } }
+ dispatch: { dims: [snapshot, generator], bounds: { lower: 0 } }
on: { dims: [snapshot, generator], where: committable, domain: binary }
expressions:
@@ -72,13 +72,13 @@ the recipe needs no second model file.
cases:
committed:
when: committable
- expression: p_max * on
- otherwise: p_max
+ expression: capacity * on
+ otherwise: capacity
constraints:
ceiling:
dims: [snapshot, generator]
- expression: p <= available
+ expression: dispatch <= available
```
The loader proves at load that no two cases can hold at one coordinate,
diff --git a/docs/index.md b/docs/index.md
index a5c97d51..7e9e2a11 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -89,7 +89,7 @@ and the file prints as the math it stands for.
--8<-- "README.md:model"
-### What that file says
+### The math it prints
Generated from the YAML above, with no data and no solver. Only the notation is a
choice, and **How** shows the one made here.
@@ -111,7 +111,7 @@ choice, and **How** shows the one made here.
| Symbol | Meaning |
|---|---|
- | $`\bar p`$ | `p_max` over $`\mathcal{G}`$ — installed capacity |
+ | $`\bar p`$ | `capacity` over $`\mathcal{G}`$ — installed capacity |
| $`\ell`$ | `load` over $`\mathcal{S}`$ — demand to be met |
| $`c`$ | `cost` over $`\mathcal{G}`$ — marginal cost |
@@ -119,12 +119,12 @@ choice, and **How** shows the one made here.
| Symbol | Meaning |
|---|---|
- | $`p`$ | `p` over $`\mathcal{S} \times \mathcal{G}`$ — output of a generator in a snapshot |
+ | $`\mathit{dispatch}`$ | `dispatch` over $`\mathcal{S} \times \mathcal{G}`$ — output of a generator in a snapshot |
#### Objective
```math
- \min \sum_{s \in \mathcal{S},\ g \in \mathcal{G}} p_{s,g} \cdot c_{g}
+ \min \sum_{s \in \mathcal{S},\ g \in \mathcal{G}} \mathit{dispatch}_{s,g} \cdot c_{g}
```
#### Subject to
@@ -132,15 +132,15 @@ choice, and **How** shows the one made here.
**`power_balance`**
```math
- \sum_{g \in \mathcal{G}} p_{s,g} = \ell_{s} \qquad \forall\, s \in \mathcal{S}
+ \sum_{g \in \mathcal{G}} \mathit{dispatch}_{s,g} = \ell_{s} \qquad \forall\, s \in \mathcal{S}
```
#### Variable domains
- **`p`**
+ **`dispatch`**
```math
- 0 \le p_{s,g} \le \bar p_{g} \qquad \forall\, s \in \mathcal{S},\ g \in \mathcal{G} \,:\, \bar p_{g} > 0
+ 0 \le \mathit{dispatch}_{s,g} \le \bar p_{g} \qquad \forall\, s \in \mathcal{S},\ g \in \mathcal{G} \,:\, \bar p_{g} > 0
```
=== "LaTeX"
@@ -156,29 +156,29 @@ choice, and **How** shows the one made here.
\paragraph{Parameters}
\begin{description}
- \item[{$\bar p$}] \texttt{p\_max} over $\mathcal{G}$ --- installed capacity
+ \item[{$\bar p$}] \texttt{capacity} over $\mathcal{G}$ --- installed capacity
\item[{$\ell$}] \texttt{load} over $\mathcal{S}$ --- demand to be met
\item[{$c$}] \texttt{cost} over $\mathcal{G}$ --- marginal cost
\end{description}
\paragraph{Variables}
\begin{description}
- \item[{$p$}] \texttt{p} over $\mathcal{S} \times \mathcal{G}$ --- output of a generator in a snapshot
+ \item[{$\mathit{dispatch}$}] \texttt{dispatch} over $\mathcal{S} \times \mathcal{G}$ --- output of a generator in a snapshot
\end{description}
\paragraph{Objective}
\begin{align*}
- && \min & \sum_{s \in \mathcal{S},\ g \in \mathcal{G}} p_{s,g} \cdot c_{g}
+ && \min & \sum_{s \in \mathcal{S},\ g \in \mathcal{G}} \mathit{dispatch}_{s,g} \cdot c_{g}
\end{align*}
\paragraph{Subject to}
\begin{align*}
- \text{power\_balance} && \sum_{g \in \mathcal{G}} p_{s,g} & = \ell_{s} && \forall\, s \in \mathcal{S}
+ \text{power\_balance} && \sum_{g \in \mathcal{G}} \mathit{dispatch}_{s,g} & = \ell_{s} && \forall\, s \in \mathcal{S}
\end{align*}
\paragraph{Variable domains}
\begin{align*}
- \text{p} && 0 \le p_{s,g} & \le \bar p_{g} && \forall\, s \in \mathcal{S},\ g \in \mathcal{G} \,:\, \bar p_{g} > 0
+ \text{dispatch} && 0 \le \mathit{dispatch}_{s,g} & \le \bar p_{g} && \forall\, s \in \mathcal{S},\ g \in \mathcal{G} \,:\, \bar p_{g} > 0
\end{align*}
```
@@ -196,7 +196,7 @@ choice, and **How** shows the one made here.
'names': {
'cost': 'c',
'load': '\\ell',
- 'p_max': '\\bar p',
+ 'capacity': '\\bar p',
},
}
@@ -207,26 +207,25 @@ choice, and **How** shows the one made here.
ms.to_markdown(spec) # renders as-is on GitHub
```
- `symbols` is optional — drop it and the same model prints as
- $\mathit{load}_t$, $p^{\mathrm{max}}_g$. A dict, a YAML path or a
- `SymbolTable`; a key naming nothing in the model is an error, not a symbol that
- silently never applies. Every spelling is printed verbatim — `notation` says
- which language they are, and a render in the other one refuses.
+ `symbols` gives every name its conventional spelling. Pass a dict, a YAML path
+ or a `SymbolTable`. It is optional: drop it and the same model prints from the
+ names in the file, as $\mathrm{load}_t$ and $\mathrm{capacity}_g$.
- Or from a shell, where the table is that same YAML on disk and `--standalone`
- emits a document that compiles rather than a fragment to `\input`:
+ Or from a shell, where the table is that same YAML on disk. `--standalone` emits
+ a document that compiles, rather than a fragment to `\input`:
```bash
python -m math_spec latex dispatch.yaml --symbols dispatch.symbols.yaml
python -m math_spec typst dispatch.yaml --standalone -o dispatch.typ
```
- The renderer is [the typesetter](reference/typeset.md), and it reads the same
- file every other page here loads.
+ [Typeset the math](reference/typeset.md) documents the three functions, their
+ options and symbol tables. Each reads the same file every other page here
+ loads.
-### How a tool reads it
+### `Spec` and `Program`
--8<-- "README.md:load"
diff --git a/docs/reference/language/absence.md b/docs/reference/language/absence.md
index 478fc0db..9310e453 100644
--- a/docs/reference/language/absence.md
+++ b/docs/reference/language/absence.md
@@ -13,15 +13,15 @@ follows from that.
dimensions:
g: { dtype: str }
parameters:
- p_max: { dims: [g] }
+ capacity: { dims: [g] }
variables:
- p:
+ dispatch:
dims: [g]
- where: "p_max > 0"
+ where: "capacity > 0"
```
-With `p_max = {wind: 10, gas: 5, old: 0}`, the model has `p[wind]` and
-`p[gas]`. There is no `p[old]`.
+With `capacity = {wind: 10, gas: 5, old: 0}`, the model has `dispatch[wind]` and
+`dispatch[gas]`. There is no `dispatch[old]`.
The [grammar](expressions.md#where-strings) says what a `where:` may contain.
This page says what the mask means for the rows that are built.
@@ -51,7 +51,7 @@ operator, it does not.
```yaml
variables:
x: { dims: [g] }
- y: { dims: [g], where: "p_max > 0" } # no y[old]
+ y: { dims: [g], where: "capacity > 0" } # no y[old]
constraints:
each:
dims: [g]
@@ -148,8 +148,9 @@ them, and that is the start of the recurrence rather than a bug.
A [reported expression](reported.md) is arithmetic over solved numbers, so it
inherits their absence by the same rule as above. Through pointwise arithmetic,
a null spreads: `cost / delivered` has no value wherever either operand is
-masked. Out of a summing operator, it does not: `sum(p, over=g)` is one summand
-shorter where a `p[g]` is masked, and stands as long as one slot does.
+masked. Out of a summing operator, it does not: `sum(dispatch, over=g)` is one
+summand shorter where a `dispatch[g]` is masked, and stands as long as one slot
+does.
A quotient whose divisor solved to zero is absent in the same way. The language
has one "no value", and an undefined quotient joins it rather than raising a
@@ -163,7 +164,7 @@ separate not-a-number.
| You want | You write |
| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| the row kept, the masked variable read as zero | `absence: zero` on the variable |
-| the row dropped where a parameter has no data | `where: p` on the constraint |
+| the row dropped where a parameter has no data | `where: capacity` on the constraint |
| a vacated shift position to contribute | `shift(x, along=d, offset=n, edge=0)` |
| to test whether a variable exists here | its bare name in a `where` |
| a bound only where the data has one | supply the bound, because `inf` is a value, or mask the variable. These are different models, so the language infers neither |
diff --git a/docs/reference/language/declarations.md b/docs/reference/language/declarations.md
index d7f7f2ae..9b604b55 100644
--- a/docs/reference/language/declarations.md
+++ b/docs/reference/language/declarations.md
@@ -78,14 +78,14 @@ dimensions:
snapshot: { dtype: int }
generator: { dtype: str }
parameters:
- p_max: { dims: [generator] }
+ capacity: { dims: [generator] }
variables:
- p:
+ dispatch:
dims: [snapshot, generator]
- where: "p_max > 0"
+ where: "capacity > 0"
bounds:
lower: 0
- upper: p_max
+ upper: capacity
```
| Field | | |
@@ -101,7 +101,7 @@ variables:
You write non-negativity. The language does not assume it.
-A bound is a name or a number, never arithmetic. `upper: p_max` is accepted, and
+A bound is a name or a number, never arithmetic. `upper: capacity` is accepted, and
`upper: -rating` is refused with a message that says so. Ship the negated column
as data. Arithmetic in a bound is
[#31](https://github.com/fluxopt/lpspec/issues/31). The dimensions of a bound
@@ -126,11 +126,11 @@ dimensions:
parameters:
load: { dims: [snapshot] }
variables:
- p: { dims: [snapshot, generator] }
+ dispatch: { dims: [snapshot, generator] }
constraints:
power_balance:
dims: [snapshot]
- expression: sum(p, over=generator) == load
+ expression: sum(dispatch, over=generator) == load
```
| Field | | |
@@ -188,10 +188,10 @@ dimensions:
parameters:
cost: { dims: [generator] }
variables:
- p: { dims: [generator] }
+ dispatch: { dims: [generator] }
objective:
sense: minimize
- expression: sum(p * cost)
+ expression: sum(dispatch * cost)
```
| Field | | |
diff --git a/docs/reference/language/dimensions.md b/docs/reference/language/dimensions.md
index f1604225..2bada4d0 100644
--- a/docs/reference/language/dimensions.md
+++ b/docs/reference/language/dimensions.md
@@ -38,7 +38,7 @@ same model.
1. **The members come from the key named after the dimension.** An engine reads
`generator` from the `generator` table, and from nowhere else. It reads
- `p_max` for its values, never for its list of generators, and it does not
+ `capacity` for its values, never for its list of generators, and it does not
treat `gen_bus` as the list either. If a declaration uses `generator` and
no `generator` table arrives, the engine raises an error that names
`generator`. It does not build an empty axis, because an empty axis would
@@ -48,7 +48,7 @@ same model.
sort them, whether they are strings, integers or dates.
[`shift`](operators.md#shift), `sum_back` and `position()` all count along
this order, so an engine that sorted `snapshot` would give
- `shift(p, along=snapshot, offset=1)` a different meaning. To get a
+ `shift(dispatch, along=snapshot, offset=1)` a different meaning. To get a
particular order, write the table in that order.
3. **A table has each coordinate at most once.** Two rows for `snapshot == 3`
is an error that names `3`. The engine does not keep the last, keep the
diff --git a/docs/reference/language/errors.md b/docs/reference/language/errors.md
index 7354836a..aaa3bdad 100644
--- a/docs/reference/language/errors.md
+++ b/docs/reference/language/errors.md
@@ -23,8 +23,8 @@ message lists the valid options:
```text
Constraint 'balance', equation 0: 'p_charge' not found.
- Variables: ['p', 'soc']
- Parameters: ['p_max', 'load', 'efficiency']
+ Variables: ['dispatch', 'soc']
+ Parameters: ['capacity', 'load', 'efficiency']
Check for typos, or ensure 'p_charge' is declared.
```
diff --git a/docs/reference/language/index.md b/docs/reference/language/index.md
index d2146542..c081f237 100644
--- a/docs/reference/language/index.md
+++ b/docs/reference/language/index.md
@@ -19,22 +19,22 @@ dimensions:
parameters:
load: { dims: [snapshot] }
cost: { dims: [generator] }
- p_max: { dims: [generator] }
+ capacity: { dims: [generator] }
variables:
- p:
+ dispatch:
dims: [snapshot, generator]
- where: "p_max > 0"
- bounds: { lower: 0, upper: p_max }
+ where: "capacity > 0"
+ bounds: { lower: 0, upper: capacity }
constraints:
power_balance:
dims: [snapshot]
- expression: sum(p, over=generator) == load
+ expression: sum(dispatch, over=generator) == load
objective:
sense: minimize
- expression: sum(p * cost) # an objective is one number, so the sum is written
+ expression: sum(dispatch * cost) # an objective is one number, so the sum is written
```
That file is a complete model. The pages below give the exact rules.
@@ -49,12 +49,12 @@ message that names the fix. These ten rules are what it checks.
| 1 | A file has ten declaration keys, plus `version` and `description`. A key the schema does not know is refused, with the nearest valid key named: `boundz` → `bounds`. | [File shape](file.md) |
| 2 | Everything that can be checked without data is checked when the file loads. | [Errors](errors.md) |
| 3 | Every name is declared once. A parameter and a dimension both called `snapshot` is refused, and the message names both lines. | [Names](expressions.md#name-resolution) |
-| 4 | Where a name may stand depends on what it is. A dimension may follow `over=` or `along=`, and may not be multiplied: `p * snapshot` is refused, because `snapshot` is an axis and not a column of numbers. | [Names](expressions.md#name-resolution) |
+| 4 | Where a name may stand depends on what it is. A dimension may follow `over=` or `along=`, and may not be multiplied: `dispatch * snapshot` is refused, because `snapshot` is an axis and not a column of numbers. | [Names](expressions.md#name-resolution) |
| 5 | `a + b` carries the dimensions of `a` and of `b` together. A constraint's expression must carry **exactly** its `dims`. The objective must carry none. A `where` or a bound may carry fewer dimensions than its declaration, never more. | [How dimensions combine](expressions.md#how-dimensions-combine) |
| 6 | A variable's `where:` deletes the variable at the masked coordinates. There is no column there, not a column fixed at zero. A constraint's `where:` deletes the row. | [Absence](absence.md) |
| 7 | A deleted variable takes its row with it: `x + y >= 1` has no row where `y` is deleted. Inside a `sum` it is one term fewer, and the row stays. So `sum(x + y)` and `sum(x) + sum(y)` are different constraints. | [Absence](absence.md#how-absence-travels) |
| 8 | A parameter row that is missing from the table reads as `0` in arithmetic and as false in a `where`. Where `0` would change the model, as in a divisor or a bound, the missing row is refused instead. | [Absence](absence.md#what-creates-absence) |
-| 9 | The objective and the constraints may multiply two variables: `p * p * wear`. A bound and a `piecewise:` link may not. `x / y` needs `y` free of variables, and `a ** b` needs both `a` and `b` free of them. | [Expressions](expressions.md) |
+| 9 | The objective and the constraints may multiply two variables: `dispatch * dispatch * wear`. A bound and a `piecewise:` link may not. `x / y` needs `y` free of variables, and `a ** b` needs both `a` and `b` free of them. | [Expressions](expressions.md) |
| 10 | The operators are `sum`, `sum_back`, `at`, `shift`, and `dual` in a reported expression. There are no others, and a file cannot add one. Write a composition of them as a macro. | [Operators](operators.md) |
## The pages
diff --git a/docs/reference/notation.md b/docs/reference/notation.md
index e37ca6b6..86346c9b 100644
--- a/docs/reference/notation.md
+++ b/docs/reference/notation.md
@@ -27,10 +27,10 @@ operator _does_ is [Operators](language/operators.md), which renders the same
math one row per call shape. And it is not a tutorial: the models under
`examples/` are the ones written to be read.
-The symbols are the **derived** ones, taken with no symbol table, because that
-is what a model prints with no setup — $\mathit{load}_{t}$ rather than
-$\ell_t$. A [symbol table](typeset.md#symbol-tables) replaces them wholesale
-and changes nothing else on this page.
+The symbols below are **derived** from the names in the file, which is what a
+model prints with no setup, so you see $\mathrm{load}_{t}$ rather than $\ell_t$.
+A [symbol table](typeset.md#symbol-tables) replaces every symbol, and changes
+nothing else on this page.
### The legend
@@ -991,7 +991,7 @@ names:
cost_curve:
over: bp
links:
- - [p, bp_x]
+ - [dispatch, bp_x]
- [op_cost, bp_y]
method: sos2
```
@@ -1001,7 +1001,7 @@ cost_curve:
```
```math
-p_{t,g} = \sum_{b \in \mathcal{B}} \lambda_{t,g,b} \cdot \mathrm{x}_{g,b} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G}
+\mathit{dispatch}_{t,g} = \sum_{b \in \mathcal{B}} \lambda_{t,g,b} \cdot \mathrm{x}_{g,b} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G}
```
```math
@@ -1035,7 +1035,7 @@ names:
cost_curve:
over: bp
links:
- - [p, bp_x]
+ - [dispatch, bp_x]
- [op_cost, bp_y]
method: convex
```
@@ -1045,7 +1045,7 @@ cost_curve:
```
```math
-p_{t,g} = \sum_{b \in \mathcal{B}} \lambda_{t,g,b} \cdot \mathrm{x}_{g,b} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G}
+\mathit{dispatch}_{t,g} = \sum_{b \in \mathcal{B}} \lambda_{t,g,b} \cdot \mathrm{x}_{g,b} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G}
```
```math
@@ -1074,21 +1074,21 @@ names:
cost_curve:
over: bp
links:
- - [p, bp_x]
+ - [dispatch, bp_x]
- [op_cost, bp_y, ">="]
method: lp
```
```math
-\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\, t \in \mathcal{T},\ g \in \mathcal{G},\ b \in \mathcal{B} \,:\, \mathrm{pos}(b) \neq 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( \mathit{dispatch}_{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\, t \in \mathcal{T},\ g \in \mathcal{G},\ b \in \mathcal{B} \,:\, \mathrm{pos}(b) \neq 0
```
```math
-p_{t,g} \ge \mathrm{x}_{g,b} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G},\ b \in \mathcal{B} \,:\, \mathrm{pos}(b) = 0
+\mathit{dispatch}_{t,g} \ge \mathrm{x}_{g,b} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G},\ b \in \mathcal{B} \,:\, \mathrm{pos}(b) = 0
```
```math
-p_{t,g} \le \mathrm{x}_{g,b} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G},\ b \in \mathcal{B} \,:\, \mathrm{pos}(b) = \lvert \mathcal{B} \rvert - 1
+\mathit{dispatch}_{t,g} \le \mathrm{x}_{g,b} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G},\ b \in \mathcal{B} \,:\, \mathrm{pos}(b) = \lvert \mathcal{B} \rvert - 1
```
### Sets carried to the solver
diff --git a/docs/reference/typeset.md b/docs/reference/typeset.md
index 29c0f7b9..41f2047b 100644
--- a/docs/reference/typeset.md
+++ b/docs/reference/typeset.md
@@ -94,9 +94,9 @@ comment beside the value it computes:
```python
ms.typeset_declaration('model.yaml', 'spend', 'latex')
-# \mathit{spend}_{t} = \sum_{g \in \mathcal{G}} p_{t,g} \cdot \mathrm{cost}_{g} \qquad \forall\, t \in \mathcal{T}
+# \mathit{spend}_{t} = \sum_{g \in \mathcal{G}} \mathit{dispatch}_{t,g} \cdot \mathrm{cost}_{g} \qquad \forall\, t \in \mathcal{T}
ms.typeset_declaration('model.yaml', 'balance', 'latex')
-# \sum_{g \in \mathcal{G}} p_{t,g} = \mathrm{load}_{t} \qquad \forall\, t \in \mathcal{T}
+# \sum_{g \in \mathcal{G}} \mathit{dispatch}_{t,g} = \mathrm{load}_{t} \qquad \forall\, t \in \mathcal{T}
```
It takes what the other functions take, plus the name, the format and an
@@ -123,7 +123,7 @@ name for both.
## Symbol tables
With no table, the symbols are **derived** from the names in the file, such as
-$\mathit{load}_t$ and $p^{\mathrm{max}}_g$. A derived symbol names one
+$\mathrm{load}_t$ and $\mathrm{capacity}_g$. A derived symbol names one
declaration and no other, so a model prints with no setup. A symbol table makes
the output conventional:
@@ -137,7 +137,7 @@ symbols = {
'names': {
'cost': 'c',
'load': '\\ell',
- 'p_max': '\\bar p',
+ 'capacity': '\\bar p',
},
}
@@ -157,7 +157,7 @@ dimensions:
names:
cost: c
load: "\\ell"
- p_max: "\\bar p"
+ capacity: "\\bar p"
```
| Section | |
diff --git a/examples/commitment.yaml b/examples/commitment.yaml
index 07e59081..770ba19e 100644
--- a/examples/commitment.yaml
+++ b/examples/commitment.yaml
@@ -16,18 +16,18 @@ dimensions:
parameters:
committable: { dims: [generator], dtype: bool, description: whether the unit may be switched off }
status_initial: { dims: [generator], description: whether the unit was running before the horizon }
- p_max: { dims: [generator], description: installed capacity }
- p_min: { dims: [generator], description: output floor while running }
+ capacity: { dims: [generator], description: installed capacity }
+ min_output: { dims: [generator], description: output floor while running }
ramp_limit: { dims: [generator], description: how far output may move between snapshots while running }
start_up_limit: { dims: [generator], description: how far it may move in the snapshot it starts in }
load: { dims: [snapshot], description: demand to be met }
cost: { dims: [generator], description: marginal cost }
variables:
- p:
+ dispatch:
description: output of a generator in a snapshot
dims: [snapshot, generator]
- bounds: { lower: 0, upper: p_max }
+ bounds: { lower: 0, upper: capacity }
status:
description: whether the unit is running in a snapshot
dims: [snapshot, generator]
@@ -49,24 +49,24 @@ expressions:
constraints:
power_balance:
dims: [snapshot]
- expression: sum(p, over=generator) == load
+ expression: sum(dispatch, over=generator) == load
upper:
description: a unit that is not running produces nothing
dims: [snapshot, generator]
- expression: p <= status * p_max
+ expression: dispatch <= status * capacity
lower:
description: and one that is running produces at least its floor
dims: [snapshot, generator]
- expression: p >= status * p_min
+ expression: dispatch >= status * min_output
ramp_up:
description: >-
one inequality for both regimes — a unit already running is held to
`ramp_limit`, a unit starting up to `start_up_limit`.
dims: [snapshot, generator]
expression: >-
- p - shift(p, along=snapshot, offset=1, edge=0)
+ dispatch - shift(dispatch, along=snapshot, offset=1, edge=0)
<= ramp_limit * previous_status + start_up_limit * (1 - previous_status)
objective:
sense: minimize
- expression: sum(p * cost)
+ expression: sum(dispatch * cost)
diff --git a/examples/dispatch.yaml b/examples/dispatch.yaml
index 318e48ff..4d0bcef8 100644
--- a/examples/dispatch.yaml
+++ b/examples/dispatch.yaml
@@ -9,22 +9,22 @@ dimensions:
generator: { description: generating units }
parameters:
- p_max: { dims: [generator], description: installed capacity }
+ capacity: { dims: [generator], description: installed capacity }
load: { dims: [snapshot], description: demand to be met }
cost: { dims: [generator], description: marginal cost }
variables:
- p:
+ dispatch:
description: output of a generator in a snapshot
dims: [snapshot, generator]
- where: "p_max > 0"
- bounds: { lower: 0, upper: p_max }
+ where: "capacity > 0"
+ bounds: { lower: 0, upper: capacity }
constraints:
power_balance:
dims: [snapshot]
- expression: sum(p, over=generator) == load
+ expression: sum(dispatch, over=generator) == load
objective:
sense: minimize
- expression: sum(p * cost)
+ expression: sum(dispatch * cost)
diff --git a/examples/piecewise.yaml b/examples/piecewise.yaml
index 19bb7d0b..60953048 100644
--- a/examples/piecewise.yaml
+++ b/examples/piecewise.yaml
@@ -18,7 +18,7 @@ dimensions:
dtype: int
parameters:
- p_max:
+ capacity:
description: maximum dispatch
dims: [generator]
load:
@@ -32,12 +32,12 @@ parameters:
dims: [generator, bp]
variables:
- p:
+ dispatch:
description: dispatched power
dims: [snapshot, generator]
bounds:
lower: 0
- upper: p_max
+ upper: capacity
op_cost:
description: operating cost, piecewise-linear in dispatch
dims: [snapshot, generator]
@@ -51,14 +51,14 @@ piecewise:
binaries to keep them on one segment
over: bp
links:
- - [p, bp_x]
+ - [dispatch, bp_x]
- [op_cost, bp_y]
method: convex
constraints:
balance:
dims: [snapshot]
- expression: sum(p, over=generator) == load
+ expression: sum(dispatch, over=generator) == load
objective:
sense: minimize
diff --git a/examples/piecewise_lp.yaml b/examples/piecewise_lp.yaml
index 05318da2..ff59a8c9 100644
--- a/examples/piecewise_lp.yaml
+++ b/examples/piecewise_lp.yaml
@@ -21,7 +21,7 @@ dimensions:
dtype: int
parameters:
- p_max:
+ capacity:
description: maximum dispatch
dims: [generator]
load:
@@ -35,12 +35,12 @@ parameters:
dims: [generator, bp]
variables:
- p:
+ dispatch:
description: dispatched power
dims: [snapshot, generator]
bounds:
lower: 0
- upper: p_max
+ upper: capacity
op_cost:
description: operating cost, held above every segment of the generator's curve
dims: [snapshot, generator]
@@ -56,14 +56,14 @@ piecewise:
optimal either way
over: bp
links:
- - [p, bp_x]
+ - [dispatch, bp_x]
- [op_cost, bp_y, ">="]
method: lp
constraints:
balance:
dims: [snapshot]
- expression: sum(p, over=generator) == load
+ expression: sum(dispatch, over=generator) == load
objective:
sense: minimize
diff --git a/examples/sos.yaml b/examples/sos.yaml
index 6d7ca2a8..94a2978e 100644
--- a/examples/sos.yaml
+++ b/examples/sos.yaml
@@ -18,7 +18,7 @@ dimensions:
dtype: int
parameters:
- p_max:
+ capacity:
description: maximum dispatch
dims: [generator]
load:
@@ -32,12 +32,12 @@ parameters:
dims: [generator, bp]
variables:
- p:
+ dispatch:
description: dispatched power
dims: [snapshot, generator]
bounds:
lower: 0
- upper: p_max
+ upper: capacity
op_cost:
description: operating cost, piecewise-linear in dispatch
dims: [snapshot, generator]
@@ -52,14 +52,14 @@ piecewise:
declared as a set instead
over: bp
links:
- - [p, bp_x]
+ - [dispatch, bp_x]
- [op_cost, bp_y]
method: sos2
constraints:
balance:
dims: [snapshot]
- expression: sum(p, over=generator) == load
+ expression: sum(dispatch, over=generator) == load
objective:
sense: minimize
diff --git a/examples/symbols/dispatch.yaml b/examples/symbols/dispatch.yaml
index 0c046f9f..3e953ed0 100644
--- a/examples/symbols/dispatch.yaml
+++ b/examples/symbols/dispatch.yaml
@@ -23,4 +23,4 @@ dimensions:
names:
cost: c
load: "\\ell"
- p_max: "\\bar p"
+ capacity: "\\bar p"
diff --git a/tests/fixtures.py b/tests/fixtures.py
index f389bc84..cf5c0caa 100644
--- a/tests/fixtures.py
+++ b/tests/fixtures.py
@@ -22,8 +22,10 @@
#: from the same directory, so a probe added for the page is swept here too.
OPERATOR_PROBES = sorted((EXAMPLES / 'operators').glob('*.yaml'))
-#: ``examples/dispatch.yaml`` without its ``where:`` and with the constraint
-#: named ``balance``, as a dict a test can vary with :func:`override`.
+#: The shape of ``examples/dispatch.yaml`` as a dict a test can vary with
+#: :func:`override`: no ``where:``, the constraint named ``balance``, and short
+#: names, so a test that prints it asserts on the math rather than on the
+#: example's own vocabulary.
DISPATCH_MODEL: dict[str, Any] = {
'dimensions': {'snapshot': {'dtype': 'int'}, 'generator': {'dtype': 'str'}},
'parameters': {
diff --git a/tests/test_lowering.py b/tests/test_lowering.py
index 7ea96867..84ddb1f8 100644
--- a/tests/test_lowering.py
+++ b/tests/test_lowering.py
@@ -69,8 +69,8 @@
DISPATCH_YAML = EXAMPLES / 'dispatch.yaml'
-#: The mask `examples/dispatch.yaml` puts on `p`, as the plan carries it.
-P_MAX_POSITIVE = ParameterComparisonNode('p_max', '>', 0.0, ('generator',))
+#: The mask `examples/dispatch.yaml` puts on `dispatch`, as the plan carries it.
+CAPACITY_POSITIVE = ParameterComparisonNode('capacity', '>', 0.0, ('generator',))
#: One dimension, one parameter, one bounded variable and a scalar constraint:
#: the smallest model that loads, for a claim about the plan's record rather
@@ -134,24 +134,24 @@ def shapes_schema() -> Spec:
def test_lower_program_structure(dispatch_program):
- assert list(dispatch_program.parameters) == ['p_max', 'load', 'cost'], 'keyed by name, in declaration order'
+ assert list(dispatch_program.parameters) == ['capacity', 'load', 'cost'], 'keyed by name, in declaration order'
((vname, v),) = dispatch_program.variables.items()
- assert vname == 'p'
+ assert vname == 'dispatch'
assert v.dims == ('snapshot', 'generator'), 'the frame is the dims, in the order the file wrote it'
- assert v.where == Mask(P_MAX_POSITIVE)
- assert v.upper == Parameter('p_max')
+ assert v.where == Mask(CAPACITY_POSITIVE)
+ assert v.upper == Parameter('capacity')
((cname, c),) = dispatch_program.constraints.items()
assert cname == 'power_balance'
assert c.dims == ('snapshot',), 'the frame is the dims, in the order the file wrote it'
- assert c.lhs == Sum(Variable('p'), ('generator',))
+ assert c.lhs == Sum(Variable('dispatch'), ('generator',))
assert c.sense == '==', "the comparison crosses as the file's own operator, untranslated"
assert c.rhs == Parameter('load')
assert dispatch_program.objective.sense == 'minimize', "the program carries the language's spelling, untranslated"
- assert dispatch_program.objective.expression == Sum(Variable('p') * Parameter('cost'), ('generator', 'snapshot')), (
- 'the objective carries the sum the file wrote, over the dims it named none of'
- )
+ assert dispatch_program.objective.expression == Sum(
+ Variable('dispatch') * Parameter('cost'), ('generator', 'snapshot')
+ ), 'the objective carries the sum the file wrote, over the dims it named none of'
@pytest.mark.parametrize('sense', [pytest.param('minimize', id='minimize'), pytest.param('maximize', id='maximize')])
@@ -171,7 +171,7 @@ def test_a_file_with_no_objective_lowers_to_no_sense():
def test_a_literal_amount_resolves_to_one_signed_number(dispatch_schema):
"""`offset=-1` parses as a unary minus over `1`; after resolution it is `-1`, for every reader alike."""
ns = Namespace.of(dispatch_schema)
- node = expression_of('shift(p, along=snapshot, offset=-1, edge=+2)', dispatch_schema, ns, 't')
+ node = expression_of('shift(dispatch, along=snapshot, offset=-1, edge=+2)', dispatch_schema, ns, 't')
assert isinstance(node, FunctionCallNode)
assert (node.kwargs['offset'], node.kwargs['edge']) == (NumberNode(-1.0), NumberNode(2.0))
@@ -181,32 +181,32 @@ def test_a_literal_amount_resolves_to_one_signed_number(dispatch_schema):
[
pytest.param(None, None, id='no-where-at-all'),
pytest.param('True', None, id='True-is-no-mask'),
- pytest.param('p_max', ParameterDefinedNode('p_max', ('generator',)), id='a-bare-parameter-name'),
+ pytest.param('capacity', ParameterDefinedNode('capacity', ('generator',)), id='a-bare-parameter-name'),
pytest.param(
'snapshot > 5',
DimensionComparisonNode('snapshot', '>', 5),
id='a-dimension-coordinate-compares-like-a-parameter',
),
pytest.param(
- 'p_max > 0 AND NOT load == 0',
- AndNode(P_MAX_POSITIVE, NotNode(ParameterComparisonNode('load', '==', 0.0, ('snapshot',)))),
+ 'capacity > 0 AND NOT load == 0',
+ AndNode(CAPACITY_POSITIVE, NotNode(ParameterComparisonNode('load', '==', 0.0, ('snapshot',)))),
id='a-compound-where-keeps-its-connectives',
),
pytest.param('False', BooleanLiteralNode(False), id='the-empty-declaration-keeps-its-own-spelling'),
- pytest.param('p_max > 0 AND True', P_MAX_POSITIVE, id='and-true-is-the-other-side'),
- pytest.param('p_max > 0 OR False', P_MAX_POSITIVE, id='or-false-is-the-other-side'),
- pytest.param('p_max > 0 OR True', None, id='or-true-is-no-mask-at-all'),
- pytest.param('p_max > 0 AND False', BooleanLiteralNode(False), id='and-false-is-the-empty-declaration'),
+ pytest.param('capacity > 0 AND True', CAPACITY_POSITIVE, id='and-true-is-the-other-side'),
+ pytest.param('capacity > 0 OR False', CAPACITY_POSITIVE, id='or-false-is-the-other-side'),
+ pytest.param('capacity > 0 OR True', None, id='or-true-is-no-mask-at-all'),
+ pytest.param('capacity > 0 AND False', BooleanLiteralNode(False), id='and-false-is-the-empty-declaration'),
pytest.param('NOT True', BooleanLiteralNode(False), id='not-true-is-false'),
pytest.param('NOT False', None, id='not-false-is-no-mask'),
- pytest.param('NOT (p_max > 0 AND False)', None, id='a-branch-folded-away-folds-the-one-above-it'),
+ pytest.param('NOT (capacity > 0 AND False)', None, id='a-branch-folded-away-folds-the-one-above-it'),
pytest.param(
- 'NOT (NOT p_max)',
- ParameterDefinedNode('p_max', ('generator',)),
+ 'NOT (NOT capacity)',
+ ParameterDefinedNode('capacity', ('generator',)),
id='a-double-negation-cancels-on-the-load-path',
),
pytest.param(
- '(p_max > 0 OR True) AND load',
+ '(capacity > 0 OR True) AND load',
ParameterDefinedNode('load', ('snapshot',)),
id='an-absorbed-side-takes-its-own-branch-with-it',
),
@@ -243,18 +243,18 @@ def test_a_lowered_mask_cannot_be_rewritten_in_place(dispatch_program):
"""A consumer handed a program could invert the mask another one reads.
The where nodes were plain dataclasses while every declaration embedding
- them was frozen, so `variable.where.root.op = '!='` rewrote `p_max > 0` into
- `p_max != 0` on the shared object — two consumers disagreeing about one
+ them was frozen, so `variable.where.root.op = '!='` rewrote `capacity > 0` into
+ `capacity != 0` on the shared object — two consumers disagreeing about one
file, which is the failure a program exists to prevent. It also left
hashability depending on the file: an unmasked declaration hashed and a
masked one raised TypeError.
"""
(v,) = dispatch_program.variables.values()
- assert v.where == Mask(P_MAX_POSITIVE)
+ assert v.where == Mask(CAPACITY_POSITIVE)
with pytest.raises(FrozenInstanceError):
v.where.root.op = '!='
- assert v.where == Mask(P_MAX_POSITIVE), 'the mask the file wrote, unchanged'
+ assert v.where == Mask(CAPACITY_POSITIVE), 'the mask the file wrote, unchanged'
assert isinstance(hash(v), int), 'a masked declaration hashes like an unmasked one'
@@ -267,10 +267,10 @@ def test_a_lowered_where_is_a_mask_that_answers_from_its_root(dispatch_program):
"""
(v,) = dispatch_program.variables.values()
- assert v.where == Mask(P_MAX_POSITIVE)
- assert v.where.names_read == {'p_max'}, 'the declarations the mask names'
- assert v.where.conjuncts == (P_MAX_POSITIVE,), 'a mask that is not an AND is its own only conjunct'
- assert v.where.atoms == (P_MAX_POSITIVE,), 'a single leaf, connectives removed'
+ assert v.where == Mask(CAPACITY_POSITIVE)
+ assert v.where.names_read == {'capacity'}, 'the declarations the mask names'
+ assert v.where.conjuncts == (CAPACITY_POSITIVE,), 'a mask that is not an AND is its own only conjunct'
+ assert v.where.atoms == (CAPACITY_POSITIVE,), 'a single leaf, connectives removed'
@pytest.mark.parametrize(
@@ -311,10 +311,10 @@ def test_a_lowered_mask_answers_its_dims_conjuncts_and_atoms(variable, where, di
@pytest.mark.parametrize(
('where', 'under'),
[
- pytest.param(NotNode(P_MAX_POSITIVE), (P_MAX_POSITIVE,), id='a-not-carries-its-operand'),
- pytest.param(AndNode(P_MAX_POSITIVE, FLAG), (P_MAX_POSITIVE, FLAG), id='an-and-carries-both-sides'),
- pytest.param(OrNode(P_MAX_POSITIVE, FLAG), (P_MAX_POSITIVE, FLAG), id='an-or-carries-both-sides'),
- pytest.param(P_MAX_POSITIVE, (), id='a-leaf-carries-nothing'),
+ pytest.param(NotNode(CAPACITY_POSITIVE), (CAPACITY_POSITIVE,), id='a-not-carries-its-operand'),
+ pytest.param(AndNode(CAPACITY_POSITIVE, FLAG), (CAPACITY_POSITIVE, FLAG), id='an-and-carries-both-sides'),
+ pytest.param(OrNode(CAPACITY_POSITIVE, FLAG), (CAPACITY_POSITIVE, FLAG), id='an-or-carries-both-sides'),
+ pytest.param(CAPACITY_POSITIVE, (), id='a-leaf-carries-nothing'),
pytest.param(BooleanLiteralNode(False), (), id='a-literal-carries-nothing'),
],
)
@@ -340,9 +340,9 @@ def test_a_synthetic_predicate_answers_its_own_dims():
"""
b = ParameterDefinedNode('load', ('snapshot',))
- assert Mask(NotNode(P_MAX_POSITIVE)).dims == {'generator'}, 'negation keeps the dims it negates'
- assert (Mask(P_MAX_POSITIVE) & Mask(b)).dims == {'generator', 'snapshot'}, 'conjunction unions both sides'
- assert (Mask(P_MAX_POSITIVE) & Mask(b)).root == AndNode(P_MAX_POSITIVE, b), (
+ assert Mask(NotNode(CAPACITY_POSITIVE)).dims == {'generator'}, 'negation keeps the dims it negates'
+ assert (Mask(CAPACITY_POSITIVE) & Mask(b)).dims == {'generator', 'snapshot'}, 'conjunction unions both sides'
+ assert (Mask(CAPACITY_POSITIVE) & Mask(b)).root == AndNode(CAPACITY_POSITIVE, b), (
'the conjunction joins the roots under one AND'
)
@@ -547,8 +547,10 @@ def test_a_relation_lowers_with_the_walk_each_call_takes():
def test_a_binary_variable_lowers_to_a_binary_domain():
- program = to_program(schema_of(DISPATCH_YAML, **{'variables.p.domain': 'binary', 'variables.p.bounds': {}}))
- assert program.variable('p').domain == 'binary'
+ program = to_program(
+ schema_of(DISPATCH_YAML, **{'variables.dispatch.domain': 'binary', 'variables.dispatch.bounds': {}})
+ )
+ assert program.variable('dispatch').domain == 'binary'
def test_a_divisor_under_a_pullback_is_still_named():
diff --git a/tools/home_math.py b/tools/home_math.py
index 72576d55..0c365f56 100644
--- a/tools/home_math.py
+++ b/tools/home_math.py
@@ -2,15 +2,16 @@
#
# SPDX-License-Identifier: MIT
-"""The homepage's model and the math block under it, from one file.
+"""The homepage's model and the math under it, from one file.
- pixi run python -m tools.home_math # rewrite both blocks
- pixi run python -m tools.home_math --check # fail if either has drifted
+ pixi run python -m tools.home_math # rewrite every block
+ pixi run python -m tools.home_math --check # fail if one has drifted
-Two files carry it: ``README.md`` holds the YAML, which the site pulls in as a
-snippet, and ``docs/index.md`` holds the math, which is a tabbed block and
-would be raw markup on GitHub. The third tab is the call that produced the
-other two.
+``examples/dispatch.yaml`` is printed once and spliced into two pages that need
+different shapes for it. GitHub renders the Markdown math but not a tabbed
+block, so ``README.md`` takes the equations with the other two formats folded
+under them, and ``docs/index.md`` takes tabs, plus a third tab holding the call
+that produced the other two.
"""
from __future__ import annotations
@@ -18,7 +19,7 @@
import textwrap
from math_spec import to_spec
-from math_spec.typesetting import to_latex, to_markdown
+from math_spec.typesetting import to_latex, to_markdown, to_typst
from tools._page import ROOT, sidecar_for, splice, without_header
from tools._page import main as page_main
@@ -26,6 +27,7 @@
README = ROOT / 'README.md'
MODEL = ROOT / 'examples' / 'dispatch.yaml'
BEGIN, END = '', ''
+README_BEGIN, README_END = '', ''
#: The snippet markers `pymdownx.snippets` reads, which is how the same YAML
#: reaches the site without being typed twice.
MODEL_BEGIN, MODEL_END = '', ''
@@ -46,7 +48,7 @@
'names': {
'cost': 'c',
'load': '\\\\ell',
- 'p_max': '\\\\bar p',
+ 'capacity': '\\\\bar p',
},
}
@@ -57,22 +59,21 @@
ms.to_markdown(spec) # renders as-is on GitHub
```
-`symbols` is optional — drop it and the same model prints as
-$\\mathit{load}_t$, $p^{\\mathrm{max}}_g$. A dict, a YAML path or a
-`SymbolTable`; a key naming nothing in the model is an error, not a symbol that
-silently never applies. Every spelling is printed verbatim — `notation` says
-which language they are, and a render in the other one refuses.
+`symbols` gives every name its conventional spelling. Pass a dict, a YAML path
+or a `SymbolTable`. It is optional: drop it and the same model prints from the
+names in the file, as $\\mathrm{load}_t$ and $\\mathrm{capacity}_g$.
-Or from a shell, where the table is that same YAML on disk and `--standalone`
-emits a document that compiles rather than a fragment to `\\input`:
+Or from a shell, where the table is that same YAML on disk. `--standalone` emits
+a document that compiles, rather than a fragment to `\\input`:
```bash
python -m math_spec latex dispatch.yaml --symbols dispatch.symbols.yaml
python -m math_spec typst dispatch.yaml --standalone -o dispatch.typ
```
-The renderer is [the typesetter](reference/typeset.md), and it reads the same
-file every other page here loads."""
+[Typeset the math](reference/typeset.md) documents the three functions, their
+options and symbol tables. Each reads the same file every other page here
+loads."""
def tab(title: str, body: str) -> str:
@@ -95,9 +96,52 @@ def block() -> str:
)
+def details(summary: str, body: str) -> str:
+ """A folded block. GitHub reads what is inside as markdown only across a blank line."""
+ return f'\n{summary}
\n\n{body}\n\n '
+
+
+def readme_block() -> str:
+ """The equations GitHub renders, then the whole document folded under them.
+
+ The visible block carries no legend and no symbol table, because a README
+ is read before anything else: three legend tables are half its length, and
+ a derived symbol is the file's own name, which needs no table to be read.
+ The first fold is what the legend and a table add.
+
+ Typst is printed with no table for a second reason: the sidecar is written
+ in LaTeX, and a render in the other notation is refused.
+ """
+ spec = to_spec(MODEL)
+ symbols = sidecar_for(MODEL)
+ return '\n\n'.join(
+ (
+ to_markdown(spec, numbered=False, legend=False).strip(),
+ details(
+ 'The whole document: a symbol table, and the legend it prints',
+ to_markdown(spec, symbols=symbols, numbered=False).strip(),
+ ),
+ details(
+ 'The same document as LaTeX',
+ f'```latex\n{to_latex(spec, symbols=symbols, numbered=False).strip()}\n```',
+ ),
+ details(
+ 'The same document as Typst, printed with no symbol table',
+ f'```typst\n{to_typst(spec, numbered=False).strip()}\n```',
+ ),
+ )
+ )
+
+
def rendered_readme(readme: str) -> str:
- """Prettier wants a blank line on each side of the markers, so the block carries them."""
- return splice(readme, MODEL_BEGIN, MODEL_END, f'\n```yaml title="{MODEL.name}"\n{without_header(MODEL)}\n```\n')
+ """Prettier wants a blank line on each side of the markers, so each block carries them."""
+ model = f'\n```yaml title="{MODEL.name}"\n{without_header(MODEL)}\n```\n'
+ return splice(
+ splice(readme, MODEL_BEGIN, MODEL_END, model),
+ README_BEGIN,
+ README_END,
+ f'\n{readme_block()}\n',
+ )
def rendered_page(page: str) -> str: