2026-09-17 11:42:58 +02:00
|
|
|
|
"""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
|
|
|
|
|
|
|
refactor: Pipeline-Stufen in Ordner mit ihren Prompts
Im Wurzelverzeichnis liegt nur noch TextToModel.py (Ablauf und CLI).
Jede Stufe liegt mit Code und Prompt in einem eigenen Ordner: core,
understanding, modeling, checks, solving, reporting, data, benchmark;
Tests unter tests/, Aufgaben unter tasks/. understand_task,
text_to_model/revise_model und review_assumptions/needs_work/select_best
sind aus TextToModel.py zu ihren Prompts gewandert, inhaltlich
unveraendert. Prompt-Kennung des Benchmarks bleibt 93744636095a.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 12:11:51 +02:00
|
|
|
|
from core.model_schema import PipelineResult, ProblemClass, TargetStack
|
|
|
|
|
|
from solving.solver_stage import assemble_script
|
2026-09-17 11:42:58 +02:00
|
|
|
|
|
|
|
|
|
|
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)
|