Skip to content

Commit 3ee0f27

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

File tree

2 files changed

+125
-64
lines changed

2 files changed

+125
-64
lines changed

src/frequenz/sdk/power/_distribution_algorithm.py

Lines changed: 104 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"""Power distribution algorithm to distribute power between batteries."""
55

66
import logging
7+
import math
78
from dataclasses import dataclass
89
from typing import Dict, List, NamedTuple, Tuple
910

@@ -277,8 +278,11 @@ def _total_capacity(self, components: List[InvBatPair]) -> float:
277278
return total_capacity
278279

279280
def _compute_battery_availability_ratio(
280-
self, components: List[InvBatPair], available_soc: Dict[int, float]
281-
) -> Tuple[List[Tuple[InvBatPair, float]], float]:
281+
self,
282+
components: List[InvBatPair],
283+
available_soc: Dict[int, float],
284+
excl_bounds: Dict[int, float],
285+
) -> Tuple[List[Tuple[InvBatPair, float, float]], float]:
282286
r"""Compute battery ratio and the total sum of all of them.
283287
284288
battery_availability_ratio = capacity_ratio[i] * available_soc[i]
@@ -291,6 +295,7 @@ def _compute_battery_availability_ratio(
291295
available_soc: How much SoC remained to reach
292296
* SoC upper bound - if need to distribute consumption power
293297
* SoC lower bound - if need to distribute supply power
298+
excl_bounds: Exclusion bounds for each inverter
294299
295300
Returns:
296301
Tuple where first argument is battery availability ratio for each
@@ -299,32 +304,37 @@ def _compute_battery_availability_ratio(
299304
of all battery ratios in the list.
300305
"""
301306
total_capacity = self._total_capacity(components)
302-
battery_availability_ratio: List[Tuple[InvBatPair, float]] = []
307+
battery_availability_ratio: List[Tuple[InvBatPair, float, float]] = []
303308
total_battery_availability_ratio: float = 0.0
304309

305310
for pair in components:
306-
battery = pair[0]
311+
battery, inverter = pair
307312
capacity_ratio = battery.capacity / total_capacity
308313
soc_factor = pow(
309314
available_soc[battery.component_id], self._distributor_exponent
310315
)
311316

312317
ratio = capacity_ratio * soc_factor
313-
battery_availability_ratio.append((pair, ratio))
318+
battery_availability_ratio.append(
319+
(pair, excl_bounds[inverter.component_id], ratio)
320+
)
314321
total_battery_availability_ratio += ratio
315322

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

318327
return battery_availability_ratio, total_battery_availability_ratio
319328

320-
def _distribute_power(
329+
def _distribute_power( # pylint: disable=too-many-arguments
321330
self,
322331
components: List[InvBatPair],
323332
power_w: float,
324333
available_soc: Dict[int, float],
325-
upper_bounds: Dict[int, float],
334+
incl_bounds: Dict[int, float],
335+
excl_bounds: Dict[int, float],
326336
) -> DistributionResult:
327-
# pylint: disable=too-many-locals
337+
# pylint: disable=too-many-locals,too-many-branches,too-many-statements
328338
"""Distribute power between given components.
329339
330340
After this method power should be distributed between batteries
@@ -336,57 +346,98 @@ def _distribute_power(
336346
available_soc: how much SoC remained to reach:
337347
* SoC upper bound - if need to distribute consumption power
338348
* 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
349+
incl_bounds: Inclusion bounds for each inverter
350+
excl_bounds: Exclusion bounds for each inverter
342351
343352
Returns:
344353
Distribution result.
345354
"""
346355
(
347356
battery_availability_ratio,
348357
sum_ratio,
349-
) = self._compute_battery_availability_ratio(components, available_soc)
358+
) = self._compute_battery_availability_ratio(
359+
components, available_soc, excl_bounds
360+
)
350361

351362
distribution: Dict[int, float] = {}
352-
363+
print(f"{power_w=}")
353364
# sum_ratio == 0 means that all batteries are fully charged / discharged
354365
if is_close_to_zero(sum_ratio):
355366
distribution = {inverter.component_id: 0 for _, inverter in components}
356367
return DistributionResult(distribution, power_w)
357368

358369
distributed_power: float = 0.0
370+
reserved_power: float = 0.0
359371
power_to_distribute: float = power_w
360372
used_ratio: float = 0.0
361373
ratio = sum_ratio
362-
for pair, battery_ratio in battery_availability_ratio:
374+
excess_reserved: dict[int, float] = {}
375+
deficits: dict[int, float] = {}
376+
for pair, excl_bound, battery_ratio in battery_availability_ratio:
363377
inverter = pair[1]
364378
# ratio = 0, means all remaining batteries reach max SoC lvl or have no
365379
# capacity
366380
if is_close_to_zero(ratio):
367381
distribution[inverter.component_id] = 0.0
368382
continue
369383

370-
distribution[inverter.component_id] = (
371-
power_to_distribute * battery_ratio / ratio
372-
)
373-
384+
power_to_distribute = power_w - reserved_power
385+
calculated_power = power_to_distribute * battery_ratio / ratio
386+
reserved_power += max(calculated_power, excl_bound)
374387
used_ratio += battery_ratio
375-
388+
ratio = sum_ratio - used_ratio
376389
# If the power allocated for that inverter is out of bound,
377390
# 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
382-
# Distribute only the remaining power.
383-
power_to_distribute = power_w - distributed_power
384-
# Distribute between remaining batteries
385-
ratio = sum_ratio - used_ratio
391+
incl_bound = incl_bounds[inverter.component_id]
392+
if calculated_power > incl_bound:
393+
excess_reserved[inverter.component_id] = incl_bound - excl_bound
394+
# # Distribute between remaining batteries
395+
elif calculated_power < excl_bound:
396+
deficits[inverter.component_id] = calculated_power - excl_bound
386397
else:
387-
distributed_power += distribution[inverter.component_id]
398+
excess_reserved[inverter.component_id] = calculated_power - excl_bound
399+
400+
distributed_power += excl_bound
401+
distribution[inverter.component_id] = excl_bound
402+
403+
for inverter_id, deficit in deficits.items():
404+
while not math.isclose(deficit, 0.0, abs_tol=1e-6) and deficit < 0.0:
405+
take_from = max(excess_reserved.items(), key=lambda item: item[1])
406+
if math.isclose(take_from[1], 0.0, abs_tol=1e-6) or take_from[1] < 0.0:
407+
break
408+
if take_from[1] >= -deficit or math.isclose(
409+
take_from[1], -deficit, abs_tol=1e-6
410+
):
411+
excess_reserved[take_from[0]] += deficit
412+
deficits[inverter_id] = 0.0
413+
deficit = 0.0
414+
else:
415+
deficit += excess_reserved[take_from[0]]
416+
deficits[inverter_id] = deficit
417+
excess_reserved[take_from[0]] = 0.0
418+
419+
for inverter_id, excess in excess_reserved.items():
420+
distribution[inverter_id] += excess
421+
distributed_power += excess
422+
423+
for inverter_id, deficit in deficits.items():
424+
if deficit < -0.1:
425+
left_over = power_w - distributed_power
426+
if left_over > -deficit:
427+
distributed_power += deficit
428+
deficit = 0.0
429+
deficits[inverter_id] = 0.0
430+
elif left_over > 0.0:
431+
deficit += left_over
432+
distributed_power += left_over
433+
deficits[inverter_id] = deficit
434+
435+
left_over = power_w - distributed_power
436+
dist = DistributionResult(distribution, left_over)
388437

389-
return DistributionResult(distribution, power_w - distributed_power)
438+
return self._greedy_distribute_remaining_power(
439+
dist.distribution, incl_bounds, dist.remaining_power
440+
)
390441

391442
def _greedy_distribute_remaining_power(
392443
self,
@@ -487,19 +538,21 @@ def _distribute_consume_power(
487538
0.0, battery.soc_upper_bound - battery.soc
488539
)
489540

490-
bounds: Dict[int, float] = {}
541+
incl_bounds: Dict[int, float] = {}
542+
excl_bounds: Dict[int, float] = {}
491543
for battery, inverter in components:
492544
# 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)
545+
incl_bounds[inverter.component_id] = min(
546+
inverter.active_power_inclusion_upper_bound,
547+
battery.power_inclusion_upper_bound,
548+
)
549+
excl_bounds[inverter.component_id] = max(
550+
inverter.active_power_exclusion_upper_bound,
551+
battery.power_exclusion_upper_bound,
552+
)
496553

497-
result: DistributionResult = self._distribute_power(
498-
components, power_w, available_soc, bounds
499-
)
500-
501-
return self._greedy_distribute_remaining_power(
502-
result.distribution, bounds, result.remaining_power
554+
return self._distribute_power(
555+
components, power_w, available_soc, incl_bounds, excl_bounds
503556
)
504557

505558
def _distribute_supply_power(
@@ -525,19 +578,20 @@ def _distribute_supply_power(
525578
0.0, battery.soc - battery.soc_lower_bound
526579
)
527580

528-
bounds: Dict[int, float] = {}
581+
incl_bounds: Dict[int, float] = {}
582+
excl_bounds: Dict[int, float] = {}
529583
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)
584+
incl_bounds[inverter.component_id] = -1 * max(
585+
inverter.active_power_inclusion_lower_bound,
586+
battery.power_inclusion_lower_bound,
587+
)
588+
excl_bounds[inverter.component_id] = -1 * min(
589+
inverter.active_power_exclusion_lower_bound,
590+
battery.power_exclusion_lower_bound,
591+
)
534592

535593
result: DistributionResult = self._distribute_power(
536-
components, -1 * power_w, available_soc, bounds
537-
)
538-
539-
result = self._greedy_distribute_remaining_power(
540-
result.distribution, bounds, result.remaining_power
594+
components, -1 * power_w, available_soc, incl_bounds, excl_bounds
541595
)
542596

543597
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)