feat: Zielwert-Zerlegung als Reparaturgrund, SOLVER_TIMEOUT, Summenzeilen-Fehlalarm behoben
- check_objective_decomposition: bei LP/MILP/CP muss eine objective_column den Zielwert
belegen, eine abweichende Beitragsspalte ist überall ein Befund -> Solver-Reparatur,
bleibt es: Warnung. 90 von 90 linearen Läufen seit 17.09. hätten bestanden.
- Summenzeilen: „Gesamt…“/„Nutzen…“ nur noch in der letzten Zeile, einzelne Zeile nie,
Zwischensummen nicht gegen die Gesamtsumme. Vorher 15 Rucksack-Läufe mit falschem
„Summe (0) weicht vom Zielwert (51) ab“; von 17 Abweichungen bleibt 1 echte.
- SOLVER_TIMEOUT (Default 60) für alle Programmläufe und als {ZEITLIMIT} im Solver-Prompt
- Benchmark: dirty zählt nur versionierte Dateien; SOLVER_TIMEOUT in der Konfiguration
Ändert die Prompt-Kennung (Solver-Prompt.txt).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
8dd5073cd1
commit
6ecfdf6ae0
2 changed files with 244 additions and 60 deletions
|
|
@ -78,7 +78,7 @@ Ergebnisausgabe:
|
|||
- 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"}
|
||||
- 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 {ZEITLIMIT} s), sonst leere Liste.
|
||||
- relaxation_bound: bei ganzzahligen/binären Variablen der Zielwert der stetigen Relaxation (alle Ganzzahligkeitsforderungen aufgehoben), sonst null. Zeigt, was die Ganzzahligkeit kostet.
|
||||
- answers: Die Rückprojektion auf die gestellte Aufgabe – genau ein Eintrag je Teilfrage aus der Liste im Auftrag, mit deren question_id. Das gilt auch dann, wenn es nur EINE Teilfrage gibt: answers bleibt nie leer.
|
||||
[{"question_id": "a", "text": "…"}, {"question_id": "b", "text": "…"}]
|
||||
|
|
@ -94,7 +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.
|
||||
- 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.
|
||||
- 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]
|
||||
- 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 {ZEITLIMIT} s bleiben. [→ check_scenarios_solved]
|
||||
- graph: NUR bei Graphenproblemen (Stack NetworkX), sonst null. Grundlage für die Zeichnung im Report:
|
||||
{"nodes": ["0", "1", "2", "3"],
|
||||
"arcs": [{"source": "0", "target": "1", "weight": 8}, ...],
|
||||
|
|
@ -120,7 +120,7 @@ Ergebnisausgabe:
|
|||
- Entscheidungswerte als formatierte Texte, Beiträge als Zahlen; Dezimalkomma im Text ist erlaubt.
|
||||
- unit: Einheit der objective_column für die Diagrammbeschriftung (z. B. " €", " %"), sonst weglassen.
|
||||
- note: ein Satz, wie die Tabelle zu lesen ist.
|
||||
Erzeuge nach Möglichkeit diese drei Tabellen (in dieser Reihenfolge):
|
||||
Erzeuge nach Möglichkeit diese drei Tabellen (in dieser Reihenfolge). Bei LP, MILP und CP ist die Zielwert-Zerlegung Pflicht (die Zielfunktion ist dort immer eine Summe), und jede objective_column muss sich zum Zielwert summieren – sonst wird das Programm zur Reparatur zurückgegeben. [→ check_objective_decomposition]
|
||||
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.
|
||||
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]
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from core.llm import LLMSession
|
|||
from checks.model_checks import numbers_in, unexplained_jargon
|
||||
from core.model_schema import (
|
||||
Answer,
|
||||
ProblemClass,
|
||||
TargetStack,
|
||||
Scenario,
|
||||
ConstraintValue,
|
||||
|
|
@ -40,6 +41,9 @@ RESULT_MARKER = "RESULT_JSON:"
|
|||
# Isolierung des erzeugten Codes: "auto" nutzt bubblewrap, wenn vorhanden; "bwrap" verlangt es
|
||||
# (Fehler, wenn es fehlt); "aus" führt ungeschützt aus (nur für Systeme ohne bubblewrap).
|
||||
SANDBOX = os.environ.get("SOLVER_SANDBOX", "auto").strip().lower()
|
||||
# Zeitlimit je Programmlauf in Sekunden. Geht auch als Zeitbudget in den Solver-Prompt ({ZEITLIMIT}): ein höheres
|
||||
# Limit nützt nur, wenn das Programm seine internen Solver-Zeitlimits danach wählt (SCIP-Szenarien, Portfolio Nr. 2).
|
||||
SOLVER_TIMEOUT = int(os.environ.get("SOLVER_TIMEOUT", "60"))
|
||||
|
||||
ALLOWED_MODULES = {"json", "math", "itertools", "functools", "collections", "numpy", "scipy", "highspy", "ortools", "cvxpy", "pyscipopt", "cyipopt", "networkx", "pandas"}
|
||||
FORBIDDEN_CALLS = {"__import__", "eval", "exec", "compile", "open", "input", "breakpoint"}
|
||||
|
|
@ -260,13 +264,21 @@ def _tail(text: str, limit: int = 2000) -> str:
|
|||
|
||||
|
||||
SUM_ROW_LABELS = ("summe", "gesamt", "total", "σ", "sum", "zielwert", "ergebnis", "nutzen", "insgesamt")
|
||||
# Vor der letzten Zeile zählt nur eine eindeutige Beschriftung: „Nutzen der eingepackten Gegenstände“ und
|
||||
# „Gesamtnutzen …“ sind dort Bestandteile. Beobachtet: 15 Rucksack-Läufe meldeten „Summe (0) weicht vom
|
||||
# Zielwert (51) ab“, weil der einzige Bestandteil als Summenzeile galt und aus der Summe fiel.
|
||||
_INNER_SUM_ROW = re.compile(r"(zwischen)?summe\b.*|(gesamt|total|insgesamt|zielwert|σ|sum)\s*:?\s*", re.IGNORECASE)
|
||||
|
||||
|
||||
def _is_sum_row(row, value=None, others=None) -> bool:
|
||||
"""Summenzeile: am Text erkennbar oder (letzte Zeile) am Wert = Summe der übrigen Zeilen."""
|
||||
if row and isinstance(row[0], str) and row[0].strip().lower().startswith(SUM_ROW_LABELS):
|
||||
def _is_sum_row(row, value=None, others=None, last: bool = True) -> bool:
|
||||
"""Summenzeile: am Text erkennbar oder (letzte Zeile) am Wert = Summe der übrigen Zeilen.
|
||||
|
||||
Wortanfänge wie „Gesamt…“ oder „Nutzen…“ zählen nur in der letzten Zeile, davor nur eindeutige Beschriftungen.
|
||||
"""
|
||||
label = row[0].strip().lower() if row and isinstance(row[0], str) else ""
|
||||
if label and (label.startswith(SUM_ROW_LABELS) if last else _INNER_SUM_ROW.fullmatch(label)):
|
||||
return True
|
||||
if value is None or not others:
|
||||
if not last or value is None or not others:
|
||||
return False
|
||||
return _close(value, sum(others))
|
||||
|
||||
|
|
@ -286,6 +298,60 @@ def _close(a: float, b: float) -> bool:
|
|||
return abs(a - b) <= 1e-3 * max(abs(a), abs(b)) + 1e-6
|
||||
|
||||
|
||||
def _is_number(value) -> bool:
|
||||
return isinstance(value, (int, float)) and not isinstance(value, bool)
|
||||
|
||||
|
||||
def check_table(table: ResultTable, objective_value) -> tuple:
|
||||
"""Eine Tabelle gegen den Zielwert prüfen: (belegt, abweichung, Warnungen).
|
||||
|
||||
`belegt` heißt: die Beitragsspalte summiert sich zum gemeldeten Zielwert – der Zielwert ist damit aus
|
||||
Bestandteilen nachgerechnet und nicht bloß behauptet. `abweichung` ist die Meldung, wenn sie es nicht tut.
|
||||
"""
|
||||
if not table.objective_column:
|
||||
return False, None, []
|
||||
if table.objective_column not in table.columns:
|
||||
return False, None, [f"Tabelle '{table.title}': Spalte '{table.objective_column}' existiert nicht."]
|
||||
col = table.columns.index(table.objective_column)
|
||||
cells = [(row, row[col] if col < len(row) else None) for row in table.rows]
|
||||
numeric = [v for _, v in cells if _is_number(v)]
|
||||
letzte = len(cells) - 1
|
||||
|
||||
def summenzeile(n, row, v):
|
||||
# letzte Zeile kann auch ohne Schlüsselwort die Summe sein (z. B. Beschriftung "Zielwert");
|
||||
# eine einzige Zeile ist nie eine Summe („Gesamtnutzen …“ als einziger Bestandteil)
|
||||
if letzte == 0:
|
||||
return False
|
||||
return _is_sum_row(row, v if n == letzte else None, numeric[:-1] if n == letzte else None, last=n == letzte)
|
||||
|
||||
values = [v for n, (row, v) in enumerate(cells) if not summenzeile(n, row, v)]
|
||||
if not all(_is_number(v) for v in values):
|
||||
return False, None, [f"Tabelle '{table.title}': Spalte '{table.objective_column}' enthält nicht nur "
|
||||
f"Zahlen – Summe nicht prüfbar."]
|
||||
total = sum(values)
|
||||
if _is_comparison(values, len(values) == len(cells), total, objective_value):
|
||||
return False, None, []
|
||||
warnings, abweichung = [], None
|
||||
belegt = objective_value is not None and bool(values) and _close(total, objective_value)
|
||||
if objective_value is not None and not _close(total, objective_value):
|
||||
abweichung = (f"Tabelle '{table.title}': Summe der Spalte '{table.objective_column}' ({total:.6g}) "
|
||||
f"weicht vom Zielwert ({objective_value:.6g}) ab – Tabelle bitte nicht verwenden.")
|
||||
warnings.append(abweichung)
|
||||
if len(values) > 1 and max(abs(v) for v in values) > 0:
|
||||
largest = max(abs(v) for v in values)
|
||||
tiny = [row[0] for row, v in cells if _is_number(v) and v in values and 0 < abs(v) < largest * 1e-4]
|
||||
if tiny:
|
||||
warnings.append(
|
||||
f"Tabelle '{table.title}': {', '.join(str(t) for t in tiny)} ist um Größenordnungen kleiner "
|
||||
f"als die übrigen Bestandteile – vermutlich uneinheitliche Einheiten (z. B. relativ statt absolut)."
|
||||
)
|
||||
# nur die Schlusszeile: eine Zwischensumme weicht von der Gesamtsumme naturgemäß ab
|
||||
if cells and _is_number(cells[-1][1]) and summenzeile(letzte, *cells[-1]) and not _close(cells[-1][1], total):
|
||||
warnings.append(f"Tabelle '{table.title}': Summenzeile ({cells[-1][1]:.6g}) passt nicht zu den Zeilen "
|
||||
f"({total:.6g}).")
|
||||
return belegt, abweichung, warnings
|
||||
|
||||
|
||||
def parse_result_tables(raw_tables: list, objective_value) -> tuple:
|
||||
"""Ergebnistabellen validieren und deterministisch gegen den Zielwert prüfen.
|
||||
|
||||
|
|
@ -299,47 +365,37 @@ def parse_result_tables(raw_tables: list, objective_value) -> tuple:
|
|||
warnings.append(f"Ergebnistabelle ungültig und verworfen: {str(exc)[:200]}")
|
||||
continue
|
||||
tables.append(table)
|
||||
if not table.objective_column:
|
||||
continue
|
||||
if table.objective_column not in table.columns:
|
||||
warnings.append(f"Tabelle '{table.title}': Spalte '{table.objective_column}' existiert nicht.")
|
||||
continue
|
||||
col = table.columns.index(table.objective_column)
|
||||
cells = [(row, row[col] if col < len(row) else None) for row in table.rows]
|
||||
numeric = [v for _, v in cells if isinstance(v, (int, float)) and not isinstance(v, bool)]
|
||||
# letzte Zeile kann auch ohne Schlüsselwort die Summe sein (z. B. Beschriftung "Zielwert")
|
||||
values = [v for n, (row, v) in enumerate(cells)
|
||||
if not _is_sum_row(row, v if n == len(cells) - 1 else None, numeric[:-1] if n == len(cells) - 1 else None)]
|
||||
if not all(isinstance(v, (int, float)) and not isinstance(v, bool) for v in values):
|
||||
warnings.append(f"Tabelle '{table.title}': Spalte '{table.objective_column}' enthält nicht nur Zahlen – Summe nicht prüfbar.")
|
||||
continue
|
||||
total = sum(values)
|
||||
if _is_comparison(values, len(values) == len(cells), total, objective_value):
|
||||
continue
|
||||
if objective_value is not None and not _close(total, objective_value):
|
||||
warnings.append(
|
||||
f"Tabelle '{table.title}': Summe der Spalte '{table.objective_column}' ({total:.6g}) "
|
||||
f"weicht vom Zielwert ({objective_value:.6g}) ab – Tabelle bitte nicht verwenden."
|
||||
)
|
||||
if len(values) > 1 and max(abs(v) for v in values) > 0:
|
||||
largest = max(abs(v) for v in values)
|
||||
tiny = [row[0] for row, v in cells
|
||||
if isinstance(v, (int, float)) and not isinstance(v, bool) and v in values
|
||||
and 0 < abs(v) < largest * 1e-4]
|
||||
if tiny:
|
||||
warnings.append(
|
||||
f"Tabelle '{table.title}': {', '.join(str(t) for t in tiny)} ist um Größenordnungen kleiner "
|
||||
f"als die übrigen Bestandteile – vermutlich uneinheitliche Einheiten (z. B. relativ statt absolut)."
|
||||
)
|
||||
for n, (row, value) in enumerate(cells):
|
||||
if not isinstance(value, (int, float)) or isinstance(value, bool):
|
||||
continue
|
||||
if _is_sum_row(row, value if n == len(cells) - 1 else None, numeric[:-1] if n == len(cells) - 1 else None) \
|
||||
and not _close(value, total):
|
||||
warnings.append(f"Tabelle '{table.title}': Summenzeile ({value:.6g}) passt nicht zu den Zeilen ({total:.6g}).")
|
||||
warnings += check_table(table, objective_value)[2]
|
||||
return tables, warnings
|
||||
|
||||
|
||||
# Lineare Zielfunktionen sind immer eine Summe von Bestandteilen – dort ist die Zerlegung Pflicht. Bei
|
||||
# QP/SOCP/NLP nicht (Volatilität = Wurzel ist nicht additiv); eine vorhandene Beitragsspalte muss aber stimmen.
|
||||
DECOMPOSITION_REQUIRED = {ProblemClass.LP, ProblemClass.MILP, ProblemClass.CP}
|
||||
DECOMPOSITION_MISSING = "Zielwert nicht aus Bestandteilen belegt"
|
||||
|
||||
|
||||
def check_objective_decomposition(run, spec: OptimizationModelSpecification) -> list:
|
||||
"""Ist der gemeldete Zielwert aus Bestandteilen nachgerechnet – oder nur behauptet?
|
||||
|
||||
`check_feasibility` prüft die Restriktionen, nicht den Zielwert. Den belegt erst eine Beitragsspalte,
|
||||
deren Summe ihn ergibt. Gemessen an 90 Läufen linearer Klassen seit dem 17.09.2026: jede hatte eine –
|
||||
die Pflicht kostet also kaum Reparaturen, fängt aber Rückfälle. Bleibt der Befund nach den Reparaturen,
|
||||
ist er eine Warnung: die Lösung selbst ist geprüft zulässig.
|
||||
"""
|
||||
if run.status not in (SolverStatus.OPTIMAL, SolverStatus.FEASIBLE) or run.objective_value is None:
|
||||
return []
|
||||
geprueft = [check_table(t, run.objective_value) for t in run.tables]
|
||||
befunde = [abweichung for _, abweichung, _ in geprueft if abweichung]
|
||||
if spec.problem_class in DECOMPOSITION_REQUIRED and not any(belegt for belegt, _, _ in geprueft):
|
||||
befunde.append(
|
||||
f"{DECOMPOSITION_MISSING}: keine Ergebnistabelle hat eine objective_column, deren Summe den Zielwert "
|
||||
f"{run.objective_value:.6g} ergibt. Die Zielfunktion ist linear, also eine Summe – melde die Tabelle "
|
||||
f"„Zielwert-Zerlegung“ mit einer Zeile je Bestandteil, objective_column \"Wert\" und der Summenzeile "
|
||||
f"„Zielwert“.")
|
||||
return befunde
|
||||
|
||||
|
||||
def _parse_list(raw, model_type) -> list:
|
||||
"""Fehlerhafte Einträge verwerfen statt den Lauf scheitern zu lassen (die Lösung ist davon unberührt)."""
|
||||
parsed = []
|
||||
|
|
@ -512,7 +568,7 @@ def solver_env(tmp: str) -> dict:
|
|||
return {**env, "HOME": tmp, "TMPDIR": tmp, "PYTHONDONTWRITEBYTECODE": "1"}
|
||||
|
||||
|
||||
def run_solver_code(code: str, timeout: int = 60, data: dict | None = None,
|
||||
def run_solver_code(code: str, timeout: int = SOLVER_TIMEOUT, data: dict | None = None,
|
||||
data_files=()) -> SolverRun:
|
||||
try:
|
||||
validate_imports(code)
|
||||
|
|
@ -576,6 +632,11 @@ def run_solver_code(code: str, timeout: int = 60, data: dict | None = None,
|
|||
return SolverRun(status=SolverStatus.ERROR, error=f"Ungültiges Ergebnis von report(): {exc}: {result_lines[0][:500]}")
|
||||
|
||||
|
||||
def solver_prompt() -> str:
|
||||
"""System-Prompt der Solver-Stufe mit dem Zeitbudget dieses Laufs (SOLVER_TIMEOUT)."""
|
||||
return SOLVER_PROMPT_FILE.read_text(encoding="utf-8").replace("{ZEITLIMIT}", str(SOLVER_TIMEOUT))
|
||||
|
||||
|
||||
def generate_solver_code(
|
||||
session: LLMSession, task: str, spec: OptimizationModelSpecification,
|
||||
previous: Optional[SolverCode] = None, previous_error: Optional[str] = None, understanding=None,
|
||||
|
|
@ -597,7 +658,7 @@ def generate_solver_code(
|
|||
return session.create(
|
||||
SolverCode,
|
||||
[
|
||||
{"role": "system", "content": SOLVER_PROMPT_FILE.read_text(encoding="utf-8")},
|
||||
{"role": "system", "content": solver_prompt()},
|
||||
{"role": "user", "content": content},
|
||||
],
|
||||
step="Solver-Reparatur" if previous is not None else "Solver-Code",
|
||||
|
|
@ -746,7 +807,7 @@ def check_text_quality(run, summary: str = "") -> list:
|
|||
MAX_SCENARIOS = 5 # jedes Szenario kostet einen weiteren Programmlauf (kein LLM-Aufruf)
|
||||
|
||||
|
||||
def verify_scenarios(run, code: str, timeout: int = 60, data_files=()) -> list:
|
||||
def verify_scenarios(run, code: str, timeout: int = SOLVER_TIMEOUT, data_files=()) -> list:
|
||||
"""Jedes Szenario mit übersteuerten Daten NEU rechnen und den gemeldeten Zielwert bestätigen.
|
||||
|
||||
Ein Szenario ist eine Datenvariante desselben Programms – dieselbe Maschinerie wie `--resolve`.
|
||||
|
|
@ -797,7 +858,7 @@ def check_scenarios_solved(run) -> list:
|
|||
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.")
|
||||
f"zusammen unter {SOLVER_TIMEOUT} s bleiben.")
|
||||
return befunde
|
||||
|
||||
|
||||
|
|
@ -1033,7 +1094,7 @@ def _check_and_run(code: str, timeout: int, spec: OptimizationModelSpecification
|
|||
|
||||
|
||||
def solve(session: LLMSession, task: str, spec: OptimizationModelSpecification,
|
||||
retries: int = 2, timeout: int = 60, log=lambda msg: None, understanding=None,
|
||||
retries: int = 2, timeout: int = SOLVER_TIMEOUT, log=lambda msg: None, understanding=None,
|
||||
data_files=(), data_prompt: str = "") -> tuple:
|
||||
"""Code erzeugen und ausführen; bei Fehlern bis zu `retries` Reparaturversuche.
|
||||
|
||||
|
|
@ -1047,16 +1108,18 @@ def solve(session: LLMSession, task: str, spec: OptimizationModelSpecification,
|
|||
run = _check_and_run(solver_code.code, timeout, spec, understanding, task, data_files, data_prompt)
|
||||
run.undeclared_data = check_generated_data(solver_code.code, solver_code, spec)
|
||||
einheiten = check_answer_units(run, task, data_prompt)
|
||||
zerlegung = check_objective_decomposition(run, spec)
|
||||
attempt = 1
|
||||
while (run.status == SolverStatus.ERROR or run.feasibility or run.undeclared_data or run.unsolved_scenarios
|
||||
or einheiten) and attempt <= retries:
|
||||
or einheiten or zerlegung) and attempt <= retries:
|
||||
# Alle Befundarten zugleich melden – sonst kostet jede weitere Art einen eigenen Reparaturversuch
|
||||
fehler = run.error or "\n\n".join(
|
||||
titel + ":\n- " + "\n- ".join(befunde)
|
||||
for titel, befunde in (("Die Lösung hält das Modell nicht ein", run.feasibility),
|
||||
("Eingabedaten nicht ausgewiesen", run.undeclared_data),
|
||||
("Szenario ohne Ergebnis", run.unsolved_scenarios),
|
||||
("Einheit in der Antwort erfunden", einheiten))
|
||||
("Einheit in der Antwort erfunden", einheiten),
|
||||
("Zielwert nicht nachvollziehbar", zerlegung))
|
||||
if befunde)
|
||||
log(f"Solver-Code fehlgeschlagen – Reparaturversuch {attempt}/{retries}: {fehler[:200]}")
|
||||
solver_code = generate_solver_code(session, task, spec, previous=solver_code, previous_error=fehler,
|
||||
|
|
@ -1065,8 +1128,10 @@ def solve(session: LLMSession, task: str, spec: OptimizationModelSpecification,
|
|||
run = _check_and_run(solver_code.code, timeout, spec, understanding, task, data_files, data_prompt)
|
||||
run.undeclared_data = check_generated_data(solver_code.code, solver_code, spec)
|
||||
einheiten = check_answer_units(run, task, data_prompt)
|
||||
zerlegung = check_objective_decomposition(run, spec)
|
||||
attempt += 1
|
||||
# Bleibt die erfundene Einheit, ist das Ergebnis trotzdem richtig – Warnung, nicht unzuverlässig
|
||||
run.warnings += einheiten
|
||||
# Bleiben erfundene Einheit oder fehlende Zerlegung, ist die Lösung trotzdem geprüft zulässig – Warnung, nicht
|
||||
# unzuverlässig. Eine abweichende Summe steht schon in den Warnungen (parse_result_tables).
|
||||
run.warnings += einheiten + [z for z in zerlegung if z.startswith(DECOMPOSITION_MISSING)]
|
||||
run.attempts = attempt
|
||||
return solver_code, run
|
||||
|
|
|
|||
Loading…
Reference in a new issue