Fix dynamic forecast dashed line drifting right during a run

on_plot_timer() re-anchored the server's RemainingForecast to the
live, ever-advancing elapsed_min on every 1s tick, even though that
forecast is only recomputed on step changes. Between step changes,
the unchanged (t_rem, theta_rem) array kept getting pushed further
right by the full live elapsed_min each tick instead of staying
anchored to when it was actually computed.

Store server_remaining_forecast as (anchor_min, T, Theta), with
anchor_min captured at receipt via a new _elapsed_min() helper, and
anchor the dashed projection to that fixed point instead of the live
tick's elapsed_min.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhiQe64F74uHV8jzuoSa5K
This commit is contained in:
2026-06-22 08:14:16 +02:00
co-authored by Claude Sonnet 4.6
parent d91bba1ba2
commit 7d79d885b5
+38 -15
View File
@@ -291,10 +291,13 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow):
# [(t_min, theta), ...] actually measured since the current run
# started - the "already happened" part of the dynamic forecast.
self.forecast_history = []
# (T, Theta) from the server's simulation-based estimate of the
# remaining steps, recomputed on every step change - None until it
# arrives (or after a step change, until the next one does), in
# which case on_plot_timer() falls back to the naive estimate.
# (anchor_min, T, Theta) from the server's simulation-based estimate
# of the remaining steps, recomputed on every step change -
# anchor_min is elapsed_min as of receipt, so on_plot_timer() can
# re-anchor the projection without it drifting right every tick.
# None until it arrives (or after a step change, until the next one
# does), in which case on_plot_timer() falls back to the naive
# estimate.
self.server_remaining_forecast = None
self.msg_pot = self.msg_dispatch.msgio_get('Pot')
self.msg_sensor = self.msg_dispatch.msgio_get('Sensor')
@@ -411,20 +414,27 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow):
if remembered_ambient is not None:
self.msg_system.send({'AmbientTemp': remembered_ambient})
def _elapsed_min(self):
"""Simulated-schedule minutes elapsed since the run started, or
None if no run is in progress. While paused, uses the moment the
pause began as "now" so it freezes instead of drifting ahead of
actual progress."""
if self.sud_start_time is None:
return None
now = self.sud_pause_started_at if self.sud_pause_started_at is not None else time.monotonic()
elapsed_real_s = now - self.sud_start_time - self.sud_paused_total
# The forecast's x-axis is simulated-schedule time; scale real
# elapsed time by the server's warp factor to match it.
return elapsed_real_s * self.warp_factor / 60.0
def on_plot_timer(self):
self.plot.sample(
self.plot_temp_ist, self.plot_temp_soll,
self.plot_rate_ist, self.plot_rate_soll,
self.plot_power_set, self.plot_power_eff)
if self.sud_start_time is not None:
# While paused, use the moment the pause began as "now" so the
# line freezes instead of drifting ahead of actual progress.
now = self.sud_pause_started_at if self.sud_pause_started_at is not None else time.monotonic()
elapsed_real_s = now - self.sud_start_time - self.sud_paused_total
# The forecast's x-axis is simulated-schedule time; scale real
# elapsed time by the server's warp factor to match it.
elapsed_min = elapsed_real_s * self.warp_factor / 60.0
elapsed_min = self._elapsed_min()
if elapsed_min is not None:
self.forecast_plot.set_progress(elapsed_min)
# Re-anchor the forecast from the live state instead of redrawing
@@ -435,14 +445,21 @@ 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))
if self.server_remaining_forecast is not None:
t_rem, theta_rem = self.server_remaining_forecast
# Anchored to elapsed_min as of when the server computed
# this forecast, not the current tick's - it's only
# recomputed on step changes, so re-adding the live,
# ever-growing elapsed_min here would push the whole
# projection further right every tick until the next
# step change, instead of holding it fixed.
anchor_min, t_rem, theta_rem = self.server_remaining_forecast
else:
# Server's simulation-based remainder hasn't arrived
# yet (it's recomputed on every step change, off the
# event loop) - fall back to the quick naive estimate
# for this one tick.
anchor_min = elapsed_min
t_rem, theta_rem = self.forecast_plot._estimate_course(self._remaining_schedule(), self.plot_temp_ist)
t_rem_min = [elapsed_min + seconds / 60.0 for seconds in t_rem]
t_rem_min = [anchor_min + seconds / 60.0 for seconds in t_rem]
self.forecast_plot.show_dynamic(self.forecast_history, t_rem_min, theta_rem, self.sud_name)
else:
self.forecast_plot.clear_progress()
@@ -849,7 +866,13 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow):
self.update_status_step_label()
elif "RemainingForecast" in key:
forecast = msg['RemainingForecast']
self.server_remaining_forecast = (forecast['T'], forecast['Theta'])
# Anchor to elapsed_min as of receipt - close enough to
# when the server actually computed it (it's seeded from
# the live theta_ist at that moment) - rather than the
# live elapsed_min on_plot_timer() sees on later ticks.
anchor_min = self._elapsed_min()
if anchor_min is not None:
self.server_remaining_forecast = (anchor_min, forecast['T'], forecast['Theta'])
elif "Forecast" in key:
self.on_sud_forecast_received(msg['Forecast'])
self.update_sud_actions()