Skip to content

Commit e7e3066

Browse files
committed
Implement fetching and streaming of exclusion bounds in battery pool
Signed-off-by: Sahas Subramanian <[email protected]>
1 parent b12b789 commit e7e3066

File tree

2 files changed

+125
-29
lines changed

2 files changed

+125
-29
lines changed

src/frequenz/sdk/timeseries/battery_pool/_metric_calculator.py

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -480,11 +480,15 @@ def __init__(
480480
super().__init__(used_batteries)
481481
self._battery_metrics = [
482482
ComponentMetricId.POWER_INCLUSION_LOWER_BOUND,
483+
ComponentMetricId.POWER_EXCLUSION_LOWER_BOUND,
484+
ComponentMetricId.POWER_EXCLUSION_UPPER_BOUND,
483485
ComponentMetricId.POWER_INCLUSION_UPPER_BOUND,
484486
]
485487

486488
self._inverter_metrics = [
487489
ComponentMetricId.ACTIVE_POWER_INCLUSION_LOWER_BOUND,
490+
ComponentMetricId.ACTIVE_POWER_EXCLUSION_LOWER_BOUND,
491+
ComponentMetricId.ACTIVE_POWER_EXCLUSION_UPPER_BOUND,
488492
ComponentMetricId.ACTIVE_POWER_INCLUSION_UPPER_BOUND,
489493
]
490494

@@ -554,6 +558,45 @@ def _fetch_inclusion_bounds(
554558

555559
return (timestamp, inclusion_lower_bounds, inclusion_upper_bounds)
556560

561+
def _fetch_exclusion_bounds(
562+
self,
563+
battery_id: int,
564+
inverter_id: int,
565+
metrics_data: dict[int, ComponentMetricsData],
566+
) -> tuple[datetime, list[float], list[float]]:
567+
timestamp = _MIN_TIMESTAMP
568+
exclusion_lower_bounds: list[float] = []
569+
exclusion_upper_bounds: list[float] = []
570+
571+
# Exclusion upper and lower bounds are not related.
572+
# If one is missing, then we can still use the other.
573+
if battery_id in metrics_data:
574+
data = metrics_data[battery_id]
575+
value = data.get(ComponentMetricId.POWER_EXCLUSION_UPPER_BOUND)
576+
if value is not None:
577+
timestamp = max(timestamp, data.timestamp)
578+
exclusion_upper_bounds.append(value)
579+
580+
value = data.get(ComponentMetricId.POWER_EXCLUSION_LOWER_BOUND)
581+
if value is not None:
582+
timestamp = max(timestamp, data.timestamp)
583+
exclusion_lower_bounds.append(value)
584+
585+
if inverter_id in metrics_data:
586+
data = metrics_data[inverter_id]
587+
588+
value = data.get(ComponentMetricId.ACTIVE_POWER_EXCLUSION_UPPER_BOUND)
589+
if value is not None:
590+
timestamp = max(data.timestamp, timestamp)
591+
exclusion_upper_bounds.append(value)
592+
593+
value = data.get(ComponentMetricId.ACTIVE_POWER_EXCLUSION_LOWER_BOUND)
594+
if value is not None:
595+
timestamp = max(data.timestamp, timestamp)
596+
exclusion_lower_bounds.append(value)
597+
598+
return (timestamp, exclusion_lower_bounds, exclusion_upper_bounds)
599+
557600
def calculate(
558601
self,
559602
metrics_data: dict[int, ComponentMetricsData],
@@ -573,11 +616,11 @@ def calculate(
573616
High level metric calculated from the given metrics.
574617
Return None if there are no component metrics.
575618
"""
576-
# In the future we will have lower bound, too.
577-
578619
timestamp = _MIN_TIMESTAMP
579620
inclusion_bounds_lower = 0.0
580621
inclusion_bounds_upper = 0.0
622+
exclusion_bounds_lower = 0.0
623+
exclusion_bounds_upper = 0.0
581624

582625
for battery_id in working_batteries:
583626
inverter_id = self._bat_inv_map[battery_id]
@@ -587,10 +630,19 @@ def calculate(
587630
inclusion_upper_bounds,
588631
) = self._fetch_inclusion_bounds(battery_id, inverter_id, metrics_data)
589632
timestamp = max(timestamp, _ts)
633+
(
634+
_ts,
635+
exclusion_lower_bounds,
636+
exclusion_upper_bounds,
637+
) = self._fetch_exclusion_bounds(battery_id, inverter_id, metrics_data)
590638
if len(inclusion_upper_bounds) > 0:
591639
inclusion_bounds_upper += min(inclusion_upper_bounds)
592640
if len(inclusion_lower_bounds) > 0:
593641
inclusion_bounds_lower += max(inclusion_lower_bounds)
642+
if len(exclusion_upper_bounds) > 0:
643+
exclusion_bounds_upper += max(exclusion_upper_bounds)
644+
if len(exclusion_lower_bounds) > 0:
645+
exclusion_bounds_lower += min(exclusion_lower_bounds)
594646

595647
if timestamp == _MIN_TIMESTAMP:
596648
return None
@@ -601,5 +653,8 @@ def calculate(
601653
Power.from_watts(inclusion_bounds_lower),
602654
Power.from_watts(inclusion_bounds_upper),
603655
),
604-
exclusion_bounds=Bounds(Power.from_watts(0.0), Power.from_watts(0.0)),
656+
exclusion_bounds=Bounds(
657+
Power.from_watts(exclusion_bounds_lower),
658+
Power.from_watts(exclusion_bounds_upper),
659+
),
605660
)

tests/timeseries/_battery_pool/test_battery_pool.py

Lines changed: 67 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -833,6 +833,8 @@ async def run_power_bounds_test( # pylint: disable=too-many-locals
833833
timestamp=datetime.now(tz=timezone.utc),
834834
power_inclusion_lower_bound=-1000,
835835
power_inclusion_upper_bound=5000,
836+
power_exclusion_lower_bound=-300,
837+
power_exclusion_upper_bound=300,
836838
),
837839
sampling_rate=0.05,
838840
)
@@ -842,6 +844,8 @@ async def run_power_bounds_test( # pylint: disable=too-many-locals
842844
timestamp=datetime.now(tz=timezone.utc),
843845
active_power_inclusion_lower_bound=-900,
844846
active_power_inclusion_upper_bound=6000,
847+
active_power_exclusion_lower_bound=-200,
848+
active_power_exclusion_upper_bound=200,
845849
),
846850
sampling_rate=0.1,
847851
)
@@ -856,44 +860,60 @@ async def run_power_bounds_test( # pylint: disable=too-many-locals
856860
expected = PowerMetrics(
857861
timestamp=now,
858862
inclusion_bounds=Bounds(Power.from_watts(-1800), Power.from_watts(10000)),
859-
exclusion_bounds=Bounds(Power.from_watts(0), Power.from_watts(0)),
863+
exclusion_bounds=Bounds(Power.from_watts(-600), Power.from_watts(600)),
860864
)
861865
compare_messages(msg, expected, WAIT_FOR_COMPONENT_DATA_SEC + 0.2)
862866

863867
batteries_in_pool = list(battery_pool.battery_ids)
864868
scenarios: list[Scenario[PowerMetrics]] = [
865869
Scenario(
866870
bat_inv_map[batteries_in_pool[0]],
867-
{"active_power_inclusion_lower_bound": -100},
871+
{
872+
"active_power_inclusion_lower_bound": -100,
873+
"active_power_exclusion_lower_bound": -400,
874+
},
868875
PowerMetrics(
869876
now,
870877
Bounds(Power.from_watts(-1000), Power.from_watts(10000)),
871-
Bounds(Power.from_watts(0), Power.from_watts(0)),
878+
Bounds(Power.from_watts(-700), Power.from_watts(600)),
872879
),
873880
),
874881
# Inverter bound changed, but metric result should not change.
875882
Scenario(
876883
component_id=bat_inv_map[batteries_in_pool[0]],
877-
new_metrics={"active_power_inclusion_upper_bound": 9000},
884+
new_metrics={
885+
"active_power_inclusion_upper_bound": 9000,
886+
"active_power_exclusion_upper_bound": 250,
887+
},
878888
expected_result=None,
879889
wait_for_result=False,
880890
),
881891
Scenario(
882892
batteries_in_pool[0],
883-
{"power_inclusion_lower_bound": 0, "power_inclusion_upper_bound": 4000},
893+
{
894+
"power_inclusion_lower_bound": 0,
895+
"power_inclusion_upper_bound": 4000,
896+
"power_exclusion_lower_bound": 0,
897+
"power_exclusion_upper_bound": 100,
898+
},
884899
PowerMetrics(
885900
now,
886901
Bounds(Power.from_watts(-900), Power.from_watts(9000)),
887-
Bounds(Power.from_watts(0), Power.from_watts(0)),
902+
Bounds(Power.from_watts(-700), Power.from_watts(550)),
888903
),
889904
),
890905
Scenario(
891906
batteries_in_pool[1],
892-
{"power_inclusion_lower_bound": -10, "power_inclusion_upper_bound": 200},
907+
{
908+
"power_inclusion_lower_bound": -10,
909+
"power_inclusion_upper_bound": 200,
910+
"power_exclusion_lower_bound": -5,
911+
"power_exclusion_upper_bound": 5,
912+
},
893913
PowerMetrics(
894914
now,
895915
Bounds(Power.from_watts(-10), Power.from_watts(4200)),
896-
Bounds(Power.from_watts(0), Power.from_watts(0)),
916+
Bounds(Power.from_watts(-600), Power.from_watts(450)),
897917
),
898918
),
899919
# Test 2 things:
@@ -905,56 +925,67 @@ async def run_power_bounds_test( # pylint: disable=too-many-locals
905925
{
906926
"power_inclusion_lower_bound": -50,
907927
"power_inclusion_upper_bound": math.nan,
928+
"power_exclusion_lower_bound": -30,
929+
"power_exclusion_upper_bound": 300,
908930
},
909931
PowerMetrics(
910932
now,
911933
Bounds(Power.from_watts(-60), Power.from_watts(9200)),
912-
Bounds(Power.from_watts(0), Power.from_watts(0)),
934+
Bounds(Power.from_watts(-600), Power.from_watts(500)),
913935
),
914936
),
915937
Scenario(
916938
bat_inv_map[batteries_in_pool[0]],
917939
{
918940
"active_power_inclusion_lower_bound": math.nan,
919941
"active_power_inclusion_upper_bound": math.nan,
942+
"active_power_exclusion_lower_bound": math.nan,
943+
"active_power_exclusion_upper_bound": math.nan,
920944
},
921945
PowerMetrics(
922946
now,
923947
Bounds(Power.from_watts(-60), Power.from_watts(200)),
924-
Bounds(Power.from_watts(0), Power.from_watts(0)),
948+
Bounds(Power.from_watts(-230), Power.from_watts(500)),
925949
),
926950
),
927951
Scenario(
928952
batteries_in_pool[0],
929-
{"power_inclusion_lower_bound": math.nan},
953+
{
954+
"power_inclusion_lower_bound": math.nan,
955+
"power_exclusion_lower_bound": math.nan,
956+
},
930957
PowerMetrics(
931958
now,
932959
Bounds(Power.from_watts(-10), Power.from_watts(200)),
933-
Bounds(Power.from_watts(0), Power.from_watts(0)),
960+
Bounds(Power.from_watts(-200), Power.from_watts(500)),
934961
),
935962
),
936963
Scenario(
937964
batteries_in_pool[1],
938965
{
939966
"power_inclusion_lower_bound": -100,
940967
"power_inclusion_upper_bound": math.nan,
968+
"power_exclusion_lower_bound": -50,
969+
"power_exclusion_upper_bound": 50,
941970
},
942971
PowerMetrics(
943972
now,
944973
Bounds(Power.from_watts(-100), Power.from_watts(6000)),
945-
Bounds(Power.from_watts(0), Power.from_watts(0)),
974+
Bounds(Power.from_watts(-200), Power.from_watts(500)),
946975
),
947976
),
948977
Scenario(
949978
bat_inv_map[batteries_in_pool[1]],
950979
{
951980
"active_power_inclusion_lower_bound": math.nan,
952981
"active_power_inclusion_upper_bound": math.nan,
982+
"active_power_exclusion_lower_bound": math.nan,
983+
"active_power_exclusion_upper_bound": math.nan,
953984
},
954985
PowerMetrics(
955986
now,
956987
Bounds(Power.from_watts(-100), Power.from_watts(0)),
957-
Bounds(Power.from_watts(0), Power.from_watts(0)),
988+
Bounds(Power.from_watts(-50), Power.from_watts(350)),
958989
),
959990
),
960991
# All components are sending NaN, can't calculate bounds
@@ -968,51 +999,61 @@ async def run_power_bounds_test( # pylint: disable=too-many-locals
968999
),
9691000
Scenario(
9701001
batteries_in_pool[0],
971-
{"power_inclusion_lower_bound": -100, "power_inclusion_upper_bound": 100},
1002+
{
1003+
"power_inclusion_lower_bound": -100,
1004+
"power_inclusion_upper_bound": 100,
1005+
"power_exclusion_lower_bound": -20,
1006+
"power_exclusion_upper_bound": 20,
1007+
},
9721008
PowerMetrics(
9731009
now,
9741010
Bounds(Power.from_watts(-100), Power.from_watts(100)),
975-
Bounds(Power.from_watts(0), Power.from_watts(0)),
1011+
Bounds(Power.from_watts(-70), Power.from_watts(70)),
9761012
),
9771013
),
9781014
Scenario(
9791015
bat_inv_map[batteries_in_pool[1]],
9801016
{
9811017
"active_power_inclusion_lower_bound": -400,
9821018
"active_power_inclusion_upper_bound": 400,
1019+
"active_power_exclusion_lower_bound": -100,
1020+
"active_power_exclusion_upper_bound": 100,
9831021
},
9841022
PowerMetrics(
9851023
now,
9861024
Bounds(Power.from_watts(-500), Power.from_watts(500)),
987-
Bounds(Power.from_watts(0), Power.from_watts(0)),
1025+
Bounds(Power.from_watts(-120), Power.from_watts(120)),
9881026
),
9891027
),
9901028
Scenario(
9911029
batteries_in_pool[1],
9921030
{
9931031
"power_inclusion_lower_bound": -300,
9941032
"power_inclusion_upper_bound": 700,
1033+
"power_exclusion_lower_bound": -130,
1034+
"power_exclusion_upper_bound": 130,
9951035
},
9961036
PowerMetrics(
9971037
now,
9981038
Bounds(Power.from_watts(-400), Power.from_watts(500)),
999-
Bounds(Power.from_watts(0), Power.from_watts(0)),
1039+
Bounds(Power.from_watts(-150), Power.from_watts(150)),
10001040
),
10011041
),
10021042
Scenario(
10031043
bat_inv_map[batteries_in_pool[0]],
10041044
{
10051045
"active_power_inclusion_lower_bound": -200,
10061046
"active_power_inclusion_upper_bound": 50,
1047+
"active_power_exclusion_lower_bound": -80,
1048+
"active_power_exclusion_upper_bound": 80,
10071049
},
10081050
PowerMetrics(
10091051
now,
10101052
Bounds(Power.from_watts(-400), Power.from_watts(450)),
1011-
Bounds(Power.from_watts(0), Power.from_watts(0)),
1053+
Bounds(Power.from_watts(-210), Power.from_watts(210)),
10121054
),
10131055
),
10141056
]
1015-
10161057
waiting_time_sec = setup_args.min_update_interval + 0.02
10171058
await run_scenarios(scenarios, streamer, receiver, waiting_time_sec)
10181059

@@ -1025,12 +1066,12 @@ async def run_power_bounds_test( # pylint: disable=too-many-locals
10251066
all_pool_result=PowerMetrics(
10261067
now,
10271068
Bounds(Power.from_watts(-400), Power.from_watts(450)),
1028-
Bounds(Power.from_watts(0), Power.from_watts(0)),
1069+
Bounds(Power.from_watts(-210), Power.from_watts(210)),
10291070
),
10301071
only_first_battery_result=PowerMetrics(
10311072
now,
10321073
Bounds(Power.from_watts(-100), Power.from_watts(50)),
1033-
Bounds(Power.from_watts(0), Power.from_watts(0)),
1074+
Bounds(Power.from_watts(-80), Power.from_watts(80)),
10341075
),
10351076
)
10361077

@@ -1043,7 +1084,7 @@ async def run_power_bounds_test( # pylint: disable=too-many-locals
10431084
PowerMetrics(
10441085
now,
10451086
Bounds(Power.from_watts(-500), Power.from_watts(450)),
1046-
Bounds(Power.from_watts(0), Power.from_watts(0)),
1087+
Bounds(Power.from_watts(-180), Power.from_watts(180)),
10471088
),
10481089
0.2,
10491090
)
@@ -1057,7 +1098,7 @@ async def run_power_bounds_test( # pylint: disable=too-many-locals
10571098
PowerMetrics(
10581099
now,
10591100
Bounds(Power.from_watts(-600), Power.from_watts(450)),
1060-
Bounds(Power.from_watts(0), Power.from_watts(0)),
1101+
Bounds(Power.from_watts(-180), Power.from_watts(180)),
10611102
),
10621103
0.2,
10631104
)
@@ -1071,7 +1112,7 @@ async def run_power_bounds_test( # pylint: disable=too-many-locals
10711112
PowerMetrics(
10721113
now,
10731114
Bounds(Power.from_watts(-400), Power.from_watts(400)),
1074-
Bounds(Power.from_watts(0), Power.from_watts(0)),
1115+
Bounds(Power.from_watts(-100), Power.from_watts(100)),
10751116
),
10761117
0.2,
10771118
)
@@ -1091,7 +1132,7 @@ async def run_power_bounds_test( # pylint: disable=too-many-locals
10911132
PowerMetrics(
10921133
now,
10931134
Bounds(Power.from_watts(-100), Power.from_watts(100)),
1094-
Bounds(Power.from_watts(0), Power.from_watts(0)),
1135+
Bounds(Power.from_watts(-20), Power.from_watts(20)),
10951136
),
10961137
0.2,
10971138
)

0 commit comments

Comments
 (0)