Forecast a Sud schedule through its confirm steps, not just up to the first one

A user_wait_for_continue step used to stop SudForecastEstimator.estimate()
dead, so the GUI's forecast plot only ever showed the first segment on Load.
Model the wait as a zero-delay auto-confirm instead, so the whole schedule's
projected curve is visible right away; correct that assumption piecewise as
real confirmations actually happen, anchored at the real elapsed time and
temperature.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DkkuG48uHFCGKe6dPSERFk
This commit is contained in:
2026-06-22 17:12:19 +02:00
co-authored by Claude Sonnet 4.6
parent 6e5fd23ccb
commit 84b1607c78
4 changed files with 144 additions and 123 deletions
+26 -15
View File
@@ -34,18 +34,23 @@ class SudForecastEstimator:
self.theta_amb = theta_amb
def estimate(self, doc, start_theta=None):
"""Returns (t, theta, final_state): t/theta are parallel lists of
elapsed simulated seconds and temperature for one piecewise
segment of the schedule, starting from doc['steps'][0] - either
all the way to the end (final_state is SudState.DONE), or, if a
step requires user confirmation, only up to and including that
step's hold (final_state is SudState.WAIT_USER). A real user's
confirm time can't be forecast, so the simulation simply stops
there instead of guessing zero delay or otherwise - the caller is
expected to compute the next segment for real once the user
actually confirms (see tasks/sud.py's SudTask), anchored at
whatever the real elapsed time and temperature are by then,
rather than this estimate continuing to guess at it.
"""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.
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.
start_theta defaults to the configured ambient temperature - i.e.
a cold start, same as the GUI's static estimate."""
@@ -54,7 +59,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, []
pot = Pot(self.dt, self.plant_params, self.theta_amb)
pot.initial(start_theta)
@@ -87,10 +92,16 @@ class SudForecastEstimator:
t = [0.0]
theta = [pot.get_temperature()]
confirm_points = []
sud.start()
ticks = 0
while sud.state not in (SudState.DONE, SudState.WAIT_USER) and ticks < MAX_TICKS:
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
pot.process()
tc.set_theta_ist(pot.get_temperature())
tc.process()
@@ -105,4 +116,4 @@ class SudForecastEstimator:
theta.append(pot.get_temperature())
ticks += 1
return t, theta, sud.state
return t, theta, sud.state, confirm_points