text_to_solver_program/solving/script_export.py
dschlueter 7459690bf5 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

194 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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 core.model_schema import PipelineResult, ProblemClass, TargetStack
from solving.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)