Skip to content

feat(language): a decided variable becomes a supplied number, so a subproblem is a call rather than a second file - #249

Closed
FBumann wants to merge 3 commits into
claude/parameter-coveragefrom
claude/fix-a-decision
Closed

FBumann wants to merge 3 commits into
claude/parameter-coveragefrom
claude/fix-a-decision

Conversation

@FBumann

@FBumann FBumann commented Aug 29, 2026 •

Copy link
Copy Markdown
Contributor

Prompt: "I really like both 2 AND 3 … Keep it simple and encapsulated, like the rest of the codebase"

Note

The following content was generated by AI.

What this changes

subproblem = fix(model, 'cap')            # `cap` is now a number somebody else chose
myopic     = fix(model, *already_built)   # a myopic step fixes many at once

A myopic pathway, a rolling horizon and a Benders subproblem share one move: a variable stops being a decision and becomes a supplied number. docs/about/decomposition.md says it outright — "that single change is the whole of the decomposition" — and written by hand it is a second file to keep in step with the first.

The name does not move, so every expression naming it goes on reading. Every name is given in one call and the model is revalidated once at the end of it, which is the shape the motivating workflows actually have: a myopic step fixes what earlier periods built, which is many.

Why it is not four lines in a driver

I originally proposed this as a Spec → Spec toolkit above, and demonstrated a four-line version. Testing it changed the answer: the four-line version is wrong twice, and once silently.

naive fix drops the mask. what did 'cap' become?
   dims=['g'] dtype='float' coverage='total'

the variable it replaced was masked: p_max > 0
so rows where p_max == 0 have NO value — but the parameter claims coverage: total

and a fixed binary became dtype: float — a float, where the values are 0/1

Two drivers writing their own would each decide these, and differently. By "would two consumers answering separately be a bug?" that is one home, and the home is here.

Where the line falls

fix follows; it does not decide. That is why it is in the language and elastic relaxation is not: where slacks go and what they cost is a modelling choice, closer to a formulation than to a rename.

It is also not every half of a decomposition, which my first docstring overclaimed and the tests corrected. A Benders master has the dispatch gone, not fixed. Fixing every variable a constraint names leaves a row that decides nothing — and because fix revalidates, the language says so at the call:

Constraint 'k': neither side of the comparison carries a variable, so the row decides nothing.

Bounds are dropped, the one thing lost: they constrained a decision the model no longer makes, and whether the supplied numbers respect them is a question about data.

What lpspec removes or changes

Nothing. No verb changes and nothing binds differently.

What it gains is that examples/benders/, examples/myopic/ and examples/rolling/ currently ship a hand-written variant file each — sub.yaml beside monolith.yaml — and the subproblem ones become fix(monolith, 'cap'). That is a real reduction in files kept in step by hand, and it is the drift docs/about/decomposition.md warns about.

The masters stay files, because a master is the other move.

Verified

Toolchain at the versions pixi.toml pins: ruff format --check / ruff check (0.16.1) clean, pyrefly (1.2.0) 0 errors, pytest -q 915 passed (905 on the base — 10 new). reuse, typos, prettier and mkdocs build --strict were run on the top of the stack (#250), which contains this commit.

Not run: zizmor, taplo, and the tectonic half of compile-tex. mkdocs build --strict needed docs.python.org/objects.inv dropped from mkdocs.yml, which this environment's proxy refuses; that affects cross-links into CPython's docs and nothing in this diff.

Mutation table

Mutation Result
a mask does not become a coverage claim 1 failed
every domain becomes a float 2 failed
the rewritten model is not revalidated (model_construct) 7 failed
restored all green

The third needed care: my first attempt at it was a no-op that passed the whole suite, which would have recorded a catch that did not happen. tools/mutate.py cannot express "skip validation", so it was taken by hand through Spec.model_construct.

Deliberately not done

No relax. It belongs above, per the line drawn earlier — and shipping it here would blur the distinction this PR's argument rests on.

The description is carried, the bounds are not. A description describes the quantity, which the fix does not change; bounds describe a decision that is gone.

A name given twice raises rather than deduplicating. fix(m, 'cap', 'cap') refuses on the second, because after the first it genuinely is not a variable any more.

Changed in review. An earlier version of this description defended taking one name per call, on the grounds that a variadic form was an option nobody had asked for. Reading it back against the workflows the module docstring names, that was wrong the other way round — a myopic step fixes many, and composing calls costs a full revalidation each. fix(model, *names) is what the PR now ships; fix(fix(m, 'a'), 'b') still composes and is asserted to agree with fix(m, 'a', 'b').

Stack: eighth, on #248 → #247 → #246 → #245 → #244 → #243 → #242 → #168.


Generated by Claude Code

Closes #303

claude and others added 3 commits August 31, 2026 20:13
…o whichever engine reads the data

Three facts decide which model a file and a table make together — where a
dimension's members come from, the order they stand in, and that a coordinate
appears at most once. Each was real and each was written down in a consumer, so
a second consumer would have inherited none of them.

Co-Authored-By: Claude <noreply@anthropic.com>
(cherry picked from commit 1e2b8a9)
…oordinate, so a row lost in preparation is not read as a mask

A table short of a coordinate and a table that never had one are identical in
the data and mean opposite things. `coverage:` says which was meant, and a
parameter declaring itself a mask is refused as a bound or a divisor — the two
positions where absence has no reading — before any data arrives.

Co-Authored-By: Claude <noreply@anthropic.com>
(cherry picked from commit 9a8045c)
…bproblem is a call rather than a second file

A myopic pathway, a rolling horizon and a Benders subproblem share one move: a
variable stops being a decision and becomes a number somebody else chose. fix
takes every name in one call and validates once at the end. Two translations are
decisions, not copies: a where-masked variable becomes coverage: masked, and a
binary or integer variable becomes an int parameter — never a float, never a
bool that would read as a mask.

Rebased onto the parameter-coverage branch (#243) it needs for Coverage, off the
closed #247/#248 stack it was authored on; only the fix additions are kept.

Co-Authored-By: Claude <noreply@anthropic.com>
@FBumann
FBumann force-pushed the claude/separable-along branch from 5918b28 to b207667 Compare August 31, 2026 18:29
@FBumann
FBumann force-pushed the claude/fix-a-decision branch from 163000f to 3348d70 Compare August 31, 2026 18:29
@FBumann
FBumann changed the base branch from claude/separable-along to claude/parameter-coverage August 31, 2026 18:29
@FBumann
FBumann force-pushed the claude/parameter-coverage branch 2 times, most recently from 200b831 to 4ada6e5 Compare September 2, 2026 08:44
@FBumann FBumann added the area: composition Assembling a model from files, and fixing a decision label Sep 15, 2026 — with Claude

FBumann commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Note

The following content was generated by AI.

Closed at the author's request, in a sweep of the open PRs. Closed rather than retargeted, for the same reason as #244: the base branch never landed.

#303 stays open. The finding this PR is built on is the durable part, and it is not in the diff: the four-line version in a driver is wrong twice, silently — a masked variable becomes a total parameter that claims a number everywhere, and a binary variable becomes a float. That is the argument for fix living in the language, and it should be re-made against current main.


Generated by Claude Code

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