-
Notifications
You must be signed in to change notification settings - Fork 14
/
app.js
373 lines (309 loc) · 13.6 KB
/
app.js
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
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
/**
* Nonlinear Auto-Soylent Solver v0.2
*
* by Alrecenk (Matt McDaniel) of Inductive Bias LLC (http://www.inductivebias.com)
* and Nick Poulden of DIY Soylent (http://diy.soylent.me)
*
*/
// This can be replaced with any of the recipes on http://diy.soylent.me
var recipeUrl = "http://diy.soylent.me/recipes/people-chow-301-tortilla-perfection";
// Calorie goal
var calories = 2200;
// Ratio of carbs / protein / fat. Should add to 100
var macros = {
carbs: 40,
protein: 30,
fat: 30
};
var ingredientLength,
targetLength, // Length of ingredient and target array (also dimensions of m)
M, // Matrix mapping ingredient amounts to chemical amounts (values are fraction per serving of target value)
cost, // Cost of each ingredient per serving
w = .0001, // Weight cost regularization (creates sparse recipes for large numbers of ingredient, use 0 for few ingredients)
maxPerMin, // Ratio of maximum value to taget value for each ingredient
lowWeight,
highWeight; // How to weight penalties for going over or under a requirement
var nutrients = [
'calories', 'carbs', 'protein', 'fat', 'biotin', 'calcium', 'chloride', 'cholesterol', 'choline', 'chromium', 'copper',
'fiber', 'folate', 'iodine', 'iron', 'maganese', 'magnesium', 'molybdenum', 'niacin', 'omega_3', 'omega_6',
'panthothenic', 'phosphorus', 'potassium', 'riboflavin', 'selinium', 'sodium', 'sulfur', 'thiamin',
'vitamin_a', 'vitamin_b12', 'vitamin_b6', 'vitamin_c', 'vitamin_d', 'vitamin_e', 'vitamin_k', 'zinc'
];
// These nutrients are considered 'more important'
var macroNutrients = ["calories", "protein", "carbs", "fat"];
/**
* Fitness function that is being optimized
*
* Note: target values are assumed as 1 meaning M amounts are normalized to be fractions of target values does not
* consider constraints, those are managed elsewhere.
*
* Based on the formula (M * x-1)^2 + w *(x dot c) except that penalties are only given if above max or below min and
* quadratically from that point.
*
* @author Alrecenk (Matt McDaniel) of Inductive Bias LLC (www.inductivebias.com) March 2014
*/
function f(x) {
var output = createArray(targetLength),
totalError = 0;
// M*x - 1
for (var t = 0; t < targetLength; t++) {
// Calculate output
output[t] = 0;
for (var i = 0; i < ingredientLength; i++) {
output[t] += M[i][t] * x[i];
}
// If too low penalize with low weight
if (output[t] < 1) {
totalError += lowWeight[t] * (1 - output[t]) * (1 - output[t]);
}
else if (output[t] > maxPerMin[t]){ // If too high penalize with high weight
totalError += highWeight[t] * (maxPerMin[t] - output[t]) * (maxPerMin[t] - output[t]);
}
}
// Calculate cost penalty, |c*x|
// but X is nonnegative so absolute values aren't necessarry
var penalty = 0;
for (var i = 0; i < ingredientLength; i++) {
penalty += cost[i] * x[i];
}
return totalError + w * penalty;
}
/**
* Gradient of f with respect to x.
* Based on the formula 2 M^T(Mx-1) + wc except with separate parabolas for going over or under.
* Does not consdier constraints, those are managed elsewhere.
*
* @author Alrecenk (Matt McDaniel) of Inductive Bias LLC (www.inductivebias.com) March 2014
*/
function gradient(x){
var output = createArray(targetLength);
// output = M*x
for (var t = 0; t < targetLength; t++) {
// Calculate output
output[t] = 0;
for (var i = 0; i < ingredientLength; i++) {
output[t] += M[i][t] * x[i];
}
}
// Initialize gradient
var dx = [];
for (var i = 0; i < ingredientLength; i++) {
dx[i] = 0;
for (var t = 0; t < targetLength; t++) {
// M^t (error)
if (output[t] < 1) { // If output too low calculate gradient from low parabola
dx[i] += lowWeight[t] * M[i][t] * (output[t] - 1);
}
else if (output[t] > maxPerMin[t]) { // If output too high calculate gradient from high parabola
dx[i] += highWeight[t] * M[i][t] * (output[t] - maxPerMin[t]);
}
}
dx[i] += cost[i] * w; // + c w
}
return dx;
}
/**
* Generates a recipe based on gradient descent minimzation of a fitness function cosisting of half parabola penalties
* for out of range items and weighted monetary cost minimzation.
*
* @author Alrecenk (Matt McDaniel) of Inductive Bias LLC (www.inductivebias.com) March 2014
*/
function generateRecipe(ingredients, nutrientTargets) {
// Initialize our return object: an array of ingredient quantities (in the same order the ingredients are passed in)
var ingredientQuantities = [],
targetAmount = [], // Target amounts used to convert ingredient amounts to per serving ratios
targetName = [],
x = []; // Number of servings of each ingredient
// Fetch the target values ignoring the "max" values and any nonnumerical variables
for (var key in nutrientTargets) {
var name = key,
nutrient = name.replace(/_max$/, '')
value = nutrientTargets[key];
if (nutrients.indexOf(nutrient) > -1 && name.substring(name.length - 4, name.length) != "_max" && value > 0) {
targetName.push(name);
targetAmount.push(value);
}
}
maxPerMin = [];
lowWeight = [];
highWeight = [];
// Initialize target amount maxes and mins along with weights.
// There are some hardcoded rules that should be made configurable in the future.
for (var t = 0; t < targetAmount.length; t++) {
// If has a max for this element
if (typeof nutrientTargets[targetName[t] + "_max"] > targetAmount[t]) {
var maxvalue = nutrientTargets[targetName[t] + "_max"];
maxPerMin[t] = maxvalue / targetAmount[t]; // Record it
}
else {
maxPerMin[t] = 1000; // Max is super high for things that aren't limited
}
// Weight macro nutrients values higher and make sure we penalize for going over (ad hoc common sense rule)
if (macroNutrients.indexOf(targetName[t]) >= 0) {
lowWeight[t] = 5;
highWeight[t] = 5;
maxPerMin[t] = 1;
}
else {
lowWeight[t] = 1;
highWeight[t] = 1;
}
// Weird glitch where niacin isn't being read as having a max, so I hardcoded in this
// should be removed when that is tracked down
if (targetName[t] =="niacin"){
maxPerMin[t] = 30.0 / 16.0;
}
// console.log(targetName[t] + " : " + targetAmount[t] +" --max ratio :" + maxPerMin[t] +" weights :" + lowWeight[t]+"," + highWeight[t]);
}
// Intitialize the matrix mapping ingredients to chemicals and the cost weights.
// These are the constants necessary to evaluate the fitness function and gradient.
ingredientLength = ingredients.length;
targetLength = targetAmount.length;
M = createArray(ingredientLength, targetLength);
cost = [];
for (var i = 0; i < ingredients.length; i++) {
for (var t = 0; t < targetAmount.length; t++) {
// Fraction of daily value of target t in ingredient i
M[i][t] = ingredients[i][targetName[t]] / (targetAmount[t]);
}
// Initial x doesn't affect result but a good guess may improve speed
x[i] = 1; // Initialize with one of everything
// Cost per serving is cost per container * servings per container
cost[i] = ingredients[i].item_cost * ingredients[i].serving / ingredients[i].container_size;
}
// Projected Gradient descent with halving step size, accepting largest step with improvement.
// Could be made faster by moving to LBGS and implementing a proper inexact line search
// but this method does guarantee convergence so those improvements are on the back burner
console.log("Calculating Optimal Recipe...");
var fv = f(x),
g = gradient(x),
iteration = 0;
while (!done && iteration < 50000) { // Loops until no improvement can be made or max iterations
iteration++;
var done = false,
stepsize = 10, // Start with big step
linesearch = true;
while (linesearch) {
var newx = [];
// Calculate new potential value
for (var i = 0; i < x.length; i++) {
newx[i] = x[i] - g[i] * stepsize;
if (newx[i] < 0) {
newx[i] = 0;
}
}
var newf = f(newx); // Get fitness
if (newf < fv) { // If improvement then accept and recalculate gradient
fv = newf;
x = newx;
g = gradient(x);
linesearch = false; // exit line search
}
else {
stepsize *= 0.5; // If bad then halve step size
if (stepsize < 0.00000001) { // If stepsize too small then quit search entirely
done = true;
linesearch = false;
}
else { // otherwise continue line search
linesearch = true;
}
}
}
}
var pricePerDay = 0;
for (var k = 0; k < x.length; k++) {
pricePerDay += x[k] * cost[k];
}
console.log("Price per day: $" + pricePerDay.toFixed(2));
// Map number of servings into raw quantities because that's what this function is supposed to return
for (var i = 0; i < ingredients.length; i++) {
ingredientQuantities[i] = x[i] * ingredients[i].serving;
}
return ingredientQuantities;
}
// Convenience function for preinitializing arrays because I'm not accustomed to working on javascript
function createArray(length) {
var arr = new Array(length || 0),
i = length;
if (arguments.length > 1) {
var args = Array.prototype.slice.call(arguments, 1);
while(i--) arr[length-1 - i] = createArray.apply(this, args);
}
return arr;
}
// Fetch recipe, pass to generateRecipe function and output results...
var request = require('superagent'), // Library to request recipe from diy.soylent.me
Table = require('cli-table'), // Library to output the results in a pretty way
colors = require('colors');
console.log("\nFetching the recipe from the DIY Soylent website...");
request.get(recipeUrl + "/json?nutrientProfile=51e4e6ca7789bc0200000007", function(err, response) {
if (err) {
console.log("An error occurred", err);
return;
}
console.log("Successfully fetched recipe.\n");
var ingredients = response.body.ingredients,
nutrientTargets = response.body.nutrientTargets,
i, j, nutrient;
// Override macros based on user variables from top of this file
nutrientTargets.calories = calories;
nutrientTargets.carbs = Math.round(macros.carbs * calories / 100 / 4);
nutrientTargets.protein = Math.round(macros.protein * calories / 100 / 4);
nutrientTargets.fat = Math.round(macros.fat * calories / 100 / 9);
nutrientTargets.calories_max = Number((nutrientTargets.calories * 1.04).toFixed(2));
nutrientTargets.carbs_max = Number((nutrientTargets.carbs * 1.04).toFixed(2));
nutrientTargets.protein_max = Number((nutrientTargets.protein * 1.04).toFixed(2));
nutrientTargets.fat_max = Number((nutrientTargets.fat * 1.04).toFixed(2));
// Here's where the magic happens...
var ingredientQuantities = generateRecipe(ingredients, nutrientTargets);
// Now lets output the results. First the ingredients.
var ingredientsTable = new Table({
style: { compact: true },
head: ["Ingredient", "Official\nAmount", "Optimized\nAmount"]
});
for (i=0; i< ingredients.length; i++) {
ingredientsTable.push([
ingredients[i].name,
ingredients[i].amount + " " + ingredients[i].unit,
ingredientQuantities[i].toFixed(2) + " " + ingredients[i].unit
]);
}
console.log(ingredientsTable.toString());
// Output the nutrients.
var nutrientsTable = new Table({
style: { compact: true },
head: ['Nutrient', 'Target', 'Max', 'Recipe', '%']
});
var pct;
for (var n=0; n < nutrients.length; n++) {
var nutrient = nutrients[n];
// Add up the amount of the current nutrient in each of the ingredients.
var nutrientInIngredients = 0;
for (j=0; j< ingredients.length; j++) {
if (typeof ingredients[j][nutrient] == 'number' && ingredientQuantities[j] > 0) {
nutrientInIngredients += ingredients[j][nutrient] * ingredientQuantities[j] / ingredients[j].serving;
}
}
// Format percentages nicely. Cyan: too little. Green: just right. Red: too much
pct = nutrientTargets[nutrient] ? (nutrientInIngredients / nutrientTargets[nutrient] * 100) : 100;
if (pct < 99) {
pct = (pct.toFixed(0) + " %").cyan.bold;
}
else if (nutrientTargets[nutrient + '_max'] > 0 && nutrientInIngredients > nutrientTargets[nutrient + '_max']) {
pct = (pct.toFixed(0) + " %").red.bold.inverse;
}
else {
pct = (pct.toFixed(0) + " %").green;
}
nutrientsTable.push([
nutrient || '', // Nutrient Name
nutrientTargets[nutrient] || '', // Target amount
nutrientTargets[nutrient + '_max'] || '', // Maximum amount
nutrientInIngredients.toFixed(2) || '', // Amount in Recipe
pct || '' // % of Target in recipe
]);
}
console.log(nutrientsTable.toString());
// That's it!
});