Skip to content
Merged
130 changes: 10 additions & 120 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,9 @@ 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
typesetter that prints it as LaTeX, Typst or Markdown. It builds nothing and
solves nothing itself. Every tool reads the file through the same checked syntax
tree, so an engine and a renderer cannot disagree about what the file means
([what counts as language](docs/about/what-counts-as-language.md)).

<!--- --8<-- [start:engines] -->

math-spec builds nothing and solves nothing itself.
[specsolve](https://github.com/fluxopt/specsolve) and
[linopy](https://github.com/PyPSA/linopy) build and solve a math-spec model.
Support in both is work in progress.
Expand All @@ -50,33 +44,12 @@ Support in both is work in progress.
- **One model, many tools.** Engines, renderers and analysers read the model
through one public API, so no two of them can read the file differently.
[Program API →](https://math-spec.readthedocs.io/en/latest/reference/program/)
- **Review a model as a diff.** The model is plain YAML, and no Python
state changes what it means.
- **Write full-size models.** PyPSA's `n.optimize()` model is
one file, with stochastic, multi-period and quadratic variants.
[PyPSA in one file →](https://math-spec.readthedocs.io/en/latest/examples/pypsa/)

<!--- --8<-- [end:benefits] -->

```mermaid
flowchart LR
Y["model.yaml"] --> S["schema<br/>closed at every level"]
S --> AST["syntax tree<br/>two grammars"]
AST --> Q{"inside the<br/>language?"}
Q -->|"no"| ERR["load error<br/>naming the construct + rewrite"]
Q -->|"yes"| M["Spec<br/>what the file says"]
M -->|".program"| P["Program<br/>names, dimensions and operators resolved"]
P --> ENG["an engine that builds → solver"]
M --> T["to_latex / to_typst / to_markdown"]

classDef spec fill:#f0f7f0,stroke:#3a7d44,stroke-width:2px,color:#111
classDef consumer fill:#eef1fb,stroke:#4a5fc1,stroke-width:2px,color:#111
classDef err fill:#fdf3e7,stroke:#b7791f,color:#111
class S,AST,M,P spec
class ENG,T consumer
class ERR err
```

## Example

<!--- --8<-- [start:model] -->
Expand Down Expand Up @@ -191,80 +164,6 @@ $`0 \le \mathit{dispatch}_{s,g} \le \bar p_{g} \qquad \forall\, s \in \mathcal{S

</details>

<details>
<summary>The same document as LaTeX</summary>

```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*}
```

</details>

<details>
<summary>The same document as Typst, printed with no symbol table</summary>

```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 $
```

</details>

<!-- readme-math:end -->
<!-- prettier-ignore-end -->

Expand All @@ -275,27 +174,23 @@ import math_spec as ms

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)
ms.to_latex(spec)
ms.to_typst(spec)
```

A [symbol table](docs/reference/typeset.md#symbol-tables) gives the names their
conventional spelling, as in the first folded block.
conventional spelling, as in the folded block.
[Print a model as math](docs/howto/print.md) does the same from a shell.
`to_spec` returns a `Spec`, and `spec.program` its `Program`
([reading a spec and its program](docs/reference/reading.md#spec-and-program)).

## Documentation

The documentation is at <https://math-spec.readthedocs.io>.

## Installation

Nothing is published yet.
[Installation](docs/howto/installation.md) gives the command that installs from
git, and [contributing](docs/contributing.md#setting-up-a-development-environment)
sets up a development clone.
See [installation](docs/howto/installation.md). To work on math-spec, see
[contributing](docs/contributing.md#setting-up-a-development-environment).

## Prior art

Expand All @@ -304,8 +199,7 @@ and extracted here. 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(over=)` and the dimension rules are named against. Issue numbers in these
pages point at specsolve, where the arguments happened.
`sum(over=)` and the dimension rules are named against.

## Status

Expand All @@ -324,9 +218,5 @@ not yet frozen.

## Licence

The code is [MIT](LICENSE): everything under `src/`, `tests/`, `tools/`, the
examples, and the generated schema.

The prose is [CC-BY-4.0](LICENSES/CC-BY-4.0.txt): everything under `docs/`, this
README, `CHANGELOG.md`, and the logos in `resources/`. Reuse it freely, with
attribution.
The code is [MIT](LICENSE), and the prose is
[CC-BY-4.0](LICENSES/CC-BY-4.0.txt).
11 changes: 5 additions & 6 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,11 +153,11 @@ call.
},
}

spec = ms.to_spec('dispatch.yaml') # read and checked once, then printed three ways
spec = ms.to_spec('dispatch.yaml')

ms.to_latex(spec, symbols=symbols) # amsmath align
ms.to_typst(spec) # compiles without a TeX toolchain
ms.to_markdown(spec) # renders as-is on GitHub
ms.to_latex(spec, symbols=symbols)
ms.to_typst(spec)
ms.to_markdown(spec)
```

`symbols` gives every name its conventional spelling. Pass a dict, a YAML path
Expand Down Expand Up @@ -193,8 +193,7 @@ call.

## Install it

Nothing is published yet. [Installation](howto/installation.md) gives the
command that installs from git.
See [installation](howto/installation.md).

!!! warning "Alpha, pre-1.0"

Expand Down
4 changes: 4 additions & 0 deletions docs/static/extras.css
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,10 @@

/* Benefit cards: the README bullet's bold lead-in is the card's title, and its
* one link covers the whole card, since the README cannot carry card markup. */
.md-typeset .landing .grid.cards {
grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr));
}

.md-typeset .landing .grid.cards > ul > li {
position: relative;
color: var(--md-default-fg-color--light);
Expand Down
2 changes: 1 addition & 1 deletion tests/test_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def test_no_fold_in_the_readme_prints_its_math_as_a_code_block():
`tools._page.inlined` is what rewrites a fold into it.
"""
folds = re.findall(r'<details>.*?</details>', home_math.README.read_text(), re.DOTALL)
assert len(folds) == 3, 'the README folds the whole document, then the same document in the other two notations'
assert len(folds) == 1, 'the README folds one block, the whole document with its symbol table'
hiding = [fold[:60] for fold in folds if '```math' in fold]
assert not hiding, f'a `math` fence inside a fold prints its TeX rather than its math: {hiding}'

Expand Down
25 changes: 7 additions & 18 deletions tools/home_math.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

``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
block, so ``README.md`` takes the equations with the whole document folded
under them, and ``docs/index.md`` takes tabs, plus a third tab holding the call
that produced the other two.
"""
Expand All @@ -19,7 +19,7 @@
import textwrap

from math_spec import to_spec
from math_spec.typesetting import to_latex, to_markdown, to_typst
from math_spec.typesetting import to_latex, to_markdown
from tools._page import ROOT, inlined, sidecar_for, splice, without_header
from tools._page import main as page_main

Expand Down Expand Up @@ -52,11 +52,11 @@
},
}

spec = ms.to_spec('dispatch.yaml') # read and checked once, then printed three ways
spec = ms.to_spec('dispatch.yaml')

ms.to_latex(spec, symbols=symbols) # amsmath align
ms.to_typst(spec) # compiles without a TeX toolchain
ms.to_markdown(spec) # renders as-is on GitHub
ms.to_latex(spec, symbols=symbols)
ms.to_typst(spec)
ms.to_markdown(spec)
```

`symbols` gives every name its conventional spelling. Pass a dict, a YAML path
Expand Down Expand Up @@ -107,10 +107,7 @@ def readme_block() -> str:
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.
The fold is what the legend and a table add.
"""
spec = to_spec(MODEL)
symbols = sidecar_for(MODEL)
Expand All @@ -121,14 +118,6 @@ def readme_block() -> str:
'The whole document: a symbol table, and the legend it prints',
inlined(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```',
),
)
)

Expand Down
Loading