From 98212d559a0b10be5045ed2d075fe2e5ead8f865 Mon Sep 17 00:00:00 2001 From: cafzal Date: Mon, 8 Jun 2026 16:09:44 -0700 Subject: [PATCH 1/3] Correct dual-value scope to LP/QP in numerical-optimization API skills MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cuOpt's barrier solver is primal-dual (SOCP barrier #1290, general convex quadratic constraints #1361), so a QP with a quadratic objective and linear constraints returns dual values and reduced costs — not LP only. The Python and C API skills still described duals as LP-only, wording that predates the QP/barrier work (#1183). - Python SKILL.md: retitle "Getting Dual Values (LP only)" -> "(LP / QP)" and rewrite the prose; add a "Reading Duals from a QP" example to references/qp_examples.md. - C SKILL.md: add a "Dual values (LP / QP)" section; add a duals note to assets/README.md. Boundary documented in all four edits: cuOpt returns no dual variables for problems with quadratic constraints (the dual/reduced-cost arrays come back filled with NaN), so the constraints whose duals you read must be linear. This matches docs/cuopt/source/cuopt-c/convex/convex-examples.rst and the QCQP path in cpp/src/pdlp/solve.cu. Existing lp_duals assets are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: cafzal --- .../SKILL.md | 8 +++++ .../assets/README.md | 5 ++++ .../SKILL.md | 12 ++++++-- .../references/qp_examples.md | 30 +++++++++++++++++++ 4 files changed, 52 insertions(+), 3 deletions(-) diff --git a/skills/cuopt-numerical-optimization-api-c/SKILL.md b/skills/cuopt-numerical-optimization-api-c/SKILL.md index b25acc14ba..808c110ce8 100644 --- a/skills/cuopt-numerical-optimization-api-c/SKILL.md +++ b/skills/cuopt-numerical-optimization-api-c/SKILL.md @@ -35,6 +35,14 @@ QP uses the same library, include/lib paths, and build pattern as LP/MILP — on - **Continuous variables only** — set `CUOPT_CONTINUOUS` for every variable; integer QP is not supported. - **Q should be PSD** for a convex problem. +## Dual values (LP / QP) + +`cuOptGetDualSolution` (shadow prices) and `cuOptGetReducedCosts` (reduced costs) return values +for **LP and QP with linear constraints** — the barrier solver is primal-dual, so a quadratic +objective still yields duals. cuOpt returns **no duals for a problem with quadratic constraints** +(the returned arrays are filled with `NaN`). See [assets/lp_duals](assets/lp_duals/) for the call +sequence — it is an LP, but the same calls apply to a QP whose constraints are all linear. + ## Debugging (MPS / C) **MPS parsing:** Required sections in order: NAME, ROWS, COLUMNS, RHS, (optional) BOUNDS, ENDATA. Integer markers: `'MARKER'`, `'INTORG'`, `'INTEND'`. diff --git a/skills/cuopt-numerical-optimization-api-c/assets/README.md b/skills/cuopt-numerical-optimization-api-c/assets/README.md index e354988da1..d9bb715d00 100644 --- a/skills/cuopt-numerical-optimization-api-c/assets/README.md +++ b/skills/cuopt-numerical-optimization-api-c/assets/README.md @@ -11,6 +11,11 @@ LP/MILP C API reference implementations. Use as reference when building new appl | [milp_production_planning](milp_production_planning/) | MILP | Production planning with resource constraints | | [mps_solver](mps_solver/) | LP/MILP | Solve from MPS file via `cuOptReadProblem` | +> **Duals:** `lp_duals` is an LP, but `cuOptGetDualSolution` / `cuOptGetReducedCosts` also return +> shadow prices and reduced costs for a **QP with linear constraints** (the barrier solver is +> primal-dual). No duals are returned for problems with **quadratic constraints** — the arrays +> come back filled with `NaN`. + ## Build and run Set include and library paths, then build and run. diff --git a/skills/cuopt-numerical-optimization-api-python/SKILL.md b/skills/cuopt-numerical-optimization-api-python/SKILL.md index a7460c7c7c..07d7e7f632 100644 --- a/skills/cuopt-numerical-optimization-api-python/SKILL.md +++ b/skills/cuopt-numerical-optimization-api-python/SKILL.md @@ -253,12 +253,18 @@ settings.set_parameter("log_to_console", 1) | QP rejected with MAXIMIZE | QP only supports MINIMIZE | Negate the objective: minimize `-f(x)` | | QP returns non-optimal | Q not PSD or variables badly scaled | Check Q is PSD; rescale variables to similar magnitudes | -## Getting Dual Values (LP only) +## Getting Dual Values (LP / QP) + +Shadow prices (`DualValue`) and reduced costs (`ReducedCost`) are returned for **LP and QP** — +cuOpt's barrier solver is primal-dual, so a QP with a quadratic objective and **linear** +constraints returns duals just like an LP. The constraints you read duals from must be linear: +cuOpt returns **no dual variables for a problem that has any quadratic constraint** (every +`DualValue`/`ReducedCost` comes back as `NaN`). MILP returns no duals. ```python if problem.Status.name == "Optimal": - constraint = problem.getConstraint("resource_a") - shadow_price = constraint.DualValue + constraint = problem.getConstraint("resource_a") # linear constraint + shadow_price = constraint.DualValue # NaN if the model has quadratic constraints print(f"Shadow price: {shadow_price}") ``` diff --git a/skills/cuopt-numerical-optimization-api-python/references/qp_examples.md b/skills/cuopt-numerical-optimization-api-python/references/qp_examples.md index 80b9802dbb..10ceba8ac9 100644 --- a/skills/cuopt-numerical-optimization-api-python/references/qp_examples.md +++ b/skills/cuopt-numerical-optimization-api-python/references/qp_examples.md @@ -123,6 +123,36 @@ if problem.Status.name == "Optimal": print(f"Objective = {problem.ObjValue:.4f}") ``` +## Reading Duals from a QP + +A QP with **linear** constraints returns shadow prices and reduced costs just like an LP +(cuOpt's barrier solver is primal-dual). A quadratic *objective* is fine; a quadratic +*constraint* is not — cuOpt returns **no duals for a problem with quadratic constraints** +(every `DualValue`/`ReducedCost` is `NaN`), so read duals only when all constraints are linear. + +```python +""" +minimize x² + y² + z² +subject to x + y + z == 10 (linear) +The equality's shadow price is the marginal change in the optimal objective +per unit increase of the right-hand side. +""" +from cuopt.linear_programming.problem import Problem, MINIMIZE + +problem = Problem("QPDuals") +x = problem.addVariable(lb=0, name="x") +y = problem.addVariable(lb=0, name="y") +z = problem.addVariable(lb=0, name="z") +problem.setObjective(x*x + y*y + z*z, sense=MINIMIZE) +problem.addConstraint(x + y + z == 10, name="budget") +problem.solve() + +if problem.Status.name in ["Optimal", "PrimalFeasible"]: + for c in problem.getConstraints(): + # 'budget' dual magnitude ≈ 6.667 (Δobjective per unit of the RHS) + print(f"{c.ConstraintName} DualValue = {c.DualValue}") +``` + ## Maximization Workaround ```python From 708f5d1c94887bdd8b41bd22cf831d604340b07f Mon Sep 17 00:00:00 2001 From: cafzal Date: Tue, 9 Jun 2026 12:25:12 -0700 Subject: [PATCH 2/3] Address review: remove duplicate Duals note from api-c assets/README The Dual values (LP/QP) scope lives in the api-c SKILL.md "Dual values (LP / QP)" section; the assets/README note restated it. Per review feedback, remove the duplicate so the skill body is the single source. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: cafzal --- skills/cuopt-numerical-optimization-api-c/assets/README.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/skills/cuopt-numerical-optimization-api-c/assets/README.md b/skills/cuopt-numerical-optimization-api-c/assets/README.md index d9bb715d00..e354988da1 100644 --- a/skills/cuopt-numerical-optimization-api-c/assets/README.md +++ b/skills/cuopt-numerical-optimization-api-c/assets/README.md @@ -11,11 +11,6 @@ LP/MILP C API reference implementations. Use as reference when building new appl | [milp_production_planning](milp_production_planning/) | MILP | Production planning with resource constraints | | [mps_solver](mps_solver/) | LP/MILP | Solve from MPS file via `cuOptReadProblem` | -> **Duals:** `lp_duals` is an LP, but `cuOptGetDualSolution` / `cuOptGetReducedCosts` also return -> shadow prices and reduced costs for a **QP with linear constraints** (the barrier solver is -> primal-dual). No duals are returned for problems with **quadratic constraints** — the arrays -> come back filled with `NaN`. - ## Build and run Set include and library paths, then build and run. From 224030d5b7aabbbcd8f9d8466f33ad711534f89f Mon Sep 17 00:00:00 2001 From: cafzal Date: Tue, 9 Jun 2026 14:21:01 -0700 Subject: [PATCH 3/3] Address review: minimize per reviewer feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Drop "shadow prices"/"reduced costs" parenthetical glosses (api-c, api-python). - Remove the barrier-solver mention — the solve method isn't part of the dual contract for the API. - Remove the standalone "Reading Duals from a QP" example; lp_duals already shows the call sequence and the SKILL.md sections state it applies to QP (this also resolves reading duals on PrimalFeasible status). Reduces the change to the minimal LP-only -> LP/QP scope correction. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: cafzal --- .../SKILL.md | 6 +--- .../SKILL.md | 9 ++---- .../references/qp_examples.md | 30 ------------------- 3 files changed, 3 insertions(+), 42 deletions(-) diff --git a/skills/cuopt-numerical-optimization-api-c/SKILL.md b/skills/cuopt-numerical-optimization-api-c/SKILL.md index 808c110ce8..9362936b88 100644 --- a/skills/cuopt-numerical-optimization-api-c/SKILL.md +++ b/skills/cuopt-numerical-optimization-api-c/SKILL.md @@ -37,11 +37,7 @@ QP uses the same library, include/lib paths, and build pattern as LP/MILP — on ## Dual values (LP / QP) -`cuOptGetDualSolution` (shadow prices) and `cuOptGetReducedCosts` (reduced costs) return values -for **LP and QP with linear constraints** — the barrier solver is primal-dual, so a quadratic -objective still yields duals. cuOpt returns **no duals for a problem with quadratic constraints** -(the returned arrays are filled with `NaN`). See [assets/lp_duals](assets/lp_duals/) for the call -sequence — it is an LP, but the same calls apply to a QP whose constraints are all linear. +`cuOptGetDualSolution` and `cuOptGetReducedCosts` return duals and reduced costs for **LP and QP**. They are not returned for a problem with quadratic constraints (the arrays are filled with `NaN`), so read them only when all constraints are linear. See [assets/lp_duals](assets/lp_duals/) for the call sequence. ## Debugging (MPS / C) diff --git a/skills/cuopt-numerical-optimization-api-python/SKILL.md b/skills/cuopt-numerical-optimization-api-python/SKILL.md index 07d7e7f632..87d3d247f9 100644 --- a/skills/cuopt-numerical-optimization-api-python/SKILL.md +++ b/skills/cuopt-numerical-optimization-api-python/SKILL.md @@ -255,17 +255,12 @@ settings.set_parameter("log_to_console", 1) ## Getting Dual Values (LP / QP) -Shadow prices (`DualValue`) and reduced costs (`ReducedCost`) are returned for **LP and QP** — -cuOpt's barrier solver is primal-dual, so a QP with a quadratic objective and **linear** -constraints returns duals just like an LP. The constraints you read duals from must be linear: -cuOpt returns **no dual variables for a problem that has any quadratic constraint** (every -`DualValue`/`ReducedCost` comes back as `NaN`). MILP returns no duals. +Duals and reduced costs are returned for **LP and QP**. They are not returned for a problem with quadratic constraints (every value comes back as `NaN`), so read them only when all constraints are linear. MILP returns no duals. ```python if problem.Status.name == "Optimal": constraint = problem.getConstraint("resource_a") # linear constraint - shadow_price = constraint.DualValue # NaN if the model has quadratic constraints - print(f"Shadow price: {shadow_price}") + print(f"Dual value: {constraint.DualValue}") # NaN if the model has quadratic constraints ``` ## Reference Models diff --git a/skills/cuopt-numerical-optimization-api-python/references/qp_examples.md b/skills/cuopt-numerical-optimization-api-python/references/qp_examples.md index 10ceba8ac9..80b9802dbb 100644 --- a/skills/cuopt-numerical-optimization-api-python/references/qp_examples.md +++ b/skills/cuopt-numerical-optimization-api-python/references/qp_examples.md @@ -123,36 +123,6 @@ if problem.Status.name == "Optimal": print(f"Objective = {problem.ObjValue:.4f}") ``` -## Reading Duals from a QP - -A QP with **linear** constraints returns shadow prices and reduced costs just like an LP -(cuOpt's barrier solver is primal-dual). A quadratic *objective* is fine; a quadratic -*constraint* is not — cuOpt returns **no duals for a problem with quadratic constraints** -(every `DualValue`/`ReducedCost` is `NaN`), so read duals only when all constraints are linear. - -```python -""" -minimize x² + y² + z² -subject to x + y + z == 10 (linear) -The equality's shadow price is the marginal change in the optimal objective -per unit increase of the right-hand side. -""" -from cuopt.linear_programming.problem import Problem, MINIMIZE - -problem = Problem("QPDuals") -x = problem.addVariable(lb=0, name="x") -y = problem.addVariable(lb=0, name="y") -z = problem.addVariable(lb=0, name="z") -problem.setObjective(x*x + y*y + z*z, sense=MINIMIZE) -problem.addConstraint(x + y + z == 10, name="budget") -problem.solve() - -if problem.Status.name in ["Optimal", "PrimalFeasible"]: - for c in problem.getConstraints(): - # 'budget' dual magnitude ≈ 6.667 (Δobjective per unit of the RHS) - print(f"{c.ConstraintName} DualValue = {c.DualValue}") -``` - ## Maximization Workaround ```python