Repository navigation
feat(language)!: a missing row is refused only where the model reads it, a file names its value instead of neutral, and the program says where each parameter is read - #880
Conversation
… reads it `refused` was checked over every coordinate of a parameter's dimensions, so no `where:` could excuse a row the model never reads. It now refuses a missing row only where the model reads the value: a row it builds, a bound of a variable that exists, a summand that exists, an offset or window amount, the objective, a reported expression, a curve's breakpoints. A `where` reads no parameter. Two load refusals that read `refused` as complete coverage go: a bare `int` or `str` name under `refused`, and a values table of a curve under `points:` declared `refused`. absence.md: median 16 words, 4 sentences over 25. piecewise.md: median 16 words, 3 sentences over 25. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AvtZGymCbY9swLnR8JwHng
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AvtZGymCbY9swLnR8JwHng
Documentation build overview
61 files changed ·
|
… `missing: neutral` is gone `neutral` read a missing row as 0 or false, named for an intent that holds only where the parameter is an additive term. Elsewhere it read 0 anyway: a whole term dropped, an exponent gave 1, an offset read the coordinate itself. Write the value instead: `missing: 0` or `missing: false`. A variable takes `missing: 0` for "zero outside the mask". A parameter named `missing: 0` that stands as a divisor is refused at load. absence.md states what each reading does at each position, amounts included: under `absent`, a missing offset or window makes the operator absent there. The PyPSA module sizes `*_nom_mod` keep the default `refused`: every arithmetic read sits inside a `> 0` mask, and the divisor refusal caught them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AvtZGymCbY9swLnR8JwHng
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AvtZGymCbY9swLnR8JwHng
…ide its mask `missing:` on a variable took `absent` or `0`. It now takes any finite number, refused at load where the number leaves the variable's domain or its literal bounds. The absence table states a constant once, on either side of a comparison, and the zero-divisor refusal ignores a `false`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AvtZGymCbY9swLnR8JwHng
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AvtZGymCbY9swLnR8JwHng
Note The following content was generated by AI. Closed in favour of #881, which carries every commit of this PR and has one changelog line for both changes. Generated by Claude Code |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AvtZGymCbY9swLnR8JwHng
neutral
Note The following content was generated by AI. Two points for the contract, from porting specsolve to 0.3.0 (fluxopt/specsolve#2047):
What the read set is made of, and what it cannot beFor a parameter
The image through a relation is a push-forward. As far as I can see, the This also answers the guard idiom without exempting masks: Generated by Claude Code |
…rogram Work in progress, not for review. `Program.reads` maps each refused parameter to the mask over its dims where the model reads it. The reversed shift and the look-ahead window still use the `reverse` and `ahead` flags on `Translate` and `WindowSum`, which are to be replaced by one predicate node. No tests or docs yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AvtZGymCbY9swLnR8JwHng
…ing oracle Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AvtZGymCbY9swLnR8JwHng
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AvtZGymCbY9swLnR8JwHng
…evaluated A `where` read no parameter, so a `refused` boolean flag that only masks rows, such as an `extendable` or `committable` flag, was never refused: a missing row read as false. A bare `bool` name reads the value of the flag, and has no guard idiom to protect, so it now counts as a read at every coordinate where the model evaluates that `where`. A comparison and a bare number or label still read nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AvtZGymCbY9swLnR8JwHng
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AvtZGymCbY9swLnR8JwHng
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AvtZGymCbY9swLnR8JwHng
…ameter `Program.reads` maps each `refused` parameter to a mask over its dims that holds where the model reads it, so a tool evaluates it when it attaches the data and refuses a missing row before it builds anything. `TranslatedPredicate` is `WindowPredicate`: the operand holds at some position of a window along one dimension, back or ahead, with wrap and a partition. A file's predicate shift is a window of one position. docs: reading.md documents `program.reads` and the window predicate, and absence.md points to it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AvtZGymCbY9swLnR8JwHng
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AvtZGymCbY9swLnR8JwHng
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AvtZGymCbY9swLnR8JwHng
neutralConflicts came from #855, #878 and #870 on main. Resolved toward main's names: modules start with an underscore, so the read masks live in `mathspec._reads`; program nodes are `ParameterReference`, `VariableReference`, `ExpressionReference` and `MaskReference`; spec classes end in `Spec`; the docs call a named item an entry. The refusals main still carried for `refused` under `points:` and a bare `int` or `str` name stay removed, and `neutral` stays gone from main's new lines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AvtZGymCbY9swLnR8JwHng
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AvtZGymCbY9swLnR8JwHng
…ssing row means, a sum or a lookup names the columns of its relation, and a dimension declares whether its order counts (#2047) > **Prompt:** Lets update specsolve to the latest mathspec > [!NOTE] > The following content was generated by AI. specsolve now requires mathspec 0.3.0. The engine and the linopy lane build 0.3.0's program and apply each declaration's `missing:`. Every spec is in the 0.3.0 language. The PyPSA parity gate runs the 0.3.0 corpus: 68 rungs match, and rung 63 differs as a known failure of that corpus. <details><summary>What changed in the engine</summary> - **Relations.** A sum through a relation is now a `Sum` over the axes of a `Join`, and a lookup is a bare `Join`. A `where` reads a `JoinedPredicate` or a `NamedMask`. `relations.py` maps `JoinColumns` onto the old consumed/produced/joined roles: dropped, added, kept. - **`missing:` at attach** (`attaching.py`): - A parameter or relation short of a row is refused by default. The message names the first missing coordinate and the number of others. - A declared value fills the table, so nothing downstream sees the gap. A bare numeric name in a `where` still asks whether the data gave the row, through the `given` frames, as 0.3.0 says. - A dimensionless parameter may arrive with no row when it declares `missing:`. - **`missing: absent` in the build.** - The parameter's piece carries a presence frame, so absence follows mathspec's rules: a constant or divisor removes the row, a summand is one fewer, a bound leaves its side open. - A constant piece's presence now restricts the rows (`assembly.py`). - **`missing: neutral`.** It keeps 0.2.1's reading. The engine refuses a missing row only where it stands as the whole constant side, as a divisor or as a bound. Under a sum, a missing row is one summand fewer: mathspec's own PyPSA file needs this for `sum(Transformer_phase_shift_weight, over=transformer)`. - **`given: masks:`**, new in 0.3.0, is refused like every other `given:` name. The refusal named no mask before (`this spec reads under given:`). `a-mask-another-file-declares` in `test_composition.py` fails without the fix. - **A latent bug, fixed** (`pieces.py`). A product carried a presence whose key was only implied onto a piece with more dims, which crashed. A masked variable under a filled `shift` could reach it before. The absent breakpoint table of a ragged curve now reaches it, and `test_a_ragged_curve_down_to_one_point_is_refused` covers it. - **The archive's `INPUTS_LAYOUT` rises to 2**, because `spec.yaml` is now written in the 0.3.0 language. </details> <details><summary>How the specs were migrated</summary> - **The call syntax** was rewritten mechanically: `sum(x, by=l, over=a, into=b)` → `sum(x, over=a, by=l[b])`; `at(x, by=l, over=a, into=b)` → `at(x, by=l[a])`; `by=l, within=c` → `within=l[c]`; `rel.col` → `rel[col]` in a `where`; `absence: zero/undefined` → `missing: neutral/absent`. - **`ordered:` and `missing:` were decided by what each declaration means, not by the errors.** Time axes, periods, breakpoints and SOS sizes are `ordered`; sets of things are not. A parameter keeps the default `refused` unless its description, or the intent of its test, says the table is sparse. - **Errors after that pass were treated as findings:** - OSeMOSYS `build_cost` and `holding_cost` are given only for technologies that have a cost, so they are `neutral`. - The tests of the engine's own refusals (sparse divisor, sparse constant side, NULL bound) now declare `missing: neutral`, the only reading that still reaches those guards. `tests/test_missing.py` covers the default refusal and each reading on both lanes. - A test that shifted along `generator` now shifts along `snapshot`, because 0.3.0 refuses a `shift` along a set. - **Generated pages** were regenerated or re-synced: the gallery fences, gallery math, constructs, the walkthrough golden file and the ladder pages. </details> <details><summary>PyPSA parity: the corpus moves to v0.3.0</summary> The v0.2.1 corpus does not load under 0.3.0, so `pypsa-parity.yml` now checks out `v0.3.0`. That corpus pins PyPSA master at `51986084`, not a release, and the workflow pins the same commit. `prep.py` prepares the 9 parameters 0.3.0 adds. Rungs that patch the base file are now read through their `PATCH`. The projections keep the `masks:` a rung needs. `untested.txt` lists the gaps as the gate measures them; the gate reports 0 unlisted. **The linopy lane now builds rungs 56, 66 and 68.** Each has a dimension with no labels, and the lane's `operator_at` raised `IndexError` there. It now stands a made-up label in, which the mask removes again. `test_a_lookup_into_a_dimension_with_no_labels_reads_nothing_on_both_lanes` fails without the fix. On rung 66 the lane keeps rows of zero coefficients, as linopy does, so the gate's row reader now skips a label with no nonzero coefficient, as linopy's LP writer and row count do. All three rungs are structurally equal to PyPSA. **A dual that differs is checked against the range PyPSA's model admits.** Where an optimum is degenerate, a row has a range of optimal duals, and the two solvers may return different ones. For each differing row, the gate moves the row's right-hand side by ±0.001 in PyPSA's own model, re-solves on HiGHS over a copy of its LP file, and measures the slope of the objective on each side (`differential/pypsa/slopes.py`). Ours counts as equal if it lies between the two slopes. PyPSA's own dual must lie there too, or the run stops; that self-check caught a sign error in the first version. The ladder pages show the count as `inside PyPSA's range`. - **Rung 55:** 8 differing rows, all inside. - **Rung 67:** 6 differing rows, all inside. `Process-p-ramp_limit_down` is written negated, as `Link-p-ramp_limit_down` is, and `deviations.yaml` now says so. Rungs 17 and 45 check that record too. - **Rung 57:** the `duals:` reason for `Generator-p_nom_set` (two identical pins that split one dual) is gone, because the range check now accepts that split and the gate refuses a reason no rung needs. **Rung 63 is a known failure of the 0.3.0 corpus.** `pypsa.yaml` builds `p_nom` and its floor row for an extendable unit that is not active, and PyPSA builds neither ([energy-models/mathspec#883](energy-models/mathspec#883)). The fix is [energy-models/mathspec#886](energy-models/mathspec#886). It adds a `<Component>_active_asset` parameter for each of seven components, so this gate's `prep.py` must feed them when the corpus moves past 0.3.0. `differential/pypsa/known_failing.yaml` lists the rung with that issue. The gate and the ladder-page test expect it to differ, and fail once it matches, so the line goes with the corpus move that carries the fix. </details> <details><summary>Mutation table of the dual range check (taken by hand)</summary> Each mutation was made in `differential/pypsa/slopes.py`, the file restored with `git checkout --`, and `__pycache__` dropped on both sides. `tests/test_slope_ranges.py` ran each time. | guard | result | |---|---| | a dual outside the range is refused (`inside`) | caught | | a side that makes the model infeasible has an infinite slope | caught | | the range takes both sides, not one | caught | | the tolerance at an end of the range | still green, then caught by the probe `past-an-end-by-less-than-the-tolerance` | | PyPSA's own dual lies in the range (`parity.py`) | not reachable from the suite; the gate's own self-check | </details> <details><summary>Mutation table of the engine (taken by hand)</summary> Each guard is a condition inside a line, which `tools/mutate.py` cannot express as a line deletion. Each mutation was taken in a clean worktree at `c801bf1`. The file was restored with `git checkout --`, `__pycache__` was dropped on both sides, and the full suite ran without the two parity-dependent files. The tree came back clean. | guard | result | |---|---| | a relation short of a key is refused (`attaching.py`) | caught | | a parameter short of a row is refused (`attaching.py`) | caught | | a declared value fills a missing row (`attaching.py`) | caught | | a bare numeric name asks whether the data gave the row (`predicates.py`) | caught | | an absent parameter carries its presence (`compiler.py`) | caught | | an absent bound leaves its side open (`compiler.py`) | caught | | a constant piece restricts the rows (`assembly.py`) | caught | | an absent parameter is not owed its coordinates (`assembly.py`) | still green, then caught by the purpose-built probe `absent-summand-on-a-constant-side-is-not-owed` | | a product spells out the keys it carries (`pieces.py`) | caught | The `assembly.py` filter this last probe tested was removed later, in `e154a77`, when summed gaps stopped being refused. The probe still checks the behaviour. </details> <details><summary>Gates run, what was not run, and defaults departed from</summary> - **Run:** - Full suite, at `86324dd`: 5759 passed. - The PyPSA parity gate, in full at `b913a56`: 68 match, 1 differs (rung 63). Its exit at `86324dd` was checked on the committed stamps: 0 with rung 63 listed as a known failure, and 1 when a rung that matches is listed. - `ruff check .`, `ruff format --check .` and `pyrefly check` pass. - **Not run:** `pixi run check` (no pixi in this container; the same gates ran through uv), `docs-build`, and the benchmarks. - **One PR, not three.** The pin forces the engine port, the spec migration and the corpus move together, because none of them loads without the others. The commits keep them apart for review. - **Not done:** - The `refused` check where the model reads the value, which energy-models/mathspec#880 introduces, is left for the release that carries it. This PR follows 0.3.0 as released. </details> 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01A481aCHLAzU6coTE2VB1zP --------- Co-authored-by: Claude <noreply@anthropic.com>
A term added to a given sum, a flag in an assumption, and a wrapped shift by a missing named offset each get a wrong read mask. A program that still holds a curve answers its tables with a mask false everywhere. The zero-divisor refusal misses every spelling but a bare parameter. All of these fail on this commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AvtZGymCbY9swLnR8JwHng
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AvtZGymCbY9swLnR8JwHng
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AvtZGymCbY9swLnR8JwHng
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AvtZGymCbY9swLnR8JwHng
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AvtZGymCbY9swLnR8JwHng
One list, `model_trees`, names each tree whose value goes into the model: constraint sides, the objective, bounds, curve links and terms. Lowering marks `in_math` from it, and `Program.reads` walks from it, so a term added to a given sum is read where it is present, and a program that still holds a curve is refused with the rewrite. A bare flag in an assumption is read at every coordinate of its frame. A named offset or width with no row makes its operator absent, and the window predicate holds nothing there; a wrapped shift no longer folds that away. The window contract states its normal form: a literal offset is 0 or more, a named offset is signed, and a width of 0 or less is an empty window. The zero-divisor refusal walks the whole divisor tree. Connectives are flattened before repeated parts are dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AvtZGymCbY9swLnR8JwHng
|
Superseeded by #894 |
Note
The following content was generated by AI.
missing: refusednow refuses a row only where the model reads it, andProgram.readsgives that set as a mask a tool evaluates at attach.neutralis gone: a file writes the value. A masked variable may stand as any number. Imports break:TranslatedPredicateisWindowPredicate.This PR carries what were #881 and #885. Departure from AGENTS.md: the two halves can be separated, but the contract has no code in mathspec except
Program.reads, and its oracle is the only check of the contract, so they are reviewed together.Contract: where a parameter is read
absence.md has a new section, Where a parameter is read. The model reads a parameter at these coordinates: a row it builds, a summand that exists, a bound of a variable that exists, an
offset=/window=amount, the objective, a reported expression, the breakpoints of a curve, and a term withadds_to:a given expression wherever the term is present. A row that an absent term removes reads nothing, and a row with no variable term left is not built.Inside a
where, only a bareboolname reads. A flag such aswhere: committableis read wherever the model evaluates thatwhere: every coordinate of a variable's or constraint's dims, thewhenof a case wherever the expression that holds it is evaluated, and the positions thatcount,shiftoratlook at. A flag in anassumptions:entry is read at every coordinate of the frame of the assumption. So a missing flag is still refused, as in 0.3.0. Without this rule, 45boolflags in the PyPSA spec would never be refused.A comparison and a bare number or label read nothing. A bare number tests whether the row is there, and a comparison at a missing row is false unless
missing:names a value. Both sides ofANDare evaluated, so if a comparison counted as a read, the guardwhere: cost AND cost > 0would refuse the rows it is there to skip. Twostrlabels in PyPSA are used only in comparisons, so they are not protected.Two load refusals relied on "a row at every coordinate", so they are removed. This moves a limit: a file may now write both forms.
int/strname in awhereunderrefused;points:declaredrefused. The curve reads no row past its mask.limits.mdnever listed either refusal, so it has no line to change.Readings: refused, absent, or a value
neutralread a missing row as0orfalse. It was named for "contributes nothing", which holds only for an additive term. As a factor it dropped the whole term. As an exponent it gave1. As anoffset=it read the coordinate itself. Its three refusals (divisor, bound, constant side) are now whatrefuseddoes.refused,absentor a value.'neutral'gets the closed-vocabulary error, which names the valid readings.offset=/window=amount,wherecomparison, bare flag, bare number or label) and one column per reading. Underabsent, a missing amount makes theshift/sum_backabsent there.missing: 0parameter can make 0 is refused at load. The check walks the whole divisor: a factor, the numerator of a quotient, the base of a power, the operand of a sum, a window, a shift or a lookup, a named expression, any region of a case, and a sum only where every addend can be 0, soc + 1loads.where: c > 0besidex / cis still refused: the refusal is strict but sound, and its rewrite (missing: absent) works without a guard.absentor any finite number. It needs awhere:, and the number must fit the variable:0/1forbinary, an integer forinteger, and inside any literal bound. A parameter bound is data, so it is not checked. A curve gate is binary, so it takes0(no curve) or1(ungated, which is also whatabsentgives).One reading changes: a
wherecomparison. Underneutral, a comparison at a missing row was false. A value compares as itself, socost < 1becomes true at a missing row undermissing: 0. Every comparison on a migrated parameter inexamples/is> 0, which is false both ways. To keep the old reading, writewhere: cost AND cost < 1.Migration.
examples/,tests/expand/and the golden fixture use0forfloat/intandfalseforbool. The divisor refusal caught the PyPSA module sizes:Generator_p_nom_moddivides in*_maint_modstatus_*. Every arithmetic read of the seven*_nom_modparameters sits inside a*_nom_mod > 0mask, so they keep the defaultrefused. Regenerated: the schema, the golden output, the gallery and notation pages.Program.reads, model_trees and WindowPredicate
Program.readsmaps eachrefusedparameter to aMaskover its dims that holds where the model reads it. A tool evaluates the mask at attach, as it evaluates awhere, and refuses a missing row where it holds. No row needs building first. reading.md documents it with an example the suite runs. On a program that still holds apiecewise:curve,readsraises aLanguageErrorthat namesspec.expand('piecewise'), because the rows of the curve do not exist yet.One list of what goes into the model.
model_treesyields every tree whose value goes into the model, with what holds it: both sides of each constraint, the objective, each bound, each curve link and each term. Lowering marksin_mathfrom it, andreadsstarts its walk from it, matching on the kind withassert_never, so a kind added to the list reaches both. A reported expression and an assumption are not model trees;readswalks them separately.mathspec/_reads.pybuilds the masks with three walks.kbecomes a window ahead byk,sum_backa window ahead of the same width, a lookupat()thesum(by=)that inverts it, andsum(by=)the lookup, both as aJoinwith the columns swapped. A summand also needs its own presence.when, and a walk over eachwhereand each assumption passes it to the flags undercount,shiftandat.WindowPredicatereplacesTranslatedPredicate: the operand holds at some position of a window along a dimension, looking back or ahead, withwrapand a partition. A file's predicateshiftis a window of one position, andoffset=-1loads as "ahead by 1". Onlyprogram.readswrites a wider window, or one that looks ahead by a named offset. Its contract states the normal form:offsetis 0 or more, andaheadcarries the direction;offsetcarries its sign in the data, as onTranslate;A missing named amount therefore makes its operator absent in the masks too: a shift with
edge=and asum_backare present only where the amount has a row. A read through a relation is aJoinof an indicator (Casesof 1 and 0) compared> 0, so a missing map row reads as false.Alternative rejected:
reverseandaheadflags onTranslateandWindowSum. A default-off flag gives one node two meanings and gets around theassert_neverexhaustiveness every walker relies on.Review fixes
A review of
34cc571found three wrong masks, a contract gap on named amounts, a zero-divisor refusal that missed most spellings, and cost. Each bug was reproduced before it was fixed: commitc9a44ef7adds the tests, and on it these fail:a-term-added-to-a-given-sum(12 of 12 draws),a-flag-in-an-assumption(12 of 12), anda-missing-named-offset-that-wraps(11 of 12), a case the review did not name: a wrapped shift by a named offset folded "present" to true, so a missing offset counted as present;test_a_program_that_still_holds_a_curve_says_its_reads_are_not_known;test_a_divisor_that_reads_a_missing_row_as_zero_is_refusedfor-c,2 * c,c / 2,c ** 2,c + c,sum(c),shift(c)and a named expression standing forc.The review's finding that a named offset was not recorded as read was wrong:
amount()already records it.Cost, on
examples/pypsa.yaml, median of 5 runs, not on an idle machine, before and after the fixes on the same tree: computingreadstakes 0.37 s and then 0.26 s.snapshot_weightings_objectivehad 31 parts with 22 distinct, and now has 22. The masks total 3,588,253 characters ofreprand then 3,105,647; the largest is 434,407 characters, becausereprwrites each shared subtree out again. Connectives are flattened before repeated parts are dropped, and the dims of a predicate are cached by identity.Verification
Differential oracle.
tests/test_reads.pyholds an oracle that shares no code with_reads.py. It builds the rows of 32 small models on random data by the absence rules, and records each parameter coordinate that a built row, an existing variable's bound, the objective, a term, a reported expression, an evaluatedwhereor an assumption reads. Each mask is evaluated on the same data and must hold at exactly those coordinates. 32 models × 12 draws = 384 cases. Named offsets and widths are drawn from −1 to 3, so they can be negative, zero, or missing. The data is sparse, so most masks hold on a strict subset. The oracle caught a real bug during the first build: a read throughat()dropped the dimensions of the row beyond the node's frame.Mutation table. Each guard was deleted by hand. The relevant tests were run, the file was restored through
git checkout --with__pycache__dropped on both sides, and the tree came back clean each time. Rows before the review fixes were taken before the merge ofmain, which renamed the module and the node classes but changed no logic.missing: 0parameter-,sum,sum_back,shift/ a named expression / a factor / a numerator / a base / every addend / a case / a lookupisinstance(declared.missing, bool)falseon a numeric parameter is refused. Kept on review request, so thatFalse == 0can never read as a zero divisor…_outside_what_the_variable_takes_is_refused[binary],[integer]…[below-lower],[above-upper]test_a_variable_missing_that_reads_data_is_refused[a-flag],[an-infinity]whereabsentparameter can be missing / a variable read as a number is presentboolname readswhenis read where the case is evaluated / a case below is foundwhere/ through a map / countedwhereis evaluatedSix deletions first survived and each got a model or a test that catches it: a row needs its
where, a variable read as a number is present, only aboolname reads, awhenis read where the case is evaluated, a missing amount makes a filled shift absent, and the zero divisor through a case or a lookup.On the examples.
program.readscomputes for every spec inexamples/; the ones with a curve are expanded first. The fullpypsa.yamlhas 188 masks. Only its twostrlabels are never read.Coverage moved.
neutralcases became0/falsecases.an-int-under-neutral,neutral-on-a-labeland theneutralexclusivity reading are removed, and their value counterparts stay. The bare-name refusal cases became loading cases.…_under_points_is_not_refusedbecame…_may_be_refused. The reading page no longer shows a mask for an unexpanded curve, becausereadsraises there.Gates on
01c80e4:pixi run lintandpixi run testpass (3325 passed, 1 skipped).docs-buildstops only on the strict warning forpathlib.Path, because the container's proxy blocksdocs.python.org.compile-texwas not run.Not verified. The oracle does not cover a partition (
within=), aDualin a reported expression, or given parameters. The masks pass a partition through unchanged, so their correctness there depends onWindowPredicateandJoinmeaning in an engine what they mean on aTranslate.Not done
WindowPredicateorprogram.readsyet. specsolve and its linopy backend need their own PRs: the rename, the wider window, evaluating the masks at attach, and the value forms ofmissing:.x / shift(p, along=t, offset=1, edge=0)divides by theedge=value 0 at the edge whatevermissing:says. A check at load could catch it; it is a separate bug.refusedstill checks every key combination when the data is attached.missing: <parameter>on a variable. A named expression withcases:already says "the variable where the mask holds, the data elsewhere".∃.neutral. They are history.🤖 Generated with Claude Code
https://claude.ai/code/session_01AvtZGymCbY9swLnR8JwHng