Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
211eadf
Add cuopt-multi-objective-exploration skill (draft)
cafzal May 30, 2026
850b1da
Register cuopt-multi-objective-exploration in skills index
cafzal May 30, 2026
0064715
Link Pareto-frontier exploration from the goal-programming pattern
cafzal Jun 1, 2026
d39c9b0
Address review: drop dangling refs, reconcile MILP gap note
cafzal Jun 1, 2026
63089de
Update BENCHMARK caveat for the added MILP gap note (two → three clar…
cafzal Jun 1, 2026
10c3d13
Add constraint-objective, recognition-cue, and verify notes to the skill
cafzal Jun 1, 2026
6b725e2
Align skill and benchmark after the post-A/B edits
cafzal Jun 1, 2026
b3a7ce5
Fact-check fixes against the cuOpt codebase and the A/B instance
cafzal Jun 1, 2026
dca4c97
Mark SOCP / quadratic-constraint path as beta in the skill
cafzal Jun 1, 2026
5e65df1
Correct NVSkills-Eval process description (maintainer-run, non-fork)
cafzal Jun 1, 2026
e305498
Fix remaining 'runs on the fork' note in skill-card results section
cafzal Jun 1, 2026
4390c2c
Drop hand-written BENCHMARK.md and skill-card.md
cafzal Jun 2, 2026
7b70227
Merge branch 'main' into multi-objective-exploration-skill
cafzal Jun 3, 2026
44c008c
Add language identifiers to SKILL.md code fences (markdownlint MD040)
cafzal Jun 3, 2026
bb6add8
Address review (mlubin): drop SOCP beta label; generalize objective g…
cafzal Jun 3, 2026
3fcd84b
Re-tailor 'Getting each objective right' to the multi-objective case …
cafzal Jun 3, 2026
031e3d8
Merge branch 'main' into multi-objective-exploration-skill
cafzal Jun 3, 2026
2474dc8
Fix tradeoff spelling consistency (trade-off -> tradeoff)
cafzal Jun 3, 2026
1680336
Restructure body into a numbered workflow; make API references descri…
cafzal Jun 3, 2026
2ecb994
Remove throat-clearing intro line from Step 5 (interpret)
cafzal Jun 4, 2026
93bd461
Drop vestigial (discipline) parenthetical from Step 5 heading
cafzal Jun 4, 2026
abbafe7
Remove formulation skill's reference to cuopt-multi-objective-explora…
cafzal Jun 4, 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
6 changes: 6 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@
"skills": "./",
"description": "LP, MILP, and QP (beta) with cuOpt — CLI only (MPS files, cuopt_cli). Use when the user is solving LP, MILP, or QP from MPS via command line."
},
{
"name": "cuopt-multi-objective-exploration",
"source": "./skills/cuopt-multi-objective-exploration",
"skills": "./",
"description": "Trace and interpret the Pareto frontier across competing objectives using repeated single-objective cuOpt solves (weighted-sum and ε-constraint)."
},
{
"name": "cuopt-routing-formulation",
"source": "./skills/cuopt-routing-formulation",
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ AI agent skills for NVIDIA cuOpt optimization engine. Skills live in **`skills/`

### Common (concepts only; no API code)
- `skills/cuopt-numerical-optimization-formulation/` — LP / MILP / QP: concepts + problem parsing + common formulation patterns
- `skills/cuopt-multi-objective-exploration/` — Multi-objective: trace + interpret the Pareto frontier across competing objectives (ε-constraint / weighted-sum over repeated cuOpt solves)
- `skills/cuopt-routing-formulation/` — Routing: VRP, TSP, PDP (problem types, data)
- `skills/cuopt-server-common/` — Server: capabilities, workflow

Expand Down
135 changes: 135 additions & 0 deletions skills/cuopt-multi-objective-exploration/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
---
name: cuopt-multi-objective-exploration
version: "26.08.00"
description: Trace and interpret the Pareto frontier across competing objectives using repeated single-objective cuOpt solves (weighted-sum and ε-constraint).
license: Apache-2.0
origin: cuopt-skill-evolution
metadata:
author: NVIDIA cuOpt Team
tags:
- multi-objective
- pareto
- epsilon-constraint
- tradeoff
- workflow
---


# Multi-Objective Exploration

cuOpt optimizes **one** objective per solve. Many real problems have several objectives that pull against each other — cost vs. service level, return vs. risk, makespan vs. overtime, distance vs. vehicle count. A single solve answers "what's optimal *for one particular weighting*," but it hides the tradeoff the user actually needs to see.

This skill turns a sequence of single-objective cuOpt solves into a **Pareto frontier** — the set of solutions where you can't improve one objective without giving up another — and gives the discipline to read it. It adds no solver features; it orchestrates the LP / MILP / QP solves already covered by the formulation and API skills.

## When this applies

Reach for this workflow when the problem has **two or more objectives with no agreed-upon weighting**, signalled by language like:

- "balance X and Y", "trade off", "as cheap as possible *without* hurting service"
- "minimize cost *and* maximize coverage", "I want options, not one answer"
- any objective the user is willing to relax in exchange for another

If there is a single clear objective (everything else is a hard constraint), this skill does not apply — formulate and solve once.

## Core idea — one solve is one point on a curve

A single optimum encodes **one implicit weighting** of the objectives. Change the weighting and the optimum moves. The frontier is the curve traced by all the non-dominated optima.

A solution **A dominates** B when A is at least as good on every objective and strictly better on one. Dominated solutions are never worth choosing. The **Pareto frontier** is exactly the non-dominated set; the user's job is to pick a point on it, and yours is to show them the whole curve plus where the tradeoff is sharpest.

Do not collapse a multi-objective problem to a single weighted number and report its optimum as "the answer" — that silently makes the tradeoff decision *for* the user. Trace the frontier and let them choose.

Objectives and constraints are interchangeable. A requirement currently treated as fixed — a coverage floor, a fairness cap, a budget — is often a latent objective: its level was assumed, not given. Promoting such a constraint to a parametric ε-constraint and sweeping it reveals a tradeoff you'd otherwise hide, so read a single-objective model's hard constraints as candidate objectives, not just limits — but only when the level was an assumption. A genuinely fixed, non-negotiable limit (a hard budget cap, a regulatory minimum) stays a constraint; don't manufacture a tradeoff that isn't there. Express any promoted quantity linearly so it can serve as an ε-constraint (see `cuopt-numerical-optimization-formulation`).

## Step 1 — build a payoff table (anchor each objective)

Solve each objective **on its own** first. For *k* objectives this is *k* solves. Record, for each, the value of every objective at that optimum:

```
f1 f2 f3
min f1 → f1* f2(at f1*) f3(at f1*)
min f2 → ... f2* ...
min f3 → ... ... f3*
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

The diagonal (`f1*`, `f2*`, …) is each objective's best achievable value; the off-diagonals give the **range** each objective spans across the others' optima. This table does double duty:

- It sets the **sweep bounds** for the ε-constraint method (the feasible range of each constrained objective).
- It supplies the **scales** for normalization — objectives in dollars, percent, and hours can't be weighted meaningfully until divided by their ranges.

If any single-objective solve is already infeasible, stop and fix the model before sweeping — the frontier doesn't exist yet.

## Step 2 — choose a scalarization

### Weighted sum

Combine the objectives into one and sweep the weights:

```
minimize w1·f1(x) + w2·f2(x) + ... , for a grid of weight vectors w
```

Cheap and trivial with any solver. Two limitations to respect:

- **It only finds points on the convex hull of the frontier.** Concave (non-convex) regions of the frontier are unreachable no matter how you choose weights, and for MILP the reachable points can be sparse with large gaps. A frontier that looks suspiciously linear or has only a few clustered points is the symptom.
- **Weights are not priorities until the objectives are normalized.** Divide each `f_k` by its payoff-table range first; otherwise the largest-magnitude objective dominates regardless of intent.

### ε-constraint (preferred for a complete frontier)

Keep one objective; move the rest to constraints and sweep their right-hand sides:

```
minimize f1(x)
subject to f2(x) ≤ ε2
f3(x) ≤ ε3
(original constraints)
```

Sweep each `ε_k` across the range from the payoff table. Each `(ε2, ε3, …)` combination is a single standard cuOpt solve. This recovers the **full** frontier, including the concave regions weighted-sum cannot reach, which is why it's the default when completeness matters. The cost is more solves (a grid over the constrained objectives) and bookkeeping of the ε values.

ε-constrain *linear* objectives directly. A quadratic objective (e.g. risk `xᵀΣx`) is simplest kept as the objective `f1` while you ε-constrain the linear ones. A **convex** quadratic objective *can* instead be ε-constrained directly: cuOpt routes a `xᵀQx ≤ ε` constraint (Q positive semidefinite, inequality only) through the barrier solver as a second-order cone (`add_quadratic_constraint`) — though SOCP support is **beta**. Non-convex or equality quadratic constraints are unsupported, and the MILP path stays linear-constraint only.
Comment thread
cafzal marked this conversation as resolved.
Outdated

Spot it in existing code: a hand-coded loop over a target or budget value (a return target, a cost cap) is already the ε-constraint method — name it as such, filter dominated points, and read the swept constraint's dual (LP/QP only).

**Picking a method:** weighted-sum for a quick convex sketch or when you know the frontier is convex (e.g. a pure-LP/QP tradeoff); ε-constraint when the problem is MILP, when the frontier may be non-convex, or when the user needs a faithful and complete curve.

## Step 3 — sweep, collect, and filter

```
frontier = []
for each weight vector (or ε vector) in the grid:
set the combined objective (or ε right-hand sides)
solve with cuOpt # reuse the prior solution as a warm start
if status is Optimal/Feasible:
record (objective values, solution)
discard dominated and duplicate points
sort the survivors to form the frontier
```

Practical notes:

- **Warm-start LP sweeps.** For an LP frontier, reuse the previous solve's PDLP warmstart data to cut solve time (`getWarmstartData` → `set_pdlp_warm_start_data`). Per cuOpt this is **LP-only**: a MILP solve doesn't take a PDLP warmstart (you can optionally seed a MIP start instead). See `cuopt-numerical-optimization-api-python`.
- **Cap each MILP solve.** Set a per-solve time limit on MILP sweeps (see `cuopt-numerical-optimization-api-python`) — a sweep is many solves, and branch-and-bound can over-spend certifying optimality past a tiny gap, while cuOpt sets no limit by default and won't warn. Report the points as optimal *to the gap you set*, not certified optimal.
- **Filter dominated points.** A correct sweep can still emit dominated points (especially weighted-sum near the hull, or MILP). Drop them; they are not part of the frontier.
- **Resolution is a budget.** Curve fidelity trades against solve count. Start coarse to see the shape, then refine the grid only where the curve bends.
- **Verify, don't assume.** When you claim one method beats another, measure it — e.g. count the efficient points ε-constraint recovered that weighted-sum missed — rather than asserting it; and flag any solve returning feasible-but-not-`Optimal` so a non-certified point is never read as exact.

## Step 4 — interpret the frontier (discipline)

Producing the curve is half the work; reading it correctly is the other half.

- **Report tradeoffs, not single numbers.** A frontier point means nothing in isolation. Quote the exchange rate — "≈ $4k of extra cost per 1% of added coverage in this region" — so the user can judge whether a move is worth it.
- **Flag knee points; don't auto-pick them.** The "knee" is where the curve bends most sharply — beyond it you pay a lot for a little. It's often the best-balanced compromise and worth highlighting, but the final choice is the user's preference, not a rule.
- **Treat dominated or gappy output as a diagnostic.** If dominated points survive filtering, or the frontier is implausibly sparse or perfectly linear, suspect the sweep or the model — most often weighted-sum hiding a concave region (switch to ε-constraint) or a normalization mistake.
- **State the weighting/ε you used.** Every reported point is conditional on its scalarization. Make that explicit so a single solve is never mistaken for "the" optimum.

## Getting each objective right

The frontier is only as correct as the objectives feeding it. Formulate each one with `cuopt-numerical-optimization-formulation` before sweeping. One trap matters especially here: a **risk / volatility** objective is the quadratic form `xᵀΣx`, **not** a linear sum of per-asset volatility — modeling it linearly distorts the entire return-vs-risk frontier. See that skill's *Typical QP use cases* (portfolio variance) and *QP rule: minimize only*.
Comment thread
cafzal marked this conversation as resolved.
Outdated

## Interfaces

This skill is solver- and interface-agnostic. The per-solve mechanics — building the objective, adding the ε constraints, passing a warm start, reading status — live in the API skills:

- `cuopt-numerical-optimization-api-python` / `-api-c` / `-api-cli` — LP, MILP, QP solves.
- `cuopt-routing-formulation` + `cuopt-routing-api-python` — the same frontier workflow applies to routing tradeoffs (distance vs. vehicles vs. time).
42 changes: 42 additions & 0 deletions skills/cuopt-multi-objective-exploration/evals/evals.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
[
{
"id": "multiobj-explore-eval-001-supplier-interpretation",
"question": "A procurement lead is sourcing a component. The candidate suppliers lie on a cost-vs-reliability tradeoff (cheaper tends to be less reliable): CN03 cost $7.05 reliability 81.1; SEA03 $7.63 / 82.6; LA04 $9.93 / 87.2; EU04 $11.29 / 88.2; NA01 $11.74 / 90.3; NA03 $12.33 / 91.0; NA04 $13.37 / 91.1. She asks: 'Which suppliers should we commit to?' Advise her.",
"expected_skill": "cuopt-multi-objective-exploration",
"expected_script": null,
"ground_truth": "The agent treats this as a two-objective tradeoff with no fixed weighting. It does NOT collapse to a single 'best' supplier; it lays out the cost/reliability tradeoff, quotes the exchange rate (e.g. roughly how much cost per reliability point between adjacent options), flags the knee/balanced region, states the assumption behind any option it highlights, and leaves the final pick to the lead (often a diversified mix rather than one supplier).",
"expected_behavior": [
"Frames it as two competing objectives (cost vs reliability) with no agreed weighting",
"Does NOT declare one supplier as THE answer; preserves a genuine choice across options",
"Quantifies the tradeoff as an exchange rate (cost per unit of reliability, or vice versa)",
"Flags a knee / balanced region but leaves the final pick to the lead",
"States where on the tradeoff any specific option it names sits"
]
},
{
"id": "multiobj-explore-eval-002-supplier-exploration",
"question": "A procurement lead must choose which of 12 candidate suppliers to contract to maximize total supply-chain resilience and minimize total annual cost, while covering required demand. There is no agreed weighting between resilience and cost. Using cuOpt, how would you approach this and what would you report back?",
"expected_skill": "cuopt-multi-objective-exploration",
"expected_script": null,
"ground_truth": "The agent recognizes a multi-objective (resilience vs cost) selection problem with no fixed weighting and a hard demand constraint, builds a payoff table by solving each objective alone for ranges, then traces the Pareto frontier with repeated single-objective cuOpt solves. Because the supplier selection is combinatorial (non-convex), it prefers the epsilon-constraint method (e.g. minimize cost subject to a resilience floor, sweeping the floor) over weighted-sum, which would miss non-supported portfolios. It filters dominated points and reports the tradeoff curve plus the knee, deferring the final pick, and defers per-solve mechanics to the api-* skills and formulation to cuopt-numerical-optimization-formulation.",
"expected_behavior": [
"Recognizes two competing objectives with no agreed weighting; does NOT collapse to one weighted optimum",
"Builds a payoff table (each objective alone) for ranges/normalization",
"Traces the Pareto frontier via repeated single-objective cuOpt solves; prefers epsilon-constraint over weighted-sum for completeness on a non-convex/MILP problem",
"Filters dominated and duplicate portfolios",
"Reports the tradeoff and flags the knee, leaving the final pick to the lead"
]
},
{
"id": "multiobj-explore-eval-003-single-objective-decoy",
"question": "A procurement lead must contract suppliers to MAXIMIZE total supply-chain resilience, but the annual budget is hard-capped: total cost must not exceed $34 (a firm limit, not negotiable), and the set must cover demand. Which suppliers should we contract?",
"expected_skill": null,
"expected_script": null,
"ground_truth": "DECOY (negative) — the multi-objective-exploration skill should NOT activate. There is a single clear objective (maximize resilience) with the cost as a hard constraint, so there is no tradeoff to explore. The correct response is a single optimization (maximize resilience subject to cost <= 34 and demand coverage) returning ONE recommended supplier set, not a Pareto sweep or a range of options.",
"expected_behavior": [
"Recognizes a single objective with a hard constraint, not a tradeoff",
"Does NOT trace a frontier or sweep the budget as if it were a tradeoff dial",
"Returns one recommended supplier set (one solve), citing the binding budget and demand coverage"
]
}
]
6 changes: 6 additions & 0 deletions skills/cuopt-numerical-optimization-formulation/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,12 @@ Goal programming optimizes multiple objectives in priority order. Implement it a

Deviation variables (d⁻, d⁺) and slack/idle-time variables are always **continuous**. However, **decision variables must still be INTEGER when they represent discrete/countable quantities** (units produced, vehicles, workers, etc.). Do not let the presence of continuous deviation variables cause you to make all variables continuous — the integrality of decision variables directly affects feasibility and objective values.

### Multiple objectives with no fixed priority

Goal programming (above) needs a **priority order** and returns **one** prioritized solution. When objectives genuinely conflict and there is **no fixed priority or weighting** — the user wants to see the tradeoffs and choose — don't pick one weighting up front. Trace the **Pareto frontier**: keep one objective and sweep the others as parametric ε-constraints (or sweep weighted-sum weights), then filter to the non-dominated set. On integer / non-convex problems prefer ε-constraint — weighted-sum provably misses unsupported efficient points.

For the full workflow (anchor each objective → sweep → filter → read the frontier with exchange rates and the knee) see the **`cuopt-multi-objective-exploration`** skill.
Comment thread
rgsl888prabhu marked this conversation as resolved.
Outdated

### Multi-period inventory / purchasing models

In problems with buying, selling, and warehouse capacity over multiple periods, decide which capacity constraints to include based on the problem's timing assumptions.
Expand Down