Skip to content

feat(spec): a decided variable becomes a supplied number in one call, so a subproblem is not a second file - #824

Open
FBumann wants to merge 8 commits into
claude/nice-goldberg-tum8xcfrom
claude/quirky-newton-fm9fev
Open

FBumann wants to merge 8 commits into
claude/nice-goldberg-tum8xcfrom
claude/quirky-newton-fm9fev

Conversation

@FBumann

@FBumann FBumann commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Prompt: "Lets tackle the remaining issues in https://github.com/energy-models/mathspec/milestone/12"

Then, for #303: "Port #748 onto main (Recommended)"

Then: "Review 824. Any simplifications or refactorings? Is the wording good? Is it placed in a how to?", and "Yes" to applying the findings.

Then: "Should we rebase 824 onto this PR?" (#810), "I think so!!", and "i removed the stack. Retarget both and rebase them."

Note

The following content was generated by AI.

What this changes

spec.fix(*names) turns each named variable into a parameter with the same name, and every expression goes on reading it. A Benders subproblem, a myopic step or a rolling window is then a call on the whole model, not a second file. The parameter takes the variable's missing:, so no row changes. Closes #303.

Stacked on #810, rebased onto its branch. GitHub moves the base to main when #810 merges. #826 is stacked on this PR.

Rules, the stack, what moved, guards, gates, what was not done

The rewrite

  • The variable becomes a parameter over the same dims. binary and integer become int, and continuous becomes float. The description is kept.
  • The parameter takes the variable's missing:. A masked variable that is absent outside its mask gives a parameter that is missing: absent: a row that reads it there is still not built, and a sum has one summand fewer. A neutral variable gives a neutral parameter, which still reads 0. A variable with no where: existed everywhere, so its parameter keeps the default, error.
  • Its bounds, and a binary's 0 and 1, become the assumption <name>_within_bounds, under the variable's where:. Each side is written once.
  • A constraint that named only fixed variables becomes an assumption under its own name, with its where: and description.
  • A set or a curve that names the variable refuses the fix. spec.expand().fix(...) works.
  • All names are checked before the first one is rewritten. The result is validated once. fix('a').fix('b') equals fix('a', 'b').
  • A variable that a fragment reads under given: variables: is refused, and the message tells you to fix it on the merged spec.

What the stack removed

Before #810, a parameter read 0 where the variable had been absent, so fix had to prove every read outside a sum was guarded, add the variable's mask to a row's where:, or refuse. With missing: absent every read keeps its meaning, so these are gone:

  • _unguarded_reads, _spreading_reads and _guards, and the mask added to a row's where:;
  • the refusal of a read in a case, and step 4 of the how-to;
  • fixing.py is 147 lines, down from 250.

A read through a shift is still refused, now by the language's rule on a shift over an expression with no variable (it needs edge=), when the fixed spec reloads. Whether that rule should let an absent parameter through is a question for limits.md, not this PR.

Where the tests' coverage moved

Test before the stack Now
test_an_unguarded_read_takes_the_mask_into_the_row, test_the_mask_joins_a_where_the_row_already_has, test_a_guarded_read_leaves_the_row_as_written, test_a_case_under_the_mask_is_guarded, test_a_read_the_mask_cannot_reach_is_refused[in-a-case…] test_every_read_keeps_its_meaning_and_no_row_changes, over five readers: pointwise, under another mask, under its own mask, inside a sum, in a case
test_a_read_the_mask_cannot_reach_is_refused[through-a-shift] test_a_read_through_a_shift_is_refused_by_the_language
test_two_masks_added_to_one_row_are_both_kept test_fixing_two_masked_variables_together_is_fixing_them_one_at_a_time
— test_the_parameter_reads_a_missing_row_as_the_variable_read_its_mask: error, absent, neutral
test_the_pypsa_capacities_are_guarded_where_they_are_read test_the_pypsa_capacities_are_fixed_without_changing_a_row

Guards

Guard deleted tests/test_fixing.py
the parameter takes the variable's missing: 8 failed

The guards for the rules that stayed (dtype, bounds written once, settled rows, set, curve, a name given twice, a given variable, a taken assumption name) were measured before the stack, and not re-measured.

Gates

On 87af38e, whose tree is the one tested before the rebase:

  • pixi run lint: pass.
  • pixi run test: 2692 passed.
  • docs-build: the only warning is pathlib.Path, which needs docs.python.org/3/objects.inv; the proxy here refuses it.
  • compile-tex: not run. tectonic cannot download its bundle here. The diff changes no typeset output.

The rebase replays the four commits onto #810, so the first three still read the old variable.absence, and only the last one passes. Pushed with --force-with-lease, at your instruction, against the AGENTS.md rule.

Not done

Why

#303: a fix that a driver writes by hand is wrong in two ways, and nothing reports either. A masked variable reads 0 where it did not exist, and a binary becomes a float. Two drivers would also each decide these rules differently.

@FBumann
FBumann requested a review from brynpickering as a code owner October 1, 2026 15:42
FBumann pushed a commit that referenced this pull request Oct 1, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjTvqu5LubaVCgWeKogCqj
@read-the-docs-community

read-the-docs-community Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

@FBumann FBumann changed the title feat(spec): spec.fix turns a decided variable into a supplied number, so a subproblem is a call on the whole model feat(spec): a decided variable becomes a supplied number in one call, so a subproblem is not a second file Oct 1, 2026
@FBumann
FBumann added this pull request to stack #827 October 1, 2026 16:49
@FBumann FBumann added the area: composition Assembling a model from files, and fixing a decision label Oct 2, 2026
@FBumann
FBumann removed this pull request from stack #827 October 2, 2026 09:05
@FBumann
FBumann changed the base branch from main to claude/nice-goldberg-tum8xc October 2, 2026 09:06
claude added 4 commits October 2, 2026 09:07
… so a subproblem is a call on the whole model

Each named variable becomes a parameter under the same name: binary and
integer become int, continuous becomes float. Its bounds become the
assumption <name>_within_bounds, and a constraint that named only fixed
variables becomes an assumption. A masked variable with absence:
undefined keeps its meaning: each read outside a sum is proven guarded,
masked at its row, or refused with the reader named.

Ported from #748 onto main, without its base (#746, #747). A given
variable is refused with a message that names it as given.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjTvqu5LubaVCgWeKogCqj
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjTvqu5LubaVCgWeKogCqj
The second variable's rewrite read the row's where: from the spec, not
from the rewrite in progress, so it replaced the mask the first one
added. Every name is now checked before the first one is rewritten, and
the rewrite reads only what it is writing. A binary whose bounds restate
0 and 1 writes each side of its assumption once. The how-to gives
spec.fix as steps, with the call, its result and a refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjTvqu5LubaVCgWeKogCqj
…o row is rewritten and no read is refused

The parameter is `missing: absent` where the variable was masked and
absent, `neutral` where it was neutral, and keeps the default `error`
where the variable had no `where:`. Every read means what it meant, so
the proof that a read stands under the mask, the mask added to a row's
`where:`, and the refusal of a read in a case are gone. A read through a
shift is still refused, by the language's rule on a shift over an
expression with no variable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiDTE4WvKobVxTDxUeWhc9
@FBumann
FBumann force-pushed the claude/quirky-newton-fm9fev branch from 81b7c36 to 87af38e Compare October 2, 2026 09:08
@FBumann
FBumann added this pull request to stack #832 October 2, 2026 09:11
claude added 4 commits October 2, 2026 09:35
…o claude/quirky-newton-fm9fev

# Conflicts:
#	CHANGELOG.md
…ding, now called refused

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiDTE4WvKobVxTDxUeWhc9
…o claude/quirky-newton-fm9fev

# Conflicts:
#	CHANGELOG.md

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

area: composition Assembling a model from files, and fixing a decision

Projects

None yet

Development

Successfully merging this pull request may close these issues.

a decided variable has no verb turning it into a supplied number, so every decomposition driver writes its own

2 participants