Skip to content

Commit 8df671c

Browse files
committed
Update distribution algorithm to support exclusion bounds
Signed-off-by: Sahas Subramanian <[email protected]>
1 parent e9b93e1 commit 8df671c

File tree

2 files changed

+79
-44
lines changed

2 files changed

+79
-44
lines changed

src/frequenz/sdk/power/_distribution_algorithm.py

Lines changed: 58 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -277,8 +277,11 @@ def _total_capacity(self, components: List[InvBatPair]) -> float:
277277
return total_capacity
278278

279279
def _compute_battery_availability_ratio(
280-
self, components: List[InvBatPair], available_soc: Dict[int, float]
281-
) -> Tuple[List[Tuple[InvBatPair, float]], float]:
280+
self,
281+
components: List[InvBatPair],
282+
available_soc: Dict[int, float],
283+
excl_bounds: Dict[int, float],
284+
) -> Tuple[List[Tuple[InvBatPair, float, float]], float]:
282285
r"""Compute battery ratio and the total sum of all of them.
283286
284287
battery_availability_ratio = capacity_ratio[i] * available_soc[i]
@@ -291,6 +294,7 @@ def _compute_battery_availability_ratio(
291294
available_soc: How much SoC remained to reach
292295
* SoC upper bound - if need to distribute consumption power
293296
* SoC lower bound - if need to distribute supply power
297+
excl_bounds: Exclusion bounds for each inverter
294298
295299
Returns:
296300
Tuple where first argument is battery availability ratio for each
@@ -299,30 +303,35 @@ def _compute_battery_availability_ratio(
299303
of all battery ratios in the list.
300304
"""
301305
total_capacity = self._total_capacity(components)
302-
battery_availability_ratio: List[Tuple[InvBatPair, float]] = []
306+
battery_availability_ratio: List[Tuple[InvBatPair, float, float]] = []
303307
total_battery_availability_ratio: float = 0.0
304308

305309
for pair in components:
306-
battery = pair[0]
310+
battery, inverter = pair
307311
capacity_ratio = battery.capacity / total_capacity
308312
soc_factor = pow(
309313
available_soc[battery.component_id], self._distributor_exponent
310314
)
311315

312316
ratio = capacity_ratio * soc_factor
313-
battery_availability_ratio.append((pair, ratio))
317+
battery_availability_ratio.append(
318+
(pair, excl_bounds[inverter.component_id], ratio)
319+
)
314320
total_battery_availability_ratio += ratio
315321

316-
battery_availability_ratio.sort(key=lambda item: item[1], reverse=True)
322+
battery_availability_ratio.sort(
323+
key=lambda item: (item[1], item[2]), reverse=True
324+
)
317325

318326
return battery_availability_ratio, total_battery_availability_ratio
319327

320-
def _distribute_power(
328+
def _distribute_power( # pylint: disable=too-many-arguments
321329
self,
322330
components: List[InvBatPair],
323331
power_w: float,
324332
available_soc: Dict[int, float],
325-
upper_bounds: Dict[int, float],
333+
incl_bounds: Dict[int, float],
334+
excl_bounds: Dict[int, float],
326335
) -> DistributionResult:
327336
# pylint: disable=too-many-locals
328337
"""Distribute power between given components.
@@ -336,17 +345,18 @@ def _distribute_power(
336345
available_soc: how much SoC remained to reach:
337346
* SoC upper bound - if need to distribute consumption power
338347
* SoC lower bound - if need to distribute supply power
339-
upper_bounds: Min between upper bound of each pair in the components list:
340-
* supply upper bound - if need to distribute consumption power
341-
* consumption lower bound - if need to distribute supply power
348+
incl_bounds: Inclusion bounds for each inverter
349+
excl_bounds: Exclusion bounds for each inverter
342350
343351
Returns:
344352
Distribution result.
345353
"""
346354
(
347355
battery_availability_ratio,
348356
sum_ratio,
349-
) = self._compute_battery_availability_ratio(components, available_soc)
357+
) = self._compute_battery_availability_ratio(
358+
components, available_soc, excl_bounds
359+
)
350360

351361
distribution: Dict[int, float] = {}
352362

@@ -359,7 +369,7 @@ def _distribute_power(
359369
power_to_distribute: float = power_w
360370
used_ratio: float = 0.0
361371
ratio = sum_ratio
362-
for pair, battery_ratio in battery_availability_ratio:
372+
for pair, excl_bound, battery_ratio in battery_availability_ratio:
363373
inverter = pair[1]
364374
# ratio = 0, means all remaining batteries reach max SoC lvl or have no
365375
# capacity
@@ -375,10 +385,17 @@ def _distribute_power(
375385

376386
# If the power allocated for that inverter is out of bound,
377387
# then we need to distribute more power over all remaining batteries.
378-
upper_bound = upper_bounds[inverter.component_id]
379-
if distribution[inverter.component_id] > upper_bound:
380-
distribution[inverter.component_id] = upper_bound
381-
distributed_power += upper_bound
388+
incl_bound = incl_bounds[inverter.component_id]
389+
if distribution[inverter.component_id] > incl_bound:
390+
distribution[inverter.component_id] = incl_bound
391+
distributed_power += incl_bound
392+
# Distribute only the remaining power.
393+
power_to_distribute = power_w - distributed_power
394+
# Distribute between remaining batteries
395+
ratio = sum_ratio - used_ratio
396+
elif distribution[inverter.component_id] < excl_bound:
397+
distribution[inverter.component_id] = excl_bound
398+
distributed_power += excl_bound
382399
# Distribute only the remaining power.
383400
power_to_distribute = power_w - distributed_power
384401
# Distribute between remaining batteries
@@ -487,19 +504,25 @@ def _distribute_consume_power(
487504
0.0, battery.soc_upper_bound - battery.soc
488505
)
489506

490-
bounds: Dict[int, float] = {}
507+
incl_bounds: Dict[int, float] = {}
508+
excl_bounds: Dict[int, float] = {}
491509
for battery, inverter in components:
492510
# We can supply/consume with int only
493-
inverter_bound = inverter.active_power_inclusion_upper_bound
494-
battery_bound = battery.power_inclusion_upper_bound
495-
bounds[inverter.component_id] = min(inverter_bound, battery_bound)
511+
incl_bounds[inverter.component_id] = min(
512+
inverter.active_power_inclusion_upper_bound,
513+
battery.power_inclusion_upper_bound,
514+
)
515+
excl_bounds[inverter.component_id] = max(
516+
inverter.active_power_exclusion_upper_bound,
517+
battery.power_exclusion_upper_bound,
518+
)
496519

497520
result: DistributionResult = self._distribute_power(
498-
components, power_w, available_soc, bounds
521+
components, power_w, available_soc, incl_bounds, excl_bounds
499522
)
500523

501524
return self._greedy_distribute_remaining_power(
502-
result.distribution, bounds, result.remaining_power
525+
result.distribution, incl_bounds, result.remaining_power
503526
)
504527

505528
def _distribute_supply_power(
@@ -525,19 +548,24 @@ def _distribute_supply_power(
525548
0.0, battery.soc - battery.soc_lower_bound
526549
)
527550

528-
bounds: Dict[int, float] = {}
551+
incl_bounds: Dict[int, float] = {}
552+
excl_bounds: Dict[int, float] = {}
529553
for battery, inverter in components:
530-
# We can consume with int only
531-
inverter_bound = inverter.active_power_inclusion_lower_bound
532-
battery_bound = battery.power_inclusion_lower_bound
533-
bounds[inverter.component_id] = -1 * max(inverter_bound, battery_bound)
554+
incl_bounds[inverter.component_id] = -1 * max(
555+
inverter.active_power_inclusion_lower_bound,
556+
battery.power_inclusion_lower_bound,
557+
)
558+
excl_bounds[inverter.component_id] = -1 * min(
559+
inverter.active_power_exclusion_lower_bound,
560+
battery.power_exclusion_lower_bound,
561+
)
534562

535563
result: DistributionResult = self._distribute_power(
536-
components, -1 * power_w, available_soc, bounds
564+
components, -1 * power_w, available_soc, incl_bounds, excl_bounds
537565
)
538566

539567
result = self._greedy_distribute_remaining_power(
540-
result.distribution, bounds, result.remaining_power
568+
result.distribution, incl_bounds, result.remaining_power
541569
)
542570

543571
for inverter_id in result.distribution.keys():

tests/power/test_distribution_algorithm.py

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,12 @@ def test_distribute_power_one_battery(self) -> None:
140140
components = self.create_components_with_capacity(1, capacity)
141141

142142
available_soc: Dict[int, float] = {0: 40}
143-
upper_bounds: Dict[int, float] = {1: 500}
143+
incl_bounds: Dict[int, float] = {1: 500}
144+
excl_bounds: Dict[int, float] = {1: 0}
144145

145146
algorithm = DistributionAlgorithm(distributor_exponent=1)
146147
result = algorithm._distribute_power( # pylint: disable=protected-access
147-
components, 650, available_soc, upper_bounds
148+
components, 650, available_soc, incl_bounds, excl_bounds
148149
)
149150

150151
assert result.distribution == approx({1: 500})
@@ -160,11 +161,12 @@ def test_distribute_power_two_batteries_1(self) -> None:
160161
components = self.create_components_with_capacity(2, capacity)
161162

162163
available_soc: Dict[int, float] = {0: 40, 2: 20}
163-
upper_bounds: Dict[int, float] = {1: 500, 3: 500}
164+
incl_bounds: Dict[int, float] = {1: 500, 3: 500}
165+
excl_bounds: Dict[int, float] = {1: 0, 3: 0}
164166

165167
algorithm = DistributionAlgorithm(distributor_exponent=1)
166168
result = algorithm._distribute_power( # pylint: disable=protected-access
167-
components, 600, available_soc, upper_bounds
169+
components, 600, available_soc, incl_bounds, excl_bounds
168170
)
169171

170172
assert result.distribution == approx({1: 400, 3: 200})
@@ -180,11 +182,12 @@ def test_distribute_power_two_batteries_2(self) -> None:
180182
components = self.create_components_with_capacity(2, capacity)
181183

182184
available_soc: Dict[int, float] = {0: 20, 2: 20}
183-
upper_bounds: Dict[int, float] = {1: 500, 3: 500}
185+
incl_bounds: Dict[int, float] = {1: 500, 3: 500}
186+
excl_bounds: Dict[int, float] = {1: 0, 3: 0}
184187

185188
algorithm = DistributionAlgorithm(distributor_exponent=1)
186189
result = algorithm._distribute_power( # pylint: disable=protected-access
187-
components, 600, available_soc, upper_bounds
190+
components, 600, available_soc, incl_bounds, excl_bounds
188191
)
189192

190193
assert result.distribution == approx({1: 200, 3: 400})
@@ -201,11 +204,12 @@ def test_distribute_power_two_batteries_bounds(self) -> None:
201204
components = self.create_components_with_capacity(2, capacity)
202205

203206
available_soc: Dict[int, float] = {0: 40, 2: 20}
204-
upper_bounds: Dict[int, float] = {1: 250, 3: 330}
207+
incl_bounds: Dict[int, float] = {1: 250, 3: 330}
208+
excl_bounds: Dict[int, float] = {1: 0, 3: 0}
205209

206210
algorithm = DistributionAlgorithm(distributor_exponent=1)
207211
result = algorithm._distribute_power( # pylint: disable=protected-access
208-
components, 600, available_soc, upper_bounds
212+
components, 600, available_soc, incl_bounds, excl_bounds
209213
)
210214

211215
assert result.distribution == approx({1: 250, 3: 330})
@@ -217,11 +221,12 @@ def test_distribute_power_three_batteries(self) -> None:
217221
components = self.create_components_with_capacity(3, capacity)
218222

219223
available_soc: Dict[int, float] = {0: 40, 2: 20, 4: 20}
220-
upper_bounds: Dict[int, float] = {1: 1000, 3: 3400, 5: 3550}
224+
incl_bounds: Dict[int, float] = {1: 1000, 3: 3400, 5: 3550}
225+
excl_bounds: Dict[int, float] = {1: 0, 3: 0, 5: 0}
221226

222227
algorithm = DistributionAlgorithm(distributor_exponent=1)
223228
result = algorithm._distribute_power( # pylint: disable=protected-access
224-
components, 1000, available_soc, upper_bounds
229+
components, 1000, available_soc, incl_bounds, excl_bounds
225230
)
226231

227232
assert result.distribution == approx({1: 400, 3: 400, 5: 200})
@@ -233,11 +238,12 @@ def test_distribute_power_three_batteries_2(self) -> None:
233238
components = self.create_components_with_capacity(3, capacity)
234239

235240
available_soc: Dict[int, float] = {0: 80, 2: 10, 4: 20}
236-
upper_bounds: Dict[int, float] = {1: 400, 3: 3400, 5: 300}
241+
incl_bounds: Dict[int, float] = {1: 400, 3: 3400, 5: 300}
242+
excl_bounds: Dict[int, float] = {1: 0, 3: 0, 5: 0}
237243

238244
algorithm = DistributionAlgorithm(distributor_exponent=1)
239245
result = algorithm._distribute_power( # pylint: disable=protected-access
240-
components, 1000, available_soc, upper_bounds
246+
components, 1000, available_soc, incl_bounds, excl_bounds
241247
)
242248

243249
assert result.distribution == approx({1: 400, 3: 300, 5: 300})
@@ -249,11 +255,12 @@ def test_distribute_power_three_batteries_3(self) -> None:
249255
components = self.create_components_with_capacity(3, capacity)
250256

251257
available_soc: Dict[int, float] = {0: 80, 2: 10, 4: 20}
252-
upper_bounds: Dict[int, float] = {1: 500, 3: 300, 5: 300}
258+
incl_bounds: Dict[int, float] = {1: 500, 3: 300, 5: 300}
259+
excl_bounds: Dict[int, float] = {1: 0, 3: 0, 5: 0}
253260

254261
algorithm = DistributionAlgorithm(distributor_exponent=1)
255262
result = algorithm._distribute_power( # pylint: disable=protected-access
256-
components, 1000, available_soc, upper_bounds
263+
components, 1000, available_soc, incl_bounds, excl_bounds
257264
)
258265

259266
assert result.distribution == approx({1: 0, 3: 300, 5: 0})

0 commit comments

Comments
 (0)