fix: Toleranz bei Relaxationsschranke, Grund bei fehlgeschlagenem Szenario
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
c68a293755
commit
ee9a193cd7
3 changed files with 27 additions and 3 deletions
|
|
@ -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))):
|
||||||
|
|
|
||||||
|
|
@ -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") == []
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue