From f3beb289fa792bd737d662bf7f9c25769905c17c Mon Sep 17 00:00:00 2001 From: Malte Schaaf Date: Tue, 13 Jan 2026 09:55:39 +0100 Subject: [PATCH 1/6] Reset release notes Signed-off-by: Malte Schaaf --- RELEASE_NOTES.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 05fb4498c..61ee6f2ad 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,17 @@ # Frequenz Python SDK Release Notes -## Bug fixes +## Summary -- `FormulaEngine` and `FormulaEngine3Phase` are now type aliases to `Formula` and `Formula3Phase`, fixing a typing issue introduced in `v1.0.0-rc2202`. + + +## Upgrading + + + +## New Features + + + +## Bug Fixes + + From ab9a7f54639336939d27b4b5e34aa7d142aa3c4e Mon Sep 17 00:00:00 2001 From: Malte Schaaf Date: Mon, 12 Jan 2026 15:56:45 +0100 Subject: [PATCH 2/6] fix: use timedelta integer division in `count_covered` to avoid off-by-one Previously, floating-point division could produce off-by-one errors for count_covered due to rounding issues (e.g., 1.0 // 0.1 == 9). This change uses `timedelta // timedelta`, which operates on integer nanoseconds, ensuring the returned sample count is exact and preventing count_valid from exceeding count_covered during initial window fill. Signed-off-by: Malte Schaaf --- src/frequenz/sdk/timeseries/_ringbuffer/buffer.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/frequenz/sdk/timeseries/_ringbuffer/buffer.py b/src/frequenz/sdk/timeseries/_ringbuffer/buffer.py index 238b21245..7ef7bb844 100644 --- a/src/frequenz/sdk/timeseries/_ringbuffer/buffer.py +++ b/src/frequenz/sdk/timeseries/_ringbuffer/buffer.py @@ -705,10 +705,7 @@ def count_covered( The count of samples between the oldest and newest (inclusive) valid samples or 0 if there are is no time range covered. """ - return int( - self._covered_time_range(since, until).total_seconds() - // self._sampling_period.total_seconds() - ) + return self._covered_time_range(since, until) // self._sampling_period def count_valid( self, *, since: datetime | None = None, until: datetime | None = None From 988e76b82dc2f5f6479f02a2f73a65df857c978f Mon Sep 17 00:00:00 2001 From: Malte Schaaf Date: Mon, 12 Jan 2026 13:28:58 +0100 Subject: [PATCH 3/6] test: add `test_moving_window_length` to verify window count consistency Adds a new test `test_moving_window_length` that checks the moving window length without resampling. The test ensures that `count_valid` never exceeds `count_covered` during the initial fill and that the final count matches the expected window capacity. This helps prevent regressions related to off-by-one errors in window count calculations. Signed-off-by: Malte Schaaf --- tests/timeseries/test_moving_window.py | 27 ++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/timeseries/test_moving_window.py b/tests/timeseries/test_moving_window.py index 6fce1e077..dfba44015 100644 --- a/tests/timeseries/test_moving_window.py +++ b/tests/timeseries/test_moving_window.py @@ -560,6 +560,33 @@ async def test_resampling_window(fake_time: time_machine.Coordinates) -> None: assert 4.9 < value < 5.1 +async def test_moving_window_length(fake_time: time_machine.Coordinates) -> None: + """Test moving window length without resampling.""" + channel = Broadcast[Sample[Quantity]](name="net_power") + sender = channel.new_sender() + + window_size = timedelta(seconds=1) + input_sampling = timedelta(seconds=0.1) + + async with MovingWindow( + size=window_size, + resampled_data_recv=channel.new_receiver(), + input_sampling_period=input_sampling, + ) as window: + assert window.capacity == window_size / input_sampling, "Wrong window capacity" + assert window.count_valid() == 0, "Window should be empty at the beginning" + stream_values = [4.0, 8.0, 2.0, 6.0, 5.0] * 100 + for value in stream_values: + timestamp = datetime.now(tz=timezone.utc) + sample = Sample(timestamp, Quantity(float(value))) + await sender.send(sample) + await asyncio.sleep(0.1) + fake_time.shift(0.1) + assert window.count_valid() <= window.count_covered() + + assert window.count_valid() == window_size / input_sampling + + async def test_timestamps() -> None: """Test indexing a window by timestamp.""" window, sender = init_moving_window(timedelta(seconds=5)) From e08e266b81f6c58498f06a64d3ba8ed99518c4b7 Mon Sep 17 00:00:00 2001 From: Malte Schaaf Date: Tue, 13 Jan 2026 09:32:31 +0100 Subject: [PATCH 4/6] Remove unused mypy type ignores Signed-off-by: Malte Schaaf --- benchmarks/timeseries/periodic_feature_extractor.py | 3 +-- src/frequenz/sdk/timeseries/_periodic_feature_extractor.py | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/benchmarks/timeseries/periodic_feature_extractor.py b/benchmarks/timeseries/periodic_feature_extractor.py index 7d0deda30..56d5f60a0 100644 --- a/benchmarks/timeseries/periodic_feature_extractor.py +++ b/benchmarks/timeseries/periodic_feature_extractor.py @@ -71,8 +71,7 @@ def _calculate_avg_window( reshaped = feature_extractor._reshape_np_array( # pylint: disable=protected-access window, window_size ) - # ignoring the type because np.average returns Any - return np.average(reshaped[:, :window_size], axis=0) # type: ignore[no-any-return] + return np.average(reshaped[:, :window_size], axis=0) def _calculate_avg_window_py( diff --git a/src/frequenz/sdk/timeseries/_periodic_feature_extractor.py b/src/frequenz/sdk/timeseries/_periodic_feature_extractor.py index 01602f7b4..bef37adf2 100644 --- a/src/frequenz/sdk/timeseries/_periodic_feature_extractor.py +++ b/src/frequenz/sdk/timeseries/_periodic_feature_extractor.py @@ -409,6 +409,4 @@ def avg( The averaged timeseries window. """ (reshaped, window_size) = self._get_reshaped_np_array(start, end) - return np.average( # type: ignore[no-any-return] - reshaped[:, :window_size], axis=0, weights=weights - ) + return np.average(reshaped[:, :window_size], axis=0, weights=weights) From 85746b0e0db42508d0ab2ff3fd4e7b54aa5f5049 Mon Sep 17 00:00:00 2001 From: Malte Schaaf Date: Mon, 12 Jan 2026 16:06:31 +0100 Subject: [PATCH 5/6] Update release notes Signed-off-by: Malte Schaaf --- RELEASE_NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 61ee6f2ad..3dde3573e 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -14,4 +14,4 @@ ## Bug Fixes - +- Fixed an off-by-one calculation in `OrderedRingBuffer.count_covered` by switching to integer timedelta division, ensuring accurate sample counting for all window sizes and sampling periods. From 039de136a48f9ca55e5caff971e1eb63cb109f12 Mon Sep 17 00:00:00 2001 From: Malte Schaaf Date: Wed, 14 Jan 2026 13:22:46 +0100 Subject: [PATCH 6/6] Restrict "frequenz-microgrid-component-graph" to < 0.3.4 due to breaking changes - Updated the maximum version of "frequenz-microgrid-component-graph" from `< 0.3.4` to `<= 0.3.3`. - This change ensures compatibility, as version 0.3.4 introduces breaking changes. Signed-off-by: Malte Schaaf --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a98758614..5c6994440 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ dependencies = [ # changing the version # (plugins.mkdocstrings.handlers.python.import) "frequenz-client-microgrid >= 0.18.1, < 0.19.0", - "frequenz-microgrid-component-graph >= 0.3.2, < 0.4", + "frequenz-microgrid-component-graph >= 0.3.2, < 0.3.4", "frequenz-client-common >= 0.3.6, < 0.4.0", "frequenz-channels >= 1.6.1, < 2.0.0", "frequenz-quantities[marshmallow] >= 1.0.0, < 2.0.0",