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
+20 -57
View File
@@ -110,21 +110,19 @@ class RealtimePlot(FigureCanvasQTAgg):
class SudForecastPlot(FigureCanvasQTAgg):
"""Compares a Sud schedule's actual control behavior (line, solid)
against a fixed, simulation-based forecast (line_projected, dashed) -
the same plant/controller model the real run uses
against a simulation-based forecast (line_projected, dashed) - the
same plant/controller model the real run uses
(components/sud_forecast.py's SudForecastEstimator - see
show_computing()/show_forecast()). The forecast is computed once per
piecewise segment and never revisited once a run is using it - the
whole point is an honest, unmoving baseline to compare reality
against, not a prediction that keeps re-anchoring itself to match
whatever's actually happening. The one exception is a step requiring
user confirmation: a human's response time genuinely can't be
forecast, so the segment just stops there, and the next one is
computed for real (anchored at the real elapsed time and
temperature) only once the user actually confirms - see
tasks/sud.py's SudTask._continue_forecast_after_confirm().
extend_forecast_while_waiting() keeps the dashed line visually flat
through the wait in the meantime."""
show_computing()/show_forecast()). The forecast covers the whole
schedule from the very first Load, including through any step
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
guess outright rather than the GUI patching it up itself."""
def __init__(self):
figure = Figure(facecolor='white')
@@ -143,13 +141,6 @@ class SudForecastPlot(FigureCanvasQTAgg):
figure.tight_layout()
# The forecast as last received from the server (see
# show_forecast()) - kept around so extend_forecast_while_waiting()
# can repeat its last value without the caller needing to pass it
# again every tick.
self._forecast_t_min = []
self._forecast_theta = []
def show_computing(self, name):
"""Shown right after a schedule loads, while the first
simulation-based forecast segment is still being computed
@@ -159,8 +150,6 @@ class SudForecastPlot(FigureCanvasQTAgg):
(there's nothing meaningful to fit yet), so whatever was on
screen before just stays frozen underneath the title change
until show_forecast() replaces it."""
self._forecast_t_min = []
self._forecast_theta = []
self.line.set_data([], [])
self.line_projected.set_data([], [])
self.ax.set_title("{} - computing forecast...".format(name), fontsize='small')
@@ -168,18 +157,16 @@ class SudForecastPlot(FigureCanvasQTAgg):
self.draw_idle()
def show_forecast(self, t_min, theta, name, finished):
"""The forecast curve (dashed) - computed once, piecewise, by the
server simulating the schedule with its real plant/controller
"""The forecast curve (dashed) - computed by the server simulating
the schedule with its real plant/controller
(components/sud_forecast.py) - see Window.on_sud_forecast_
received(). Called again, with a longer t_min/theta, each time a
further piecewise segment gets appended after a real user
confirmation; finished is False while the forecast doesn't yet
cover the rest of the schedule (stopped at a step requiring
confirmation - see extend_forecast_while_waiting()). Locks the
axes to fit the forecast alone, regardless of whatever the
received(). Called again, with a corrected t_min/theta, each time
a real user confirmation replaces a zero-delay guess with one
anchored at the real elapsed time and temperature; finished is
False only in the pathological case of a step whose target can
never be reached (components/sud_forecast.py's MAX_TICKS). Locks
the axes to fit the forecast alone, regardless of whatever the
actual trace (line) is currently doing."""
self._forecast_t_min = t_min
self._forecast_theta = theta
self.line_projected.set_data(t_min, theta)
self._set_fixed_limits(t_min, theta)
total = t_min[-1] if t_min else 0.0
@@ -188,21 +175,6 @@ class SudForecastPlot(FigureCanvasQTAgg):
self.figure.tight_layout()
self.draw_idle()
def extend_forecast_while_waiting(self, elapsed_min):
"""Called every tick while the real Sud is waiting on user
confirmation: a human's response time can't be forecast, so
rather than guess, the dashed line just repeats its last value
out to 'now' until the next segment is appended for real (see
tasks/sud.py's SudTask._continue_forecast_after_confirm(), fired
the moment the real confirmation happens) - at which point
show_forecast() overwrites this temporary extension with the
actual next segment."""
if not self._forecast_t_min or elapsed_min <= self._forecast_t_min[-1]:
return
last_theta = self._forecast_theta[-1]
self.line_projected.set_data(self._forecast_t_min + [elapsed_min], self._forecast_theta + [last_theta])
self.draw_idle()
def _set_fixed_limits(self, t_list, theta_list):
"""Explicitly sets (and, unlike relim()+autoscale_view(), locks)
the axes' view to fit exactly the given data, with matplotlib's
@@ -235,8 +207,6 @@ class SudForecastPlot(FigureCanvasQTAgg):
self.draw_idle()
def show_no_schedule(self):
self._forecast_t_min = []
self._forecast_theta = []
self.line.set_data([], [])
self.line_projected.set_data([], [])
self.clear_progress()
@@ -472,13 +442,6 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow):
if self.sud_state != SUD_PAUSED_STATE and self.plot_temp_ist:
self.forecast_history.append((elapsed_min, self.plot_temp_ist))
self.forecast_plot.show_dynamic(self.forecast_history)
if self.sud_state == SUD_WAIT_USER_STATE:
# A human's confirm time can't be forecast - the dashed
# line just repeats its last value out to 'now' until
# the real confirmation appends the next segment (see
# tasks/sud.py's SudTask._continue_forecast_after_
# confirm()).
self.forecast_plot.extend_forecast_while_waiting(elapsed_min)
else:
self.forecast_plot.clear_progress()