From eb919d3b8c165f6fe8ec7a902e180cf38b63b837 Mon Sep 17 00:00:00 2001 From: cafzal Date: Thu, 18 Jun 2026 09:13:28 -0700 Subject: [PATCH 01/14] cuopt-agent: add duals-interpretation guidance to the debugging skill Signed-off-by: cafzal --- .../max-supply/cuopt-debugging/SKILL.md | 4 ++ .../resources/interpreting_duals.md | 72 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md diff --git a/cuopt-agent/skills/max-supply/cuopt-debugging/SKILL.md b/cuopt-agent/skills/max-supply/cuopt-debugging/SKILL.md index 5ff5761..de00b73 100755 --- a/cuopt-agent/skills/max-supply/cuopt-debugging/SKILL.md +++ b/cuopt-agent/skills/max-supply/cuopt-debugging/SKILL.md @@ -201,6 +201,10 @@ See [resources/diagnostic_snippets.md](resources/diagnostic_snippets.md) for cop - Constraint analysis - Memory and performance checks +## Interpreting Duals (Shadow Prices & Reduced Costs) + +When an LP/QP solve returns dual values and you need the *decision* read — which constraint is the binding bottleneck, what relaxing it is worth, and which unused option is the closest near-miss — see [resources/interpreting_duals.md](resources/interpreting_duals.md). (Integer models / MILP return no usable duals; that reference covers the MILP fallback.) + ## When to Escalate File a GitHub issue if: diff --git a/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md b/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md new file mode 100644 index 0000000..d74da58 --- /dev/null +++ b/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md @@ -0,0 +1,72 @@ +# Interpreting Duals: Shadow Prices, Reduced Costs, and Slack + +`diagnostic_snippets.md` shows how to *read* `DualValue`, `ReducedCost`, and `Slack` off a solved +problem. This explains what they *mean* for the decision — turning solver output into "which +constraint is the binding bottleneck, what relaxing it is worth, and which unused option is the +closest near-miss." + +> **LP / QP only.** Duals and reduced costs exist for **continuous** (LP / QP) solutions. An +> integer model (MILP) — **including the max-supply model** — returns no usable duals; +> `DualValue` / `ReducedCost` are not meaningful there. For a MILP, get the marginal value by +> **differencing adjacent solves** (re-solve with the bound relaxed by one unit and compare +> objectives), or read duals from the **LP relaxation**. + +## Shadow price — the value of relaxing a constraint + +A constraint's `DualValue` is its **shadow price**: the change in the optimal objective per unit +relaxation of that constraint's right-hand side, holding everything else fixed. + +- A **binding** constraint (`Slack ≈ 0`) carries a nonzero shadow price — it is actively limiting + the objective. A **slack** constraint (`Slack > 0`) has a shadow price of ~0: relaxing it changes + nothing, because it is not the bottleneck. +- **Rank the binding constraints by `|DualValue|`** → the largest is the highest-leverage limit to + renegotiate: "relax this by one unit and the objective improves by `DualValue`." + +```python +# Which constraints bind, and what each is worth (LP / QP only): +binding = [(c.ConstraintName, c.DualValue) for c in problem.getConstraints() if abs(c.Slack) < 1e-6] +for name, dual in sorted(binding, key=lambda kv: -abs(kv[1])): + print(f"{name}: shadow price {dual:+.4g} (objective change per unit relaxed)") +``` + +In the max-supply shape the constraints that typically bind are the **resource-hour capacities** +and the **per-period supply limits** — the shadow price tells you which machine-hour (e.g. a tight +`RES2` period) or which material is the binding bottleneck, and what one more hour or unit of supply +is worth in finished-goods terms. (Read it from the LP relaxation, since the model itself is a MILP.) + +## Reduced cost — how far an unused option is from entering + +A variable resting at a bound (often `0`) carries a `ReducedCost`: how much its objective +coefficient must improve before it would enter the optimal solution. It is the **near-miss** signal. + +- A variable with `Value > 0` is already in the mix; its reduced cost is ~0. +- Among the variables left at `0`, the one with the **smallest `|ReducedCost|`** is closest to + becoming worthwhile — the option to watch if a cost or yield shifts slightly. + +```python +# Unused options ranked by how close they are to entering (LP / QP only): +near = [(v.VariableName, v.ReducedCost) for v in problem.getVariables() + if abs(v.Value) < 1e-6 and abs(v.ReducedCost) > 1e-9] +for name, rc in sorted(near, key=lambda kv: abs(kv[1])): + print(f"{name}: reduced cost {rc:+.4g} (improve its coefficient by ~{abs(rc):.4g} to use it)") +``` + +## The decision read + +Two questions answered straight from the duals: + +- **Where to invest / what to renegotiate** — the binding constraint with the largest shadow price. + Lift that limit and you gain the most per unit. +- **The closest near-miss** — the unused option with the smallest reduced cost. The first thing that + would enter the plan if the economics shift. + +Report both in decision language, not raw numbers: "the *RES2 machine-hour cap* is the binding +bottleneck — each extra hour is worth ~`X` finished units; *material Y* is the closest unused option, +~`Z` away from being worth procuring." + +## Sign conventions + +`DualValue` / `ReducedCost` signs depend on the constraint sense and the objective direction. Read +the **magnitude** for leverage ("how much per unit") and the **constraint sense** for direction +(relaxing a `<=` capacity raises a maximize objective). When unsure, confirm with a one-unit +re-solve: on an LP / QP the objective difference matches the dual to solver tolerance. From fbcf2ca5d54c3f01d9735957696d86bcdb3655b8 Mon Sep 17 00:00:00 2001 From: cafzal Date: Thu, 18 Jun 2026 09:51:33 -0700 Subject: [PATCH 02/14] Note quadratic-constraint dual scope; lead with marginal-value framing Signed-off-by: cafzal --- .../max-supply/cuopt-debugging/SKILL.md | 4 +-- .../resources/interpreting_duals.md | 32 +++++++++++-------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/cuopt-agent/skills/max-supply/cuopt-debugging/SKILL.md b/cuopt-agent/skills/max-supply/cuopt-debugging/SKILL.md index de00b73..46142fa 100755 --- a/cuopt-agent/skills/max-supply/cuopt-debugging/SKILL.md +++ b/cuopt-agent/skills/max-supply/cuopt-debugging/SKILL.md @@ -201,9 +201,9 @@ See [resources/diagnostic_snippets.md](resources/diagnostic_snippets.md) for cop - Constraint analysis - Memory and performance checks -## Interpreting Duals (Shadow Prices & Reduced Costs) +## Interpreting Duals (Marginal Values & Reduced Costs) -When an LP/QP solve returns dual values and you need the *decision* read — which constraint is the binding bottleneck, what relaxing it is worth, and which unused option is the closest near-miss — see [resources/interpreting_duals.md](resources/interpreting_duals.md). (Integer models / MILP return no usable duals; that reference covers the MILP fallback.) +When an LP/QP solve returns dual values and you need the *decision* read — which constraint is the binding bottleneck, what relaxing it is worth, and which unused option is the closest near-miss — see [resources/interpreting_duals.md](resources/interpreting_duals.md). (Integer models / MILP — and quadratic *constraints* — return no usable duals; that reference covers the fallback.) ## When to Escalate diff --git a/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md b/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md index d74da58..985d556 100644 --- a/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md +++ b/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md @@ -1,23 +1,27 @@ -# Interpreting Duals: Shadow Prices, Reduced Costs, and Slack +# Interpreting Duals: Marginal Values, Reduced Costs, and Slack `diagnostic_snippets.md` shows how to *read* `DualValue`, `ReducedCost`, and `Slack` off a solved problem. This explains what they *mean* for the decision — turning solver output into "which constraint is the binding bottleneck, what relaxing it is worth, and which unused option is the closest near-miss." -> **LP / QP only.** Duals and reduced costs exist for **continuous** (LP / QP) solutions. An -> integer model (MILP) — **including the max-supply model** — returns no usable duals; -> `DualValue` / `ReducedCost` are not meaningful there. For a MILP, get the marginal value by -> **differencing adjacent solves** (re-solve with the bound relaxed by one unit and compare -> objectives), or read duals from the **LP relaxation**. +> **Continuous models, linear constraints.** Duals and reduced costs exist for **continuous** +> (LP / QP) solutions off **linear** constraints. Two cases return none: an **integer model +> (MILP)** — **including the max-supply model** — has no usable duals, and a **quadratic +> _constraint_** makes cuOpt NaN-fill every dual (a quadratic _objective_ is fine — it is a +> quadratic _constraint_ that breaks them). `DualValue` / `ReducedCost` are not meaningful in +> either case. For a MILP, get the marginal value by **differencing adjacent solves** (re-solve +> with the bound relaxed by one unit and compare objectives), or read duals from the **LP +> relaxation**. -## Shadow price — the value of relaxing a constraint +## Constraint dual — the marginal value of relaxing a limit -A constraint's `DualValue` is its **shadow price**: the change in the optimal objective per unit -relaxation of that constraint's right-hand side, holding everything else fixed. +A constraint's `DualValue` is the **sensitivity** of the optimum to that constraint: the change in +the optimal objective per unit relaxation of its right-hand side, holding everything else fixed — +the marginal value of one more unit of that limit (classically, its *shadow price*). -- A **binding** constraint (`Slack ≈ 0`) carries a nonzero shadow price — it is actively limiting - the objective. A **slack** constraint (`Slack > 0`) has a shadow price of ~0: relaxing it changes +- A **binding** constraint (`Slack ≈ 0`) carries a nonzero dual — it is actively limiting + the objective. A **slack** constraint (`Slack > 0`) prices to ~0: relaxing it changes nothing, because it is not the bottleneck. - **Rank the binding constraints by `|DualValue|`** → the largest is the highest-leverage limit to renegotiate: "relax this by one unit and the objective improves by `DualValue`." @@ -26,11 +30,11 @@ relaxation of that constraint's right-hand side, holding everything else fixed. # Which constraints bind, and what each is worth (LP / QP only): binding = [(c.ConstraintName, c.DualValue) for c in problem.getConstraints() if abs(c.Slack) < 1e-6] for name, dual in sorted(binding, key=lambda kv: -abs(kv[1])): - print(f"{name}: shadow price {dual:+.4g} (objective change per unit relaxed)") + print(f"{name}: dual {dual:+.4g} (objective change per unit relaxed)") ``` In the max-supply shape the constraints that typically bind are the **resource-hour capacities** -and the **per-period supply limits** — the shadow price tells you which machine-hour (e.g. a tight +and the **per-period supply limits** — the dual tells you which machine-hour (e.g. a tight `RES2` period) or which material is the binding bottleneck, and what one more hour or unit of supply is worth in finished-goods terms. (Read it from the LP relaxation, since the model itself is a MILP.) @@ -55,7 +59,7 @@ for name, rc in sorted(near, key=lambda kv: abs(kv[1])): Two questions answered straight from the duals: -- **Where to invest / what to renegotiate** — the binding constraint with the largest shadow price. +- **Where to invest / what to renegotiate** — the binding constraint with the largest dual. Lift that limit and you gain the most per unit. - **The closest near-miss** — the unused option with the smallest reduced cost. The first thing that would enter the plan if the economics shift. From 592b1de6edab743fa187d9cbf058a4d071b6def1 Mon Sep 17 00:00:00 2001 From: cafzal Date: Thu, 18 Jun 2026 10:02:35 -0700 Subject: [PATCH 03/14] Drop 'shadow price'; use dual-value terminology throughout Signed-off-by: cafzal --- cuopt-agent/skills/max-supply/cuopt-debugging/SKILL.md | 2 +- .../cuopt-debugging/resources/interpreting_duals.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cuopt-agent/skills/max-supply/cuopt-debugging/SKILL.md b/cuopt-agent/skills/max-supply/cuopt-debugging/SKILL.md index 46142fa..064d07a 100755 --- a/cuopt-agent/skills/max-supply/cuopt-debugging/SKILL.md +++ b/cuopt-agent/skills/max-supply/cuopt-debugging/SKILL.md @@ -201,7 +201,7 @@ See [resources/diagnostic_snippets.md](resources/diagnostic_snippets.md) for cop - Constraint analysis - Memory and performance checks -## Interpreting Duals (Marginal Values & Reduced Costs) +## Interpreting Dual Values & Reduced Costs When an LP/QP solve returns dual values and you need the *decision* read — which constraint is the binding bottleneck, what relaxing it is worth, and which unused option is the closest near-miss — see [resources/interpreting_duals.md](resources/interpreting_duals.md). (Integer models / MILP — and quadratic *constraints* — return no usable duals; that reference covers the fallback.) diff --git a/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md b/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md index 985d556..1c78204 100644 --- a/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md +++ b/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md @@ -1,4 +1,4 @@ -# Interpreting Duals: Marginal Values, Reduced Costs, and Slack +# Interpreting Dual Values, Reduced Costs, and Slack `diagnostic_snippets.md` shows how to *read* `DualValue`, `ReducedCost`, and `Slack` off a solved problem. This explains what they *mean* for the decision — turning solver output into "which @@ -14,11 +14,11 @@ closest near-miss." > with the bound relaxed by one unit and compare objectives), or read duals from the **LP > relaxation**. -## Constraint dual — the marginal value of relaxing a limit +## Constraint dual value — the marginal value of relaxing a limit A constraint's `DualValue` is the **sensitivity** of the optimum to that constraint: the change in the optimal objective per unit relaxation of its right-hand side, holding everything else fixed — -the marginal value of one more unit of that limit (classically, its *shadow price*). +the marginal value of one more unit of that limit. - A **binding** constraint (`Slack ≈ 0`) carries a nonzero dual — it is actively limiting the objective. A **slack** constraint (`Slack > 0`) prices to ~0: relaxing it changes From 516bfdbdc4a40873c8243a78344d206ebc6c904c Mon Sep 17 00:00:00 2001 From: cafzal Date: Mon, 22 Jun 2026 18:42:21 -0700 Subject: [PATCH 04/14] interpreting_duals: caveat soft duals under degeneracy At a degenerate optimum (many constraints binding at once) the basis is non-unique, so a single dual is one-sided. Report the ranking of binding constraints and present a dual as a direction (confirmed by the one-unit re-solve) rather than a hard per-unit rate. LP/simplex effect; strictly convex QP duals stay unique. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: cafzal --- .../resources/interpreting_duals.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md b/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md index 1c78204..27ed0ec 100644 --- a/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md +++ b/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md @@ -68,6 +68,21 @@ Report both in decision language, not raw numbers: "the *RES2 machine-hour cap* bottleneck — each extra hour is worth ~`X` finished units; *material Y* is the closest unused option, ~`Z` away from being worth procuring." +## When a dual is soft (degeneracy) + +A dual is exact for the basis the solver returned, but at a **degenerate** optimum — many +constraints binding at once (a lot of `Slack ≈ 0`) — that basis is one of several, so the dual is +one-sided and non-unique. It reads most precise exactly where it is least reliable, the common case +on large LPs. + +- Report the **ranking** of binding constraints as solid; present a single dual as a *direction* + ("this is the lever to renegotiate"), not a hard per-unit rate. +- Confirm any rate you quote with the one-unit re-solve (below): if the objective change does not + match `DualValue`, the optimum is degenerate — give the direction, not the number. + +An LP / simplex effect; a strictly convex QP (quadratic _objective_, not constraint) has unique +duals, so its read stays firm. + ## Sign conventions `DualValue` / `ReducedCost` signs depend on the constraint sense and the objective direction. Read From e20617abddb69e0944cb796abf3f2f653aa88144 Mon Sep 17 00:00:00 2001 From: cafzal Date: Mon, 29 Jun 2026 14:28:00 -0700 Subject: [PATCH 05/14] interpreting_duals: condition the dual's marginal-value reading on non-degeneracy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review feedback: the headline stated the smooth sensitivity reading unconditionally. Hedge it where the claim is made — exact change per unit only at a non-degenerate optimum; one-sided under degeneracy (the common case), read as a direction. Detail stays in the existing degeneracy note. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: cafzal --- .../cuopt-debugging/resources/interpreting_duals.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md b/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md index 27ed0ec..ff3f4ff 100644 --- a/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md +++ b/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md @@ -16,9 +16,10 @@ closest near-miss." ## Constraint dual value — the marginal value of relaxing a limit -A constraint's `DualValue` is the **sensitivity** of the optimum to that constraint: the change in -the optimal objective per unit relaxation of its right-hand side, holding everything else fixed — -the marginal value of one more unit of that limit. +A constraint's `DualValue` is the **sensitivity** of the optimum to that constraint: the marginal +value of one more unit of that limit, holding everything else fixed. At a non-degenerate optimum +that is the exact change in objective per unit relaxed; under **degeneracy** (common in practice) it +is one-sided, so read it as a direction (see *When a dual is soft*). - A **binding** constraint (`Slack ≈ 0`) carries a nonzero dual — it is actively limiting the objective. A **slack** constraint (`Slack > 0`) prices to ~0: relaxing it changes From 94ec9ecc29a78b9091a53e69369a2057a1680740 Mon Sep 17 00:00:00 2001 From: cafzal Date: Mon, 29 Jun 2026 14:47:38 -0700 Subject: [PATCH 06/14] interpreting_duals: make the ranking the robust read, not the per-unit rate Follow-through on the degeneracy hedge. The bullet under the headline still quoted the unconditional per-unit reading ("relax by one unit and the objective improves by DualValue") two lines below the hedge that conditions it. Align it: the ranking of binding constraints by |DualValue| is the degeneracy-robust read; a single dual is a direction, not a guaranteed per-unit rate. Points to the existing 'When a dual is soft' section. Signed-off-by: cafzal --- .../max-supply/cuopt-debugging/resources/interpreting_duals.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md b/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md index ff3f4ff..33be916 100644 --- a/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md +++ b/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md @@ -25,7 +25,8 @@ is one-sided, so read it as a direction (see *When a dual is soft*). the objective. A **slack** constraint (`Slack > 0`) prices to ~0: relaxing it changes nothing, because it is not the bottleneck. - **Rank the binding constraints by `|DualValue|`** → the largest is the highest-leverage limit to - renegotiate: "relax this by one unit and the objective improves by `DualValue`." + renegotiate. The *ranking* is the robust read; a single dual is a direction, not a guaranteed + per-unit rate (see *When a dual is soft*). ```python # Which constraints bind, and what each is worth (LP / QP only): From ec792c708f88cc5872942b3df201d5d43cc627ce Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 02:30:52 +0000 Subject: [PATCH 07/14] =?UTF-8?q?interpreting=5Fduals:=20address=20review?= =?UTF-8?q?=20=E2=80=94=20implication=20direction,=20unit=20comparability,?= =?UTF-8?q?=20no-basis=20duals?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - State the slack/dual implication one-way: slack > 0 forces a ~0 dual, but a binding constraint can still price to 0 (degeneracy) - Caveat the |DualValue| ranking: only meaningful across constraints in comparable units; suggest a common scale (e.g. value of a 1% relaxation) - Same units caveat for sorting unused variables by |ReducedCost| - Note cuOpt often returns no basis (PDLP / concurrent path): duals are tolerance-accurate directions and reduced costs lose the crisp at-bound split --- .../resources/interpreting_duals.md | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md b/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md index 33be916..3c974cc 100644 --- a/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md +++ b/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md @@ -21,12 +21,17 @@ value of one more unit of that limit, holding everything else fixed. At a non-de that is the exact change in objective per unit relaxed; under **degeneracy** (common in practice) it is one-sided, so read it as a direction (see *When a dual is soft*). -- A **binding** constraint (`Slack ≈ 0`) carries a nonzero dual — it is actively limiting - the objective. A **slack** constraint (`Slack > 0`) prices to ~0: relaxing it changes - nothing, because it is not the bottleneck. +- The implication runs **one way**: a **slack** constraint (`Slack > 0`) always prices to ~0 — + relaxing it changes nothing, because it is not the bottleneck. But `Slack ≈ 0` does **not** + guarantee a nonzero dual: a binding constraint can still price to 0 (a form of degeneracy — see + *When a dual is soft*). A nonzero dual means binding; binding does not mean a nonzero dual. - **Rank the binding constraints by `|DualValue|`** → the largest is the highest-leverage limit to renegotiate. The *ranking* is the robust read; a single dual is a direction, not a guaranteed - per-unit rate (see *When a dual is soft*). + per-unit rate (see *When a dual is soft*). One catch: a dual is objective-units **per unit of + that constraint**, so the raw ranking only makes sense across constraints in **comparable units** + (hours vs hours). To rank a machine-hour cap against a material-tonnage limit, put them on a + common scale first — e.g. multiply each dual by a realistic relaxation step, or compare the value + of a 1% relaxation (`|DualValue| × 0.01 × |RHS|`). ```python # Which constraints bind, and what each is worth (LP / QP only): @@ -47,7 +52,10 @@ coefficient must improve before it would enter the optimal solution. It is the * - A variable with `Value > 0` is already in the mix; its reduced cost is ~0. - Among the variables left at `0`, the one with the **smallest `|ReducedCost|`** is closest to - becoming worthwhile — the option to watch if a cost or yield shifts slightly. + becoming worthwhile — the option to watch if a cost or yield shifts slightly. Same units catch as + the dual ranking: a reduced cost is objective-units per unit of *that variable*, so sort only + variables in comparable units against each other — or compare each `|ReducedCost|` as a fraction + of its own objective coefficient ("needs a 3% price move" vs "needs a 40% one"). ```python # Unused options ranked by how close they are to entering (LP / QP only): @@ -77,6 +85,12 @@ constraints binding at once (a lot of `Slack ≈ 0`) — that basis is one of se one-sided and non-unique. It reads most precise exactly where it is least reliable, the common case on large LPs. +Which solver ran matters too: cuOpt **often returns no basis at all** — the first-order **PDLP** +path (common on large LPs, and one arm of the concurrent default) produces duals only to the +convergence tolerance, with no basis behind them. Those duals get the same treatment: leverage and +direction, not exact rates, and at-bound reduced costs are not the crisp zero / nonzero split a +simplex basis gives. + - Report the **ranking** of binding constraints as solid; present a single dual as a *direction* ("this is the lever to renegotiate"), not a hard per-unit rate. - Confirm any rate you quote with the one-unit re-solve (below): if the objective change does not From f952bdbb471daa117b8c450edf4cb30176c48d9c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 03:57:07 +0000 Subject: [PATCH 08/14] interpreting_duals: carry the review caveats through every section The one-way implication, unit-comparability, and no-basis caveats were stated once but contradicted downstream. Align the rest of the doc: - Ranking snippets, the decision-read summary, the max-supply example, and the soft-dual bullet now all scope |dual| / |reduced cost| comparisons to comparable-unit groups - Sign-conventions re-solve check conditioned on non-degeneracy instead of asserting the difference always matches the dual - QP aside no longer claims unique duals outright (degenerate active constraints can leave multipliers non-unique) - Drop a redundant restatement of the implication direction --- .../resources/interpreting_duals.md | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md b/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md index 3c974cc..25232c0 100644 --- a/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md +++ b/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md @@ -24,7 +24,7 @@ is one-sided, so read it as a direction (see *When a dual is soft*). - The implication runs **one way**: a **slack** constraint (`Slack > 0`) always prices to ~0 — relaxing it changes nothing, because it is not the bottleneck. But `Slack ≈ 0` does **not** guarantee a nonzero dual: a binding constraint can still price to 0 (a form of degeneracy — see - *When a dual is soft*). A nonzero dual means binding; binding does not mean a nonzero dual. + *When a dual is soft*). - **Rank the binding constraints by `|DualValue|`** → the largest is the highest-leverage limit to renegotiate. The *ranking* is the robust read; a single dual is a direction, not a guaranteed per-unit rate (see *When a dual is soft*). One catch: a dual is objective-units **per unit of @@ -34,16 +34,18 @@ is one-sided, so read it as a direction (see *When a dual is soft*). of a 1% relaxation (`|DualValue| × 0.01 × |RHS|`). ```python -# Which constraints bind, and what each is worth (LP / QP only): +# Which constraints bind, and what each is worth (LP / QP only). +# The |dual| ranking is meaningful within comparable-unit constraints: binding = [(c.ConstraintName, c.DualValue) for c in problem.getConstraints() if abs(c.Slack) < 1e-6] for name, dual in sorted(binding, key=lambda kv: -abs(kv[1])): print(f"{name}: dual {dual:+.4g} (objective change per unit relaxed)") ``` In the max-supply shape the constraints that typically bind are the **resource-hour capacities** -and the **per-period supply limits** — the dual tells you which machine-hour (e.g. a tight -`RES2` period) or which material is the binding bottleneck, and what one more hour or unit of supply -is worth in finished-goods terms. (Read it from the LP relaxation, since the model itself is a MILP.) +and the **per-period supply limits** — the dual prices each one: what one more hour of a tight +resource period (e.g. `RES2`), or one more unit of a material's supply, is worth in finished-goods +terms. Hour-caps rank against hour-caps and supply limits against supply limits; across the two, +convert to a common scale first. (Read it from the LP relaxation, since the model itself is a MILP.) ## Reduced cost — how far an unused option is from entering @@ -58,21 +60,24 @@ coefficient must improve before it would enter the optimal solution. It is the * of its own objective coefficient ("needs a 3% price move" vs "needs a 40% one"). ```python -# Unused options ranked by how close they are to entering (LP / QP only): +# Unused options ranked by how close they are to entering (LP / QP only). +# Compare |reduced cost| within comparable-unit variables: near = [(v.VariableName, v.ReducedCost) for v in problem.getVariables() if abs(v.Value) < 1e-6 and abs(v.ReducedCost) > 1e-9] for name, rc in sorted(near, key=lambda kv: abs(kv[1])): - print(f"{name}: reduced cost {rc:+.4g} (improve its coefficient by ~{abs(rc):.4g} to use it)") + print(f"{name}: reduced cost {rc:+.4g} (~{abs(rc):.4g} coefficient improvement before it could enter)") ``` ## The decision read Two questions answered straight from the duals: -- **Where to invest / what to renegotiate** — the binding constraint with the largest dual. - Lift that limit and you gain the most per unit. -- **The closest near-miss** — the unused option with the smallest reduced cost. The first thing that - would enter the plan if the economics shift. +- **Where to invest / what to renegotiate** — the binding constraint with the largest dual among + comparable-unit limits (or after the common-scale conversion above). Lift that limit and you gain + the most per unit. +- **The closest near-miss** — the unused option with the smallest reduced cost, compared in like + units or as a fraction of its own coefficient. The first thing that would enter the plan if the + economics shift. Report both in decision language, not raw numbers: "the *RES2 machine-hour cap* is the binding bottleneck — each extra hour is worth ~`X` finished units; *material Y* is the closest unused option, @@ -91,17 +96,19 @@ convergence tolerance, with no basis behind them. Those duals get the same treat direction, not exact rates, and at-bound reduced costs are not the crisp zero / nonzero split a simplex basis gives. -- Report the **ranking** of binding constraints as solid; present a single dual as a *direction* - ("this is the lever to renegotiate"), not a hard per-unit rate. +- Report the **ranking** of binding constraints (within comparable units) as solid; present a + single dual as a *direction* ("this is the lever to renegotiate"), not a hard per-unit rate. - Confirm any rate you quote with the one-unit re-solve (below): if the objective change does not match `DualValue`, the optimum is degenerate — give the direction, not the number. -An LP / simplex effect; a strictly convex QP (quadratic _objective_, not constraint) has unique -duals, so its read stays firm. +An LP / simplex effect; a strictly convex QP (quadratic _objective_, not constraint) has a unique +optimum and its duals read firmer — though degenerate active constraints can still make the +multipliers non-unique, so the re-solve check stays worthwhile. ## Sign conventions `DualValue` / `ReducedCost` signs depend on the constraint sense and the objective direction. Read the **magnitude** for leverage ("how much per unit") and the **constraint sense** for direction (relaxing a `<=` capacity raises a maximize objective). When unsure, confirm with a one-unit -re-solve: on an LP / QP the objective difference matches the dual to solver tolerance. +re-solve: at a non-degenerate optimum the objective difference matches the dual to solver +tolerance (when it does not, see *When a dual is soft*). From 4ffca6a848520d2022715db157d38c13740565da Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 04:03:20 +0000 Subject: [PATCH 09/14] interpreting_duals: dual is a marginal rate, not a per-unit forecast; drop strict-convexity aside - Even without degeneracy the active set can change within a finite step, so dual x step is not the realized change: present the dual as a derivative and route any finite-change quote through a differencing re-solve (intro, snippet label, soft-dual bullet, sign conventions, decision read, MILP callout) - Remove the strictly-convex-QP reassurance: most QPs are not strictly convex, and the claim carried no decision weight --- .../resources/interpreting_duals.md | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md b/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md index 25232c0..c4562ee 100644 --- a/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md +++ b/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md @@ -10,16 +10,18 @@ closest near-miss." > (MILP)** — **including the max-supply model** — has no usable duals, and a **quadratic > _constraint_** makes cuOpt NaN-fill every dual (a quadratic _objective_ is fine — it is a > quadratic _constraint_ that breaks them). `DualValue` / `ReducedCost` are not meaningful in -> either case. For a MILP, get the marginal value by **differencing adjacent solves** (re-solve +> either case. For a MILP, get the value of one more unit by **differencing adjacent solves** (re-solve > with the bound relaxed by one unit and compare objectives), or read duals from the **LP > relaxation**. ## Constraint dual value — the marginal value of relaxing a limit A constraint's `DualValue` is the **sensitivity** of the optimum to that constraint: the marginal -value of one more unit of that limit, holding everything else fixed. At a non-degenerate optimum -that is the exact change in objective per unit relaxed; under **degeneracy** (common in practice) it -is one-sided, so read it as a direction (see *When a dual is soft*). +rate at which the objective improves as that limit relaxes, holding everything else fixed. It is a +derivative, not a per-unit forecast — over a finite step (even one unit) the solution can +restructure and the rate change along the way, and under **degeneracy** (common in practice) the +rate is one-sided, so read it as a direction (see *When a dual is soft*). For the actual value of a +finite relaxation, difference two solves. - The implication runs **one way**: a **slack** constraint (`Slack > 0`) always prices to ~0 — relaxing it changes nothing, because it is not the bottleneck. But `Slack ≈ 0` does **not** @@ -38,7 +40,7 @@ is one-sided, so read it as a direction (see *When a dual is soft*). # The |dual| ranking is meaningful within comparable-unit constraints: binding = [(c.ConstraintName, c.DualValue) for c in problem.getConstraints() if abs(c.Slack) < 1e-6] for name, dual in sorted(binding, key=lambda kv: -abs(kv[1])): - print(f"{name}: dual {dual:+.4g} (objective change per unit relaxed)") + print(f"{name}: dual {dual:+.4g} (marginal rate as this limit relaxes)") ``` In the max-supply shape the constraints that typically bind are the **resource-hour capacities** @@ -73,8 +75,8 @@ for name, rc in sorted(near, key=lambda kv: abs(kv[1])): Two questions answered straight from the duals: - **Where to invest / what to renegotiate** — the binding constraint with the largest dual among - comparable-unit limits (or after the common-scale conversion above). Lift that limit and you gain - the most per unit. + comparable-unit limits (or after the common-scale conversion above). Relaxing that limit improves the + objective at the highest marginal rate. - **The closest near-miss** — the unused option with the smallest reduced cost, compared in like units or as a fraction of its own coefficient. The first thing that would enter the plan if the economics shift. @@ -98,17 +100,15 @@ simplex basis gives. - Report the **ranking** of binding constraints (within comparable units) as solid; present a single dual as a *direction* ("this is the lever to renegotiate"), not a hard per-unit rate. -- Confirm any rate you quote with the one-unit re-solve (below): if the objective change does not - match `DualValue`, the optimum is degenerate — give the direction, not the number. +- Quote a finite change only from a differencing re-solve: the dual is the rate at the current + optimum, and the realized change over a step can differ from `dual × step` (the active set can + change along the way, degenerate or not). -An LP / simplex effect; a strictly convex QP (quadratic _objective_, not constraint) has a unique -optimum and its duals read firmer — though degenerate active constraints can still make the -multipliers non-unique, so the re-solve check stays worthwhile. ## Sign conventions `DualValue` / `ReducedCost` signs depend on the constraint sense and the objective direction. Read the **magnitude** for leverage ("how much per unit") and the **constraint sense** for direction -(relaxing a `<=` capacity raises a maximize objective). When unsure, confirm with a one-unit -re-solve: at a non-degenerate optimum the objective difference matches the dual to solver -tolerance (when it does not, see *When a dual is soft*). +(relaxing a `<=` capacity raises a maximize objective). When unsure, re-solve with the bound +slightly relaxed: the sign of the objective change gives the direction, and the difference itself +is the number to quote for a finite step (see *When a dual is soft*). From 21408dca8adfd028564c5d42df72d5d7e9915a64 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 04:04:24 +0000 Subject: [PATCH 10/14] interpreting_duals: finish the rate-not-forecast sweep (max-supply gloss, could-enter wording) --- .../cuopt-debugging/resources/interpreting_duals.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md b/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md index c4562ee..cbdb6bd 100644 --- a/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md +++ b/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md @@ -44,15 +44,15 @@ for name, dual in sorted(binding, key=lambda kv: -abs(kv[1])): ``` In the max-supply shape the constraints that typically bind are the **resource-hour capacities** -and the **per-period supply limits** — the dual prices each one: what one more hour of a tight -resource period (e.g. `RES2`), or one more unit of a material's supply, is worth in finished-goods -terms. Hour-caps rank against hour-caps and supply limits against supply limits; across the two, +and the **per-period supply limits** — the dual prices each one: the marginal rate, in +finished-goods terms, at which a tight resource period (e.g. `RES2`) or a material's supply limit +pays off as it relaxes. Hour-caps rank against hour-caps and supply limits against supply limits; across the two, convert to a common scale first. (Read it from the LP relaxation, since the model itself is a MILP.) ## Reduced cost — how far an unused option is from entering A variable resting at a bound (often `0`) carries a `ReducedCost`: how much its objective -coefficient must improve before it would enter the optimal solution. It is the **near-miss** signal. +coefficient must improve before it could enter the optimal solution. It is the **near-miss** signal. - A variable with `Value > 0` is already in the mix; its reduced cost is ~0. - Among the variables left at `0`, the one with the **smallest `|ReducedCost|`** is closest to @@ -104,7 +104,6 @@ simplex basis gives. optimum, and the realized change over a step can differ from `dual × step` (the active set can change along the way, degenerate or not). - ## Sign conventions `DualValue` / `ReducedCost` signs depend on the constraint sense and the objective direction. Read From 8e367126c9b011f607a255e78d9f41be4e2ff586 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 04:05:17 +0000 Subject: [PATCH 11/14] interpreting_duals: rank LP-relaxation duals as a guide below differencing for MILPs --- .../cuopt-debugging/resources/interpreting_duals.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md b/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md index cbdb6bd..2a0014a 100644 --- a/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md +++ b/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md @@ -11,8 +11,8 @@ closest near-miss." > _constraint_** makes cuOpt NaN-fill every dual (a quadratic _objective_ is fine — it is a > quadratic _constraint_ that breaks them). `DualValue` / `ReducedCost` are not meaningful in > either case. For a MILP, get the value of one more unit by **differencing adjacent solves** (re-solve -> with the bound relaxed by one unit and compare objectives), or read duals from the **LP -> relaxation**. +> with the bound relaxed by one unit and compare objectives); duals of the **LP relaxation** are a +> quicker guide, but the integer optimum can respond differently — differencing is the ground truth. ## Constraint dual value — the marginal value of relaxing a limit @@ -46,8 +46,9 @@ for name, dual in sorted(binding, key=lambda kv: -abs(kv[1])): In the max-supply shape the constraints that typically bind are the **resource-hour capacities** and the **per-period supply limits** — the dual prices each one: the marginal rate, in finished-goods terms, at which a tight resource period (e.g. `RES2`) or a material's supply limit -pays off as it relaxes. Hour-caps rank against hour-caps and supply limits against supply limits; across the two, -convert to a common scale first. (Read it from the LP relaxation, since the model itself is a MILP.) +pays off as it relaxes. Hour-caps rank against hour-caps and supply limits against supply limits; +across the two, convert to a common scale first. (The model itself is a MILP, so read this from the LP relaxation +as a guide and difference two MILP solves for the real number.) ## Reduced cost — how far an unused option is from entering From 65bb51737ec9e6c2f6ee472ff61884837ec7895c Mon Sep 17 00:00:00 2001 From: cafzal Date: Wed, 1 Jul 2026 21:24:10 -0700 Subject: [PATCH 12/14] interpreting_duals: reduced cost as a pay-off threshold; surface zero-RC-at-bound as degenerate Signed-off-by: cafzal --- .../resources/interpreting_duals.md | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md b/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md index 2a0014a..91686e0 100644 --- a/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md +++ b/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md @@ -50,12 +50,16 @@ pays off as it relaxes. Hour-caps rank against hour-caps and supply limits again across the two, convert to a common scale first. (The model itself is a MILP, so read this from the LP relaxation as a guide and difference two MILP solves for the real number.) -## Reduced cost — how far an unused option is from entering +## Reduced cost — how far an unused option is from being worth using -A variable resting at a bound (often `0`) carries a `ReducedCost`: how much its objective -coefficient must improve before it could enter the optimal solution. It is the **near-miss** signal. +A variable resting at a bound (often `0`) carries a `ReducedCost`: improve its objective +coefficient by more than `|ReducedCost|` and the current plan stops being optimal — using that +variable starts to pay off; by less, and leaving it at its bound stays optimal. It is the +**near-miss** signal. - A variable with `Value > 0` is already in the mix; its reduced cost is ~0. +- A variable **at its bound** can also price to ~0 — degeneracy again: an alternative optimum uses + it with no coefficient change at all (the mirror of a binding constraint with a zero dual). - Among the variables left at `0`, the one with the **smallest `|ReducedCost|`** is closest to becoming worthwhile — the option to watch if a cost or yield shifts slightly. Same units catch as the dual ranking: a reduced cost is objective-units per unit of *that variable*, so sort only @@ -63,12 +67,13 @@ coefficient must improve before it could enter the optimal solution. It is the * of its own objective coefficient ("needs a 3% price move" vs "needs a 40% one"). ```python -# Unused options ranked by how close they are to entering (LP / QP only). +# Unused options ranked by how close they are to paying off (LP / QP only). # Compare |reduced cost| within comparable-unit variables: -near = [(v.VariableName, v.ReducedCost) for v in problem.getVariables() - if abs(v.Value) < 1e-6 and abs(v.ReducedCost) > 1e-9] +near = [(v.VariableName, v.ReducedCost) for v in problem.getVariables() if abs(v.Value) < 1e-6] for name, rc in sorted(near, key=lambda kv: abs(kv[1])): - print(f"{name}: reduced cost {rc:+.4g} (~{abs(rc):.4g} coefficient improvement before it could enter)") + note = ("already interchangeable — degenerate" if abs(rc) < 1e-9 + else f"~{abs(rc):.4g} coefficient improvement before it pays off") + print(f"{name}: reduced cost {rc:+.4g} ({note})") ``` ## The decision read From 6ec96800c594e2a91101657316e9a9c87f06af11 Mon Sep 17 00:00:00 2001 From: cafzal Date: Wed, 1 Jul 2026 21:27:51 -0700 Subject: [PATCH 13/14] =?UTF-8?q?interpreting=5Fduals:=20degeneracy=20swee?= =?UTF-8?q?p=20=E2=80=94=20one-way=20reduced-cost=20threshold,=20rankings?= =?UTF-8?q?=20as=20shortlists,=20between-bounds=20not=20Value>0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: cafzal --- .../resources/interpreting_duals.md | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md b/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md index 91686e0..5f116f1 100644 --- a/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md +++ b/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md @@ -27,16 +27,17 @@ finite relaxation, difference two solves. relaxing it changes nothing, because it is not the bottleneck. But `Slack ≈ 0` does **not** guarantee a nonzero dual: a binding constraint can still price to 0 (a form of degeneracy — see *When a dual is soft*). -- **Rank the binding constraints by `|DualValue|`** → the largest is the highest-leverage limit to - renegotiate. The *ranking* is the robust read; a single dual is a direction, not a guaranteed - per-unit rate (see *When a dual is soft*). One catch: a dual is objective-units **per unit of +- **Rank the binding constraints by `|DualValue|`** → a shortlist of the highest-leverage limits to + renegotiate. Treat it as a shortlist, not a verdict: under degeneracy the dual solution itself is + non-unique, so even the ordering can shift between equally optimal solves — confirm the top + lever with a differencing re-solve (see *When a dual is soft*). One catch: a dual is objective-units **per unit of that constraint**, so the raw ranking only makes sense across constraints in **comparable units** (hours vs hours). To rank a machine-hour cap against a material-tonnage limit, put them on a common scale first — e.g. multiply each dual by a realistic relaxation step, or compare the value of a 1% relaxation (`|DualValue| × 0.01 × |RHS|`). ```python -# Which constraints bind, and what each is worth (LP / QP only). +# Which constraints bind, and each one's returned dual (LP / QP only). # The |dual| ranking is meaningful within comparable-unit constraints: binding = [(c.ConstraintName, c.DualValue) for c in problem.getConstraints() if abs(c.Slack) < 1e-6] for name, dual in sorted(binding, key=lambda kv: -abs(kv[1])): @@ -52,16 +53,20 @@ as a guide and difference two MILP solves for the real number.) ## Reduced cost — how far an unused option is from being worth using -A variable resting at a bound (often `0`) carries a `ReducedCost`: improve its objective -coefficient by more than `|ReducedCost|` and the current plan stops being optimal — using that -variable starts to pay off; by less, and leaving it at its bound stays optimal. It is the -**near-miss** signal. +A variable resting at a bound (often `0`) carries a `ReducedCost` — a one-way threshold on its +objective coefficient: improve the coefficient by **less** than `|ReducedCost|` and leaving the +variable at its bound stays optimal; improve it by more and using the variable **can** start to +pay off. (The returned value certifies the first direction, not the second — degeneracy again.) +It is the **near-miss** signal. -- A variable with `Value > 0` is already in the mix; its reduced cost is ~0. +- A variable strictly **between** its bounds prices to ~0. One pressed against an *upper* bound can + carry a nonzero reduced cost with `Value > 0` — there it reads as the marginal value of raising + that bound. - A variable **at its bound** can also price to ~0 — degeneracy again: an alternative optimum uses it with no coefficient change at all (the mirror of a binding constraint with a zero dual). -- Among the variables left at `0`, the one with the **smallest `|ReducedCost|`** is closest to - becoming worthwhile — the option to watch if a cost or yield shifts slightly. Same units catch as +- Among the variables left at `0` with a clearly nonzero reduced cost, the one with the + **smallest `|ReducedCost|`** is closest to becoming worthwhile — the option to watch if a cost + or yield shifts slightly. Same units catch as the dual ranking: a reduced cost is objective-units per unit of *that variable*, so sort only variables in comparable units against each other — or compare each `|ReducedCost|` as a fraction of its own objective coefficient ("needs a 3% price move" vs "needs a 40% one"). @@ -78,11 +83,11 @@ for name, rc in sorted(near, key=lambda kv: abs(kv[1])): ## The decision read -Two questions answered straight from the duals: +Two questions the duals point to — confirm any number you quote with a differencing re-solve: - **Where to invest / what to renegotiate** — the binding constraint with the largest dual among - comparable-unit limits (or after the common-scale conversion above). Relaxing that limit improves the - objective at the highest marginal rate. + comparable-unit limits (or after the common-scale conversion above): the first lever to test + for relaxing. - **The closest near-miss** — the unused option with the smallest reduced cost, compared in like units or as a fraction of its own coefficient. The first thing that would enter the plan if the economics shift. @@ -104,8 +109,10 @@ convergence tolerance, with no basis behind them. Those duals get the same treat direction, not exact rates, and at-bound reduced costs are not the crisp zero / nonzero split a simplex basis gives. -- Report the **ranking** of binding constraints (within comparable units) as solid; present a - single dual as a *direction* ("this is the lever to renegotiate"), not a hard per-unit rate. +- Use the **ranking** of binding constraints (within comparable units) as a shortlist and a single + dual as a *direction* ("this is the lever to renegotiate"), not a hard per-unit rate — the dual + solution is non-unique under degeneracy, so even the ordering can shift between equally optimal + solves. Confirm the lever you act on with a differencing re-solve. - Quote a finite change only from a differencing re-solve: the dual is the rate at the current optimum, and the realized change over a step can differ from `dual × step` (the active set can change along the way, degenerate or not). From f1ec6ff583ecde25015fe17376d0582c86c7aea2 Mon Sep 17 00:00:00 2001 From: cafzal Date: Wed, 1 Jul 2026 21:38:39 -0700 Subject: [PATCH 14/14] interpreting_duals: rewrap two ragged lines Signed-off-by: cafzal --- .../cuopt-debugging/resources/interpreting_duals.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md b/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md index 5f116f1..1d067dd 100644 --- a/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md +++ b/cuopt-agent/skills/max-supply/cuopt-debugging/resources/interpreting_duals.md @@ -30,8 +30,9 @@ finite relaxation, difference two solves. - **Rank the binding constraints by `|DualValue|`** → a shortlist of the highest-leverage limits to renegotiate. Treat it as a shortlist, not a verdict: under degeneracy the dual solution itself is non-unique, so even the ordering can shift between equally optimal solves — confirm the top - lever with a differencing re-solve (see *When a dual is soft*). One catch: a dual is objective-units **per unit of - that constraint**, so the raw ranking only makes sense across constraints in **comparable units** + lever with a differencing re-solve (see *When a dual is soft*). One catch: a dual is + objective-units **per unit of that constraint**, so the raw ranking only makes sense across + constraints in **comparable units** (hours vs hours). To rank a machine-hour cap against a material-tonnage limit, put them on a common scale first — e.g. multiply each dual by a realistic relaxation step, or compare the value of a 1% relaxation (`|DualValue| × 0.01 × |RHS|`). @@ -66,8 +67,8 @@ It is the **near-miss** signal. it with no coefficient change at all (the mirror of a binding constraint with a zero dual). - Among the variables left at `0` with a clearly nonzero reduced cost, the one with the **smallest `|ReducedCost|`** is closest to becoming worthwhile — the option to watch if a cost - or yield shifts slightly. Same units catch as - the dual ranking: a reduced cost is objective-units per unit of *that variable*, so sort only + or yield shifts slightly. Same units catch as the dual ranking: a reduced cost is + objective-units per unit of *that variable*, so sort only variables in comparable units against each other — or compare each `|ReducedCost|` as a fraction of its own objective coefficient ("needs a 3% price move" vs "needs a 40% one").