diff --git a/README.md b/README.md index 92c5ddb..94bfa48 100644 --- a/README.md +++ b/README.md @@ -309,8 +309,16 @@ looks up the relevant `confirm_points` entry, truncates the forecast right back to that point, and splices in a freshly anchored simulation of the remaining steps - anchored at the real elapsed time and real current temperature, so an actual delay shows up honestly as a gap in the timeline -rather than as the assumed zero. The corrected forecast is sent in full -each time, so the GUI's `SudForecastPlot.show_forecast()` simply redraws +rather than as the assumed zero. That gap is filled at the real +controller's setpoint at the moment of confirmation (`recv()` captures +`tc.get_theta_soll_set()` *before* calling `Sud.confirm()`, since confirming +synchronously pushes the next step's own target onto it) rather than +whatever value the forecast happened to log the instant `WAIT_USER` +tripped - the controller stays enabled and actively holding throughout +`WAIT_USER`, so the setpoint is what it was actually converging toward +during the wait, not a possibly-still-mid-ramp snapshot. The corrected +forecast is sent in full each time, so the GUI's `SudForecastPlot. +show_forecast()` simply redraws the dashed line outright rather than patching it up itself. Comparing the two lines: real-world divergence from the forecast - more diff --git a/tasks/sud.py b/tasks/sud.py index 6c3deb5..5a7d010 100644 --- a/tasks/sud.py +++ b/tasks/sud.py @@ -164,7 +164,7 @@ class SudTask(ATask): 'Finished': self.forecast_finished, }}) - async def _continue_forecast_after_confirm(self, confirmed_index): + async def _continue_forecast_after_confirm(self, confirmed_index, confirmed_target): """Corrects the optimistic, zero-delay guess send_forecast() (or a previous call to this method) made at confirmed_index's user-confirmation step, now that the confirmation has actually @@ -177,6 +177,12 @@ class SudTask(ATask): the real current temperature (more trustworthy than whatever the now-superseded guess predicted it would be by now). + confirmed_target is the real controller's theta_soll_set at the + moment of confirmation (captured by recv() *before* calling + Sud.confirm(), since that synchronously pushes the next step's + own target onto it) - used to fill the real-world wait itself + (see below). + No-op if confirmed_index was never part of a computed forecast in the first place (e.g. the estimator isn't configured).""" if self.forecast_estimator is None: @@ -210,12 +216,15 @@ class SudTask(ATask): loop = asyncio.get_event_loop() t, theta, final_state, confirm_points = await loop.run_in_executor(None, self.forecast_estimator.estimate, doc, start_theta) # Bridge the gap between the confirmation point and the real - # elapsed time it actually happened at, holding flat at its last - # temperature - then append the new segment, offset to start - # exactly there. + # elapsed time it actually happened at - the real controller + # stays enabled and actively holding through WAIT_USER, so + # confirmed_target (its setpoint at the time) is what it was + # actually converging toward during the wait, not whatever + # (possibly still mid-ramp) value the forecast happened to log + # the instant WAIT_USER tripped. if self.forecast_t and self.forecast_t[-1] < real_elapsed: self.forecast_t.append(real_elapsed) - self.forecast_theta.append(self.forecast_theta[-1]) + self.forecast_theta.append(confirmed_target) self.forecast_t.extend(real_elapsed + seconds for seconds in t) self.forecast_theta.extend(theta) # confirm_points' indices/times are relative to this sub-schedule @@ -232,8 +241,9 @@ class SudTask(ATask): self.sud.start() elif 'Confirm' in pair[0]: confirmed_index = self.sud.index + confirmed_target = self.tc.get_theta_soll_set() self.sud.confirm() - asyncio.create_task(self._continue_forecast_after_confirm(confirmed_index)) + asyncio.create_task(self._continue_forecast_after_confirm(confirmed_index, confirmed_target)) elif 'Pause' in pair[0]: self.sud.pause() elif 'Stop' in pair[0]: