Recommends daily menus from 405 food items and returns a Pareto front of preference / cost / preparation-time trade-offs, using NSGA-II (pymoo).
👥 Team: Saed O S Radi, Kenan Alemam, Ahmed Alsaleh, Abdulrahman Aljabahji, Mamdouh Al Masri — Heuristic Optimization course project (FSMVU)
Open the demo → Pick a user profile, NSGA-II or SPEA2 and a seed, press Run, and explore the Pareto front (3D or 2D), the hypervolume, the number of fully DRI-compliant menus and the best compliant menu. There is no server: the repository's own Python code (src/, the SQLite backend, pymoo 0.6.1.6) runs in your browser through Pyodide in a Web Worker. A run (population 100, 50 generations, same as main.py) takes about 3 s on a laptop after a one-time ~25 MB download of the Python runtime.
What is and is not identical to the desktop code. src/ and data/ are used as they are in the repo. Two things differ, neither affects results: (1) pymoo's compiled helpers (moocore for hypervolume, a terminal progress bar) cannot run in WebAssembly, so docs/demo/shims/ provides a pure-Python exact hypervolume and a stub; (2) docs/demo/runner.py is a thin driver that calls the same run_nsga2 / run_spea2 with the same seed argument that main.py uses, so the demo with seed 42 reproduces the results above.
Parity check. scripts/demo_parity.py compares the browser with the original code under CPython (pymoo 0.6.1.6, compiled helpers) for 2 users × 2 algorithms × 2 seeds. On the live site all 8 cases are identical: every one of the 100 × 3 Pareto objective values (max difference 0.0), every decoded menu, the DRI-compliant count and the best menu; hypervolume agrees to ≤ 5.3e-16 relative (floating-point rounding).
case result max |dF| HV rel diff HV compliant
user1_nsga2_seed42 PASS 0.0e+00 1.2e-16 120204235.5 12
user1_nsga2_seed2024 PASS 0.0e+00 4.2e-16 107598014.9 10
user1_spea2_seed42 PASS 0.0e+00 0.0e+00 120322729.5 16
user1_spea2_seed2024 PASS 0.0e+00 5.0e-16 120300213.6 10
user2_nsga2_seed42 PASS 0.0e+00 5.2e-16 170956582.6 3
user2_nsga2_seed2024 PASS 0.0e+00 5.3e-16 168910941.5 2
user2_spea2_seed42 PASS 0.0e+00 1.7e-16 176831638.2 7
user2_spea2_seed2024 PASS 0.0e+00 0.0e+00 199446166.3 3
To rebuild the bundle after changing src/ or data/: python scripts/build_demo_bundle.py. To re-run the parity check: python scripts/demo_parity.py reference, then node scripts/demo_parity_browser.mjs <demo url> browser.json (needs playwright) and python scripts/demo_parity.py compare browser.json.
Choosing a day of meals means balancing what a person likes, what it costs and how long it takes to prepare, while still meeting daily nutrient targets. These goals conflict, so there is no single best menu. This project searches for non-dominated menus (a Pareto front) for a given user, taking the user's own food ratings, their age/gender-specific Dietary Reference Intake (DRI) bounds, and a vegetarian filter into account.
Besides NSGA-II, the repository implements SPEA2 for comparison, tracks hypervolume per generation, and includes an experiment on a diversity penalty (food-group variety).
The problem is a knapsack-style selection with several nutrient constraints and three objectives. It is not coded as a binary 0/1 knapsack vector: the optimizer evolves a priority ordering over foods and a greedy decoder turns it into a menu.
Decision variables. One gene per candidate food: breakfast pool + lunch/dinner pool (user 1: 91 + 314 = 405 genes; user 2, vegetarian: 91 + 212 = 303). pymoo evolves them as a float vector with its default operators (random sampling, SBX crossover, polynomial mutation). Each gene is cast to an integer and mapped mod pool size to a food id, giving a priority list for each meal. The genes are not constrained to be a permutation, so a food can appear more than once in a menu.
Greedy decoder (src/chromosome.py). Foods are scanned in priority order and skipped if their global preference is −1:
- Breakfast: add foods until energy and protein both reach 35 % of their daily lower bound; skip a food that would push energy or protein above 35 % of its daily upper bound.
- Lunch/dinner: continue with the running nutrient totals; skip a food if any of the five nutrients would exceed its upper bound × 1.15; stop once all five reach their lower bound × 0.90.
Objectives (what pymoo minimizes), with R the penalty below:
| Objective | Direction | |
|---|---|---|
| f1 | −(total preference) + R | maximize preference |
| f2 | total cost + R | minimize cost |
| f3 | total preparation + cooking time + R | minimize time |
A food's preference is the user's own rating (user_foods) when present, otherwise the food's default rating.
Constraints (soft, via penalty). Energy, protein, carbohydrate, fibre and sodium should lie in the user's [RLL, RUL] DRI range. For each nutrient, R += 0.7·max(0, RLL−v)/(RUL−RLL) + 0.3·max(0, v−RUL)/(RUL−RLL), plus a diversity term alpha / (number of distinct food groups) with alpha = 1.0 (alpha = 0 disables it). Penalty weight λ = 1. Because the constraints are soft, a decoded menu can still fall outside the bounds (see Known Limitations).
Vegetarian filter. A user is treated as vegetarian if more than 50 % of their ratings for meat/fish/poultry food groups are −1; those groups are then removed from the candidate pools.
The three objectives are in conflict and there is no sensible way to fix their weights in advance. NSGA-II ranks solutions by non-domination and keeps the front spread out with crowding distance, so a single run returns many alternative menus to choose from rather than one weighted-sum compromise. It is available as a tested implementation in pymoo, and SPEA2 from the same library gives a point of comparison on the identical problem.
data/*.csv ──► SQLite (or MySQL) ──► DietProblem ──► NSGA-II / SPEA2 ──► Pareto front ──► decoded menus
foods, nutrients, src/database.py decoder + objectives pymoo, 100 pop × 50 gen plots, CSVs sample menus + DRI table
DRI, user ratings + penalty
Run of python main.py with the default seed 42 (population 100, 50 generations, ~10 s on a laptop). The run is reproducible: the same seed gives identical results (python main.py --seed 7 or SEED=7 python main.py for another). The raw outputs are in results/. Objective values in the plots and CSVs include the penalty term R; the menu tables show the raw preference, cost and time of the decoded menu.
| User | Algorithm | Pareto solutions | Final hypervolume* | Solutions inside all strict DRI bounds |
|---|---|---|---|---|
| 1 (non-vegetarian) | NSGA-II | 100 | 120,204,236 | 12 / 100 |
| 1 (non-vegetarian) | SPEA2 | 100 | 120,322,730 | 16 / 100 |
| 2 (vegetarian) | NSGA-II | 100 | 170,956,583 | 3 / 100 |
| 2 (vegetarian) | SPEA2 | 100 | 176,831,638 | 7 / 100 |
* Reference point [0, 1000, 1000] on (−preference, cost, time). Both sample users in the dataset are 25-year-old women (DRI bounds: 2000–2400 kcal, 40–100 g protein, 170–300 g carbohydrate, ≥ 20 g fibre, 1500–2300 mg sodium).
This is a single seed. With the earlier unseeded code the order of NSGA-II and SPEA2 by hypervolume changed between repeated runs, so one seed does not show that either algorithm is better; compare several seeds before drawing conclusions.
Pareto fronts (preference vs cost, preference vs time):
| User 1 | User 2 |
|---|---|
![]() |
![]() |
Hypervolume convergence:
| User 1 | User 2 |
|---|---|
![]() |
![]() |
Diversity penalty (NSGA-II, user 1, alpha 1.0 vs 0.0): both settings fill the 100-slot population with non-dominated solutions, so front size does not separate them in this run. See results/diversity_user1.png.
Example recommended menu — the highest-preference NSGA-II solution for user 1 that satisfies every DRI bound (food names are the original Turkish dataset names):
Preference: 138.40 | Cost: 40.93 | Time: 445 min | Food groups: 11
Breakfast: INCİR REÇELİ, KİRAZ, BEYAZ PEYNIR, INCİR REÇELİ, AYVA, INCİR REÇELİ,
INCİR REÇELİ, ÜZÜM PEKMEZİ, LOR PEYNİRİ, FINDIK EZMESİ,
HAVUÇ SUYU LD, PORTAKAL SUYU
Lunch/Dinner: ALİ PAŞA PİLAVI, CACIK, KREMALI DOMATES ÇORBASI, ZEYTİNYAĞLI SEMİZOTU,
KREMALI MANTAR ÇORBASI, ANDALOZ ÇORBASI, SALÇALI HİNDİ BİFTEK
Nutrient Total RLL RUL
Energy 2199.1 2000.0 2400.0 ok
Protein 93.8 40.0 100.0 ok
Carbohydrate 250.1 170.0 300.0 ok
Fiber 26.8 20.0 9999.0 ok
Sodium 2191.8 1500.0 2300.0 ok
More menus: results/example_menu_user1.txt, results/example_menu_user2.txt, results/sample_menus_user1.txt, results/sample_menus_user2.txt.
405 prepared foods (29 food groups) with cost, preference rating, preparation/cooking time and CO₂ values, nutrient contents for five nutrients (energy, protein, carbohydrate, fibre, sodium), per-age/gender DRI bounds, and per-user food ratings for 125 users. The data is the Turkish food database provided for the Heuristic Optimization course (FSMVU); food data and images from diyetasistan.com.
data/*.csv is a sanitized export of only the columns the code reads. User names, usernames and password hashes are not included; users are identified by an anonymous numeric id with age and gender. data/schema.sql is the matching schema.
├── main.py # runs all experiments, writes results/
├── src/
│ ├── database.py # SQLite / MySQL access (backend chosen by env vars)
│ ├── chromosome.py # greedy decoder (genes → menu)
│ ├── objectives.py # objective evaluation
│ ├── penalty.py # nutrient + diversity penalties
│ ├── menu_table.py # decoded sample menus and DRI table
│ └── algorithms/ # nsga2.py, spea2.py
├── data/ # sanitized CSVs + schema.sql (diet.sqlite is built on first run)
├── scripts/ # build_sqlite.py, import_to_mysql.py, export_from_mysql.py
├── results/ # plots, CSVs, menus, summary from the last run
├── docs/MODP_Report.pdf # original course report (student numbers redacted)
├── docs/demo/ # live demo (GitHub Pages): index.html, worker.js, runner.py, pybundle.zip, parity/
├── .env.example # database settings template
├── requirements.txt
└── requirements-mysql.txt
Tested with Python 3.12.
git clone https://github.com/SaadOsama10/diet-optimization.git
cd diet-optimization
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
python main.pyOn the first run the SQLite database data/diet.sqlite is built from the CSV files automatically; no database server is needed. main.py runs user 1 and user 2 (NSGA-II vs SPEA2, hypervolume) and the diversity experiment for user 1, then writes everything to results/.
Using MySQL instead (optional):
pip install -r requirements-mysql.txt
cp .env.example .env # set DB_BACKEND=mysql, DB_USER, DB_PASSWORD, ...
python scripts/import_to_mysql.py
python main.pyDatabase settings are read from environment variables or a .env file; no credentials are stored in the code.
- Bug fix: cost and time were being maximized.
evaluate()returns all objectives in maximize form, but only preference was negated before pymoo (which minimizes) saw them, so cost and time were maximized and their penalty was rewarded. All three are now minimized correctly (fix: cost and time were being maximizedcommit). The numbers in the original course report (docs/MODP_Report.pdf) come from the earlier, buggy version and are not comparable; all results in this README are from the corrected code. - Database credentials moved out of the code into environment variables; SQLite backend added so the project runs without MySQL.
- Sanitized dataset published; reproducible setup with requirements files and a tested fresh-clone run.
- Reproducible runs:
main.py --seed N(orSEED=N, default 42) is passed to pymoo'sminimize. - Headless-safe plotting, automatic
results/creation, and saved summaries/menus. - Student numbers redacted from the report PDF.
- Soft constraints. In the seed-42 run only 3–16 of the 100 Pareto solutions fall inside every strict DRI bound; the rest violate at least one bound (the decoder tolerates ×0.90 / ×1.15 on the daily bounds).
- Not a true permutation encoding. Genes are real-coded, cast to integers and wrapped with
mod, so duplicates can occur (a food can appear twice in one menu). - Only two example users (both 25-year-old women) are evaluated; the dataset contains 125.
- Penalised objective values. Plotted/exported objective values include the penalty term; the menu tables show the raw values.
- Food names are Turkish and are not translated in the code output.
- The preference −1 exclusion in the decoder uses the food's default
preference, not the user's own rating. - The unredacted course report remains in git history of earlier commits.




