Skip to content

fix(language): dual(c) is the rate at which the optimal objective rises with the right side of c, so an equality has a sign too - #751

Merged
FBumann merged 10 commits into
mainfrom
claude/mathspec-analysis-features-zy7x4n-dual-sign
Sep 29, 2026
Merged

FBumann merged 10 commits into
mainfrom
claude/mathspec-analysis-features-zy7x4n-dual-sign

Conversation

@FBumann

@FBumann FBumann commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Prompt: Let's do a pr in mathspec and specsolve fixing the dual ambiguity

Note

The following content was generated by AI.

What this changes

dual(c) is now the rate at which the optimal objective rises with the right side of c, for every comparator and under either sense. The old wording left an equality without a sign and disagreed with specsolve on <=. fluxopt/specsolve#1774 pins every specsolve sink to this definition.

The ambiguity, the evidence, what moves

Before

named.md: "dual(c) is the rate at which the optimal objective improves as c is relaxed in the direction its comparator points."

  • An == row points in no direction, so its dual had no sign.
  • A <= row under minimize came out non-negative. specsolve returns the opposite sign, and specsolve's own examples/benders/run.py builds its cut slope on the returned sign.

After

named.md and the Dual docstring: read lhs <= rhs as lhs <= rhs + d; the dual is the rate in d at d = 0. The same for >= and ==, under minimize and maximize. Which side a term is written on decides the sign: p <= cap and -p >= -cap are one row with opposite duals. A table gives the sign of a binding row of each comparator under minimize.

Evidence

specsolve main at e78c1a3, HiGHS, Gurobi and Xpress. One row, "p is at least 2 + b", written six ways, under both senses; the dual against a finite difference in b (step 1e-3), times how b moves the right side:

Written Sign of b in rhs − lhs
p >= 2 + b +
2 + b <= p −
-p <= -2 - b −
p - b >= 2 +
p == 2 + b +
2 + b == p −

36 of 36 cases match (6 rows × 2 senses × 3 sinks). The test is in fluxopt/specsolve#1774. On specsolve's Benders subproblem the same reading gives the cut slope that finite differences give (67.5 for cap[wind]); the old reading gives 112.5.

What does not change

  • No code path: the loader never reads a dual's value.
  • examples/operators/dual.yaml uses a >= row under minimize, where the two readings agree.

Gates

  • pixi run lint: pass.
  • pixi run test: 1684 passed.
  • docs-build and compile-tex were not run in the session (network). CI runs both.

Relation to the open stack

#749 (spec.sensitivity) carries the same named.md hunk, because its formula depends on this definition. Once this merges, that hunk is already on main.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CvcU9JRnsinc9e9syvVcHS

…es with the right side of c, so an equality has a sign too

named.md defined dual(c) as the rate at which the objective improves as c
is relaxed in the direction its comparator points. That left an equality
without a sign, and it made a <= dual non-negative under minimize. specsolve
returns the rate in the right side on HiGHS, Gurobi and Xpress. The page
and the Dual docstring now state that definition, the same for every
comparator and under either sense.

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
@FBumann FBumann added the bug Something isn't working label Sep 29, 2026 — with Claude
@FBumann
FBumann merged commit 9ace314 into main Sep 29, 2026
6 checks passed
FBumann pushed a commit that referenced this pull request Sep 29, 2026
…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 pushed a commit that referenced this pull request Sep 29, 2026
Brings in #751. CHANGELOG.md keeps both sides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N66KMDkSv1rZbhr8jqqJzJ
FBumann pushed a commit that referenced this pull request Sep 29, 2026
Brings in #778, #779 and #751. CHANGELOG.md keeps both sides; nothing
else conflicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N66KMDkSv1rZbhr8jqqJzJ
FBumann added a commit to fluxopt/specsolve that referenced this pull request Sep 29, 2026
…th its row's right side, on every sink (#1774)

> **Prompt:** Let's do a pr in mathspec and specsolve fixing the dual
ambiguity

> [!NOTE]
> The following content was generated by AI.

`Result.dual` states its sign: the rate at which the optimal objective
rises with the row's right side, for every comparator and sense. That is
the definition energy-models/mathspec#751 gives `dual(c)`. A test pins
it on every sink against finite differences.

<details><summary>Method, mutation, gates, departures</summary>

### What was ambiguous

`Result.dual` said "shadow prices" and nothing about sign. mathspec's
page said a dual is the rate at which the objective *improves* as the
row is *relaxed*. That makes a `<=` dual under `minimize` non-negative,
where every sink here returns it non-positive, and it leaves an `==` row
without a sign. No code changes here: all three sinks already return the
rate in the right side, as written in the YAML.
energy-models/mathspec#751 moves mathspec's definition to match.

### The test

`test_a_dual_is_the_rate_of_the_optimum_in_its_rows_right_side`, in
`tests/test_duals.py`. One row, "p is at least 2 + b", is written six
ways: `p >= 2 + b`, `2 + b <= p`, `-p <= -2 - b`, `p - b >= 2`, `p == 2
+ b` and `2 + b == p`. Each runs under `minimize` and `maximize`, on
every sink `solver_name` yields. The dual has to equal the finite
difference of the objective in `b` (step `1e-3`; the row binds, so the
difference is exact), times how `b` moves `rhs - lhs`.

Result: 36 passed, highs, gurobi and xpress. Gurobi and Xpress ran on
the size-limited licences their PyPI wheels carry.

### Mutation

| Mutation | Result |
|---|---|
| HiGHS's `row_dual` negated in `highs.py` | 12 failed: every HiGHS case
|

### Gates

`pixi install` could not run in the session: it fetches the
conda-to-PyPI mapping from a host the session's network refuses. The
gates ran from `uv sync --group dev --extra gurobi --extra xpress` on
Python 3.12, with the tool versions the `dev` group pins.

- `ruff check .`: clean.
- `ruff format --check .`: clean.
- `pyrefly check`, both configs: 0 errors.
- `pytest -q -n auto`: 4078 passed, 320 skipped, 1 xfailed.

`docs-build` was not run.

### Not done

- `examples/benders/run.py` already reads the slope with this sign, so
it is unchanged.
- `dual_ray` has its own convention, stated in `test_dual_ray.py`, and
is untouched.

</details>

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01CvcU9JRnsinc9e9syvVcHS

---
_Generated by [Claude
Code](https://claude.ai/code/session_01CvcU9JRnsinc9e9syvVcHS)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
FBumann pushed a commit that referenced this pull request Oct 1, 2026
The Upcoming section becomes 0.3.0, grouped into composition, language,
typesetting and advice, and documentation. The notes name the two breaks
against 0.2.0 (#788, #751). The #742 line is left out: #763 replaced
`empty: true` before any release carried it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PBmqeqBrqpQC6MVoSrguHK
@FBumann FBumann mentioned this pull request Oct 1, 2026
FBumann pushed a commit that referenced this pull request Oct 1, 2026
…s working

#788 and #751 change only documentation and a docstring. mathspec neither
attaches data nor computes duals, so neither breaks a file or an import.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PBmqeqBrqpQC6MVoSrguHK
FBumann added a commit that referenced this pull request Oct 1, 2026
* fix(language): a where that reads a given expression names it as a given expression, not as a variable

The namespace files a given expression with the variables, because it is
read as a column. A where that read one was refused as if it read a
variable. The three refusals (the left name, the right name, a side of a
comparison of expressions) now name the given expression and say why a
mask may not read it: it may hold a variable.

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

* docs: the changelog line links #809

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

* chore: release 0.3.0

The Upcoming section becomes 0.3.0, grouped into composition, language,
typesetting and advice, and documentation. The notes name the two breaks
against 0.2.0 (#788, #751). The #742 line is left out: #763 replaced
`empty: true` before any release carried it.

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

* docs: the 0.3.0 notes say that nothing a 0.2.0 file or call does stops working

#788 and #751 change only documentation and a docstring. mathspec neither
attaches data nor computes duals, so neither breaks a file or an import.

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

* chore: the release is 0.2.1, since nothing in it breaks a file or an import

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

* chore: a release with no break raises the patch version, features included

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

* docs: the 0.2.1 notes say that every fragment typesets and gets advice on its own

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants