Skip to content

Commit 802508c

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 4b858b9 commit 802508c

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
@@ -80,19 +80,11 @@ def _calc_targets( # pylint: disable=too-many-locals,too-many-branches
8080
if not proposals:
8181
return None, Bounds[Power](lower=lower_bound, upper=upper_bound)
8282

83-
exclusion_bounds = None
84-
if system_bounds.exclusion_bounds is not None and (
85-
system_bounds.exclusion_bounds.lower != Power.zero()
86-
or system_bounds.exclusion_bounds.upper != Power.zero()
87-
):
88-
exclusion_bounds = Bounds(
89-
system_bounds.exclusion_bounds.lower,
90-
system_bounds.exclusion_bounds.upper,
91-
)
83+
available_bounds = Bounds[Power](lower=lower_bound, upper=upper_bound)
84+
top_pri_bounds: Bounds[Power] | None = None
9285

9386
target_power = Power.zero()
9487
for next_proposal in sorted(proposals, reverse=True):
95-
unshifted_power = Power.zero()
9688
if priority is not None and next_proposal.priority <= priority:
9789
break
9890

@@ -112,45 +104,57 @@ def _calc_targets( # pylint: disable=too-many-locals,too-many-branches
112104
continue
113105

114106
if proposal_lower >= upper_bound:
115-
if proposal_power:
116-
proposal_power = upper_bound
107+
proposal_lower = upper_bound
108+
proposal_upper = upper_bound
117109
elif proposal_upper <= lower_bound:
118-
if proposal_power:
119-
proposal_power = lower_bound
120-
else:
121-
lower_bound = max(lower_bound, proposal_lower)
122-
upper_bound = min(upper_bound, proposal_upper)
123-
124-
if proposal_power:
125-
match _bounds.clamp_to_bounds(
110+
proposal_lower = lower_bound
111+
proposal_upper = lower_bound
112+
113+
lower_bound = max(lower_bound, proposal_lower)
114+
upper_bound = min(upper_bound, proposal_upper)
115+
116+
if proposal_power is not None:
117+
if top_pri_bounds is None and proposal_power != Power.zero():
118+
top_pri_bounds = Bounds[Power](lower=lower_bound, upper=upper_bound)
119+
clamped = _bounds.clamp_to_bounds(
126120
proposal_power,
127121
lower_bound,
128122
upper_bound,
129-
exclusion_bounds,
130-
):
123+
None,
124+
)
125+
match clamped:
126+
case (None, None):
127+
proposal_power = Power.zero()
131128
case (None, power) | (power, None) if power:
132-
unshifted_power = power
129+
proposal_power = power
133130
case (power_low, power_high) if power_low and power_high:
134131
if power_high - proposal_power < proposal_power - power_low:
135-
unshifted_power = power_high
132+
proposal_power = power_high
136133
else:
137-
unshifted_power = power_low
138-
case _:
139-
pass
140-
141-
lower_bound, upper_bound = _bounds.adjust_exclusion_bounds(
142-
lower_bound, upper_bound, exclusion_bounds
143-
)
144-
145-
lower_bound = lower_bound - unshifted_power
146-
upper_bound = upper_bound - unshifted_power
147-
target_power += unshifted_power
148-
149-
if exclusion_bounds is not None:
150-
exclusion_bounds = Bounds[Power](
151-
exclusion_bounds.lower - unshifted_power,
152-
exclusion_bounds.upper - unshifted_power,
153-
)
134+
proposal_power = power_low
135+
lower_bound = lower_bound - proposal_power
136+
upper_bound = upper_bound - proposal_power
137+
target_power += proposal_power
138+
139+
if top_pri_bounds is not None:
140+
available_bounds = top_pri_bounds
141+
142+
clamped = _bounds.clamp_to_bounds(
143+
target_power,
144+
available_bounds.lower,
145+
available_bounds.upper,
146+
system_bounds.exclusion_bounds,
147+
)
148+
match clamped:
149+
case (None, None):
150+
target_power = Power.zero()
151+
case (None, power) | (power, None) if power:
152+
target_power = power
153+
case (power_low, power_high) if power_low and power_high:
154+
if power_high - target_power < target_power - power_low:
155+
target_power = power_high
156+
else:
157+
target_power = power_low
154158

155159
return target_power, Bounds[Power](lower=lower_bound, upper=upper_bound)
156160

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)