|
4 | 4 | """Tests for the Matryoshka power manager algorithm."""
|
5 | 5 |
|
6 | 6 | import asyncio
|
| 7 | +import re |
7 | 8 | from datetime import datetime, timedelta, timezone
|
8 | 9 |
|
| 10 | +import pytest |
9 | 11 | from frequenz.quantities import Power
|
10 | 12 |
|
11 | 13 | from frequenz.sdk import timeseries
|
@@ -36,13 +38,14 @@ def tgt_power( # pylint: disable=too-many-arguments,too-many-positional-argumen
|
36 | 38 | expected: float | None,
|
37 | 39 | creation_time: float | None = None,
|
38 | 40 | must_send: bool = False,
|
| 41 | + batteries: frozenset[int] | None = None, |
39 | 42 | ) -> None:
|
40 | 43 | """Test the target power calculation."""
|
41 | 44 | self._call_count += 1
|
42 | 45 | tgt_power = self.algorithm.calculate_target_power(
|
43 |
| - self._batteries, |
| 46 | + self._batteries if batteries is None else batteries, |
44 | 47 | Proposal(
|
45 |
| - component_ids=self._batteries, |
| 48 | + component_ids=self._batteries if batteries is None else batteries, |
46 | 49 | source_id=f"actor-{priority}",
|
47 | 50 | preferred_power=None if power is None else Power.from_watts(power),
|
48 | 51 | bounds=timeseries.Bounds(
|
@@ -368,6 +371,7 @@ async def test_matryoshka_drop_old_proposals() -> None:
|
368 | 371 | With inclusion bounds, and exclusion bounds -30.0 to 30.0.
|
369 | 372 | """
|
370 | 373 | batteries = frozenset({2, 5})
|
| 374 | + overlapping_batteries = frozenset({5, 8}) |
371 | 375 |
|
372 | 376 | system_bounds = _base_types.SystemBounds(
|
373 | 377 | timestamp=datetime.now(tz=timezone.utc),
|
@@ -424,3 +428,115 @@ async def test_matryoshka_drop_old_proposals() -> None:
|
424 | 428 | tester.tgt_power(
|
425 | 429 | priority=1, power=20.0, bounds=(20.0, 50.0), expected=25.0, must_send=True
|
426 | 430 | )
|
| 431 | + |
| 432 | + # When all proposals are too old, they are dropped, and the buckets are dropped as |
| 433 | + # well. After that, sending a request for a different but overlapping bucket will |
| 434 | + # succeed. And it will fail until then. |
| 435 | + with pytest.raises( |
| 436 | + NotImplementedError, |
| 437 | + match=re.escape( |
| 438 | + "PowerManagingActor: component IDs frozenset({8, 5}) are already " |
| 439 | + + "part of another bucket. Overlapping buckets are not yet supported." |
| 440 | + ), |
| 441 | + ): |
| 442 | + tester.tgt_power( |
| 443 | + priority=1, |
| 444 | + power=25.0, |
| 445 | + bounds=(25.0, 50.0), |
| 446 | + expected=25.0, |
| 447 | + must_send=True, |
| 448 | + batteries=overlapping_batteries, |
| 449 | + ) |
| 450 | + |
| 451 | + tester.tgt_power( |
| 452 | + priority=1, |
| 453 | + power=25.0, |
| 454 | + bounds=(25.0, 50.0), |
| 455 | + creation_time=now - 70.0, |
| 456 | + expected=25.0, |
| 457 | + must_send=True, |
| 458 | + ) |
| 459 | + tester.tgt_power( |
| 460 | + priority=2, |
| 461 | + power=25.0, |
| 462 | + bounds=(25.0, 50.0), |
| 463 | + creation_time=now - 70.0, |
| 464 | + expected=25.0, |
| 465 | + must_send=True, |
| 466 | + ) |
| 467 | + tester.tgt_power( |
| 468 | + priority=3, |
| 469 | + power=25.0, |
| 470 | + bounds=(25.0, 50.0), |
| 471 | + creation_time=now - 70.0, |
| 472 | + expected=25.0, |
| 473 | + must_send=True, |
| 474 | + ) |
| 475 | + |
| 476 | + tester.algorithm.drop_old_proposals(now) |
| 477 | + |
| 478 | + tester.tgt_power( |
| 479 | + priority=1, |
| 480 | + power=25.0, |
| 481 | + bounds=(25.0, 50.0), |
| 482 | + expected=25.0, |
| 483 | + must_send=True, |
| 484 | + batteries=overlapping_batteries, |
| 485 | + ) |
| 486 | + |
| 487 | + |
| 488 | +async def test_matryoshka_none_proposals() -> None: |
| 489 | + """Tests for the power managing actor. |
| 490 | +
|
| 491 | + When a `None` proposal is received, is source id should be dropped from the bucket. |
| 492 | + Then if the bucket becomes empty, it should be dropped as well. |
| 493 | + """ |
| 494 | + batteries = frozenset({2, 5}) |
| 495 | + overlapping_batteries = frozenset({5, 8}) |
| 496 | + |
| 497 | + system_bounds = _base_types.SystemBounds( |
| 498 | + timestamp=datetime.now(tz=timezone.utc), |
| 499 | + inclusion_bounds=timeseries.Bounds( |
| 500 | + lower=Power.from_watts(-200.0), upper=Power.from_watts(200.0) |
| 501 | + ), |
| 502 | + exclusion_bounds=timeseries.Bounds(lower=Power.zero(), upper=Power.zero()), |
| 503 | + ) |
| 504 | + |
| 505 | + def ensure_overlapping_bucket_request_fails() -> None: |
| 506 | + with pytest.raises( |
| 507 | + NotImplementedError, |
| 508 | + match=re.escape( |
| 509 | + "PowerManagingActor: component IDs frozenset({8, 5}) are already " |
| 510 | + + "part of another bucket. Overlapping buckets are not yet supported." |
| 511 | + ), |
| 512 | + ): |
| 513 | + tester.tgt_power( |
| 514 | + priority=1, |
| 515 | + power=None, |
| 516 | + bounds=(20.0, 50.0), |
| 517 | + expected=None, |
| 518 | + must_send=True, |
| 519 | + batteries=overlapping_batteries, |
| 520 | + ) |
| 521 | + |
| 522 | + tester = StatefulTester(batteries, system_bounds) |
| 523 | + |
| 524 | + tester.tgt_power(priority=3, power=22.0, bounds=(22.0, 30.0), expected=22.0) |
| 525 | + tester.tgt_power(priority=2, power=25.0, bounds=(25.0, 50.0), expected=25.0) |
| 526 | + tester.tgt_power(priority=1, power=20.0, bounds=(20.0, 50.0), expected=None) |
| 527 | + |
| 528 | + ensure_overlapping_bucket_request_fails() |
| 529 | + tester.tgt_power(priority=1, power=None, bounds=(None, None), expected=None) |
| 530 | + ensure_overlapping_bucket_request_fails() |
| 531 | + tester.tgt_power(priority=3, power=None, bounds=(None, None), expected=None) |
| 532 | + ensure_overlapping_bucket_request_fails() |
| 533 | + tester.tgt_power(priority=2, power=None, bounds=(None, None), expected=None) |
| 534 | + |
| 535 | + # Overlapping battery bucket is dropped. |
| 536 | + tester.tgt_power( |
| 537 | + priority=1, |
| 538 | + power=20.0, |
| 539 | + bounds=(20.0, 50.0), |
| 540 | + expected=20.0, |
| 541 | + batteries=overlapping_batteries, |
| 542 | + ) |
0 commit comments