diff --git a/supplier_selection/README.md b/supplier_selection/README.md
new file mode 100644
index 0000000..b0b7e80
--- /dev/null
+++ b/supplier_selection/README.md
@@ -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.
diff --git a/supplier_selection/supplier_sourcing_frontier_duals.ipynb b/supplier_selection/supplier_sourcing_frontier_duals.ipynb
new file mode 100644
index 0000000..053b45e
--- /dev/null
+++ b/supplier_selection/supplier_sourcing_frontier_duals.ipynb
@@ -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",
+ "
\n",
+ "
✅ GPU is enabled
\n",
+ "
{gpu_info_escaped}\n",
+ "
\n",
+ " \"\"\"))\n",
+ " return True\n",
+ " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n",
+ " display(HTML(\"\"\"\n",
+ " \n",
+ "
⚠️ GPU not detected!
\n",
+ "
This notebook requires a GPU runtime.
\n",
+ "\n",
+ "
If running in Google Colab:
\n",
+ "
\n",
+ " - Click on Runtime → Change runtime type
\n",
+ " - Set Hardware accelerator to GPU
\n",
+ " - Then click Save and Runtime → Restart runtime.
\n",
+ "
\n",
+ "\n",
+ "
If running in Docker:
\n",
+ "
\n",
+ " - Ensure you have NVIDIA Docker runtime installed (
nvidia-docker2) \n",
+ " - Run container with GPU support:
docker run --gpus all ... \n",
+ " - Or use:
docker run --runtime=nvidia ... for older Docker versions \n",
+ " - Verify GPU access:
docker run --gpus all nvidia/cuda:12.0.0-base-ubuntu22.04 nvidia-smi \n",
+ "
\n",
+ "\n",
+ "
Additional resources:
\n",
+ "
\n",
+ "
\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
+}