Skip to content

Commit 8ad2249

Browse files
authored
Fix the resampler handling of output zeros (#811)
A bug made the resampler interpret output zero values as `None`, producing wrong resampled values when the result of the resampling function is zero. Fixes #810.
2 parents 90ad331 + 413296b commit 8ad2249

File tree

3 files changed

+117
-3
lines changed

3 files changed

+117
-3
lines changed

RELEASE_NOTES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,6 @@
5858

5959
- 0W power requests are now not adjusted to exclusion bounds by the `PowerManager` and `PowerDistributor`, and are sent over to the microgrid API directly.
6060
- Fixed that `microgrid.frequency()` was sending `Quantity` objects instead of `Frequency`.
61+
- The resampler now properly handles sending zero values.
62+
63+
A bug made the resampler interpret zero values as `None` when generating new samples, so if the result of the resampling is zero, the resampler would just produce `None` values.

src/frequenz/sdk/timeseries/_resampling.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -744,7 +744,7 @@ def resample(self, timestamp: datetime) -> Sample[Quantity]:
744744
if relevant_samples
745745
else None
746746
)
747-
return Sample(timestamp, None if not value else Quantity(value))
747+
return Sample(timestamp, None if value is None else Quantity(value))
748748

749749

750750
class _StreamingHelper:

tests/timeseries/test_resampling.py

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -607,7 +607,7 @@ async def test_resampling_with_one_window(
607607
#
608608
# t(s) 0 1 2 2.5 3 4
609609
# |----------|----------R----|-----|----------R-----> (no more samples)
610-
# value 5.0 12.0 2.0 4.0 5.0
610+
# value 5.0 12.0 0.0 4.0 5.0
611611
#
612612
# R = resampling is done
613613

@@ -637,7 +637,7 @@ async def test_resampling_with_one_window(
637637
resampling_fun_mock.reset_mock()
638638

639639
# Second resampling run
640-
sample2_5s = Sample(timestamp + timedelta(seconds=2.5), value=Quantity(2.0))
640+
sample2_5s = Sample(timestamp + timedelta(seconds=2.5), value=Quantity.zero())
641641
sample3s = Sample(timestamp + timedelta(seconds=3), value=Quantity(4.0))
642642
sample4s = Sample(timestamp + timedelta(seconds=4), value=Quantity(5.0))
643643
await source_sender.send(sample2_5s)
@@ -1195,6 +1195,117 @@ async def test_timer_is_aligned(
11951195
resampling_fun_mock.reset_mock()
11961196

11971197

1198+
async def test_resampling_all_zeros(
1199+
fake_time: time_machine.Coordinates, source_chan: Broadcast[Sample[Quantity]]
1200+
) -> None:
1201+
"""Test resampling with one resampling window full of zeros."""
1202+
timestamp = datetime.now(timezone.utc)
1203+
1204+
resampling_period_s = 2
1205+
expected_resampled_value = 0.0
1206+
1207+
resampling_fun_mock = MagicMock(
1208+
spec=ResamplingFunction, return_value=expected_resampled_value
1209+
)
1210+
config = ResamplerConfig(
1211+
resampling_period=timedelta(seconds=resampling_period_s),
1212+
max_data_age_in_periods=1.0,
1213+
resampling_function=resampling_fun_mock,
1214+
initial_buffer_len=4,
1215+
)
1216+
resampler = Resampler(config)
1217+
1218+
source_receiver = source_chan.new_receiver()
1219+
source_sender = source_chan.new_sender()
1220+
1221+
sink_mock = AsyncMock(spec=Sink, return_value=True)
1222+
1223+
resampler.add_timeseries("test", source_receiver, sink_mock)
1224+
source_props = resampler.get_source_properties(source_receiver)
1225+
1226+
# Test timeline
1227+
#
1228+
# t(s) 0 1 2 2.5 3 4
1229+
# |----------|----------R----|-----|----------R-----> (no more samples)
1230+
# value 0.0 0.0 0.0 0.0 0.0
1231+
#
1232+
# R = resampling is done
1233+
1234+
# Send a few samples and run a resample tick, advancing the fake time by one period
1235+
sample0s = Sample(timestamp, value=Quantity.zero())
1236+
sample1s = Sample(timestamp + timedelta(seconds=1), value=Quantity.zero())
1237+
await source_sender.send(sample0s)
1238+
await source_sender.send(sample1s)
1239+
await _advance_time(fake_time, resampling_period_s)
1240+
await resampler.resample(one_shot=True)
1241+
1242+
assert datetime.now(timezone.utc).timestamp() == 2
1243+
sink_mock.assert_called_once_with(
1244+
Sample(
1245+
timestamp + timedelta(seconds=resampling_period_s),
1246+
Quantity(expected_resampled_value),
1247+
)
1248+
)
1249+
resampling_fun_mock.assert_called_once_with(
1250+
a_sequence(sample1s), config, source_props
1251+
)
1252+
assert source_props == SourceProperties(
1253+
sampling_start=timestamp, received_samples=2, sampling_period=None
1254+
)
1255+
assert _get_buffer_len(resampler, source_receiver) == config.initial_buffer_len
1256+
sink_mock.reset_mock()
1257+
resampling_fun_mock.reset_mock()
1258+
1259+
# Second resampling run
1260+
sample2_5s = Sample(timestamp + timedelta(seconds=2.5), value=Quantity.zero())
1261+
sample3s = Sample(timestamp + timedelta(seconds=3), value=Quantity.zero())
1262+
sample4s = Sample(timestamp + timedelta(seconds=4), value=Quantity.zero())
1263+
await source_sender.send(sample2_5s)
1264+
await source_sender.send(sample3s)
1265+
await source_sender.send(sample4s)
1266+
await _advance_time(fake_time, resampling_period_s)
1267+
await resampler.resample(one_shot=True)
1268+
1269+
assert datetime.now(timezone.utc).timestamp() == 4
1270+
sink_mock.assert_called_once_with(
1271+
Sample(
1272+
timestamp + timedelta(seconds=resampling_period_s * 2),
1273+
Quantity(expected_resampled_value),
1274+
)
1275+
)
1276+
resampling_fun_mock.assert_called_once_with(
1277+
a_sequence(sample2_5s, sample3s, sample4s), config, source_props
1278+
)
1279+
# By now we have a full buffer (5 samples and a buffer of length 4), which
1280+
# we received in 4 seconds, so we have an input period of 0.8s.
1281+
assert source_props == SourceProperties(
1282+
sampling_start=timestamp,
1283+
received_samples=5,
1284+
sampling_period=timedelta(seconds=0.8),
1285+
)
1286+
# The buffer should be able to hold 2 seconds of data, and data is coming
1287+
# every 0.8 seconds, so we should be able to store 3 samples.
1288+
assert _get_buffer_len(resampler, source_receiver) == 3
1289+
sink_mock.reset_mock()
1290+
resampling_fun_mock.reset_mock()
1291+
1292+
await _assert_no_more_samples(
1293+
resampler,
1294+
timestamp,
1295+
sink_mock,
1296+
resampling_fun_mock,
1297+
fake_time,
1298+
resampling_period_s,
1299+
current_iteration=3,
1300+
)
1301+
assert source_props == SourceProperties(
1302+
sampling_start=timestamp,
1303+
received_samples=5,
1304+
sampling_period=timedelta(seconds=0.8),
1305+
)
1306+
assert _get_buffer_len(resampler, source_receiver) == 3
1307+
1308+
11981309
def _get_buffer_len(resampler: Resampler, source_receiver: Source) -> int:
11991310
# pylint: disable=protected-access
12001311
blen = resampler._resamplers[source_receiver]._helper._buffer.maxlen

0 commit comments

Comments
 (0)