Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
3ddc56b
docs: a glossary defines each word the docs use in a fixed sense
claude Sep 24, 2026
bc82807
fix(language): an unknown operator's refusal points at the limits pag…
claude Sep 24, 2026
ec419dd
chore(skills): the docs-writing skill no longer lists escape as house…
claude Sep 24, 2026
19f710f
docs: a development section holds the PyPSA parity pages and the cont…
claude Sep 24, 2026
f3ba79d
docs: what spec.expand() returns is stated once, as a different model…
claude Sep 24, 2026
9ab6442
docs: one Python API page documents every public name, including the …
claude Sep 24, 2026
b8bed31
docs: a first tutorial writes the dispatch model one block at a time,…
claude Sep 24, 2026
e2c3d63
docs: the notation page heads each section with the construct it shows
claude Sep 24, 2026
b33f8ab
Merge remote-tracking branch 'origin/claude/docs-review-glossary-fbhe…
claude Sep 24, 2026
7f80c61
Merge branch 'claude/fix-sos-key-and-escape-message' into claude/docs…
claude Sep 24, 2026
7d7d005
Merge branch 'claude/docs-expand-one-home' into claude/docs-developme…
claude Sep 24, 2026
3d51307
Merge branch 'claude/docs-first-tutorial' into claude/docs-notation-b…
claude Sep 24, 2026
7cc63e2
Merge branch 'claude/docs-development-section' into claude/docs-publi…
claude Sep 24, 2026
3cd4183
Merge branch 'claude/docs-public-api-page' into claude/docs-first-tut…
claude Sep 24, 2026
459da21
Merge branch 'claude/docs-first-tutorial' into claude/docs-notation-b…
claude Sep 24, 2026
ec2fd06
docs: the nav splits into a tab for writing models, one for building …
claude Sep 24, 2026
efddd34
docs: the glossary and the tutorial are cut to what no other page say…
claude Sep 24, 2026
7849d13
docs: the readme, the explanation pages, reading.md, typeset and the …
claude Sep 24, 2026
51d7f2a
docs: the language reference loses rationale and repeated rules (in p…
claude Sep 24, 2026
2b824af
docs: the operator table no longer points readers at the notation pag…
claude Sep 24, 2026
7ef6707
docs: the nav puts tutorials, how-to guides, reference and about at t…
claude Sep 24, 2026
dff126c
Merge branch 'claude/docs-split-by-reader' into claude/docs-cut
claude Sep 24, 2026
c1a697a
fix(language): messages and docs say data is attached rather than bou…
claude Sep 25, 2026
9ddd944
docs: the python api page sits in reference beside typeset, and the t…
claude Sep 25, 2026
702def1
Merge branch 'claude/docs-split-by-reader' into claude/docs-cut
claude Sep 25, 2026
f233fbd
Merge branch 'claude/docs-cut' into claude/attach-not-bind
claude Sep 25, 2026
853b9b7
docs: what spec.expand() returns is documented on the model writer's …
claude Sep 25, 2026
7b89343
Merge branch 'claude/docs-cut' into claude/attach-not-bind
claude Sep 25, 2026
e0ef611
Merge remote-tracking branch 'origin/main' into claude/docs-review-gl…
claude Sep 25, 2026
8ba787a
Merge branch 'claude/docs-review-glossary-fbheb3' into claude/fix-sos…
claude Sep 25, 2026
02706c4
Merge branch 'claude/fix-sos-key-and-escape-message' into claude/docs…
claude Sep 25, 2026
00ce721
Merge branch 'claude/docs-expand-one-home' into claude/docs-developme…
claude Sep 25, 2026
ca4975c
Merge branch 'claude/docs-development-section' into claude/docs-publi…
claude Sep 25, 2026
f449066
Merge branch 'claude/docs-public-api-page' into claude/docs-first-tut…
claude Sep 25, 2026
7a7f1b9
Merge branch 'claude/docs-first-tutorial' into claude/docs-notation-b…
claude Sep 25, 2026
bcb56c5
Merge branch 'claude/docs-notation-by-construct' into claude/docs-spl…
claude Sep 25, 2026
b0fd9ac
Merge main into claude/docs-cut
claude Sep 25, 2026
ffcd68e
Merge branch 'claude/docs-cut' into claude/attach-not-bind
claude Sep 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 37 additions & 22 deletions .claude/skills/docs-writing/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,31 @@ Two questions decide it, and they work on a paragraph as well as a page:
1. Does it inform **action** or **cognition**?
2. Does it serve **acquiring** a skill or **applying** one?

| Kind | Informs | Serves | Answers | Nav section · folder |
| Kind | Informs | Serves | Answers | Section · folder |
| ----------- | --------- | ------- | ----------------------------------------------------- | -------------------------------------------------------------- |
| Tutorial | action | acquire | "Get me a first file that loads and prints" | Tutorials · `docs/` |
| How-to | action | apply | "I have this task" | How-to guides · `docs/howto/` |
| Reference | cognition | apply | "What exactly does X accept, and what does it print?" | Reference · `docs/reference/`, model pages in `docs/examples/` |
| Explanation | cognition | acquire | "Why is it like this?" | About · `docs/about/` |

The nav and the tree are both arranged by kind. A new page goes in the folder
of its kind and under the nav section of the same name; the first tutorial
opens the `Tutorials:` section, above the how-to guides. The model pages sit at
the end of the Reference section, after the pages a reader looks things up in.
A worked example is neither a tutorial nor a how-to: it teaches no path and
names no task, it shows that the language says a model.
The nav and the tree are both arranged by kind, for someone who writes a
model. A new page goes in the folder of its kind and under the nav section of
the same name. The model pages sit at the end of the Reference section, after
the pages a reader looks things up in. A worked example is neither a tutorial
nor a how-to: it teaches no path and names no task, it shows that the language
says a model.

The Development section, last in the nav, holds every page a model writer does
not need, in three groups:

- **Building on math-spec** is for someone who writes a tool against `Spec`
and `Program`: an engine such as specsolve, a renderer, a checker.
- **Contributing** is for someone who changes math-spec itself.
- **Proofs of concept** holds the notation page, which renders the typesetting
test model, and the PyPSA pages. The PyPSA pages stay in `docs/examples/`,
where `tools/gallery.py` writes them.

A page in Development keeps the folder of its kind.

Each kind has one job, and one thing it must not do:

Expand All @@ -80,16 +92,19 @@ math the typesetter prints from it. The block is written by `tools/gallery.py`
between `<!-- gallery:begin -->` and `<!-- gallery:end -->`; the paragraph is
the only prose on the page, and it says what the model is and the one or two
things worth reading for, which the `description:` line in the file does not.
The PyPSA pages add a generated block per rung, holding the reference script
and what PyPSA solved it to. Every model is a file under `examples/`, loaded
by the suite and compiled by the LaTeX gate, and `tests/test_docs.py` holds
each block to its generator byte for byte. The catalogue in
`docs/examples/index.md` is hand-written: one bullet per page, saying why a
reader would open it.

**The Python API pages are built, not written.** mkdocs renders
`reference/math_spec/` from the docstrings at build time, so their prose is
the docstring rules in `AGENTS.md`.
The PyPSA pages, in the Development section, add a generated block per rung,
holding the reference script and what PyPSA solved it to. Every model is a
file under `examples/`, loaded by the suite and compiled by the LaTeX gate,
and `tests/test_docs.py` holds each block to its generator byte for byte. The catalogue in
`docs/examples/index.md` is hand-written: one bullet per page in the Examples
section, saying why a reader would open it.

**The Python API is rendered from the docstrings.**
`docs/reference/api.md` holds one `:::` entry per name in `math_spec.__all__`,
and mkdocstrings renders each from its docstring. `docs/static/hooks.py`
renders one page per module under `src/math_spec/`, and puts them in the
Contributing group of the Development section as `Modules`. The prose of both is the docstring rules in
`AGENTS.md`.

Mixing kinds is the most common failure. Rationale inside a reference section
makes the rules unskimmable, and rules inside an explanation page make the
Expand All @@ -98,11 +113,11 @@ survives into an explanation page is the part a user needs to make decisions.

The language is documented here and only here. A page says what a file may
contain, what it means, what the loader refuses, and what the typesetter
prints from it. What a consumer does with a spec — the data it binds, how it
prints from it. What a consumer does with a spec — the data it attaches, how it
solves, what it reads back — is that consumer's page, not this tree's
([what counts as language](../../../docs/about/what-counts-as-language.md)).
A rule about a consumer says only what the file guarantees it
([reading a loaded model](../../../docs/reference/language/reading.md)).
([reading a loaded model](../../../docs/reference/reading.md)).

Answer the two questions before starting. If a page needs two kinds, it is
two sections with two headings, or two pages.
Expand Down Expand Up @@ -179,7 +194,7 @@ not

- **Gloss house vocabulary at first use** — _spec_, _program_, _declaration_,
_dimension_, _coordinate_, _frame_, _relation_, _absence_, _macro_, _named
expression_, _reported expression_, _escape_. One clause with a concrete
expression_, _reported expression_. One clause with a concrete
instance: "one point of it, one generator in one snapshot, is a coordinate".
- **Gloss every acronym and domain term at first use**, in parentheses, six
words or fewer.
Expand All @@ -204,7 +219,7 @@ The bar, and it is checkable:
1. **One idea per sentence.** Median at or under 20 words; over 25 is where a
newcomer re-reads.
2. **Active voice, with a real subject.** "The loader refuses it before any
data binds", not "the refusal comes before any data binds". An abstract
data is attached", not "the refusal comes before any data is attached". An abstract
noun as subject is the single biggest reason technical prose reads
expert-only.
3. **State the rule in things, then in abstractions.** "One generator at one
Expand Down Expand Up @@ -307,7 +322,7 @@ PY
- **Anything that duplicates another page.** One fact, one home; link instead.
A second copy drifts silently. The README is pulled into `docs/index.md` as
snippets, so a sentence that appears on both is edited once, in the README.
- **A rule of an engine.** How a spec is bound to data, solved, or read back
- **A rule of an engine.** How data is attached to a spec, how it is solved, or read back
is a consumer's page. Here a consumer is named only for what the file
guarantees it.
- **Generated content.** The model and its math on every example page and the
Expand Down
6 changes: 4 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,10 @@ When opening a pull request, please provide a clear summary of your changes!
decides where it goes, in the nav and in the tree**: a tutorial (`docs/`), a
how-to guide (`docs/howto/`), reference (`docs/reference/`, and the model pages
in `docs/examples/`) or explanation (`docs/about/`) — the four kinds of
[Diátaxis](https://diataxis.fr) — and one page is one kind. The rules each kind
has to meet, and the sentence-level bar, are in
[Diátaxis](https://diataxis.fr) — and one page is one kind. A page a model
writer does not need goes under Development in the nav: building on
math-spec, contributing, or a proof of concept. The rules each kind has to meet, and the sentence-level
bar, are in
[the docs-writing skill](https://github.com/energy-models/math-spec/blob/main/.claude/skills/docs-writing/SKILL.md).
Every page needs a `nav:` entry in `mkdocs.yml`, links inside `docs/` are
relative, and a link outside it is the full GitHub URL; `pixi run docs-build`
Expand Down
137 changes: 24 additions & 113 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,8 @@ 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; that
is the [test](docs/about/what-counts-as-language.md) for what belongs here.

Three properties follow:
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)).

- **Nothing is guessed.** A misspelled name, a `where` string on an undeclared
parameter, a constraint whose dimensions do not match its `dims`: each fails
Expand All @@ -42,7 +40,8 @@ Three properties follow:
composition of them goes in `macros:` ([the limits](docs/about/limits.md)).
- **The file is the document.** `to_latex(spec)` prints the model as equations
from the file alone, so the math you publish is the math you solve
([typeset](docs/reference/typeset.md)).
([typeset](docs/reference/typeset.md)). The file diffs in review, and no
Python state changes what it means.

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

Expand Down Expand Up @@ -102,13 +101,10 @@ objective:

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

That file is a complete model. Nothing outside it changes what it means.

### 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
is one of three formats, so GitHub renders it here.
The typesetter prints the file above as math, with no data and no solver.
Markdown is one of three formats, and GitHub renders it here.

<!-- Prettier pads the legend tables that the generator emits unpadded, so the
two would rewrite each other forever. The range keeps this file formatted
Expand Down Expand Up @@ -261,7 +257,7 @@ $ upright("dispatch") & 0 <= italic("dispatch")_(t,g) & <= upright("capacity")_(
<!-- readme-math:end -->
<!-- prettier-ignore-end -->

Each format is one call, and the file is read and checked once:
Each format is one call:

```python
import math_spec as ms
Expand All @@ -273,107 +269,27 @@ ms.to_latex(spec) # amsmath align
ms.to_typst(spec) # compiles without a TeX toolchain
```

Those symbols are the file's own names: `load` prints as $`\mathrm{load}_t`$,
and `capacity` as $`\mathrm{capacity}_g`$. Nothing had to be set up for
that. Pass `symbols='dispatch.symbols.yaml'` and the typesetter prints
$`\ell_t`$ and $`\bar p_g`$ instead, above a legend that defines them. The
first folded block shows it. The table can be a dict, a `SymbolTable`, or a
path to YAML. A key that names nothing in the model is an error, and nothing
in a table changes what the file means.

Or from a shell, beside `pdflatex` in a Makefile:

```bash
python -m math_spec latex dispatch.yaml --symbols dispatch.symbols.yaml --standalone -o dispatch.tex
python -m math_spec typst dispatch.yaml --standalone -o dispatch.typ
python -m math_spec markdown dispatch.yaml
```

### `Spec` and `Program`

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

Whatever is wrong with a model is wrong when it loads, not when it solves:

```python
import math_spec as ms

spec = ms.to_spec('dispatch.yaml') # schema, names, dimensions, degree: all checked here
sorted(spec.variables) # ['dispatch']
A [symbol table](docs/reference/typeset.md#symbol-tables) gives the names their
conventional spelling, as in the first 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` the model it builds
([reading a loaded model](docs/reference/reading.md#spec-and-program)).

program = spec.expand().program # curves expanded, names typed, operators resolved to nodes
sorted(program.constraints) # ['power_balance']
```
## Documentation

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
kept as the block it is. `spec.expand()` turns each curve into its variables and
constraints; an engine that builds rows reads that model's `Program`.

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

[Reading a loaded model](docs/reference/reading.md) says what a tool gets
from each, and [the file and the program](docs/about/file-and-program.md) says
why there are two.

## Why

- **Declarative math.** A file is readable without knowing any implementation,
and no Python state changes what it means. It diffs in review and travels as a
research artefact.
- **Fail early, fail loud.** Nothing falls back silently, and an error names the
problem and its rewrite. A model that does not load does not print either.
- **One flat namespace, ten rules.** A collision is a load error naming both
declarations, position decides which kinds of name are legal, and a name's kind
is fixed at load. The [ten rules](docs/reference/language/index.md) are one
principle in ten positions.
- **A closed operator set.** `sum`, `sum_back`, `at` and `shift`, with the
arithmetic and `where` grammars. A composition of them goes in `macros:`, so
every engine expands it the same way.
- **A finite language.** An operator joins the language only if each output row
reads a bounded number of input rows, and a file cannot add one. Math the
language cannot express is refused, with the rewrite named.

## Docs

Start with [the language](https://math-spec.readthedocs.io/latest/reference/language/):
the ten rules, and the pages that give the exact ones. Then
[every construct as math](https://math-spec.readthedocs.io/latest/reference/notation/),
which prints all of it beside the notation the typesetter gives it, and
[typeset the math](https://math-spec.readthedocs.io/latest/reference/typeset/)
for how to print your own. Why the language is shaped this way, what may enter
it, and who owns a rule once it is in are under
[about](https://math-spec.readthedocs.io/latest/about/limits/). To work on it,
read [CONTRIBUTING.md](CONTRIBUTING.md).
The documentation is at <https://math-spec.readthedocs.io>.

## Installation

This project is managed by [pixi](https://pixi.prefix.dev/). To develop against
it:

<!--- --8<-- [start:docs-install-dev] -->

```bash
git clone https://github.com/energy-models/math-spec
cd math-spec

pixi run pre-commit-install
pixi run test
```

<!--- --8<-- [end:docs-install-dev] -->

Releases are on the alpha stream, and **nothing is published yet**. The publish
job is off until the project leaves it, so `pip install math-spec` is what the
first release will look like, not what today does. Install from a checkout or a
git reference until then; see [RELEASING.md](RELEASING.md).
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.

## Prior art

Every file under `src/` was written in [specsolve](https://github.com/fluxopt/specsolve)
and extracted here, so that the language and the syntax tree a tool reads it
through are a dependency rather than one engine's internals. The keys themselves,
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
Expand All @@ -386,17 +302,12 @@ Alpha, pre-1.0.

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

**Breaking changes land without a deprecation cycle.** When a construct is named
wrong, a default is wrong, or a permissive input hides a silent wrong answer, it
is fixed rather than aliased. A compatibility shim for every earlier spelling
would defeat the point of a small language.

Pin an exact version if you depend on this, and read the
**Breaking changes land without a deprecation cycle.** Pin an exact version if
you depend on this, and read the
[changelog](https://github.com/energy-models/math-spec/blob/main/CHANGELOG.md)
before upgrading. What exists is tested: every construct the language has
round-trips through the schema, the parsers and all three typeset formats, and
the LaTeX is compiled rather than eyeballed. It is the accepted YAML that is not
yet frozen, not the behaviour.
before upgrading. Every construct round-trips through the schema, the parsers
and all three typeset formats, and the LaTeX is compiled. The accepted YAML is
not yet frozen.

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

Expand Down
Loading
Loading