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:
parent
dd7206aee5
commit
4bd701c87e
6 changed files with 110 additions and 5 deletions
|
|
@ -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).")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue