diff --git a/diet_optimization/README.md b/diet_optimization/README.md index 3145795..b050a32 100644 --- a/diet_optimization/README.md +++ b/diet_optimization/README.md @@ -12,6 +12,11 @@ The diet optimization notebook solves a linear programming problem where: - The diet is a mix of different foods. - The foods have different prices and nutritional values. +The notebook also demonstrates **sensitivity analysis** on the solved LP: reading constraint +**dual values** (`DualValue`) and variable **reduced costs** (`ReducedCost`) to see which +nutritional requirements drive the cost at the margin and roughly how far each unused food is +from entering the diet (local rates — see the notebook's degeneracy and units caveats). + ### 2. Diet Optimization (MILP) diff --git a/diet_optimization/diet_optimization_lp.ipynb b/diet_optimization/diet_optimization_lp.ipynb index de3e0b5..9c3ab3b 100644 --- a/diet_optimization/diet_optimization_lp.ipynb +++ b/diet_optimization/diet_optimization_lp.ipynb @@ -384,48 +384,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Adding Additional Constraints\n", + "## Sensitivity Analysis: Dual Values and Reduced Costs\n", "\n", - "Now let's demonstrate how to add additional constraints to the existing model. We'll add a constraint to limit dairy servings to at most 6.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Create LinearExpression for dairy constraint\n", - "dairy_expr = buy_vars[\"milk\"] + buy_vars[\"ice cream\"]\n", - "dairy_constraint = problem.addConstraint(dairy_expr <= 6, name=\"limit_dairy\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Solve the problem again with the new constraint\n", - "print(\"\\nSolving with dairy constraint...\")\n", - "print(f\"Problem now has {problem.NumVariables} variables and {problem.NumConstraints} constraints\")\n", + "Every variable here is continuous, so cuOpt returns dual information at the optimum — the economic read behind the plan:\n", "\n", - "start_time = time.time()\n", - "problem.solve(settings)\n", - "solve_time = time.time() - start_time\n", - "\n", - "print(f\"\\nSolve completed in {solve_time:.3f} seconds\")\n", - "print(f\"Solver status: {problem.Status.name}\")\n", - "print(f\"Objective value: ${problem.ObjValue:.2f}\")\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Solution Comparison\n", + "- Each nutrition limit carries a **dual value** — at a non-degenerate optimum, how much total cost moves per unit you tighten or relax that limit. The implication runs one way: a limit with slack prices to ~0, while a binding limit (slack ≈ 0) *can* carry a nonzero dual but need not (a binding limit with a zero dual is a form of degeneracy). And a dual is $ per *that constraint's own unit* — per kcal for calories, per mg for sodium — so raw magnitudes aren't comparable across limits; to find where renegotiating pays off most, compare the value of, say, a 1% relaxation of each limit rather than ranking raw duals.\n", + "- A food left at 0 carries a **reduced cost** — roughly how far its per-serving price must fall before it *could* enter the diet without raising total cost. Reduced costs are per serving and serving sizes are arbitrary, so compare each as a fraction of the food's own price (\"needs a 5% price cut\" vs. \"a 40% one\") rather than sorting raw values.\n", "\n", - "Let's compare the solutions before and after adding the dairy constraint to see the impact.\n" + "Two caveats keep the read honest: under degeneracy a dual is a local, one-sided rate — confirm any rate you quote with a one-unit re-solve — and cuOpt's default first-order (PDLP) path can return duals without a simplex basis, accurate only to the convergence tolerance, so tiny near-zero values are noise, not signal." ] }, { @@ -434,8 +400,17 @@ "metadata": {}, "outputs": [], "source": [ - "# Display the new solution\n", - "print_solution()" + "# Sensitivity analysis — read the LP duals at the optimum\n", + "if problem.Status.name == \"Optimal\":\n", + " print(\"Constraint duals — local marginal cost per unit of each limit (units differ per constraint):\")\n", + " for c in problem.getConstraints():\n", + " print(f\" {c.ConstraintName:14s} dual={c.DualValue:+.4f} slack={c.Slack:.4f}\")\n", + "\n", + " print(\"\\nReduced costs (variable duals) — for foods at 0, ~price drop before it could enter the diet:\")\n", + " for v in problem.getVariables():\n", + " print(f\" {v.VariableName:12s} amount={v.getValue():7.3f} reduced_cost={v.ReducedCost:+.4f}\")\n", + "else:\n", + " print(f\"No duals available — solver status is {problem.Status.name}.\")" ] }, { @@ -451,8 +426,7 @@ "3. **Define an objective function** to minimize total cost\n", "4. **Add nutritional constraints** with both lower and upper bounds\n", "5. **Solve the optimization problem** using cuOpt's high-performance solver\n", - "6. **Add additional constraints** to the existing model\n", - "7. **Analyze and compare solutions** before and after constraint modifications\n", + "6. **Read dual values and reduced costs** for a local sensitivity read — which limits drive cost at the margin, and which unused foods sit closest to entering\n", "\n", "The cuOpt Python API provides a clean, intuitive interface for building and solving optimization problems, making it easy to model complex real-world scenarios like diet optimization.\n", "\n", @@ -468,7 +442,7 @@ "metadata": {}, "source": [ "\n", - "SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n", + "SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n", "\n", "SPDX-License-Identifier: Apache-2.0\n", "\n",