Repository navigation
cuopt-agent: add duals-interpretation guidance to the debugging skill #159
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
eb919d3
fbcf2ca
592b1de
516bfdb
e20617a
94ec9ec
ec792c7
f952bdb
4ffca6a
21408dc
8e36712
65bb517
6ec9680
f1ec6ff
b191fe0
c96d9d2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
| 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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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** | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is "max-supply shape"?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed: the degeneracy section now covers the no-basis (PDLP) case.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Barrier also does not need to return a basis
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Most QPs are not strictly convex.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
Uh oh!
There was an error while loading. Please reload this page.