text_to_solver_program/tests/test_model_checks.py
dschlueter f101b63947 fix: Solver-Fehler gilt als unzuverlässig; Aufräumrunde ohne LLM
- Status error nach allen Reparaturen -> reliable=False („Solver-Fehler: …“, auch bei
  --resolve); vorher Exit-Code 0 ohne Warnkasten. Benchmark zählt einen Absturz nicht
  als ehrliches Scheitern.
- Report: „längster Weg“ bei maximize; Harness druckt „Gefundener Weg“.
- check_requested_method: Verneinung kurz davor im selben Satz („ohne/nicht/statt/
  anders als Dijkstra“) ist keine Forderung.
- Reparatur-Prompt enthält die Fehlertexte aller früheren Versuche.
- ruff (F, E9) als Test tests/test_lint.py, Konfiguration in pyproject.toml.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 18:15:22 +02:00

514 lines
26 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import pytest
from checks.model_checks import check_spec, has_errors
from core.model_schema import OptimizationModelSpecification
def make_spec(**overrides) -> OptimizationModelSpecification:
data = {
"problem_class": "Linear Programming (LP)",
"target_stack": "SciPy (scipy.optimize.linprog)",
"routing_rationale": "-",
"executive_summary": "-",
"indices": ["j \\in J (Produkte)"],
"parameters": [
{"symbol": "c_j", "dimension": "Vektor", "unit": "€/t", "description": "Kosten", "extracted_values": "[3, 2]"},
{"symbol": "b", "dimension": "Skalar", "unit": "t", "description": "Bedarf", "extracted_values": "4"},
],
"variables": [
{"name": "x_{j}", "var_type": "continuous", "domain": ">= 0", "description": "Menge"},
],
"objective": {"sense": "minimize", "latex_formula": "\\min \\sum_{j \\in J} c_j x_j", "description": "-"},
"constraints": [
{"identifier": "Bedarf", "latex_formula": "\\sum_{j \\in J} x_j \\ge b", "description": "-"},
],
"suggested_export_format": "Matrix-Dict",
}
data.update(overrides)
return OptimizationModelSpecification.model_validate(data)
def codes(spec):
return {issue.code for issue in check_spec(spec)}
def test_valid_spec_has_no_issues():
assert check_spec(make_spec()) == []
def test_binary_variable_in_lp_is_error():
spec = make_spec(variables=[{"name": "x_j", "var_type": "binary", "domain": "{0,1}", "description": "-"}])
assert {"LP_WITH_DISCRETE_VARS", "SCIPY_WITH_DISCRETE_VARS"} <= codes(spec)
assert has_errors(check_spec(spec))
def test_stack_mismatch_is_error():
spec = make_spec(problem_class="Quadratic Programming (QP)")
assert "STACK_MISMATCH" in codes(spec)
def test_continuous_variable_with_cpsat_is_error():
spec = make_spec(problem_class="Constraint Programming (CP)", target_stack="Google OR-Tools (CP-SAT)")
assert "CPSAT_WITH_CONTINUOUS_VARS" in codes(spec)
def test_quadratic_term_in_lp_is_warning():
spec = make_spec(objective={"sense": "minimize", "latex_formula": "\\min \\sum_j c_j x_j^2", "description": "-"})
issues = check_spec(spec)
assert "NONLINEAR_TERM" in codes(spec)
assert not has_errors(issues)
def test_undeclared_symbol_is_warning():
spec = make_spec(constraints=[
{"identifier": "Kapazitaet", "latex_formula": "\\sum_{j} a_j x_j \\le K", "description": "-"},
])
messages = [i.message for i in check_spec(spec) if i.code == "UNDECLARED_SYMBOL"]
assert any("'a'" in m for m in messages)
assert any("'K'" in m for m in messages)
def test_superscript_and_escaped_underscore_names_are_recognized():
spec = make_spec(
parameters=[
{"symbol": "C^{prod}_{j}", "dimension": "Vektor", "unit": "€", "description": "-", "extracted_values": "1"},
{"symbol": "min\\_staff_1", "dimension": "Skalar", "unit": "Personen", "description": "-", "extracted_values": "3"},
{"symbol": "\\Sigma", "dimension": "Matrix", "unit": "dimensionslos", "description": "-", "extracted_values": "I"},
],
objective={"sense": "minimize", "latex_formula": "\\min \\sum_{j} C^{prod}_{j} x_{j}", "description": "-"},
constraints=[
{"identifier": "A", "latex_formula": "\\sum_{j} x_j \\ge min\\_staff_1 \\quad \\forall j \\in J", "description": "-"},
{"identifier": "B", "latex_formula": "x^T \\Sigma x \\le 1 \\text{ (Risiko)}", "description": "-"},
],
problem_class="Quadratic Programming (QP)",
target_stack="CVXPY",
)
assert "UNDECLARED_SYMBOL" not in codes(spec)
def test_missing_parameter_values_is_warning():
spec = make_spec(parameters=[{"symbol": "c_j", "dimension": "Vektor", "unit": "€", "description": "-"}])
assert "MISSING_DATA" in codes(spec)
def test_no_constraints_is_error():
assert "NO_CONSTRAINTS" in codes(make_spec(constraints=[]))
def test_indices_with_inline_math_delimiters_are_declared():
# beobachtet: "Mitarbeitende: \(p \in P\)" führte zu Fehlalarmen für p, P, d, D, Mo, Di …
spec = make_spec(
indices=["Mitarbeitende: \\(p \\in P = \\{A,B\\}\\)", "Tage: \\(d \\in D = \\{Mo,Di\\}\\) (Folgetag d+1)"],
variables=[{"name": "x_{p,d}", "var_type": "continuous", "domain": ">= 0", "description": "-"}],
objective={"sense": "minimize", "latex_formula": "\\min \\sum_{p \\in P} \\sum_{d \\in D} x_{p,d}", "description": "-"},
constraints=[{"identifier": "Folgetag", "latex_formula": "x_{p,Mo} + x_{p,Di} \\le 1 \\quad \\forall p \\in P", "description": "-"}],
)
assert "UNDECLARED_SYMBOL" not in codes(spec)
def test_miqp_with_scip_is_consistent():
spec = make_spec(
problem_class="Mixed-Integer Quadratic Programming (MIQP)", target_stack="SCIP (pyscipopt)",
variables=[{"name": "w_j", "var_type": "continuous", "domain": "[0,1]", "description": "-"},
{"name": "y_j", "var_type": "binary", "domain": "{0,1}", "description": "-"}],
objective={"sense": "maximize", "latex_formula": "\\max \\sum_j c_j w_j - \\sum_j c_j w_j^2 - b \\sum_j y_j", "description": "-"},
constraints=[{"identifier": "Kopplung", "latex_formula": "w_j \\le y_j", "description": "-"}],
)
assert check_spec(spec) == [] # insbesondere kein NONLINEAR_TERM, kein Routing-Fehler
def test_quadratic_with_binary_on_cvxpy_is_error():
spec = make_spec(
problem_class="Quadratic Programming (QP)", target_stack="CVXPY",
variables=[{"name": "y_j", "var_type": "binary", "domain": "{0,1}", "description": "-"}],
)
assert "CVXPY_WITH_DISCRETE_VARS" in codes(spec)
assert has_errors(check_spec(spec))
def test_miqp_without_discrete_vars_is_warning():
spec = make_spec(problem_class="Mixed-Integer Quadratic Programming (MIQP)", target_stack="SCIP (pyscipopt)")
assert "MIQP_WITHOUT_DISCRETE_VARS" in codes(spec)
assert not has_errors(check_spec(spec))
def test_every_problem_class_has_allowed_stacks():
from checks.model_checks import ALLOWED_STACKS
from core.model_schema import ProblemClass
assert set(ALLOWED_STACKS) == set(ProblemClass)
def test_nlp_with_ipopt_is_consistent():
spec = make_spec(problem_class="Nonlinear Programming (NLP)", target_stack="Ipopt (cyipopt)")
assert not has_errors(check_spec(spec))
def test_ipopt_with_integer_vars_is_error():
spec = make_spec(problem_class="Nonlinear Programming (NLP)", target_stack="Ipopt (cyipopt)",
variables=[{"name": "n_j", "var_type": "integer", "domain": ">= 0", "description": "-"}])
assert "IPOPT_WITH_DISCRETE_VARS" in codes(spec)
# --- Einheiten der Zielfunktion (ParameterDefinition.unit, ohne LLM) ---
def portfolio_spec(risk_unit: str, objective: str):
"""Portfolio: Rendite und Kosten in Euro, Risiko wahlweise dimensionslos oder in Euro."""
return make_spec(
problem_class="Quadratic Programming (QP)", target_stack="CVXPY",
objective={"sense": "maximize", "latex_formula": objective, "description": "-"},
parameters=[
{"symbol": "\\mu_i", "dimension": "Vektor", "unit": "€", "description": "Ertrag", "extracted_values": "[1]"},
{"symbol": "\\lambda", "dimension": "Skalar", "unit": "dimensionslos", "description": "Risikoaversion",
"extracted_values": "0.5"},
{"symbol": "\\Sigma", "dimension": "Matrix", "unit": risk_unit, "description": "Kovarianz",
"extracted_values": "[[1]]"},
],
variables=[{"name": "w_i", "var_type": "continuous", "domain": "[0, 1]", "description": "Anteil"}],
constraints=[{"identifier": "Budget", "latex_formula": "\\sum_i w_i \\le 1", "description": "-"}],
)
def test_dimensionless_risk_next_to_euro_amounts_is_error():
# beobachtet: Risiko als Summe über Anteile ist um den Faktor Kapital zu klein und damit wirkungslos
spec = portfolio_spec("dimensionslos", "\\max \\sum_i \\mu_i w_i - \\lambda w^T \\Sigma w")
assert "MIXED_OBJECTIVE_UNITS" in codes(spec)
assert has_errors(check_spec(spec))
def test_risk_scaled_to_euro_is_accepted():
spec = portfolio_spec("€", "\\max \\sum_i \\mu_i w_i - \\lambda w^T \\Sigma w")
assert "MIXED_OBJECTIVE_UNITS" not in codes(spec)
def test_single_term_objective_is_never_mixed():
spec = portfolio_spec("dimensionslos", "\\min w^T \\Sigma w")
assert "MIXED_OBJECTIVE_UNITS" not in codes(spec)
def test_index_range_is_not_split_into_terms():
# \sum_{i \in I} enthält kein Minus, wohl aber Klammern – der Term darf nicht zerschnitten werden
from checks.model_checks import _additive_terms
assert len(_additive_terms("\\min \\sum_{j \\in J} c_j x_j")) == 1
assert len(_additive_terms("\\max \\sum_i \\mu_i w_i - \\lambda w^T \\Sigma w")) == 2
assert len(_additive_terms("\\min z = a x + b y")) == 2 # linke Seite zählt nicht mit
def test_unknown_units_produce_no_finding():
# Stunden neben Stück: kein Fehlalarm, geprüft werden nur Geld gegen dimensionslos
spec = make_spec(parameters=[
{"symbol": "c_j", "dimension": "Vektor", "unit": "h", "description": "Zeit", "extracted_values": "[3]"},
{"symbol": "b", "dimension": "Skalar", "unit": "Stück", "description": "Bedarf", "extracted_values": "4"},
], objective={"sense": "minimize", "latex_formula": "\\min \\sum_j c_j x_j + b", "description": "-"})
assert "MIXED_OBJECTIVE_UNITS" not in codes(spec)
def test_objective_label_is_not_an_undeclared_symbol():
# beobachtet: Zielfunktion "Z = 12\,x" meldete Z als undeklariertes Symbol
spec = make_spec(objective={"sense": "maximize", "latex_formula": "Z = 12\\,x_j", "description": "-"})
assert "UNDECLARED_SYMBOL" not in codes(spec)
def test_symbol_right_of_the_equals_sign_is_still_checked():
spec = make_spec(objective={"sense": "maximize", "latex_formula": "Z = q \\cdot x_j", "description": "-"})
assert "UNDECLARED_SYMBOL" in codes(spec)
def test_number_sets_are_not_symbols():
# beobachtet: Restriktion "x \in \mathbb{Z}" meldete Z als undeklariertes Symbol
spec = make_spec(constraints=[
{"identifier": "ganzzahligkeit", "latex_formula": "x_j \\in \\mathbb{Z}", "description": "-"},
{"identifier": "vorzeichen", "latex_formula": "x_j \\in \\mathbb{R}_{\\ge 0}", "description": "-"},
])
assert "UNDECLARED_SYMBOL" not in codes(spec)
def test_implicit_products_are_not_undeclared():
# beobachtet: "\sin(cx)" meinte c*x, der Check las 'cx' als eigenen Namen (ebenso 'xy')
spec = make_spec(
parameters=[{"symbol": "c", "dimension": "Skalar", "unit": "1/K", "description": "-", "extracted_values": "3"},
{"symbol": "q", "dimension": "Skalar", "unit": "dimensionslos", "description": "-", "extracted_values": "0.5"}],
variables=[{"name": "x", "var_type": "continuous", "domain": "[0, 4]", "description": "-"},
{"name": "y", "var_type": "continuous", "domain": "[0, 4]", "description": "-"}],
objective={"sense": "maximize", "latex_formula": "\\max x\\sin(cx) + y\\sin(cy) + q\\,xy", "description": "-"},
constraints=[{"identifier": "Sicherheit", "latex_formula": "x + y \\le 6", "description": "-"}],
)
assert "UNDECLARED_SYMBOL" not in codes(spec)
def test_real_names_are_still_reported():
# Gegenprobe: 'Kapazitaet' besteht nicht nur aus deklarierten Einzelsymbolen
spec = make_spec(constraints=[
{"identifier": "K1", "latex_formula": "x_j \\le Kapazitaet", "description": "-"},
])
assert "UNDECLARED_SYMBOL" in codes(spec)
# --- Fachbegriffe müssen erklärt sein (Block ERKLÄREN im Routing-Prompt) ---
def test_unexplained_jargon_is_reported():
# beobachtet: Restriktion "Flusserhaltung_Jahr1", Zusammenfassung erwähnt "Fluss" nirgends
spec = make_spec(
executive_summary="Der Bauer entscheidet, wann er die Maschine ersetzt.",
constraints=[{"identifier": "Flusserhaltung_Jahr1", "latex_formula": "\\sum_j x_j = 1", "description": "-"},
{"identifier": "Quelle_Startkauf", "latex_formula": "\\sum_j x_j = 1", "description": "-"}],
)
meldungen = [i.message for i in check_spec(spec) if i.code == "UNEXPLAINED_JARGON"]
assert meldungen and "Fluss" in meldungen[0]
assert not has_errors(check_spec(spec)) # Warnung, kein Fehler: sonst gälte ein richtiges Ergebnis als unzuverlässig
def test_explained_jargon_is_accepted():
spec = make_spec(
executive_summary="Ein Bogen i → j bedeutet Kauf am Ende von Jahr i. Die Flusserhaltung sorgt dafür, "
"dass die Kette nicht abreißt: zu jedem Knoten gehört genau ein Nachfolger.",
constraints=[{"identifier": "Flusserhaltung_Jahr1", "latex_formula": "\\sum_j x_j = 1", "description": "-"}],
indices=["Knoten j \\in J (Jahresenden)"],
)
assert "UNEXPLAINED_JARGON" not in codes(spec)
def test_everyday_words_are_not_jargon():
from checks.model_checks import unexplained_jargon
assert unexplained_jargon("Menge je Produkt und Lager, Kosten in Euro", "Eine schlichte Zusammenfassung.") == []
# --- Im Aufgabentext verlangtes Verfahren (check_requested_method) ---
DIJKSTRA_AUFGABE = "a) Formulieren Sie das Problem als Kürzeste-Wege-Problem!\nb) Lösen Sie es mit dem Dijkstra-Algorithmus!"
def test_requested_dijkstra_with_flow_model_is_error():
# beobachtet: Fall graph_ersatz_dijkstra wurde als MILP-Flussmodell geloest - richtig gerechnet, Aufgabe verfehlt
spec = make_spec(problem_class="Mixed-Integer Linear Programming (MILP)", target_stack="HiGHS",
variables=[{"name": "x_{ij}", "var_type": "binary", "domain": "{0,1}", "description": "-"}])
issues = check_spec(spec, DIJKSTRA_AUFGABE)
assert "REQUESTED_METHOD_IGNORED" in {i.code for i in issues}
assert has_errors(issues) # loest eine Korrekturrunde aus, die neu routen kann
def test_requested_dijkstra_with_networkx_is_accepted():
spec = make_spec(problem_class="Kürzeste Wege im Graphen (Shortest Path)", target_stack="NetworkX (Dijkstra)",
variables=[{"name": "x_{ij}", "var_type": "binary", "domain": "{0,1}", "description": "-"}])
assert "REQUESTED_METHOD_IGNORED" not in {i.code for i in check_spec(spec, DIJKSTRA_AUFGABE)}
def test_method_check_stays_quiet_without_a_named_method():
# ohne Aufgabentext und ohne genanntes Verfahren darf die Pruefung nichts melden
spec = make_spec()
assert "REQUESTED_METHOD_IGNORED" not in codes(spec)
assert "REQUESTED_METHOD_IGNORED" not in {i.code for i in check_spec(spec, "Minimiere die Kosten.")}
def test_every_checker_marker_in_the_prompts_exists():
"""Jede [→ X]-Kennung in den Prompts muss einem echten Check-Code oder einer Prüffunktion entsprechen.
Verhindert, dass eine Regel eine Prüfung behauptet, die es nicht (mehr) gibt.
"""
import re
from pathlib import Path
from checks import model_checks
from solving import solver_stage
quelle = Path(model_checks.__file__).read_text() + Path(solver_stage.__file__).read_text()
bekannt = set(re.findall(r'code="([A-Z_]+)"', quelle)) | set(re.findall(r"^def (\w+)", quelle, re.M))
marker = set()
projekt = Path(model_checks.__file__).parent.parent
for datei in projekt.glob("*/*-Prompt.txt"):
for treffer in re.findall(r"\[→ ([^\]]+)\]", datei.read_text(encoding="utf-8")):
marker |= {teil.strip() for teil in treffer.split(",") if teil.strip() != "…"} # Legende ausnehmen
assert marker, "keine Prüfer-Kennungen in den Prompts gefunden"
assert marker <= bekannt, f"Kennung ohne zugehörige Prüfung: {sorted(marker - bekannt)}"
# --- Verstehensstufe: Zahlen und Teilfragen des Aufgabentexts ---
def make_understanding(**overrides):
from core.model_schema import TaskUnderstanding
daten = {"is_optimization_task": True, "reason": "-", "facts": [], "ignored_numbers": [],
"sub_questions": [{"id": "1", "quote": "-", "kind": "rechnen", "needs_solver": True}]}
daten.update(overrides)
return TaskUnderstanding.model_validate(daten)
def test_numbers_are_normalised_once():
from checks.model_checks import normalize_number
assert normalize_number("1.234,56") == "1234.56" # deutsch: Punkt trennt Tausender
assert normalize_number("3.8") == "3.8" # Aufgabennummer, kein Tausendertrenner
assert normalize_number("1.234") == "1234"
assert normalize_number("0,05") == "0.05"
assert normalize_number("100.000") == "100000"
def test_numbers_inside_words_are_not_numbers():
from checks.model_checks import numbers_in
assert numbers_in("Aktie A02, Variable x1, Schicht 3") == {"3"}
def test_uncaptured_numbers_are_reported():
from checks.model_checks import check_understanding
# beobachtet: 0 von 14 Zahlen des Aufgabentexts landeten in der Spezifikation
aufgabe = "Der Deckungsbeitrag ist 12 € je Stück. Mindestens 100 Stück sind abzusetzen."
codes = {i.code for i in check_understanding(make_understanding(), aufgabe)}
assert "UNCAPTURED_NUMBERS" in codes
def test_numbers_quoted_in_a_fact_count_as_captured():
# eine Tabellenzeile als Zitat deckt alle Zahlen dieser Zeile ab
from checks.model_checks import check_understanding
aufgabe = "| A01 | 6,9 % | 0,68 % | 2,04 % |"
verstanden = make_understanding(facts=[{
"value": "6,9 %", "unit": "%", "meaning": "Rendite von A01", "quote": "| A01 | 6,9 % | 0,68 % | 2,04 % |"}])
assert "UNCAPTURED_NUMBERS" not in {i.code for i in check_understanding(verstanden, aufgabe)}
def test_explicitly_ignored_numbers_count_as_captured():
from checks.model_checks import check_understanding
aufgabe = "## Aufgabe 2.7: Der Preis ist 8 €."
verstanden = make_understanding(
facts=[{"value": "8", "unit": "€", "meaning": "Preis", "quote": "Der Preis ist 8 €."}],
ignored_numbers=[{"value": "2.7", "reason": "Aufgabennummer"}])
assert "UNCAPTURED_NUMBERS" not in {i.code for i in check_understanding(verstanden, aufgabe)}
def test_missing_sub_questions_are_reported():
from checks.model_checks import check_understanding
aufgabe = "- a) Formulieren Sie das Problem!\n- b) Lösen Sie es mit Dijkstra!"
verstanden = make_understanding(sub_questions=[{"id": "a", "quote": "-", "kind": "formulieren", "needs_solver": False}])
meldungen = [i.message for i in check_understanding(verstanden, aufgabe) if i.code == "MISSING_SUB_QUESTIONS"]
assert meldungen and "b" in meldungen[0]
def test_complete_sub_questions_are_accepted():
from checks.model_checks import check_understanding
aufgabe = "- a) Formulieren Sie das Problem!\n- b) Lösen Sie es mit Dijkstra!"
verstanden = make_understanding(sub_questions=[
{"id": "a", "quote": "-", "kind": "formulieren", "needs_solver": False},
{"id": "b", "quote": "-", "kind": "rechnen", "needs_solver": True}])
assert "MISSING_SUB_QUESTIONS" not in {i.code for i in check_understanding(verstanden, aufgabe)}
def test_split_sub_questions_cover_their_parent():
# beobachtet an "Portfolio Nr. 2": Teil 2 als 2a/2b erfasst, gemeldet wurde "es fehlen: 2"
from checks.model_checks import check_understanding
aufgabe = "* **Teil 1: Daten**\n* **Teil 2: Optimierung**\n* **Teil 3: Evaluation**"
verstanden = make_understanding(sub_questions=[
{"id": i, "quote": "-", "kind": "rechnen", "needs_solver": True} for i in ("Teil 1", "Teil 2a", "Teil 2b", "Teil 3")])
assert "MISSING_SUB_QUESTIONS" not in {i.code for i in check_understanding(verstanden, aufgabe)}
# "21" ist eine andere Teilfrage, keine Zerlegung von "2"
verstanden = make_understanding(sub_questions=[
{"id": i, "quote": "-", "kind": "rechnen", "needs_solver": True} for i in ("1", "21", "3")])
assert "MISSING_SUB_QUESTIONS" in {i.code for i in check_understanding(verstanden, aufgabe)}
def test_captured_facts_must_reach_the_specification():
# beobachtet: Verstehensstufe erfasste die ganze Kostenmatrix, extracted_values blieb None
from checks.model_checks import check_data_transfer
verstanden = make_understanding(facts=[
{"value": "8", "unit": "€", "meaning": "Kosten 0->1", "quote": "| 0 | 8 | 18 | 31 |"}])
ohne = make_spec(parameters=[{"symbol": "c_{ij}", "dimension": "Matrix", "unit": "€", "description": "Kosten"}])
meldungen = [i.message for i in check_data_transfer(ohne, verstanden)]
assert meldungen and "8" in meldungen[0]
mit = make_spec(parameters=[{"symbol": "c_{ij}", "dimension": "Matrix", "unit": "€", "description": "Kosten",
"extracted_values": "8, 18, 31"}])
assert check_data_transfer(mit, verstanden) == []
def test_data_transfer_tolerates_partially_transferred_values():
# Indexwerte und Zeilenbeschriftungen muessen nicht in extracted_values stehen - kein Fehlalarm
from checks.model_checks import check_data_transfer
verstanden = make_understanding(facts=[
{"value": "0,1,2", "unit": "Jahr", "meaning": "moegliche Kaufzeitpunkte", "quote": "i = 0,1,2"},
{"value": "8", "unit": "€", "meaning": "Kosten", "quote": "| 0 | 8 |"}])
spec = make_spec(parameters=[{"symbol": "c", "dimension": "Matrix", "unit": "€", "description": "Kosten",
"extracted_values": "8"}])
assert check_data_transfer(spec, verstanden) == []
def test_data_transfer_check_is_quiet_without_understanding():
from checks.model_checks import check_data_transfer
assert check_data_transfer(make_spec(), None) == []
def test_sum_limits_are_not_an_objective_label():
# beobachtet: "\sum_{i=0}^{2}..." wurde am "=" der Summengrenze zerschnitten -> NONLINEAR_TERM
spec = make_spec(
problem_class="Kürzeste Wege im Graphen (Shortest Path)", target_stack="NetworkX (Dijkstra)",
variables=[{"name": "x_{i,j}", "var_type": "binary", "domain": "{0,1}", "description": "-"}],
parameters=[{"symbol": "c_{i,j}", "dimension": "Matrix", "unit": "€", "description": "Kosten",
"extracted_values": "8"}],
objective={"sense": "minimize", "description": "-",
"latex_formula": "\\sum_{i=0}^{2}\\sum_{j=i+1}^{3} c_{i,j}\\, x_{i,j}"},
constraints=[{"identifier": "Weg", "latex_formula": "\\sum_j x_{0,j} = 1", "description": "-"}],
)
assert "NONLINEAR_TERM" not in codes(spec)
def test_objective_label_is_still_stripped():
from checks.model_checks import _objective_without_label
assert _objective_without_label("Z = 12 x").strip() == "12 x"
assert _objective_without_label("\\sum_{i=0}^{2} c_i x_i").startswith("\\sum")
def test_everyday_words_neither_trigger_nor_explain_jargon():
from checks.model_checks import unexplained_jargon
# beobachtet: „beeinflusst“ in der Zusammenfassung galt als Erklärung von „Flusserhaltung“
assert unexplained_jargon("Flusserhaltung", "Die Bestellmenge beeinflusst die Lagerkosten.") == ["Fluss"]
assert unexplained_jargon("Der Einfluss des Preises", "Zusammenfassung") == []
assert unexplained_jargon("Flusserhaltung", "Der Fluss durch einen Knoten bleibt erhalten.") == []
def test_unambiguous_english_numbers_are_read_correctly():
from checks.model_checks import normalize_number
assert normalize_number("1,234.56") == "1234.56" # vorher 1.23456
assert normalize_number("12,345,678") == "12345678"
assert normalize_number("1,500") == "1.5" # mehrdeutig: bleibt deutsch gelesen
assert normalize_number("1.234,56") == "1234.56" # deutsch unverändert
def test_english_sub_question_markers_are_recognised():
from checks.model_checks import QUESTION_PATTERN
for zeile in ("Part 2: Solve the model", "* **Question 1: How many units**", "- Task 3: Compare"):
assert QUESTION_PATTERN.search(zeile), zeile
assert not QUESTION_PATTERN.search("In part 2 of the text we see") # Fließtext bleibt Fließtext
def test_bold_marker_with_colon_inside_is_recognised():
# vorher nur "**Teil 1: Daten**", nicht "**Teil 1:** Daten" (Doppelpunkt vor dem schließenden **)
from checks.model_checks import QUESTION_PATTERN
for zeile in ("**Teil 1:** Daten laden", "* **Question 2:** Solve", "**a)** Graph zeichnen"):
assert QUESTION_PATTERN.search(zeile), zeile
def test_leading_zero_is_never_a_thousands_separator():
from checks.model_checks import normalize_number
assert normalize_number("0.040") == "0.04" # vorher 40 (portfolio_qp)
assert normalize_number("1.234") == "1234" # deutsche Tausender unverändert
def test_latex_decimal_comma_is_one_number():
from checks.model_checks import numbers_in
assert numbers_in(r"\mu_A=0{,}08, \sigma=0{,}040") == {"0.08", "0.04"}
def test_heading_markers_win_over_numbered_lists():
from checks.model_checks import question_ids_in
text = ("## Restriktionen\n\n1. **Kassenbestand** je Periode\n\n2. **Lagerbilanz** je Produkt\n\n"
"### Teil 1: Formulierung\nKlassifizieren Sie das Problem.\n\n### Teil 2: Lösung\nLösen Sie das Modell.")
assert question_ids_in(text) == {"1", "2"}
assert question_ids_in("1. Berechnen Sie x.\n2. Deuten Sie y.\n3. Vergleichen Sie.") == {"1", "2", "3"}
@pytest.mark.parametrize("text, verlangt", [
("Lösen Sie das Problem ohne den Dijkstra-Algorithmus, als Flussmodell.", False),
("Anders als beim Dijkstra-Verfahren sind hier negative Kosten möglich – nicht mit Dijkstra rechnen.", False),
("Verwenden Sie statt Dijkstra ein lineares Programm.", False),
("b) Lösen Sie es mit dem Dijkstra-Algorithmus!", True),
("Lösen Sie mit Dijkstra. Ohne Rechenweg gibt es keine Punkte.", True), # Verneinung im nächsten Satz
("Nicht jede Kante ist befahrbar. Bestimmen Sie den Weg mit Dijkstra.", True),
])
def test_negated_method_mention_is_not_a_request(text, verlangt):
spec = make_spec(problem_class="Mixed-Integer Linear Programming (MILP)", target_stack="HiGHS",
variables=[{"name": "x_{ij}", "var_type": "binary", "domain": "{0,1}", "description": "-"}])
gemeldet = "REQUESTED_METHOD_IGNORED" in {i.code for i in check_spec(spec, text)}
assert gemeldet is verlangt