feat: Szenario ohne Ergebnis nicht mehr zuverlässig; Schlupf aus dem Solver-Prompt

check_scenarios_solved meldet Szenarien ohne Zielwert, die nicht belegt unlösbar
sind (beobachtet: SCIP-Zeitlimit ohne Lösung als 'keine Lösung' gemeldet);
Befund löst Solver-Reparatur aus, bleibt er, reliable=False. Solver-Prompt:
timelimit ohne Lösung ist keine Unlösbarkeit, Warmstart über createSol/addSol.
Der Fachbegriff Schlupf kam aus dem Prompt selbst und ist entfernt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-09-17 14:41:54 +02:00
commit 4bd701c87e
6 changed files with 110 additions and 5 deletions

View file

@ -186,6 +186,10 @@ def _solver_stage(session, task: str, result: PipelineResult, model: str,
result.reliable = False result.reliable = False
result.reliability_issues += [f"Datenherkunft: {p}" for p in run.undeclared_data] result.reliability_issues += [f"Datenherkunft: {p}" for p in run.undeclared_data]
log("ERGEBNIS UNZUVERLÄSSIG – selbst erzeugte Eingabedaten sind nicht ausgewiesen.") log("ERGEBNIS UNZUVERLÄSSIG – selbst erzeugte Eingabedaten sind nicht ausgewiesen.")
if run.unsolved_scenarios:
result.reliable = False
result.reliability_issues += [f"Szenario ohne Ergebnis: {p}" for p in run.unsolved_scenarios]
log("ERGEBNIS UNZUVERLÄSSIG – mindestens ein Szenario hat kein Ergebnis und ist nicht als unlösbar belegt.")
if result.solver_code.data_assumptions: if result.solver_code.data_assumptions:
log(f" Mit angenommenen Daten gelöst ({len(result.solver_code.data_assumptions)} Annahmen).") log(f" Mit angenommenen Daten gelöst ({len(result.solver_code.data_assumptions)} Annahmen).")

View file

@ -257,6 +257,8 @@ class Scenario(BaseModel):
default=None, description="Zielwert dieser Konfiguration, vom Programm berechnet") default=None, description="Zielwert dieser Konfiguration, vom Programm berechnet")
verified_value: Optional[float] = Field( verified_value: Optional[float] = Field(
default=None, description="Vom Runner durch erneutes Ausführen bestätigter Zielwert (ohne LLM)") default=None, description="Vom Runner durch erneutes Ausführen bestätigter Zielwert (ohne LLM)")
verified_status: Optional[str] = Field(
default=None, description="Status des Nachlaufs mit data_override (vom Runner gesetzt, ohne LLM)")
class Multistart(BaseModel): class Multistart(BaseModel):
@ -318,6 +320,8 @@ class SolverRun(BaseModel):
default_factory=list, description="Befunde der deterministischen Zulässigkeitsprüfung; leer = Lösung hält alles ein") default_factory=list, description="Befunde der deterministischen Zulässigkeitsprüfung; leer = Lösung hält alles ein")
undeclared_data: List[str] = Field( undeclared_data: List[str] = Field(
default_factory=list, description="Selbst erzeugte, nicht ausgewiesene Eingabedaten; leer = alles ausgewiesen") default_factory=list, description="Selbst erzeugte, nicht ausgewiesene Eingabedaten; leer = alles ausgewiesen")
unsolved_scenarios: List[str] = Field(
default_factory=list, description="Szenarien ohne Ergebnis, die nicht als unlösbar belegt sind; leer = jedes hat eines")
warnings: List[str] = Field(default_factory=list, description="Deterministische Prüfungen der Ergebnistabellen") warnings: List[str] = Field(default_factory=list, description="Deterministische Prüfungen der Ergebnistabellen")
error: Optional[str] = None error: Optional[str] = None
attempts: int = 1 attempts: int = 1

View file

@ -26,7 +26,8 @@ API je target_stack:
- SCIP (pyscipopt): `from pyscipopt import Model, quicksum`; `model = Model(); model.hideOutput(); model.setParam("limits/time", 30)`; Variablen `model.addVar(name, vtype="C"|"I"|"B", lb=..., ub=...)` (Achtung: Standard lb=0; für unbeschränkt `lb=None`); Restriktionen `model.addCons(ausdruck <= wert)` – auch quadratisch, z. B. `model.addCons(t * t >= quicksum(...))`. - SCIP (pyscipopt): `from pyscipopt import Model, quicksum`; `model = Model(); model.hideOutput(); model.setParam("limits/time", 30)`; Variablen `model.addVar(name, vtype="C"|"I"|"B", lb=..., ub=...)` (Achtung: Standard lb=0; für unbeschränkt `lb=None`); Restriktionen `model.addCons(ausdruck <= wert)` – auch quadratisch, z. B. `model.addCons(t * t >= quicksum(...))`.
WICHTIG: SCIP akzeptiert KEINE nichtlineare Zielfunktion (`setObjective` mit quadratischem Ausdruck wirft ValueError). Verwende eine Hilfsvariable (Epigraph): bei Maximierung `z = model.addVar("z", lb=None, ub=None)`, `model.addCons(z <= quadratischer_ausdruck)`, `model.setObjective(z, "maximize")`; bei Minimierung `model.addCons(z >= quadratischer_ausdruck)`, `model.setObjective(z, "minimize")`. WICHTIG: SCIP akzeptiert KEINE nichtlineare Zielfunktion (`setObjective` mit quadratischem Ausdruck wirft ValueError). Verwende eine Hilfsvariable (Epigraph): bei Maximierung `z = model.addVar("z", lb=None, ub=None)`, `model.addCons(z <= quadratischer_ausdruck)`, `model.setObjective(z, "maximize")`; bei Minimierung `model.addCons(z >= quadratischer_ausdruck)`, `model.setObjective(z, "minimize")`.
Standardabweichung/Volatilität: `t = model.addVar("t", lb=0)`, `model.addCons(t * t >= quicksum(Sigma[i][j] * w[i] * w[j] ...))`, `model.setObjective(t, "minimize")`. Standardabweichung/Volatilität: `t = model.addVar("t", lb=0)`, `model.addCons(t * t >= quicksum(Sigma[i][j] * w[i] * w[j] ...))`, `model.setObjective(t, "minimize")`.
Lösen `model.optimize()`; Status `model.getStatus()` ("optimal", "infeasible", "unbounded", "inforunbd", "timelimit" → bei vorhandener Lösung `model.getNSols() > 0` als "feasible" melden); Zielwert `model.getObjVal()`; Werte `model.getVal(var)`. Binärwerte mit `round()` ausgeben. Lösen `model.optimize()`; Status `model.getStatus()` ("optimal", "infeasible", "unbounded", "inforunbd", "timelimit" → bei vorhandener Lösung `model.getNSols() > 0` als "feasible" melden; `timelimit` mit `getNSols() == 0` ist KEINE Unlösbarkeit – dann nie „keine Lösung" oder „unlösbar" schreiben); Zielwert `model.getObjVal()`; Werte `model.getVal(var)`. Binärwerte mit `round()` ausgeben.
Startlösung (Warmstart): `sol = model.createSol(); model.setSolVal(sol, var, wert)` für jede Variable, dann `model.addSol(sol)` vor `model.optimize()`.
- NetworkX (Dijkstra): `import networkx as nx`; `G = nx.DiGraph()`; Bögen mit `G.add_edge(i, j, weight=c)`; `laenge, pfad = nx.single_source_dijkstra(G, quelle, ziel, weight="weight")` liefert Weglänge und Knotenfolge; alle endgültigen Marken in der Reihenfolge ihrer Vergabe: `marken = nx.single_source_dijkstra_path_length(G, quelle, weight="weight")` (das Wörterbuch ist in genau dieser Reihenfolge gefüllt). Ist das Ziel nicht erreichbar, wirft `single_source_dijkstra` `nx.NetworkXNoPath` – dann Status "infeasible" melden. - NetworkX (Dijkstra): `import networkx as nx`; `G = nx.DiGraph()`; Bögen mit `G.add_edge(i, j, weight=c)`; `laenge, pfad = nx.single_source_dijkstra(G, quelle, ziel, weight="weight")` liefert Weglänge und Knotenfolge; alle endgültigen Marken in der Reihenfolge ihrer Vergabe: `marken = nx.single_source_dijkstra_path_length(G, quelle, weight="weight")` (das Wörterbuch ist in genau dieser Reihenfolge gefüllt). Ist das Ziel nicht erreichbar, wirft `single_source_dijkstra` `nx.NetworkXNoPath` – dann Status "infeasible" melden.
WICHTIG: Bei negativen Bogengewichten wirft networkx KEINEN Fehler, liefert aber ein falsches Ergebnis (geprüft: 3 statt 2). Prüfe die Gewichte im Programm und melde Status "error" mit einem Hinweis, falls eines negativ ist. WICHTIG: Bei negativen Bogengewichten wirft networkx KEINEN Fehler, liefert aber ein falsches Ergebnis (geprüft: 3 statt 2). Prüfe die Gewichte im Programm und melde Status "error" mit einem Hinweis, falls eines negativ ist.
Melde den Graphen im Argument graph (siehe unten) – ohne ihn kann der Report die verlangte Zeichnung nicht erzeugen. Die Restriktionen der Spezifikation (Flusserhaltung) sind trotzdem in constraints auszuweisen, ausgewertet am gefundenen Weg. Melde den Graphen im Argument graph (siehe unten) – ohne ihn kann der Report die verlangte Zeichnung nicht erzeugen. Die Restriktionen der Spezifikation (Flusserhaltung) sind trotzdem in constraints auszuweisen, ausgewertet am gefundenen Weg.
@ -74,7 +75,7 @@ Ergebnisausgabe:
- variables: Liste aller Entscheidungsvariablen mit Wert, Typ und Schranken: - variables: Liste aller Entscheidungsvariablen mit Wert, Typ und Schranken:
{"name": "...", "value": <Zahl>, "var_type": "continuous" | "integer" | "binary", "lb": <Zahl oder null>, "ub": <Zahl oder null>} {"name": "...", "value": <Zahl>, "var_type": "continuous" | "integer" | "binary", "lb": <Zahl oder null>, "ub": <Zahl oder null>}
Die Pipeline prüft damit Schranken und Ganzzahligkeit. Höchstens 200 Einträge; bei mehr nur die Variablen ungleich 0. Die Pipeline prüft damit Schranken und Ganzzahligkeit. Höchstens 200 Einträge; bei mehr nur die Variablen ungleich 0.
- sensitivity: Wirkung der bindenden Restriktionen, durch NEULÖSEN ermittelt (nicht aus Solver-Attributen ablesen). Für höchstens 8 bindende Restriktionen (Schlupf = 0), am besten die inhaltlich wichtigsten: - sensitivity: Wirkung der bindenden Restriktionen, durch NEULÖSEN ermittelt (nicht aus Solver-Attributen ablesen). Für höchstens 8 bindende Restriktionen (linke Seite erreicht die Grenze genau), am besten die inhaltlich wichtigsten:
{"name": "<wie in constraints>", "delta": <Verschiebung > 0>, "objective_plus": <Zielwert mit rhs+delta oder null>, "objective_minus": <Zielwert mit rhs-delta oder null>, "unit": "€ je Einheit"} {"name": "<wie in constraints>", "delta": <Verschiebung > 0>, "objective_plus": <Zielwert mit rhs+delta oder null>, "objective_minus": <Zielwert mit rhs-delta oder null>, "unit": "€ je Einheit"}
- delta klein wählen (etwa 1 % der rechten Seite, mindestens eine sinnvolle Einheit), das Modell je Richtung einmal neu lösen und den Zielwert eintragen; bei unlösbar null. - delta klein wählen (etwa 1 % der rechten Seite, mindestens eine sinnvolle Einheit), das Modell je Richtung einmal neu lösen und den Zielwert eintragen; bei unlösbar null.
- Nur ausführen, wenn das Neulösen schnell geht (Gesamtlaufzeit unter 60 s), sonst leere Liste. - Nur ausführen, wenn das Neulösen schnell geht (Gesamtlaufzeit unter 60 s), sonst leere Liste.
@ -93,6 +94,7 @@ Ergebnisausgabe:
- data_override enthält genau die Einträge von DATA, die sich ändern. Das Programm rechnet jedes Szenario selbst und trägt seinen objective_value ein. - data_override enthält genau die Einträge von DATA, die sich ändern. Das Programm rechnet jedes Szenario selbst und trägt seinen objective_value ein.
- Die Pipeline führt dein Programm anschließend mit jedem data_override ERNEUT aus und vergleicht – abweichende Werte fallen auf. Kein LLM ist daran beteiligt. [→ verify_scenarios] - Die Pipeline führt dein Programm anschließend mit jedem data_override ERNEUT aus und vergleicht – abweichende Werte fallen auf. Kein LLM ist daran beteiligt. [→ verify_scenarios]
- Damit das funktioniert, muss das Programm ausschließlich aus DATA lesen; höchstens fünf Szenarien werden nachgerechnet. - Damit das funktioniert, muss das Programm ausschließlich aus DATA lesen; höchstens fünf Szenarien werden nachgerechnet.
- Jedes Szenario braucht einen objective_value – null nur, wenn es bewiesen unlösbar ist. Unterscheiden sich Szenarien nur in Zielgewichten, ist die Lösung eines Szenarios für die anderen zulässig: übergib sie als Startlösung, dann endet keines im Zeitlimit ohne Ergebnis. Zeitlimits so wählen, dass alle Läufe zusammen unter 60 s bleiben. [→ check_scenarios_solved]
- graph: NUR bei Graphenproblemen (Stack NetworkX), sonst null. Grundlage für die Zeichnung im Report: - graph: NUR bei Graphenproblemen (Stack NetworkX), sonst null. Grundlage für die Zeichnung im Report:
{"nodes": ["0", "1", "2", "3"], {"nodes": ["0", "1", "2", "3"],
"arcs": [{"source": "0", "target": "1", "weight": 8}, ...], "arcs": [{"source": "0", "target": "1", "weight": 8}, ...],
@ -121,7 +123,7 @@ Ergebnisausgabe:
Erzeuge nach Möglichkeit diese drei Tabellen (in dieser Reihenfolge): Erzeuge nach Möglichkeit diese drei Tabellen (in dieser Reihenfolge):
1. Die Objekttabelle wie oben beschrieben. 1. Die Objekttabelle wie oben beschrieben.
2. "Zielwert-Zerlegung": Zeilen für die Bestandteile der Zielfunktion (z. B. "Erwartete Rendite", "Risiko (λ·Varianz)", "Transaktionskosten") mit Spalten ["Bestandteil", "Wert", "Einheit"], Vorzeichen wie in der Zielfunktion (Abzüge negativ), objective_column "Wert", danach die Summenzeile "Zielwert". Alle Bestandteile müssen dieselbe Einheit haben; steht dort Unterschiedliches (z. B. "€" neben "dimensionslos"), ist das Modell fehlerhaft. 2. "Zielwert-Zerlegung": Zeilen für die Bestandteile der Zielfunktion (z. B. "Erwartete Rendite", "Risiko (λ·Varianz)", "Transaktionskosten") mit Spalten ["Bestandteil", "Wert", "Einheit"], Vorzeichen wie in der Zielfunktion (Abzüge negativ), objective_column "Wert", danach die Summenzeile "Zielwert". Alle Bestandteile müssen dieselbe Einheit haben; steht dort Unterschiedliches (z. B. "€" neben "dimensionslos"), ist das Modell fehlerhaft.
3. "Bindende Restriktionen": welche Grenzen die Lösung ausreizt, mit Spalten ["Restriktion", "Wert", "Grenze", "bindend"] und Werten aus der Lösung (z. B. "Höchstgewicht A02" | "30,00 %" | "30,00 %" | "ja"). Nur Tatsachen aus der Lösung, keine Bewertung; ohne objective_column. 3. "Bindende Restriktionen": welche Grenzen die Lösung ausreizt, mit Spalten ["Restriktion", "Wert", "Grenze", "bindend"] und Werten aus der Lösung (z. B. "Höchstgewicht A02" | "30,00 %" | "30,00 %" | "ja"). Nur Tatsachen aus der Lösung, keine Bewertung; ohne objective_column. Die note erklärt „bindend" in Alltagssprache („die Lösung schöpft diese Grenze voll aus") – ohne Fachbegriffe. [→ check_text_quality]
Zahlen in der objective_column immer im Maßstab von objective_value (nicht in Prozent umrechnen); Datenspalten dagegen in lesbaren Einheiten als Text ("1,63 %", "0,74 %", "36 h"). Zahlen in der objective_column immer im Maßstab von objective_value (nicht in Prozent umrechnen); Datenspalten dagegen in lesbaren Einheiten als Text ("1,63 %", "0,74 %", "36 h").
- Weitere print-Ausgaben sind erlaubt, aber nicht nötig. - Weitere print-Ausgaben sind erlaubt, aber nicht nötig.
- Liefere im Feld code nur den reinen Python-Code ohne Markdown-Codeblock. - Liefere im Feld code nur den reinen Python-Code ohne Markdown-Codeblock.

View file

@ -707,6 +707,7 @@ def verify_scenarios(run, code: str, timeout: int = 60, data_files=()) -> list:
nachlauf = run_solver_code(code, timeout=timeout, data={**(run.data or {}), **szenario.data_override}, nachlauf = run_solver_code(code, timeout=timeout, data={**(run.data or {}), **szenario.data_override},
data_files=data_files) data_files=data_files)
szenario.verified_value = nachlauf.objective_value szenario.verified_value = nachlauf.objective_value
szenario.verified_status = nachlauf.status.value
if nachlauf.status == SolverStatus.ERROR: if nachlauf.status == SolverStatus.ERROR:
grund = (nachlauf.error or "").strip()[:120] or ( grund = (nachlauf.error or "").strip()[:120] or (
"das Programm meldete selbst Status 'error' ohne Fehlertext (z. B. keine Lösung im Zeitlimit)") "das Programm meldete selbst Status 'error' ohne Fehlertext (z. B. keine Lösung im Zeitlimit)")
@ -719,6 +720,29 @@ def verify_scenarios(run, code: str, timeout: int = 60, data_files=()) -> list:
return warnings return warnings
def check_scenarios_solved(run) -> list:
"""Hat jedes nachgerechnete Szenario ein Ergebnis – oder ist es belegt unlösbar?
Beobachtet an Portfolio Nr. 2: „Ausgewogene Skalarisierung" ohne Zielwert, Teil 2b antwortete
„keine Lösung", das Ergebnis galt trotzdem als zuverlässig. Nachgestellt ohne LLM: SCIP meldete
nach 8 s `timelimit` mit 0 Lösungen, nach 30 s mit einer – bei derselben zulässigen Menge wie die
beiden gelösten Konfigurationen. Ein abgelaufenes Zeitlimit ist keine Unlösbarkeit.
"""
befunde = []
for szenario in run.scenarios[:MAX_SCENARIOS]:
if not szenario.data_override or szenario.objective_value is not None:
continue
if szenario.verified_status in (SolverStatus.INFEASIBLE.value, SolverStatus.UNBOUNDED.value):
continue
befunde.append(
f"Szenario '{szenario.name}' hat keinen Zielwert, ist aber nicht als unlösbar belegt (Nachlauf: "
f"{szenario.verified_status or 'nicht ausgeführt'}) – vermutlich ein Zeitlimit ohne Lösung. "
f"„Keine Lösung\" nur bei bewiesener Unlösbarkeit. Haben die Szenarien dieselben Restriktionen, die "
f"Lösung eines anderen Szenarios als Startlösung übergeben; Zeitlimits so wählen, dass alle Läufe "
f"zusammen unter 60 s bleiben.")
return befunde
# Zehnerpotenzen, mit denen Antworten Einheiten umrechnen: „9,00 Tsd. € (also 9000 €)", „%" statt Anteil # Zehnerpotenzen, mit denen Antworten Einheiten umrechnen: „9,00 Tsd. € (also 9000 €)", „%" statt Anteil
SCALES = [10 ** k for k in range(-3, 4)] SCALES = [10 ** k for k in range(-3, 4)]
@ -939,6 +963,7 @@ def _check_and_run(code: str, timeout: int, spec: OptimizationModelSpecification
understanding.model_dump_json() if understanding else "")) understanding.model_dump_json() if understanding else ""))
run.warnings += check_answers(run, understanding, task, data_summary) run.warnings += check_answers(run, understanding, task, data_summary)
run.warnings += verify_scenarios(run, code, timeout, data_files) run.warnings += verify_scenarios(run, code, timeout, data_files)
run.unsolved_scenarios = check_scenarios_solved(run)
return run return run
@ -956,12 +981,14 @@ def solve(session: LLMSession, task: str, spec: OptimizationModelSpecification,
run.undeclared_data = check_generated_data(solver_code.code, solver_code, spec) run.undeclared_data = check_generated_data(solver_code.code, solver_code, spec)
einheiten = check_answer_units(run, task, data_prompt) einheiten = check_answer_units(run, task, data_prompt)
attempt = 1 attempt = 1
while (run.status == SolverStatus.ERROR or run.feasibility or run.undeclared_data or einheiten) \ while (run.status == SolverStatus.ERROR or run.feasibility or run.undeclared_data or run.unsolved_scenarios
and attempt <= retries: or einheiten) and attempt <= retries:
fehler = run.error or (("Die Lösung hält das Modell nicht ein:\n- " + "\n- ".join(run.feasibility)) fehler = run.error or (("Die Lösung hält das Modell nicht ein:\n- " + "\n- ".join(run.feasibility))
if run.feasibility else if run.feasibility else
("Eingabedaten nicht ausgewiesen:\n- " + "\n- ".join(run.undeclared_data)) ("Eingabedaten nicht ausgewiesen:\n- " + "\n- ".join(run.undeclared_data))
if run.undeclared_data else if run.undeclared_data else
("Szenario ohne Ergebnis:\n- " + "\n- ".join(run.unsolved_scenarios))
if run.unsolved_scenarios else
("Einheit in der Antwort erfunden:\n- " + "\n- ".join(einheiten))) ("Einheit in der Antwort erfunden:\n- " + "\n- ".join(einheiten)))
log(f"Solver-Code fehlgeschlagen – Reparaturversuch {attempt}/{retries}: {fehler[:200]}") log(f"Solver-Code fehlgeschlagen – Reparaturversuch {attempt}/{retries}: {fehler[:200]}")
solver_code = generate_solver_code(session, task, spec, previous=solver_code, previous_error=fehler, solver_code = generate_solver_code(session, task, spec, previous=solver_code, previous_error=fehler,

View file

@ -583,3 +583,25 @@ def test_rejected_result_cannot_be_continued_and_counts_as_failure(tmp_path, mon
zeile = benchmark.run_case({"name": "gleichung", "task": "x! = 120", "expected": {}}, "fake", 1) zeile = benchmark.run_case({"name": "gleichung", "task": "x! = 120", "expected": {}}, "fake", 1)
assert zeile["solver_status"] == "abgelehnt" and zeile["task_ok"] is False assert zeile["solver_status"] == "abgelehnt" and zeile["task_ok"] is False
benchmark.print_table([zeile]) # die Ergebnistabelle verkraftet die Zeile benchmark.print_table([zeile]) # die Ergebnistabelle verkraftet die Zeile
def _mit_szenario(wert):
return STRUCTURED_CODE.replace(
" )\n\n\nif __name__",
' scenarios=[{"name": "Ausgewogen", "description": "-", "data_override": {"faktor": 2}, '
f'"objective_value": {wert}}}],\n )\n\n\nif __name__')
def test_scenario_without_result_is_repaired():
# beobachtet (Portfolio Nr. 2): Zeitlimit ohne Lösung wurde als „keine Lösung" gemeldet
session = FakeSession([GOOD], [OK], [SolverCode(code=_mit_szenario(None)), SolverCode(code=_mit_szenario(8.0))])
result = run_pipeline("Minimiere die Kosten.", session=session, max_revisions=0, max_restarts=0, solver_retries=1)
assert session.calls[SolverCode] == 2 # der Befund hat eine Reparatur ausgelöst
assert result.reliable and result.solver_run.unsolved_scenarios == []
def test_persistent_scenario_without_result_is_unreliable():
session = FakeSession([GOOD], [OK], [SolverCode(code=_mit_szenario(None))] * 2)
result = run_pipeline("Minimiere die Kosten.", session=session, max_revisions=0, max_restarts=0, solver_retries=1)
assert not result.reliable
assert any(p.startswith("Szenario ohne Ergebnis") for p in result.reliability_issues)

View file

@ -765,3 +765,49 @@ def test_scenario_failing_without_error_text_gets_a_reason():
run = run_solver_code(code) run = run_solver_code(code)
befunde = verify_scenarios(run, code) befunde = verify_scenarios(run, code)
assert any("doppelter Preis" in b and "ohne Fehlertext" in b for b in befunde) assert any("doppelter Preis" in b and "ohne Fehlertext" in b for b in befunde)
def test_scip_warm_start_as_in_the_prompt():
# Solver-Prompt: Lösung eines Szenarios als Startlösung für das nächste (createSol/setSolVal/addSol)
code = (
"from pyscipopt import Model, quicksum\n"
"m = Model(); m.hideOutput(); m.setParam('limits/time', 5)\n"
"x = [m.addVar(f'x{i}', vtype='B') for i in range(3)]\n"
"m.addCons(quicksum(x) == 2)\n"
"m.setObjective(quicksum((i + 1) * x[i] for i in range(3)), 'maximize')\n"
"sol = m.createSol()\n"
"for v, wert in zip(x, [1, 1, 0]):\n"
" m.setSolVal(sol, v, wert)\n"
"assert m.addSol(sol)\n"
"m.optimize()\n"
"report(m.getStatus(), m.getObjVal(), {})"
)
run = run_solver_code(code)
assert run.status == SolverStatus.OPTIMAL, run.error
assert run.objective_value == 5.0
def test_verify_scenarios_records_the_status_of_the_rerun():
from solving.solver_stage import verify_scenarios
run = run_solver_code(SZENARIO_CODE)
verify_scenarios(run, SZENARIO_CODE)
assert [s.verified_status for s in run.scenarios] == ["optimal", "optimal"]
@pytest.mark.parametrize("wert, status, befund", [
(None, "error", True), # beobachtet: Zeitlimit ohne Lösung, als „keine Lösung" gemeldet
(None, None, True), # gar nicht nachgerechnet
(None, "infeasible", False), # belegt unlösbar – ehrlich
(None, "unbounded", False),
(12.5, "optimal", False),
])
def test_scenario_without_result_is_a_finding_unless_proven_unsolvable(wert, status, befund):
from core.model_schema import Scenario, SolverRun
from solving.solver_stage import check_scenarios_solved
run = SolverRun(status=SolverStatus.OPTIMAL, objective_value=1.0, scenarios=[
Scenario(name="Ausgewogene Skalarisierung", description="-", data_override={"lambda_1": 0.5},
objective_value=wert, verified_status=status)])
befunde = check_scenarios_solved(run)
assert bool(befunde) == befund
if befund:
assert "Ausgewogene Skalarisierung" in befunde[0] and "Startlösung" in befunde[0]