From f25159f10900c5975c29c237921a961db3e16e6b Mon Sep 17 00:00:00 2001 From: Felix Bumann Date: Thu, 10 Sep 2026 07:23:56 +0000 Subject: [PATCH 1/3] docs: the README shows the math a model prints, in all three formats The README described the typesetter and never showed it. The dispatch model now stands beside the equations printed from it, in the Markdown that GitHub renders as math. `tools/home_math.py` writes the block from `examples/dispatch.yaml`, so nothing in it is hand-typed. The visible block carries no legend and no symbol table. Three legend tables are half the length of the document, and a derived symbol is the file's own name, so the equations read without them: 44 lines become 23. A smaller model does not do this. The same model cut to two parameters, with no `where:` and no upper bound, prints 45 lines, because dropping the table adds the convention note. Four folded blocks hold the whole document: with the symbol table and its legend, as LaTeX, and as Typst. The Typst block is printed with no table, which makes it the evidence for what a derived symbol looks like. A parameter is upright, so `load` prints as \mathrm{load}_t and `p_max` as \mathrm{p}^{\mathrm{max}}_g. Three pages claimed \mathit{load}_t and p^{\mathrm{max}}_g for the same two names, and docs/reference/notation.md contradicted the legend printed further down its own page. The README also named a variable the example does not declare: the model declares `p`, and three sentences called it `dispatch`. README.md: n 88, avg 12.7, median 11, over25 13. Every sentence over 25 words is in a section this commit does not touch. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01EQ6np3vx2fqJB5PfQjquRJ --- .prettierignore | 11 +- README.md | 224 ++++++++++++++++++++++++++++++++----- docs/index.md | 20 ++-- docs/reference/notation.md | 2 +- docs/reference/typeset.md | 2 +- tools/home_math.py | 85 ++++++++++---- 6 files changed, 282 insertions(+), 62 deletions(-) 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 7a887fec..bb815ec6 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,8 @@ 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, over=generator) == load`. The file [below](#example) is a complete +decisions the solver makes, such as `p`; and the rules those decisions obey, such +as `sum(p, over=generator) == load`. The file [below](#example) is a complete model. math-spec reads that file, checks everything that can be checked without data, @@ -102,46 +102,190 @@ objective: -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. - +### What that file says -```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}} p_{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}} p_{t,g} = \mathrm{load}_{t} \qquad \forall\, t \in \mathcal{T} +``` + +#### Variable domains + +**`p`** + +```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 +``` + +
+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`$ | `p_max` over $`\mathcal{G}`$ — installed capacity | +| $`\ell`$ | `load` over $`\mathcal{S}`$ — demand to be met | +| $`c`$ | `cost` over $`\mathcal{G}`$ — marginal cost | + +#### Variables + +| Symbol | Meaning | +|---|---| +| $`p`$ | `p` 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} +``` + +#### Subject to + +**`power_balance`** + +```math +\sum_{g \in \mathcal{G}} p_{s,g} = \ell_{s} \qquad \forall\, s \in \mathcal{S} +``` + +#### Variable domains + +**`p`** + +```math +0 \le p_{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{p\_max} 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 +\end{description} + +\paragraph{Objective} +\begin{align*} + && \min & \sum_{s \in \mathcal{S},\ g \in \mathcal{G}} p_{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} +\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 +\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("p")^(upright("max"))$: `p_max` over $cal(G)$ --- installed capacity +/ $upright("load")$: `load` over $cal(T)$ --- demand to be met +/ $upright("cost")$: `cost` over $cal(G)$ --- marginal cost + +== Variables +/ $p$: `p` 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("p")^(upright("max"))$, a coordinate map, a label --- and italic is what the solver chooses, such as $p$. An index is italic too, being what a quantifier chooses, and a set is script. + +== Objective +$ & min & sum_(t in cal(T), g in cal(G)) p_(t,g) dot upright("cost")_(g) $ + +== Subject to +$ upright("power_balance") & sum_(g in cal(G)) p_(t,g) & = upright("load")_(t) & forall t in cal(T) $ + +== Variable domains +$ upright("p") & 0 <= p_(t,g) & <= upright("p")^(upright("max"))_(g) & forall t in cal(T), g in cal(G) colon upright("p")^(upright("max"))_(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 `p_max` as $`\mathrm{p}^{\mathrm{max}}_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 ``` +### How a tool reads it + + + +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) # ['p'] + +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 second. + + + +[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, diff --git a/docs/index.md b/docs/index.md index a5c97d51..4fe31121 100644 --- a/docs/index.md +++ b/docs/index.md @@ -207,22 +207,24 @@ 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` is optional. Drop it and the same model prints as + $\mathrm{load}_t$ and $\mathrm{p}^{\mathrm{max}}_g$, with no setup. Pass a dict, + a YAML path or a `SymbolTable`. A key that names nothing in the model is an + error, rather than a symbol that silently never applies. Every spelling is + printed as written, and `notation` says which language it is written in. A + render in the other notation is refused. - 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. diff --git a/docs/reference/notation.md b/docs/reference/notation.md index b9cb558f..82d88584 100644 --- a/docs/reference/notation.md +++ b/docs/reference/notation.md @@ -25,7 +25,7 @@ prints the same math with one row per call. For models written to be read, start with the [examples](../examples/index.md). The symbols below are **derived** from the names in the file, which is what a -model prints with no setup, so you see $\mathit{load}_{t}$ rather than $\ell_t$. +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. diff --git a/docs/reference/typeset.md b/docs/reference/typeset.md index 29c0f7b9..cae4d233 100644 --- a/docs/reference/typeset.md +++ b/docs/reference/typeset.md @@ -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{p}^{\mathrm{max}}_g$. A derived symbol names one declaration and no other, so a model prints with no setup. A symbol table makes the output conventional: diff --git a/tools/home_math.py b/tools/home_math.py index 72576d55..d601087f 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. +Two files carry it. ``README.md`` holds the YAML, which the site pulls in as a +snippet, and the document printed from it — Markdown, which GitHub renders as +math, with the other two formats folded under it. ``docs/index.md`` holds the +same document as a tabbed block, which would be raw markup on GitHub, and a +third tab that is the call which 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 = '', '' @@ -57,22 +59,24 @@ 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` is optional. Drop it and the same model prints as +$\\mathrm{load}_t$ and $\\mathrm{p}^{\\mathrm{max}}_g$, with no setup. Pass a dict, +a YAML path or a `SymbolTable`. A key that names nothing in the model is an +error, rather than a symbol that silently never applies. Every spelling is +printed as written, and `notation` says which language it is written in. A +render in the other notation is refused. -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 +99,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: From 62ce3a41d86a2443ef570b4e1511b9d78016ba5d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 10 Sep 2026 14:40:23 +0000 Subject: [PATCH 2/3] docs: the examples say dispatch and capacity, rather than p and p_max `p` and `p_max` are PyPSA's spellings, so a reader without that background had to guess what the decision was. The gallery models and the reference pages now name the decision `dispatch` and its bound `capacity`, and `p_min` becomes `min_output`. The six `pypsa*.yaml` files keep `Generator_p_nom` and `Generator_p_max_pu`, which are PyPSA's own API and are the point of those files. The golden fixture and the operator probes keep `p` too: their subject is which construct prints what, not what a model calls things. Also on this page: `docs/howto/print.md` said a parameter with no symbol table prints as $\mathit{load}_t$, and it prints $\mathrm{load}_t$. Two headings that narrated rather than named a subject become `The math it prints` and `Spec` and `Program`, in the README and on the site homepage together. The `How` tab on the homepage repeated six sentences of `docs/reference/typeset.md`. It now says what `symbols` does, then that it is optional, and leaves the rest to the link it already carried. `tests.fixtures.DISPATCH_MODEL` stays on `p` and `p_max`. It is an inline dict that 11 test files vary; renaming it moved 87 assertions and changed nothing a reader sees. Its docstring no longer claims to be `examples/dispatch.yaml` name for name. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01FerXtCLtx64Mag9uKnA6FZ --- README.md | 70 +++++++++++----------- docs/examples/commitment.md | 40 ++++++------- docs/examples/dispatch.md | 34 +++++------ docs/howto/print.md | 4 +- docs/howto/regimes.md | 22 +++---- docs/index.md | 37 ++++++------ docs/reference/language/absence.md | 19 +++--- docs/reference/language/declarations.md | 18 +++--- docs/reference/language/dimensions.md | 4 +- docs/reference/language/errors.md | 4 +- docs/reference/language/index.md | 16 ++--- docs/reference/notation.md | 16 ++--- docs/reference/typeset.md | 10 ++-- examples/commitment.yaml | 18 +++--- examples/dispatch.yaml | 12 ++-- examples/piecewise.yaml | 10 ++-- examples/piecewise_lp.yaml | 10 ++-- examples/sos.yaml | 10 ++-- examples/symbols/dispatch.yaml | 2 +- tests/fixtures.py | 6 +- tests/test_lowering.py | 80 +++++++++++++------------ tools/home_math.py | 21 +++---- 22 files changed, 231 insertions(+), 232 deletions(-) diff --git a/README.md b/README.md index bb815ec6..21196f5e 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 `p`; and the rules those decisions obey, such -as `sum(p, over=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,32 +79,32 @@ 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 foreach: [snapshot, generator] - where: "p_max > 0" - bounds: { lower: 0, upper: p_max } + where: "capacity > 0" + bounds: { lower: 0, upper: capacity } constraints: power_balance: foreach: [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. -### What that file says +### The math it prints 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 @@ -121,7 +121,7 @@ Least-cost dispatch of a generator fleet against an hourly load. #### 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 @@ -129,15 +129,15 @@ Least-cost dispatch of a generator fleet against an hourly load. **`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 ```
@@ -156,7 +156,7 @@ Least-cost dispatch of a generator fleet against an hourly load. | 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 | @@ -164,12 +164,12 @@ Least-cost dispatch of a generator fleet against an hourly load. | 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 @@ -177,15 +177,15 @@ Least-cost dispatch of a generator fleet against an hourly load. **`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 ```
@@ -204,29 +204,29 @@ Least-cost dispatch of a generator fleet against an hourly load. \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*} ``` @@ -243,23 +243,23 @@ Least-cost dispatch of a generator fleet against an hourly load. / $cal(G)$: index $g$ --- `generator` --- generating units == Parameters -/ $upright("p")^(upright("max"))$: `p_max` over $cal(G)$ --- installed capacity +/ $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 -/ $p$: `p` over $cal(T) times cal(G)$ --- output of a generator in a snapshot +/ $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("p")^(upright("max"))$, a coordinate map, a label --- and italic is what the solver chooses, such as $p$. An index is italic too, being what a quantifier chooses, and a set is script. +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)) p_(t,g) dot upright("cost")_(g) $ +$ & 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)) p_(t,g) & = upright("load")_(t) & forall t in cal(T) $ +$ upright("power_balance") & sum_(g in cal(G)) italic("dispatch")_(t,g) & = upright("load")_(t) & forall t in cal(T) $ == Variable domains -$ upright("p") & 0 <= p_(t,g) & <= upright("p")^(upright("max"))_(g) & forall t in cal(T), g in cal(G) colon upright("p")^(upright("max"))_(g) > 0 $ +$ upright("dispatch") & 0 <= italic("dispatch")_(t,g) & <= upright("capacity")_(g) & forall t in cal(T), g in cal(G) colon upright("capacity")_(g) > 0 $ ``` @@ -280,7 +280,7 @@ ms.to_typst(spec) # compiles without a TeX toolchain ``` Those symbols are the file's own names: `load` prints as $`\mathrm{load}_t`$, -and `p_max` as $`\mathrm{p}^{\mathrm{max}}_g`$. Nothing had to be set up for +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 @@ -295,7 +295,7 @@ python -m math_spec typst dispatch.yaml --standalone -o dispatch.typ python -m math_spec markdown dispatch.yaml ``` -### How a tool reads it +### `Spec` and `Program` @@ -305,7 +305,7 @@ Whatever is wrong with a model is wrong when it loads, not when it solves: import math_spec as ms spec = ms.to_spec('dispatch.yaml') # schema, names, dimensions, degree: all checked here -sorted(spec.variables) # ['p'] +sorted(spec.variables) # ['dispatch'] program = ms.to_program(spec) # curves expanded, names typed, operators resolved to nodes sorted(program.constraints) # ['power_balance'] @@ -314,7 +314,7 @@ 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 second. +turned into its variables and constraints. An engine reads the `Program`. diff --git a/docs/examples/commitment.md b/docs/examples/commitment.md index 797c081f..dac71c30 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 foreach: [snapshot, generator] - bounds: { lower: 0, upper: p_max } + bounds: { lower: 0, upper: capacity } status: description: whether the unit is running in a snapshot foreach: [snapshot, generator] @@ -65,27 +65,27 @@ expressions: constraints: power_balance: foreach: [snapshot] - expression: sum(p, over=generator) == load + expression: sum(dispatch, over=generator) == load upper: description: a unit that is not running produces nothing foreach: [snapshot, generator] - expression: p <= status * p_max + expression: dispatch <= status * capacity lower: description: and one that is running produces at least its floor foreach: [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`. foreach: [snapshot, generator] expression: >- - p - shift(p, over=snapshot, offset=1, edge=0) + dispatch - shift(dispatch, over=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 80172f79..a5c44834 100644 --- a/docs/examples/dispatch.md +++ b/docs/examples/dispatch.md @@ -10,10 +10,10 @@ 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 -[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 -the constraint's `foreach` is what remains. +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(dispatch, over=generator)` names the dimension it +reduces, so the constraint's `foreach` is what remains. ```yaml @@ -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 foreach: [snapshot, generator] - where: "p_max > 0" - bounds: { lower: 0, upper: p_max } + where: "capacity > 0" + bounds: { lower: 0, upper: capacity } constraints: power_balance: foreach: [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 932c3555..ec2ab3bf 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: { foreach: [snapshot, generator], bounds: { lower: 0, upper: p_max } } + dispatch: { foreach: [snapshot, generator], bounds: { lower: 0, upper: capacity } } on: { foreach: [snapshot, generator], where: committable, domain: binary } constraints: floor_committed: foreach: [snapshot, generator] where: committable - expression: p >= p_min * on + expression: dispatch >= min_output * on ceiling_committed: foreach: [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: { foreach: [snapshot, generator], bounds: { lower: 0 } } + dispatch: { foreach: [snapshot, generator], bounds: { lower: 0 } } on: { foreach: [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: foreach: [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 4fe31121..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,12 +207,9 @@ 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 - $\mathrm{load}_t$ and $\mathrm{p}^{\mathrm{max}}_g$, with no setup. Pass a dict, - a YAML path or a `SymbolTable`. A key that names nothing in the model is an - error, rather than a symbol that silently never applies. Every spelling is - printed as written, and `notation` says which language it is written in. A - render in the other notation is refused. + `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. `--standalone` emits a document that compiles, rather than a fragment to `\input`: @@ -228,7 +225,7 @@ choice, and **How** shows the one made here. -### 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 fafdb8d0..5087190c 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: foreach: [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: { foreach: [g] } - y: { foreach: [g], where: "p_max > 0" } # no y[old] + y: { foreach: [g], where: "capacity > 0" } # no y[old] constraints: each: foreach: [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, over=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 0a129101..400a842f 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: foreach: [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: { foreach: [snapshot, generator] } + dispatch: { foreach: [snapshot, generator] } constraints: power_balance: foreach: [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: { foreach: [generator] } + dispatch: { foreach: [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 7b4373b7..fd55102b 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, over=snapshot, offset=1)` a different meaning. To get a + `shift(dispatch, over=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 897397f0..2475083e 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 1ff4fb13..58e6ac0a 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: foreach: [snapshot, generator] - where: "p_max > 0" - bounds: { lower: 0, upper: p_max } + where: "capacity > 0" + bounds: { lower: 0, upper: capacity } constraints: power_balance: foreach: [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=`, 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=`, 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 `foreach`. 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 82d88584..49727a19 100644 --- a/docs/reference/notation.md +++ b/docs/reference/notation.md @@ -855,7 +855,7 @@ names: cost_curve: over: bp links: - - [p, bp_x] + - [dispatch, bp_x] - [op_cost, bp_y] method: sos2 ``` @@ -865,7 +865,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 @@ -899,7 +899,7 @@ names: cost_curve: over: bp links: - - [p, bp_x] + - [dispatch, bp_x] - [op_cost, bp_y] method: convex ``` @@ -909,7 +909,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 @@ -938,21 +938,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 cae4d233..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 -$\mathrm{load}_t$ and $\mathrm{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 b46931e8..60d71797 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 foreach: [snapshot, generator] - bounds: { lower: 0, upper: p_max } + bounds: { lower: 0, upper: capacity } status: description: whether the unit is running in a snapshot foreach: [snapshot, generator] @@ -49,24 +49,24 @@ expressions: constraints: power_balance: foreach: [snapshot] - expression: sum(p, over=generator) == load + expression: sum(dispatch, over=generator) == load upper: description: a unit that is not running produces nothing foreach: [snapshot, generator] - expression: p <= status * p_max + expression: dispatch <= status * capacity lower: description: and one that is running produces at least its floor foreach: [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`. foreach: [snapshot, generator] expression: >- - p - shift(p, over=snapshot, offset=1, edge=0) + dispatch - shift(dispatch, over=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 c24be113..05b8a82c 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 foreach: [snapshot, generator] - where: "p_max > 0" - bounds: { lower: 0, upper: p_max } + where: "capacity > 0" + bounds: { lower: 0, upper: capacity } constraints: power_balance: foreach: [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 a63168bc..5b20b1e6 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 foreach: [snapshot, generator] bounds: lower: 0 - upper: p_max + upper: capacity op_cost: description: operating cost, piecewise-linear in dispatch foreach: [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: foreach: [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 7eea5c4b..f7cfb67f 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 foreach: [snapshot, generator] bounds: lower: 0 - upper: p_max + upper: capacity op_cost: description: operating cost, held above every segment of the generator's curve foreach: [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: foreach: [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 9e1f1b85..49626aa8 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 foreach: [snapshot, generator] bounds: lower: 0 - upper: p_max + upper: capacity op_cost: description: operating cost, piecewise-linear in dispatch foreach: [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: foreach: [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 d06c0c07..568d40d2 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 3bab6500..b1987433 100644 --- a/tests/test_lowering.py +++ b/tests/test_lowering.py @@ -68,8 +68,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 @@ -126,24 +126,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 foreach, 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 foreach, 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')]) @@ -163,7 +163,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, over=snapshot, offset=-1, edge=+2)', dispatch_schema, ns, 't') + node = expression_of('shift(dispatch, over=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)) @@ -173,32 +173,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', ), @@ -235,18 +235,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' @@ -259,10 +259,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( @@ -303,10 +303,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'), ], ) @@ -332,9 +332,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' ) @@ -466,8 +466,10 @@ def test_a_construct_lowers_to_its_node(shapes_schema, expression, expected): 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 d601087f..0c365f56 100644 --- a/tools/home_math.py +++ b/tools/home_math.py @@ -7,11 +7,11 @@ 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 the document printed from it — Markdown, which GitHub renders as -math, with the other two formats folded under it. ``docs/index.md`` holds the -same document as a tabbed block, which would be raw markup on GitHub, and a -third tab that is the call which 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 @@ -48,7 +48,7 @@ 'names': { 'cost': 'c', 'load': '\\\\ell', - 'p_max': '\\\\bar p', + 'capacity': '\\\\bar p', }, } @@ -59,12 +59,9 @@ ms.to_markdown(spec) # renders as-is on GitHub ``` -`symbols` is optional. Drop it and the same model prints as -$\\mathrm{load}_t$ and $\\mathrm{p}^{\\mathrm{max}}_g$, with no setup. Pass a dict, -a YAML path or a `SymbolTable`. A key that names nothing in the model is an -error, rather than a symbol that silently never applies. Every spelling is -printed as written, and `notation` says which language it is written in. A -render in the other notation is refused. +`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. `--standalone` emits a document that compiles, rather than a fragment to `\\input`: From 5a54acec43ce781b6e62b7bfaad9834dee522232 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 15 Sep 2026 20:34:43 +0000 Subject: [PATCH 3/3] docs: the README names sum's own keyword, which is over #437 wrote `sum(consume=)` into the README's opening paragraph and its prior art section. No version of the language has parsed `consume=`: the keyword that reduces a dimension away is `over=`, as `BUILTINS['sum'].usage` and `docs/reference/language/operators.md` both say. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01LjQuLw7nctEbY7ACCGsaLP --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 415cbdc9..953aa399 100644 --- a/README.md +++ b/README.md @@ -381,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