Skip to content

feat!: specsolve requires mathspec 0.4.0, where missing: neutral is gone and a file names the value that a missing row or a masked variable reads as - #2054

Draft
FBumann wants to merge 4 commits into
mainfrom
claude/bold-wozniak-jewqij
Draft

FBumann wants to merge 4 commits into
mainfrom
claude/bold-wozniak-jewqij

Conversation

@FBumann

@FBumann FBumann commented Oct 18, 2026

Copy link
Copy Markdown
Collaborator

Prompt: ANd do the specsolve PR!

Note

The following content was generated by AI.

specsolve builds the files of mathspec 0.4.0 (energy-models/mathspec#894): neutral is gone, a parameter reads a missing row as the value it names, and a masked variable stands as its missing: number. The PR waits for mathspec 0.4.0 to be released.

What a missing row reads as
  • A missing: 0 or missing: false table stays as the data gave it, as a neutral one did (Scope.reads_zero). A term it multiplies is no term, and a row left with no term is not built. The zero is read where a position reads the value itself:
    • a bound reads 0 (Compiler.bounds);
    • a constant side reads 0, so the gap is not refused (coverage.constant_side);
    • a where comparison compares 0 (predicates, which also stops counting such a name as one the mask is certain of);
    • a comparison of expressions reads 0 for a piece with such a factor (Compiler.summed_onto).
  • Any other value fills the table when the data is attached, as before.
  • A masked variable with a missing: number compiles to its term plus a constant piece: the number where the mask leaves the variable out, and 0 where it exists (Compiler._standing_in). A read fills the number where missing: 0 filled zero.
  • The linopy lane fills a variable's number with fillna(missing). A bare name of a parameter with no dims now asks whether the data has the row, as it already did for a parameter with dims (given_rows).
The port to mathspec 0.4.0
  • mathspec renamed the program classes (energy-models/mathspec#855). ParameterDeclaration is Parameter, and the old Parameter is ParameterReference. Variable and VariableDeclaration follow the same pattern. NamedExpression is ExpressionReference, NamedMask is MaskReference, and each other *Declaration drops the suffix. One mapping applied all of them at once, so no name was renamed twice.
  • mathspec.validation is mathspec._validation (energy-models/mathspec#878), for the one test that spies on to_spec.
  • Every model file, fixture and test that wrote missing: neutral writes 0, or false on a bool parameter. The PyPSA rungs declare *_nom_mod missing: absent, as mathspec's own PyPSA file does: mathspec refuses a missing: 0 divisor at load, and Generator_p_nom_mod divides in *_maint_modstatus_*.
  • The ladder, gallery and construct pages are regenerated. The walkthrough golden file shows the new class names and nothing else.
  • pyproject.toml pins mathspec>=0.4.0,<0.5, and the floors environment pins ==0.4.0.
Tests changed, and where their coverage moved
  • Removed, because mathspec now refuses the model at load: the tests of a sparse missing: 0 divisor refused at data time, through a power, an addition, the objective, a constant side, a sum, a case region and a reported expression (test_arithmetic_laws.py, test_cases.py, test_expression_reader.py). mathspec's test_a_divisor_that_reads_a_missing_row_as_zero_is_refused holds that refusal now.
  • Rewritten, because the position now reads the value: a sparse constant side, a constant piece beside a term, and a hole inside a case region (test_absence.py, test_cases.py) now assert the objective on both lanes. The build-time bound refusals in test_relational.py and test_linopy_lane.py are replaced by test_a_missing_bound_reads_its_value_on_both_lanes.
  • The escape for a sparse divisor is missing: absent: test_an_absent_divisor_takes_its_row_with_it.
  • Tests that needed a reported expression to fail read dual(...) on an integer model instead of dividing by a sparse parameter (test_api.py, test_strategy.py).
  • New, on both lanes: a missing bound, a where comparison of a parameter and of an expression, a masked variable that stands as its number in a row, and the same variable read at a point.
Verification

pixi run cannot solve the environment until mathspec 0.4.0 is on PyPI, so each gate ran with the environment's own Python, with the mathspec of energy-models/mathspec#894 at 20fc215 installed into it. On 3011a4c:

  • ruff check . and ruff format --check . pass.
  • pyrefly check and the linopy references' pyrefly check report 0 errors.
  • pytest -n auto: 5750 passed, 574 skipped, 4 xfailed. main with mathspec 0.3.0 gives 5760 passed, 574 skipped, 4 xfailed: this PR removes 18 tests and adds 8.
  • pytest bench/test_harness.py: 482 passed, 15 skipped, run on an idle machine.

Mutation table. A script deleted each guard and ran the full suite. It restored the file with git checkout --, dropped __pycache__ before and after each run, and the tree came back clean. Most guards are inline expressions that tools/mutate.py cannot delete line by line, so the script mutates by text instead. The first eight rows were taken on 0811ae7; the last three rows first stayed green, got the tests of 3011a4c, and were taken again on 3011a4c.

Guard deleted Caught by
attach keeps a zero table sparse 7, among them test_a_sparse_coefficient_is_still_a_zero_coefficient and test_rows_a_restriction_removed_and_rows_with_no_terms_are_counted_together
a bound reads the zero test_a_missing_bound_reads_its_value_on_both_lanes
a constant side owes a zero piece nothing test_a_missing_constant_reads_as_its_value_on_both_lanes, 3 cases
a comparison reads the zero test_a_comparison_reads_the_value_of_a_missing_row_on_both_lanes[true-at-the-zero]
a zero comparison is not certain the same
a masked variable stands as its number test_a_masked_variable_stands_as_its_missing_number_on_both_lanes
linopy: a bare scalar asks for its row test_a_dimensionless_parameter_with_no_row_switches_off_what_it_masks
linopy: a term stands as the number test_a_masked_variable_stands_as_its_missing_number_on_both_lanes
a comparison of expressions reads the zero test_a_comparison_reads_the_value_of_a_missing_row_on_both_lanes[an-expression-true-at-the-zero]
a read fills the number, not 0 test_a_read_values_a_masked_variable_at_its_missing_number_on_both_lanes
linopy: a read fills the number the same

Departures from AGENTS.md. The gates ran without pixi run, as above. The mutation table came from a script rather than tools/mutate.py. The renames of #855 and #878 ride in this PR, because specsolve does not import mathspec 0.4.0 without them.

Not done
  • uv.lock still resolves mathspec 0.3.0. Refreshing it needs 0.4.0 on PyPI, so CI fails to resolve until the release. That is why this PR is a draft.
  • The data-time refusals of a null bound, an uncovered constant side and a sparse divisor look unreachable now, because every declared reading answers before the build. They are left for a separate PR that deletes each one with its own mutation evidence.
  • A row whose every coefficient is zero because the data writes 0.0 is built by the relational lane and dropped by the linopy lane. That is true on main too, and it is a separate issue.
  • attaching._filled joins without maintain_order, so a parameter filled with a value can sum in a different order between two builds. With the missing: 0 tables kept sparse, no test here reaches it. It is a separate fix.

Generated by Claude Code

… gone and a file names the value that a missing row or a masked variable reads as

A parameter under `missing: 0` or `missing: false` keeps its table as the
data gave it, as `neutral` did, and reads the zero in a bound, on a
constant side and in a comparison. A masked variable stands as its
`missing:` number outside the mask. The program classes follow
mathspec's renames.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2crTbivTGt2WzGpTebdmf
mathspec now gives a given parameter a class of its own, and specsolve
refuses a program that reads one anyway.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2crTbivTGt2WzGpTebdmf
…e the value of a missing row

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

This branch has not been deployed

No deployments
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