Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dd7206aee5 | |||
| ee9a193cd7 | |||
| c68a293755 | |||
| 7aac3ff3ec |
7 changed files with 216 additions and 17 deletions
|
|
@ -26,9 +26,10 @@ RESULTS_DIR = BENCHMARK_DIR / "results"
|
|||
# Nach dem ersten fatalen API-Fehler (Kontingent/Guthaben/Key) keine weiteren Läufe starten
|
||||
quota_exhausted = threading.Event()
|
||||
|
||||
# Dateien, die das LLM-Verhalten steuern: die drei System-Prompts und die Feldbeschreibungen
|
||||
# Dateien, die das LLM-Verhalten steuern: die vier System-Prompts und die Feldbeschreibungen
|
||||
# (Field(description=...) geht als JSON-Schema mit und wirkt wie eine Prompt-Anweisung).
|
||||
PROMPT_FILES = ("modeling/Routing-und-Extraktions-Prompt.txt", "checks/Pruef-Prompt.txt", "solving/Solver-Prompt.txt",
|
||||
PROMPT_FILES = ("understanding/Verstehen-Prompt.txt", "modeling/Routing-und-Extraktions-Prompt.txt", "checks/Pruef-Prompt.txt",
|
||||
"solving/Solver-Prompt.txt",
|
||||
"core/model_schema.py")
|
||||
|
||||
# Umgebungsvariablen, die das Verhalten ändern – ohne sie ließe sich ein A/B-Lauf nicht zuordnen
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ 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.
|
||||
- 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, 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 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.
|
||||
|
|
@ -85,6 +85,7 @@ Ergebnisausgabe:
|
|||
- Auch bei Status infeasible oder unbounded wird JEDE Teilfrage beantwortet – dann lautet die Antwort, dass und warum die Aufgabe keine zulässige Lösung hat (welche Bedingungen sich widersprechen). Eine unbeantwortete Frage ist schlechter als die Antwort „unlösbar“.
|
||||
- Teilfragen ohne Rechnung (formulieren, zeichnen, erklaeren) werden ebenfalls beantwortet – dort beschreibt der Text das aufgestellte Modell bzw. die Struktur der Zeichnung in Worten.
|
||||
- Verwende NUR Zahlen, die dieses Programm berechnet hat oder die im Aufgabentext stehen. [→ check_answers]
|
||||
- Ebenso bei Einheiten: nur solche, die im Aufgabentext oder in beigelegten Daten stehen. Nennt der Text keine, steht die Zahl ohne Einheit („Der kleinste Zielwert ist 459.“); GE bzw. ME nur, wenn der Text von Kosten, Preisen bzw. Mengen spricht. [→ check_answer_units]
|
||||
- WICHTIG, häufigster Fehler: Sobald eine Antwort einen Vergleichs- oder Zwischenwert nennt, muss dieser Wert auch in einer Ergebnistabelle stehen. Beobachtet: „der beste Weg kostet 29, die Alternative 30“ – die 30 stand nirgends, der Leser konnte sie nicht prüfen. Richtig ist eine zusätzliche Tabelle („Alle Wege“, „Vergleich der Varianten“, „Kostenbestandteile“) mit genau diesen Zahlen. Ebenso für Summen wie „insgesamt 2,32 % Gebühren“: entweder eine Summenzeile in der Tabelle oder die Zahl weglassen.
|
||||
- Fehlt eine Teilfrage, ist die Aufgabe nicht erfüllt – auch wenn der Zielwert stimmt. [→ check_answers]
|
||||
- scenarios: NUR wenn die Aufgabe mehrere Konfigurationen vergleicht (z. B. drei Gewichtungen), sonst leer. Ein Szenario ist eine DATENVARIANTE desselben Modells, kein zweites Modell:
|
||||
|
|
|
|||
|
|
@ -271,6 +271,17 @@ def _is_sum_row(row, value=None, others=None) -> bool:
|
|||
return _close(value, sum(others))
|
||||
|
||||
|
||||
def _is_comparison(values, no_sum_row: bool, total: float, objective_value) -> bool:
|
||||
"""Vergleichstabelle: jede Zeile ist ein eigener Zielwert (Konfiguration, Szenario), keine Zerlegung.
|
||||
|
||||
Erkennbar daran, dass die Summe nicht passt, aber genau eine Zeile den gemeldeten Zielwert trägt.
|
||||
Beobachtet: "Vergleich der Konfigurationen" (Portfolio Nr. 2) galt sonst als "Summe weicht ab".
|
||||
"""
|
||||
if objective_value is None or not no_sum_row or len(values) < 2 or _close(total, objective_value):
|
||||
return False
|
||||
return sum(_close(v, objective_value) for v in values) == 1
|
||||
|
||||
|
||||
def _close(a: float, b: float) -> bool:
|
||||
return abs(a - b) <= 1e-3 * max(abs(a), abs(b)) + 1e-6
|
||||
|
||||
|
|
@ -303,6 +314,8 @@ def parse_result_tables(raw_tables: list, objective_value) -> tuple:
|
|||
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}) "
|
||||
|
|
@ -383,7 +396,9 @@ def check_sensitivity(run, sense: str) -> list:
|
|||
Die Richtung hängt am Sinn der Restriktion und daran, ob maximiert oder minimiert wird.
|
||||
"""
|
||||
warnings = []
|
||||
besser = (lambda a, b: a > b) if sense == "maximize" else (lambda a, b: a < b)
|
||||
# mit Toleranz: beobachtet Schranke 0.13896862211718014 gegen Lösung ...17 als „schlechter" gemeldet
|
||||
besser = ((lambda a, b: a > b + _tolerance(b)) if sense == "maximize"
|
||||
else (lambda a, b: a < b - _tolerance(b)))
|
||||
by_name = {c.name: c for c in run.constraints}
|
||||
for s in run.sensitivity:
|
||||
constraint = by_name.get(s.name)
|
||||
|
|
@ -553,7 +568,9 @@ def check_multistart(run, sense: str) -> list:
|
|||
if m is None:
|
||||
return []
|
||||
warnings = []
|
||||
besser = (lambda a, b: a > b) if sense == "maximize" else (lambda a, b: a < b)
|
||||
# mit Toleranz: beobachtet Schranke 0.13896862211718014 gegen Lösung ...17 als „schlechter" gemeldet
|
||||
besser = ((lambda a, b: a > b + _tolerance(b)) if sense == "maximize"
|
||||
else (lambda a, b: a < b - _tolerance(b)))
|
||||
if m.converged > m.starts or m.converged < 0:
|
||||
warnings.append(f"Mehrstart: {m.converged} erfolgreiche von {m.starts} Startpunkten ist nicht möglich.")
|
||||
elif m.converged < 2:
|
||||
|
|
@ -587,8 +604,20 @@ UMLAUT_STEMS = {
|
|||
"jaehrl": "jährl", "kuerzest": "kürzest", "laenge": "länge", "staerk": "stärk", "hoeh": "höh",
|
||||
"fruehest": "frühest", "spaetest": "spätest", "baell": "bäll", "heisst": "heißt",
|
||||
"betraeg": "beträg", "ausfuehr": "ausführ", "abzueg": "abzüg", "enthaelt": "enthält", "tatsaechl": "tatsächl",
|
||||
# Wortstämme aus 252 gespeicherten Läufen; greifen auch in Komposita („Gesamtstueckzahl") und Beugungen.
|
||||
# Jeder Stamm ist gegen /usr/share/dict/ngerman und american-english geprüft: er steht in keinem
|
||||
# korrekt geschriebenen Wort („loes" fiel heraus: aloes, floes).
|
||||
"taet": "tät", "stueck": "stück", "fuehr": "führ", "traeg": "träg", "vollstaend": "vollständ",
|
||||
"zulaess": "zuläss", "loesbar": "lösbar", "erloes": "erlös", "schoepf": "schöpf", "gebuehr": "gebühr",
|
||||
"noetig": "nötig", "faell": "fäll", "haelt": "hält", "waehr": "währ", "waechs": "wächs", "waere": "wäre",
|
||||
"laeuft": "läuft", "schluess": "schlüss", "duerf": "dürf", "kaeuf": "käuf", "wuerd": "würd",
|
||||
"gueltig": "gültig", "gehoer": "gehör", "drueck": "drück", "uebrig": "übrig", "haeng": "häng",
|
||||
"naemlich": "nämlich", "lueck": "lück", "saetz": "sätz", "plaen": "plän", "aermst": "ärmst",
|
||||
"spaet": "spät", "frueh": "früh", "aender": "änder", "zaehl": "zähl", "verhaeltnis": "verhältnis",
|
||||
"ergaenz": "ergänz", "begruend": "begründ", "pruef": "prüf", "rueck": "rück", "maessig": "mäßig", "vermoeg": "vermög",
|
||||
}
|
||||
_STEM_PATTERN = re.compile("|".join(UMLAUT_STEMS), re.IGNORECASE)
|
||||
_CAMEL_CASE = re.compile(r"[a-zäöüß][A-ZÄÖÜ]") # KeinFruehNachSpaet
|
||||
|
||||
|
||||
_UMLAUT_WORD = re.compile(r"\b\w*[äöüÄÖÜß]\w*\b")
|
||||
|
|
@ -618,17 +647,24 @@ def umlaut_map(aufgabe: str, *weitere: str) -> dict:
|
|||
def fix_transliterations(text: str, extra: dict | None = None) -> str:
|
||||
"""'guenstigste' -> 'günstigste': erst Wörter aus dem Lauf (`umlaut_map`), dann die feste Stammliste.
|
||||
|
||||
Nur für Lesertexte. Bezeichner und Restriktionsnamen bleiben ASCII – der Namensabgleich hängt daran.
|
||||
Nur für Lesertexte. Bezeichner und Restriktionsnamen bleiben ASCII – der Namensabgleich hängt daran;
|
||||
auch in Tabellenzellen bleibt ein Wort mit `_` oder Binnenmajuskel unverändert (vorher wurde mal berichtigt, mal nicht).
|
||||
"""
|
||||
def gross_wie(vorlage, richtig):
|
||||
return richtig[:1].upper() + richtig[1:] if vorlage[:1].isupper() else richtig
|
||||
|
||||
text = text or ""
|
||||
if extra:
|
||||
muster = re.compile(r"\b(?:" + "|".join(sorted(map(re.escape, extra), key=len, reverse=True)) + r")\b",
|
||||
re.IGNORECASE)
|
||||
text = muster.sub(lambda t: gross_wie(t.group(0), extra[t.group(0).lower()]), text)
|
||||
return _STEM_PATTERN.sub(lambda t: gross_wie(t.group(0), UMLAUT_STEMS[t.group(0).lower()]), text)
|
||||
muster = (re.compile("^(?:" + "|".join(sorted(map(re.escape, extra), key=len, reverse=True)) + ")$", re.IGNORECASE)
|
||||
if extra else None)
|
||||
|
||||
def wort(t):
|
||||
w = t.group(0)
|
||||
if "_" in w or _CAMEL_CASE.search(w): # Bezeichner (Restriktionsnamen in Tabellenzellen) bleiben
|
||||
return w
|
||||
if muster and muster.match(w):
|
||||
return gross_wie(w, extra[w.lower()])
|
||||
return _STEM_PATTERN.sub(lambda m: gross_wie(m.group(0), UMLAUT_STEMS[m.group(0).lower()]), w)
|
||||
|
||||
return re.sub(r"\w+", wort, text or "")
|
||||
|
||||
|
||||
def check_text_quality(run, summary: str = "") -> list:
|
||||
|
|
@ -672,7 +708,9 @@ def verify_scenarios(run, code: str, timeout: int = 60, data_files=()) -> list:
|
|||
data_files=data_files)
|
||||
szenario.verified_value = nachlauf.objective_value
|
||||
if nachlauf.status == SolverStatus.ERROR:
|
||||
warnings.append(f"Szenario '{szenario.name}' ließ sich nicht nachrechnen: {(nachlauf.error or '')[:120]}")
|
||||
grund = (nachlauf.error or "").strip()[:120] or (
|
||||
"das Programm meldete selbst Status 'error' ohne Fehlertext (z. B. keine Lösung im Zeitlimit)")
|
||||
warnings.append(f"Szenario '{szenario.name}' ließ sich nicht nachrechnen: {grund}")
|
||||
elif (szenario.objective_value is not None and nachlauf.objective_value is not None
|
||||
and abs(szenario.objective_value - nachlauf.objective_value)
|
||||
> 1e-6 * max(1.0, abs(szenario.objective_value))):
|
||||
|
|
@ -730,7 +768,8 @@ def check_generated_data(code: str, solver_code, spec=None) -> list:
|
|||
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 "
|
||||
"Daten aus. Selbst erzeugte Eingabedaten gehören in data_assumptions – sonst ist dem "
|
||||
"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."]
|
||||
|
||||
|
||||
|
|
@ -789,6 +828,57 @@ def check_answers(run, understanding, task: str = "", data_summary: str = "") ->
|
|||
return befunde
|
||||
|
||||
|
||||
# Einheiten nach Größenart: gedeckt ist eine Einheit, wenn der Text irgendeine derselben Art nennt
|
||||
# ("Tausend Euro" deckt "€", "Stunden" deckt "min"). Prozent fehlt bewusst – es ist keine Einheit, die
|
||||
# man erfinden kann, sondern eine Schreibweise für Anteile.
|
||||
UNIT_FAMILIES = {
|
||||
"Geld": ("€", "EUR", "Euro", "$", "USD", "Dollar", "CHF", "Cent", "ct", "GE", "Geldeinheit", "Geldeinheiten"),
|
||||
"Menge": ("Stück", "Stk", "ME", "Mengeneinheit", "Mengeneinheiten"),
|
||||
"Zeit": ("h", "Std", "Stunde", "Stunden", "min", "Minute", "Minuten", "s", "Sekunden", "Tag", "Tage", "Tagen",
|
||||
"Woche", "Wochen", "Monat", "Monate", "Monaten", "Jahr", "Jahre", "Jahren"),
|
||||
"Masse": ("g", "kg", "t", "Tonne", "Tonnen"),
|
||||
"Länge": ("mm", "cm", "m", "km", "Meter", "Kilometer"),
|
||||
"Volumen": ("l", "Liter", "hl", "m³"),
|
||||
}
|
||||
_UNIT_OF = {einheit: art for art, einheiten in UNIT_FAMILIES.items() for einheit in einheiten}
|
||||
_UNIT_ALT = "|".join(sorted(map(re.escape, _UNIT_OF), key=len, reverse=True))
|
||||
# Kurze Einheiten (h, s, t, kg) decken nur mit Zahl davor oder in Klammern – sonst deckt „s.t." oder x_t alles
|
||||
_UNIT_COVER = re.compile(r"(?<![\wäöüß])([(\[]\s*|in\s+)?(" + _UNIT_ALT + r")(?![\wäöüß])")
|
||||
_UNIT_AFTER_NUMBER = re.compile(r"\d(?:[\d.,]*\d)?\s*(?:(?:Tsd|Mio|Mrd)\.?\s*)?(" + _UNIT_ALT + r")(?![\wäöüß])")
|
||||
# Neutrale Platzhalter sind keine Erfindung, wenn der Text die Größe benennt, nur nicht ihre Einheit
|
||||
_NEUTRAL_GELD = re.compile(r"kost|preis|gewinn|erlös|erloes|umsatz|budget|kapital|deckungsbeitrag|ausgabe|einnahme", re.I)
|
||||
_NEUTRAL_MENGE = re.compile(r"menge|stück|stueck|produz|liefer|bedarf|nachfrage", re.I)
|
||||
_NEUTRAL_UNITS = {"GE": _NEUTRAL_GELD, "Geldeinheit": _NEUTRAL_GELD, "Geldeinheiten": _NEUTRAL_GELD,
|
||||
"ME": _NEUTRAL_MENGE, "Mengeneinheit": _NEUTRAL_MENGE, "Mengeneinheiten": _NEUTRAL_MENGE}
|
||||
|
||||
|
||||
def check_answer_units(run, task: str, data_summary: str = "") -> list:
|
||||
"""Nennt eine Antwort eine Einheit, die weder Aufgabentext noch beigelegte Daten hergeben?
|
||||
|
||||
Beobachtet an `tasks/nur_formeln.md` (reines ILP ohne jede Einheit): „459 Geldeinheiten (GE)",
|
||||
obwohl die eigene Annahme die Zahlen dimensionslos nannte; in der Urfassung der Ersatzplanung
|
||||
(„Kosten" ohne Währung) dreimal „29 €". Die Regel im Erklär-Block allein wirkte nicht.
|
||||
Belegt ist nur, was im Text steht – `ParameterDefinition.unit` taugt nicht, dort erfand dasselbe
|
||||
LLM „Stück" und „Mengeneinheit". An 146 gespeicherten Läufen: 4 Treffer, alle echt, kein Fehlalarm.
|
||||
Der Größenfaktor („Tausend Euro" gegen „€") wird bewusst nicht geprüft.
|
||||
"""
|
||||
gedeckt = {_UNIT_OF[m.group(1)] for m in _UNIT_AFTER_NUMBER.finditer(task)}
|
||||
gedeckt |= {_UNIT_OF[m.group(2)] for m in _UNIT_COVER.finditer(task)
|
||||
if len(m.group(2)) > 2 or not m.group(2).isalpha() or m.group(1)}
|
||||
# Datenprofil: Spaltennamen wie emission_kg tragen die Einheit ohne Zahl davor
|
||||
gedeckt |= {_UNIT_OF[m.group(2)] for m in _UNIT_COVER.finditer(data_summary.replace("_", " "))}
|
||||
befunde = []
|
||||
for antwort in run.answers:
|
||||
erfunden = sorted({m.group(1) for m in _UNIT_AFTER_NUMBER.finditer(antwort.text or "")
|
||||
if _UNIT_OF[m.group(1)] not in gedeckt
|
||||
and not (m.group(1) in _NEUTRAL_UNITS and _NEUTRAL_UNITS[m.group(1)].search(task))})
|
||||
if erfunden:
|
||||
befunde.append(f"Antwort zu '{antwort.question_id}' nennt die Einheit {', '.join(erfunden)}, die weder im "
|
||||
f"Aufgabentext noch in beigelegten Daten steht. Nennt der Text keine Einheit, steht die "
|
||||
f"Zahl ohne Einheit – erfinde keine.")
|
||||
return befunde
|
||||
|
||||
|
||||
def check_graph(run, sense: str) -> list:
|
||||
"""Graph und kürzesten Weg deterministisch nachrechnen (ohne LLM).
|
||||
|
||||
|
|
@ -864,17 +954,24 @@ def solve(session: LLMSession, task: str, spec: OptimizationModelSpecification,
|
|||
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)
|
||||
einheiten = check_answer_units(run, task, data_prompt)
|
||||
attempt = 1
|
||||
while (run.status == SolverStatus.ERROR or run.feasibility or run.undeclared_data) and attempt <= retries:
|
||||
while (run.status == SolverStatus.ERROR or run.feasibility or run.undeclared_data or einheiten) \
|
||||
and attempt <= retries:
|
||||
fehler = run.error or (("Die Lösung hält das Modell nicht ein:\n- " + "\n- ".join(run.feasibility))
|
||||
if run.feasibility else
|
||||
("Eingabedaten nicht ausgewiesen:\n- " + "\n- ".join(run.undeclared_data)))
|
||||
("Eingabedaten nicht ausgewiesen:\n- " + "\n- ".join(run.undeclared_data))
|
||||
if run.undeclared_data else
|
||||
("Einheit in der Antwort erfunden:\n- " + "\n- ".join(einheiten)))
|
||||
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)
|
||||
einheiten = check_answer_units(run, task, data_prompt)
|
||||
attempt += 1
|
||||
# Bleibt die erfundene Einheit, ist das Ergebnis trotzdem richtig – Warnung, nicht unzuverlässig
|
||||
run.warnings += einheiten
|
||||
run.attempts = attempt
|
||||
return solver_code, run
|
||||
|
|
|
|||
|
|
@ -143,3 +143,9 @@ def test_report_shows_sensitivity_and_integrality_price():
|
|||
page = to_html(build_document(result))
|
||||
assert "Sensitivität der bindenden Restriktionen" in page and "Preis der Ganzzahligkeit" in page
|
||||
assert "0,1" in page.replace(".", ",") or "0.1" in page # (12100-12000)/1000 = 0.1
|
||||
|
||||
|
||||
def test_relaxation_bound_equal_up_to_rounding_is_no_warning():
|
||||
# beobachtet (Portfolio Nr. 2): 0.13896862211718014 gegen 0.13896862211718017
|
||||
run = sens_run([], objective=0.13896862211718017, relaxation=0.13896862211718014)
|
||||
assert check_sensitivity(run, "maximize") == []
|
||||
|
|
|
|||
|
|
@ -309,6 +309,7 @@ def test_benchmark_records_the_prompt_state():
|
|||
assert len(stand["prompts"]) == 12 and stand["commit"]
|
||||
assert "modeling/Routing-und-Extraktions-Prompt.txt" in PROMPT_FILES
|
||||
assert "core/model_schema.py" in PROMPT_FILES # Feldbeschreibungen wirken wie Prompt-Anweisungen
|
||||
assert "understanding/Verstehen-Prompt.txt" in PROMPT_FILES
|
||||
|
||||
|
||||
def test_deviating_settings_are_recorded(monkeypatch):
|
||||
|
|
@ -421,7 +422,7 @@ if __name__ == "__main__":
|
|||
sub_questions=[SubQuestion(id="a", quote="Formulieren Sie das Modell!", kind="formulieren", needs_solver=False),
|
||||
SubQuestion(id="b", quote="Berechnen Sie die Kosten!", kind="rechnen", needs_solver=True)])
|
||||
session = FakeSession([GOOD], [OK], [SolverCode(code=code)], understandings=[verstanden])
|
||||
result = run_pipeline("Aufgabe", session=session, max_revisions=0, max_restarts=0)
|
||||
result = run_pipeline("Minimiere die Kosten.", session=session, max_revisions=0, max_restarts=0)
|
||||
markdown = to_markdown(build_document(result))
|
||||
assert "Antworten auf die Aufgabe" in markdown
|
||||
assert markdown.index("Formulieren Sie das Modell!") < markdown.index("| Status |")
|
||||
|
|
|
|||
|
|
@ -228,3 +228,84 @@ def test_ascii_form_present_in_the_task_is_not_replaced():
|
|||
zuordnung = umlaut_map("Herr Müller und die Firma Mueller liefern.")
|
||||
assert "mueller" not in zuordnung
|
||||
assert fix_transliterations("Firma Mueller", zuordnung) == "Firma Mueller"
|
||||
|
||||
|
||||
# beobachtet in Portfolio Nr. 2: eine Zeile je Konfiguration, die gewählte trägt den Zielwert
|
||||
COMPARISON = {
|
||||
"title": "Vergleich der Konfigurationen",
|
||||
"columns": ["Konfiguration", "lambda1", "lambda2", "Zielfunktionswert"],
|
||||
"rows": [["Reine Varianzminimierung", "1.0", "0.0", 0.18217355],
|
||||
["Reine Korrelationsminimierung", "0.0", "1.0", -187.29514112],
|
||||
["Ausgewogene Skalarisierung", "0.5", "0.5", -93.56364364]],
|
||||
"objective_column": "Zielfunktionswert",
|
||||
}
|
||||
|
||||
|
||||
def test_comparison_table_is_not_a_sum_mismatch():
|
||||
tables, warnings = parse_result_tables([COMPARISON], objective_value=0.18217353985482126)
|
||||
assert warnings == [] and len(tables) == 1
|
||||
|
||||
|
||||
def test_decomposition_with_one_term_equal_to_objective_is_still_checked():
|
||||
# eine Summenzeile macht daraus eine Zerlegung – deren Fehler bleibt sichtbar
|
||||
table = dict(DECOMPOSITION, rows=[["Rendite", 0.097557], ["Kosten", -0.05], ["Zielwert", 0.2]])
|
||||
_, warnings = parse_result_tables([table], objective_value=0.097557)
|
||||
assert any("weicht vom Zielwert" in w for w in warnings)
|
||||
|
||||
|
||||
# --- Einheiten in Antworten: nur, was der Text hergibt ---
|
||||
|
||||
def _run_with_answer(text):
|
||||
from core.model_schema import Answer
|
||||
return SolverRun(status=SolverStatus.OPTIMAL, objective_value=459, answers=[Answer(question_id="1", text=text)])
|
||||
|
||||
|
||||
def test_invented_unit_in_answer_is_found():
|
||||
# beobachtet an tasks/nur_formeln.md: reines ILP ohne jede Einheit
|
||||
from solving.solver_stage import check_answer_units
|
||||
befunde = check_answer_units(_run_with_answer("Der minimale Zielfunktionswert beträgt 459 Geldeinheiten (GE)."),
|
||||
"min z = 17*x1 + 23*x2 s.t. 6*x1 + 4*x2 >= 120")
|
||||
assert len(befunde) == 1 and "Geldeinheiten" in befunde[0]
|
||||
|
||||
|
||||
def test_currency_is_invented_when_the_text_only_says_costs():
|
||||
from solving.solver_stage import check_answer_units
|
||||
aufgabe = "Die Kostenmatrix C enthält die Kosten je Nutzungsdauer."
|
||||
assert check_answer_units(_run_with_answer("Die Gesamtkosten betragen 29 €."), aufgabe)
|
||||
assert check_answer_units(_run_with_answer("Die Gesamtkosten betragen 29 GE."), aufgabe) == [] # neutral
|
||||
|
||||
|
||||
def test_unit_of_the_same_kind_in_the_text_is_covered():
|
||||
from solving.solver_stage import check_answer_units
|
||||
aufgabe = "Kosten in Tausend Euro; Rüstzeit 2 Stunden."
|
||||
run = _run_with_answer("Die Kosten betragen 29 Tsd. € bei 90 min Rüstzeit, Auslastung 85 %.")
|
||||
assert check_answer_units(run, aufgabe) == []
|
||||
|
||||
|
||||
def test_unit_from_attached_data_is_covered():
|
||||
from solving.solver_stage import check_answer_units
|
||||
run = _run_with_answer("Der Transport kostet 120 kg CO2.")
|
||||
assert check_answer_units(run, "Minimiere die Emissionen.") != []
|
||||
assert check_answer_units(run, "Minimiere die Emissionen.", data_summary="Spalte emission_kg") == []
|
||||
|
||||
|
||||
def test_numbers_and_words_are_not_mistaken_for_units():
|
||||
from solving.solver_stage import check_answer_units
|
||||
run = _run_with_answer("Teil 2 zeigt: x1 = 6, 3 Maschinen, Stufe 2a, am Tag 3 sind alle 5 Aufträge erledigt.")
|
||||
assert check_answer_units(run, "Plane die Aufträge.") == []
|
||||
|
||||
|
||||
def test_repair_reaches_compounds_and_inflections():
|
||||
# beobachtet in Portfolio Nr. 2 und nur_formeln (17.09.2026), Stämme gegen ngerman geprüft
|
||||
from solving.solver_stage import fix_transliterations
|
||||
text = "Aufgefuehrt: Beitraege, Jahresvolatilitaet, Gesamtstueckzahl, zulaessige Kardinalitaet, vollstaendig."
|
||||
assert fix_transliterations(text) == ("Aufgeführt: Beiträge, Jahresvolatilität, Gesamtstückzahl, "
|
||||
"zulässige Kardinalität, vollständig.")
|
||||
|
||||
|
||||
def test_repair_leaves_correct_words_and_identifiers_alone():
|
||||
from solving.solver_stage import fix_transliterations
|
||||
richtig = "Die neue Dauer, Quelle, aktuell, Kostenkoeffizient, Bauer, streuen, blue."
|
||||
assert fix_transliterations(richtig) == richtig
|
||||
namen = "Bindend: Spaet_zu_Frueh_Verbot und KeinFruehNachSpaet sowie Kapazitaet_M1."
|
||||
assert fix_transliterations(namen) == namen
|
||||
|
|
|
|||
|
|
@ -753,3 +753,15 @@ def test_ordinary_programs_are_not_suspected():
|
|||
from solving.solver_stage import check_generated_data
|
||||
code = "import highspy\nh = highspy.Highs()\nDATA = {'kosten': [3, 2]}\n"
|
||||
assert check_generated_data(code, SolverCode(code=code, data_assumptions=[])) == []
|
||||
|
||||
|
||||
def test_scenario_failing_without_error_text_gets_a_reason():
|
||||
# beobachtet (Portfolio Nr. 2): Szenario ohne Lösung im Zeitlimit, Warnung endete mit leerem Grund
|
||||
from core.model_schema import Scenario
|
||||
from solving.solver_stage import verify_scenarios
|
||||
code = SZENARIO_CODE.replace(' basis = loesen(DATA)\n',
|
||||
' basis = loesen(DATA)\n if DATA["preis"] > 15:\n'
|
||||
' report("error", None, {})\n return\n')
|
||||
run = run_solver_code(code)
|
||||
befunde = verify_scenarios(run, code)
|
||||
assert any("doppelter Preis" in b and "ohne Fehlertext" in b for b in befunde)
|
||||
|
|
|
|||
Loading…
Reference in a new issue