"""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 \"\"\" 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 .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)