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:
+13
-16
@@ -66,23 +66,22 @@ class SudForecastEstimator:
|
||||
self.theta_amb = theta_amb
|
||||
|
||||
def estimate(self, doc, start_theta=None):
|
||||
"""Returns (t, theta, final_state, confirm_points): t/theta are
|
||||
parallel lists of elapsed simulated seconds and temperature,
|
||||
covering doc['steps'] from the start all the way to the end
|
||||
(final_state is SudState.DONE), or, in the pathological case of
|
||||
a step whose target can never actually be reached, wherever
|
||||
MAX_TICKS cut the simulation off.
|
||||
"""Returns (t, theta, final_state): t/theta are parallel lists of
|
||||
elapsed simulated seconds and temperature, covering doc['steps']
|
||||
from the start all the way to the end (final_state is
|
||||
SudState.DONE), or, in the pathological case of a step whose
|
||||
target can never actually be reached, wherever MAX_TICKS cut the
|
||||
simulation off.
|
||||
|
||||
A step requiring user confirmation doesn't stop the simulation
|
||||
either - a human's response time genuinely can't be forecast,
|
||||
so it's modeled as zero delay (auto-confirmed the instant that
|
||||
step's hold completes) rather than leaving the estimate stuck
|
||||
there forever. confirm_points records every place that
|
||||
assumption was made, as (step_index, t) pairs, so the caller
|
||||
(tasks/sud.py's SudTask) can correct it once a real
|
||||
confirmation actually happens: truncate the forecast at that
|
||||
point and splice in a freshly anchored simulation of the
|
||||
remaining steps in place of the optimistic guess.
|
||||
there forever. That optimistic guess gets corrected for real once
|
||||
the caller (tasks/sud.py's SudTask) sees the schedule actually
|
||||
reach the next step boundary - see SudTask._reanchor_forecast(),
|
||||
called from on_step_changed() on every real transition, not just
|
||||
confirmations.
|
||||
|
||||
start_theta defaults to the configured ambient temperature - i.e.
|
||||
a cold start, same as the GUI's static estimate."""
|
||||
@@ -91,7 +90,7 @@ class SudForecastEstimator:
|
||||
|
||||
sud = Sud()
|
||||
if not sud.load(doc) or not sud.schedule:
|
||||
return [0.0], [start_theta], SudState.DONE, []
|
||||
return [0.0], [start_theta], SudState.DONE
|
||||
|
||||
# Plant params (M/C/L/Td) are deliberately *not* seeded here from
|
||||
# any default - sud.start() below synchronously fires the first
|
||||
@@ -157,13 +156,11 @@ class SudForecastEstimator:
|
||||
|
||||
t = [0.0]
|
||||
theta = [pot.get_temperature()]
|
||||
confirm_points = []
|
||||
|
||||
sud.start()
|
||||
ticks = 0
|
||||
while sud.state != SudState.DONE and ticks < MAX_TICKS:
|
||||
if sud.state == SudState.WAIT_USER:
|
||||
confirm_points.append((sud.index, t[-1]))
|
||||
sud.confirm()
|
||||
continue
|
||||
|
||||
@@ -184,4 +181,4 @@ class SudForecastEstimator:
|
||||
theta.append(pot.get_temperature())
|
||||
ticks += 1
|
||||
|
||||
return t, theta, sud.state, confirm_points
|
||||
return t, theta, sud.state
|
||||
|
||||
Reference in New Issue
Block a user