diff --git a/smart_control/simulator/dbo_pump.py b/smart_control/simulator/dbo_pump.py new file mode 100644 index 00000000..358455d8 --- /dev/null +++ b/smart_control/simulator/dbo_pump.py @@ -0,0 +1,147 @@ +"""Models a pump for the simulation.""" + +import enum +from typing import Optional +import uuid + +import gin + +from smart_control.proto import smart_control_building_pb2 +from smart_control.simulator import smart_device +from smart_control.utils import constants + + +RunStatus = enum.Enum('RunStatus', [('On', 1), ('Off', 2)]) + + +@gin.configurable +class Pump(smart_device.SmartDevice): + """Models a water pump. + + Attributes: + differential_pressure: Differential pressure of the pump in bars. + run_command: Command indicating if the pump is on or off + _water_pump_differential_head: Length in meters of pump head. + _water_pump_efficiency: Electrical efficiency of water pump [0,1]. + """ + + def __init__( + self, + water_pump_differential_head: float, + water_pump_efficiency: float, + device_id: Optional[str] = None, + ): + observable_fields = { + 'differential_pressure': smart_device.AttributeInfo( + 'differential_pressure', float + ), + 'run_command': smart_device.AttributeInfo('run_command', RunStatus), + } + + action_fields = { + 'differential_pressure': smart_device.AttributeInfo( + 'differential_pressure', float + ), + 'run_command': smart_device.AttributeInfo('run_command', RunStatus), + } + + if device_id is None: + device_id = f'pump_id_{uuid.uuid4()}' + + super().__init__( + observable_fields, + action_fields, + device_type=smart_control_building_pb2.DeviceInfo.DeviceType.PMP, + device_id=device_id, + ) + + self._init_water_pump_differential_head = water_pump_differential_head + self._init_water_pump_efficiency = water_pump_efficiency + self._init_run_command = RunStatus.Off + self.reset() + + def reset(self): + self._water_pump_differential_head = self._init_water_pump_differential_head + self._water_pump_efficiency = self._init_water_pump_efficiency + self._run_command = self._init_run_command + + def compute_pump_power(self, total_flow_rate_demand) -> float: + """Returns power consumed by pump in W to move water to VAVs. + + derived from: https://www.engineeringtoolbox.com/pumps-power-d_505.html + + Args: + total_flow_rate_demand: The total flow rate of water through the pump in + m3/s. + """ + return ( + total_flow_rate_demand + * constants.WATER_DENSITY + * constants.GRAVITY + * self._water_pump_differential_head + / self._water_pump_efficiency + ) + + def _convert_differential_head_to_pressure( + self, differential_head: float + ) -> float: + """Converts a differential head (m) to differential pressure (bar). + + formula derived from: + https://www.engineeringtoolbox.com/pump-head-pressure-d_663.html + pressure (pa) = fluid_density * gravity * differential_head + water density = 1000 kg/m^3, gravity = 9.81 m/s^2, + so we get p= 9810 * differential_head + now, to convert to bar, 1 bar = 100,000 Pa, so we get: + pressure (bar) = differential_head * 0.0981 + (specific gravity of water is 1 so we leave it as is) + + Args: + differential_head: The differential head of the pump in meters. + + Returns: + The differential pressure of the pump in bars. + """ + return ( + constants.GRAVITY + * constants.WATER_DENSITY + * differential_head + / constants.PASCALS_PER_BAR + ) + + def _convert_pressure_to_differential_head(self, pressure: float) -> float: + """Converts a differential pressure (bar) to differential head (m). + + We simmple reverse the conversion above: + differential_head = pressure / 0.0981 + This simplifies to differential_head = pressure * 10.1937 + + Args: + pressure: The differential pressure of the pump in bars. + + Returns: + The differential head of the pump in meters. + """ + return pressure / ( + constants.GRAVITY * constants.WATER_DENSITY / constants.PASCALS_PER_BAR + ) + + @property + def differential_pressure(self) -> float: + return self._convert_differential_head_to_pressure( + self._water_pump_differential_head + ) + + @differential_pressure.setter + def differential_pressure(self, value: float) -> None: + self._water_pump_differential_head = ( + self._convert_pressure_to_differential_head(value) + ) + + @property + def run_command(self) -> RunStatus: + return self._run_command + + @run_command.setter + def run_command(self, value: RunStatus) -> None: + self._run_command = value diff --git a/smart_control/simulator/dbo_pump_test.py b/smart_control/simulator/dbo_pump_test.py new file mode 100644 index 00000000..3744deac --- /dev/null +++ b/smart_control/simulator/dbo_pump_test.py @@ -0,0 +1,97 @@ +"""Tests for Dbo compliant pump.""" + +from absl.testing import absltest +from absl.testing import parameterized + +from smart_buildings.smart_control.simulator import dbo_pump +from smart_buildings.smart_control.utils import constants + + +class DboPumpTest(parameterized.TestCase): + + @parameterized.parameters( + (0.5, 3, 0.9), + (0.2, 7, 0.5), + (0.5, 8, 0.23), + (0.5, 9, 0.7), + ) + def test_compute_pump_power( + self, total_flow_rate, water_pump_differential_head, water_pump_efficiency + ): + pump = dbo_pump.Pump( + water_pump_differential_head=water_pump_differential_head, + water_pump_efficiency=water_pump_efficiency, + ) + + expected = ( + total_flow_rate + * constants.WATER_DENSITY + * constants.GRAVITY + * water_pump_differential_head + / water_pump_efficiency + ) + self.assertEqual(pump.compute_pump_power(total_flow_rate), expected) + + def test_run_command(self): + pump = dbo_pump.Pump( + water_pump_differential_head=3, + water_pump_efficiency=0.9, + ) + self.assertEqual(pump.run_command, dbo_pump.RunStatus.Off) + pump.run_command = dbo_pump.RunStatus.On + self.assertEqual(pump.run_command, dbo_pump.RunStatus.On) + + def test_differential_pressure(self): + water_pump_differential_head = 3 + pump = dbo_pump.Pump( + water_pump_differential_head=water_pump_differential_head, + water_pump_efficiency=0.9, + ) + expected_dp = ( + constants.GRAVITY + * constants.WATER_DENSITY + * water_pump_differential_head + / constants.PASCALS_PER_BAR + ) + self.assertAlmostEqual(pump.differential_pressure, expected_dp) + pump.differential_pressure = 30 + self.assertAlmostEqual(pump.differential_pressure, 30) + expected_head = 30 / ( + constants.GRAVITY * constants.WATER_DENSITY / constants.PASCALS_PER_BAR + ) + self.assertAlmostEqual(pump._water_pump_differential_head, expected_head) + + def test_pressure_conversion(self): + pump = dbo_pump.Pump( + water_pump_differential_head=3, + water_pump_efficiency=0.9, + ) + self.assertAlmostEqual( + pump._convert_pressure_to_differential_head( + pump._convert_differential_head_to_pressure(3) + ), + 3, + ) + self.assertAlmostEqual( + pump._convert_differential_head_to_pressure( + pump._convert_pressure_to_differential_head(30) + ), + 30, + ) + + def test_reset(self): + pump = dbo_pump.Pump( + water_pump_differential_head=3, + water_pump_efficiency=0.9, + ) + pump.run_command = dbo_pump.RunStatus.On + pump._water_pump_differential_head = 4 + pump._water_pump_efficiency = 0.1 + pump.reset() + self.assertEqual(pump.run_command, dbo_pump.RunStatus.Off) + self.assertEqual(pump._water_pump_differential_head, 3) + self.assertEqual(pump._water_pump_efficiency, 0.9) + + +if __name__ == '__main__': + absltest.main() diff --git a/smart_control/utils/constants.py b/smart_control/utils/constants.py index eaf20457..8b55d20d 100644 --- a/smart_control/utils/constants.py +++ b/smart_control/utils/constants.py @@ -22,6 +22,9 @@ WATTS_PER_BTU_HR: float = 0.29307107 # Number of Watts in a BTU/hr HZ_PERCENT: float = 100.0 / 60.0 # Converts blower/pump Hz to Percentage Power +# --------- Pressure Constants --------------- +PASCALS_PER_BAR: float = 100000.0 # Number of Pascals in a bar. + # https://www.rapidtables.com/convert/power/hp-to-watt.html WATTS_PER_HORSEPOWER = 746.0