Skip to content

feat(language): a model states its topology as relations, the way math-spec 0.0.0-alpha.89 does - #1646

Merged
FBumann merged 2 commits into
mainfrom
claude/lpspec-mathspec-update-9j5gmu
Sep 15, 2026
Merged

FBumann merged 2 commits into
mainfrom
claude/lpspec-mathspec-update-9j5gmu

Conversation

@FBumann

@FBumann FBumann commented Sep 15, 2026 •

Copy link
Copy Markdown
Collaborator

Prompt: Let's update lpspec to the latest mathspec version

Note

The following content was generated by AI.

The pin moves to v0.0.0-alpha.89, whose one feature (#437) replaced lookups: with relations: and renamed two operator keywords. Both lanes read the new plan; every model, example, rung and page is written in it.

Either lane still builds the single-valued map the old lookup was — two columns, one of them the key. A wider table is refused at lowered, so check, build and an archive refuse the same file.

What the language change is

A relation is a table with one column per dimension it relates, and key: names the columns a row is identified by. The declaration fixes no direction: the operator says which column it consumes and which it produces. shift(over=, …) became shift(along=, …), and sum_back(over=, within=) became sum_back(along=, window=).

before after
lookups: {gen_bus: {over: generator, into: bus}} relations: {gen_bus: {columns: [generator, bus], key: generator}}
shift(soc, over=snapshot, offset=1) shift(soc, along=snapshot, offset=1)
sum_back(started, over=hour, within=min_up) sum_back(started, along=hour, window=min_up)
What moved in the source
  • relations.py is new: the one reading of a declared relation — refusal, and the accessors that are well-defined once a wider table has been turned away. Documented in ARCHITECTURE's module table.
  • Both lanes read the plan's new shape: Walk on GroupSum, At, Translate.partition, Window.partition and DimensionPositionNode.partition; RelationComparisonNode, RelationPairComparisonNode and RelationDefinedNode in where.
  • sources.py reads a relation's table under its declared roles and hands on the (key dim, relation) frame both lanes already read, so a self-map supplies cleanly and no reader downstream changed shape. The checks are the ones it ran before: keys are labels, values are labels, one row per key, no null.
  • The engine reads the plan directly, not lpspec.relations: its subpackage may import only lpspec.errors and math_spec.program (hard rule 2), and Walk already carries everything it asked DimensionDeclaration.targets for. _Walk.of, _named_amount and _grouped_into take the walk instead of a partition name, which drops a compiler.program lookup from _grouped_into.
  • The PyPSA parity runner reads a relation's key: where it read lookup.over, and the projection keeps the dimensions a relation's columns: name. Second commit; it is why differential/ is a separate chore.
What I deliberately did not do

The new reach is not implemented. A bare relation, a key of several columns, a walk with a column joined on, and within= on a partition are all refused rather than built. Following a pin is not the PR that grows two lanes a new join shape, and the refusal names the rewrite its shape asks for:

relation 'gen_bus' declares no key, and this package builds the single-valued map: one row
per key, so a walk reaches one label rather than a set of them. Declare key: on it — one of
['generator', 'bus'], whichever holds each label once — or carry the membership as a bool
parameter over ['generator', 'bus'] and select on it with a where string.
relation 'zone_of' has 3 columns keyed by 2, and this package builds the single-valued map:
two columns, one of them the key, so a walk trades one dimension for one other and joins on
nothing. Split it into one relation per pair — 'zone_of' over ['generator', 'period', 'bus']
becomes a map per value column, each keyed by the same single column — or carry the wider
table as a parameter over its dimensions and select on it with a where string.

Worth its own issue, since the relational lane is close: _remap_fragment already trades tuples of dims, and a joined column is that join on more keys.

Coverage that moved

test_a_hand_built_node_whose_tuples_disagree_is_refused asserted a guard the new IR makes unreachable — GroupSum derives coordinate, over and into from walks, so the two tuples cannot disagree. The zip(strict=True) it certified is still there and still reachable, by the route that now exists: a walk onto two columns at once. It is test_a_hand_built_walk_onto_two_columns_is_refused in the same file, with the same purpose in its docstring.

tests/test_sum_by_lookups.py is tests/test_sum_by_relations.py. tests/test_assertions.py's ratchet falls 208 → 207.

The new guard, deleted and re-run

relations.refusal's loop deleted, whole suite re-run:

FAILED tests/test_label_coords.py::test_a_relation_wider_than_a_map_is_refused_with_the_rewrite_named[a-bare-relation-is-a-set-per-label-and-not-a-map]
FAILED tests/test_label_coords.py::test_a_relation_wider_than_a_map_is_refused_with_the_rewrite_named[a-key-of-two-columns-joins-on-one-the-walk-does-not-trade]
2 failed, 3964 passed, 251 skipped, 1 xfailed
guard caught by
relations.refusal's shape loop the two cases above, and nothing else

Its complement, test_the_map_the_wider_shapes_are_rewritten_to_is_the_one_that_loads, keeps the pair from passing by refusing everything.

PyPSA parity

Every rung matches, and references.json and the written tables come back byte-identical — so no objective, dual or structural comparison moved. The rung projections changed in layout only, because the first commit hand-edited files the harness generates; they and the ladder pages that embed them are regenerated by the harness and by tools.ladder.

rung_01_transport: MATCH · 45 rows, 16 columns · duals on 45 rows · 10 equal · 0 region
rung_02_storage: MATCH · 103 rows, 48 columns · duals on 103 rows, 2 negated · 27 equal · 0 region
rung_03_expansion: MATCH · 2 of 59 names differ · duals on 184 rows, 4 negated · 57 equal · 3 region
rung_04_ramps: MATCH · 64 rows, 20 columns · duals on 64 rows, 2 negated · 12 equal · 0 region
rung_05_global_constraints: MATCH · 2 of 24 names differ · duals on 102 rows, 2 negated · 23 equal · 2 region
rung_06_kvl: MATCH · 2 of 21 names differ · duals on 123 rows · 19 equal · 3 region
rung_07_commitment: MATCH · 116 rows, 44 columns · no duals — mixed-integer · 20 equal · 0 region · 3 recorded
rung_08_modular_big_m: MATCH · 191 rows, 80 columns · no duals — mixed-integer · 36 equal · 0 region
rung_09_multilink: MATCH · 92 rows, 32 columns · duals on 92 rows · 8 equal · 0 region
rung_10_quadratic_costs: MATCH · 60 rows, 24 columns · duals on 60 rows · 8 equal · 0 region
rung_11_ac_dc_meshed: MATCH · 2 of 19 names differ · duals on 468 rows · 17 equal · 1 region · 1 recorded
rung_12_linearized_uc: MATCH · 128 rows, 44 columns · duals on 128 rows, 1 negated, 2 names differ · 24 equal · 0 region · 3 recorded
rung_13_losses: MATCH · 2 of 23 names differ · duals on 150 rows · 22 equal · 0 region · 1 recorded
rung_14_stochastic: MATCH · 3 of 18 names differ · duals on 87 rows, 1 names differ · 17 equal · 0 region · 2 recorded
rung_15_multi_period: MATCH · 80 rows, 31 columns · duals on 80 rows · 14 equal · 0 region
rung_16_link_delay: MATCH · 52 rows, 20 columns · duals on 52 rows · 8 equal · 0 region
every rung matches PyPSA as deep as the engines allow, and says how deep that is

Run a second time on the regenerated tree with no further change, which is what the job's git diff --exit-code -- differential/pypsa step asks.

What was verified, and what was not

Every gate the required check runs, plus the docs build, the bench harness and the parity runner, on 14de16e:

ruff check .                              All checks passed!
ruff format --check .                     321 files already formatted
pyrefly check                             0 errors (both configs)
pytest tests -n 4                         3967 passed, 251 skipped, 1 xfailed
mkdocs build --strict                     Documentation built in 10.01 seconds
pytest bench/test_harness.py              145 passed, 10 skipped
differential/pypsa/parity.py <corpus>     every rung matches

Generated output was regenerated by its own generator and the diff read: tools.gallery_math (47 pages), tools.ladder (the ladder page and 16 rungs), the parity runner (references.json, rungs/, tables/), uv lock. The YAML fences the gallery pages carry byte for byte, and the two PyPSA reference build() fences, were refreshed from their files.

Not run, and why. pixi is not installed in this environment, so the gates ran in a uv venv on the pinned ruff==0.16.1 and pyrefly==1.2.0 rather than through pixi run check. On an unpinned pyrefly 1.3.1 six errors appear, all in api.py, frames.py and assembly.py, none of which this diff touches. No benchmark was taken: nothing here claims a number, and the machine was not idle — CodSpeed reports the PR does not alter performance.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RuEnfVdQZKP2fxRWrgJRH1

…h-spec 0.0.0-alpha.89 does

The pin moves to v0.0.0-alpha.89, where `lookups:` became `relations:`: a
table with one column per dimension it relates and a `key:` saying which
columns identify a row, walked in whichever direction the call names. Both
lanes read the new plan — `Walk` on every `by=` node, the relation predicate
nodes, and `shift`/`sum_back`'s renamed `along=`/`window=` keywords — and the
models, examples, ladder rungs and pages are written in it.

Either lane builds the single-valued map the old lookup was: two columns, one
of them the key. `lpspec.relations.refusal` turns away a wider table at
`lowered`, so check, build and an archive refuse the same file, and the
message names the rewrite its shape asks for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RuEnfVdQZKP2fxRWrgJRH1
@codspeed

codspeed Bot commented Sep 15, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 24 untouched benchmarks
⏩ 58 skipped benchmarks1


Comparing claude/lpspec-mathspec-update-9j5gmu (14de16e) with main (c47ae6c)2

Open in CodSpeed

Footnotes

  1. 58 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

  2. No successful run was found on main (dd4a857) during the generation of this report, so c47ae6c was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩

@read-the-docs-community

read-the-docs-community Bot commented Sep 15, 2026 •

Copy link
Copy Markdown

…ather than a field it lost

`prepared` read `relation.over` and the projection read `lk['over']`/`lk['into']`,
which a `RelationBlock` no longer carries: its columns are `columns:` and the
side that is one is `key:`. The runner now cuts a relation's table by its key
roles and keeps the dimensions its columns name, so every rung prepares again.

The rung projections are regenerated, and the ladder pages that embed them with
them. `references.json` and the written tables are byte-identical, so no
objective, dual or structural comparison moved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RuEnfVdQZKP2fxRWrgJRH1
@FBumann
FBumann merged commit 1fd8926 into main Sep 15, 2026
13 checks passed
@FBumann
FBumann deleted the claude/lpspec-mathspec-update-9j5gmu branch September 15, 2026 20:54
fluxopt-release-bot Bot added a commit that referenced this pull request Sep 15, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.0.1-alpha.334](v0.0.1-alpha.333...v0.0.1-alpha.334)
(2026-09-15)


### Features

* **language:** a model states its topology as relations, the way
math-spec 0.0.0-alpha.89 does
([#1646](#1646))
([1fd8926](1fd8926))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: fluxopt-release-bot[bot] <307443024+fluxopt-release-bot[bot]@users.noreply.github.com>
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