forked from tmbb/dantzig
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdiet_problem.exs
More file actions
270 lines (228 loc) · 9.84 KB
/
Copy pathdiet_problem.exs
File metadata and controls
270 lines (228 loc) · 9.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
# Diet Problem Example
# =====================
#
# This example demonstrates the classic Diet Problem using the Dantzig DSL.
# The diet problem is fundamental to resource allocation and nutritional optimization,
# with applications in meal planning, food production, and cost optimization.
#
# BUSINESS CONTEXT:
# A nutritionist needs to create a meal plan that meets daily nutritional
# requirements while minimizing cost. This is a classic linear programming
# problem that demonstrates constraint modeling and cost optimization.
#
# Real-world applications:
# - Hospital meal planning with dietary restrictions
# - Military rations optimization
# - Sports nutrition planning
# - Budget-conscious meal planning
# - Food aid distribution
#
# MATHEMATICAL FORMULATION:
# Variables: x[f] = amount of food f to include in diet
# Parameters:
# - cost[f] = cost per unit of food f
# - nutrient[f,n] = amount of nutrient n in food f
# - min_req[n] = minimum required amount of nutrient n
# - max_req[n] = maximum allowed amount of nutrient n
#
# Constraints:
# - Nutritional minimum: Σf nutrient[f,n] * x[f] >= min_req[n] for all nutrients n
# - Nutritional maximum: Σf nutrient[f,n] * x[f] <= max_req[n] for all nutrients n
# - Non-negativity: x[f] >= 0 for all foods f
#
# Objective: Minimize total cost: minimize Σf cost[f] * x[f]
#
# DSL SYNTAX EXPLANATION:
# - variables("qty", [food <- food_names], :continuous, min_bound: 0.0, max_bound: :infinity)
# Creates continuous variables for each food with non-negativity constraints
# - constraints([limit <- limits_names], sum(...) <= limits_dict[limit].max)
# Uses pattern-based constraints to enforce nutritional limits
# - sum(for food <- food_names, do: qty(food) * foods[food][limit])
# Weighted sum expressions to calculate total nutrients
#
# COMMON GOTCHAS:
# 1. **Food Names**: Convert food names to use underscores for valid variable names
# 2. **Dictionary Access**: Use foods[food][limit] for nested map access
# 3. **Nutritional Limits**: Handle :infinity for unbounded nutrients (like protein minimum)
# 4. **Variable Bounds**: :continuous variables default to non-negative (min_bound: 0.0)
# 5. **Model Parameters**: Data must be passed via model_parameters for DSL access
# 6. **Weighted Sums**: Multiplication of variables by constants in sum expressions
# 7. **Dictionary Lookup**: Ensure all foods have entries for all nutritional limits
require Dantzig.Problem, as: Problem
require Dantzig.Problem.DSL, as: DSL
IO.puts("=== Diet Problem DSL Example ===")
IO.puts("Creating a cost-optimal meal plan that meets nutritional requirements")
IO.puts("")
# Display problem setup
IO.puts("Problem Setup:")
IO.puts("==============")
# Food data with nutritional information and costs
foods = %{
"hamburger" => %{
name: "hamburger",
cost: 2.49,
calories: 410,
protein: 24,
fat: 26,
sodium: 730
},
"chicken" => %{name: "chicken", cost: 2.89, calories: 420, protein: 32, fat: 10, sodium: 1190},
"hot dog" => %{name: "hot dog", cost: 1.50, calories: 560, protein: 20, fat: 32, sodium: 1800},
"fries" => %{name: "fries", cost: 1.89, calories: 380, protein: 4, fat: 19, sodium: 270},
"macaroni" => %{name: "macaroni", cost: 2.09, calories: 320, protein: 12, fat: 10, sodium: 930},
"pizza" => %{name: "pizza", cost: 1.99, calories: 320, protein: 15, fat: 12, sodium: 820},
"salad" => %{name: "salad", cost: 2.49, calories: 320, protein: 31, fat: 12, sodium: 1230},
"milk" => %{name: "milk", cost: 0.89, calories: 100, protein: 8, fat: 2.5, sodium: 125},
"ice cream" => %{name: "ice cream", cost: 1.59, calories: 330, protein: 8, fat: 10, sodium: 180}
}
# Convert food names to use underscores for valid variable names
food_names = Map.keys(foods)
# Display food information
IO.puts("Available Foods:")
Enum.each(foods, fn {_name, food} ->
IO.puts(" #{food.name}: $#{food.cost}/unit")
IO.puts(
" Nutrition: #{food.calories} cal, #{food.protein}g protein, #{food.fat}g fat, #{food.sodium}mg sodium"
)
end)
# Nutritional requirements
nutrient_limits = %{
"calories" => %{nutrient: "calories", min_bound: 1800, max_bound: 2200},
"protein" => %{nutrient: "protein", min_bound: 91, max_bound: :infinity},
"fat" => %{nutrient: "fat", min_bound: 0, max_bound: 65},
"sodium" => %{nutrient: "sodium", min_bound: 0, max_bound: 1779}
}
nutrient_names = Map.keys(nutrient_limits)
# Display nutritional requirements
IO.puts("")
IO.puts("Daily Nutritional Requirements:")
Enum.each(nutrient_limits, fn {_nutrient, limit} ->
max_str = if limit.max_bound == :infinity, do: "unlimited", else: "#{limit.max_bound}"
IO.puts(" #{limit.nutrient}: #{limit.min_bound} - #{max_str}")
end)
# Create the problem
problem_diet =
Problem.define model_parameters: %{
foods: foods,
nutrient_limits: nutrient_limits,
food_names: food_names,
nutrient_names: nutrient_names
} do
new(
name: "Diet Problem",
description: "Minimize cost of food while meeting nutritional requirements"
)
variables(
"qty",
[food <- food_names],
:continuous,
min_bound: 0.0,
max_bound: :infinity,
description: "Amount of food to buy"
)
# Nutritional constraints - Maximum limits
constraints(
sum(for food <- food_names, do: qty(food) * foods[food].calories) <= nutrient_limits["calories"].max_bound,
"Max calories"
)
# Protein has no max limit (:infinity), so skip max constraint
constraints(
sum(for food <- food_names, do: qty(food) * foods[food].fat) <= nutrient_limits["fat"].max_bound,
"Max fat"
)
constraints(
sum(for food <- food_names, do: qty(food) * foods[food].sodium) <= nutrient_limits["sodium"].max_bound,
"Max sodium"
)
# Nutritional constraints - Minimum limits
constraints(
sum(for food <- food_names, do: qty(food) * foods[food].calories) >= nutrient_limits["calories"].min_bound,
"Min calories"
)
constraints(
sum(for food <- food_names, do: qty(food) * foods[food].protein) >= nutrient_limits["protein"].min_bound,
"Min protein"
)
constraints(
sum(for food <- food_names, do: qty(food) * foods[food].fat) >= nutrient_limits["fat"].min_bound,
"Min fat"
)
constraints(
sum(for food <- food_names, do: qty(food) * foods[food].sodium) >= nutrient_limits["sodium"].min_bound,
"Min sodium"
)
objective(
sum(for food <- food_names, do: qty(food) * foods[food].cost),
direction: :minimize
)
end
# Solve the optimization problem
IO.puts("\nSolving the diet optimization problem...")
result = Problem.solve(problem_diet, print_optimizer_input: true)
case result do
{solution, objective_value} ->
IO.puts("\nSolution:")
IO.puts("=========")
objective_value =
if is_integer(objective_value), do: objective_value * 1.0, else: objective_value
IO.puts("Minimum daily cost: $#{Float.round(objective_value, 2)}")
IO.puts("")
IO.puts("Optimal Food Selection:")
{total_cost, total_nutrients} = Enum.reduce(foods, {0, %{calories: 0, protein: 0, fat: 0, sodium: 0}}, fn {_name, food}, {cost_acc, nutrients_acc} ->
# Variable names are generated as "qty(foodname)" by the DSL
var_name = "qty(#{food.name})"
quantity = solution.variables[var_name] || 0
if quantity > 0.001 do
food_cost = quantity * food.cost
# Accumulate nutritional totals
new_nutrients = nutrients_acc
|> Map.update!(:calories, &(&1 + quantity * food.calories))
|> Map.update!(:protein, &(&1 + quantity * food.protein))
|> Map.update!(:fat, &(&1 + quantity * food.fat))
|> Map.update!(:sodium, &(&1 + quantity * food.sodium))
IO.puts(
" #{food.name}: #{Float.round(quantity, 2)} units (cost: $#{Float.round(food_cost, 2)})"
)
{cost_acc + food_cost, new_nutrients}
else
{cost_acc, nutrients_acc}
end
end)
IO.puts("")
IO.puts("Nutritional Analysis:")
IO.puts(" Total calories: #{Float.round(total_nutrients.calories, 0)}")
IO.puts(" Total protein: #{Float.round(total_nutrients.protein * 1.0, 1)}g")
IO.puts(" Total fat: #{Float.round(total_nutrients.fat * 1.0, 1)}g")
IO.puts(" Total sodium: #{Float.round(total_nutrients.sodium * 1.0, 0)}mg")
IO.puts("")
IO.puts("Validation:")
all_constraints_satisfied =
Enum.all?(nutrient_limits, fn {_nutrient_name, limit} ->
current_amount = total_nutrients[String.to_atom(limit.nutrient)]
case limit.max_bound do
:infinity -> current_amount >= limit.min_bound
max -> current_amount >= limit.min_bound and current_amount <= max
end
end)
if all_constraints_satisfied do
IO.puts(" ✅ All nutritional requirements satisfied")
else
IO.puts(" ❌ Some nutritional requirements violated")
end
IO.puts(" Cost matches objective: #{abs(total_cost - objective_value) < 0.001}")
IO.puts("")
IO.puts("LEARNING INSIGHTS:")
IO.puts("==================")
IO.puts("• Diet problems optimize resource allocation under nutritional constraints")
IO.puts("• Linear programming naturally handles weighted sum constraints for nutrition")
IO.puts("• Model parameters enable clean separation of data from optimization logic")
IO.puts("• Continuous variables naturally model fractional food quantities")
IO.puts("• Real-world applications: meal planning, food aid, dietary optimization")
IO.puts("• The DSL demonstrates pattern-based constraint generation for multiple nutrients")
IO.puts("")
IO.puts("✅ Diet problem solved successfully!")
:error ->
IO.puts("ERROR: Diet problem could not be solved.")
IO.puts("This may be due to infeasible nutritional requirements.")
IO.puts("Try adjusting the nutritional limits to ensure they can be satisfied.")
end