Fill the post-confirm forecast gap with the real setpoint, not a mid-ramp snapshot

_continue_forecast_after_confirm()'s bridge was repeating forecast_theta[-1]
to cover the real-world wait, but that's whatever value the simulation
happened to log the instant WAIT_USER tripped - often still mid-ramp, not the
step's actual hold target. The real controller stays enabled and actively
holding throughout WAIT_USER, so capture its setpoint (tc.get_theta_soll_set())
in recv() before calling Sud.confirm() - confirming synchronously pushes the
next step's own target onto it - and use that to fill the gap instead.

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:24:02 +02:00
co-authored by Claude Sonnet 4.6
parent 84b1607c78
commit 031b13ca2c
2 changed files with 26 additions and 8 deletions
+10 -2
View File
@@ -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
+16 -6
View File
@@ -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]: