Skip to content

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

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

FBumann wants to merge 5 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 #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

  • On fb3d219: pixi run check 5910 passed, 579 skipped, 4 xfailed. docs-build (strict): no issues. docs-test: 116 passed.
  • On 864d079, after merging feat(api): a result gives the rate at which its objective moves with each value of a parameter #2073's how-to: pixi run check 5911 passed. docs-build (strict), docs-test (116 passed), and test_doc_examples, test_docs_site and test_changelog (162 passed) are clean.
  • On 3dc9195, after merging the rename: pixi run check 5927 passed, 579 skipped, 4 xfailed. docs-build (strict): no issues. docs-test: 116 passed. The rendered how-to prints solar at -75.

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

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

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: 72e4c966-79c3-4c97-b216-1516fb2db457

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@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 degrade performance by 67.75%

1 warning

⚠️ Base and head ran on different runner types

Some benchmarks with significant performance changes were compared across different runner types, so the changes might not come from the code. Run base and head on the same runner type, or use CodSpeed Macro Runners for more stable results.

Learn more about Macro Runners

Open the report in CodSpeed to investigate

❌ 1 regressed benchmark
✅ 103 untouched benchmarks
⏩ 307 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
❌ test_read[commitment-s-specsolve-parquet] 220.7 KB 684.3 KB -67.75%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing claude/specsolve-marginal-thresholds-w3yt75 (3dc9195) with claude/specsolve-feature-differentiation-w3yt75 (a861e06)

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

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