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
194
script_export.py
Normal file
194
script_export.py
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
"""Eigenständiges, dokumentiertes Solver-Skript aus einem PipelineResult bauen (für --code und Reports).
|
||||
|
||||
Aufbau des Skripts:
|
||||
#!/usr/bin/env python3
|
||||
r\"\"\" Aufgabe / Mathematisches Modell / Algorithmus \"\"\"
|
||||
<generierter Code: Imports, Daten, Funktionen, main()>
|
||||
<report()-Hilfsfunktion>
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Das JSON-Ergebnis enthält weiterhin nur den rohen generierten Code (solver_code.code).
|
||||
"""
|
||||
import re
|
||||
import textwrap
|
||||
|
||||
from model_schema import PipelineResult, ProblemClass, TargetStack
|
||||
from solver_stage import assemble_script
|
||||
|
||||
WIDTH = 88
|
||||
|
||||
# Verfahren je Solver-Stack (feste Texte, nicht vom LLM erzeugt)
|
||||
ALGORITHMS = {
|
||||
TargetStack.HIGHS: (
|
||||
"HiGHS (highspy)",
|
||||
"Lineare Relaxationen werden mit dem dualen revidierten Simplex-Verfahren (alternativ "
|
||||
"Innere-Punkte-Verfahren) gelöst. Ganzzahlige und binäre Variablen behandelt HiGHS mit "
|
||||
"Branch-and-Cut: Das Problem wird schrittweise in Teilprobleme zerlegt, deren LP-Relaxation "
|
||||
"Schranken liefert; Schnittebenen und Heuristiken verkleinern den Suchbaum. Der Status "
|
||||
"'optimal' bedeutet, dass die Optimalitätslücke (im Rahmen der Toleranz) geschlossen ist.",
|
||||
),
|
||||
TargetStack.OR_TOOLS_CPSAT: (
|
||||
"Google OR-Tools CP-SAT",
|
||||
"CP-SAT kombiniert Constraint-Propagation mit einem SAT-Solver (Lazy Clause Generation, "
|
||||
"konfliktgesteuertes Klausellernen) und LP-Relaxationen; mehrere Suchstrategien laufen parallel. "
|
||||
"Logische Bedingungen (Implikationen, 'nicht gemeinsam', Reihenfolgen) werden direkt und exakt "
|
||||
"verarbeitet. Der Status 'optimal' bedeutet, dass die Optimalität bewiesen wurde. CP-SAT arbeitet "
|
||||
"nur mit ganzzahligen Werten.",
|
||||
),
|
||||
TargetStack.CVXPY: (
|
||||
"CVXPY",
|
||||
"CVXPY prüft, ob das Modell den Regeln der disziplinierten konvexen Programmierung (DCP) genügt, "
|
||||
"formt es in ein konisches Standardproblem um und übergibt es einem passenden Solver – für "
|
||||
"Kegel- und quadratische Probleme typischerweise einem Innere-Punkte-Verfahren (z. B. Clarabel) "
|
||||
"oder OSQP. Bei konvexen Problemen ist jedes lokale Optimum zugleich global optimal.",
|
||||
),
|
||||
TargetStack.SCIP: (
|
||||
"SCIP (pyscipopt)",
|
||||
"SCIP löst gemischt-ganzzahlige nichtlineare Probleme mit Branch-and-Bound bzw. Branch-and-Cut: "
|
||||
"Ganzzahlige Variablen werden verzweigt, quadratische Terme durch lineare Unterschätzer (Schnittebenen, "
|
||||
"bei nichtkonvexen Termen räumliches Verzweigen) relaxiert; Propagation, Presolve und Heuristiken "
|
||||
"verkleinern den Suchbaum. Der Status 'optimal' bedeutet, dass die Optimalitätslücke im Rahmen der "
|
||||
"Toleranz geschlossen ist. Da SCIP nur lineare Zielfunktionen verarbeitet, wird ein quadratisches Ziel "
|
||||
"über eine Hilfsvariable (Epigraph-Formulierung) abgebildet.",
|
||||
),
|
||||
TargetStack.IPOPT: (
|
||||
"Ipopt (cyipopt)",
|
||||
"Ipopt ist ein Innere-Punkte-Verfahren für glatte nichtlineare Optimierung: Es folgt einer Folge von "
|
||||
"Barriereproblemen, nutzt Ableitungen (hier numerisch bestimmt) und konvergiert gegen einen Punkt, der "
|
||||
"die Karush-Kuhn-Tucker-Bedingungen erfüllt. Das ist ein LOKALES Optimum – bei konvexen Problemen "
|
||||
"zugleich das globale, sonst nicht. Mehrere Startpunkte verringern das Risiko, in einem schlechten "
|
||||
"lokalen Punkt zu landen; ganzzahlige Variablen kann Ipopt nicht behandeln.",
|
||||
),
|
||||
TargetStack.NETWORKX: (
|
||||
"NetworkX (Dijkstra)",
|
||||
"Der Dijkstra-Algorithmus markiert Knoten schrittweise: Er nimmt stets den unmarkierten Knoten mit "
|
||||
"der kleinsten vorläufigen Entfernung, erklärt dessen Marke für endgültig und verbessert damit die "
|
||||
"Marken seiner Nachbarn. Da jede Marke beim Endgültigwerden bereits minimal ist, steht das Ergebnis "
|
||||
"nach einem Durchlauf fest – ohne Suchbaum und ohne Relaxation. Voraussetzung sind nicht negative "
|
||||
"Bogengewichte: bei negativen Gewichten liefert das Verfahren stillschweigend ein falsches Ergebnis.",
|
||||
),
|
||||
TargetStack.SCIPY: (
|
||||
"SciPy scipy.optimize.linprog (method='highs')",
|
||||
"linprog nutzt die HiGHS-Verfahren für lineare Programme (duales Simplex- oder "
|
||||
"Innere-Punkte-Verfahren). Das Optimum eines LP liegt an einer Ecke des zulässigen Polyeders "
|
||||
"und wird exakt (im Rahmen der Toleranz) gefunden. Ganzzahligkeit wird nicht unterstützt.",
|
||||
),
|
||||
}
|
||||
|
||||
# Warum die Problemklasse zum Verfahren passt (feste Texte)
|
||||
CLASS_FIT = {
|
||||
ProblemClass.LP: "Zielfunktion und Restriktionen sind linear, alle Variablen kontinuierlich – ein LP, "
|
||||
"für das Simplex-/Innere-Punkte-Verfahren beweisbar optimale Lösungen liefern.",
|
||||
ProblemClass.MILP: "Zielfunktion und Restriktionen sind linear, aber einige Variablen sind ganzzahlig "
|
||||
"oder binär – ein MILP, für das Branch-and-Cut das Standardverfahren ist.",
|
||||
ProblemClass.CP: "Das Problem besteht aus diskreten Entscheidungen mit logischen Verknüpfungen – "
|
||||
"typisch für Constraint Programming, das solche Bedingungen direkt propagiert.",
|
||||
ProblemClass.QP: "Die Zielfunktion ist quadratisch und konvex (positiv semidefinite Matrix), die "
|
||||
"Restriktionen sind linear – ein konvexes QP mit global optimaler Lösung.",
|
||||
ProblemClass.MIQP: "Das Problem verbindet quadratische Terme (z. B. Varianz/Risiko) mit ganzzahligen oder "
|
||||
"binären Entscheidungen (z. B. Auswahl- oder Kardinalitätsbedingungen) – ein MIQP, das "
|
||||
"weder reine Kegelsolver (keine Ganzzahligkeit) noch CP-SAT (keine kontinuierlichen "
|
||||
"Variablen) abdecken, wohl aber SCIPs Branch-and-Cut für nichtlineare Probleme.",
|
||||
ProblemClass.NLP: "Zielfunktion oder Restriktionen sind glatt nichtlinear und alle Variablen stetig – ein NLP, "
|
||||
"für das Innere-Punkte-Verfahren (Ipopt) effizient einen KKT-Punkt liefern; globale Optimalität "
|
||||
"ist nur bei konvexen Problemen garantiert.",
|
||||
ProblemClass.SHORTEST_PATH: "Gesucht ist eine Folge von Entscheidungen, die sich als Weg durch einen Graphen "
|
||||
"lesen lässt: Knoten sind Zustände (Zeitpunkte, Orte), Bögen die Übergänge mit ihren "
|
||||
"Kosten. Für solche Probleme gibt es mit dem Dijkstra-Algorithmus ein exaktes "
|
||||
"Verfahren, das ohne Ganzzahligkeitsbedingungen auskommt.",
|
||||
ProblemClass.SOCP: "Das Problem enthält Normen bzw. Wurzeln quadratischer Formen – ein konvexes "
|
||||
"Kegelproblem (SOCP), das Innere-Punkte-Verfahren effizient und global optimal lösen.",
|
||||
}
|
||||
|
||||
|
||||
def _plain(text) -> str:
|
||||
"""Inline-Mathe-Begrenzer \\( \\) \\[ \\] entfernen – im Docstring stehen Formeln als Klartext."""
|
||||
return re.sub(r"\\[()\[\]]", "", str(text))
|
||||
|
||||
|
||||
def _wrap(text: str, indent: str = "") -> str:
|
||||
paragraphs = [p for p in _plain(text).strip().split("\n") if p.strip()] or [""]
|
||||
return "\n".join(
|
||||
textwrap.fill(p.strip(), width=WIDTH, initial_indent=indent, subsequent_indent=indent) for p in paragraphs
|
||||
)
|
||||
|
||||
|
||||
def _heading(title: str) -> str:
|
||||
return f"{title}\n{'=' * len(title)}"
|
||||
|
||||
|
||||
def build_header(result: PipelineResult) -> str:
|
||||
spec = result.specification
|
||||
parts = []
|
||||
|
||||
if result.task.strip():
|
||||
parts += [_heading("AUFGABE"), result.task.strip()]
|
||||
|
||||
model = [_heading("MATHEMATISCHES MODELL"),
|
||||
f"Problemklasse: {spec.problem_class.value}",
|
||||
"",
|
||||
_wrap(spec.executive_summary)]
|
||||
if spec.indices:
|
||||
model += ["", "Mengen und Indizes:"] + [f" - {_plain(i)}" for i in spec.indices]
|
||||
if spec.parameters:
|
||||
model += ["", "Parameter:"]
|
||||
for p in spec.parameters:
|
||||
model.append(_wrap(f"- {p.symbol} [{p.dimension}]: {p.description}", " "))
|
||||
if p.extracted_values:
|
||||
model.append(_wrap(f"Werte: {p.extracted_values}", " "))
|
||||
model += ["", "Entscheidungsvariablen:"]
|
||||
for v in spec.variables:
|
||||
model.append(_wrap(f"- {v.name} ({v.var_type.value}, {v.domain}): {v.description}", " "))
|
||||
model += ["", f"Zielfunktion ({spec.objective.sense.value}):",
|
||||
f" {_plain(spec.objective.latex_formula)}",
|
||||
_wrap(spec.objective.description, " "),
|
||||
"", "Restriktionen:"]
|
||||
for n, c in enumerate(spec.constraints, 1):
|
||||
soft = "" if c.is_hard_constraint else " (weich)"
|
||||
model += [f" ({n}) {c.identifier}{soft}:", f" {_plain(c.latex_formula)}", _wrap(c.description, " ")]
|
||||
if spec.open_assumptions:
|
||||
model += ["", "Annahmen (Aufgabentext war hier nicht eindeutig):"]
|
||||
for a in spec.open_assumptions:
|
||||
model.append(_wrap(f"- {a.unclear_point}: {a.assumption} (Auswirkung: {a.impact})", " "))
|
||||
if result.solver_code and result.solver_code.data_assumptions:
|
||||
model += ["", "Angenommene Daten (nicht im Aufgabentext enthalten):"]
|
||||
model += [_wrap(f"- {d}", " ") for d in result.solver_code.data_assumptions]
|
||||
model += ["", "Formeln in LaTeX-Schreibweise."]
|
||||
parts.append("\n".join(model))
|
||||
|
||||
name, method = ALGORITHMS[spec.target_stack]
|
||||
parts.append("\n".join([
|
||||
_heading("ALGORITHMUS"),
|
||||
f"Solver: {name}",
|
||||
"",
|
||||
"Verfahren:",
|
||||
_wrap(method, " "),
|
||||
"",
|
||||
"Warum dieses Verfahren hier passt:",
|
||||
_wrap(CLASS_FIT[spec.problem_class], " "),
|
||||
_wrap(spec.routing_rationale, " "),
|
||||
]))
|
||||
|
||||
if result.solver_run:
|
||||
run = result.solver_run
|
||||
parts.append("\n".join([
|
||||
_heading("ERGEBNIS BEI DER ERZEUGUNG"),
|
||||
f"Status: {run.status.value}, Zielwert: {run.objective_value}",
|
||||
"Aufruf: python3 <diese_datei>.py",
|
||||
]))
|
||||
return "\n\n".join(parts)
|
||||
|
||||
|
||||
def _docstring(text: str) -> str:
|
||||
# Roh-String, damit LaTeX-Backslashes keine Escape-Warnungen erzeugen
|
||||
text = text.replace('"""', "'''").rstrip()
|
||||
if text.endswith("\\"):
|
||||
text += " "
|
||||
return f'r"""\n{text}\n"""\n'
|
||||
|
||||
|
||||
def build_solver_script(result: PipelineResult) -> str:
|
||||
if not result.solver_code:
|
||||
raise ValueError("Ergebnis enthält keinen Solver-Code (Lauf mit --no-solve?)")
|
||||
return "#!/usr/bin/env python3\n" + _docstring(build_header(result)) + "\n" + assemble_script(result.solver_code.code)
|
||||
Loading…
Reference in a new issue