Erste öffentliche Fassung: Math Model Generator & Solver
Übersetzt natürlichsprachige Optimierungsaufgaben (Deutsch) per LLM in eine geprüfte Modellspezifikation, erzeugt Solver-Code und führt ihn in einer bubblewrap-Sandbox aus. Deterministische Prüfungen ohne LLM, Report als Markdown/HTML, Benchmark mit 19 verifizierten Fällen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
commit
b74445b8d6
1 changed files with 7655 additions and 0 deletions
203
README.md
Normal file
203
README.md
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
# 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).
|
||||
Loading…
Reference in a new issue