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: