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:
+38
-15
@@ -291,10 +291,13 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow):
|
|||||||
# [(t_min, theta), ...] actually measured since the current run
|
# [(t_min, theta), ...] actually measured since the current run
|
||||||
# started - the "already happened" part of the dynamic forecast.
|
# started - the "already happened" part of the dynamic forecast.
|
||||||
self.forecast_history = []
|
self.forecast_history = []
|
||||||
# (T, Theta) from the server's simulation-based estimate of the
|
# (anchor_min, T, Theta) from the server's simulation-based estimate
|
||||||
# remaining steps, recomputed on every step change - None until it
|
# of the remaining steps, recomputed on every step change -
|
||||||
# arrives (or after a step change, until the next one does), in
|
# anchor_min is elapsed_min as of receipt, so on_plot_timer() can
|
||||||
# which case on_plot_timer() falls back to the naive estimate.
|
# 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.server_remaining_forecast = None
|
||||||
self.msg_pot = self.msg_dispatch.msgio_get('Pot')
|
self.msg_pot = self.msg_dispatch.msgio_get('Pot')
|
||||||
self.msg_sensor = self.msg_dispatch.msgio_get('Sensor')
|
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:
|
if remembered_ambient is not None:
|
||||||
self.msg_system.send({'AmbientTemp': remembered_ambient})
|
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):
|
def on_plot_timer(self):
|
||||||
self.plot.sample(
|
self.plot.sample(
|
||||||
self.plot_temp_ist, self.plot_temp_soll,
|
self.plot_temp_ist, self.plot_temp_soll,
|
||||||
self.plot_rate_ist, self.plot_rate_soll,
|
self.plot_rate_ist, self.plot_rate_soll,
|
||||||
self.plot_power_set, self.plot_power_eff)
|
self.plot_power_set, self.plot_power_eff)
|
||||||
|
|
||||||
if self.sud_start_time is not None:
|
elapsed_min = self._elapsed_min()
|
||||||
# While paused, use the moment the pause began as "now" so the
|
if elapsed_min is not None:
|
||||||
# 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
|
|
||||||
self.forecast_plot.set_progress(elapsed_min)
|
self.forecast_plot.set_progress(elapsed_min)
|
||||||
|
|
||||||
# Re-anchor the forecast from the live state instead of redrawing
|
# 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:
|
if self.sud_state != SUD_PAUSED_STATE and self.plot_temp_ist:
|
||||||
self.forecast_history.append((elapsed_min, self.plot_temp_ist))
|
self.forecast_history.append((elapsed_min, self.plot_temp_ist))
|
||||||
if self.server_remaining_forecast is not None:
|
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:
|
else:
|
||||||
# Server's simulation-based remainder hasn't arrived
|
# Server's simulation-based remainder hasn't arrived
|
||||||
# yet (it's recomputed on every step change, off the
|
# yet (it's recomputed on every step change, off the
|
||||||
# event loop) - fall back to the quick naive estimate
|
# event loop) - fall back to the quick naive estimate
|
||||||
# for this one tick.
|
# 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, 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)
|
self.forecast_plot.show_dynamic(self.forecast_history, t_rem_min, theta_rem, self.sud_name)
|
||||||
else:
|
else:
|
||||||
self.forecast_plot.clear_progress()
|
self.forecast_plot.clear_progress()
|
||||||
@@ -849,7 +866,13 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow):
|
|||||||
self.update_status_step_label()
|
self.update_status_step_label()
|
||||||
elif "RemainingForecast" in key:
|
elif "RemainingForecast" in key:
|
||||||
forecast = msg['RemainingForecast']
|
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:
|
elif "Forecast" in key:
|
||||||
self.on_sud_forecast_received(msg['Forecast'])
|
self.on_sud_forecast_received(msg['Forecast'])
|
||||||
self.update_sud_actions()
|
self.update_sud_actions()
|
||||||
|
|||||||
Reference in New Issue
Block a user