Anchor the forecast to the real current temperature, not a cold start
send_forecast() was defaulting to a cold start at ambient (SudForecastEstimator. estimate()'s default when start_theta isn't passed), so the forecast's t=0 never matched whatever temperature the pot actually was at - understating how long the first ramp would really take whenever it wasn't already cold. Anchoring at tc.get_theta_ist() seemed like the obvious fix but isn't reliable right after Load: that's only ever updated inside process(), which a model-based controller (Smith) doesn't run until its plant params are configured - which now only happens once a Sud is loaded (see the "inert until loaded" change) - so theta_ist can still be sitting at its never-updated __init__ default of 0 the instant send_forecast() reads it. Added TempControllerBase.get_theta_ist_set() (mirroring the existing get_theta_soll_set()) - the raw sensor reading, updated the instant a reading comes in regardless of whether process() has ever run - and used that instead. That still leaves a gap between Load and Start: the real temperature can drift (time passing, manual heating via the Manual tab) between loading a schedule and actually starting it, leaving the forecast anchored to a now-stale temperature. recv()'s 'Start' handler now re-sends the forecast, freshly anchored to the real temperature at that moment, on every fresh start (not a Pause->resume, which keeps the already-established forecast rather than discarding it mid-run). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DkkuG48uHFCGKe6dPSERFk
This commit is contained in:
@@ -97,6 +97,17 @@ class TempControllerBase(APid):
|
||||
def get_theta_ist(self):
|
||||
return self.theta_ist
|
||||
|
||||
def get_theta_ist_set(self):
|
||||
"""The raw sensor reading, as last pushed via set_theta_ist() -
|
||||
unlike get_theta_ist() (self.theta_ist), this is updated the
|
||||
instant a reading comes in, regardless of whether process() has
|
||||
ever actually run (e.g. before set_params()/a model-based
|
||||
controller's plant params have been configured - see SudTask.
|
||||
send_forecast(), which needs a real "current temperature" at the
|
||||
moment a Sud is loaded, before this controller may have ticked
|
||||
even once)."""
|
||||
return self.theta_ist_set
|
||||
|
||||
def set_heatrate_ist(self, value):
|
||||
self.heatrate_ist_set = value
|
||||
|
||||
|
||||
+29
-1
@@ -176,6 +176,22 @@ class SudTask(ATask):
|
||||
first one. Always a fresh start: discards whatever forecast was
|
||||
accumulated for the previously loaded schedule.
|
||||
|
||||
Anchored at the real current temperature
|
||||
(self.tc.get_theta_ist_set()), not a cold start at ambient - the
|
||||
pot may already be warm (a previous run, or manual heating) at
|
||||
the moment this Sud is loaded, and a forecast that assumes
|
||||
ambient regardless would reach every target later than it
|
||||
actually will, never lining up with the actual trace even at
|
||||
t=0. Deliberately the raw sensor reading (theta_ist_set), not
|
||||
the controller's own get_theta_ist() (theta_ist) -
|
||||
_continue_forecast_after_confirm() can trust that one because a
|
||||
real run has been actively ticking for a while by the time it
|
||||
runs, but this is called right after Load, before this
|
||||
controller may have processed even a single tick yet (e.g. its
|
||||
plant params/model only just got configured - see SudTask.
|
||||
recv()), so theta_ist itself could still be sitting at its
|
||||
never-updated __init__ default.
|
||||
|
||||
Those zero-delay assumptions get corrected piecewise as real
|
||||
confirmations actually happen - see
|
||||
_continue_forecast_after_confirm()."""
|
||||
@@ -186,7 +202,9 @@ class SudTask(ATask):
|
||||
# otherwise stall every other task (heater, sensor, ...) for that
|
||||
# whole window.
|
||||
loop = asyncio.get_event_loop()
|
||||
t, theta, final_state, confirm_points = await loop.run_in_executor(None, self.forecast_estimator.estimate, doc)
|
||||
start_theta = self.tc.get_theta_ist_set()
|
||||
t, theta, final_state, confirm_points = await loop.run_in_executor(
|
||||
None, self.forecast_estimator.estimate, doc, start_theta)
|
||||
self.forecast_t = t
|
||||
self.forecast_theta = theta
|
||||
self.forecast_finished = (final_state == SudState.DONE)
|
||||
@@ -279,7 +297,17 @@ class SudTask(ATask):
|
||||
async def recv(self, data):
|
||||
for pair in data.items():
|
||||
if 'Start' in pair[0]:
|
||||
# A fresh start (not a resume from Pause, which keeps
|
||||
# whatever forecast the run already established) re-
|
||||
# anchors the forecast to the real temperature right now
|
||||
# - that's the actual "t=0" the about-to-start actual
|
||||
# trace will be plotted from, which may no longer match
|
||||
# whatever temperature existed back at Load (time passed,
|
||||
# possibly manual heating in between).
|
||||
fresh_start = self.sud.state in (SudState.IDLE, SudState.DONE)
|
||||
self.sud.start()
|
||||
if fresh_start:
|
||||
asyncio.create_task(self.send_forecast(self.sud.save()))
|
||||
elif 'Confirm' in pair[0]:
|
||||
confirmed_index = self.sud.index
|
||||
confirmed_target = self.tc.get_theta_soll_set()
|
||||
|
||||
Reference in New Issue
Block a user