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. Die Richtung hängt am Sinn der Restriktion und daran, ob maximiert oder minimiert wird.
""" """
warnings = [] 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} by_name = {c.name: c for c in run.constraints}
for s in run.sensitivity: for s in run.sensitivity:
constraint = by_name.get(s.name) constraint = by_name.get(s.name)
@ -566,7 +568,9 @@ def check_multistart(run, sense: str) -> list:
if m is None: if m is None:
return [] return []
warnings = [] 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: if m.converged > m.starts or m.converged < 0:
warnings.append(f"Mehrstart: {m.converged} erfolgreiche von {m.starts} Startpunkten ist nicht möglich.") warnings.append(f"Mehrstart: {m.converged} erfolgreiche von {m.starts} Startpunkten ist nicht möglich.")
elif m.converged < 2: elif m.converged < 2:
@ -685,7 +689,9 @@ def verify_scenarios(run, code: str, timeout: int = 60, data_files=()) -> list:
data_files=data_files) data_files=data_files)
szenario.verified_value = nachlauf.objective_value szenario.verified_value = nachlauf.objective_value
if nachlauf.status == SolverStatus.ERROR: 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 elif (szenario.objective_value is not None and nachlauf.objective_value is not None
and abs(szenario.objective_value - nachlauf.objective_value) and abs(szenario.objective_value - nachlauf.objective_value)
> 1e-6 * max(1.0, abs(szenario.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)) page = to_html(build_document(result))
assert "Sensitivität der bindenden Restriktionen" in page and "Preis der Ganzzahligkeit" in page 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 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 from solving.solver_stage import check_generated_data
code = "import highspy\nh = highspy.Highs()\nDATA = {'kosten': [3, 2]}\n" code = "import highspy\nh = highspy.Highs()\nDATA = {'kosten': [3, 2]}\n"
assert check_generated_data(code, SolverCode(code=code, data_assumptions=[])) == [] 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)