Skip to content

feat: upright is what the model is given, italic is what the solver chooses - #44

Merged
FBumann merged 5 commits into
mainfrom
claude/notation-symbols
Aug 23, 2026
Merged

FBumann merged 5 commits into
mainfrom
claude/notation-symbols

Conversation

@FBumann

@FBumann FBumann commented Aug 23, 2026 •

Copy link
Copy Markdown
Contributor

Targets main, so #31 and the stack rebase onto it. A pass over the page as a whole — which is what notation.md exists to make possible — turned up one distinction the notation was not drawing at all, six places where it said something other than what it meant, and one about to start saying something a reader could not trace.

The distinction that was missing

$$\sum_{g\,:\,\mathrm{gen\_bus}(g)=b} p_{t,g} + \mathit{spill}_{t} - \mathit{slack}_{t} = \mathrm{load}_{t,b}$$

Three of those four are chosen by the solver and one is data. Before this, all four printed identically: ParameterNode and VariableNode reached the page through one call on one symbol dict, with nothing between them.

Upright is what the model is given — a parameter, a coordinate map, a label. Italic is what the solver chooses. The nomenclature table stays and is a real convention in this field, but it is a lookup rather than a reading: quote one equation on a slide and the legend does not travel with it.

It also completes a system the page was already three-quarters through — script for index sets, upright for the maps and qualifiers a model is handed, italic for quantities. The cut this adds is inside italic.

before after
parameter $\mathit{load}_{t,b}$ $\mathrm{load}_{t,b}$
variable $\mathit{spill}_{t}$ unchanged
parameter, single letter $p^{\mathrm{max}}$ $\mathrm{p}^{\mathrm{max}}$
parameter, Greek name — $\mathrm{eta}_{g}$
variable, Greek name — $\theta_{b}$

The rule admits no exception. Not for single letters: $\mathrm{p}^{\mathrm{max}}$ beside a variable $p$ is exactly the pair a reader has to be able to tell apart. And not for Greek, which was the interesting one:

upgreek is what LaTeX needs for an upright lower-case Greek letter, and taking it costs two things this repository holds on purpose. A two-package preamble — the CI render gate exists partly to assert it stays installable from a small TeX. And markdown that renders the same in both places: docs/static/mathjax.js says it outright, "a block pasted out of to_markdown should render the same here as it does on GitHub", and GitHub's MathJax takes no configuration, so an extension the docs site can load is one GitHub cannot — \uptheta would render there as an error.

So the letter yields to the rule rather than the rule to the letter. A Greek name prints as the letter where it is chosen and upright as the word where it is given; an italic $\eta$ that might be either is worse than an upright $\mathrm{eta}$ that is one. The escape hatch costs nothing and already exists: a table entry is printed verbatim, so an author whose own preamble loads upgreek writes eta: "\upeta". The fixture now shows both cases in one legend, $\mathrm{eta}$ beside $\theta$.

The legend states the convention once, in the model's own symbols, gated on the model having both — a note explaining a contrast the page does not draw is the dead end the translation notes already avoid. The index-collision guard shrank to exactly the collisions that are still collisions: a dimension may now take p beside a parameter p, because $\mathrm{p}$ and $p$ are not the same symbol.

The six fixes

before after
a name that is a Greek letter $\mathit{theta}_b$ $\theta_b$
an axis as a qualifier head $\mathit{zone}^{\mathrm{cap}}$ $\mathrm{zone_cap}$
negation under a + $-\mathit{reserve} + -\mathit{headroom}$ $-\mathrm{reserve} - \mathrm{headroom}$
a bracketed negation $\left(-\left(\sum p\right)\right)\cdot 3$ $-\left(\sum p\right)\cdot 3$
the fill and the group $\boxminus_{0,\mathrm{season_of}(t)}$ $\boxminus_{0}^{\mathrm{season_of}(t)}$
a mask that is only True $\forall t \in \mathcal{T} : \top$ $\forall t \in \mathcal{T}$
  • Greek is the same curated-map move as _INDEX_ALIASES, lower case only, and it composes with the qualifier rule so theta_max is a head a qualifier hangs off.
  • The qualifier head must name a quantity. zone_cap is a capacity indexed by zone, not a zone qualified by cap — and reading the axis as the head made a parameter's symbol depend on whether some unrelated dimension shared its prefix, which is why the fixture showed zone_cap splitting and tech_cap not.
  • a + -b is a spelling nobody uses; folding also hands the right operand subtraction's bracket rule, which is the one it needs.
  • The fill and the group shared one subscript because two subscripts is a Double subscript error that stopped the page compiling (#1165) — but comma-joined there, 0,season_of(t) said nothing about which was the value at the boundary and which was the group. Fill below, group above, with a legend note.
  • True is the same as no where per expressions.md, so the condition read as one and was not. Nested it still prints; ⊥ stays either way, because "this constraint has no rows" is worth seeing.

One inconsistency left deliberately: a group rides the superscript on a translation and the subscript on pos (#31). Each is unambiguous alone, but they are not the same slot. Making them agree means moving pos, which is a decision in flight on that PR rather than one to take from here.

The seventh: a table, not a rename

economies_of_scale_lam printed six times in one row is unreadable, and the piecewise-linear literature calls it λ. Renaming it inside the typesetter would have produced a symbol nobody could trace back to the file, so the rename is a declaration — the --symbols sidecar any reader may write, under the filename convention tools/render_tex.py already uses — and the notation page prints the table beside the math it renamed:

Rendered with the sidecar symbol table examples/symbols/transport_pwl.yaml, which is what the weights print as:

notation: latex
names:
  economies_of_scale_lam: "\lambda"
  economies_of_scale_seg: "\delta"
  bp_x: "\mathrm{x}"
  bp_y: "\mathrm{y}"

$\mathit{economies_of_scale_lam}{p,m,b} \le \mathit{economies_of_scale_seg}{p,m,b} + \ldots$ becomes $\lambda_{p,m,b} \le \delta_{p,m,b} + \delta_{p,m,b \boxminus_{0} 1}$, and none of it is the typesetter's opinion. The tables follow the convention they now live under: the weights are variables, so they stay Greek; bp_x/bp_y are data, so they print upright.

The fixture

Four cases it owed: a nested True beside the mask that is only one, a unary + — whose walk arm the line-coverage guard had never reached, because it sat inside a ternary until this change split it — and a Greek name on each side of the convention.

Checks

pixi.sh is unreachable from this environment, so the gates ran on a 3.13 venv at the versions pixi.toml pins, plus prettier@3.9.3 and typos fetched directly: 375 passed (including the Typst compiles and the walk's line-coverage guard), ruff check and format clean, pyrefly 0 errors, prettier clean over the files this touches, typos clean, reuse lint compliant. Goldens, notation.md and docs/examples/ all regenerated by their own tools; render_tex renders all nineteen models, the four new tables included. pixi.lock and schema/math-spec.schema.json fail a repo-wide prettier --check here exactly as they do on main — both generator-owned and outside the hook's *.{md,yml,yaml} glob.

…an a rename

A pass over the page as a whole — which is what `notation.md` exists to make
possible — turned up six places where the notation said something other than
what it meant.

- **A name that is a Greek letter prints as the letter.** A variable called
  `theta` set as the italic word *theta* is the one derived symbol no paper
  would accept. Same shape as `_INDEX_ALIASES`: a small curated map, lower
  case only, with `--symbols` for anything it does not know. It composes with
  the qualifier rule, so `theta_max` is now a head a qualifier hangs off.
- **A dimension is not a head a qualifier hangs off.** `zone_cap` is a
  capacity *indexed by* zone, not a zone qualified by cap — and reading the
  axis as the head made a parameter's symbol depend on whether some unrelated
  dimension happened to share its prefix. The head must name a quantity now.
  The cost is real and visible in the goldens: `zone_cap` and `bp_x` print as
  plain names where they used to split, which is what a symbol table is for.
- **A negation under a `+` folds into the operator.** The objective was
  printing `- reserve + -headroom`; nobody writes `a + -b`. Folding also hands
  the right operand subtraction's bracket rule, which is the one it needs.
- **A bracketed operand delimits its own negation**, so `-(Σ x) · 3` no longer
  wears a second pair of brackets around the whole of it.
- **The fill and the group take the operator's two slots** — fill below, group
  above. They shared one subscript because two subscripts is a TeX error
  (#1165), but comma-joined there, `0,season_of(t)` said nothing about which
  was the value standing at the boundary and which was the group. A legend
  note now states the rule, gated like the others on the symbol appearing.
- **A mask that is only `True` prints no condition.** `expressions.md` says
  `True` is the same as no `where`, so `∀ t ∈ T : ⊤` was a condition that
  reads as one and is not. Nested it still prints: `⊤ ∧ x` is what the file
  says, and simplifying a mask belongs to resolution.

The seventh was the curve weights: `economies_of_scale_lam` printed six times
in one row is unreadable, and papers call it λ. Renaming it in the typesetter
would have been a symbol nobody could trace back to the file, so the rename is
a **declaration** — the `--symbols` sidecar any reader may write, under the
filename convention `tools/render_tex.py` already uses — and the notation page
prints the table beside the math it renamed. `bp_x`/`bp_y` ride along as x and
y, which is also what those models' equations want.

The fixture gains the two cases these need: a nested `True` beside the mask
that is only one, and a unary `+`, whose walk arm the line-coverage guard had
never reached — it was hidden inside a ternary before this change split it.
…hooses

Which of a linear model's symbols the solver picks is the distinction a reader
cannot afford to guess at, and the page was leaving it to the legend. In

    Σ_{g : gen_bus(g) = b} p_{t,g} + spill_t - slack_t = load_{t,b}

three of those four are chosen and one is data, and all four printed the same.
`ParameterNode` and `VariableNode` reached the page through one call on one
symbol dict; nothing between them.

The nomenclature table is a real convention and stays — but it is a *lookup*
rather than a reading, and an equation quoted on a slide does not take the
legend with it. So the symbols carry it. This also completes a system the page
was already three-quarters through: script for index sets, upright for the
maps, qualifiers and labels a model is handed, italic for quantities. The cut
this adds is *inside* italic.

    load_{t,b}  →  \mathrm{load}_{t,b}      given
    spill_t     →  \mathit{spill}_{t}       chosen, unchanged
    p_max       →  \mathrm{p}^{\mathrm{max}}

No exception for single letters: `\mathrm{p}^{\mathrm{max}}` beside a variable
`p` is exactly the pair a reader has to be able to tell apart, and carving it
out would have left the rule with one. The legend states the convention once,
in the model's own symbols, gated on the model having both — a note explaining
a contrast the page does not draw is the dead end the translation notes avoid.

Two things the distinction does not reach, both deliberate:

- **Greek.** LaTeX sets lower-case Greek italic and has no upright form in the
  two-package preamble the render gate holds itself to, so a Greek-named
  parameter prints as the letter and the legend is what places it.
- **The index-collision guard**, which shrank to exactly the collisions that
  are still collisions: a dimension may take `p` beside a parameter `p`,
  because `\mathrm{p}` and `p` are not the same symbol on the page.

The sidecar tables follow the rule they now live under: `bp_x`/`bp_y` are data,
so they print upright, while the curve weights they sit beside are variables
and stay Greek — `λ_{p,m,b} ≤ δ_{p,m,b} + δ_{p,m,b ⊟₀ 1}` is one row that used
to name a 25-character word three times.
@FBumann FBumann changed the title feat: six notation fixes, and the curve weights get a table rather than a rename feat: upright is what the model is given, italic is what the solver chooses Aug 23, 2026
claude added 3 commits August 23, 2026 12:08
`upgreek` is what LaTeX needs for an upright lower-case Greek letter, and
taking it costs two things this repository holds on purpose:

- **A two-package preamble.** `latex.py` loads amsmath and amssymb, and the CI
  render gate exists partly to assert the preamble stays installable from a
  small TeX.
- **Markdown that renders the same in both places.** `docs/static/mathjax.js`
  says it outright — "a block pasted out of `to_markdown` should render the
  same here as it does on GitHub" — and GitHub's MathJax takes no
  configuration, so an extension the docs site can load is one GitHub cannot.
  `\uptheta` would render there as an error.

So the letter yields to the rule rather than the rule to the letter: a Greek
name prints as the letter where it is **chosen**, and upright as the word where
it is **given**. An italic `\eta` that might be either is worse than an upright
`\mathrm{eta}` that is one — the whole point of the distinction being that a
reader can tell without the legend.

The escape hatch costs nothing and is already there: a symbol table entry is
printed verbatim, so an author whose own preamble loads `upgreek` writes
`eta: "\\upeta"` and gets it.

The fixture gains the case, which the page now shows in one legend beside the
other: `eta` given prints $\mathrm{eta}$, `theta` chosen prints $\theta$.
Commenting `examples/symbols/dispatch.yaml` — a table whose three parameter
entries are italic, which the convention says they should not be — turned up
two things worth more than the comment.

**The homepage was stale, and nothing was holding it.** `docs/index.md` and
`README.md` carry `examples/dispatch.yaml` rendered two ways, written by
`tools/home_math.py`, and no test or workflow ran its `--check`. The upright
convention changed how a parameter prints and the page kept the old math with
every test green — the same failure `tools/notation.py` had until #41, on the
page a reader arrives at first. It has the same guard now, and the guard was
checked against a perturbed page rather than assumed.

**The note contradicted itself there.** It quotes the model's own first
parameter, and on that page the first parameter is `\bar p` — italic, because
the table says so — under a sentence claiming a parameter is upright. So the
note now quotes only symbols the *derivation* produced, and is suppressed
where a table has taken over the ones it would have quoted. A table is printed
verbatim and is the author's to write; a symbol it supplies is not one the
note governs.

Which is what the comment says, in the file that earns it: entries are the
reader's notation to choose, and the same property is what lets an author
whose preamble loads `upgreek` write `\upeta` for a parameter the derivation
has to spell out.
Asking whether anything would catch the next divergence turned up a third
stale page. `docs/reference/language/operators.md` carries the operator table
`tools/spec_math.py` writes, its `--check` was called by no test and no
workflow, and the upright convention had left sixteen of its rows behind.

It is in the same trap the other two were: the generator writes unpadded
tables, prettier pads them, and the committed block has been prettier's shape
and never the generator's since the day it was written — which is why no
guard could exist. So it joins `.prettierignore` under the rule
`docs/examples/` set, and the entry was checked to be load-bearing rather than
decorative.

`tools/home_math.py` is the other way out of the same conflict and needs no
entry: it emits the blank lines around its markers that prettier wants, so
`--check` and the formatter already agree. Padding a table is a harder shape
for a generator to match than a blank line, which is why three pages take the
generator-wins route and one does not — now said out loud in `.prettierignore`
rather than rediscovered per page.

The guards are a table now, one row per committed page, and `test_every_
generator_is_asked` holds that table to the tools: a `tools/*.py` that can
detect drift and has no row fails the suite. That is the part that catches the
*next* one — three pages have now gone stale with everything green, each time
because the tool knew and nothing asked it. Verified by making a tool look
like a generator and watching the test fail.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants