diff --git a/diet_optimization/README.md b/diet_optimization/README.md index 3145795..f5632de 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 +**shadow prices** (`DualValue`) and variable **reduced costs** (`ReducedCost`) to see which +nutritional requirements drive the cost and how far each unused food is from entering the diet, +then adding a constraint and reading *its* shadow price (the marginal cost of the cap). + ### 2. Diet Optimization (MILP) diff --git a/diet_optimization/diet_optimization_lp.ipynb b/diet_optimization/diet_optimization_lp.ipynb index de3e0b5..f48fd94 100644 --- a/diet_optimization/diet_optimization_lp.ipynb +++ b/diet_optimization/diet_optimization_lp.ipynb @@ -13,7 +13,7 @@ "We need to select quantities of different foods to:\n", "- Meet minimum and maximum nutritional requirements\n", "- Minimize total cost\n", - "- Satisfy additional constraints (like limiting dairy servings)\n", + "- Satisfy additional constraints (like limiting red-meat servings)\n", "\n", "The nutrition guidelines are based on USDA Dietary Guidelines for Americans, 2005.\n" ] @@ -380,13 +380,44 @@ "print_solution()\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Sensitivity Analysis: Duals and Reduced Costs\n", + "\n", + "Because every variable here is continuous, this is a linear program, and cuOpt returns dual information at the optimum — the economic \"why\" behind the plan:\n", + "\n", + "- **Constraint dual** — the sensitivity of the optimal cost to a constraint's bound: how much the minimum cost changes per unit change in that bound (traditionally called the *shadow price*). A *binding* constraint has a nonzero dual; a slack one is approximately zero.\n", + "- **Reduced cost (variable dual)** — for a food left out of the diet (amount 0), how much its per-serving price would have to fall before buying it could lower total cost." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Sensitivity analysis — read the LP duals at the optimum\n", + "if problem.Status.name == \"Optimal\":\n", + " print(\"Constraint duals — change in cost per unit change in the bound:\")\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 needed to 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}.\")" + ] + }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Adding Additional Constraints\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" + "Now let's add a constraint to the existing model and read its **dual** — the sensitivity of cost to that constraint. We'll cap **hamburger + hot dog at 0.4 servings** combined — say, a red-meat preference — and re-solve." ] }, { @@ -395,9 +426,9 @@ "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\")" + "# Limit combined hamburger + hot dog servings (a red-meat preference)\n", + "meat_expr = buy_vars[\"hamburger\"] + buy_vars[\"hot dog\"]\n", + "meat_constraint = problem.addConstraint(meat_expr <= 0.4, name=\"limit_meat\")" ] }, { @@ -407,7 +438,7 @@ "outputs": [], "source": [ "# Solve the problem again with the new constraint\n", - "print(\"\\nSolving with dairy constraint...\")\n", + "print(\"\\nSolving with the meat constraint...\")\n", "print(f\"Problem now has {problem.NumVariables} variables and {problem.NumConstraints} constraints\")\n", "\n", "start_time = time.time()\n", @@ -419,13 +450,35 @@ "print(f\"Objective value: ${problem.ObjValue:.2f}\")\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### The Dual of the Meat Cap\n", + "\n", + "Capping hamburger and hot dog tightens the model and nudges the cost up. The cap's **dual** gives the marginal cost of that limit directly — how much total cost changes per additional combined serving you allow — without re-solving at every level." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Marginal cost of the meat cap, read straight off its dual\n", + "if problem.Status.name == \"Optimal\":\n", + " print(f\"limit_meat dual: {meat_constraint.DualValue:+.4f} \"\n", + " f\"(cost change per +1 combined serving of hamburger/hot dog allowed)\")\n", + " print(f\"slack on the cap: {meat_constraint.Slack:.4f} (0 means the cap is binding)\")" + ] + }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Solution Comparison\n", "\n", - "Let's compare the solutions before and after adding the dairy constraint to see the impact.\n" + "Let's compare the solutions before and after adding the meat cap to see the impact." ] }, { @@ -468,7 +521,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",