Conversation
Documentation build overview
41 files changed ·
|
29a8e33 to
94b8a31
Compare
94b8a31 to
157d920
Compare
d5fafb6 to
36ec005
Compare
87fb522 to
f95759c
Compare
f95759c to
42d69dc
Compare
42d69dc to
bd2dcdb
Compare
bd2dcdb to
faca235
Compare
faca235 to
417f239
Compare
417f239 to
643dba9
Compare
A constraint with `cases:` is one rule whose expression varies by region, and it is *one* constraint — one name, one row per coordinate, one dual — only if the cases claim each row exactly once. This decides that claim before any data binds, which is rule 2 in a new position. Both obligations are conditioned on the constraint's own `where`, which keeps that key meaning exactly what it means today (which rows exist) while `cases` says only which expression each existing row gets: disjoint where AND case_i AND case_j unsatisfiable exhaustive where AND NOT (case_1 OR ... case_n) unsatisfiable no dead case where AND case_i satisfiable Conditioning cuts both ways: cases overlapping somewhere the `where` already excludes are not an ambiguity and an unconditional check would refuse them, while a `where` wider than the cases cover is a gap that a "the rows are whatever the cases claim" reading could not express. Atoms are grouped by the subject they talk about and each subject is cut into cells, so equality against distinct labels comes out exclusive — a purely propositional reading invents a region where a storage is both a battery and hydrogen, and refuses the category split that motivates the feature. Three outcomes, and the third is refused rather than assumed: two positions counted from opposite ends of a dimension whose extent only data knows are the same row on a one-member axis. Every case carries its own mask. An open "everything else" case would have made exhaustiveness true by construction, but `not (x)` says the same thing and a mask edited without its restated negation lands here as a gap or an overlap rather than a silent change of model. Not wired into the schema: `cases:` is not a key the model accepts, and the spelling is still open (#2). This is the decision procedure the argument turns on, so it can be judged on what it proves and refuses. Tests pin one atom's reading at a time, plus a seeded fuzz asserting that anything proved a partition survives a value grid finer than the cells. Two of them cover a split counted from the back, where the rank cells had to mirror: the open end is before the earliest position named, since nothing follows -1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ADtfZf4V6W9XcLRSwSgHzE
`_dtype_of` re-derived what `Namespace.dtypes` already builds, and got a worse answer: it handled parameters and dimensions and returned None for every lookup, so an ordering on one — `period_of < 2040`, with `period` declared `dtype: int` — was refused as "has dtype None". Namespace covers lookups, including the non-obvious part that a targeted lookup takes its target dimension's dtype, so a natural multi-period split is now decided rather than refused. The dtype tuple also listed 'date', which is not a declarable dtype. `_Observed` is gone. `bare` was never read, `ordered` never outlived the node that set it, and `literals`/`positions` were never both populated for one subject — the tag was already `Subject.kind`, so a set per subject says it. `_Frame` replaces threading `domains` and `extents` separately, and memoises each atom's subject. `_subject_of` is a pure function of the node that allocates a fresh `Subject`, and `_atom` was calling it once per atom per cell: 972k calls in the fuzz. The nodes are `@dataclass(eq=True)` and so unhashable, hence the `id(node)` key. Witnesses are now built only while there is room for them — the verdict carries four, and a wide `where` over narrow cases would otherwise render one per uncovered cell and discard all but those. An unresolved node reaching `_subject_of` is now an AssertionError, as in `dimensions.py` and the typesetter. It is a caller that skipped `resolve_where`, not a model to refuse, and routing it to UNDECIDED put a message with no rewrite in front of a model author. Smaller: the two `except Undecidable` arms are one, `Gap` carried nothing a witness did not, the rank-cell frames share their head, and the prose loses what it said twice. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ADtfZf4V6W9XcLRSwSgHzE
#2 moved: the cases go on a named expression rather than on the constraint that uses it. Three independent axes multiply into eight constraint cases and add into seven expression cases, and the inequality is written once instead of eight times — a change to it is then one edit rather than eight that nothing checks agree. It also answers the objection that opened #2 better than a constraint `cases:` did. The constraint keeps one name and one expression, so its dual is unambiguous by construction rather than by proof. `where` goes with it. An expression carries no mask: it is **total** over its frame or it is refused. Two expressions each masked would intersect in the constraint that used them — absence spreads and takes the row — so a constraint's row set would stop being readable at the constraint, which is what `diagnostics().omissions` exists to catch ("rows lost to a mask the constraint never mentions"). Nothing is conditioned here now, and the price is visible in the tests: with no mask to narrow the frame, the cases have to say what an absent capacity or an unnamed label gets. The per-case key is `when`, not `where`. A case selects which value a coordinate takes; it creates no absence and deletes no row, which is what `where` means everywhere else (rule 6). Under this design an expression has no `where` at all, so a `where:` here would be the only one on the block and would read as extent — precisely the wrong reading. Adds the three quantities #2 factors a PyPSA ramp limit into as a test, since they are the case the design is for. Not wired into the schema — `expressions:` accepts neither `foreach` nor `cases` yet, and that is the next change rather than this one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ADtfZf4V6W9XcLRSwSgHzE
643dba9 to
0a622c5
Compare
|
Superseded by #70, along with #36 — the PR this decision procedure existed for. The difference. This module answers "do these cases partition the frame?" without data: pairwise disjointness, joint exhaustiveness and no dead case, all as one unsatisfiability question over cells of each where-atom's subject. It is careful work and the conservatism argument is right. #70 makes the question not arise. The arms of a cases:
always_on: { when: "not committable", expression: 1 }
boundary: { when: "position(snapshot) == 0", expression: status_initial }
interior: { expression: shift(status, over=snapshot, offset=1) }Disjointness is then unrepresentable rather than proved — two arms cannot disagree at a coordinate, because only the first to match is read. Totality is structural — the fallback has no condition to fail. Both properties belong to the shape of the block, so nothing has to decide what a predicate could be true of, and there is no case set that loads and later turns out to have a hole in it. The reason to close this rather than keep it for a stricter mode later is the coupling: every atom the What is genuinely lost: a fully shadowed arm is now silently dead rather than a load error naming a witness. If that turns out to matter, it is a much smaller check than this one — and it can be written against the arms that are actually there, rather than against every world the atoms could describe. The branch stays; nothing here is deleted, only unmerged. |
…rather than the first written winning The middle ground between #70 and #36, decided in #70. Each case's `when` is proved disjoint from every other's before any data binds, so the arms carry no order: each says where it applies on its own terms, and a reader checks one without the ones above it in mind. The fallback stays what #70 made it — the last case, carrying no `when` — so a value *everywhere* is still the block's shape rather than a second proof, and the complement no longer has to be written out as #36 required. `exclusivity.py` is #33's decision procedure with the exhaustiveness and dead-case halves cut, and the frame built per pair rather than over the whole case set: `when_i AND when_j` unsatisfiable, decided by cutting each subject into cells every atom over it is constant on. Independence between subjects over-approximates, so a spurious world can manufacture a witness but never hide one. A pair it cannot decide is refused the way a proven overlap is, and the message names the rewrite. The unit commitment example and the golden model pay the price the rule asks for: `boundary` says `committable and position(snapshot) == 0`, and the golden model's `winter` arm says `position(snapshot) > 0 and`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Prototype for #2, stacked on #31 — base is
claude/new-session-divgg4, so the diff here is one module and its tests. Retarget tomainonce #31 lands.Follows @FabianHofmann's proposal: the cases go on a named expression, not on the constraint. Not wired into the schema —
expressions:accepts neitherforeachnorcasesyet, and that is the next change rather than this one. This is the decision procedure the #2 argument turns on, so it can be judged on what it proves and what it refuses.Why expressions
Three independent axes — horizon boundary,
is_modular,is_committable— multiply at the constraint level and add at the expression level: eight constraint cases against seven expression cases. The count is the smaller half of it. The eight write the ramp inequality eight times, so a change to it is eight edits that nothing checks agree; the seven write it once:It also answers the objection that opened #2 better than a constraint
cases:did: the constraint keeps one name and one expression, soGenerator_ramp_limit_up.dualis unambiguous by construction rather than by proof.And the obligation factors. If each expression's cases partition independently, the product is automatically a partition of the constraint's rows — 2+2+3 small proofs rather than one eight-way one. The three quantities #2 factors that ramp limit into are a test here.
What it decides
check_partition(cases, schema)answers whether a set ofwhenmasks claims each coordinate of the frame exactly once. Three obligations, all the same unsatisfiability question:case_i AND case_junsatisfiable, for each pairNOT (case_1 OR … OR case_n)unsatisfiablecase_ialone satisfiableThree outcomes:
PARTITION,VIOLATED,UNDECIDED. The third is refused by a caller exactly as the second is; a checker that guesses where it cannot decide buys nothing over no checker.Atoms are grouped by the subject they talk about and each subject is cut into cells, so equality against distinct labels comes out exclusive. A purely propositional reading invents a region where one storage is both a battery and hydrogen, and refuses the category split that motivates the feature.
x/NOT (x)== 0/> 0== -1/< -1values:whencan never holdsoc_initialvssoc_initial == 00and-1are one row on a one-member axisby=groupDecisions this encodes
cases:, notbranches:. It renders to\begin{cases}, and "branch" is a control-flow word for a language whose ceiling forbids control flow.when:, notwhere:. A case selects which value a coordinate takes — it creates no absence and deletes no row, which is whatwheremeans everywhere else (rule 6). Under this design an expression has nowhereat all, so awhere:here would be the only one on the block and would read as extent.whereon expressions; cases are total over the frame. Two masked expressions would intersect in the constraint using them — absence spreads and takes the row — so the constraint's row set would stop being readable at the constraint. That is whatdiagnostics().omissionsexists to catch: "rows lost to a mask the constraint never mentions." The price is visible in the tests: with nothing to narrow the frame, the cases have to say what an absent capacity or an unnamed label gets.cases:. Everything varying in the value factors into a named quantity; what cannot factor is the relation, and a differing relation is a differing rule that wants its own name and its own dual.piecewise.pyalready settles this by precedent — facing>=on the first breakpoint and<=on the last, it emits two named constraints.Soundness
The check rests on its cells covering every value a subject can take, so "no witness among the cells" means "no witness".
TestSoundnesswalks a value grid finer than the cells — two points per band, both infinities, absence, labels no mask names — and asserts anything proved a partition survives it. Wider runs off-branch, with ordering comparators in both frames: 40,000 random case sets per frame, 202 and 167 proved partitions, 0 unsound; and 3,000 sets built to be partitions gave 2,187 proved, every one of the 813 refusals a dead case rather than a spurious overlap or gap.Independence between subjects is an over-approximation, so a spurious world can only manufacture a witness, never hide one — every outcome is conservative in the safe direction.
Still open
The dim rule. A named expression has no
foreachtoday — dims fall out of the body — but no single case's body gives the quantity's shape once cases exist, and one case's body may be a scalar while itswhenis not.foreachrequired alongsidecasesis the proposal; that is the wiring change, not this one.Checks
Gates reproduced with a 3.13 venv on the versions
pixi.tomlpins plusprettier@3.9.3:ruffclean,pyrefly0 errors,reuse lintcompliant, 379 passed / 4 skipped.