Skip to content

feat: a spec reports how its optimum moves with a parameter, which for a fixed decision is a Benders cut - #749

Closed
FBumann wants to merge 11 commits into
claude/mathspec-analysis-features-zy7x4n-fixfrom
claude/mathspec-analysis-features-zy7x4n-sensitivity
Closed

FBumann wants to merge 11 commits into
claude/mathspec-analysis-features-zy7x4n-fixfrom
claude/mathspec-analysis-features-zy7x4n-sensitivity

Conversation

@FBumann

@FBumann FBumann commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Prompt: Start with #303 and the cut formula
Draft PRs, stacked

Note

The following content was generated by AI.

What this changes

spec.sensitivity(*names) reports <name>_sensitivity, the rise of the optimal objective per unit of a parameter, written over dual(). It depends on #751, which defines dual(c) as the rate in the right side of c. Stacked on #748.

The dual it reads, evidence, rules, gates

The dual it reads

The formula is Σ_c dual(c) · ∂(rhs − lhs)/∂q, plus the objective's own ∂f/∂q. It is one expression whatever the comparator and the sense, because #751 defines dual(c) as the rate at which the optimal objective rises with the right side of c. That is what every specsolve sink returns; fluxopt/specsolve#1774 pins it. This PR carries the same named.md hunk as #751. Once #751 merges, the hunk is already on main.

Evidence: finite differences on HiGHS

A scratch venv with this branch and specsolve main. The model is specsolve's Benders monolith, fix('cap'), then sensitivity('cap', 'load'), at cap = [60, 90], with a step of 1e-3:

derived finite difference
cap[wind] 67.5 67.5
cap[gas] 30.0 30.0
load[0..3] 0, 25, 25, 25 0, 25, 25, 25
cap, balance as == 67.5, 30.0 67.5, 30.0
cap, under maximize of the negated cost −67.5, −30.0 −67.5, −30.0

Under the old wording of dual(c) the derivation writes invest - sum(dual(capacity) * avail, …), which gives 112.5 for cap[wind] against a true 67.5.

The derived expressions:

cap_sensitivity  = invest + sum(dual(capacity) * avail, over=snapshot)
load_sensitivity = dual(balance)

What it carries, and what it refuses

  • It carries arithmetic, sum(over=) and named expressions, from the row down to the parameter. On the way it multiplies by each factor, and at the parameter it sums back over every dimension the parameter does not carry.
  • It refuses, naming the operator: shift, at, sum(by=), sum_back or a case between the row and the parameter; the parameter as a divisor; a sum over a dimension the factor outside it also carries.
  • It refuses a parameter that bounds a variable, because that price is a reduced cost, and no expression reads one.
  • It refuses a row with a where: that reads the parameter pointwise, because the absent dual would take the whole sum with it (a parameter cannot say that a missing row is absent, so a decision supplied as data brings back the rows its mask deleted #750 names the reverse case).
  • where: masks and assumptions that read the parameter are steps the rate does not see. The docstring says so.

Guards

Guard deleted Fails
the left side enters negated on-the-left, divided
a negation flips the seed the same two
sum back over the dims the parameter lacks 10
the objective's own rate 9
a bound refuses a-bound
a masked pointwise row refuses a-masked-row-read-pointwise
a sum over a carried dim refuses a-sum-over-a-dim-the-factor-outside-carries
a divisor refuses a-divisor

Gates

  • pixi run lint: pass.
  • pixi run test: 1834 passed.
  • The finite-difference check is a scratch script. mathspec has no solver dependency, so it is not in the suite.
  • docs-build and compile-tex were not run in the session (network). CI runs both.

Not done

  • shift, at, sum(by=) and cases on the path. The transpose of each is an operator the language has (sum(by=) is the adjoint of at), except sum_back, whose adjoint would be sum_forward #49's sum_forward.
  • A variable that multiplies the parameter makes the formula read that variable's solved value. This is allowed, and not checked numerically.

Why

A Benders loop needs the cut slope, and a modeller reads the marginal value of capacity or load. Both are this derivative, and a driver that writes it by hand has to get the dual's sign right; the evidence above shows how easily that goes wrong.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CvcU9JRnsinc9e9syvVcHS

…r a fixed decision is a Benders cut

Spec.sensitivity(*names) adds a reported expression <name>_sensitivity for
each named parameter: the objective's own rate in it, plus dual(c) times the
rate of rhs - lhs for each row that reads it.

named.md now defines dual(c) as the rate at which the optimal objective rises
with the right side of c. That is what specsolve returns. It gives an equality
a sign, and it makes the rate the same expression under either sense. The old
wording made a <= dual non-negative under minimize, which specsolve does not
return.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvcU9JRnsinc9e9syvVcHS
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvcU9JRnsinc9e9syvVcHS
…ec-analysis-features-zy7x4n-sensitivity

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvcU9JRnsinc9e9syvVcHS
…ec-analysis-features-zy7x4n-sensitivity

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvcU9JRnsinc9e9syvVcHS
…ec-analysis-features-zy7x4n-sensitivity

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvcU9JRnsinc9e9syvVcHS
…ec-analysis-features-zy7x4n-sensitivity

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvcU9JRnsinc9e9syvVcHS
…ec-analysis-features-zy7x4n-sensitivity

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvcU9JRnsinc9e9syvVcHS
…ec-analysis-features-zy7x4n-sensitivity

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvcU9JRnsinc9e9syvVcHS
…ec-analysis-features-zy7x4n-sensitivity

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvcU9JRnsinc9e9syvVcHS
…ec-analysis-features-zy7x4n-sensitivity

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvcU9JRnsinc9e9syvVcHS
@FBumann FBumann added area: composition Assembling a model from files, and fixing a decision enhancement labels Sep 29, 2026 — with Claude
…ec-analysis-features-zy7x4n-sensitivity

named.md takes the dual definition as #751 merged it, and keeps the
paragraph on spec.sensitivity after it.

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

FBumann commented Oct 1, 2026

Copy link
Copy Markdown
Contributor Author

Prompt: "Then port 749 as a stacked pr onto it and close 748 and 749 as superseded"

Note

The following content was generated by AI.

Superseded by #826, which carries this diff onto #824 (the port of #748). It also refuses a given parameter with a message that names it as given.


Generated by Claude Code

@FBumann FBumann closed this Oct 1, 2026
FBumann pushed a commit that referenced this pull request Oct 2, 2026
…ich for a fixed decision is a Benders cut

spec.sensitivity(*names) reports <name>_sensitivity, the rise of the
optimal objective per unit of a parameter, written over dual(). It
carries arithmetic, sum(over=) and named expressions from a row down to
the parameter, and refuses a bound, a divisor, a masked row read
pointwise, and any other operator on the path.

Ported from #749 onto spec.fix. A given parameter 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
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.

2 participants