Compare commits
16 changed files with 21 additions and 310 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 – 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.
|
**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.
|
||||||
|
|
||||||
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). 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.
|
`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.
|
||||||
|
|
||||||
```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,31 +94,6 @@ 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
|
||||||
|
|
@ -143,17 +118,13 @@ 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=routing, stack_ok=routing, check_errors=0, check_warnings=len(result.checks),
|
class_ok=False, stack_ok=False, 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=bool(ehrlich), questions=fragen, answered=0, answer_findings=0,
|
objective_value=None, objective_ok=False, questions=fragen, answered=0, answer_findings=0,
|
||||||
task_ok=bool(ehrlich), behavior=sorted(observed_behaviors(result)),
|
task_ok=False, solver_error="; ".join(result.reliability_issues), reliable=False, restarts=0,
|
||||||
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,
|
||||||
|
|
@ -165,13 +136,7 @@ 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,
|
||||||
|
|
@ -190,8 +155,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=task_ok,
|
task_ok=bool(objective_ok and fragen and fragen <= antworten and not abnahme
|
||||||
behavior=sorted(observed_behaviors(result)),
|
and len(fragen) >= expected.get("min_sub_questions", 1)),
|
||||||
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,
|
||||||
|
|
@ -242,10 +207,9 @@ 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}/{mit_routing}, "
|
print(f" {model}: Ergebnis korrekt {hits}/{len(mine)}, Aufgabe erfüllt {erfuellt}/{len(mine)}, Routing korrekt {routing}/{len(mine)}, "
|
||||||
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:
|
||||||
|
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"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."
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
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.
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"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."
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
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.
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"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."
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
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?
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"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."
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
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?
|
|
||||||
|
|
@ -10,9 +10,8 @@ GEMEINSAMER RAHMEN (gilt für alle vier Stufen dieser Pipeline: Verstehen, Model
|
||||||
Regeln für das Modell:
|
Regeln für das Modell:
|
||||||
- Implementiere Variablen, Zielfunktion und Restriktionen exakt wie in der Spezifikation. Lasse keine Restriktion weg, lockere keine und füge keine hinzu.
|
- Implementiere Variablen, Zielfunktion und Restriktionen exakt wie in der Spezifikation. Lasse keine Restriktion weg, lockere keine und füge keine hinzu.
|
||||||
- Ist das Modell unlösbar oder unbeschränkt, melde genau das (Status infeasible bzw. unbounded). Verändere das Modell nicht, um eine Lösung zu erzwingen.
|
- Ist das Modell unlösbar oder unbeschränkt, melde genau das (Status infeasible bzw. unbounded). Verändere das Modell nicht, um eine Lösung zu erzwingen.
|
||||||
- Daten: Verwende die Werte aus extracted_values bzw. dem Aufgabentext. Fehlen Werte, wähle kleine, plausible Beispieldaten und führe JEDEN angenommenen Wert in data_assumptions auf (Symbol, Wert, Begründung). Stehen alle Werte im Text, bleibt data_assumptions leer. Nennt die Spezifikation einen Wert als fehlend („nicht genannt“, „unbekannt“), gilt das auch dann, wenn du ihn nur beispielhaft einsetzt oder einen konstanten Term weglässt. [→ check_generated_data]
|
- Daten: Verwende die Werte aus extracted_values bzw. dem Aufgabentext. Fehlen Werte, wähle kleine, plausible Beispieldaten und führe JEDEN angenommenen Wert in data_assumptions auf (Symbol, Wert, Begründung). Stehen alle Werte im Text, bleibt data_assumptions leer.
|
||||||
- Erzeugst du Eingabedaten selbst – Zufallszahlen, ein synthetischer Markt, ein verkleinertes Universum, weil die echten Daten nicht erreichbar sind –, gehört das IMMER nach data_assumptions (Feld deiner Antwort neben code, kein Argument von report()), nicht nur in eine Tabelle oder einen Antwortsatz. Sonst ist dem Ergebnis maschinell nicht anzusehen, dass es nicht auf den Daten der Aufgabe beruht. [→ check_generated_data]
|
- Erzeugst du Eingabedaten selbst – Zufallszahlen, ein synthetischer Markt, ein verkleinertes Universum, weil die echten Daten nicht erreichbar sind –, gehört das IMMER nach data_assumptions (Feld deiner Antwort neben code, kein Argument von report()), nicht nur in eine Tabelle oder einen Antwortsatz. Sonst ist dem Ergebnis maschinell nicht anzusehen, dass es nicht auf den Daten der Aufgabe beruht. [→ check_generated_data]
|
||||||
Die Antwort mit dem Ergebnis sagt dann ausdrücklich, dass ihre Zahlen auf angenommenen bzw. simulierten Daten beruhen, und stellt sie nie so dar, als stammten sie aus den Daten der Aufgabe. [→ check_answers_disclose_assumptions]
|
|
||||||
- Die offenen Annahmen (open_assumptions) der Spezifikation gelten und sind umzusetzen.
|
- Die offenen Annahmen (open_assumptions) der Spezifikation gelten und sind umzusetzen.
|
||||||
|
|
||||||
Erlaubte Imports (andere sind verboten und führen zum Abbruch): json, math, itertools, functools, collections, numpy, scipy, highspy, ortools, cvxpy, pyscipopt, cyipopt, networkx, pandas.
|
Erlaubte Imports (andere sind verboten und führen zum Abbruch): json, math, itertools, functools, collections, numpy, scipy, highspy, ortools, cvxpy, pyscipopt, cyipopt, networkx, pandas.
|
||||||
|
|
|
||||||
|
|
@ -325,10 +325,6 @@ def check_table(table: ResultTable, objective_value) -> tuple:
|
||||||
return _is_sum_row(row, v if n == letzte else None, numeric[:-1] if n == letzte else None, last=n == letzte)
|
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)]
|
values = [v for n, (row, v) in enumerate(cells) if not summenzeile(n, row, v)]
|
||||||
if not values and letzte > 0:
|
|
||||||
# Alle Zeilen als Summe erkannt ist ein Prüffehler, keine Abweichung („Summe der Varianzbeiträge“ als
|
|
||||||
# Bestandteil vor der Schlusszeile „Summe“) – dann ist nur die Schlusszeile die Summe.
|
|
||||||
values = [v for n, (_, v) in enumerate(cells) if n != letzte]
|
|
||||||
if not all(_is_number(v) for v in values):
|
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 "
|
return False, None, [f"Tabelle '{table.title}': Spalte '{table.objective_column}' enthält nicht nur "
|
||||||
f"Zahlen – Summe nicht prüfbar."]
|
f"Zahlen – Summe nicht prüfbar."]
|
||||||
|
|
@ -891,35 +887,7 @@ def _is_traceable(wert: str, belegt: set) -> bool:
|
||||||
RANDOM_SOURCE = re.compile(r"\bnp\.random\b|\bnumpy\.random\b|\brandom\.\w|default_rng|\bseed\b", re.IGNORECASE)
|
RANDOM_SOURCE = re.compile(r"\bnp\.random\b|\bnumpy\.random\b|\brandom\.\w|default_rng|\bseed\b", re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
# „Wert fehlt“ in extracted_values: die Spezifikation sagt selbst, dass der Text ihn nicht hergibt
|
def check_generated_data(code: str, solver_code, spec=None) -> list:
|
||||||
MISSING_VALUE = re.compile(
|
|
||||||
r"nicht (?:explizit |numerisch |konkret |ausdrücklich )?(?:genannt|angegeben|gegeben|bekannt|mitgeteilt|beziffert|"
|
|
||||||
r"vorgegeben|enthalten|spezifiziert)|\bfehl(?:t|en|ender?)\b|unbekannt|offene[rn]? Parameter|keine Angabe|"
|
|
||||||
r"keine Werte|nicht im (?:Aufgaben)?[Tt]ext", re.IGNORECASE)
|
|
||||||
|
|
||||||
|
|
||||||
def missing_values(spec, data_files=()) -> list:
|
|
||||||
"""(Symbol, extracted_values) der Parameter, deren Wert laut Spezifikation im Text fehlt.
|
|
||||||
|
|
||||||
Ausgenommen sind Werte aus beigelegten Dateien – die fehlen im Text, aber nicht in der Aufgabe.
|
|
||||||
"""
|
|
||||||
if spec is None:
|
|
||||||
return []
|
|
||||||
dateien = [Path(d).name.lower() for d in data_files or ()]
|
|
||||||
return [(p.symbol, p.extracted_values) for p in spec.parameters
|
|
||||||
if MISSING_VALUE.search(p.extracted_values or "")
|
|
||||||
and not any(d in (p.extracted_values or "").lower() for d in dateien)]
|
|
||||||
|
|
||||||
|
|
||||||
def _generates_data(code: str, spec=None) -> bool:
|
|
||||||
"""Zufallszahlen im Programm, die Eingabedaten sind – nicht Ipopt-Startpunkte bei vollständigen Zahlen."""
|
|
||||||
if not RANDOM_SOURCE.search(code or ""):
|
|
||||||
return False
|
|
||||||
return not (spec is not None and spec.target_stack == TargetStack.IPOPT
|
|
||||||
and all(numbers_in(p.extracted_values or "") for p in spec.parameters))
|
|
||||||
|
|
||||||
|
|
||||||
def check_generated_data(code: str, solver_code, spec=None, data_files=()) -> list:
|
|
||||||
"""Erzeugt das Programm seine Eingabedaten selbst, ohne sie auszuweisen?
|
"""Erzeugt das Programm seine Eingabedaten selbst, ohne sie auszuweisen?
|
||||||
|
|
||||||
Beobachtet an der Projektaufgabe „Portfolio Nr. 2", die Kursdaten aus dem Netz verlangt: Das
|
Beobachtet an der Projektaufgabe „Portfolio Nr. 2", die Kursdaten aus dem Netz verlangt: Das
|
||||||
|
|
@ -936,51 +904,18 @@ def check_generated_data(code: str, solver_code, spec=None, data_files=()) -> li
|
||||||
Zufallsstartwert für die Startpunkte. „Werte vorhanden" lässt sich nicht an einem nicht-leeren
|
Zufallsstartwert für die Startpunkte. „Werte vorhanden" lässt sich nicht an einem nicht-leeren
|
||||||
`extracted_values` festmachen – beobachtet: „aus Kursdaten berechnet (Teil 1); im Text nicht
|
`extracted_values` festmachen – beobachtet: „aus Kursdaten berechnet (Teil 1); im Text nicht
|
||||||
numerisch vorgegeben" –, deshalb gilt der Stack als Unterscheidung, nicht die Datenlage allein.
|
numerisch vorgegeben" –, deshalb gilt der Stack als Unterscheidung, nicht die Datenlage allein.
|
||||||
|
|
||||||
Zweiter Weg zu nicht ausgewiesenen Daten: die Spezifikation sagt selbst, dass ein Wert fehlt („nicht
|
|
||||||
genannt“, „unbekannt“, „offener Parameter“), das Programm setzt trotzdem einen ein. Beobachtet an
|
|
||||||
`ehrlich_fehlender_bedarf` (18.09.2026): d_G3 „im Text nicht genannt“, im Programm `d_G3 = 40`, gemeldet als
|
|
||||||
„optimal 270, zuverlässig“. An 286 gespeicherten Läufen: 9 Treffer, alle echt, kein Fehlalarm.
|
|
||||||
"""
|
"""
|
||||||
if solver_code and solver_code.data_assumptions:
|
if not RANDOM_SOURCE.search(code or "") or (solver_code and solver_code.data_assumptions):
|
||||||
return []
|
return []
|
||||||
befunde = [f"Die Spezifikation nennt für {symbol} keinen Wert ({text[:120]}), das Programm weist aber keine "
|
if (spec is not None and spec.target_stack == TargetStack.IPOPT
|
||||||
f"angenommenen Daten aus. Jeder Wert, den das Programm dafür einsetzt – auch ein nur beispielhafter –, und "
|
and all(numbers_in(p.extracted_values or "") for p in spec.parameters)):
|
||||||
f"auch das Weglassen eines konstanten Terms gehört in data_assumptions (das Feld der Antwort neben code, "
|
return []
|
||||||
f"kein Argument von report()). Hängt das Ergebnis von dem fehlenden Wert ab, sag das in der Antwort."
|
return ["Das Programm erzeugt Zahlen mit einem Zufallsgenerator, weist aber keine angenommenen "
|
||||||
for symbol, text in missing_values(spec, data_files)]
|
|
||||||
if not _generates_data(code, spec):
|
|
||||||
return befunde
|
|
||||||
return befunde + ["Das Programm erzeugt Zahlen mit einem Zufallsgenerator, weist aber keine angenommenen "
|
|
||||||
"Daten aus. Selbst erzeugte Eingabedaten gehören in data_assumptions – das Feld der Antwort "
|
"Daten aus. Selbst erzeugte Eingabedaten gehören in data_assumptions – das Feld der Antwort "
|
||||||
"neben code, kein Argument von report() –, sonst ist dem "
|
"neben code, kein Argument von report() –, sonst ist dem "
|
||||||
"Ergebnis nicht anzusehen, dass es nicht auf den Daten der Aufgabe beruht."]
|
"Ergebnis nicht anzusehen, dass es nicht auf den Daten der Aufgabe beruht."]
|
||||||
|
|
||||||
|
|
||||||
# Wörter, mit denen eine Antwort sagt, dass ihre Zahlen auf angenommenen Daten beruhen
|
|
||||||
DISCLOSURE = re.compile(r"angenommen|annahme|simuliert|synthetisch|erzeugt|fiktiv|beispiel|hypothetisch|unterstellt|"
|
|
||||||
r"platzhalter|nicht die (?:echten|tatsächlichen)|keine echten", re.IGNORECASE)
|
|
||||||
|
|
||||||
|
|
||||||
def check_answers_disclose_assumptions(run, code: str, solver_code, spec=None) -> list:
|
|
||||||
"""Sagen die Antworten, dass das Ergebnis auf selbst erzeugten Daten beruht?
|
|
||||||
|
|
||||||
Beobachtet an `ehrlich_fehlende_kursdaten` (18.09.2026): Kurse simuliert und in data_assumptions ausgewiesen,
|
|
||||||
die Antwort aber lautete „Aus den Tagesschlusskursen der letzten zwei Jahre ergeben sich … 78,54 %“ – Simuliertes
|
|
||||||
als echt dargestellt. Geprüft wird nur bei selbst erzeugten Daten und nur, ob es MINDESTENS EINE Antwort oder der
|
|
||||||
Antwortsatz sagt: eine Pflicht je Antwort schlug an gespeicherten Läufen fehl an (angenommene Währung,
|
|
||||||
ein konstanter Preis ohne Einfluss auf das Optimum).
|
|
||||||
"""
|
|
||||||
if not (solver_code and solver_code.data_assumptions) or not run.answers or not _generates_data(code, spec):
|
|
||||||
return []
|
|
||||||
texte = [run.headline or ""] + [a.text for a in run.answers]
|
|
||||||
if any(DISCLOSURE.search(t) for t in texte):
|
|
||||||
return []
|
|
||||||
return ["Das Ergebnis beruht auf selbst erzeugten Eingabedaten (data_assumptions), aber weder der Antwortsatz "
|
|
||||||
"noch eine Antwort sagt das – sie lesen sich, als wären es die Daten der Aufgabe. Nenne in der Antwort mit "
|
|
||||||
"dem Ergebnis ausdrücklich, dass die Zahlen auf angenommenen bzw. simulierten Daten beruhen."]
|
|
||||||
|
|
||||||
|
|
||||||
def repair_texts(run, extra: dict | None = None) -> None:
|
def repair_texts(run, extra: dict | None = None) -> None:
|
||||||
"""Umschriebene Umlaute in allen Texten für Menschen berichtigen; `extra` aus `umlaut_map`."""
|
"""Umschriebene Umlaute in allen Texten für Menschen berichtigen; `extra` aus `umlaut_map`."""
|
||||||
run.headline = fix_transliterations(run.headline, extra) if run.headline else run.headline
|
run.headline = fix_transliterations(run.headline, extra) if run.headline else run.headline
|
||||||
|
|
@ -1171,13 +1106,12 @@ def solve(session: LLMSession, task: str, spec: OptimizationModelSpecification,
|
||||||
data_prompt=data_prompt)
|
data_prompt=data_prompt)
|
||||||
solver_code.code = clean_generated_code(solver_code.code)
|
solver_code.code = clean_generated_code(solver_code.code)
|
||||||
run = _check_and_run(solver_code.code, timeout, spec, understanding, task, data_files, data_prompt)
|
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, data_files)
|
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)
|
||||||
zerlegung = check_objective_decomposition(run, spec)
|
zerlegung = check_objective_decomposition(run, spec)
|
||||||
verschwiegen = check_answers_disclose_assumptions(run, solver_code.code, solver_code, spec)
|
|
||||||
attempt = 1
|
attempt = 1
|
||||||
while (run.status == SolverStatus.ERROR or run.feasibility or run.undeclared_data or run.unsolved_scenarios
|
while (run.status == SolverStatus.ERROR or run.feasibility or run.undeclared_data or run.unsolved_scenarios
|
||||||
or einheiten or zerlegung or verschwiegen) and attempt <= retries:
|
or einheiten or zerlegung) and attempt <= retries:
|
||||||
# Alle Befundarten zugleich melden – sonst kostet jede weitere Art einen eigenen Reparaturversuch
|
# Alle Befundarten zugleich melden – sonst kostet jede weitere Art einen eigenen Reparaturversuch
|
||||||
fehler = run.error or "\n\n".join(
|
fehler = run.error or "\n\n".join(
|
||||||
titel + ":\n- " + "\n- ".join(befunde)
|
titel + ":\n- " + "\n- ".join(befunde)
|
||||||
|
|
@ -1185,22 +1119,19 @@ def solve(session: LLMSession, task: str, spec: OptimizationModelSpecification,
|
||||||
("Eingabedaten nicht ausgewiesen", run.undeclared_data),
|
("Eingabedaten nicht ausgewiesen", run.undeclared_data),
|
||||||
("Szenario ohne Ergebnis", run.unsolved_scenarios),
|
("Szenario ohne Ergebnis", run.unsolved_scenarios),
|
||||||
("Einheit in der Antwort erfunden", einheiten),
|
("Einheit in der Antwort erfunden", einheiten),
|
||||||
("Zielwert nicht nachvollziehbar", zerlegung),
|
("Zielwert nicht nachvollziehbar", zerlegung))
|
||||||
("Antwort verschweigt angenommene Daten", verschwiegen))
|
|
||||||
if befunde)
|
if befunde)
|
||||||
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,
|
||||||
understanding=understanding, data_prompt=data_prompt)
|
understanding=understanding, data_prompt=data_prompt)
|
||||||
solver_code.code = clean_generated_code(solver_code.code)
|
solver_code.code = clean_generated_code(solver_code.code)
|
||||||
run = _check_and_run(solver_code.code, timeout, spec, understanding, task, data_files, data_prompt)
|
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, data_files)
|
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)
|
||||||
zerlegung = check_objective_decomposition(run, spec)
|
zerlegung = check_objective_decomposition(run, spec)
|
||||||
verschwiegen = check_answers_disclose_assumptions(run, solver_code.code, solver_code, spec)
|
|
||||||
attempt += 1
|
attempt += 1
|
||||||
# Bleiben erfundene Einheit oder fehlende Zerlegung, ist die Lösung trotzdem geprüft zulässig – Warnung, nicht
|
# 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).
|
# unzuverlässig. Eine abweichende Summe steht schon in den Warnungen (parse_result_tables).
|
||||||
# Verschwiegene Annahmen: die Daten sind ausgewiesen (Report „Angenommene Daten“), nur der Antworttext führt irre
|
run.warnings += einheiten + [z for z in zerlegung if z.startswith(DECOMPOSITION_MISSING)]
|
||||||
run.warnings += einheiten + [z for z in zerlegung if z.startswith(DECOMPOSITION_MISSING)] + verschwiegen
|
|
||||||
run.attempts = attempt
|
run.attempts = attempt
|
||||||
return solver_code, run
|
return solver_code, run
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
"""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,
|
||||||
|
|
@ -737,86 +736,3 @@ 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
|
|
||||||
|
|
||||||
|
|
||||||
def test_missing_value_without_declared_assumption_is_repaired_or_unreliable():
|
|
||||||
# ehrlich_fehlender_bedarf: fehlender Wert still eingesetzt ⇒ Reparatur; bleibt es ⇒ unzuverlässig
|
|
||||||
from core.model_schema import ParameterDefinition
|
|
||||||
fehlend = GOOD.model_copy(update={"parameters": GOOD.parameters + [ParameterDefinition(
|
|
||||||
symbol="d_3", dimension="Skalar", unit="Kisten", description="Bedarf G3",
|
|
||||||
extracted_values="im Text nicht genannt")]})
|
|
||||||
ausgewiesen = SolverCode(code=STRUCTURED_CODE, data_assumptions=["d_3 = 0 Kisten angenommen, im Text nicht genannt"])
|
|
||||||
session = FakeSession([fehlend], [OK], [SolverCode(code=STRUCTURED_CODE), ausgewiesen])
|
|
||||||
result = run_pipeline("Aufgabe", session=session, max_revisions=0, max_restarts=0)
|
|
||||||
assert session.steps.count("Solver-Reparatur") == 1 and result.reliable
|
|
||||||
session = FakeSession([fehlend], [OK], [SolverCode(code=STRUCTURED_CODE)] * 3)
|
|
||||||
result = run_pipeline("Aufgabe", session=session, max_revisions=0, max_restarts=0)
|
|
||||||
assert not result.reliable and any(i.startswith("Datenherkunft") for i in result.reliability_issues)
|
|
||||||
|
|
|
||||||
|
|
@ -396,11 +396,3 @@ def test_no_decomposition_needed_without_a_solution():
|
||||||
from solving.solver_stage import check_objective_decomposition
|
from solving.solver_stage import check_objective_decomposition
|
||||||
assert check_objective_decomposition(_lauf([], status="infeasible", objective=None), make_spec()) == []
|
assert check_objective_decomposition(_lauf([], status="infeasible", objective=None), make_spec()) == []
|
||||||
assert check_objective_decomposition(_lauf([_tabelle([["Kosten", 51.0]])]), make_spec()) == []
|
assert check_objective_decomposition(_lauf([_tabelle([["Kosten", 51.0]])]), make_spec()) == []
|
||||||
|
|
||||||
|
|
||||||
def test_component_starting_with_summe_is_not_dropped():
|
|
||||||
# „Summe (0) weicht vom Zielwert ab“ in zwei Reparaturen von ehrlich_fehlende_kursdaten (20260918-140124)
|
|
||||||
from solving.solver_stage import check_table
|
|
||||||
rows = [["Summe der Varianzbeiträge", 0.0174321], ["Summe", 0.0174321]]
|
|
||||||
belegt, abweichung, _ = check_table(_tabelle(rows), 0.0174321)
|
|
||||||
assert belegt and abweichung is None
|
|
||||||
|
|
|
||||||
|
|
@ -884,57 +884,3 @@ def test_timeout_ends_a_slow_program():
|
||||||
code = "def main():\n while True:\n pass\n\n\nif __name__ == '__main__':\n main()\n"
|
code = "def main():\n while True:\n pass\n\n\nif __name__ == '__main__':\n main()\n"
|
||||||
run = run_solver_code(code, timeout=1)
|
run = run_solver_code(code, timeout=1)
|
||||||
assert run.status == SolverStatus.ERROR and "Timeout nach 1 s" in run.error
|
assert run.status == SolverStatus.ERROR and "Timeout nach 1 s" in run.error
|
||||||
|
|
||||||
|
|
||||||
# --- Fehlende Werte laut Spezifikation und verschwiegene Annahmen (ehrlich_* Fälle, 18.09.2026) ---
|
|
||||||
|
|
||||||
def _spec_mit_fehlendem_wert(text="d_{G1} = 30, d_{G2} = 40; d_{G3} im Text nicht genannt (offener Parameter)"):
|
|
||||||
from test_model_checks import make_spec
|
|
||||||
return make_spec(parameters=[{"symbol": "d_j", "dimension": "Vektor", "unit": "Kisten", "description": "Bedarf",
|
|
||||||
"extracted_values": text}])
|
|
||||||
|
|
||||||
|
|
||||||
def test_missing_value_in_the_specification_must_be_declared():
|
|
||||||
# beobachtet an ehrlich_fehlender_bedarf: d_G3 „nicht genannt“, im Programm d_G3 = 40, „optimal 270, zuverlässig“
|
|
||||||
from core.model_schema import SolverCode
|
|
||||||
from solving.solver_stage import check_generated_data
|
|
||||||
code = "DATA = {'d_G3': 40}\n"
|
|
||||||
befunde = check_generated_data(code, SolverCode(code=code, data_assumptions=[]), _spec_mit_fehlendem_wert())
|
|
||||||
assert len(befunde) == 1 and "d_j" in befunde[0] and "data_assumptions" in befunde[0]
|
|
||||||
ausgewiesen = SolverCode(code=code, data_assumptions=["d_G3 = 40 Kisten angenommen (nicht genannt)"])
|
|
||||||
assert check_generated_data(code, ausgewiesen, _spec_mit_fehlendem_wert()) == []
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("text", ["30, 40, 55", "aus kurse.csv berechnet (im Text nicht genannt)", ""])
|
|
||||||
def test_complete_values_and_data_files_are_not_missing(text):
|
|
||||||
from solving.solver_stage import missing_values
|
|
||||||
assert missing_values(_spec_mit_fehlendem_wert(text), data_files=["/tmp/lauf/kurse.csv"]) == []
|
|
||||||
|
|
||||||
|
|
||||||
def _lauf_mit_antworten(*texte, headline=None):
|
|
||||||
from core.model_schema import Answer, SolverRun
|
|
||||||
return SolverRun(status=SolverStatus.OPTIMAL, objective_value=1.0, headline=headline,
|
|
||||||
answers=[Answer(question_id=str(i), text=t) for i, t in enumerate(texte, 1)])
|
|
||||||
|
|
||||||
|
|
||||||
SIMULIERT = "import numpy as np\nrng = np.random.default_rng(1)\nkurse = rng.normal(0, 1, (504, 3))\n"
|
|
||||||
|
|
||||||
|
|
||||||
def test_answers_must_say_that_data_is_simulated():
|
|
||||||
# beobachtet an ehrlich_fehlende_kursdaten: „Aus den Tagesschlusskursen der letzten zwei Jahre ergeben sich …“
|
|
||||||
from core.model_schema import SolverCode
|
|
||||||
from solving.solver_stage import check_answers_disclose_assumptions
|
|
||||||
code = SolverCode(code=SIMULIERT, data_assumptions=["Kurse simuliert, Seed 1"])
|
|
||||||
verschwiegen = _lauf_mit_antworten("Die Renditen betragen 19,6 %, 33,9 % und 78,5 %.", "Alpha erhält 250.000 Euro.")
|
|
||||||
assert check_answers_disclose_assumptions(verschwiegen, SIMULIERT, code)
|
|
||||||
offen = _lauf_mit_antworten("Auf Basis simulierter Kurse erhält Alpha 250.000 Euro.", "Das Modell ist ein QP.")
|
|
||||||
assert check_answers_disclose_assumptions(offen, SIMULIERT, code) == []
|
|
||||||
|
|
||||||
|
|
||||||
def test_assumptions_without_generated_data_need_no_disclosure():
|
|
||||||
# angenommene Währung oder ein konstanter Preis ohne Einfluss auf das Optimum: kein Befund (sonst Fehlalarme)
|
|
||||||
from core.model_schema import SolverCode
|
|
||||||
from solving.solver_stage import check_answers_disclose_assumptions
|
|
||||||
code = "DATA = {'kosten': [3, 2]}\n"
|
|
||||||
lauf = _lauf_mit_antworten("Die Kosten betragen 9.")
|
|
||||||
assert check_answers_disclose_assumptions(lauf, code, SolverCode(code=code, data_assumptions=["Währung Euro"])) == []
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue