From 8cd38f470ab7e2e32160d4761e58cb541345d794 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 20:34:56 +0000 Subject: [PATCH 1/2] 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 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 Claude-Session: https://claude.ai/code/session_01CvcU9JRnsinc9e9syvVcHS --- docs/reference/language/named.md | 15 ++++++++++++--- src/mathspec/program.py | 14 ++++++++------ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/docs/reference/language/named.md b/docs/reference/language/named.md index 3df1763d..c8d40ed1 100644 --- a/docs/reference/language/named.md +++ b/docs/reference/language/named.md @@ -151,9 +151,18 @@ Constraint 'd': a dual exists only after a solve; the math cannot read one — keep the entry that carries it out of constraints, the objective, bounds and where. ``` -`dual(c)` is the rate at which the optimal objective improves as `c` is relaxed -in the direction its comparator points, under the spec's own `minimize` or -`maximize`. +`dual(c)` is the rate at which the optimal objective rises as the right side +of `c` rises. Read `lhs <= rhs` as `lhs <= rhs + d`: the dual is the rate in `d` +at `d = 0`. The rule is the same for `<=`, `>=` and `==`, and under `minimize` +and `maximize`, so an equality has a dual with a sign too. Which side a term is +written on decides the sign: `p <= cap` and `-p >= -cap` state one row, and their +duals are opposite. + +| Under `minimize`, a binding row | Its dual | +| ------------------------------- | ------------------------------------ | +| `p <= cap` | at most 0 | +| `p >= load` | at least 0 | +| `sum(p, over=g) == load` | the price of one more unit of `load` | A row that `c`'s `where:` deletes has no dual. diff --git a/src/mathspec/program.py b/src/mathspec/program.py index 36483e9f..bac35fc3 100644 --- a/src/mathspec/program.py +++ b/src/mathspec/program.py @@ -187,12 +187,14 @@ class Variable: @dataclass(frozen=True) class Dual: - """A constraint's dual — its shadow price, read after the solve. - - Stands only under an [`ExpressionDeclaration`][] the math never reads: - the loader refuses ``dual()`` anywhere a solver ingests. One value per - coordinate of the named constraint's own ``dims`` frame: the leaf reshapes - nothing, like a parameter. + """A constraint's dual — the rate the optimal objective rises with the constraint's right side, read after the solve. + + Of ``lhs <= rhs`` it is the rate in ``d`` of ``lhs <= rhs + d``, for every + comparator and under either sense. Stands only under an + [`ExpressionDeclaration`][] the math never reads: the loader refuses + ``dual()`` anywhere a solver ingests. One value per coordinate of the named + constraint's own ``dims`` frame: the leaf reshapes nothing, like a + parameter. """ constraint: str From 37b4abece3d170e23e4e8c1afaa318c2c543595d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 20:35:39 +0000 Subject: [PATCH 2/2] docs: the changelog names #751 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CvcU9JRnsinc9e9syvVcHS --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8026a946..98208c53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ it releases that version ([RELEASING.md](https://github.com/energy-models/mathsp ## Upcoming version +- 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](https://github.com/energy-models/mathspec/pull/751)) - docs: code examples on the site are readable in light and dark mode, and a diagram shows what mathspec leaves to engines and other tools ([#730](https://github.com/energy-models/mathspec/pull/730)) - docs: the site follows the reader's light or dark setting, and a page shows where it sits in the navigation ([#727](https://github.com/energy-models/mathspec/pull/727))