Skip to content

feat(api): a parameter value on a where: threshold gets the rate at which the objective moves past it - #2074

Closed
FBumann wants to merge 7 commits into
claude/specsolve-feature-differentiation-w3yt75from
claude/specsolve-marginal-thresholds-w3yt75
Closed

FBumann wants to merge 7 commits into
claude/specsolve-feature-differentiation-w3yt75from
claude/specsolve-marginal-thresholds-w3yt75

Conversation

@FBumann

@FBumann FBumann commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

Prompt: We need to not resolve for such a feature!! What can we achieve without it ?

Prompt: No, no solve for now

Note

The following content was generated by AI.

parameter_dual now prices a value on a where: threshold by what its rise switches on or off, from the same solve and with no re-solve. A jump is ±inf. What one solve cannot decide keeps the rate as built, and a warning names it.

result.parameter_dual('p_max')     # dispatch.yaml, solar at p_max = 0 under `where: p_max > 0`
# wind   -120.0
# solar   -75.0   # was 0.0: its first unit replaces gas in three snapshots, 25 cheaper each
# gas       0.0
What each threshold reports, how, verification, what is left out

What a rise past a threshold reports

A rise… Example (tests/test_parameter_dual_thresholds.py) Reported
switches on a column whose bound is the value solar, upper: p_max its reduced cost: -75
the same, maximized -sum(p * cost) +75
switches on a column capped by a zero-slack row that came with it p <= avail * p_max, solar avail = 0.4 reduced cost × 0.4: -30
the same cap, written with a coefficient 2 * p <= 2 * avail * p_max -30
switches on a zero-slack row that forces a column up p >= p_min under where: p_min > 0, gas gas's reduced cost: 40
switches on a row a running column already meets the same row, wind 0
switches on a column with a fixed bound it would use solar, upper: 100 -inf
switches on a row the solve violates p >= 10 under where: p_min > 0, gas +inf
switches off a row with a dual p <= 30 under where: tight <= 0, wind -inf
switches off a column the solve uses where: retire <= 0, gas +inf
switches on a floor that rises faster than the column's cap p >= ramp * p_max, solar ramp = 2 +inf
switches on a zero-slack row across several columns a pooled row as built, warned
crosses a mask that reads it through a sum, shift, lookup or count where: … and sum(p_min, over=generator) <= 0 as built, warned
crosses a when: cases: when: p_min > 0 as built, warned
switches rows of a quadratic constraint p * p <= 1e6 under where: p_min > 0 (gurobi, QCPDual) as built, warned

Signs mirror under maximize. A bool parameter has no rise, so nothing crosses. Each finite case matches a forward re-solve at rel 1e-5. Each infinite case is checked by a forward re-solve that moves the objective the same way by more than 1e3 times the step, or loses its solution.

How

  • Find the crossings. _risen raises every value of the parameter by 1e-9 × max(1, |v|). The masks that read it at their own coordinate are evaluated again, and the coordinates that change are the crossings. Each crossing is charged to the parameter row its coordinate projects onto.
  • Price the new columns. A switched-on column gets a label past the solve's last, primal 0. It is priced by compiling with that variable varied over the grown frame, the same pass that gives a bound's reduced cost in feat(api): a result gives the rate at which its objective moves with each value of a parameter, also where a where: condition switches at that value #2073.
  • Evaluate the new rows. They are compiled as a build with the parameter varied too. Its labels are now negative (varied_label), so variables and parameter rows share one compile without colliding, and a variable × parameter product is a quadratic term. One pass gives each row's slack at the solve, its columns, and its rate in each parameter row.
  • Decide each column. It moves the way its reduced cost prices as a gain, as far as the tightest zero-distance cap allows: its own bounds, at rate 1 where they are the charged row, and the zero-slack single-column rows. Its rate is the reduced cost times that move. If no cap stops it that way, or a cap would push it past another, the rate is a jump.
  • No solve, no full build. Only the declarations whose mask reads the parameter, or that read a variable that flipped, are evaluated again.

The parameter dual moves from _engine/readback.py to _engine/parameter_dual.py, and the architecture table follows. Result.parameter_dual raises the notes as SpecsolveWarning. The how-to What a change is worth, from #2073, now prices solar at -75, and says what an infinite rate and a warning mean.

The reader was marginal when this PR opened. #2073 renamed it parameter_dual (the reason is in #2073), and 3dc9195 carries the rename here, to the module and the test files too.

Verified

Not run: test-via-pyarrow, test-floors, test-bench.

Mutation table

Taken before the rename, so the paths read marginal.py, now parameter_dual.py. Each guard was deleted in turn, and the full suite was run each time. The rows marked tool come from tools/mutate.py. The rows marked hand replace a branch or a constant the tool cannot express, with the same precautions (git checkout -- restore, __pycache__ dropped on both sides). The tree came back clean after every run.

The first run left six survivors, and one range the tool could not apply. Each got a probe in 3c7d44f, and the second run, on 3c7d44f, caught them all.

Guard How First run (54fe6ff) On 3c7d44f
a parameter that is not a number crosses nothing tool green caught
a used column switched off is a jump tool caught —
a when: on a threshold is named tool green caught
a quadratic row is held as built tool green caught
a row with a dual switched off is a jump hand not applied by the tool caught
a violated row switched on is a jump tool caught —
a zero-slack row over several columns is held as built tool caught —
a mask that reads through a sum is not charged hand green caught
no rise: nothing crosses (_RISE = 0) hand caught —
a column that gains by rising is priced hand caught —
a column that gains by falling is priced hand caught —
a cap that crosses its push is a jump hand green caught
a cap is divided by its coefficient hand green caught

Left out

  • Coupled zero-slack rows. A switched-on row at zero slack across several columns needs a small LP over the crossed columns. It stays as built and warned, by decision: no solve for now.
  • The size of a jump. It is not given; the warning names the rows.
  • when: flips, and masks read through a shift, sum, lookup or count. These stay as built and warned, because a flip cannot be charged to one parameter row cheaply there.
  • Quadratic constraints whose rows a rise switches. These stay as built and warned.
  • Cost not measured. The threshold pass runs only when a mask reads the parameter. Its cost on the published ladder is not measured.

Coverage that moved

test_a_value_a_mask_reads_is_held_where_the_mask_put_it, from #2073's tests/test_parameter_dual.py, asserted the held-as-built 0. tests/test_parameter_dual_thresholds.py replaces it.

🤖 Generated with Claude Code

https://claude.ai/code/session_018B3nA1Q4XLWBq2jJFFLFWR

…hich the objective moves past it

A marginal no longer holds every mask as built. A value whose rise switches
columns or rows on or off is priced from the same solve: a switched-on column
at its reduced cost, as far as its bounds and the zero-slack rows that come
with it allow; a jump as an infinite rate. What one solve cannot decide keeps
the rate as built and is named in a warning.

The marginal moves from readback into its own module.

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

coderabbitai Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 59 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 266d0586-fa8c-42ad-bb16-5730899f22d5
Commits

Reviewing files that changed from the base of the PR and between a248849 and 0c043e9.

Files selected for processing: 12

  • CHANGELOG.md
  • docs/development/architecture.md
  • docs/howto/prices.md
  • src/specsolve/_api.py
  • src/specsolve/_archive.py
  • src/specsolve/_engine/compiler.py
  • src/specsolve/_engine/engine.py
  • src/specsolve/_engine/parameter_dual.py
  • src/specsolve/_engine/readback.py
  • src/specsolve/_result.py
  • tests/test_parameter_dual.py
  • tests/test_parameter_dual_thresholds.py
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@read-the-docs-community

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

Copy link
Copy Markdown

@codspeed

codspeed Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 104 untouched benchmarks
⏩ 307 skipped benchmarks1


Comparing claude/specsolve-marginal-thresholds-w3yt75 (0c043e9) with claude/specsolve-feature-differentiation-w3yt75 (a248849)

Open in CodSpeed

Footnotes

  1. 307 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

…ts pull request

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

The tutorial section this branch rewrote has left for the how-to, which now
prices solar at -75 and says what an infinite rate and a warning mean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018B3nA1Q4XLWBq2jJFFLFWR
The threshold module and its tests follow the name: _engine/parameter_dual.py
and tests/test_parameter_dual_thresholds.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018B3nA1Q4XLWBq2jJFFLFWR
@FBumann
FBumann added this pull request to stack #2077 November 9, 2026 16:13
@FBumann FBumann added the feature New capability label Nov 11, 2026 — with Claude
@FBumann FBumann closed this Nov 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New capability

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants