fix: Toleranz bei Relaxationsschranke, Grund bei fehlgeschlagenem Szenario

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-09-17 14:22:22 +02:00
commit ee9a193cd7
3 changed files with 27 additions and 3 deletions

View file

@ -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))):

View file

@ -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") == []

View file

@ -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)