Erste öffentliche Fassung: Math Model Generator & Solver
Übersetzt natürlichsprachige Optimierungsaufgaben (Deutsch) per LLM in eine geprüfte Modellspezifikation, erzeugt Solver-Code und führt ihn in einer bubblewrap-Sandbox aus. Deterministische Prüfungen ohne LLM, Report als Markdown/HTML, Benchmark mit 19 verifizierten Fällen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
commit
b74445b8d6
1 changed files with 7655 additions and 0 deletions
142
test_script_export.py
Normal file
142
test_script_export.py
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
import ast
|
||||
import subprocess
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
import pytest
|
||||
|
||||
from model_schema import PipelineResult, SolverCode, SolverRun, SolverStatus
|
||||
from report import build_document, to_html
|
||||
from script_export import build_solver_script
|
||||
from solver_stage import CodeRejected, assemble_script, run_solver_code, validate_structure
|
||||
from test_model_checks import make_spec
|
||||
from test_review import make_review
|
||||
|
||||
# min 3x + 2y s.t. x + y >= 4, 0 <= x, y <= 3 -> Zielwert 9
|
||||
STRUCTURED_CODE = '''
|
||||
from scipy.optimize import linprog
|
||||
|
||||
# Kosten je Tonne Rohstoff X und Y [Tsd. €]
|
||||
COSTS = [3, 2]
|
||||
|
||||
|
||||
def solve_model():
|
||||
"""Baut das LP auf und löst es."""
|
||||
# Bedarf: x + y >= 4 -> -x - y <= -4
|
||||
return linprog(COSTS, A_ub=[[-1, -1]], b_ub=[-4], bounds=[(0, 3), (0, 3)], method="highs")
|
||||
|
||||
|
||||
def main():
|
||||
"""Löst das Modell und gibt das Ergebnis aus."""
|
||||
res = solve_model()
|
||||
x, y = float(res.x[0]), float(res.x[1])
|
||||
report(
|
||||
"optimal" if res.status == 0 else "error", res.fun, {"x": x, "y": y},
|
||||
constraints=[{"name": "Bedarf", "lhs": x + y, "sense": ">=", "rhs": 4}],
|
||||
variables=[{"name": "x", "value": x, "var_type": "continuous", "lb": 0, "ub": 3},
|
||||
{"name": "y", "value": y, "var_type": "continuous", "lb": 0, "ub": 3}],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
'''
|
||||
|
||||
|
||||
def make_result(task="Minimiere die Kosten für mindestens 4 t Rohstoff.", code=STRUCTURED_CODE):
|
||||
return PipelineResult(
|
||||
task=task,
|
||||
specification=make_spec(),
|
||||
review=make_review(),
|
||||
revisions=0,
|
||||
solver_code=SolverCode(code=code, data_assumptions=["Lieferfrist: keine (nicht im Text)"]),
|
||||
solver_run=SolverRun(status=SolverStatus.OPTIMAL, objective_value=9.0),
|
||||
)
|
||||
|
||||
|
||||
def test_script_layout_order():
|
||||
script = build_solver_script(make_result())
|
||||
assert script.startswith('#!/usr/bin/env python3\nr"""\n')
|
||||
tree = ast.parse(script)
|
||||
docstring = ast.get_docstring(tree)
|
||||
positions = [docstring.index(h) for h in ("AUFGABE", "MATHEMATISCHES MODELL", "ALGORITHMUS")]
|
||||
assert positions == sorted(positions)
|
||||
assert "Minimiere die Kosten" in docstring
|
||||
assert "\\sum_{j \\in J} x_j \\ge b" in docstring # Restriktion in LaTeX
|
||||
assert "Branch-and-Cut" not in docstring # SciPy-Stack, nicht HiGHS-Text
|
||||
assert "linprog" in docstring
|
||||
assert "Lieferfrist" in docstring # angenommene Daten
|
||||
# Imports vor report(), report() vor dem abschließenden main-Guard
|
||||
names = [type(n).__name__ + ":" + getattr(n, "name", "") for n in tree.body]
|
||||
guard_index = len(names) - 1
|
||||
assert names.index("ImportFrom:") < names.index("FunctionDef:main") < names.index("FunctionDef:report") < guard_index
|
||||
assert isinstance(tree.body[guard_index], ast.If) and script.rstrip().endswith("main()")
|
||||
|
||||
|
||||
def test_script_is_standalone_runnable(tmp_path):
|
||||
path = tmp_path / "solve.py"
|
||||
path.write_text(build_solver_script(make_result()), encoding="utf-8")
|
||||
proc = subprocess.run([sys.executable, str(path)], capture_output=True, text=True, timeout=60)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
assert "Status: optimal" in proc.stdout
|
||||
assert "Zielwert: 9.0" in proc.stdout
|
||||
assert proc.stdout.strip().splitlines()[-1].startswith("RESULT_JSON:")
|
||||
|
||||
|
||||
def test_latex_backslashes_and_quotes_do_not_break_script():
|
||||
task = 'Text mit \\sum, \\( x \\), """ Anführungszeichen """ und Backslash am Ende \\'
|
||||
script = build_solver_script(make_result(task=task))
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("error") # SyntaxWarning (ungültige Escapes) wäre ein Fehler
|
||||
compile(script, "solve.py", "exec")
|
||||
|
||||
|
||||
def test_run_solver_code_with_structured_code():
|
||||
run = run_solver_code(STRUCTURED_CODE)
|
||||
assert run.status == SolverStatus.OPTIMAL
|
||||
assert run.objective_value == pytest.approx(9)
|
||||
|
||||
|
||||
def test_assemble_without_guard_prepends_helpers():
|
||||
script = assemble_script("x = 1")
|
||||
assert "def report(" in script and "_apply_data_override(globals())" in script
|
||||
assert script.index("def report(") < script.index("x = 1")
|
||||
|
||||
|
||||
def test_valid_structure_passes():
|
||||
validate_structure(STRUCTURED_CODE)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("code, reason", [
|
||||
("def solve():\n pass\n\nif __name__ == '__main__':\n solve()\n", "main()"),
|
||||
("def main():\n pass\n\nmain()\n", "__main__"),
|
||||
("def main():\n pass\n\nif __name__ == '__main__':\n main()\n\nx = 1\n", "__main__"),
|
||||
])
|
||||
def test_invalid_structure_is_rejected(code, reason):
|
||||
with pytest.raises(CodeRejected, match=reason.replace("(", r"\(").replace(")", r"\)")):
|
||||
validate_structure(code)
|
||||
|
||||
|
||||
def test_html_report_has_copy_button_with_full_script():
|
||||
page = to_html(build_document(make_result()))
|
||||
assert page.count('class="copy-btn"') == 1
|
||||
assert "navigator.clipboard" in page and "execCommand('copy')" in page
|
||||
assert "#!/usr/bin/env python3" in page
|
||||
assert "if __name__ == "__main__":" in page
|
||||
|
||||
|
||||
def test_inline_math_delimiters_are_removed_from_docstring():
|
||||
spec = make_spec(indices=["Tage: \\(d \\in D = \\{Mo,Di\\}\\)"])
|
||||
result = make_result()
|
||||
result.specification = spec
|
||||
docstring = ast.get_docstring(ast.parse(build_solver_script(result)))
|
||||
assert "Tage: d \\in D = \\{Mo,Di\\}" in docstring
|
||||
assert "\\(" not in docstring and "\\)" not in docstring
|
||||
|
||||
|
||||
def test_every_stack_and_class_has_algorithm_text():
|
||||
# Synchron-Regel: neuer TargetStack/ProblemClass braucht Texte in script_export
|
||||
from model_schema import ProblemClass, TargetStack
|
||||
from script_export import ALGORITHMS, CLASS_FIT
|
||||
assert set(ALGORITHMS) == set(TargetStack)
|
||||
assert set(CLASS_FIT) == set(ProblemClass)
|
||||
Loading…
Reference in a new issue