diff --git a/solving/solver_stage.py b/solving/solver_stage.py index 9220824..7613717 100644 --- a/solving/solver_stage.py +++ b/solving/solver_stage.py @@ -396,7 +396,9 @@ def check_sensitivity(run, sense: str) -> list: Die Richtung hängt am Sinn der Restriktion und daran, ob maximiert oder minimiert wird. """ warnings = [] - besser = (lambda a, b: a > b) if sense == "maximize" else (lambda a, b: a < b) + # mit Toleranz: beobachtet Schranke 0.13896862211718014 gegen Lösung ...17 als „schlechter" gemeldet + besser = ((lambda a, b: a > b + _tolerance(b)) if sense == "maximize" + else (lambda a, b: a < b - _tolerance(b))) by_name = {c.name: c for c in run.constraints} for s in run.sensitivity: constraint = by_name.get(s.name) @@ -566,7 +568,9 @@ def check_multistart(run, sense: str) -> list: if m is None: return [] warnings = [] - besser = (lambda a, b: a > b) if sense == "maximize" else (lambda a, b: a < b) + # mit Toleranz: beobachtet Schranke 0.13896862211718014 gegen Lösung ...17 als „schlechter" gemeldet + besser = ((lambda a, b: a > b + _tolerance(b)) if sense == "maximize" + else (lambda a, b: a < b - _tolerance(b))) if m.converged > m.starts or m.converged < 0: warnings.append(f"Mehrstart: {m.converged} erfolgreiche von {m.starts} Startpunkten ist nicht möglich.") elif m.converged < 2: @@ -685,7 +689,9 @@ def verify_scenarios(run, code: str, timeout: int = 60, data_files=()) -> list: data_files=data_files) szenario.verified_value = nachlauf.objective_value if nachlauf.status == SolverStatus.ERROR: - warnings.append(f"Szenario '{szenario.name}' ließ sich nicht nachrechnen: {(nachlauf.error or '')[:120]}") + grund = (nachlauf.error or "").strip()[:120] or ( + "das Programm meldete selbst Status 'error' ohne Fehlertext (z. B. keine Lösung im Zeitlimit)") + warnings.append(f"Szenario '{szenario.name}' ließ sich nicht nachrechnen: {grund}") elif (szenario.objective_value is not None and nachlauf.objective_value is not None and abs(szenario.objective_value - nachlauf.objective_value) > 1e-6 * max(1.0, abs(szenario.objective_value))): diff --git a/tests/test_feasibility.py b/tests/test_feasibility.py index 81c7bfa..45df276 100644 --- a/tests/test_feasibility.py +++ b/tests/test_feasibility.py @@ -143,3 +143,9 @@ def test_report_shows_sensitivity_and_integrality_price(): page = to_html(build_document(result)) assert "Sensitivität der bindenden Restriktionen" in page and "Preis der Ganzzahligkeit" in page assert "0,1" in page.replace(".", ",") or "0.1" in page # (12100-12000)/1000 = 0.1 + + +def test_relaxation_bound_equal_up_to_rounding_is_no_warning(): + # beobachtet (Portfolio Nr. 2): 0.13896862211718014 gegen 0.13896862211718017 + run = sens_run([], objective=0.13896862211718017, relaxation=0.13896862211718014) + assert check_sensitivity(run, "maximize") == [] diff --git a/tests/test_solver_stage.py b/tests/test_solver_stage.py index 6c8d1c9..afc836f 100644 --- a/tests/test_solver_stage.py +++ b/tests/test_solver_stage.py @@ -753,3 +753,15 @@ def test_ordinary_programs_are_not_suspected(): from solving.solver_stage import check_generated_data code = "import highspy\nh = highspy.Highs()\nDATA = {'kosten': [3, 2]}\n" assert check_generated_data(code, SolverCode(code=code, data_assumptions=[])) == [] + + +def test_scenario_failing_without_error_text_gets_a_reason(): + # beobachtet (Portfolio Nr. 2): Szenario ohne Lösung im Zeitlimit, Warnung endete mit leerem Grund + from core.model_schema import Scenario + from solving.solver_stage import verify_scenarios + code = SZENARIO_CODE.replace(' basis = loesen(DATA)\n', + ' basis = loesen(DATA)\n if DATA["preis"] > 15:\n' + ' report("error", None, {})\n return\n') + run = run_solver_code(code) + befunde = verify_scenarios(run, code) + assert any("doppelter Preis" in b and "ohne Fehlertext" in b for b in befunde)