feat: a named expression may give a value per region, and no two regions may claim one coordinate - #168
Conversation
Documentation build overview
40 files changed ·
|
fa66bf4 to
7c279a9
Compare
7c279a9 to
ee22929
Compare
|
Note The following content was generated by AI. Taking @FabianHofmann's point about the asymmetric layout: the fallback is now the case named It stays inside cases:
always_on: { when: "not committable", expression: 1 }
boundary: { when: "committable and position(snapshot) == 0", expression: status_initial }
default: { expression: shift(status, over=snapshot, offset=1) }The name is reserved and the position is fixed —
|
ee22929 to
3e729c0
Compare
|
@FabianHofmann On hold until I move the lowering + Plan |
3e729c0 to
74b094d
Compare
|
Note The following content was generated by AI. Rebased onto
One textual conflict in
|
Note The following content was generated by AI. Rebased onto
|
Note The following content was generated by AI. Not properly, no — the previous comment proved The remainder had a double negation
Which reduces to There were no testsFour in
The other three: one region per case with each value lowered, the Mutation tableTaken by hand —
The first is the one that matters: an Verified: |
74b094d to
d19cc5d
Compare
b358dad to
e4ac6f6
Compare
|
Note The following content was generated by AI. Move the fallback out of expressions:
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 reserved name inside a map of user-chosen names needs five hand-written errors to police it.
Two further reasons:
Cost is small and confined to the input spelling. Keeping the required fallback; only its spelling moves. |
FabianHofmann
left a comment
There was a problem hiding this comment.
@FBumann let's pull this is. this looks clean now and fulfills our needs at this stage.
|
@FBumann ready to pull in? |
…ons may claim one coordinate Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VUsRBP5sMNq32wkCcLTW15
5c63589 to
427ae2f
Compare
…transit at the horizon's edge Rung 16 states PyPSA's link `delay` and `cyclic_delay` in the parity gallery: a port's delivery is shift(…, offset=Link_output_delay, edge=…), and a cases: block on Link_output_cyclic_delay chooses edge='wrap' (cyclic) or edge=0 (lost). The language already had cases: (#168) and shift; nothing in src/ or the schema changes — this is a new example closing a parity row, so it documents rather than adds. Supersedes #75; the weighted-duration case is #299. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jk5LAMCoiD39q4Xz4AMVw
…transit at the horizon's edge (#300) Rung 16 states PyPSA's link `delay` and `cyclic_delay` in the parity gallery: a port's delivery is shift(…, offset=Link_output_delay, edge=…), and a cases: block on Link_output_cyclic_delay chooses edge='wrap' (cyclic) or edge=0 (lost). The language already had cases: (#168) and shift; nothing in src/ or the schema changes — this is a new example closing a parity row, so it documents rather than adds. Supersedes #75; the weighted-duration case is #299. Claude-Session: https://claude.ai/code/session_016jk5LAMCoiD39q4Xz4AMVw Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Implements the feature from #2. Supersedes #70, #36 and #33 — #70 took it through the shape of the block alone, #36 through a proved partition; this is the middle ground decided in #70's thread.
— @FBumann, in #70, agreeing with @FabianHofmann
Note
The following content was generated by AI. Rewritten after #251 and #252 landed — the earlier body described a version where the fallback was a reserved case named
default.expressions:takescases:— one case per region over a declaredforeach:— and anotherwise:beside them. Exactly one applies at every coordinate, by two different means: no twowhenmasks can hold at once, proved at load before any data binds, andotherwise:carries whatever the cases leave. So the cases have no order and no reserved name among them: each says where it applies on its own terms, and a file a key-sorting formatter rewrote says what it said before.Three regimes, one quantity, and the inequality using it written once instead of three times that nothing checks agree.
It is a
ProgramnodeThe fallback arrives carrying its own mask. The file writes none; lowering fills in the negation of every other region's, cancelling a negation rather than stacking one. So a consumer adds regions rather than working out which is left, and the remainder is resolved once here instead of once per consumer:
It could not lower away instead. The program's nodes have no predicate-in-a-value-position, and the obvious alternative — mask each region and add — fails on the absence rules: absence spreads through
+, so every region outside its own would take the row with it.Casesis the one node carrying a mask in a value position, and its docstring says so, that being why it exists.The two rules that make it a quantity
No two cases may claim one coordinate, and it is a load error when they can — decided before any data binds, with the pair named and a coordinate they both claim:
That is why
boundaryabove sayscommittable and. Two values at one coordinate is not a quantity, so the regimes are spelled apart rather than ranked.otherwise:is the value wherever nowhenholds. It takes every coordinate the cases leave — which is what makes the quantity whole without a second proof, there being no condition on it to fail. Totality is not a nicety: a gap would leave the quantity undefined, and absence spreads, so every constraint referencing it would lose rows it never masked. A cased expression being total is what keeps a constraint's row set readable at the constraint. It is not free, and the example shows the price: with no mask to narrow the frame,otherwise:has to say what an absent parameter or an unnamed label gets.It is written beside
cases:rather than inside them because it is not a region like they are — it is what is left. Having nothing but a value to carry, it is written as one, the shorthandexpressions:itself takes, and it prints as the last row, the one that reads "otherwise".Covering a coordinate is not having a value at it. The
otherwise:above carries noedge=, so it is empty at the first snapshot;previous_statusis whole only because every unit there is claimed byboundaryor byalways_on. The docs say so, and name the three ways to close such a hole. Nothing catches one left open at load, because whether a case has a value there depends on the data.The near-miss is named in each direction — no
otherwise:, anotherwise:with nocases:, both forms, neither form, aforeach:without cases — and, the typo this design invites,where:inside a case:A case with no
when:and an emptycases:are the closed schema's own errors,whenbeing required andcases:carryingminProperties: 1.The exclusivity check, and why it is not a SAT call
Every atom in the where-grammar talks about exactly one subject — a parameter, a dimension's coordinates, a dimension's rank, a lookup, a pair of lookups. A propositional reading invents a storage that is both a battery and hydrogen, and refuses the split the feature exists for. So each subject is split into cells — finitely many regions its value can sit in, chosen so every atom over that subject is constant on each — the pair's cells are multiplied out, and a cell where both masks hold is a witness. Because the cells cover every value a subject can take, "no witness" is a proof rather than a sample.
Independence between subjects over-approximates, so a spurious world can only manufacture a witness, never hide one: this refuses case sets that would have been fine and admits none that would not. A pair it will not reason about — both ends of one axis, an ordering on a dtype that has none, a product past
CELL_BUDGET— is refused exactly as an overlap is, and the refusal names the rewrite.tests/test_exclusivity.py::TestSoundnessis the check on that claim: pairs are drawn independently, filtered to the ones the check proves apart, and walked over a grid finer than the cells — several points inside single cells, both infinities, an absent value, labels no mask names.History
Five commits, then two stacked PRs merged in:
feat(language): a named expression may write a constant as a number— the bug writing the example found, separable fromcases:.feat(language): a named expression may give a value per region…— the construct, the exclusivity proof, the lowering, and the printing.docs: unit commitment, the model cases exists for— the example, its gallery page, the nav entry.cases:and becomes the block's ownotherwise:, retiring_default_is_the_last_caseand the five hand-written errors that policed a reserved name inside a map of user-chosen ones.otherwise:was printing upright, as data rather than as something solved for; an error inside an arm named the constraint that used it rather than the declaration; an integer partition was refused at a midpoint no integer can be; the exclusivity soundness fuzz built every pair as a complement and so could not fail; and an unreachable guard plus thedefaultspelling the rename left in five docstrings.The node also answered to three rebases: #201's fence (
every_program_node.yamlgains a cased expression, in the same commit as the node), #208's totalfan_in(Casesdeclares one-to-one — its regions are disjoint, so an output row reads exactly one of them, alternatives rather than slots summed together), and #211's move of the edge rules to load time.Checks
pixi run ciclean:lint(ruff,pyrefly,reuse,typos,prettier,zizmor,taplo), 884 passed againstmain's 829,mkdocs build --strict, and 27 TeX documents compiled — the new example among them.The nine PRs stacked on this branch (#242 … #250) are rebased onto its current tip.