Skip to main content
ClaudeWave
Skill3.2k repo starsupdated 3d ago

cuopt-multi-objective-exploration

Trace, complete, and interpret the Pareto frontier across competing objectives using repeated single-objective cuOpt solves (weighted-sum and ε-constraint).

Install in Claude Code
Copy
git clone --depth 1 https://github.com/NVIDIA/skills /tmp/cuopt-multi-objective-exploration && cp -r /tmp/cuopt-multi-objective-exploration/skills/cuopt-multi-objective-exploration ~/.claude/skills/cuopt-multi-objective-exploration
Then start a new Claude Code session; the skill loads automatically.

SKILL.md

# 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 — define the objectives

An informative frontier needs objectives that genuinely conflict: if they don't pull against each other, it collapses to a single point with nothing to trade off. And each objective has to be formulated correctly, since a wrong form, sense, or scale distorts the tradeoff and shifts where the knee falls. Formulate each one with `cuopt-numerical-optimization-formulation` before sweeping.

## Step 2 — 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:

```text
              f1        f2        f3
min f1   →   f1*       f2(at f1*) f3(at f1*)
min f2   →   ...       f2*        ...
min f3   →   ...       ...        f3*
```

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 3 — choose a scalarization

### Weighted sum

Combine the objectives into one and sweep the weights:

```text
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:

```text
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: add it as a quadratic constraint `xᵀQx ≤ ε`, which cuOpt supports. Non-convex or equality quadratic constraints are unsupported, and the MILP path stays linear-constraint only.

Spot it in existing code: a hand-coded loop over a target or budget value (a return target, a