diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 08daf4a8c..961e268c3 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -27,11 +27,12 @@ - Provide access to `capacity` (maximum number of elements) in `MovingWindow`. - Methods to retrieve oldest and newest timestamp of valid samples are added to both. - - Now when printing `FormulaEngine` for debugging purposes the the formula will be shown in infix notation, which should be easier to read. - The CI now runs cross-arch tests on `arm64` architectures. +- The `min` and `max` functions in the `FormulaEngine` are now public. Note that the same functions have been public in the builder. + ## Bug Fixes diff --git a/src/frequenz/sdk/timeseries/_formula_engine/_formula_engine.py b/src/frequenz/sdk/timeseries/_formula_engine/_formula_engine.py index d994b06d7..9b3744ae5 100644 --- a/src/frequenz/sdk/timeseries/_formula_engine/_formula_engine.py +++ b/src/frequenz/sdk/timeseries/_formula_engine/_formula_engine.py @@ -165,7 +165,7 @@ def __truediv__( """ return self._higher_order_builder(self, self._create_method) / other # type: ignore - def _max( + def max( self, other: _GenericEngine | _GenericHigherOrderBuilder | QuantityT ) -> _GenericHigherOrderBuilder: """Return a formula engine that outputs the maximum of `self` and `other`. @@ -180,7 +180,7 @@ def _max( """ return self._higher_order_builder(self, self._create_method).max(other) # type: ignore - def _min( + def min( self, other: _GenericEngine | _GenericHigherOrderBuilder | QuantityT ) -> _GenericHigherOrderBuilder: """Return a formula engine that outputs the minimum of `self` and `other`. diff --git a/tests/timeseries/_formula_engine/test_formula_composition.py b/tests/timeseries/_formula_engine/test_formula_composition.py index 42fc3394d..e97a50286 100644 --- a/tests/timeseries/_formula_engine/test_formula_composition.py +++ b/tests/timeseries/_formula_engine/test_formula_composition.py @@ -171,18 +171,68 @@ async def test_formula_composition_missing_bat(self, mocker: MockerFixture) -> N assert count == 10 async def test_formula_composition_min_max(self, mocker: MockerFixture) -> None: - """Test the composition of formulas with min/max values.""" + """Test the composition of formulas with the min and max.""" mockgrid = MockMicrogrid(grid_meter=True) + mockgrid.add_chps(1) await mockgrid.start(mocker) logical_meter = microgrid.logical_meter() - engine_min = logical_meter.grid_power._min( # pylint: disable=protected-access - Power.zero() - ).build("grid_power_min") + engine_min = logical_meter.grid_power.min(logical_meter.chp_power).build( + "grid_power_min" + ) + engine_min_rx = engine_min.new_receiver() + engine_max = logical_meter.grid_power.max(logical_meter.chp_power).build( + "grid_power_max" + ) + engine_max_rx = engine_max.new_receiver() + + await mockgrid.mock_resampler.send_meter_power([100.0, 200.0]) + + # Test min + min_pow = await engine_min_rx.receive() + assert ( + min_pow and min_pow.value and min_pow.value.isclose(Power.from_watts(100.0)) + ) + + # Test max + max_pow = await engine_max_rx.receive() + assert ( + max_pow and max_pow.value and max_pow.value.isclose(Power.from_watts(200.0)) + ) + + await mockgrid.mock_resampler.send_meter_power([-100.0, -200.0]) + + # Test min + min_pow = await engine_min_rx.receive() + assert ( + min_pow + and min_pow.value + and min_pow.value.isclose(Power.from_watts(-200.0)) + ) + + # Test max + max_pow = await engine_max_rx.receive() + assert ( + max_pow + and max_pow.value + and max_pow.value.isclose(Power.from_watts(-100.0)) + ) + + await engine_min._stop() # pylint: disable=protected-access + await mockgrid.cleanup() + await logical_meter.stop() + + async def test_formula_composition_min_max_const( + self, mocker: MockerFixture + ) -> None: + """Test the compositing formulas and constants with the min and max functions.""" + mockgrid = MockMicrogrid(grid_meter=True) + await mockgrid.start(mocker) + + logical_meter = microgrid.logical_meter() + engine_min = logical_meter.grid_power.min(Power.zero()).build("grid_power_min") engine_min_rx = engine_min.new_receiver() - engine_max = logical_meter.grid_power._max( # pylint: disable=protected-access - Power.zero() - ).build("grid_power_max") + engine_max = logical_meter.grid_power.max(Power.zero()).build("grid_power_max") engine_max_rx = engine_max.new_receiver() await mockgrid.mock_resampler.send_meter_power([100.0]) diff --git a/tests/timeseries/test_formula_engine.py b/tests/timeseries/test_formula_engine.py index 45b1ecf3c..7d0928a5f 100644 --- a/tests/timeseries/test_formula_engine.py +++ b/tests/timeseries/test_formula_engine.py @@ -439,6 +439,78 @@ async def test_simple(self) -> None: ], ) + async def test_min_max(self) -> None: + """Test min and max functions in combination.""" + await self.run_test( + 3, + lambda c2, c4, c5: c2.min(c4).max(c5), + [ + ([4.0, 6.0, 5.0], 5.0), + ], + ) + + async def test_max(self) -> None: + """Test the max function.""" + await self.run_test( + 3, + lambda c2, c4, c5: c2 * c4.max(c5), + [ + ([10.0, 12.0, 15.0], 150.0), + ], + ) + await self.run_test( + 3, + lambda c2, c4, c5: (c2 * c4).max(c5), + [ + ([10.0, 12.0, 15.0], 120.0), + ], + ) + await self.run_test( + 3, + lambda c2, c4, c5: (c2 + c4).max(c5), + [ + ([10.0, 12.0, 15.0], 22.0), + ], + ) + await self.run_test( + 3, + lambda c2, c4, c5: c2 + c4.max(c5), + [ + ([10.0, 12.0, 15.0], 25.0), + ], + ) + + async def test_min(self) -> None: + """Test the min function.""" + await self.run_test( + 3, + lambda c2, c4, c5: (c2 * c4).min(c5), + [ + ([10.0, 12.0, 15.0], 15.0), + ], + ) + await self.run_test( + 3, + lambda c2, c4, c5: c2 * c4.min(c5), + [ + ([10.0, 12.0, 15.0], 120.0), + ], + ) + await self.run_test( + 3, + lambda c2, c4, c5: (c2 + c4).min(c5), + [ + ([10.0, 2.0, 15.0], 12.0), + ], + ) + await self.run_test( + 3, + lambda c2, c4, c5: c2 + c4.min(c5), + [ + ([10.0, 12.0, 15.0], 22.0), + ], + ) + async def test_compound(self) -> None: """Test compound formulas.""" await self.run_test(