Skip to content

feat: position(dim) replaces index(dim, i), converting on the left - #31

Merged
FBumann merged 3 commits into
mainfrom
claude/new-session-divgg4
Aug 23, 2026
Merged

FBumann merged 3 commits into
mainfrom
claude/new-session-divgg4

Conversation

@FBumann

@FBumann FBumann commented Aug 21, 2026 •

Copy link
Copy Markdown
Contributor

Closes #32. The partition/cases: prototype that was on this branch has moved to #33, stacked on top of this. #40 has been folded back in, so this PR is the language fix and the notation that prints it.

What this changes

snapshot > index(snapshot, 0) had two readings and the language picked neither, so a file loaded and the model it meant was undetermined (#32). As "the coordinate at position 0" it compared values and depended on the axis arriving sorted; as "position 0" it compared positions. == and != agreed, which is why it had not bitten.

Renaming the operator would have fixed the label and not the shape: the token on the left changed what it denoted depending on the right-hand side — snapshot > 5 the coordinate's value, snapshot > index(snapshot, 0) its position. So the conversion moves to the left, where what is being converted is visible:

where: "position(snapshot) == 0" # first
where: "position(snapshot) > 0" # every other one
where: "position(snapshot) == -1" # last
where: "position(snapshot, by=period_of) == 0" # first of each period

Both sides are integers now, so every comparator reads one way and the wrong reading is unsayable rather than documented against — the same move objective: makes by holding one block.

The grammar shrinks: POSITION leaves the value production, and resolution loses the same-dimension check that only existed because both sides named a dimension. A value comparison is still written against the dimension itself, snapshot > '2030-01-01', where it always was.

The page, not just the file

The ambiguity reached the rendered math too, so a reader of the paper could not recover the model either — and half-fixing it there left pos(t) = 0 beside t \ge 3, both an index-ish thing against a small integer, with nothing saying which way the unmarked one goes. The reader's own convention says the opposite of this language's: papers write their sets as {1, …, T}, where the index is the ordinal and nothing needs marking.

The mark stays on the position. A coordinate is what a dimension denotes everywhere else in the language, and a position is derived from whichever index resolution produced, so the derived thing is what gets converted in view. What is added is the legend that makes the unmarked side readable, plus two corrections to what a position prints.

pos is introduced where it is used, gated the way the translation notes are — one note for the symbol, one for the grouped form, one for the size, each only where that form printed:

$\mathrm{pos}(t)$ denotes where index $t$ sits along its dimension's own order — the order shift walks, not the order labels sort in — counted from $0$. The index itself stays the coordinate, so $t$ compares against labels and $\mathrm{pos}(t)$ against positions.

A group rides a subscript, $\mathrm{pos}_{\mathrm{season_of}(t)}(t)$. As a second argument it sat exactly where the first one's integer sits and said nothing about "within" — the shape an edge fill already has, riding its translation as a subscript.

A position counted from the end prints against the size: $\mathrm{pos}(t) = \lvert\mathcal{T}\rvert - 1$, and $\lvert\mathcal{T}_{\mathrm{season_of}(t)}\rvert - 1$ where the count is grouped. The order runs 0 to |T| - 1, so -1 printed as itself was an equation asserting a position the legend had just ruled out — Python's index sugar, read as math.

A dimension compared against a number names its coordinates in the set legend: index t — snapshot (int coordinates). That is the one comparison a reader can take for a position; every other coordinate prints as prose and could not be.

Positions stay 0-based on the page, as in the file, so a clause reads off one and writes into the other.

Why not the alternatives

  • Marking both sides (coord(t) >= 3 beside pos(t) = 0) taxes the common case, and contradicts the reason the conversion moved left: only one side should ever be marked.
  • A typographic distinction (font, accent, \hat t for ordinals) is invisible at body-text size, does not survive plain text, and all three formats would have to agree on it.
  • Inverting the page — letting t range over {1..|T|} per the paper convention and printing values as \tau_t — is the only coherent inversion, and nearly free everywhere except a value comparison. It costs the symbol correspondence between page and file, which is what this PR is protecting.
  • Order relations (t \succ t_1, with the set enumerated in the legend) are more conventional than pos(t) > 1, but they are a second surface for one construct. The order relation now appears only in the legend line, where it says what pos counts along.

Why not extend it further

Asked and answered while writing this, so it is on the record rather than rediscovered later. position() takes a dimension and nothing else:

  • a parameter — redundant for "position along a dim it lives on" (position(gen) already says that, and the dim must be in the frame anyway), and undefined the moment a parameter has two dims. For "rank by its value" it is a window over unbounded rows — ceiling.md's reject row — and precomputable in data prep regardless, as an int parameter compared normally: where: "cost_rank == 0".
  • a lookup — position(dim, by=lookup) already covers positions within its groups, the only positional thing a lookup has.
  • a variable — ranking by a variable's value needs the solver's answer to decide which row is which, so it is outside the plan, not merely outside the ceiling.

Keeping it in coordinate space is what makes it free: it reads the small dim table, never the data.

Breaking

Every model using the old spelling now fails to parse. "Expected end of text, found (" is the wrong message for the reader most likely to hit it, so the failure names the rewrite:

index() is now position(), and converts on the left: write 'position(dim) == i' for 'dim == index(dim, i)', and 'position(dim, by=lookup) == i' for the grouped form.

A test pins that unrelated parse failures stay silent about positions.

Described rather than marked with !, which would move the base version off the alpha stream.

Scope

Grammar, resolution, typeset, the two masks piecewise.py generates, four docs pages, the golden model and its three regenerated outputs. New tests cover the grammar surface (both frames, every comparator, the grouped form), the resolution error paths (a parameter, an undeclared name, a by= that is not a lookup, a by= over another dimension), and the rewrite hint.

The notation half adds |·| as a Format method rather than an OPERATOR_NAMES entry — that vocabulary is infix spellings, and the Typst gate compiles every one of them between two operands — spelled by all three formats. The golden model gains last, the from-the-end pair it had no case for, which is what holds the new arms of the walk to output someone has read under the fixture's three censuses.

The notation page, now that #41 has landed

Writing this turned up that tools/notation.py could not run in this repository at all — its PIECEWISE map named four models left behind in fluxopt/lpspec at the extraction (#17) — and that nothing ran --check, so the page had been drifting from the models it claims to be generated from. That is fixed on main by #41, which restored the four models, put the page in .prettierignore under the rule docs/examples/ already follows, and added the guards.

Two consequences here, both good:

  • The page in this PR is regenerated, not hand-edited — and the new test_the_notation_page_is_current proves it, which it did not when this branch first carried a hand-spliced page.
  • The curve section is live: the LP row moved from b \neq \mathrm{index}(\mathcal{B}, 0) to \mathrm{pos}(b) \neq 0 on its own, because the tool now renders that model rather than a person copying the row across.

Checks

pixi.sh is unreachable from the environment this was written in, so the gates were reproduced with a 3.13 venv on the versions pixi.toml pins, plus prettier@3.9.3 and typos fetched directly: ruff format and check clean, pyrefly 0 errors, reuse lint compliant, typos clean, prettier clean, 381 passed — the three Typst compile tests included, which cover abs(cal(T)) and the subscripted pos and skip without the bindings. Goldens regenerated with python -m tests.typeset.golden, the page with python -m tools.notation. CI is green on this head.

@read-the-docs-community

read-the-docs-community Bot commented Aug 21, 2026 •

Copy link
Copy Markdown

@FBumann
FBumann force-pushed the claude/new-session-divgg4 branch 2 times, most recently from 363f9c5 to 3864ba8 Compare August 22, 2026 09:04
@FBumann FBumann changed the title feat: a static partition check for constraint cases feat: position(dim) replaces index(dim, i), converting on the left Aug 22, 2026
@FBumann
FBumann force-pushed the claude/new-session-divgg4 branch from 3864ba8 to c2d612f Compare August 22, 2026 10:12
@FBumann
FBumann force-pushed the claude/new-session-divgg4 branch from afb0e6a to c103c48 Compare August 22, 2026 21:13
@FBumann
FBumann changed the base branch from main to claude/examples-gallery August 22, 2026 21:13
@FBumann
FBumann force-pushed the claude/new-session-divgg4 branch from c103c48 to 937b2ee Compare August 23, 2026 06:19
@FBumann
FBumann changed the base branch from claude/examples-gallery to main August 23, 2026 06:19
@FBumann
FBumann force-pushed the claude/new-session-divgg4 branch 2 times, most recently from 3afbcea to d414e87 Compare August 23, 2026 08:26
@FBumann
FBumann force-pushed the claude/new-session-divgg4 branch from d414e87 to c52b9a7 Compare August 23, 2026 12:34
claude added 3 commits August 23, 2026 12:41
`snapshot > index(snapshot, 0)` had two readings and the language picked
neither, so the file loaded and the model it meant was undetermined
(#32). As "the coordinate at position 0" it compared values and depended
on the axis arriving sorted; as "position 0" it compared positions. `==`
and `!=` agreed, which is why it had not bitten.

Renaming it would have fixed the label and not the shape: the token on
the left changed what it denoted depending on the right-hand side —
`snapshot > 5` the coordinate's value, `snapshot > index(snapshot, 0)`
its position. So the conversion moves to the left instead, where what is
being converted is visible:

    where: "position(snapshot) == 0"                 # first
    where: "position(snapshot) > 0"                  # every other one
    where: "position(snapshot) == -1"                # last
    where: "position(snapshot, by=period_of) == 0"   # first of each period

Both sides are now integers, so every comparator reads one way and the
wrong reading is unsayable rather than documented against — the same
move `objective:` makes by holding one block. The grammar shrinks:
POSITION leaves the `value` production, and resolution loses the
same-dimension check that only existed because both sides named a
dimension. A value comparison is still written against the dimension
itself, `snapshot > '2030-01-01'`, where it always was.

The typeset follows: `pos(t) = 0` rather than `t = index(T, 0)`, an
application to the row, since that is what it converts. The ambiguity
reached the rendered math too, so a reader of the paper could not
recover the model either.

Every model that used the old spelling now fails to parse, and
"Expected end of text, found '('" is the wrong message for the most
likely reader, so the failure names the rewrite.

This is a breaking spelling change, described here rather than marked: a
'!' moves the base version rather than the alpha counter, which the
project refuses while the stream is pinned (.github/workflows/pr-title.yml).

Closes #32.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADtfZf4V6W9XcLRSwSgHzE
`OPERATOR_NAMES` is the vocabulary a walk may emit and `format.py` states
the contract — "A format spells; it never decides" — so an operator name
written into the walk escapes all three guards built around that set:
that every format spells exactly it, that every Typst spelling compiles,
and that the golden model asks for every one. `index` had the same
problem, but this commit is the notation change, so it is the moment to
move it.

The spelling is unchanged, so no golden moves. What changes is that a
format can now disagree about it, and is held to having an opinion.

Also fixes what that exposed: `docs/reference/notation.md` is generated
between markers from the golden model, and the earlier edit rewrote the
YAML snippets by hand without the rendered math beneath them, leaving
four lines reading `index(𝒯, 0)` under a snippet saying `position(...)`.
`tools/notation.py` cannot run — it reads two example files that are not
in the repo, on main as well — so the four lines are corrected from
output rendered through the same code path rather than by regenerating.

Two cleanups alongside: the position parse action reads its tokens from
one end rather than three, and the migration hint is an `if` above its
use site rather than a helper below it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADtfZf4V6W9XcLRSwSgHzE
`position(dim)` prints `pos(t)`, and a coordinate comparison prints `t >= 3`.
Both are an index-ish thing against a small integer, and nothing on the page
said which way the unmarked one goes — while the reader's own convention says
the opposite of this language's: papers write their sets as {1, …, T}, where
the index *is* the ordinal and nothing needs marking. So #32's ambiguity
survived into the artifact a reader recovers the model from.

The mark stays on the position, since a coordinate is what a dimension denotes
everywhere else in the language and a position is derived from whichever index
resolution produced. What is added is the legend that makes the unmarked side
readable, and two corrections to what a position prints:

- `pos` is introduced where it is used, gated the way the translation notes
  are: one note for the symbol, one for the grouped form, one for the size —
  each only where that form printed.
- A group rides a **subscript**, `pos_{season_of(t)}(t)`. As a second
  argument it sat where the first one's integer sits, saying nothing about
  "within".
- A position counted from the end prints against the size, `|T| - 1` rather
  than `-1`: the order runs `0` to `|T| - 1`, so `-1` was an equation
  asserting a position the legend had just ruled out. Grouped, it counts back
  from the group's size, `|T_{season_of(t)}| - 1`.
- A dimension compared against a *number* names its coordinates in the set
  legend (`snapshot` (`int` coordinates)) — the one comparison that can be
  read as a position, since every other coordinate prints as prose.

Positions stay 0-based on the page, as in the file, so a clause reads off one
and writes into the other.

`|·|` is a `Format` method rather than an operator: `OPERATOR_NAMES` is a
vocabulary of infix spellings and the Typst gate compiles every one of them
between two operands.

The golden model gains `last`, the from-the-end pair the fixture had no case
for, which is what holds the new arms of the walk to output someone has read.
@FBumann
FBumann force-pushed the claude/new-session-divgg4 branch from c52b9a7 to f1a9632 Compare August 23, 2026 12:41
@FBumann
FBumann marked this pull request as ready for review August 23, 2026 18:35
@FBumann
FBumann requested a review from brynpickering as a code owner August 23, 2026 18:35
@FBumann
FBumann merged commit 8f78ac5 into main Aug 23, 2026
5 checks passed
@FBumann
FBumann deleted the claude/new-session-divgg4 branch September 9, 2026 06:45
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.

An ordering comparison against index(dim, i) loads with no defined meaning

2 participants