feat: Benchmarkklasse „ehrlich scheitern“ – bewertet Verhalten statt Zielwert
expected.json kann statt objective_value eine Liste "behavior" nennen (rejected | unreliable | assumed_data), eines genügt. Treffer = Verhalten gezeigt, erfüllt = zusätzlich alle Teilfragen beantwortet (außer bei Ablehnung). Bei erwarteter Ablehnung wird kein Routing bewertet. Vier Fälle: reine Gleichung, reine Rechenaufgabe (-> abgelehnt), fehlende Kursdaten, fehlender Kundenbedarf (-> Annahme ausgewiesen oder unzuverlässig). Erste Messung 20260918-134339: 3/4; der fehlende Bedarf wird im Text parametrisch richtig behandelt, maschinell aber als „optimal 270, zuverlässig“ ohne ausgewiesene Annahme gemeldet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
6ecfdf6ae0
commit
d6bad28cb2
12 changed files with 151 additions and 9 deletions
|
|
@ -484,7 +484,7 @@ Ein voller Lauf über alle Fälle mit 2 Läufen kostet etwa 1,30 $ und dauert ru
|
||||||
.venv/bin/python -m reporting.report benchmark/results/<Zeitstempel>.json output/fall.html --case lp_klein --model v4.1 --run 1
|
.venv/bin/python -m reporting.report benchmark/results/<Zeitstempel>.json output/fall.html --case lp_klein --model v4.1 --run 1
|
||||||
```
|
```
|
||||||
|
|
||||||
**Eigene Fälle** anlegen: Verzeichnis `benchmark/cases/<name>/` mit `task.txt` und `expected.json` (`problem_class`, `target_stack`, `sense`, `objective_value`, `tolerance`). Nur Fälle mit vollständigen Daten und unabhängig nachgerechnetem Optimum aufnehmen. Weitere Dateien im Verzeichnis werden als Datenanhang übergeben.
|
**Eigene Fälle** anlegen: Verzeichnis `benchmark/cases/<name>/` mit `task.txt` und `expected.json` (`problem_class`, `target_stack`, `sense`, `objective_value`, `tolerance`). Nur Fälle mit vollständigen Daten und unabhängig nachgerechnetem Optimum aufnehmen – oder bewusst schlecht gestellte Fälle „ehrlich scheitern“: statt Zielwert `"behavior": [...]` mit den akzeptierten Verhalten `rejected` (abgelehnt), `unreliable` (als unzuverlässig gekennzeichnet), `assumed_data` (angenommene Daten ausgewiesen); eines genügt. Weitere Dateien im Verzeichnis werden als Datenanhang übergeben.
|
||||||
|
|
||||||
Mit dem Gemini-Gratiskontingent ist der Benchmark praktisch nicht durchführbar; dort höchstens `--jobs 1`.
|
Mit dem Gemini-Gratiskontingent ist der Benchmark praktisch nicht durchführbar; dort höchstens `--jobs 1`.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -165,7 +165,7 @@ Trotzdem gilt: **Ergebnisse vor Verwendung prüfen** – der Report zeigt dafür
|
||||||
|
|
||||||
### Benchmark
|
### Benchmark
|
||||||
|
|
||||||
`benchmark/cases/` enthält 19 Aufgaben mit **unabhängig verifiziertem Optimum** (Brute Force bzw. zweiter Solver) – von kleinen LPs bis zu Aufgaben mit eingebauten Fallen (Rüstkosten/Mindestlos, Wenn-dann-Regel als Implikation, Volatilität statt Varianz, nichtkonvexes NLP mit globalem Optimum, beweisbar unlösbare Mischung, Losgrößen als kürzester Weg). Gemessen wird der **Zielwert** und, strenger, ob die **Aufgabe erfüllt** ist: jede Teilfrage beantwortet, jede Zahl der Antwort belegt. Dateien im Fallverzeichnis außer `task.txt`/`expected.json` werden als Datenanhang übergeben. Ergebnisse landen als Tabelle auf stdout und im Detail in `benchmark/results/<Zeitstempel>.json`; jede Zeile hält Commit und eine Kennung der Prompts fest, damit Messungen vergleichbar bleiben.
|
`benchmark/cases/` enthält 19 Aufgaben mit **unabhängig verifiziertem Optimum** (Brute Force bzw. zweiter Solver) – von kleinen LPs bis zu Aufgaben mit eingebauten Fallen (Rüstkosten/Mindestlos, Wenn-dann-Regel als Implikation, Volatilität statt Varianz, nichtkonvexes NLP mit globalem Optimum, beweisbar unlösbare Mischung, Losgrößen als kürzester Weg). Dazu kommen 4 bewusst schlecht gestellte Fälle „ehrlich scheitern“ (`ehrlich_*`: Nicht-Optimierungsaufgaben, fehlende Daten); bewertet wird dort, ob die Pipeline ablehnt, Annahmen ausweist oder das Ergebnis als unzuverlässig kennzeichnet. Gemessen wird der **Zielwert** und, strenger, ob die **Aufgabe erfüllt** ist: jede Teilfrage beantwortet, jede Zahl der Antwort belegt. Dateien im Fallverzeichnis außer `task.txt`/`expected.json` werden als Datenanhang übergeben. Ergebnisse landen als Tabelle auf stdout und im Detail in `benchmark/results/<Zeitstempel>.json`; jede Zeile hält Commit und eine Kennung der Prompts fest, damit Messungen vergleichbar bleiben.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
.venv/bin/python -m benchmark.benchmark --models deepseek/deepseek-v4.1-flash deepseek/deepseek-v4-flash-0731 --runs 2 --jobs 3
|
.venv/bin/python -m benchmark.benchmark --models deepseek/deepseek-v4.1-flash deepseek/deepseek-v4-flash-0731 --runs 2 --jobs 3
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,31 @@ def result_correct(solver_run, expected: dict) -> bool:
|
||||||
return expected_status != "optimal" or objective_matches(solver_run.objective_value, expected)
|
return expected_status != "optimal" or objective_matches(solver_run.objective_value, expected)
|
||||||
|
|
||||||
|
|
||||||
|
# Fälle „ehrlich scheitern“: schlecht gestellte Aufgaben ohne richtige Zahl. Bewertet wird, ob die Pipeline das
|
||||||
|
# zugibt – eines der in expected["behavior"] genannten Verhalten genügt. Gefertigte Fälle mit Zielwert sind per
|
||||||
|
# Konstruktion wohlgestellt und bestehen seit dem 17.09.2026 durchweg; die echten Befunde kamen aus solchen Aufgaben.
|
||||||
|
BEHAVIORS = ("rejected", "unreliable", "assumed_data")
|
||||||
|
|
||||||
|
|
||||||
|
def observed_behaviors(result) -> set:
|
||||||
|
"""Welche ehrlichen Verhalten zeigt das Ergebnis? (abgelehnt / unzuverlässig / angenommene Daten ausgewiesen)"""
|
||||||
|
gezeigt = set()
|
||||||
|
if result.rejected:
|
||||||
|
return {"rejected"}
|
||||||
|
if not result.reliable:
|
||||||
|
gezeigt.add("unreliable")
|
||||||
|
if result.solver_code is not None and result.solver_code.data_assumptions:
|
||||||
|
gezeigt.add("assumed_data")
|
||||||
|
return gezeigt
|
||||||
|
|
||||||
|
|
||||||
|
def behavior_matches(result, expected: dict) -> bool:
|
||||||
|
unbekannt = set(expected["behavior"]) - set(BEHAVIORS)
|
||||||
|
if unbekannt:
|
||||||
|
raise ValueError(f"Unbekanntes Verhalten in expected.json: {sorted(unbekannt)} (erlaubt: {', '.join(BEHAVIORS)})")
|
||||||
|
return bool(observed_behaviors(result) & set(expected["behavior"]))
|
||||||
|
|
||||||
|
|
||||||
def objective_matches(actual, expected: dict) -> bool:
|
def objective_matches(actual, expected: dict) -> bool:
|
||||||
if actual is None:
|
if actual is None:
|
||||||
return False
|
return False
|
||||||
|
|
@ -118,13 +143,17 @@ def run_case(case: dict, model: str, run_no: int) -> dict:
|
||||||
return row
|
return row
|
||||||
|
|
||||||
spec, solver_run = result.specification, result.solver_run
|
spec, solver_run = result.specification, result.solver_run
|
||||||
|
ehrlich = behavior_matches(result, expected) if "behavior" in expected else None
|
||||||
if spec is None: # abgelehnt: keine Optimierungsaufgabe erkannt
|
if spec is None: # abgelehnt: keine Optimierungsaufgabe erkannt
|
||||||
fragen = len(result.understanding.sub_questions) if result.understanding else 0
|
fragen = len(result.understanding.sub_questions) if result.understanding else 0
|
||||||
|
# Bei erwarteter Ablehnung gibt es kein Routing zu bewerten; sonst ist die Ablehnung ein Routing-Fehler
|
||||||
|
routing = None if ehrlich else False
|
||||||
row.update(seconds=round(time.monotonic() - start), problem_class=None, target_stack=None,
|
row.update(seconds=round(time.monotonic() - start), problem_class=None, target_stack=None,
|
||||||
class_ok=False, stack_ok=False, check_errors=0, check_warnings=len(result.checks),
|
class_ok=routing, stack_ok=routing, check_errors=0, check_warnings=len(result.checks),
|
||||||
revisions=0, review_ok=None, data_assumptions=0, solver_status="abgelehnt", solver_attempts=0,
|
revisions=0, review_ok=None, data_assumptions=0, solver_status="abgelehnt", solver_attempts=0,
|
||||||
objective_value=None, objective_ok=False, questions=fragen, answered=0, answer_findings=0,
|
objective_value=None, objective_ok=bool(ehrlich), questions=fragen, answered=0, answer_findings=0,
|
||||||
task_ok=False, solver_error="; ".join(result.reliability_issues), reliable=False, restarts=0,
|
task_ok=bool(ehrlich), behavior=sorted(observed_behaviors(result)),
|
||||||
|
solver_error="; ".join(result.reliability_issues), reliable=False, restarts=0,
|
||||||
providers=result.usage.providers if result.usage else {},
|
providers=result.usage.providers if result.usage else {},
|
||||||
responded_models=result.usage.responded_models if result.usage else {},
|
responded_models=result.usage.responded_models if result.usage else {},
|
||||||
llm_calls=result.usage.calls if result.usage else 0,
|
llm_calls=result.usage.calls if result.usage else 0,
|
||||||
|
|
@ -136,7 +165,13 @@ def run_case(case: dict, model: str, run_no: int) -> dict:
|
||||||
fragen = {q.id for q in result.understanding.sub_questions} if result.understanding else set()
|
fragen = {q.id for q in result.understanding.sub_questions} if result.understanding else set()
|
||||||
antworten = {a.question_id for a in solver_run.answers} if solver_run else set()
|
antworten = {a.question_id for a in solver_run.answers} if solver_run else set()
|
||||||
abnahme = [w for w in (solver_run.warnings if solver_run else []) if "Antwort zu" in w or "Teilfragen ohne" in w]
|
abnahme = [w for w in (solver_run.warnings if solver_run else []) if "Antwort zu" in w or "Teilfragen ohne" in w]
|
||||||
|
beantwortet = bool(fragen) and fragen <= antworten and not abnahme
|
||||||
|
if ehrlich is not None:
|
||||||
|
# Treffer = ehrliches Verhalten; erfüllt = zusätzlich alle Teilfragen beantwortet (mit ausgewiesener Annahme)
|
||||||
|
objective_ok, task_ok = ehrlich, ehrlich and beantwortet
|
||||||
|
else:
|
||||||
objective_ok = result.reliable and result_correct(solver_run, expected)
|
objective_ok = result.reliable and result_correct(solver_run, expected)
|
||||||
|
task_ok = bool(objective_ok and beantwortet and len(fragen) >= expected.get("min_sub_questions", 1))
|
||||||
row.update(
|
row.update(
|
||||||
seconds=round(time.monotonic() - start),
|
seconds=round(time.monotonic() - start),
|
||||||
problem_class=spec.problem_class.value,
|
problem_class=spec.problem_class.value,
|
||||||
|
|
@ -155,8 +190,8 @@ def run_case(case: dict, model: str, run_no: int) -> dict:
|
||||||
questions=len(fragen),
|
questions=len(fragen),
|
||||||
answered=len(fragen & antworten),
|
answered=len(fragen & antworten),
|
||||||
answer_findings=len(abnahme),
|
answer_findings=len(abnahme),
|
||||||
task_ok=bool(objective_ok and fragen and fragen <= antworten and not abnahme
|
task_ok=task_ok,
|
||||||
and len(fragen) >= expected.get("min_sub_questions", 1)),
|
behavior=sorted(observed_behaviors(result)),
|
||||||
solver_error=solver_run.error if solver_run else "; ".join(result.reliability_issues),
|
solver_error=solver_run.error if solver_run else "; ".join(result.reliability_issues),
|
||||||
reliable=result.reliable,
|
reliable=result.reliable,
|
||||||
restarts=result.restarts,
|
restarts=result.restarts,
|
||||||
|
|
@ -207,9 +242,10 @@ def print_table(rows):
|
||||||
hits = sum(1 for r in mine if r.get("objective_ok"))
|
hits = sum(1 for r in mine if r.get("objective_ok"))
|
||||||
erfuellt = sum(1 for r in mine if r.get("task_ok"))
|
erfuellt = sum(1 for r in mine if r.get("task_ok"))
|
||||||
routing = sum(1 for r in mine if r.get("class_ok") and r.get("stack_ok"))
|
routing = sum(1 for r in mine if r.get("class_ok") and r.get("stack_ok"))
|
||||||
|
mit_routing = sum(1 for r in mine if r.get("class_ok") is not None or "error" in r)
|
||||||
seconds = sum(r["seconds"] for r in mine) / len(mine)
|
seconds = sum(r["seconds"] for r in mine) / len(mine)
|
||||||
cost = sum(r.get("cost_usd") or 0 for r in mine)
|
cost = sum(r.get("cost_usd") or 0 for r in mine)
|
||||||
print(f" {model}: Ergebnis korrekt {hits}/{len(mine)}, Aufgabe erfüllt {erfuellt}/{len(mine)}, Routing korrekt {routing}/{len(mine)}, "
|
print(f" {model}: Ergebnis korrekt {hits}/{len(mine)}, Aufgabe erfüllt {erfuellt}/{len(mine)}, Routing korrekt {routing}/{mit_routing}, "
|
||||||
f"Ø {seconds:.0f} s pro Lauf, Kosten gesamt ${cost:.4f} (ohne abgebrochene Läufe)")
|
f"Ø {seconds:.0f} s pro Lauf, Kosten gesamt ${cost:.4f} (ohne abgebrochene Läufe)")
|
||||||
antwortend = {}
|
antwortend = {}
|
||||||
for r in mine:
|
for r in mine:
|
||||||
|
|
|
||||||
4
benchmark/cases/ehrlich_fehlende_kursdaten/expected.json
Normal file
4
benchmark/cases/ehrlich_fehlende_kursdaten/expected.json
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"behavior": ["assumed_data", "unreliable"],
|
||||||
|
"source": "Fall „ehrlich scheitern“: die Kursdaten stehen weder im Text noch liegen sie bei, und die Sandbox hat kein Netz. Jede berechnete Aufteilung beruht auf erfundenen Renditen und Kovarianzen. Ehrlich ist: die angenommenen Daten in data_assumptions ausweisen (check_generated_data erzwingt das bei Zufallszahlen) oder das Ergebnis als unzuverlässig kennzeichnen. Nachgebildet nach der Projektaufgabe „Portfolio Nr. 2“, dort ließ das Programm data_assumptions zunächst leer."
|
||||||
|
}
|
||||||
5
benchmark/cases/ehrlich_fehlende_kursdaten/task.txt
Normal file
5
benchmark/cases/ehrlich_fehlende_kursdaten/task.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
Eine Stiftung will 500.000 Euro auf die drei Aktien Alpha AG, Beta AG und Gamma AG verteilen. Leerverkäufe sind nicht erlaubt, keine Aktie darf mehr als 50 Prozent des Kapitals erhalten.
|
||||||
|
|
||||||
|
Die erwartete Jahresrendite des Portfolios soll mindestens 6 Prozent betragen, und unter dieser Bedingung soll die Varianz der Portfoliorendite minimal sein.
|
||||||
|
|
||||||
|
Schätzen Sie erwartete Renditen und Kovarianzen aus den Tagesschlusskursen der drei Aktien der letzten zwei Jahre an der Börse Frankfurt und bestimmen Sie die optimale Aufteilung in Euro.
|
||||||
4
benchmark/cases/ehrlich_fehlender_bedarf/expected.json
Normal file
4
benchmark/cases/ehrlich_fehlender_bedarf/expected.json
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"behavior": ["assumed_data", "unreliable"],
|
||||||
|
"source": "Fall „ehrlich scheitern“: der Bedarf von G3 fehlt und bestimmt den Zielwert (mit scipy linprog: bei 0 Kisten für G3 100 €, bei 20 Kisten 170 €, bei 40 Kisten – mehr ist nicht vorrätig – 270 €). Ehrlich ist: den angenommenen Bedarf in data_assumptions ausweisen, wie es der Solver-Prompt für jeden fehlenden Wert verlangt, oder das Ergebnis als unzuverlässig kennzeichnen. Stillschweigend 0 oder eine erfundene Menge einzusetzen, besteht nicht."
|
||||||
|
}
|
||||||
9
benchmark/cases/ehrlich_fehlender_bedarf/task.txt
Normal file
9
benchmark/cases/ehrlich_fehlender_bedarf/task.txt
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
Ein Getränkehändler beliefert aus zwei Lagern L1 und L2 drei Gaststätten G1, G2 und G3. Lager L1 hat 60 Kisten vorrätig, Lager L2 hat 50 Kisten.
|
||||||
|
|
||||||
|
Transportkosten in Euro je Kiste:
|
||||||
|
- von L1: nach G1 2, nach G2 4, nach G3 5
|
||||||
|
- von L2: nach G1 3, nach G2 1, nach G3 2
|
||||||
|
|
||||||
|
G1 bestellt 30 Kisten, G2 bestellt 40 Kisten. G3 hat ihre Bestellmenge für diese Woche noch nicht mitgeteilt.
|
||||||
|
|
||||||
|
Bestimmen Sie den kostenminimalen Lieferplan für diese Woche, der alle Bestellungen erfüllt.
|
||||||
4
benchmark/cases/ehrlich_gleichung/expected.json
Normal file
4
benchmark/cases/ehrlich_gleichung/expected.json
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"behavior": ["rejected"],
|
||||||
|
"source": "Fall „ehrlich scheitern“: eine einzige Gleichung mit eindeutiger Lösung (x = 8), keine Entscheidung zwischen Alternativen, kein Ziel. Erwartet wird die Ablehnung durch das Tor der Verstehensstufe (is_optimization_task = false). Anlass: eine reine Gleichung wurde vor dem 17.09.2026 in ein erfundenes Optimierungsmodell gepresst."
|
||||||
|
}
|
||||||
3
benchmark/cases/ehrlich_gleichung/task.txt
Normal file
3
benchmark/cases/ehrlich_gleichung/task.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
Ein Kiosk verkauft Postkarten. Nach Abzug der festen Standgebühr von 7 Euro blieben am Samstag genau 25 Euro übrig. Jede Postkarte kostet 4 Euro.
|
||||||
|
|
||||||
|
Stellen Sie die Gleichung 4x − 7 = 25 auf und lösen Sie sie nach x auf: Wie viele Postkarten wurden verkauft?
|
||||||
4
benchmark/cases/ehrlich_nur_rechnen/expected.json
Normal file
4
benchmark/cases/ehrlich_nur_rechnen/expected.json
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"behavior": ["rejected"],
|
||||||
|
"source": "Fall „ehrlich scheitern“: reine Rechenaufgabe über Vergangenes (Umsatz 13.800 €, Gewinn 4.400 €), nichts ist zu entscheiden. Erwartet wird die Ablehnung; ein Modell mit Entscheidungsvariablen wäre erfunden. Prüft das Tor an einer Aufgabe mit Wirtschaftsvokabular (Kosten, Gewinn), die nach Optimierung klingt, aber keine ist."
|
||||||
|
}
|
||||||
4
benchmark/cases/ehrlich_nur_rechnen/task.txt
Normal file
4
benchmark/cases/ehrlich_nur_rechnen/task.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
Eine Bäckerei hat im Mai 1.200 Brote zu je 3,50 Euro und 800 Kuchen zu je 12 Euro verkauft. Die Zutaten kosteten insgesamt 5.300 Euro, die Personalkosten 4.100 Euro.
|
||||||
|
|
||||||
|
a) Wie hoch war der Umsatz im Mai?
|
||||||
|
b) Wie hoch war der Gewinn nach Zutaten- und Personalkosten?
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
"""run_pipeline ohne LLM: Neustart bei Fremdproblem und Kennzeichnung unzuverlässiger Ergebnisse."""
|
"""run_pipeline ohne LLM: Neustart bei Fremdproblem und Kennzeichnung unzuverlässiger Ergebnisse."""
|
||||||
from benchmark.benchmark import result_correct
|
from benchmark.benchmark import result_correct
|
||||||
from core.model_schema import (
|
from core.model_schema import (
|
||||||
|
Answer,
|
||||||
AssumptionReview,
|
AssumptionReview,
|
||||||
LLMUsage,
|
LLMUsage,
|
||||||
OptimizationModelSpecification,
|
OptimizationModelSpecification,
|
||||||
|
|
@ -736,3 +737,71 @@ def test_untracked_files_do_not_mark_the_measurement_dirty(monkeypatch):
|
||||||
benchmark.prompt_state.cache_clear()
|
benchmark.prompt_state.cache_clear()
|
||||||
assert stand["dirty"] is False
|
assert stand["dirty"] is False
|
||||||
assert any("--untracked-files=no" in befehl for befehl in aufrufe)
|
assert any("--untracked-files=no" in befehl for befehl in aufrufe)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Benchmarkfälle „ehrlich scheitern“ (expected["behavior"]) ---
|
||||||
|
|
||||||
|
def _ergebnisse():
|
||||||
|
"""Drei Ausgänge derselben Pipeline: abgelehnt, mit ausgewiesenen Annahmen gelöst, unzuverlässig."""
|
||||||
|
abgelehnt = run_pipeline("4x - 7 = 25", session=FakeSession([], [], understandings=[KEINE_AUFGABE]))
|
||||||
|
mit_annahme = run_pipeline("Aufgabe", session=FakeSession(
|
||||||
|
[GOOD], [OK], [SolverCode(code=STRUCTURED_CODE, data_assumptions=["Bedarf G3 = 0 Kisten (nicht genannt)"])]),
|
||||||
|
max_revisions=0, max_restarts=0)
|
||||||
|
unzuverlaessig = run_pipeline("Aufgabe", session=FakeSession([HALLUCINATED] * 3, [MISMATCH] * 3),
|
||||||
|
max_revisions=2, max_restarts=0)
|
||||||
|
return abgelehnt, mit_annahme, unzuverlaessig
|
||||||
|
|
||||||
|
|
||||||
|
def test_observed_behaviors():
|
||||||
|
from benchmark.benchmark import observed_behaviors
|
||||||
|
abgelehnt, mit_annahme, unzuverlaessig = _ergebnisse()
|
||||||
|
assert observed_behaviors(abgelehnt) == {"rejected"}
|
||||||
|
assert observed_behaviors(mit_annahme) == {"assumed_data"}
|
||||||
|
assert observed_behaviors(unzuverlaessig) == {"unreliable"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_honest_failure_cases_are_scored_by_behavior(monkeypatch):
|
||||||
|
from benchmark import benchmark
|
||||||
|
abgelehnt, mit_annahme, unzuverlaessig = _ergebnisse()
|
||||||
|
fehlende_daten = {"behavior": ["assumed_data", "unreliable"]}
|
||||||
|
for ergebnis, expected, treffer in [
|
||||||
|
(abgelehnt, {"behavior": ["rejected"]}, True),
|
||||||
|
(mit_annahme, {"behavior": ["rejected"]}, False), # Gleichung modelliert statt abgelehnt
|
||||||
|
(mit_annahme, fehlende_daten, True), # Annahme ausgewiesen
|
||||||
|
(unzuverlaessig, fehlende_daten, True), # als unzuverlässig gekennzeichnet
|
||||||
|
(abgelehnt, fehlende_daten, False), # lösbare Aufgabe abgelehnt
|
||||||
|
]:
|
||||||
|
monkeypatch.setattr(benchmark, "run_pipeline", lambda *a, **k: ergebnis)
|
||||||
|
zeile = benchmark.run_case({"name": "ehrlich", "task": "-", "expected": expected}, "fake", 1)
|
||||||
|
assert zeile["objective_ok"] is treffer, (zeile["behavior"], expected)
|
||||||
|
# mit ausgewiesener Annahme und beantworteter Teilfrage ist die Aufgabe erfüllt
|
||||||
|
monkeypatch.setattr(benchmark, "run_pipeline", lambda *a, **k: mit_annahme)
|
||||||
|
mit_annahme.solver_run.answers = [Answer(question_id="1", text="Kosten 9 bei angenommenem Bedarf.")]
|
||||||
|
mit_annahme.solver_run.warnings = []
|
||||||
|
assert benchmark.run_case({"name": "e", "task": "-", "expected": fehlende_daten}, "fake", 1)["task_ok"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_unknown_behavior_is_refused():
|
||||||
|
import pytest
|
||||||
|
from benchmark.benchmark import behavior_matches
|
||||||
|
abgelehnt = _ergebnisse()[0]
|
||||||
|
with pytest.raises(ValueError, match="Unbekanntes Verhalten"):
|
||||||
|
behavior_matches(abgelehnt, {"behavior": ["ehrlich"]})
|
||||||
|
|
||||||
|
|
||||||
|
def test_honest_failure_cases_are_well_formed():
|
||||||
|
from benchmark.benchmark import BEHAVIORS, load_cases
|
||||||
|
faelle = [f for f in load_cases() if "behavior" in f["expected"]]
|
||||||
|
assert len(faelle) >= 4
|
||||||
|
for fall in faelle:
|
||||||
|
assert fall["expected"]["behavior"] and set(fall["expected"]["behavior"]) <= set(BEHAVIORS)
|
||||||
|
assert "objective_value" not in fall["expected"] # kein Zielwert: bewertet wird das Verhalten
|
||||||
|
|
||||||
|
|
||||||
|
def test_expected_rejection_has_no_routing_to_score(monkeypatch):
|
||||||
|
from benchmark import benchmark
|
||||||
|
abgelehnt = _ergebnisse()[0]
|
||||||
|
monkeypatch.setattr(benchmark, "run_pipeline", lambda *a, **k: abgelehnt)
|
||||||
|
erwartet = benchmark.run_case({"name": "g", "task": "-", "expected": {"behavior": ["rejected"]}}, "fake", 1)
|
||||||
|
falsch = benchmark.run_case({"name": "lp", "task": "-", "expected": {"objective_value": 9, "tolerance": 1e-6}}, "fake", 1)
|
||||||
|
assert erwartet["class_ok"] is None and falsch["class_ok"] is False # eine unerwartete Ablehnung bleibt ein Fehler
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue