Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 14 additions & 0 deletions supplier_selection/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Supplier Sourcing

This folder contains examples of how to use NVIDIA cuOpt to solve supplier sourcing problems. The notebooks solve supplier sourcing problems using the cuOpt Python API.

## Examples

### 1. Multi-Objective Supplier Sourcing (QP)

The notebook splits one order across suppliers when reliability and diversification conflict — the reliable suppliers cluster, so demanding reliability concentrates the order:

- Minimizes **concentration risk** (a quadratic `wᵀ C w` over the allocation, high within-region correlation) subject to a fully-allocated order, a per-supplier cap, and a unit-cost budget.
- Traces the **concentration vs. reliability** frontier as an ε-constraint sweep (sweep the reliability floor).
- Reads each point's **dual**: the sensitivity d(concentration)/d(reliability) — the diversification given up per point of reliability.
- Builds the dense concentration quadratic from the matrix directly (`QuadraticExpression(qmatrix=...)`) rather than term by term.
349 changes: 349 additions & 0 deletions supplier_selection/supplier_sourcing_frontier_duals.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,349 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Multi-Objective Supplier Sourcing with cuOpt Python API\n",
"\n",
"This notebook uses the cuOpt Python API to trace the **efficient frontier** of a sourcing decision — splitting one\n",
"order across suppliers when reliability and diversification genuinely conflict, and there's no single best split,\n",
"only a curve of optimal tradeoffs.\n",
"\n",
"The most reliable suppliers tend to cluster (same region, same logistics), so demanding higher reliability quietly\n",
"pushes the order toward a few correlated names — concentration risk. We trace that tradeoff with the\n",
"**ε-constraint method** and read each point's **sensitivity** (the reliability-floor dual):\n",
"\n",
"1. **Two objectives** — minimize concentration risk, maximize reliability.\n",
"2. **Keep one as the objective, constrain the other** — minimize concentration subject to `reliability ≥ ε`.\n",
"3. **Sweep ε** across the achievable reliability range; each solve is one frontier point.\n",
"4. **Read the frontier** — and, because this is a continuous QP, read each point's **dual**: the sensitivity\n",
" d(concentration)/d(reliability), how much diversification one more point of reliability costs."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Environment Setup"
]
},
{
"cell_type": "code",
"metadata": {},
"execution_count": null,
"outputs": [],
"source": [
"import subprocess\n",
"import html\n",
"from IPython.display import display, HTML\n",
"\n",
"def check_gpu():\n",
" try:\n",
" result = subprocess.run([\"nvidia-smi\"], capture_output=True, text=True, timeout=5)\n",
" result.check_returncode()\n",
" lines = result.stdout.splitlines()\n",
" gpu_info = lines[2] if len(lines) > 2 else \"GPU detected\"\n",
" gpu_info_escaped = html.escape(gpu_info)\n",
" display(HTML(f\"\"\"\n",
" <div style=\"border:2px solid #4CAF50;padding:10px;border-radius:10px;background:#e8f5e9;\">\n",
" <h3>✅ GPU is enabled</h3>\n",
" <pre>{gpu_info_escaped}</pre>\n",
" </div>\n",
" \"\"\"))\n",
" return True\n",
" except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n",
" display(HTML(\"\"\"\n",
" <div style=\"border:2px solid red;padding:15px;border-radius:10px;background:#ffeeee;\">\n",
" <h3>⚠️ GPU not detected!</h3>\n",
" <p>This notebook requires a <b>GPU runtime</b>.</p>\n",
"\n",
" <h4>If running in Google Colab:</h4>\n",
" <ol>\n",
" <li>Click on <b>Runtime → Change runtime type</b></li>\n",
" <li>Set <b>Hardware accelerator</b> to <b>GPU</b></li>\n",
" <li>Then click <b>Save</b> and <b>Runtime → Restart runtime</b>.</li>\n",
" </ol>\n",
"\n",
" <h4>If running in Docker:</h4>\n",
" <ol>\n",
" <li>Ensure you have <b>NVIDIA Docker runtime</b> installed (<code>nvidia-docker2</code>)</li>\n",
" <li>Run container with GPU support: <code>docker run --gpus all ...</code></li>\n",
" <li>Or use: <code>docker run --runtime=nvidia ...</code> for older Docker versions</li>\n",
" <li>Verify GPU access: <code>docker run --gpus all nvidia/cuda:12.0.0-base-ubuntu22.04 nvidia-smi</code></li>\n",
" </ol>\n",
"\n",
" <p><b>Additional resources:</b></p>\n",
" <ul>\n",
" <li><a href=\"https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html\" target=\"_blank\">NVIDIA Container Toolkit Installation Guide</a></li>\n",
" </ul>\n",
" </div>\n",
" \"\"\"))\n",
" return False\n",
"\n",
"check_gpu()"
]
},
{
"cell_type": "code",
"metadata": {},
"execution_count": null,
"outputs": [],
"source": [
"# Uncomment for your CUDA version if cuOpt is not already installed (e.g., Google Colab):\n",
"# !pip install --upgrade --extra-index-url https://pypi.nvidia.com cuopt-cu12 # CUDA 12\n",
"# !pip install --upgrade --extra-index-url https://pypi.nvidia.com cuopt-cu13 # CUDA 13"
]
},
{
"cell_type": "code",
"metadata": {},
"execution_count": null,
"outputs": [],
"source": [
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from cuopt.linear_programming.problem import Problem, QuadraticExpression, MINIMIZE\n",
"print(\"Imports ready (cuOpt QP solver)\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 1 — the suppliers and their two objectives\n",
"\n",
"A simulated panel of 12 suppliers across 3 regions. Each has a **reliability** score (to maximize) and a **unit\n",
"cost**; the reliable suppliers are pricier and cluster in one region. The **concentration risk** is a quadratic\n",
"form `wᵀ C w` over the allocation `w`: `C` has a high within-region correlation and near-zero across regions, so\n",
"piling the order into one region—even across different suppliers there—reads as concentrated. Minimizing it is\n",
"what pushes the order to spread across regions (multi-sourcing)."
]
},
{
"cell_type": "code",
"metadata": {},
"execution_count": null,
"outputs": [],
"source": [
"np.random.seed(7)\n",
"\n",
"regions = [\"A\", \"B\", \"C\"]\n",
"region_of = [r for r in regions for _ in range(4)] # 12 suppliers, 4 per region\n",
"suppliers = [f\"{r}{i+1}\" for r in regions for i in range(4)]\n",
"n = len(suppliers)\n",
"\n",
"# Reliable suppliers cluster in region A (and cost more); region C is cheap but less reliable.\n",
"base_rel = {\"A\": 0.95, \"B\": 0.85, \"C\": 0.75}\n",
"base_cost = {\"A\": 12.0, \"B\": 9.0, \"C\": 6.0}\n",
"reliability = np.array([np.clip(base_rel[r] + np.random.normal(0, 0.02), 0.5, 0.99) for r in region_of])\n",
"unit_cost = np.array([max(2.0, base_cost[r] + np.random.normal(0, 1.0)) for r in region_of])\n",
"\n",
"# Concentration matrix: 1.0 on the diagonal, 0.6 within region, 0.05 across (dense, PSD).\n",
"within, across = 0.6, 0.05\n",
"concentration = np.array([[1.0 if i == j else (within if region_of[i] == region_of[j] else across)\n",
" for j in range(n)] for i in range(n)])\n",
"\n",
"summary = pd.DataFrame({\"Region\": region_of, \"Reliability\": reliability, \"Unit Cost\": unit_cost}, index=suppliers)\n",
"summary.style.format({\"Reliability\": \"{:.3f}\", \"Unit Cost\": \"${:.2f}\"})"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 2 — minimize concentration risk, capturing the reliability-floor dual\n",
"\n",
"The model splits one order: weights `w` sum to 1, no supplier over a cap, total unit cost within budget. The\n",
"objective is the concentration quadratic `wᵀ C w`; the swept ε-constraint is a **reliability floor** whose\n",
"**`.DualValue`** we keep after each solve. For a continuous QP that dual is the **sensitivity**\n",
"d(concentration)/d(reliability) — the diversification cost of demanding one more point of reliability.\n",
"\n",
"**Building the quadratic — matrix vs term-by-term.** A dense quadratic can be built term by\n",
"term, one Python expression per matrix entry (`for i: for j: quad += c * w[i] * w[j]`) — fine for a small dense\n",
"matrix, but O(n²) Python objects as it grows. cuOpt's `QuadraticExpression` also takes the matrix **directly**\n",
"(`qmatrix=C, qvars=w`), a single vectorized construction. Both express the same `wᵀ C w`; the matrix form is the\n",
"one to reach for on a dense `C`. We use it here."
]
},
{
"cell_type": "code",
"metadata": {},
"execution_count": null,
"outputs": [],
"source": [
"def solve_min_concentration_qp_dual(concentration, reliability, unit_cost, budget,\n",
" target_reliability=None, max_weight=0.22):\n",
" \"\"\"Solve the min-concentration sourcing QP and return the split plus the reliability-floor dual.\n",
"\n",
" Minimizes concentration risk (w' * concentration * w) subject to a fully-allocated order\n",
" (weights sum to 1), a per-supplier cap, a unit-cost budget, and — when target_reliability is\n",
" given — a minimum-reliability epsilon-constraint. That constraint's .DualValue is the\n",
" sensitivity d(concentration)/d(reliability).\n",
"\n",
" Parameters\n",
" ----------\n",
" concentration : ndarray (n, n)\n",
" Concentration-risk matrix (symmetric, positive semidefinite).\n",
" reliability : ndarray (n,)\n",
" Per-supplier reliability scores.\n",
" unit_cost : ndarray (n,)\n",
" Per-supplier unit cost.\n",
" budget : float\n",
" Maximum allowed weighted unit cost (sum of cost_i * w_i).\n",
" target_reliability : float, optional\n",
" Minimum weighted reliability (the swept epsilon-constraint); None = unconstrained.\n",
" max_weight : float\n",
" Upper bound on each supplier's share of the order.\n",
"\n",
" Returns\n",
" -------\n",
" dict\n",
" {\"weights\", \"concentration\", \"reliability\", \"cost\", \"dual\", \"active\", \"status\"}.\n",
" \"\"\"\n",
" n = len(reliability)\n",
" prob = Problem(\"Supplier_Sourcing\")\n",
" w = [prob.addVariable(lb=0.0, ub=float(max_weight), name=f\"w_{i}\") for i in range(n)]\n",
"\n",
" # Concentration quadratic w' C w, built from the matrix directly (vs term-by-term).\n",
" quad = QuadraticExpression(qmatrix=concentration, qvars=w)\n",
" prob.setObjective(quad, sense=MINIMIZE)\n",
"\n",
" prob.addConstraint(sum(w) == 1, name=\"fully_allocated\")\n",
" prob.addConstraint(sum(float(unit_cost[i]) * w[i] for i in range(n)) <= float(budget), name=\"budget\")\n",
" rel_con = None\n",
" if target_reliability is not None:\n",
" rel_expr = sum(float(reliability[i]) * w[i] for i in range(n))\n",
" rel_con = prob.addConstraint(rel_expr >= float(target_reliability), name=\"min_reliability\")\n",
"\n",
" prob.solve()\n",
" status = prob.Status.name if hasattr(prob.Status, \"name\") else str(prob.Status)\n",
" weights = np.array([w[i].Value for i in range(n)])\n",
" conc = float(weights @ concentration @ weights)\n",
" dual = abs(float(rel_con.DualValue)) if rel_con is not None else 0.0 # sensitivity d(conc)/d(reliability)\n",
" return {\"weights\": weights, \"concentration\": conc, \"reliability\": float(reliability @ weights),\n",
" \"cost\": float(unit_cost @ weights), \"dual\": dual,\n",
" \"active\": int((weights > 1e-4).sum()), \"status\": status}\n",
"\n",
"BUDGET = 11.5\n",
"base = solve_min_concentration_qp_dual(concentration, reliability, unit_cost, BUDGET)\n",
"print(f\"Min-concentration split: status={base['status']}, concentration={base['concentration']:.4f}, \"\n",
" f\"reliability={base['reliability']:.3f}, active suppliers={base['active']}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 3 — sweep the reliability floor → the frontier (and its duals)\n",
"\n",
"Sweep the reliability floor ε from the unconstrained mix’s reliability up toward the achievable ceiling; each ε is\n",
"one standard cuOpt solve. Floors past what the caps and budget allow are simply infeasible — we skip them and keep\n",
"the feasible frontier."
]
},
{
"cell_type": "code",
"metadata": {},
"execution_count": null,
"outputs": [],
"source": [
"rel_min = base[\"reliability\"]\n",
"rel_max = float(reliability.max())\n",
"targets = np.linspace(rel_min + 0.002, rel_max * 0.999, 25)\n",
"\n",
"rels, concs, duals, actives, flagged = [], [], [], [], 0\n",
"for t in targets:\n",
" r = solve_min_concentration_qp_dual(concentration, reliability, unit_cost, BUDGET, target_reliability=t)\n",
" if r[\"status\"] not in (\"Optimal\", \"PrimalFeasible\"):\n",
" continue # floor beyond the caps/budget — infeasible, skip\n",
" if r[\"status\"] != \"Optimal\":\n",
" flagged += 1\n",
" rels.append(r[\"reliability\"]); concs.append(r[\"concentration\"])\n",
" duals.append(r[\"dual\"]); actives.append(r[\"active\"])\n",
"\n",
"rels, concs, duals, actives = map(np.array, (rels, concs, duals, actives))\n",
"print(f\"frontier points: {len(rels)} | not certified-Optimal (PrimalFeasible): {flagged}\")\n",
"print(f\"Active suppliers: {actives.max()} (most diversified) -> {actives.min()} (most concentrated) as the floor rises\")\n",
"print(f\"Sensitivity d(concentration)/d(reliability): {duals.min():.3f} -> {duals.max():.3f}\")"
]
},
{
"cell_type": "code",
"metadata": {},
"execution_count": null,
"outputs": [],
"source": [
"fig, axes = plt.subplots(1, 2, figsize=(13, 5))\n",
"sc = axes[0].scatter(rels * 100, concs, c=actives, cmap=\"viridis\", s=60, zorder=3)\n",
"axes[0].plot(rels * 100, concs, \"-\", color=\"navy\", lw=1.0, alpha=0.5, zorder=2)\n",
"axes[0].set_xlabel(\"Required reliability (%)\"); axes[0].set_ylabel(\"Concentration risk (wᵀ C w)\")\n",
"axes[0].set_title(\"Sourcing frontier (concentration vs reliability)\"); axes[0].grid(alpha=0.3)\n",
"fig.colorbar(sc, ax=axes[0], label=\"active suppliers\")\n",
"\n",
"axes[1].plot(rels * 100, duals, \"o-\", color=\"purple\", lw=1.6)\n",
"axes[1].set_xlabel(\"Required reliability (%)\"); axes[1].set_ylabel(\"Sensitivity d(concentration)/d(reliability)\")\n",
"axes[1].set_title(\"Marginal diversification cost of reliability (cuOpt QP dual)\"); axes[1].grid(alpha=0.3)\n",
"plt.tight_layout(); plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 4 — read the frontier\n",
"\n",
"- The **frontier** (left) is the concentration-vs-reliability Pareto set — each point the least-concentrated split\n",
" that meets its reliability floor. There's no single \"best\"; you choose where to sit. The color shows the order\n",
" spreading across **fewer suppliers** as you demand more reliability — the reliable names cluster, so reliability\n",
" and diversification pull apart.\n",
"- The **dual** (right) is the **sensitivity** d(concentration)/d(reliability): the diversification given up for one\n",
" more point of reliability. It steepens along the frontier — the marginal cost of reliability rises, which is\n",
" exactly where a knee analysis pays off.\n",
"\n",
"### Takeaway — reusing this on your own problem\n",
"Two competing objectives and a solver for one of them is all you need: keep one objective, turn the other into a\n",
"swept constraint (`f₂ ≥ ε` or `≤ ε`), solve across the range, collect the non-dominated points, and — for an LP or\n",
"QP — read the constraint's dual for the marginal exchange rate. The **budget** here is a fixed constraint, but it\n",
"carries a dual too: re-read `budget`'s `.DualValue` for the diversification bought per dollar of budget. And when\n",
"the objective is a dense quadratic, build it from the matrix (`QuadraticExpression(qmatrix=...)`) rather than term\n",
"by term."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## License\n",
"\n",
"SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n",
"SPDX-License-Identifier: Apache-2.0\n",
"\n",
"Licensed under the Apache License, Version 2.0 (the \"License\");\n",
"you may not use this file except in compliance with the License.\n",
"You may obtain a copy of the License at\n",
"\n",
"http://www.apache.org/licenses/LICENSE-2.0\n",
"\n",
"Unless required by applicable law or agreed to in writing, software\n",
"distributed under the License is distributed on an \"AS IS\" BASIS,\n",
"WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n",
"See the License for the specific language governing permissions and\n",
"limitations under the License."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 5
}