Skip to content

Diet Problem & Sensitivity Analysis (LP Duals & Slacks) ​

Problem ClassSolverKey APIsLinks
Linear Programming (LP)HiGHS (default)m.continuous(), m.constraint(), c, minimize, dot

When solving a Linear Programming model, the optimal variable values (var.value) are only half the story. In practice, decision-makers need to know:

  1. Bottlenecks (constr.dual — Shadow Price): Which minimum requirements are actively driving up the total cost, and by how much per unit?
  2. Surpluses (constr.slack): Which constraints have room to spare?
  3. Reduced Costs (var.reducedCost): For items not selected in the optimal plan, how much cheaper would they need to become before entering the solution?

Problem Inputs ​

We choose daily servings ($0 \le x_i \le 4$) among 6 food items to meet 4 daily nutritional minimums (Calories >= 2000 kcal, Protein >= 105 g, Calcium >= 1000 mg, Iron >= 18 mg) at minimum cost:

Food ItemCost / ServingCalories (kcal)Protein (g)Calcium (mg)Iron (mg)Max Servings
Oats$0.601505201.84.0
Chicken$2.4022032151.24.0
Eggs$0.8014012501.74.0
Milk$0.9012082800.14.0
Spinach$1.504041203.54.0
Salmon$3.6020025200.84.0

Implementation in WaSHI ​

typescript
import { Model, c, minimize, dot } from "@ohtaman/washi";

const foods = [
  { name: "Oats",    cost: 0.60, calories: 150, protein: 5,  calcium: 20,  iron: 1.8 },
  { name: "Chicken", cost: 2.40, calories: 220, protein: 32, calcium: 15,  iron: 1.2 },
  { name: "Eggs",    cost: 0.80, calories: 140, protein: 12, calcium: 50,  iron: 1.7 },
  { name: "Milk",    cost: 0.90, calories: 120, protein: 8,  calcium: 280, iron: 0.1 },
  { name: "Spinach", cost: 1.50, calories: 40,  protein: 4,  calcium: 120, iron: 3.5 },
  { name: "Salmon",  cost: 3.60, calories: 200, protein: 25, calcium: 20,  iron: 0.8 },
];

async function runDietSensitivity() {
  const m = new Model("DietSensitivity");

  // Continuous servings per day (0 to 4 servings max per food)
  const srv = m.continuous(foods.map((f) => f.name), { lb: 0, ub: 4 });

  const costs = Object.fromEntries(foods.map((f) => [f.name, f.cost]));
  const cals  = Object.fromEntries(foods.map((f) => [f.name, f.calories]));
  const prots = Object.fromEntries(foods.map((f) => [f.name, f.protein]));
  const calcs = Object.fromEntries(foods.map((f) => [f.name, f.calcium]));
  const irons = Object.fromEntries(foods.map((f) => [f.name, f.iron]));

  // Objective: Minimize total daily cost via dot product
  m.objective(minimize(dot(costs, srv)));

  // Nutritional constraints defined declaratively with c and constraint()
  m.constraint(c`${dot(cals, srv)} >= 2000`, "min_calories")
   .constraint(c`${dot(prots, srv)} >= 105`,  "min_protein")
   .constraint(c`${dot(calcs, srv)} >= 1000`, "min_calcium")
   .constraint(c`${dot(irons, srv)} >= 18`,   "min_iron");

  const status = await m.solve();

  console.log(`Status: ${status}, Minimum Daily Cost: $${m.objectiveValue?.toFixed(2)}`);
  console.log("--- Food Servings & Reduced Costs ---");
  for (const f of foods) {
    const v = srv[f.name];
    console.log(`  ${f.name.padEnd(8)}: value=${v.value?.toFixed(2)}, reducedCost=${v.reducedCost?.toFixed(4)}`);
  }
  console.log("--- Nutritional Constraints (Dual & Slack) ---");
  for (const name of ["min_calories", "min_protein", "min_calcium", "min_iron"]) {
    const c = m.getConstraintByName(name);
    if (c) {
      console.log(`  ${c.name.padEnd(13)}: dual=$${c.dual?.toFixed(4)}, slack=${Math.abs(c.slack ?? 0).toFixed(2)}`);
    }
  }
}

runDietSensitivity();

Released under the MIT License.