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:
Dieter Schlüter 2026-09-18 13:24:46 +02:00
commit 6ecfdf6ae0

View file

@ -345,3 +345,54 @@ def test_english_unit_words_cover_german_answers():
aufgabe = "One ton of raw material X costs 3 thousand euros; at least 4 tons are required."
assert check_answer_units(_run_with_answer("Es sollten 1 Tonne X und 3 Tonnen Y gekauft werden, Kosten 9 Tausend Euro."), aufgabe) == []
assert check_answer_units(_run_with_answer("It takes 12 hours."), "Minimize the cost.") != [] # erfunden bleibt erfunden
# --- Summenzeilen und Zielwert-Zerlegung (check_table, check_objective_decomposition) ---
def _tabelle(rows):
from core.model_schema import ResultTable
return ResultTable(title="Zielwert-Zerlegung", columns=["Bestandteil", "Wert"], rows=rows, objective_column="Wert")
@pytest.mark.parametrize("rows", [
[["Nutzen der eingepackten Gegenstände", 51.0], ["Zielwert", 51.0]], # beobachtet: 15 Rucksack-Läufe
[["Gesamtnutzen der eingepackten Gegenstände", 51]], # einzige Zeile ist keine Summe
[["Nutzen Kocher", 13.0], ["Nutzen Schlafsack", 23.0], ["Nutzen Proviant", 15.0], ["Zielwert", 51.0]],
[["Kocher", 13.0], ["Schlafsack", 23.0], ["Zwischensumme", 36.0], ["Proviant", 15.0], ["Summe", 51.0]],
])
def test_components_named_like_a_sum_are_not_dropped(rows):
from solving.solver_stage import check_table
belegt, abweichung, warnungen = check_table(_tabelle(rows), 51.0)
assert belegt and abweichung is None and warnungen == []
def test_wrong_decomposition_is_still_detected():
from solving.solver_stage import check_table
belegt, abweichung, _ = check_table(_tabelle([["Kocher", 13.0], ["Schlafsack", 23.0], ["Summe", 36.0]]), 51.0)
assert not belegt and "weicht vom Zielwert" in abweichung
def _lauf(tables, status="optimal", objective=51.0):
from core.model_schema import SolverRun, SolverStatus
return SolverRun(status=SolverStatus(status), objective_value=objective, tables=tables)
def test_linear_model_without_decomposition_is_a_finding():
from solving.solver_stage import DECOMPOSITION_MISSING, check_objective_decomposition
befunde = check_objective_decomposition(_lauf([]), make_spec())
assert len(befunde) == 1 and befunde[0].startswith(DECOMPOSITION_MISSING)
def test_decomposition_is_optional_for_non_additive_objectives():
# Volatilität = Wurzel: keine Summe, also keine Pflicht – eine vorhandene Beitragsspalte muss aber stimmen
from solving.solver_stage import check_objective_decomposition
spec = make_spec(problem_class="Second-Order Cone Programming (SOCP)", target_stack="CVXPY")
assert check_objective_decomposition(_lauf([]), spec) == []
falsch = _tabelle([["Rendite", 0.1], ["Risiko", -0.02]])
assert "weicht vom Zielwert" in check_objective_decomposition(_lauf([falsch], objective=0.5), spec)[0]
def test_no_decomposition_needed_without_a_solution():
from solving.solver_stage import check_objective_decomposition
assert check_objective_decomposition(_lauf([], status="infeasible", objective=None), make_spec()) == []
assert check_objective_decomposition(_lauf([_tabelle([["Kosten", 51.0]])]), make_spec()) == []