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>
This commit is contained in:
Dieter Schlüter 2026-09-17 12:11:51 +02:00
commit 7459690bf5

448
tests/test_model_checks.py Normal file
View file

@ -0,0 +1,448 @@
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")