diff --git a/components/pid/temp_controller_base.py b/components/pid/temp_controller_base.py index 288e723..6712989 100644 --- a/components/pid/temp_controller_base.py +++ b/components/pid/temp_controller_base.py @@ -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 diff --git a/tasks/sud.py b/tasks/sud.py index e89cd82..27b5f38 100644 --- a/tasks/sud.py +++ b/tasks/sud.py @@ -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()