2026-09-18 18:15:22 +02:00
|
|
|
|
import pytest
|
|
|
|
|
|
|
refactor: Pipeline-Stufen in Ordner mit ihren Prompts
Im Wurzelverzeichnis liegt nur noch TextToModel.py (Ablauf und CLI).
Jede Stufe liegt mit Code und Prompt in einem eigenen Ordner: core,
understanding, modeling, checks, solving, reporting, data, benchmark;
Tests unter tests/, Aufgaben unter tasks/. understand_task,
text_to_model/revise_model und review_assumptions/needs_work/select_best
sind aus TextToModel.py zu ihren Prompts gewandert, inhaltlich
unveraendert. Prompt-Kennung des Benchmarks bleibt 93744636095a.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 12:11:51 +02:00
|
|
|
|
from checks.model_checks import check_spec, has_errors
|
|
|
|
|
|
from core.model_schema import OptimizationModelSpecification
|
2026-09-17 11:42:58 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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():
|
refactor: Pipeline-Stufen in Ordner mit ihren Prompts
Im Wurzelverzeichnis liegt nur noch TextToModel.py (Ablauf und CLI).
Jede Stufe liegt mit Code und Prompt in einem eigenen Ordner: core,
understanding, modeling, checks, solving, reporting, data, benchmark;
Tests unter tests/, Aufgaben unter tasks/. understand_task,
text_to_model/revise_model und review_assumptions/needs_work/select_best
sind aus TextToModel.py zu ihren Prompts gewandert, inhaltlich
unveraendert. Prompt-Kennung des Benchmarks bleibt 93744636095a.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 12:11:51 +02:00
|
|
|
|
from checks.model_checks import ALLOWED_STACKS
|
|
|
|
|
|
from core.model_schema import ProblemClass
|
2026-09-17 11:42:58 +02:00
|
|
|
|
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
|
refactor: Pipeline-Stufen in Ordner mit ihren Prompts
Im Wurzelverzeichnis liegt nur noch TextToModel.py (Ablauf und CLI).
Jede Stufe liegt mit Code und Prompt in einem eigenen Ordner: core,
understanding, modeling, checks, solving, reporting, data, benchmark;
Tests unter tests/, Aufgaben unter tasks/. understand_task,
text_to_model/revise_model und review_assumptions/needs_work/select_best
sind aus TextToModel.py zu ihren Prompts gewandert, inhaltlich
unveraendert. Prompt-Kennung des Benchmarks bleibt 93744636095a.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 12:11:51 +02:00
|
|
|
|
from checks.model_checks import _additive_terms
|
2026-09-17 11:42:58 +02:00
|
|
|
|
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():
|
refactor: Pipeline-Stufen in Ordner mit ihren Prompts
Im Wurzelverzeichnis liegt nur noch TextToModel.py (Ablauf und CLI).
Jede Stufe liegt mit Code und Prompt in einem eigenen Ordner: core,
understanding, modeling, checks, solving, reporting, data, benchmark;
Tests unter tests/, Aufgaben unter tasks/. understand_task,
text_to_model/revise_model und review_assumptions/needs_work/select_best
sind aus TextToModel.py zu ihren Prompts gewandert, inhaltlich
unveraendert. Prompt-Kennung des Benchmarks bleibt 93744636095a.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 12:11:51 +02:00
|
|
|
|
from checks.model_checks import unexplained_jargon
|
2026-09-17 11:42:58 +02:00
|
|
|
|
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
|
refactor: Pipeline-Stufen in Ordner mit ihren Prompts
Im Wurzelverzeichnis liegt nur noch TextToModel.py (Ablauf und CLI).
Jede Stufe liegt mit Code und Prompt in einem eigenen Ordner: core,
understanding, modeling, checks, solving, reporting, data, benchmark;
Tests unter tests/, Aufgaben unter tasks/. understand_task,
text_to_model/revise_model und review_assumptions/needs_work/select_best
sind aus TextToModel.py zu ihren Prompts gewandert, inhaltlich
unveraendert. Prompt-Kennung des Benchmarks bleibt 93744636095a.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 12:11:51 +02:00
|
|
|
|
from checks import model_checks
|
|
|
|
|
|
from solving import solver_stage
|
2026-09-17 11:42:58 +02:00
|
|
|
|
|
|
|
|
|
|
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()
|
refactor: Pipeline-Stufen in Ordner mit ihren Prompts
Im Wurzelverzeichnis liegt nur noch TextToModel.py (Ablauf und CLI).
Jede Stufe liegt mit Code und Prompt in einem eigenen Ordner: core,
understanding, modeling, checks, solving, reporting, data, benchmark;
Tests unter tests/, Aufgaben unter tasks/. understand_task,
text_to_model/revise_model und review_assumptions/needs_work/select_best
sind aus TextToModel.py zu ihren Prompts gewandert, inhaltlich
unveraendert. Prompt-Kennung des Benchmarks bleibt 93744636095a.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 12:11:51 +02:00
|
|
|
|
projekt = Path(model_checks.__file__).parent.parent
|
|
|
|
|
|
for datei in projekt.glob("*/*-Prompt.txt"):
|
2026-09-17 11:42:58 +02:00
|
|
|
|
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):
|
refactor: Pipeline-Stufen in Ordner mit ihren Prompts
Im Wurzelverzeichnis liegt nur noch TextToModel.py (Ablauf und CLI).
Jede Stufe liegt mit Code und Prompt in einem eigenen Ordner: core,
understanding, modeling, checks, solving, reporting, data, benchmark;
Tests unter tests/, Aufgaben unter tasks/. understand_task,
text_to_model/revise_model und review_assumptions/needs_work/select_best
sind aus TextToModel.py zu ihren Prompts gewandert, inhaltlich
unveraendert. Prompt-Kennung des Benchmarks bleibt 93744636095a.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 12:11:51 +02:00
|
|
|
|
from core.model_schema import TaskUnderstanding
|
2026-09-17 11:42:58 +02:00
|
|
|
|
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():
|
refactor: Pipeline-Stufen in Ordner mit ihren Prompts
Im Wurzelverzeichnis liegt nur noch TextToModel.py (Ablauf und CLI).
Jede Stufe liegt mit Code und Prompt in einem eigenen Ordner: core,
understanding, modeling, checks, solving, reporting, data, benchmark;
Tests unter tests/, Aufgaben unter tasks/. understand_task,
text_to_model/revise_model und review_assumptions/needs_work/select_best
sind aus TextToModel.py zu ihren Prompts gewandert, inhaltlich
unveraendert. Prompt-Kennung des Benchmarks bleibt 93744636095a.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 12:11:51 +02:00
|
|
|
|
from checks.model_checks import normalize_number
|
2026-09-17 11:42:58 +02:00
|
|
|
|
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():
|
refactor: Pipeline-Stufen in Ordner mit ihren Prompts
Im Wurzelverzeichnis liegt nur noch TextToModel.py (Ablauf und CLI).
Jede Stufe liegt mit Code und Prompt in einem eigenen Ordner: core,
understanding, modeling, checks, solving, reporting, data, benchmark;
Tests unter tests/, Aufgaben unter tasks/. understand_task,
text_to_model/revise_model und review_assumptions/needs_work/select_best
sind aus TextToModel.py zu ihren Prompts gewandert, inhaltlich
unveraendert. Prompt-Kennung des Benchmarks bleibt 93744636095a.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 12:11:51 +02:00
|
|
|
|
from checks.model_checks import numbers_in
|
2026-09-17 11:42:58 +02:00
|
|
|
|
assert numbers_in("Aktie A02, Variable x1, Schicht 3") == {"3"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_uncaptured_numbers_are_reported():
|
refactor: Pipeline-Stufen in Ordner mit ihren Prompts
Im Wurzelverzeichnis liegt nur noch TextToModel.py (Ablauf und CLI).
Jede Stufe liegt mit Code und Prompt in einem eigenen Ordner: core,
understanding, modeling, checks, solving, reporting, data, benchmark;
Tests unter tests/, Aufgaben unter tasks/. understand_task,
text_to_model/revise_model und review_assumptions/needs_work/select_best
sind aus TextToModel.py zu ihren Prompts gewandert, inhaltlich
unveraendert. Prompt-Kennung des Benchmarks bleibt 93744636095a.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 12:11:51 +02:00
|
|
|
|
from checks.model_checks import check_understanding
|
2026-09-17 11:42:58 +02:00
|
|
|
|
# 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
|
refactor: Pipeline-Stufen in Ordner mit ihren Prompts
Im Wurzelverzeichnis liegt nur noch TextToModel.py (Ablauf und CLI).
Jede Stufe liegt mit Code und Prompt in einem eigenen Ordner: core,
understanding, modeling, checks, solving, reporting, data, benchmark;
Tests unter tests/, Aufgaben unter tasks/. understand_task,
text_to_model/revise_model und review_assumptions/needs_work/select_best
sind aus TextToModel.py zu ihren Prompts gewandert, inhaltlich
unveraendert. Prompt-Kennung des Benchmarks bleibt 93744636095a.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 12:11:51 +02:00
|
|
|
|
from checks.model_checks import check_understanding
|
2026-09-17 11:42:58 +02:00
|
|
|
|
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():
|
refactor: Pipeline-Stufen in Ordner mit ihren Prompts
Im Wurzelverzeichnis liegt nur noch TextToModel.py (Ablauf und CLI).
Jede Stufe liegt mit Code und Prompt in einem eigenen Ordner: core,
understanding, modeling, checks, solving, reporting, data, benchmark;
Tests unter tests/, Aufgaben unter tasks/. understand_task,
text_to_model/revise_model und review_assumptions/needs_work/select_best
sind aus TextToModel.py zu ihren Prompts gewandert, inhaltlich
unveraendert. Prompt-Kennung des Benchmarks bleibt 93744636095a.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 12:11:51 +02:00
|
|
|
|
from checks.model_checks import check_understanding
|
2026-09-17 11:42:58 +02:00
|
|
|
|
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():
|
refactor: Pipeline-Stufen in Ordner mit ihren Prompts
Im Wurzelverzeichnis liegt nur noch TextToModel.py (Ablauf und CLI).
Jede Stufe liegt mit Code und Prompt in einem eigenen Ordner: core,
understanding, modeling, checks, solving, reporting, data, benchmark;
Tests unter tests/, Aufgaben unter tasks/. understand_task,
text_to_model/revise_model und review_assumptions/needs_work/select_best
sind aus TextToModel.py zu ihren Prompts gewandert, inhaltlich
unveraendert. Prompt-Kennung des Benchmarks bleibt 93744636095a.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 12:11:51 +02:00
|
|
|
|
from checks.model_checks import check_understanding
|
2026-09-17 11:42:58 +02:00
|
|
|
|
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():
|
refactor: Pipeline-Stufen in Ordner mit ihren Prompts
Im Wurzelverzeichnis liegt nur noch TextToModel.py (Ablauf und CLI).
Jede Stufe liegt mit Code und Prompt in einem eigenen Ordner: core,
understanding, modeling, checks, solving, reporting, data, benchmark;
Tests unter tests/, Aufgaben unter tasks/. understand_task,
text_to_model/revise_model und review_assumptions/needs_work/select_best
sind aus TextToModel.py zu ihren Prompts gewandert, inhaltlich
unveraendert. Prompt-Kennung des Benchmarks bleibt 93744636095a.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 12:11:51 +02:00
|
|
|
|
from checks.model_checks import check_understanding
|
2026-09-17 11:42:58 +02:00
|
|
|
|
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"
|
refactor: Pipeline-Stufen in Ordner mit ihren Prompts
Im Wurzelverzeichnis liegt nur noch TextToModel.py (Ablauf und CLI).
Jede Stufe liegt mit Code und Prompt in einem eigenen Ordner: core,
understanding, modeling, checks, solving, reporting, data, benchmark;
Tests unter tests/, Aufgaben unter tasks/. understand_task,
text_to_model/revise_model und review_assumptions/needs_work/select_best
sind aus TextToModel.py zu ihren Prompts gewandert, inhaltlich
unveraendert. Prompt-Kennung des Benchmarks bleibt 93744636095a.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 12:11:51 +02:00
|
|
|
|
from checks.model_checks import check_understanding
|
2026-09-17 11:42:58 +02:00
|
|
|
|
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
|
refactor: Pipeline-Stufen in Ordner mit ihren Prompts
Im Wurzelverzeichnis liegt nur noch TextToModel.py (Ablauf und CLI).
Jede Stufe liegt mit Code und Prompt in einem eigenen Ordner: core,
understanding, modeling, checks, solving, reporting, data, benchmark;
Tests unter tests/, Aufgaben unter tasks/. understand_task,
text_to_model/revise_model und review_assumptions/needs_work/select_best
sind aus TextToModel.py zu ihren Prompts gewandert, inhaltlich
unveraendert. Prompt-Kennung des Benchmarks bleibt 93744636095a.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 12:11:51 +02:00
|
|
|
|
from checks.model_checks import check_data_transfer
|
2026-09-17 11:42:58 +02:00
|
|
|
|
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
|
refactor: Pipeline-Stufen in Ordner mit ihren Prompts
Im Wurzelverzeichnis liegt nur noch TextToModel.py (Ablauf und CLI).
Jede Stufe liegt mit Code und Prompt in einem eigenen Ordner: core,
understanding, modeling, checks, solving, reporting, data, benchmark;
Tests unter tests/, Aufgaben unter tasks/. understand_task,
text_to_model/revise_model und review_assumptions/needs_work/select_best
sind aus TextToModel.py zu ihren Prompts gewandert, inhaltlich
unveraendert. Prompt-Kennung des Benchmarks bleibt 93744636095a.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 12:11:51 +02:00
|
|
|
|
from checks.model_checks import check_data_transfer
|
2026-09-17 11:42:58 +02:00
|
|
|
|
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():
|
refactor: Pipeline-Stufen in Ordner mit ihren Prompts
Im Wurzelverzeichnis liegt nur noch TextToModel.py (Ablauf und CLI).
Jede Stufe liegt mit Code und Prompt in einem eigenen Ordner: core,
understanding, modeling, checks, solving, reporting, data, benchmark;
Tests unter tests/, Aufgaben unter tasks/. understand_task,
text_to_model/revise_model und review_assumptions/needs_work/select_best
sind aus TextToModel.py zu ihren Prompts gewandert, inhaltlich
unveraendert. Prompt-Kennung des Benchmarks bleibt 93744636095a.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 12:11:51 +02:00
|
|
|
|
from checks.model_checks import check_data_transfer
|
2026-09-17 11:42:58 +02:00
|
|
|
|
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():
|
refactor: Pipeline-Stufen in Ordner mit ihren Prompts
Im Wurzelverzeichnis liegt nur noch TextToModel.py (Ablauf und CLI).
Jede Stufe liegt mit Code und Prompt in einem eigenen Ordner: core,
understanding, modeling, checks, solving, reporting, data, benchmark;
Tests unter tests/, Aufgaben unter tasks/. understand_task,
text_to_model/revise_model und review_assumptions/needs_work/select_best
sind aus TextToModel.py zu ihren Prompts gewandert, inhaltlich
unveraendert. Prompt-Kennung des Benchmarks bleibt 93744636095a.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 12:11:51 +02:00
|
|
|
|
from checks.model_checks import _objective_without_label
|
2026-09-17 11:42:58 +02:00
|
|
|
|
assert _objective_without_label("Z = 12 x").strip() == "12 x"
|
|
|
|
|
|
assert _objective_without_label("\\sum_{i=0}^{2} c_i x_i").startswith("\\sum")
|
2026-09-17 16:38:45 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
2026-09-17 16:50:27 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
2026-09-17 19:30:19 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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"}
|
2026-09-18 18:15:22 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@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
|