From eb919d3b8c165f6fe8ec7a902e180cf38b63b837 Mon Sep 17 00:00:00 2001 From: cafzal Date: Thu, 18 Jun 2026 09:13:28 -0700 Subject: [PATCH 1/3] 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 2/3] 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 3/3] 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