diff --git a/pioreactor/automations/temperature/thermostat.py b/pioreactor/automations/temperature/thermostat.py index 39acad76..4b8ac1bf 100644 --- a/pioreactor/automations/temperature/thermostat.py +++ b/pioreactor/automations/temperature/thermostat.py @@ -26,13 +26,12 @@ class Thermostat(TemperatureAutomationJob): def __init__(self, target_temperature: float | str, **kwargs) -> None: super().__init__(**kwargs) assert target_temperature is not None, "target_temperature must be set" - self.target_temperature = float(target_temperature) self.pid = PID( Kp=config.getfloat("temperature_automation.thermostat", "Kp"), Ki=config.getfloat("temperature_automation.thermostat", "Ki"), Kd=config.getfloat("temperature_automation.thermostat", "Kd"), - setpoint=self.target_temperature, + setpoint=None, unit=self.unit, experiment=self.experiment, job_name=self.job_name, @@ -40,9 +39,19 @@ def __init__(self, target_temperature: float | str, **kwargs) -> None: output_limits=(-25, 25), # avoid whiplashing ) + self.set_target_temperature(target_temperature) + if not is_pio_job_running("stirring"): self.logger.warning("It's recommended to have stirring on when using the thermostat.") + def _clamp_target_temperature(self, target_temperature: float) -> float: + if target_temperature > self.MAX_TARGET_TEMP: + self.logger.warning( + f"Values over {self.MAX_TARGET_TEMP}℃ are not supported. Setting to {self.MAX_TARGET_TEMP}℃." + ) + + return clamp(0.0, target_temperature, self.MAX_TARGET_TEMP) + def execute(self) -> UpdatedHeaterDC: while not hasattr(self, "pid"): # sometimes when initializing, this execute can run before the subclasses __init__ is resolved. @@ -61,10 +70,12 @@ def execute(self) -> UpdatedHeaterDC: data={ "current_dc": self.heater_duty_cycle, "delta_dc": output, + "target_temperature": self.target_temperature, + "latest_temperature": self.latest_temperature, }, ) - def set_target_temperature(self, target_temperature: float) -> None: + def set_target_temperature(self, target_temperature: float | str) -> None: """ Parameters @@ -77,11 +88,5 @@ def set_target_temperature(self, target_temperature: float) -> None: """ target_temperature = float(target_temperature) - if target_temperature > self.MAX_TARGET_TEMP: - self.logger.warning( - f"Values over {self.MAX_TARGET_TEMP}℃ are not supported. Setting to {self.MAX_TARGET_TEMP}℃." - ) - - target_temperature = clamp(0, target_temperature, self.MAX_TARGET_TEMP) - self.target_temperature = target_temperature + self.target_temperature = self._clamp_target_temperature(target_temperature) self.pid.set_setpoint(self.target_temperature) diff --git a/pioreactor/plugin_management/list_plugins.py b/pioreactor/plugin_management/list_plugins.py index 6c9e4318..ede34ecc 100644 --- a/pioreactor/plugin_management/list_plugins.py +++ b/pioreactor/plugin_management/list_plugins.py @@ -9,6 +9,8 @@ @click.command(name="list", short_help="list the installed plugins") @click.option("--json", is_flag=True, help="output as json") def click_list_plugins(json: bool) -> None: + # this is to initialize all the modules, to plugins don't fail when being loaded. + from pioreactor.cli import run # noqa: F403, F401 from pioreactor.plugin_management import get_plugins if not json: diff --git a/pioreactor/tests/test_temperature_approximation_1_0.py b/pioreactor/tests/test_temperature_approximation_1_0.py index 288a57ba..616b53db 100644 --- a/pioreactor/tests/test_temperature_approximation_1_0.py +++ b/pioreactor/tests/test_temperature_approximation_1_0.py @@ -897,7 +897,7 @@ def test_temperature_approximation17(self) -> None: assert 25.295 <= self.t.approximate_temperature_1_0(features) <= 25.430 - def test_temperature_approximation19(self) -> None: + def test_temperature_approximation21(self) -> None: # this was real data from a user ts_of_temps = [ @@ -944,3 +944,43 @@ def test_temperature_approximation19(self) -> None: } assert better_room_temp < self.t.approximate_temperature_1_0(features) <= 25 + + def test_temperature_approximation19(self) -> None: + # this was real data from a user + features = { + "previous_heater_dc": 1.3, + "room_temp": 22.0, + "time_series_of_temp": [ + 56.4375, + 56.375, + 56.3125, + 56.302083333333336, + 56.25, + 56.25, + 56.1875, + 56.1875, + 56.177083333333336, + 56.135416666666664, + 56.125, + 56.125, + 56.104166666666664, + 56.083333333333336, + 56.0625, + 56.0625, + 56.0625, + 56.0625, + 56.0625, + 56.041666666666664, + 56.052083333333336, + 56.020833333333336, + 56.0, + 56.0, + 56.0, + 56.0, + 56.0, + 56.0, + 56.0, + ], + } + + assert 55.5 <= self.t.approximate_temperature_1_0(features) <= 56.5 diff --git a/pioreactor/utils/streaming_calculations.py b/pioreactor/utils/streaming_calculations.py index 7187e4f1..89916d60 100644 --- a/pioreactor/utils/streaming_calculations.py +++ b/pioreactor/utils/streaming_calculations.py @@ -428,7 +428,7 @@ def __init__( Kp: float, Ki: float, Kd: float, - setpoint: float, + setpoint: float | None, output_limits: tuple[Optional[float], Optional[float]] = (None, None), sample_time: Optional[float] = None, unit: Optional[str] = None, @@ -478,6 +478,7 @@ def update(self, input_: float, dt: float = 1.0) -> float: Updates the controller's internal state with the current error and time step, and returns the controller output. """ + assert isinstance(self.setpoint, float) error = self.setpoint - input_ # Update error sum with clamping for anti-windup