Generalize Sud forecast reanchoring to every step boundary

Reanchoring used to only happen when a user confirmed a WAIT_USER
step, so anything the schedule advanced through on its own (a ramp
reaching target, a hold timing out) left the forecast showing a
stale, increasingly wrong prediction once reality diverged from it.
_reanchor_forecast() now fires from on_step_changed() on every real
transition, splicing in a fresh simulation anchored at the real
current temperature/elapsed time instead.

Also fixes two bugs surfaced while testing that change:
- estimate()'s early-return for an empty/not-yet-loaded schedule still
  returned the old 4-tuple shape, crashing every connection before a
  Sud was ever Loaded.
- send_forecast() and _reanchor_forecast() can run concurrently (e.g.
  a fresh Start triggers both at once), and whichever resumed second
  after its own worker-thread simulation would blindly splice its tail
  onto whatever the other had already written, producing a spurious
  connecting line across the plot. Both now carry a generation counter
  and discard their result if a newer call has since committed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019qvu5giu7gvRCyEWzf2Vpx
This commit is contained in:
2026-06-24 10:50:17 +02:00
co-authored by Claude Sonnet 4.6
parent d8e9c39fda
commit 17e8386002
4 changed files with 124 additions and 103 deletions
+7 -7
View File
@@ -129,10 +129,10 @@ class SudForecastPlot(FigureCanvasQTAgg):
requiring user confirmation - a human's response time genuinely
can't be forecast, so it's modeled as a zero-delay auto-confirm
there instead of stopping. That assumption gets corrected once the
real confirmation actually happens (anchored at the real elapsed
time and temperature) - see tasks/sud.py's SudTask.
_continue_forecast_after_confirm() - at which point show_forecast()
is called again with the corrected curve, replacing the optimistic
schedule actually reaches the next real step boundary (anchored at
the real elapsed time and temperature) - see tasks/sud.py's
SudTask._reanchor_forecast() - at which point show_forecast() is
called again with the corrected curve, replacing the optimistic
guess outright rather than the GUI patching it up itself."""
def __init__(self):
@@ -894,9 +894,9 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow):
# Computed asynchronously server-side and can arrive slightly
# after the schedule itself - applies regardless of whether a
# run is in progress, since this can also be a later piecewise
# segment appended after a real user confirmation (see
# tasks/sud.py's SudTask._continue_forecast_after_confirm()), not
# just the initial one computed at Load.
# segment appended after a real step transition (see tasks/
# sud.py's SudTask._reanchor_forecast()), not just the initial
# one computed at Load.
if self.sud_empty:
return
t_min = [seconds / 60.0 for seconds in forecast['T']]