Ü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>
194 lines
10 KiB
Python
194 lines
10 KiB
Python
"""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)
|