Wirtschaft
Am Beispiel des Ernährungsrechners: feste Nährstoff-Grenzen, kleinste Tageskosten. So ist das Minimalprinzip im Code gebaut. Die Idee dahinter: Was ist das Minimalprinzip?
Zuordnung
Im Tool ist das Ergebnis der Satz an Nährstoff-Schranken: mindestens so viel Protein, Kalorien, Eisen, …, höchstens so viel von dem, was du begrenzen willst. Der Einsatz ist das Geld: Summe der Lebensmittelpreise pro Tag.
Die Frage des Rechners lautet deshalb: Welche Mengen welcher Lebensmittel halten alle gesetzten Min- und Max-Grenzen und kosten am wenigsten? Das ist Minimalprinzip, nicht „möglichst gesund um jeden Preis“ und nicht „möglichst billig ohne Grenzen“.
Technisch ist das ein lineares Programm. Bibliothek: javascript-lp-solver (solver.js). Aufruf: solver.Solve(model), normalerweise im Web Worker plan-worker.js. Unter file:// oft ohne Worker, dann derselbe Call im Hauptthread.
Pool
Bevor der Solver läuft, schneidet foodsForPlan() die Datenbank zurecht. Nur dieser Pool wird zum Modell.
matchesDiet): vegan, vegetarisch, pescetarisch, carnivor oder alle. Bestimmt, welche Tags ein Lebensmittel haben muss.matchesTagFilters): z. B. Whole Food, Supplement, Verarbeitet. Bei Laktose und Gluten gilt: Lebensmittel, die den Tag haben, werden ausgeschlossen (Vermeidung).
Jedes Lebensmittel im Pool bekommt einen Variablennamen f0, f1, f2, … in der Reihenfolge des Arrays. Der Index ist wichtig, weil das Solver-Ergebnis dieselben Keys zurückgibt.
Einheiten
Preis und Nährwerte liegen pro Referenz (foodRef / UNIT_META):
g und ml: Referenz 100 (pro 100 g bzw. 100 ml)stk und tr: Referenz 1 (pro Stück bzw. Tropfen)
foodPrice(f) ist der gespeicherte Preis pro Referenz. Nährwerte in f.nutrients[p] sind ebenfalls pro Referenz. Wenn du das falsch skaliert, rechnet der Solver mit Müll.
Die Funktion buildModel(list, asInteger, reqMap) baut daraus die Solver-Variablen. Was die Variable u bedeutet, hängt vom Modus ab.
Kontinuierlich (asInteger = false, Default im UI):
u = Anzahl Referenz-Pakete (z. B. 1,5 → 150 g bei ref = 100)foodPrice(f)u × foodRef(f)
Ganze Mengen (asInteger = true, Schalter „Nur ganze Mengen“):
u = ganze physische Einheiten (1 g, 1 ml, 1 Stück, 1 Tropfen)scale = 1 / foodRef(f)foodPrice(f) × scale (Preis pro 1 g / 1 ml / …)raw × scaleuModell
Exakt die Struktur aus buildModel:
Die Schranken kommen aus dem Anforderungen-Tab (req), optional als reqMap übergeben. Jeder Eintrag in PARAMS wird geprüft: hat er min und/oder max? Dann landet er in constraints.
In Worten, 1:1 die Zielfunktion des Tools:
Kontinuierlich
Wenn der Integer-Schalter aus ist, bleibt der Weg kurz:
foodsForPlan()model = buildModel(pool, false) mit dem aktuellen reqresult = await solveModelAsync(model) → Worker oder Fallback solver.Solve!result.feasible: Abbruch und diagnoseInfeasiblef_i mit u > 0 in Mengen und Kosten umrechnen und anzeigenUmrechnung im kontinuierlichen Fall:
planCost(u, f, false) = u × foodPrice(f)planNutrient(u, f, p, false) = u × rawplanAmount(u, false, f) = u × foodRef(f)Das Ergebnis ist der billigste zulässige Plan im kontinuierlichen Raum. Mengen dürfen Bruchteile von 100 g-Paketen sein (z. B. 37,2 g).
Ganze Mengen
Integer-LP über die ganze Datenbank wäre zu langsam und oft unnötig. Deshalb solveIntegerPlan(pool) in zwei Phasen. Konstante: INTEGER_KCAL_BUFFER = 25 auf dem Max von Energy (cal).
Phase 1 – schneller Plan mit Kalorien-Puffer
phase1Req = reqWithKcalBuffer(realReq, 25): Energy-Max um 25 gesenkt, aber nie unter das Energy-Min.phase1Req. Scheitert es, nochmal mit den echten Grenzen realReq.u > 1e-6, höchstens 50 (die mit dem größten u).round(u_cont × ref), sonst ceil.repairIntegerPlan (siehe unten).Phase 2 – nur wenn die echten Grenzen noch verletzt sind
collectViolations(chosen, realReq): welche Nährstoffe unter Min, welche über Max?topFoodsForNutrient: Score = Nährstoff pro € und Einheit, Supplements × 1,5) ∪ Supplements, die den Defizit-Nährstoff liefern.realReq.repairIntegerPlan gegen realReq.approx: true zurückgegeben (Näherung).Der Puffer in Phase 1 schafft etwas Luft bei den Kalorien, damit Phase 2 noch Einheiten nachlegen kann, ohne sofort das Energy-Max zu sprengen.
Reparatur
repairIntegerPlan(chosen, pool, reqMap) läuft maximal 250 Schritte:
canAddOneUnit). +1 Einheit, dann nächster Schritt.
Das ist kein exaktes Kostenminimum mehr. Es ist ein Notnagel, damit der Plan möglichst alle Grenzen hält. Im Ergebnis kann method z. B. milp-support, round+repair oder milp-deficit+repair heißen.
Unmöglich
Wenn das kontinuierliche Modell unzulässig ist, läuft diagnoseInfeasible:
Die UI listet diese Nährstoffe und schlägt vor: Schranke lockern oder Lebensmittel hinzufügen. Das ist die wirtschaftliche Übersetzung von „das Soll und der erlaubte Pool passen nicht zusammen“.
Umrechnung der Lösung
Der Solver liefert für jede Variable f_i einen Wert u (oder 0 / fehlend). Daraus:
u × ref, Kosten = u × price, Nährstoff = u × rawu, Kosten = u × price / ref, Nährstoff = u × raw / refDie Tageskosten des Plans sind die Summe der Einzelkosten. Die Nährstoff-Summen sind die Summe der Beiträge. Der „limiting factor“ im UI markiert den Nährstoff, der am engsten an einer Grenze liegt: dort sitzt oft der nächste Hebel (Datenbank erweitern oder Schranke anpassen).
Mini-Beispiel
Zwei „Lebensmittel“, eine Untergrenze (vereinfacht wie der Demo-Assert im Tool):
Mit echten Nährstoffen und vielen Maxima wird der Raum enger. Die Aufgabe bleibt dieselbe: billigster Punkt, der noch alle Grenzen des Profils und der Anforderungen hält.
Kurz
Nährstoff-Min und -Max sind das Soll. Euro sind der Einsatz. Der Solver (und bei ganzen Mengen die zwei Phasen plus Repair) sucht den günstigsten zulässigen Tagesplan im gefilterten Lebensmittel-Pool. Das ist das Minimalprinzip, ausprogrammiert.
Datenbank, Profil, Schranken, optimaler Plan. Dieselbe Rechnung wie oben.
Weiterlesen