Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
eb919d3
cuopt-agent: add duals-interpretation guidance to the debugging skill
cafzal Jun 18, 2026
fbcf2ca
Note quadratic-constraint dual scope; lead with marginal-value framing
cafzal Jun 18, 2026
592b1de
Drop 'shadow price'; use dual-value terminology throughout
cafzal Jun 18, 2026
516bfdb
interpreting_duals: caveat soft duals under degeneracy
cafzal Jun 23, 2026
e20617a
interpreting_duals: condition the dual's marginal-value reading on no…
cafzal Jun 29, 2026
94ec9ec
interpreting_duals: make the ranking the robust read, not the per-uni…
cafzal Jun 29, 2026
ec792c7
interpreting_duals: address review — implication direction, unit comp…
claude Jul 2, 2026
f952bdb
interpreting_duals: carry the review caveats through every section
claude Jul 2, 2026
4ffca6a
interpreting_duals: dual is a marginal rate, not a per-unit forecast;…
claude Jul 2, 2026
21408dc
interpreting_duals: finish the rate-not-forecast sweep (max-supply gl…
claude Jul 2, 2026
8e36712
interpreting_duals: rank LP-relaxation duals as a guide below differe…
claude Jul 2, 2026
65bb517
interpreting_duals: reduced cost as a pay-off threshold; surface zero…
cafzal Jul 2, 2026
6ec9680
interpreting_duals: degeneracy sweep — one-way reduced-cost threshold…
cafzal Jul 2, 2026
f1ec6ff
interpreting_duals: rewrap two ragged lines
cafzal Jul 2, 2026
b191fe0
interpreting_duals: address review round 3
cafzal Jul 15, 2026
c96d9d2
interpreting_duals: trim meta-commentary from the availability note
cafzal Jul 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cuopt-agent/skills/max-supply/cuopt-debugging/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,10 @@ See [resources/diagnostic_snippets.md](resources/diagnostic_snippets.md) for cop
- Constraint analysis
- Memory and performance checks

## 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.)

## When to Escalate

File a GitHub issue if:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# 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
Comment thread
cafzal marked this conversation as resolved.
Outdated
constraint is the binding bottleneck, what relaxing it is worth, and which unused option is the
closest near-miss."

> **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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have this description of the NaN behavior written down in many skills. What happens in an upcoming release when the behavior changes and duals are available for quadratic constraints?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed, references other skill instead.

> 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**.

## 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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The smooth sensitivity interpretation is true only in the absence of degeneracy, and degeneracy is very common in real-world problems. I don't know how to precisely describe the perturbation sensitivity of the objective without invoking subgradients.

@cafzal cafzal Jun 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @mlubin . Follow-up commits make the ranking of binding constraints a more robust read and demote a single dual to a direction:

  • The headline now conditions the exact per-unit equality on a non-degenerate optimum; under degeneracy it is one-sided.
  • The bullet under it drops the unconditional "relax by one unit, objective improves by DualValue."
  • The When a dual is soft section adds the one-sided, non-unique caveat, the trap that the dual looks most precise where it is least reliable, and a one-unit re-solve to confirm any rate before quoting it.

Happy to tighten further if anything still reads too strong. We can also cover subgradients explicitly if you think it's warranted.

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 **binding** constraint (`Slack ≈ 0`) carries a nonzero dual — it is actively limiting

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implication is in one direction. Slack > 0 implies zero dual. Slack = 0 does not imply nonzero dual (if it doesn't hold, you have a form of degeneracy)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: now stated one-way, with the 'binding but no dual' case noted as degeneracy.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need some level of comparability of the units of the constraints for this ranking to make sense. You can always scale a constraint (multiply the coefficients and right-hand side by a constant) to increase or decrease the optimal dual, which would change this ranking but have no real meaning in practice.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: ranking scoped to comparable units, with a common-scale conversion for mixed ones

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}: dual {dual:+.4g} (objective change per unit relaxed)")
```

In the max-supply shape the constraints that typically bind are the **resource-hour capacities**

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is "max-supply shape"?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clarified to "model"

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.)

## 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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"enter the optimal solution" - the variable is already in the optimal solution, with a value of zero.
"how much its objective coefficient must improve" - what does this mean precisely? You're allowed to have a variable at it's bound 0 with zero reduced cost (again, degeneracy).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: now stated as the pay-off threshold on the coefficient with the at-bound zero-reduced-cost case noted as degeneracy


- 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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variables need to be in comparable units for this sorting to make sense, see above.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: same unit scoping for reduced costs, or compared as a fraction of each variable's own coefficient.

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 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.

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."

## When a dual is soft (degeneracy)

A dual is exact for the basis the solver returned, but at a **degenerate** optimum — many

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cuOpt often doesn't return a basis. It depends on which solver ran.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: the degeneracy section now covers the no-basis (PDLP) case.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Barrier also does not need to return a basis

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: generalized to any basis-free solve

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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't correct. Even if there's no degeneracy, the dual just gives you the marginal change but not the actual change for a unit perturbation; the basis could change multiple times in the process of solving the perturbed problem.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: the dual is now presented as a marginal rate, with any finite change quoted from a differencing re-solve


An LP / simplex effect; a strictly convex QP (quadratic _objective_, not constraint) has unique

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most QPs are not strictly convex.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: cut the claim.

duals, so its read stays firm.

## 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.