Skip to content

Commit 26590c0

Browse files
authored
Replace len magic with count_valid method in ring buffer and moving window (#664)
The current len magic returns the count of samples in the ring buffer / moving window that are determined valid. This definition is not unambiguously obvious to the caller. The current logic is therefore moved to its own more descriptive method and len magic are removed.
2 parents 910bd9c + 7dc661f commit 26590c0

File tree

7 files changed

+45
-36
lines changed

7 files changed

+45
-36
lines changed

RELEASE_NOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
- Provide access to `capacity` (maximum number of elements) in `MovingWindow`.
4646
- Methods to retrieve oldest and newest timestamp of valid samples are added to both.
4747
- `MovingWindow` exposes underlying buffers `window` method.
48+
- `len(window)` and `len(buffer)` should be replaced with `window.count_valid()` and `buffer.count_valid()`, respectively.
4849
- `OrderedRingBuffer.window`:
4950
- By default returns a copy.
5051
- Can also return a view if the window contains `None` values and if `force_copy` is set to `True`.

benchmarks/timeseries/periodic_feature_extractor.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,16 @@ def _num_windows(
115115
Returns:
116116
The number of windows that are fully contained in the MovingWindow.
117117
"""
118-
num_windows = len(window) // period
119-
if len(window) - num_windows * period >= window_size:
118+
119+
def length(window: NDArray[np.float_] | MovingWindow) -> int:
120+
return (
121+
window.count_valid()
122+
if isinstance(window, MovingWindow)
123+
else len(window)
124+
)
125+
126+
num_windows = length(window) // period
127+
if length(window) - num_windows * period >= window_size:
120128
num_windows += 1
121129

122130
return num_windows

src/frequenz/sdk/timeseries/_moving_window.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -306,14 +306,14 @@ async def sink_buffer(sample: Sample[Quantity]) -> None:
306306
asyncio.create_task(self._resampler.resample(), name="resample")
307307
)
308308

309-
def __len__(self) -> int:
309+
def count_valid(self) -> int:
310310
"""
311-
Return the size of the `MovingWindow`s underlying buffer.
311+
Count the number of valid samples in this `MovingWindow`.
312312
313313
Returns:
314-
The size of the `MovingWindow`.
314+
The number of valid samples in this `MovingWindow`.
315315
"""
316-
return len(self._buffer)
316+
return self._buffer.count_valid()
317317

318318
@overload
319319
def __getitem__(self, key: SupportsIndex) -> float:
@@ -362,12 +362,12 @@ def __getitem__(self, key: SupportsIndex | datetime | slice) -> float | ArrayLik
362362
A float if the key is a number or a timestamp.
363363
an numpy array if the key is a slice.
364364
"""
365-
if len(self._buffer) == 0:
365+
if self._buffer.count_valid() == 0:
366366
raise IndexError("The buffer is empty.")
367367
if isinstance(key, slice):
368368
if isinstance(key.start, int) or isinstance(key.stop, int):
369369
if key.start is None or key.stop is None:
370-
key = slice(slice(key.start, key.stop).indices(self.__len__()))
370+
key = slice(slice(key.start, key.stop).indices(self.count_valid()))
371371
elif isinstance(key.start, datetime) or isinstance(key.stop, datetime):
372372
if key.start is None:
373373
key = slice(self._buffer.time_bound_oldest, key.stop)

src/frequenz/sdk/timeseries/_periodic_feature_extractor.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,13 +128,13 @@ def __init__(
128128
"""Distance between two succeeding intervals in samples."""
129129

130130
_logger.debug("Initializing PeriodicFeatureExtractor!")
131-
_logger.debug("MovingWindow size: %i", len(self._moving_window))
131+
_logger.debug("MovingWindow size: %i", self._moving_window.count_valid())
132132
_logger.debug(
133133
"Period between two succeeding intervals (in samples): %i",
134134
self._period,
135135
)
136136

137-
if not len(self._moving_window) % self._period == 0:
137+
if not self._moving_window.count_valid() % self._period == 0:
138138
raise ValueError(
139139
"The MovingWindow size is not a integer multiple of the period."
140140
)
@@ -323,7 +323,7 @@ def _get_buffer_bounds(
323323

324324
rel_pos = self._get_relative_positions(start, window_size)
325325

326-
if window_size > len(self._moving_window):
326+
if window_size > self._moving_window.count_valid():
327327
raise ValueError(
328328
"The window size must be smaller than the size of the `MovingWindow`"
329329
)
@@ -379,7 +379,7 @@ def _get_reshaped_np_array(
379379
(start_pos, end_pos, window_size) = self._get_buffer_bounds(start, end)
380380

381381
if start_pos >= end_pos:
382-
window_start = self._buffer[start_pos : len(self._moving_window)]
382+
window_start = self._buffer[start_pos : self._moving_window.count_valid()]
383383
window_end = self._buffer[0:end_pos]
384384
# make the linter happy
385385
assert isinstance(window_start, np.ndarray)

src/frequenz/sdk/timeseries/_ringbuffer/buffer.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ def oldest_timestamp(self) -> datetime | None:
202202
The oldest timestamp in the buffer
203203
or None if the buffer is empty.
204204
"""
205-
if len(self) == 0:
205+
if self.count_valid() == 0:
206206
return None
207207

208208
if self.is_missing(self.time_bound_oldest):
@@ -217,7 +217,7 @@ def newest_timestamp(self) -> datetime | None:
217217
Returns:
218218
The newest timestamp in the buffer.
219219
"""
220-
if len(self) == 0:
220+
if self.count_valid() == 0:
221221
return None
222222

223223
return self.time_bound_newest
@@ -594,11 +594,11 @@ def __getitem__(self, index_or_slice: SupportsIndex | slice) -> float | FloatArr
594594
"""
595595
return self._buffer.__getitem__(index_or_slice)
596596

597-
def __len__(self) -> int:
598-
"""Return the amount of items that this container currently holds.
597+
def count_valid(self) -> int:
598+
"""Count the number of valid items that this buffer currently holds.
599599
600600
Returns:
601-
The length.
601+
The number of valid items in this buffer.
602602
"""
603603
if self._datetime_newest == self._DATETIME_MIN:
604604
return 0

tests/timeseries/test_moving_window.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -143,13 +143,13 @@ async def test_window_size() -> None:
143143
window, sender = init_moving_window(timedelta(seconds=5))
144144
async with window:
145145
assert window.capacity == 5, "Wrong window capacity"
146-
assert len(window) == 0, "Window should be empty"
146+
assert window.count_valid() == 0, "Window should be empty"
147147
await push_logical_meter_data(sender, range(0, 2))
148148
assert window.capacity == 5, "Wrong window capacity"
149-
assert len(window) == 2, "Window should be partially full"
149+
assert window.count_valid() == 2, "Window should be partially full"
150150
await push_logical_meter_data(sender, range(2, 20))
151151
assert window.capacity == 5, "Wrong window capacity"
152-
assert len(window) == 5, "Window should be full"
152+
assert window.count_valid() == 5, "Window should be full"
153153

154154

155155
# pylint: disable=redefined-outer-name
@@ -170,7 +170,7 @@ async def test_resampling_window(fake_time: time_machine.Coordinates) -> None:
170170
resampler_config=resampler_config,
171171
) as window:
172172
assert window.capacity == window_size / output_sampling, "Wrong window capacity"
173-
assert len(window) == 0, "Window should be empty at the beginning"
173+
assert window.count_valid() == 0, "Window should be empty at the beginning"
174174
stream_values = [4.0, 8.0, 2.0, 6.0, 5.0] * 100
175175
for value in stream_values:
176176
timestamp = datetime.now(tz=timezone.utc)
@@ -179,7 +179,7 @@ async def test_resampling_window(fake_time: time_machine.Coordinates) -> None:
179179
await asyncio.sleep(0.1)
180180
fake_time.shift(0.1)
181181

182-
assert len(window) == window_size / output_sampling
182+
assert window.count_valid() == window_size / output_sampling
183183
for value in window: # type: ignore
184184
assert 4.9 < value < 5.1
185185

tests/timeseries/test_ringbuffer.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -209,69 +209,69 @@ def test_gaps() -> None: # pylint: disable=too-many-statements
209209
buffer = OrderedRingBuffer([0.0] * 5, ONE_SECOND)
210210
assert buffer.oldest_timestamp is None
211211
assert buffer.newest_timestamp is None
212-
assert len(buffer) == 0
212+
assert buffer.count_valid() == 0
213213
assert len(buffer.gaps) == 0
214214

215215
buffer.update(Sample(dt(0), Quantity(0)))
216216
assert buffer.oldest_timestamp == dt(0)
217217
assert buffer.newest_timestamp == dt(0)
218-
assert len(buffer) == 1
218+
assert buffer.count_valid() == 1
219219
assert len(buffer.gaps) == 1
220220

221221
buffer.update(Sample(dt(6), Quantity(0)))
222222
assert buffer.oldest_timestamp == dt(6)
223223
assert buffer.newest_timestamp == dt(6)
224-
assert len(buffer) == 1
224+
assert buffer.count_valid() == 1
225225
assert len(buffer.gaps) == 1
226226

227227
buffer.update(Sample(dt(2), Quantity(2)))
228228
buffer.update(Sample(dt(3), Quantity(3)))
229229
buffer.update(Sample(dt(4), Quantity(4)))
230230
assert buffer.oldest_timestamp == dt(2)
231231
assert buffer.newest_timestamp == dt(6)
232-
assert len(buffer) == 4
232+
assert buffer.count_valid() == 4
233233
assert len(buffer.gaps) == 1
234234

235235
buffer.update(Sample(dt(3), None))
236236
assert buffer.oldest_timestamp == dt(2)
237237
assert buffer.newest_timestamp == dt(6)
238-
assert len(buffer) == 3
238+
assert buffer.count_valid() == 3
239239
assert len(buffer.gaps) == 2
240240

241241
buffer.update(Sample(dt(3), Quantity(np.nan)))
242242
assert buffer.oldest_timestamp == dt(2)
243243
assert buffer.newest_timestamp == dt(6)
244-
assert len(buffer) == 3
244+
assert buffer.count_valid() == 3
245245
assert len(buffer.gaps) == 2
246246

247247
buffer.update(Sample(dt(2), Quantity(np.nan)))
248248
assert buffer.oldest_timestamp == dt(4)
249249
assert buffer.newest_timestamp == dt(6)
250-
assert len(buffer) == 2
250+
assert buffer.count_valid() == 2
251251
assert len(buffer.gaps) == 2
252252

253253
buffer.update(Sample(dt(3), Quantity(3)))
254254
assert buffer.oldest_timestamp == dt(3)
255255
assert buffer.newest_timestamp == dt(6)
256-
assert len(buffer) == 3
256+
assert buffer.count_valid() == 3
257257
assert len(buffer.gaps) == 2
258258

259259
buffer.update(Sample(dt(2), Quantity(2)))
260260
assert buffer.oldest_timestamp == dt(2)
261261
assert buffer.newest_timestamp == dt(6)
262-
assert len(buffer) == 4
262+
assert buffer.count_valid() == 4
263263
assert len(buffer.gaps) == 1
264264

265265
buffer.update(Sample(dt(5), Quantity(5)))
266266
assert buffer.oldest_timestamp == dt(2)
267267
assert buffer.newest_timestamp == dt(6)
268-
assert len(buffer) == 5
268+
assert buffer.count_valid() == 5
269269
assert len(buffer.gaps) == 0
270270

271271
buffer.update(Sample(dt(99), None))
272272
assert buffer.oldest_timestamp == dt(95) # bug: should be None
273273
assert buffer.newest_timestamp == dt(99) # bug: should be None
274-
assert len(buffer) == 4 # bug: should be 0 (whole range gap)
274+
assert buffer.count_valid() == 4 # bug: should be 0 (whole range gap)
275275
assert len(buffer.gaps) == 1
276276

277277

@@ -369,7 +369,7 @@ def test_len_ringbuffer_samples_fit_buffer_size() -> None:
369369
timestamp = start_ts + timedelta(seconds=index)
370370
buffer.update(Sample(timestamp, Quantity(float(sample_value))))
371371

372-
assert len(buffer) == len(test_samples)
372+
assert buffer.count_valid() == len(test_samples)
373373

374374

375375
def test_len_with_gaps() -> None:
@@ -384,7 +384,7 @@ def test_len_with_gaps() -> None:
384384
buffer.update(
385385
Sample(datetime(2, 2, 2, 0, 0, i, tzinfo=timezone.utc), Quantity(float(i)))
386386
)
387-
assert len(buffer) == i + 1
387+
assert buffer.count_valid() == i + 1
388388

389389

390390
def test_len_ringbuffer_samples_overwrite_buffer() -> None:
@@ -412,7 +412,7 @@ def test_len_ringbuffer_samples_overwrite_buffer() -> None:
412412
timestamp = start_ts + timedelta(seconds=index)
413413
buffer.update(Sample(timestamp, Quantity(float(sample_value))))
414414

415-
assert len(buffer) == half_buffer_size
415+
assert buffer.count_valid() == half_buffer_size
416416

417417

418418
def test_ringbuffer_empty_buffer() -> None:

0 commit comments

Comments
 (0)