Skip to content

feat(language): a quantity defined by cases may mix a constant, a parameter and a variable across its regions - #1464

Merged
FBumann merged 3 commits into
mainfrom
build/pin-alpha-51
Aug 31, 2026
Merged

FBumann merged 3 commits into
mainfrom
build/pin-alpha-51

Conversation

@FBumann

@FBumann FBumann commented Aug 31, 2026 •

Copy link
Copy Markdown
Collaborator

Prompt: "mathspec release the next one again! Lets update!" — and, asked whether to bump now and build
cases: later or do both at once, "Bump and build cases: in one PR".

Note

The following content was generated by AI.

math-spec v0.0.0-alpha.49 → v0.0.0-alpha.51, whose cases: block lets a model state one
quantity by region. Both engines build it: each region is kept to its own mask and the
results added, the language having proved the regions disjoint and total before any data
binds, so neither ranks them and neither needs a tie-break.

previous_status:
  foreach: [snapshot, generator]
  cases:
    always_on: { when: "not committable", expression: 1 }
    boundary:  { when: "committable and position(snapshot) == 0", expression: status_initial }
  otherwise: shift(status, over=snapshot, offset=1)

A region reaches only the coordinates it claims. Its data is owed only there — a parameter
standing for one region is not asked to cover the frame, and a divisor is asked the same
narrowed question — and its absence stays inside it, so a region empty where it does not
apply cannot unmake a row another region covers. A when: that names no dimension is a
scalar switch and is held to the same two rules.

Cases is the first node carrying a mask in a value position, and the plan table in
docs/about/architecture.md gains its row; ARCHITECTURE rule 1 is corrected, a cased
expression being the one named expression that is not substituted away before dispatch.

The PyPSA ladder is untouched here — the corpus adopting cases: is
math-spec#292, and the committed
certificate is byte-identical after a full parity re-run.

Mutation table

Five through tools/mutate.py --tests tests/test_cases.py; four are changed lines rather
than deleted ones, which the tool cannot express, so they were taken by hand with its three
precautions — tree committed first, restored with git checkout --, __pycache__ dropped
on both sides, tree asserted clean at the end (it was).

mutation result
the region presence relaxed outside its region (compiler.py) caught
the dimensionless region mask filtered by its constant (compiler.py) caught
the region presence relaxed under a dimensionless mask (compiler.py) caught
the complement's carrier row kept when the mask names no dimension (compiler.py) caught
the gap question asked inside the region (engine.py) caught
the eager fill that keeps absence inside its region (builder.py) caught
the eager coverage mask narrowed per region (coverage.py) caught
the coverage pairs ordered by name alone (coverage.py) caught
the divisor's question narrowed to its region (coverage.py) caught
Each region is read, and its own data decides the answer

Objective agreement alone would pass on an expression that collapsed to one region, so each
case varies the data one region reads:

boundary  the unit was off before the horizon -> first_step 55   relational=4890.0  eager=4890.0
boundary  the unit was on  before the horizon -> step 35         infeasible on both
never_off the unit is not switchable          -> step 40         relational=4890.0  eager=4890.0
never_off the unit becomes switchable         -> first_step 70   relational=3900.0  eager=3900.0

A hole a sum or a divide has consumed is still unreported — #1465

A hole on a constant side is reported by both engines where the piece is joined at the row
key
. It is not reported by the relational engine once an aggregation has summed it away:
the piece is summed per coordinate before the check looks, and polars reads a null sum as
zero.

a bare parameter, joined at the row key    relational: DataError   eager: DataError
the same parameter under sum(..., over=t)  relational: 70.0        eager: DataError
a divisor with a hole, per coordinate      relational: 35.0        eager: DataError

No cases: appears in that repro — it predates this change and is filed as
#1465.
test_a_hole_in_a_divisor_inside_its_region_is_still_refused therefore asserts the eager
engine alone and names the issue for why.

Verified

  • pixi run lint / format-check clean; pyrefly 0 errors, both legs.
  • pixi run test — 3464 passed, 251 skipped, 94 failed, every failure xpress: the
    expired local licence, matched as tests/test_xpress_sink.py or a […xpress…] parameter
    id rather than by the substring, since test_..._every_expression_node contains xpress.
  • PyPSA parity all 15 rungs MATCH with the certificate unchanged; tools.ladder --check
    clean.
  • Not run: pixi run test-bench — it refuses a loaded box and no bench/ file changed.

Deliberately not done

@FBumann
FBumann force-pushed the build/pin-alpha-51 branch from f00f883 to 0112b9d Compare August 31, 2026 12:23
@codspeed

codspeed Bot commented Aug 31, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 16 untouched benchmarks
⏩ 42 skipped benchmarks1


Comparing build/pin-alpha-51 (2a5b0db) with main (126f8c0)2

Open in CodSpeed

Footnotes

  1. 42 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 (7a5f015) during the generation of this report, so 126f8c0 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 Aug 31, 2026 •

Copy link
Copy Markdown

Documentation build overview

📚 lpspec | 🛠️ Build #34313745 | 📁 Comparing 2a5b0db against latest (7a5f015)

  🔍 Preview build  

1 file changed
± about/architecture/index.html

…ach region carries only where it applies

math-spec v0.0.0-alpha.49 -> v0.0.0-alpha.51, whose `cases:` block adds the one
node that carries a mask in a value position. Both lanes build it by keeping
each region to its own mask and adding the results, the language having proved
the regions disjoint and total before any data binds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TZbCoSM1Ah7YWg6K4Ps5sU
@FBumann
FBumann force-pushed the build/pin-alpha-51 branch from 0112b9d to 22655b1 Compare August 31, 2026 12:30
… names no dimension, and a divisor inside one is asked only there

Three defects found reviewing the cases: cut, each reproduced on both lanes
before it was fixed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018kpKM4mnqRDNaXYweFvLmH
…e issue that owns it

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018kpKM4mnqRDNaXYweFvLmH
@FBumann FBumann changed the title feat(language): a named expression may give a value per region, and each region carries only where it applies feat(language): a model states a quantity by region, so a value that differs by condition is written once rather than forked across constraints Aug 31, 2026
@FBumann FBumann changed the title feat(language): a model states a quantity by region, so a value that differs by condition is written once rather than forked across constraints feat(language): a quantity defined by cases may mix a constant, a parameter and a variable across its regions Aug 31, 2026
@FBumann
FBumann merged commit 482e9ec into main Aug 31, 2026
15 checks passed
@FBumann
FBumann deleted the build/pin-alpha-51 branch August 31, 2026 15:18
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.

1 participant