diff --git a/components/pid/temp_controller_smith.py b/components/pid/temp_controller_smith.py index 6ca8ecf..1db57be 100755 --- a/components/pid/temp_controller_smith.py +++ b/components/pid/temp_controller_smith.py @@ -11,10 +11,14 @@ class TempController(TempControllerBase): # Fast model: same plant model but with zero transport delay, used # to predict the current temperature without the dead time. - self.model = Pot(dt, {**model_params, 'Td': 0}, theta_amb) + self.model = Pot(dt) + self.model.set_plant_params({**model_params, 'Td': 0}) + self.model.set_ambient_temperature(theta_amb) # Delayed model: keeps the plant's assumed transport delay, so it # can be compared like-for-like against the real (delayed) measurement. - self.model_delay = Pot(dt, model_params, theta_amb) + self.model_delay = Pot(dt) + self.model_delay.set_plant_params(model_params) + self.model_delay.set_ambient_temperature(theta_amb) self.theta_ist_plant = 0 self.theta_ist_model = 0 diff --git a/components/plant/pot.py b/components/plant/pot.py index f0ea0d8..09353ef 100644 --- a/components/plant/pot.py +++ b/components/plant/pot.py @@ -2,7 +2,7 @@ from components.aplant import APlant from components.plant import delay class Pot(APlant): - def __init__(self, dt, params, theta_amb=20): + def __init__(self, dt): APlant.__init__(self) self.dt = dt @@ -10,31 +10,49 @@ class Pot(APlant): self.x = 0 self.p_pot = 0 - # Plant specific thermal capacity [W*s/(kg*K)] - self.C = params['C'] + # Plant specific thermal capacity [W*s/(kg*K)], mass [kg], energy + # loss coefficient [W/(kg*K)] and transport propagation delay [s] + # - all None until set_plant_params() is called; process() checks + # for that rather than silently computing on bogus values. + self.C = None + self.M = None + self.L = None + self.Td = None + self.delay = None - # Plant mass [kg] - self.M = params['M'] + # Plant temperature [°C] - seeded from theta_amb the first time + # set_ambient_temperature() is called (see there), or overridden + # explicitly via initial(). None until either happens; + # process() checks for that too. + self.temp = None - # Plant energy loss coefficient [W/(kg*K)] - # Negative input power as a function of plant mass and temperature difference T_plant and T_ambient - # P_loss = L * Mass * (T_plant - T_ambient) - self.L = params['L'] - - # Energy transport propagation delay - self.Td = params['Td'] - - # Plant temperature [°C] - self.temp = theta_amb - - # Ambient temperature [°C] - self.theta_amb = theta_amb + # Ambient temperature [°C] - None until set_ambient_temperature() + # is called; process() checks for that as well. + self.theta_amb = None # Set power [W] self.p_in = 0 - # Plant delay [s] - self.delay = delay.Delay(dt, self.Td, 0) + def set_plant_params(self, params): + self.C = params['C'] + self.M = params['M'] + + # Negative input power as a function of plant mass and + # temperature difference T_plant and T_ambient: + # P_loss = L * Mass * (T_plant - T_ambient) + self.L = params['L'] + + self.Td = params['Td'] + self.delay = delay.Delay(self.dt, self.Td, 0) + + def set_ambient_temperature(self, theta_amb): + self.theta_amb = theta_amb + # Only seeds the plant's own starting temperature the first time + # this is called (i.e. there's no real temperature yet) - once a + # run is in progress, changing the ambient setting must not + # clobber whatever temperature has actually been simulated since. + if self.temp is None: + self.temp = theta_amb def initial(self, temp): self.temp = temp @@ -43,6 +61,11 @@ class Pot(APlant): pass def process(self): + if self.delay is None: + raise RuntimeError("Pot.process(): plant params not set - call set_plant_params() first") + if self.theta_amb is None: + raise RuntimeError("Pot.process(): ambient temperature not set - call set_ambient_temperature() first") + self.delay.put(self.p_in) p_loss = self.L * self.M * (self.temp - self.theta_amb) @@ -53,9 +76,6 @@ class Pot(APlant): def is_activated(self): return True - def set_ambient_temperature(self, theta_amb): - self.theta_amb = theta_amb - def set_thermal_params(self, M, C): self.M = M self.C = C diff --git a/components/sud_forecast.py b/components/sud_forecast.py index c14c77f..fbb74bf 100644 --- a/components/sud_forecast.py +++ b/components/sud_forecast.py @@ -61,7 +61,9 @@ class SudForecastEstimator: if not sud.load(doc) or not sud.schedule: return [0.0], [start_theta], SudState.DONE, [] - pot = Pot(self.dt, self.plant_params, self.theta_amb) + pot = Pot(self.dt) + pot.set_plant_params(self.plant_params) + pot.set_ambient_temperature(self.theta_amb) pot.initial(start_theta) tc = PidFactory.create(self.pid_type, self.dt, self.tempctrl_params, self.plant_params, theta_amb=self.theta_amb) tc.set_enabled(True) diff --git a/scripts/demos/pid/demo_temp_controller.py b/scripts/demos/pid/demo_temp_controller.py index e4a80b6..7ebacfa 100644 --- a/scripts/demos/pid/demo_temp_controller.py +++ b/scripts/demos/pid/demo_temp_controller.py @@ -45,7 +45,9 @@ if __name__ == '__main__': heatrate_soll = 1.25 ctrl = TempController(dt, ctrl_params) ctrl.set_enabled(True) - plant = Pot(dt, plant_params, theta_amb) + plant = Pot(dt) + plant.set_plant_params(plant_params) + plant.set_ambient_temperature(theta_amb) _y = np.empty(0) _fb = np.empty(0) _t = np.empty(0) diff --git a/scripts/demos/pid/demo_temp_controller_smith.py b/scripts/demos/pid/demo_temp_controller_smith.py index e9ca2ef..7534b5a 100644 --- a/scripts/demos/pid/demo_temp_controller_smith.py +++ b/scripts/demos/pid/demo_temp_controller_smith.py @@ -45,7 +45,9 @@ if __name__ == '__main__': heatrate_soll = 1.25 ctrl = TempController(dt, ctrl_params, plant_params, theta_amb) ctrl.set_enabled(True) - plant = Pot(dt, plant_params, theta_amb) + plant = Pot(dt) + plant.set_plant_params(plant_params) + plant.set_ambient_temperature(theta_amb) _y = np.empty(0) _fb = np.empty(0) _t = np.empty(0) diff --git a/scripts/demos/plant/demo_pot.py b/scripts/demos/plant/demo_pot.py index bf06a06..45e1520 100644 --- a/scripts/demos/plant/demo_pot.py +++ b/scripts/demos/plant/demo_pot.py @@ -19,7 +19,9 @@ if __name__ == '__main__': "gain" : 1.0 } - pot = Pot(dt, pot_params, theta_amb) + pot = Pot(dt) + pot.set_plant_params(pot_params) + pot.set_ambient_temperature(theta_amb) _t = np.empty(0) _temp = np.empty(0) diff --git a/scripts/demos/sud/demo_sud.py b/scripts/demos/sud/demo_sud.py index 7685a8b..aaa1e87 100644 --- a/scripts/demos/sud/demo_sud.py +++ b/scripts/demos/sud/demo_sud.py @@ -49,7 +49,9 @@ if __name__ == '__main__': } ctrl = TempController(dt, ctrl_params, plant_params, theta_amb) - plant = Pot(dt, plant_params, theta_amb) + plant = Pot(dt) + plant.set_plant_params(plant_params) + plant.set_ambient_temperature(theta_amb) stirrer = StirrerSim(dt) heater = HeaterSim({}) diff --git a/server/brewpi.py b/server/brewpi.py index 180a1a1..2ea7283 100755 --- a/server/brewpi.py +++ b/server/brewpi.py @@ -76,7 +76,9 @@ if __name__ == '__main__': taskmgr.add(sensor_task) # Plant - pot = Pot(DT, DEFAULT_PLANT_PARAMS, theta_amb) + pot = Pot(DT) + pot.set_plant_params(DEFAULT_PLANT_PARAMS) + pot.set_ambient_temperature(theta_amb) taskmgr.add(PotTask(pot, DT_TASK, dispatcher.msgio_get("Pot"))) # Heater