Skip to content

Commit 5caf4ae

Browse files
committed
Apply exclusion bounds only after the target power is calculated
Earlier, the exclusion bounds were applied to every proposal, and in some cases, that was leading to much higher target powers than what was necessary. Signed-off-by: Sahas Subramanian <[email protected]>
1 parent 41e0a3a commit 5caf4ae

File tree

2 files changed

+115
-85
lines changed

2 files changed

+115
-85
lines changed

src/frequenz/sdk/microgrid/_power_managing/_shifting_matryoshka.py

Lines changed: 45 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -72,19 +72,11 @@ def _calc_targets( # pylint: disable=too-many-locals,too-many-branches
7272
if not proposals:
7373
return None, Bounds[Power](lower=lower_bound, upper=upper_bound)
7474

75-
exclusion_bounds = None
76-
if system_bounds.exclusion_bounds is not None and (
77-
system_bounds.exclusion_bounds.lower != Power.zero()
78-
or system_bounds.exclusion_bounds.upper != Power.zero()
79-
):
80-
exclusion_bounds = Bounds(
81-
system_bounds.exclusion_bounds.lower,
82-
system_bounds.exclusion_bounds.upper,
83-
)
75+
available_bounds = Bounds[Power](lower=lower_bound, upper=upper_bound)
76+
top_pri_bounds: Bounds[Power] | None = None
8477

8578
target_power = Power.zero()
8679
for next_proposal in sorted(proposals, reverse=True):
87-
unshifted_power = Power.zero()
8880
if priority is not None and next_proposal.priority <= priority:
8981
break
9082

@@ -104,45 +96,57 @@ def _calc_targets( # pylint: disable=too-many-locals,too-many-branches
10496
continue
10597

10698
if proposal_lower >= upper_bound:
107-
if proposal_power:
108-
proposal_power = upper_bound
99+
proposal_lower = upper_bound
100+
proposal_upper = upper_bound
109101
elif proposal_upper <= lower_bound:
110-
if proposal_power:
111-
proposal_power = lower_bound
112-
else:
113-
lower_bound = max(lower_bound, proposal_lower)
114-
upper_bound = min(upper_bound, proposal_upper)
115-
116-
if proposal_power:
117-
match _bounds.clamp_to_bounds(
102+
proposal_lower = lower_bound
103+
proposal_upper = lower_bound
104+
105+
lower_bound = max(lower_bound, proposal_lower)
106+
upper_bound = min(upper_bound, proposal_upper)
107+
108+
if proposal_power is not None:
109+
if top_pri_bounds is None and proposal_power != Power.zero():
110+
top_pri_bounds = Bounds[Power](lower=lower_bound, upper=upper_bound)
111+
clamped = _bounds.clamp_to_bounds(
118112
proposal_power,
119113
lower_bound,
120114
upper_bound,
121-
exclusion_bounds,
122-
):
115+
None,
116+
)
117+
match clamped:
118+
case (None, None):
119+
proposal_power = Power.zero()
123120
case (None, power) | (power, None) if power:
124-
unshifted_power = power
121+
proposal_power = power
125122
case (power_low, power_high) if power_low and power_high:
126123
if power_high - proposal_power < proposal_power - power_low:
127-
unshifted_power = power_high
124+
proposal_power = power_high
128125
else:
129-
unshifted_power = power_low
130-
case _:
131-
pass
132-
133-
lower_bound, upper_bound = _bounds.adjust_exclusion_bounds(
134-
lower_bound, upper_bound, exclusion_bounds
135-
)
136-
137-
lower_bound = lower_bound - unshifted_power
138-
upper_bound = upper_bound - unshifted_power
139-
target_power += unshifted_power
140-
141-
if exclusion_bounds is not None:
142-
exclusion_bounds = Bounds[Power](
143-
exclusion_bounds.lower - unshifted_power,
144-
exclusion_bounds.upper - unshifted_power,
145-
)
126+
proposal_power = power_low
127+
lower_bound = lower_bound - proposal_power
128+
upper_bound = upper_bound - proposal_power
129+
target_power += proposal_power
130+
131+
if top_pri_bounds is not None:
132+
available_bounds = top_pri_bounds
133+
134+
clamped = _bounds.clamp_to_bounds(
135+
target_power,
136+
available_bounds.lower,
137+
available_bounds.upper,
138+
system_bounds.exclusion_bounds,
139+
)
140+
match clamped:
141+
case (None, None):
142+
target_power = Power.zero()
143+
case (None, power) | (power, None) if power:
144+
target_power = power
145+
case (power_low, power_high) if power_low and power_high:
146+
if power_high - target_power < target_power - power_low:
147+
target_power = power_high
148+
else:
149+
target_power = power_low
146150

147151
return target_power, Bounds[Power](lower=lower_bound, upper=upper_bound)
148152

tests/actor/_power_managing/test_shifting_matryoshka.py

Lines changed: 70 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -122,15 +122,15 @@ async def test_matryoshka_no_excl() -> None: # pylint: disable=too-many-stateme
122122
tester.tgt_power(priority=3, power=10.0, bounds=(10.0, 15.0), expected=15.0)
123123
tester.bounds(priority=3, expected_power=15.0, expected_bounds=(-200.0, 200.0))
124124
tester.bounds(priority=2, expected_power=15.0, expected_bounds=(0.0, 5.0))
125-
tester.bounds(priority=1, expected_power=15.0, expected_bounds=(-5.0, 0.0))
125+
tester.bounds(priority=1, expected_power=15.0, expected_bounds=(0.0, 0.0))
126126

127127
tester.tgt_power(priority=3, power=10.0, bounds=(10.0, 22.0), expected=22.0)
128128
tester.bounds(priority=3, expected_power=22.0, expected_bounds=(-200.0, 200.0))
129129
tester.bounds(priority=2, expected_power=22.0, expected_bounds=(0.0, 12.0))
130-
tester.bounds(priority=1, expected_power=22.0, expected_bounds=(-12.0, 0.0))
130+
tester.bounds(priority=1, expected_power=22.0, expected_bounds=(0.0, 0.0))
131131

132132
tester.tgt_power(priority=1, power=30.0, bounds=(20.0, 50.0), expected=None)
133-
tester.bounds(priority=1, expected_power=22.0, expected_bounds=(-12.0, 0.0))
133+
tester.bounds(priority=1, expected_power=22.0, expected_bounds=(0.0, 0.0))
134134

135135
tester.tgt_power(priority=3, power=10.0, bounds=(10.0, 50.0), expected=50.0)
136136
tester.bounds(priority=3, expected_power=50.0, expected_bounds=(-200.0, 200.0))
@@ -140,7 +140,7 @@ async def test_matryoshka_no_excl() -> None: # pylint: disable=too-many-stateme
140140
tester.tgt_power(priority=2, power=40.0, bounds=(40.0, None), expected=None)
141141
tester.bounds(priority=3, expected_power=50.0, expected_bounds=(-200.0, 200.0))
142142
tester.bounds(priority=2, expected_power=50.0, expected_bounds=(0.0, 40.0))
143-
tester.bounds(priority=1, expected_power=50.0, expected_bounds=(-40.0, 0.0))
143+
tester.bounds(priority=1, expected_power=50.0, expected_bounds=(0.0, 0.0))
144144

145145
tester.tgt_power(priority=2, power=0.0, bounds=(-200.0, 200.0), expected=40.0)
146146
tester.bounds(priority=4, expected_power=40.0, expected_bounds=(-200.0, 200.0))
@@ -151,8 +151,8 @@ async def test_matryoshka_no_excl() -> None: # pylint: disable=too-many-stateme
151151
tester.tgt_power(priority=4, power=-50.0, bounds=(None, -50.0), expected=-50.0)
152152
tester.bounds(priority=4, expected_power=-50.0, expected_bounds=(-200.0, 200.0))
153153
tester.bounds(priority=3, expected_power=-50.0, expected_bounds=(-150.0, 0.0))
154-
tester.bounds(priority=2, expected_power=-50.0, expected_bounds=(-150.0, 0.0))
155-
tester.bounds(priority=1, expected_power=-50.0, expected_bounds=(-150.0, 0.0))
154+
tester.bounds(priority=2, expected_power=-50.0, expected_bounds=(0.0, 0.0))
155+
tester.bounds(priority=1, expected_power=-50.0, expected_bounds=(0.0, 0.0))
156156

157157
tester.tgt_power(priority=3, power=0.0, bounds=(-200.0, 200.0), expected=None)
158158
tester.bounds(priority=1, expected_power=-50.0, expected_bounds=(-150.0, 0.0))
@@ -202,6 +202,35 @@ async def test_matryoshka_no_excl() -> None: # pylint: disable=too-many-stateme
202202
tester.bounds(priority=1, expected_power=50.0, expected_bounds=(-150.0, 50.0))
203203

204204

205+
async def test_matryoshka_simple() -> None:
206+
"""Tests for the power managing actor.
207+
208+
With inclusion bounds, and exclusion bounds -30.0 to 0.0.
209+
"""
210+
batteries = frozenset({2, 5})
211+
212+
system_bounds = _base_types.SystemBounds(
213+
timestamp=datetime.now(tz=timezone.utc),
214+
inclusion_bounds=timeseries.Bounds(
215+
lower=Power.from_watts(-200.0), upper=Power.from_watts(200.0)
216+
),
217+
exclusion_bounds=timeseries.Bounds(
218+
lower=Power.from_watts(-30.0), upper=Power.from_watts(30.0)
219+
),
220+
)
221+
222+
tester = StatefulTester(batteries, system_bounds)
223+
tester.tgt_power(priority=3, power=None, bounds=(-200.0, 200.0), expected=0.0)
224+
tester.bounds(priority=2, expected_power=0.0, expected_bounds=(-200.0, 200.0))
225+
tester.tgt_power(priority=1, power=None, bounds=(-200.0, 200.0), expected=None)
226+
tester.bounds(priority=2, expected_power=0.0, expected_bounds=(-200.0, 200.0))
227+
tester.tgt_power(priority=2, power=25.0, bounds=(25.0, 50.0), expected=30.0)
228+
# tester.tgt_power(priority=2, power=-10.0, bounds=(-10.0, 50.0), expected=-10.0)
229+
# tester.bounds(priority=1, expected_power=-10.0, expected_bounds=(0.0, 60.0))
230+
# tester.tgt_power(priority=1, power=-10.0, bounds=(-10.0, 20.0), expected=None)
231+
# tester.bounds(priority=0, expected_power=-10.0, expected_bounds=(0.0, 20.0))
232+
233+
205234
async def test_matryoshka_with_excl_1() -> None:
206235
"""Tests for the power managing actor.
207236
@@ -228,32 +257,29 @@ async def test_matryoshka_with_excl_1() -> None:
228257
tester.tgt_power(priority=1, power=20.0, bounds=(20.0, 50.0), expected=45.0)
229258
tester.bounds(priority=1, expected_power=45.0, expected_bounds=(0.0, 25.0))
230259

231-
tester.tgt_power(priority=2, power=-10.0, bounds=(-10.0, 50.0), expected=20.0)
232-
tester.bounds(priority=1, expected_power=20.0, expected_bounds=(0.0, 50.0))
233-
tester.bounds(priority=0, expected_power=20.0, expected_bounds=(0.0, 30.0))
260+
tester.tgt_power(priority=2, power=-10.0, bounds=(-10.0, 50.0), expected=10.0)
261+
tester.bounds(priority=1, expected_power=10.0, expected_bounds=(0.0, 60.0))
262+
tester.bounds(priority=0, expected_power=10.0, expected_bounds=(0.0, 30.0))
234263

235264
tester.tgt_power(priority=1, power=-10.0, bounds=(-10.0, 50.0), expected=0.0)
236265
tester.bounds(priority=0, expected_power=0.0, expected_bounds=(0.0, 50.0))
237266

238-
tester.tgt_power(priority=1, power=-10.0, bounds=(-10.0, 20.0), expected=None)
239-
tester.bounds(priority=0, expected_power=0.0, expected_bounds=(0.0, 20.0))
240-
241267
tester.tgt_power(priority=1, power=-10.0, bounds=(-10.0, -5.0), expected=None)
242-
tester.bounds(priority=0, expected_power=0.0, expected_bounds=(0.0, 50.0))
268+
tester.bounds(priority=0, expected_power=0.0, expected_bounds=(0.0, 0.0))
243269

244-
tester.tgt_power(priority=2, power=-10.0, bounds=(-200.0, -5.0), expected=-40.0)
245-
tester.bounds(priority=1, expected_power=-40.0, expected_bounds=(-170.0, 0.0))
246-
tester.bounds(priority=0, expected_power=-40.0, expected_bounds=(0.0, 5.0))
270+
tester.tgt_power(priority=2, power=-10.0, bounds=(-200.0, -5.0), expected=-30.0)
271+
tester.bounds(priority=1, expected_power=-30.0, expected_bounds=(-190.0, 5.0))
272+
tester.bounds(priority=0, expected_power=-30.0, expected_bounds=(0.0, 5.0))
247273

248274
tester.tgt_power(priority=2, power=None, bounds=(None, None), expected=0.0)
249275
tester.bounds(priority=2, expected_power=0.0, expected_bounds=(-200.0, 200.0))
250276
tester.bounds(priority=1, expected_power=0.0, expected_bounds=(-200.0, 200.0))
251277

252278
tester.tgt_power(priority=1, power=-10.0, bounds=(-10.0, -5.0), expected=None)
253-
tester.bounds(priority=0, expected_power=0.0, expected_bounds=(0.0, 0.0))
279+
tester.bounds(priority=0, expected_power=0.0, expected_bounds=(0.0, 5.0))
254280

255281
tester.tgt_power(priority=1, power=-10.0, bounds=(-100.0, -5.0), expected=-30.0)
256-
tester.bounds(priority=0, expected_power=-30.0, expected_bounds=(-70.0, 0.0))
282+
tester.bounds(priority=0, expected_power=-30.0, expected_bounds=(-90.0, 5.0))
257283

258284
tester.tgt_power(priority=1, power=-40.0, bounds=(-100.0, -35.0), expected=-40.0)
259285
tester.bounds(priority=0, expected_power=-40.0, expected_bounds=(-60.0, 5.0))
@@ -280,13 +306,13 @@ async def test_matryoshka_with_excl_2() -> None:
280306

281307
tester.tgt_power(priority=2, power=25.0, bounds=(25.0, 50.0), expected=30.0)
282308
tester.bounds(priority=2, expected_power=30.0, expected_bounds=(-200.0, 200.0))
283-
tester.bounds(priority=1, expected_power=30.0, expected_bounds=(0.0, 20.0))
309+
tester.bounds(priority=1, expected_power=30.0, expected_bounds=(0.0, 25.0))
284310

285-
tester.tgt_power(priority=1, power=20.0, bounds=(20.0, 50.0), expected=50.0)
286-
tester.bounds(priority=1, expected_power=50.0, expected_bounds=(0.0, 20.0))
311+
tester.tgt_power(priority=1, power=20.0, bounds=(20.0, 50.0), expected=45.0)
312+
tester.bounds(priority=1, expected_power=45.0, expected_bounds=(0.0, 25.0))
287313

288-
tester.tgt_power(priority=1, power=10.0, bounds=(5.0, 10.0), expected=40.0)
289-
tester.bounds(priority=0, expected_power=40.0, expected_bounds=(-5.0, 0.0))
314+
tester.tgt_power(priority=1, power=10.0, bounds=(5.0, 10.0), expected=35.0)
315+
tester.bounds(priority=0, expected_power=35.0, expected_bounds=(-5.0, 0.0))
290316

291317
tester.tgt_power(priority=2, power=-10.0, bounds=(-10.0, 50.0), expected=0.0)
292318
tester.bounds(priority=1, expected_power=0.0, expected_bounds=(0.0, 60.0))
@@ -298,10 +324,10 @@ async def test_matryoshka_with_excl_2() -> None:
298324
tester.bounds(priority=0, expected_power=30.0, expected_bounds=(0.0, 50.0))
299325

300326
tester.tgt_power(priority=1, power=-10.0, bounds=(-10.0, 20.0), expected=-10.0)
301-
tester.bounds(priority=0, expected_power=-10.0, expected_bounds=(0.0, 10.0))
327+
tester.bounds(priority=0, expected_power=-10.0, expected_bounds=(0.0, 20.0))
302328

303-
tester.tgt_power(priority=1, power=-10.0, bounds=(-10.0, -5.0), expected=30.0)
304-
tester.bounds(priority=0, expected_power=30.0, expected_bounds=(0.0, 60.0))
329+
tester.tgt_power(priority=1, power=-10.0, bounds=(-10.0, -5.0), expected=None)
330+
tester.bounds(priority=0, expected_power=-10.0, expected_bounds=(0.0, 0.0))
305331

306332
tester.tgt_power(priority=2, power=-10.0, bounds=(-200.0, -5.0), expected=-20.0)
307333
tester.bounds(priority=1, expected_power=-20.0, expected_bounds=(-190.0, 5.0))
@@ -336,39 +362,39 @@ async def test_matryoshka_with_excl_3() -> None:
336362
tester.tgt_power(priority=2, power=-10.0, bounds=(-200.0, 200.0), expected=-30.0)
337363
tester.tgt_power(priority=2, power=0.0, bounds=(-200.0, 200.0), expected=0.0)
338364
tester.tgt_power(priority=3, power=20.0, bounds=(-200.0, 200.0), expected=30.0)
339-
tester.tgt_power(priority=1, power=-20.0, bounds=(-200.0, 200.0), expected=None)
365+
tester.tgt_power(priority=1, power=-20.0, bounds=(-200.0, 200.0), expected=0.0)
340366
tester.tgt_power(priority=3, power=None, bounds=(-200.0, 200.0), expected=-30.0)
341367
tester.tgt_power(priority=1, power=None, bounds=(-200.0, 200.0), expected=0.0)
342368

343369
tester.tgt_power(priority=2, power=25.0, bounds=(25.0, 50.0), expected=30.0)
344370
tester.bounds(priority=2, expected_power=30.0, expected_bounds=(-200.0, 200.0))
345-
tester.bounds(priority=1, expected_power=30.0, expected_bounds=(0.0, 20.0))
371+
tester.bounds(priority=1, expected_power=30.0, expected_bounds=(0.0, 25.0))
346372

347-
tester.tgt_power(priority=1, power=20.0, bounds=(20.0, 50.0), expected=50.0)
348-
tester.bounds(priority=1, expected_power=50.0, expected_bounds=(0.0, 20.0))
373+
tester.tgt_power(priority=1, power=20.0, bounds=(20.0, 50.0), expected=45.0)
374+
tester.bounds(priority=1, expected_power=45.0, expected_bounds=(0.0, 25.0))
349375

350-
tester.tgt_power(priority=1, power=10.0, bounds=(5.0, 10.0), expected=40.0)
351-
tester.bounds(priority=0, expected_power=40.0, expected_bounds=(-5.0, 0.0))
376+
tester.tgt_power(priority=1, power=10.0, bounds=(5.0, 10.0), expected=35.0)
377+
tester.bounds(priority=0, expected_power=35.0, expected_bounds=(-5.0, 0.0))
352378

353-
tester.tgt_power(priority=2, power=-10.0, bounds=(-10.0, 50.0), expected=None)
354-
tester.bounds(priority=1, expected_power=40.0, expected_bounds=(0.0, 20.0))
355-
tester.bounds(priority=0, expected_power=40.0, expected_bounds=(-5.0, 0.0))
379+
tester.tgt_power(priority=2, power=-10.0, bounds=(-10.0, 50.0), expected=30.0)
380+
tester.bounds(priority=1, expected_power=30.0, expected_bounds=(0.0, 60.0))
381+
tester.bounds(priority=0, expected_power=30.0, expected_bounds=(-5.0, 0.0))
356382

357-
tester.tgt_power(priority=1, power=40.0, bounds=(-10.0, 50.0), expected=50.0)
358-
tester.bounds(priority=0, expected_power=50.0, expected_bounds=(-20.0, 0.0))
383+
tester.tgt_power(priority=1, power=40.0, bounds=(-10.0, 50.0), expected=None)
384+
tester.bounds(priority=0, expected_power=30.0, expected_bounds=(-40.0, 10.0))
359385

360-
tester.tgt_power(priority=1, power=-10.0, bounds=(-10.0, 20.0), expected=30.0)
386+
tester.tgt_power(priority=1, power=-10.0, bounds=(-10.0, 20.0), expected=None)
361387
tester.bounds(priority=0, expected_power=30.0, expected_bounds=(0.0, 20.0))
362388

363-
tester.tgt_power(priority=2, power=-10.0, bounds=(-200.0, -5.0), expected=-40.0)
364-
tester.bounds(priority=1, expected_power=-40.0, expected_bounds=(-170.0, 0.0))
365-
tester.bounds(priority=0, expected_power=-40.0, expected_bounds=(0.0, 10.0))
389+
tester.tgt_power(priority=2, power=-10.0, bounds=(-200.0, -5.0), expected=-30.0)
390+
tester.bounds(priority=1, expected_power=-30.0, expected_bounds=(-190.0, 5.0))
391+
tester.bounds(priority=0, expected_power=-30.0, expected_bounds=(0.0, 15.0))
366392

367393
tester.tgt_power(priority=1, power=-10.0, bounds=(-100.0, -5.0), expected=None)
368-
tester.bounds(priority=0, expected_power=-40.0, expected_bounds=(-90.0, 5.0))
394+
tester.bounds(priority=0, expected_power=-30.0, expected_bounds=(-90.0, 5.0))
369395

370-
tester.tgt_power(priority=1, power=-40.0, bounds=(-100.0, -35.0), expected=-70.0)
371-
tester.bounds(priority=0, expected_power=-70.0, expected_bounds=(-60.0, 5.0))
396+
tester.tgt_power(priority=1, power=-40.0, bounds=(-100.0, -35.0), expected=-50.0)
397+
tester.bounds(priority=0, expected_power=-50.0, expected_bounds=(-60.0, 5.0))
372398

373399

374400
async def test_matryoshka_drop_old_proposals() -> None:

0 commit comments

Comments
 (0)