feat: fehlende Werte und simulierte Daten müssen ausgewiesen und genannt werden
- check_generated_data: nennt extracted_values einen Wert als fehlend („nicht genannt“, „unbekannt“, „offener Parameter“; Werte aus beigelegten Dateien ausgenommen) und data_assumptions ist leer -> undeclared_data (Reparatur, sonst unzuverlässig). An 211 gespeicherten Läufen 8 Treffer, alle echt. - check_answers_disclose_assumptions: bei selbst erzeugten Daten muss Antwortsatz oder eine Antwort das sagen -> Reparatur, sonst Warnung. 1 Treffer, echt. - Summenprüfung: bleibt nach Abzug der Summenzeilen keine Zeile, ist nur die Schlusszeile die Summe (Fehlalarm „Summe (0)“ kostete zwei Reparaturen). Nachlauf 20260918-140124: ehrlich_fehlende_kursdaten und ehrlich_fehlender_bedarf 2/2. Ändert die Prompt-Kennung (Solver-Prompt.txt). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
d6bad28cb2
commit
20ac3f14d7
2 changed files with 159 additions and 12 deletions
|
|
@ -10,8 +10,9 @@ GEMEINSAMER RAHMEN (gilt für alle vier Stufen dieser Pipeline: Verstehen, Model
|
|||
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.
|
||||
- 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.
|
||||
- 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]
|
||||
- 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.
|
||||
|
||||
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,6 +325,10 @@ 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)
|
||||
|
||||
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):
|
||||
return False, None, [f"Tabelle '{table.title}': Spalte '{table.objective_column}' enthält nicht nur "
|
||||
f"Zahlen – Summe nicht prüfbar."]
|
||||
|
|
@ -887,7 +891,35 @@ 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)
|
||||
|
||||
|
||||
def check_generated_data(code: str, solver_code, spec=None) -> list:
|
||||
# „Wert fehlt“ in extracted_values: die Spezifikation sagt selbst, dass der Text ihn nicht hergibt
|
||||
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?
|
||||
|
||||
Beobachtet an der Projektaufgabe „Portfolio Nr. 2", die Kursdaten aus dem Netz verlangt: Das
|
||||
|
|
@ -904,18 +936,51 @@ def check_generated_data(code: str, solver_code, spec=None) -> list:
|
|||
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
|
||||
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 not RANDOM_SOURCE.search(code or "") or (solver_code and solver_code.data_assumptions):
|
||||
if solver_code and solver_code.data_assumptions:
|
||||
return []
|
||||
if (spec is not None and spec.target_stack == TargetStack.IPOPT
|
||||
and all(numbers_in(p.extracted_values or "") for p in spec.parameters)):
|
||||
return []
|
||||
return ["Das Programm erzeugt Zahlen mit einem Zufallsgenerator, weist aber keine angenommenen "
|
||||
befunde = [f"Die Spezifikation nennt für {symbol} keinen Wert ({text[:120]}), das Programm weist aber keine "
|
||||
f"angenommenen Daten aus. Jeder Wert, den das Programm dafür einsetzt – auch ein nur beispielhafter –, und "
|
||||
f"auch das Weglassen eines konstanten Terms gehört in data_assumptions (das Feld der Antwort neben code, "
|
||||
f"kein Argument von report()). Hängt das Ergebnis von dem fehlenden Wert ab, sag das in der Antwort."
|
||||
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 "
|
||||
"neben code, kein Argument von report() –, sonst ist dem "
|
||||
"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:
|
||||
"""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
|
||||
|
|
@ -1106,12 +1171,13 @@ def solve(session: LLMSession, task: str, spec: OptimizationModelSpecification,
|
|||
data_prompt=data_prompt)
|
||||
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.undeclared_data = check_generated_data(solver_code.code, solver_code, spec)
|
||||
run.undeclared_data = check_generated_data(solver_code.code, solver_code, spec, data_files)
|
||||
einheiten = check_answer_units(run, task, data_prompt)
|
||||
zerlegung = check_objective_decomposition(run, spec)
|
||||
verschwiegen = check_answers_disclose_assumptions(run, solver_code.code, solver_code, spec)
|
||||
attempt = 1
|
||||
while (run.status == SolverStatus.ERROR or run.feasibility or run.undeclared_data or run.unsolved_scenarios
|
||||
or einheiten or zerlegung) and attempt <= retries:
|
||||
or einheiten or zerlegung or verschwiegen) 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)
|
||||
|
|
@ -1119,19 +1185,22 @@ def solve(session: LLMSession, task: str, spec: OptimizationModelSpecification,
|
|||
("Eingabedaten nicht ausgewiesen", run.undeclared_data),
|
||||
("Szenario ohne Ergebnis", run.unsolved_scenarios),
|
||||
("Einheit in der Antwort erfunden", einheiten),
|
||||
("Zielwert nicht nachvollziehbar", zerlegung))
|
||||
("Zielwert nicht nachvollziehbar", zerlegung),
|
||||
("Antwort verschweigt angenommene Daten", verschwiegen))
|
||||
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,
|
||||
understanding=understanding, data_prompt=data_prompt)
|
||||
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.undeclared_data = check_generated_data(solver_code.code, solver_code, spec)
|
||||
run.undeclared_data = check_generated_data(solver_code.code, solver_code, spec, data_files)
|
||||
einheiten = check_answer_units(run, task, data_prompt)
|
||||
zerlegung = check_objective_decomposition(run, spec)
|
||||
verschwiegen = check_answers_disclose_assumptions(run, solver_code.code, solver_code, spec)
|
||||
attempt += 1
|
||||
# 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)]
|
||||
# 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)] + verschwiegen
|
||||
run.attempts = attempt
|
||||
return solver_code, run
|
||||
|
|
|
|||
Loading…
Reference in a new issue