# Math Model Generator & Solver Übersetzt eine **natürlichsprachige Optimierungsaufgabe** (Deutsch) mit einem LLM in ein formales mathematisches Modell, prüft es, korrigiert Fehler und **löst es tatsächlich** mit einem passenden Python-Solver. Ergebnis ist ein nachvollziehbarer Report mit Formeln, Prüfbericht, Lösung und dem erzeugten Python-Code. ```text Aufgabentext (+ optional beigelegte Daten: --daten kurse.csv) │ ohne LLM: Profil je Datei – Zeilen, Spalten, Spannweiten, Lücken ▼ 1. Verstehen ───────── LLM: Teilfragen a)/b)/…, alle Zahlen des Textes mit Fundstelle, │ übergangene Zahlen mit Grund; ist es überhaupt eine Aufgabe? │ ohne LLM geprüft: keine Zahl, keine Teilfrage übersehen ▼ 2. Extraktion ──────── LLM: Problemklasse, Solver-Wahl, Indizes, Parameter, │ Variablen, Zielfunktion, Restriktionen (LaTeX), Annahmen ▼ 3. Konsistenz-Checks ─ ohne LLM: Solver passt zur Klasse? Einheiten? undeklarierte │ Symbole? kamen die erfassten Zahlen im Modell an? ▼ 4. LLM-Prüfung ─────── Zielfunktion = gefragte Größe? Annahmen textkonform? │ ▲ ▼ │ Korrekturschleife (max. 2 Runden, beste Runde gewinnt) 5. Korrektur ┘ │ ▼ 6. Solver ──────────── LLM erzeugt Python-Code, Ausführung in der Sandbox, Reparatur │ bei Fehlern; Szenarien = Datenvarianten, ohne LLM nachgerechnet ▼ 7. Abnahme ─────────── ohne LLM: jede Teilfrage beantwortet? jede Zahl der Antwort │ belegt? Lösung zulässig? Tabellensummen richtig? ▼ Report (Markdown/HTML) + JSON + eigenständiges Solver-Skript ``` ## Unterstützte Problemklassen | Klasse | Solver | Typische Aufgaben | |---|---|---| | Lineare Programmierung (LP) | SciPy `linprog` | Mischungs-, einfache Produktionsprobleme | | Gemischt-ganzzahlig (MILP) | HiGHS (`highspy`) | Zuordnung, Rucksack, Standortwahl, Rüst-/Fixkosten | | Constraint Programming (CP) | Google OR-Tools CP-SAT | Schicht- und Personalplanung, logische Bedingungen | | Quadratisch / Kegel (QP, SOCP) | CVXPY | Portfoliooptimierung (Varianz, Volatilität) mit kontinuierlichen Anteilen | | Gemischt-ganzzahlig quadratisch (MIQP) | SCIP (`pyscipopt`) | Portfolio mit Kardinalität (mind./höchstens k Titel), Mindestgewicht nur für gewählte Titel, fixe Transaktionskosten | | Nichtlinear, stetig (NLP) | Ipopt (`cyipopt`) | nichtlineare Ausbeute- oder Kostenfunktionen, Kurvenanpassung; mehrere Startpunkte, Streuung wird berichtet | | Kürzeste Wege (Graph) | NetworkX (Dijkstra) | Wegewahl im Netz, Ersatz- und Erneuerungsprobleme; der Report zeichnet den Graphen und zeigt den Markierungsverlauf | ## Installation Voraussetzungen: Python ≥ 3.10, Linux. Systempakete: - `bubblewrap` für die Sandbox des erzeugten Codes (Debian/Ubuntu: `sudo apt install bubblewrap`, siehe Sicherheitshinweis) - die Ipopt-Bibliothek für `cyipopt` (Debian/Ubuntu: `sudo apt install coinor-libipopt-dev pkg-config`), sofern pip kein fertiges Paket findet ```bash python -m venv .venv .venv/bin/pip install -r requirements.txt ``` API-Key als Umgebungsvariable setzen (nicht in Dateien speichern): | Anbieter | Variable | Modellnamen | |---|---|---| | OpenRouter (Standard) | `OPENROUTER_API_KEY` | mit `/`, z. B. `deepseek/deepseek-v4.1-flash` | | Google Gemini | `GEMINI_API_KEY` | ohne `/`, z. B. `gemini-2.5-flash` | Der Anbieter wird am Modellnamen erkannt. Das kostenlose Gemini-Kontingent reicht nur für wenige Läufe pro Tag. ## Benutzung ```bash # Aufgabe aus Datei lösen, Report und Solver-Code speichern .venv/bin/python TextToModel.py aufgabe.txt --report ausgabe/modell.html --code ausgabe/solve.py > ausgabe/ergebnis.json # Aufgabe über stdin echo "Ein Betrieb ..." | .venv/bin/python TextToModel.py --report ausgabe/modell.md # nur modellieren und prüfen, nicht lösen; anderes Modell .venv/bin/python TextToModel.py aufgabe.txt --no-solve --model deepseek/deepseek-v4-pro-0813 # Report nachträglich aus gespeichertem Ergebnis erzeugen (keine LLM-Kosten) .venv/bin/python report.py ausgabe/ergebnis.json ausgabe/modell.md # exportiertes Solver-Programm erneut ausführen .venv/bin/python ausgabe/solve.py # Variante rechnen: geänderte Eingabedaten, gleiches Programm, KEIN LLM-Aufruf .venv/bin/python TextToModel.py --resolve ausgabe/ergebnis.json --data neue_daten.json > ausgabe/variante.json # Aufgabe mit beigelegten Daten (CSV/Excel); das Profil geht in die Prompts, die Datei ins Programm .venv/bin/python TextToModel.py aufgabe.md --daten kurse.csv --report ausgabe/modell.html # nur die Solver-Stufe wiederholen: Modell und Prüfbericht bleiben, spart die Aufrufe davor .venv/bin/python TextToModel.py --von solver ausgabe/ergebnis.json > ausgabe/neu.json ``` Der Ordner `ausgabe/` ist in `.gitignore` eingetragen. Ohne Eingabe wird eine eingebaute Beispielaufgabe verwendet. Fortschritt und Kosten erscheinen auf stderr, das Ergebnis-JSON auf stdout. | Option / Variable | Bedeutung | Standard | |---|---|---| | `--report DATEI` | Report als `.md` (z. B. VS Code-Vorschau: Strg+Umschalt+V) oder `.html` (Browser, Formeln via MathJax) | – | | `--code DATEI` | Solver-Programm als eigenständiges, dokumentiertes Python-Skript speichern | – | | `--model` / `LLM_MODEL` | LLM-Modell | `deepseek/deepseek-v4.1-flash` | | `--daten DATEI …` | beigelegte CSV-/Excel-Dateien; ihr Profil (Zeilen, Spalten, Spannweiten, Lücken) geht in die Prompts, die Dateien ins Arbeitsverzeichnis des Programms | – | | `--von STUFE ERGEBNIS.JSON` | gespeichertes Ergebnis fortsetzen statt alles zu wiederholen: `solver` (nur Solver-Code neu) oder `pruefung` (ab Prüfung/Korrektur) | – | | `--max-revisions` / `MAX_REVISIONS` | Korrekturrunden | 2 | | `--no-solve` | keinen Solver-Code erzeugen | aus | | `SOLVER_RETRIES` | Reparaturversuche für fehlerhaften Solver-Code | 2 | | `INFEASIBLE_RECHECK` | Gegenprobe mit unabhängiger Modellierung bei „unlösbar“ (0 schaltet ab) | 1 | | `LLM_MAX_TOKENS` | Obergrenze je Antwort (OpenRouter) | 32000 | | `--max-restarts` / `MAX_RESTARTS` | komplette Neustarts, wenn das Modell nach allen Korrekturrunden fehlerhaft ist | 1 | | `OPENROUTER_MIN_THROUGHPUT` | bevorzugte Mindestgeschwindigkeit des OpenRouter-Anbieters (Tokens/s); Präferenz, kein harter Filter | 75 | | `LLM_TIMEOUT` | Timeout je LLM-Aufruf (s) | 420 | | `SOLVER_SANDBOX` | Isolierung des erzeugten Codes: `auto` (bubblewrap, wenn vorhanden), `bwrap` (Pflicht), `aus` | `auto` | | `LLM_MODEL_EXTRACTION`, `LLM_MODEL_REVIEW`, `LLM_MODEL_REVISION`, `LLM_MODEL_SOLVER` | Modell einzelner Pipeline-Stufen, z. B. ein schnelles für den Solver-Code; verschiedene Anbieter sind erlaubt | Modell aus `--model` | ### Solver-Programme Das erzeugte Programm (`--code` sowie im HTML-/Markdown-Report) ist **separat verwendbar** und aufgebaut als: 1. `#!/usr/bin/env python3` 2. Docstring mit **Aufgabentext**, **mathematischem Modell** (Indizes, Parameter, Variablen, Zielfunktion, Restriktionen – mit Erläuterungen und Annahmen) und dem **Algorithmus** samt Begründung, warum er hier passt 3. kommentierter Python-Code: Imports, Eingabedaten als Konstanten, Modellfunktion(en), `main()` 4. `if __name__ == "__main__": main()` Die Ausgabe ist lesbar – Status, Zielwert, **verständliche Ergebnistabellen** (z. B. je Aktie: Anteil, Rendite, Kosten, Beitrag zum Ziel, Summenzeile) und die Variablenwerte – und endet mit einer maschinenlesbaren `RESULT_JSON`-Zeile. Die Tabellen berechnet das Programm aus der tatsächlichen Lösung; die Pipeline prüft automatisch, dass die Beiträge sich zum Zielwert summieren, und warnt sonst. Der Report beginnt mit dem **Ergebnis**: ein Antwortsatz, die Kennzahlen, die Ergebnistabellen samt **Diagramm** (Balken je Objekt, Wasserfall für die Zielwert-Zerlegung aus Ertrag, Risiko und Kosten) und eine Tabelle der **bindenden Restriktionen**. Darunter folgen die Rohwerte nach Variablen gruppiert, danach Aufgabe, Modell, Prüfbericht und Programm. Die Diagramme sind reines SVG ohne externe Bibliothek und funktionieren offline, hell wie dunkel. Im HTML-Report lässt sich das Programm mit einem Klick auf das Kopier-Symbol in die Zwischenablage übernehmen. Das JSON-Ergebnis enthält weiterhin nur den rohen generierten Code. **Mit Datenanhängen (`--daten`):** Das erzeugte Programm liest die Dateien über ihren Dateinamen aus dem Arbeitsverzeichnis. Ein mit `--code` exportiertes Programm braucht die Datendateien deshalb **im selben Verzeichnis** – sonst endet es mit `FileNotFoundError`. Kopiere sie mit: ```bash .venv/bin/python TextToModel.py aufgabe.md --daten kurse.csv --code ausgabe/solve.py cp kurse.csv ausgabe/ && (cd ausgabe && ../.venv/bin/python solve.py) ``` ## Wie verlässlich ist das? Das LLM kann Fehler machen – deshalb gibt es mehrere Sicherungen: - **Zielgrößen-Prüfung:** erkennt z. B., wenn die Varianz statt der gefragten Volatilität minimiert wird. - **Annahmen-Prüfung:** Unklarheiten im Text müssen als Annahme ausgewiesen werden und werden gegen den Text geprüft (z. B. „wenn A, dann B“ ist keine Äquivalenz). - **Zulässigkeitsprüfung ohne LLM:** Die gefundene Lösung wird in alle Restriktionen eingesetzt, Schranken und Ganzzahligkeit geprüft und abgeglichen, ob jede Restriktion des Modells im Programm vorkommt. Verletzungen lösen eine Reparatur aus – Solver-Toleranzen (beobachtet: 8 Cent über dem Budget) fallen damit auf. - **Unlösbare Aufgaben** werden als `infeasible` gemeldet, nicht durch Weglassen von Bedingungen „passend gemacht“ – aber auch nicht ungeprüft geglaubt: Eine zweite, unabhängige Modellierung bestätigt oder widerlegt den Status. - **Beste Korrekturrunde:** Verschlechtert eine Korrektur das Modell, wird die frühere Fassung verwendet. - **Sensitivität:** Für bindende Restriktionen rechnet das Programm mit verschobener rechter Seite neu; der Report zeigt, was eine Lockerung je Einheit bringt, und bei ganzzahligen Modellen den Preis der Ganzzahligkeit. Unplausible Richtungen werden gemeldet. - **Neustart bei Fremdproblem:** Gelegentlich modelliert das LLM ein anderes als das gestellte Problem. Passt die Zielgröße auch nach allen Korrekturrunden nicht, wird die Modellierung komplett neu gestartet. - **Jede Teilfrage beantwortet:** Der Aufgabentext wird vor dem Modellieren in seine Teilfragen zerlegt (a, b, …); das Programm liefert je Teilfrage einen Antwortsatz. Fehlt eine Antwort, gilt die Aufgabe als nicht erfüllt – auch wenn der Zielwert stimmt. - **Jede Zahl der Antwort belegt:** Zahlen im Antworttext müssen aus dem Ergebnis oder der Aufgabe stammen (Rundungen und Einheitenumrechnungen werden akzeptiert). Ein Vergleichswert, der in keiner Tabelle steht, fällt auf. - **Jede Zahl der Aufgabe erfasst:** Zahlen des Aufgabentexts müssen als Fakt mit Fundstelle oder mit Begründung als irrelevant ausgewiesen sein – sonst betreten sie die Pipeline erst im Solver-Code, hinter allen Prüfungen. - **Selbst erzeugte Daten sind gekennzeichnet:** Erzeugt das Programm Eingabedaten (Zufallszahlen, synthetischer Markt, verkleinertes Universum), muss das in `data_assumptions` stehen, nicht nur im Fließtext – sonst folgt eine Reparatur und notfalls die Markierung „unzuverlässig“. - **Szenarien nachgerechnet:** Vergleicht die Aufgabe mehrere Konfigurationen, wird jede durch erneutes Ausführen des Programms bestätigt – ohne LLM. - **Sandbox:** Der erzeugte Code läuft ohne Netzzugang, mit nur lesbarem Dateisystem und beschreibbar allein dem Arbeitsverzeichnis des Laufs. - **Kein falsches „optimal“:** Bleibt das Modell fehlerhaft, wird das Ergebnis als **unzuverlässig** markiert – bei falscher Zielgröße ohne Solver-Lauf, mit Warnkasten im Report und Exit-Code 2. Trotzdem gilt: **Ergebnisse vor Verwendung prüfen** – der Report zeigt dafür Modell, Annahmen, Prüfbericht und Code. ### Benchmark `benchmark/cases/` enthält 19 Aufgaben mit **unabhängig verifiziertem Optimum** (Brute Force bzw. zweiter Solver) – von kleinen LPs bis zu Aufgaben mit eingebauten Fallen (Rüstkosten/Mindestlos, Wenn-dann-Regel als Implikation, Volatilität statt Varianz, nichtkonvexes NLP mit globalem Optimum, beweisbar unlösbare Mischung, Losgrößen als kürzester Weg). Gemessen wird der **Zielwert** und, strenger, ob die **Aufgabe erfüllt** ist: jede Teilfrage beantwortet, jede Zahl der Antwort belegt. Dateien im Fallverzeichnis außer `task.txt`/`expected.json` werden als Datenanhang übergeben. ```bash .venv/bin/python benchmark.py --models deepseek/deepseek-v4.1-flash deepseek/deepseek-v4-flash-0731 --runs 2 --jobs 3 .venv/bin/python benchmark.py --cases hard_schichtplan_woche --runs 3 ``` Letzter Stand (September 2026, DeepSeek V4.1-Flash über OpenRouter, je 2 Läufe): **38 von 38 Läufen** über alle 19 Fälle mit richtigem Zielwert und erfüllter Aufgabe, etwa 0,05–0,15 $ und 3–10 Minuten je Lauf. Die Fallmenge ist damit gesättigt; die Grenzen der Pipeline zeigen sich eher an schlecht gestellten Aufgaben (fehlende Daten, mehrdeutige Angaben) – Beispiele dafür liegen in `aufgaben/`. Hinweis: Drei Fälle wurden nach dieser Messung sprachlich neu gefasst (gleiche Zahlen und Optima). ## Tests ```bash .venv/bin/python -m pytest -q # offline, ohne API-Kosten .venv/bin/python -m pytest -q test_solver_stage.py -k cvxpy # einzelner Test ``` ## Projektstruktur | Datei | Inhalt | |---|---| | `TextToModel.py` | CLI und Pipeline (`run_pipeline`) | | `model_schema.py` | Pydantic-Schemas aller Zwischenergebnisse | | `model_checks.py` | deterministische Konsistenz-Checks | | `solver_stage.py` | Solver-Code erzeugen, prüfen, ausführen | | `llm.py` | LLM-Zugriff (OpenRouter/Gemini), Wiederholungen, Kostenerfassung | | `report.py` | Report als Markdown/HTML (mit Kopier-Symbol), auch als eigenständiges CLI | | `script_export.py` | dokumentiertes, eigenständiges Solver-Programm (Kopf mit Aufgabe, Modell, Algorithmus) | | `data_profile.py` | Profil beigelegter CSV-/Excel-Dateien, ohne LLM | | `charts.py` | Diagramme und Graphenzeichnung als Inline-SVG | | `benchmark.py`, `benchmark/cases/` | Benchmark und Referenzaufgaben | | `aufgaben/` | weitere Aufgaben zum Ausprobieren (u. a. mit fehlenden Daten) | | `*-Prompt.txt` | System-Prompts für Verstehen, Extraktion, Prüfung und Solver-Code | | `test_*.py` | Offline-Tests | ## Sicherheitshinweis Der Solver-Code wird vom LLM erzeugt. Er läuft in einer **bubblewrap-Sandbox**: kein Netzzugang, das Dateisystem nur lesbar, beschreibbar allein das Temp-Verzeichnis des jeweiligen Laufs, `HOME` zeigt ebenfalls dorthin. Davor greifen weiterhin eine Import-Whitelist (nur Mathe- und Solver-Bibliotheken), das Verbot von `open`/`eval`/`exec` und ein Timeout. Voraussetzung ist ein installiertes `bubblewrap` (Debian/Ubuntu: `sudo apt install bubblewrap`). Fehlt es, läuft der Code **ungeschützt mit deinen Benutzerrechten** – dann nur eigene, vertrauenswürdige Aufgabentexte verwenden. Mit `SOLVER_SANDBOX=bwrap` wird die Sandbox zur Pflicht: ohne bubblewrap wird der Lauf abgelehnt statt ungeschützt ausgeführt. ```bash .venv/bin/python -m pytest -q -k sandbox # prüft, dass Schreiben außerhalb des Laufs scheitert ``` ## Lizenz MIT – siehe [`LICENSE`](LICENSE).