Skip to content

Commit ee4c2f8

Browse files
authored
Fix succeeded_power calculation in PV power distribution (#1217)
This was reported incorrectly earlier using a variable that was never set. That variable and related (unused) channels have been removed. This also updates the PV pool tests to ensure that the distribution results are accurate.
2 parents 255fd11 + 02fbd21 commit ee4c2f8

File tree

3 files changed

+106
-11
lines changed

3 files changed

+106
-11
lines changed

RELEASE_NOTES.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747

4848
## Bug Fixes
4949

50-
- Components used to be just forgotten by the power manager when all proposals are withdrawn, leaving them at their last set power values. This has been fixed by getting the power manager to set the components to their default powers, based on the component category (according to the table below), as the last step.
50+
- The power manager used to just forgot components when all proposals to them are withdrawn, leaving them at their last set power values. This has been fixed by getting the power manager to set the components to their default powers, based on the component category (according to the table below), as the last step.
5151

5252

5353
| component category | default power |
@@ -58,4 +58,6 @@
5858

5959
- PV Pool instances can now be created in sites without any PV. This allows for writing generic code that works for all locations, that depends on the PV power formula, for example.
6060

61+
- `Success/PartialFailure` results from `PVPool.power_distribution_results` now report correct `succeeded_power` values.
62+
6163
- The `find_first_descendant_component` method in the component graph was allowing non-root components to be used as the root component during traversal. This was leading to confusing behaviour when the root component couldn't be identified deterministically. For example, if the root category was specified as a meter, it could start traversing from a different meter each time. It is no-longer possible to specify a root category anymore and it always traverses from the `GRID` component.

src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_pv_inverter_manager/_pv_inverter_manager.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import logging
99
from datetime import timedelta
1010

11-
from frequenz.channels import Broadcast, LatestValueCache, Sender
11+
from frequenz.channels import LatestValueCache, Sender
1212
from frequenz.client.microgrid import (
1313
ApiClientError,
1414
ComponentCategory,
@@ -67,9 +67,6 @@ def __init__(
6767
self._component_data_caches: dict[
6868
ComponentId, LatestValueCache[InverterData]
6969
] = {}
70-
self._target_power = Power.zero()
71-
self._target_power_channel = Broadcast[Request](name="target_power")
72-
self._target_power_tx = self._target_power_channel.new_sender()
7370
self._task: asyncio.Task[None] | None = None
7471

7572
@override
@@ -241,7 +238,7 @@ async def _set_api_power( # pylint: disable=too-many-locals
241238
failed_components=failed_components,
242239
succeeded_components=succeeded_components,
243240
failed_power=failed_power,
244-
succeeded_power=self._target_power - failed_power,
241+
succeeded_power=request.power - failed_power - remaining_power,
245242
excess_power=remaining_power,
246243
request=request,
247244
)
@@ -250,7 +247,7 @@ async def _set_api_power( # pylint: disable=too-many-locals
250247
await self._results_sender.send(
251248
Success(
252249
succeeded_components=succeeded_components,
253-
succeeded_power=self._target_power,
250+
succeeded_power=request.power - remaining_power,
254251
excess_power=remaining_power,
255252
request=request,
256253
)

tests/timeseries/_pv_pool/test_pv_pool_control_methods.py

Lines changed: 100 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@ async def test_setting_power( # pylint: disable=too-many-statements
151151
bounds_rx,
152152
lambda x: x.bounds is not None and x.bounds.lower.as_watts() == -100000.0,
153153
)
154+
dist_results_rx = pv_pool.power_distribution_results.new_receiver()
155+
154156
self._assert_report(latest_report, power=None, lower=-100000.0, upper=0.0)
155157
await pv_pool.propose_power(Power.from_watts(-80000.0))
156158
await self._recv_reports_until(
@@ -172,6 +174,18 @@ async def test_setting_power( # pylint: disable=too-many-statements
172174
mocker.call(inv_ids[2], -25000.0),
173175
mocker.call(inv_ids[3], -25000.0),
174176
]
177+
dist_results = await dist_results_rx.receive()
178+
assert isinstance(
179+
dist_results, _power_distributing.Success
180+
), f"Expected a success, got {dist_results}"
181+
assert dist_results.succeeded_power == Power.from_watts(-80000.0)
182+
assert dist_results.excess_power == Power.zero()
183+
assert dist_results.succeeded_components == {
184+
ComponentId(8),
185+
ComponentId(18),
186+
ComponentId(28),
187+
ComponentId(38),
188+
}
175189

176190
set_power.reset_mock()
177191
await pv_pool.propose_power(Power.from_watts(-4000.0))
@@ -194,6 +208,18 @@ async def test_setting_power( # pylint: disable=too-many-statements
194208
mocker.call(inv_ids[2], -1000.0),
195209
mocker.call(inv_ids[3], -1000.0),
196210
]
211+
dist_results = await dist_results_rx.receive()
212+
assert isinstance(
213+
dist_results, _power_distributing.Success
214+
), f"Expected a success, got {dist_results}"
215+
assert dist_results.succeeded_power == Power.from_watts(-4000.0)
216+
assert dist_results.excess_power == Power.zero()
217+
assert dist_results.succeeded_components == {
218+
ComponentId(8),
219+
ComponentId(18),
220+
ComponentId(28),
221+
ComponentId(38),
222+
}
197223

198224
# After failing 1 inverter, bounds should go down and power shouldn't be
199225
# distributed to that inverter.
@@ -205,6 +231,17 @@ async def test_setting_power( # pylint: disable=too-many-statements
205231
self._assert_report(
206232
await bounds_rx.receive(), power=-4000.0, lower=-80000.0, upper=0.0
207233
)
234+
dist_results = await dist_results_rx.receive()
235+
assert isinstance(
236+
dist_results, _power_distributing.Success
237+
), f"Expected a success, got {dist_results}"
238+
assert dist_results.succeeded_power == Power.from_watts(-4000.0)
239+
assert dist_results.excess_power == Power.zero()
240+
assert dist_results.succeeded_components == {
241+
ComponentId(8),
242+
ComponentId(28),
243+
ComponentId(38),
244+
}
208245

209246
set_power.reset_mock()
210247
await pv_pool.propose_power(Power.from_watts(-70000.0))
@@ -227,6 +264,17 @@ async def test_setting_power( # pylint: disable=too-many-statements
227264
mocker.call(inv_ids[2], -30000.0),
228265
mocker.call(inv_ids[3], -30000.0),
229266
]
267+
dist_results = await dist_results_rx.receive()
268+
assert isinstance(
269+
dist_results, _power_distributing.Success
270+
), f"Expected a success, got {dist_results}"
271+
assert dist_results.succeeded_power == Power.from_watts(-70000.0)
272+
assert dist_results.excess_power == Power.zero()
273+
assert dist_results.succeeded_components == {
274+
ComponentId(8),
275+
ComponentId(28),
276+
ComponentId(38),
277+
}
230278

231279
# After the failed inverter recovers, bounds should go back up and power
232280
# should be distributed to all inverters
@@ -238,17 +286,29 @@ async def test_setting_power( # pylint: disable=too-many-statements
238286
self._assert_report(
239287
await bounds_rx.receive(), power=-70000.0, lower=-100000.0, upper=0.0
240288
)
289+
dist_results = await dist_results_rx.receive()
290+
assert isinstance(
291+
dist_results, _power_distributing.Success
292+
), f"Expected a success, got {dist_results}"
293+
assert dist_results.succeeded_power == Power.from_watts(-70000.0)
294+
assert dist_results.excess_power == Power.zero()
295+
assert dist_results.succeeded_components == {
296+
ComponentId(8),
297+
ComponentId(18),
298+
ComponentId(28),
299+
ComponentId(38),
300+
}
241301

242302
set_power.reset_mock()
243-
await pv_pool.propose_power(Power.from_watts(-90000.0))
303+
await pv_pool.propose_power(Power.from_watts(-200000.0))
244304
await self._recv_reports_until(
245305
bounds_rx,
246306
lambda x: x.target_power is not None
247-
and x.target_power.as_watts() == -90000.0,
307+
and x.target_power.as_watts() == -100000.0,
248308
)
249309

250310
self._assert_report(
251-
await bounds_rx.receive(), power=-90000.0, lower=-100000.0, upper=0.0
311+
await bounds_rx.receive(), power=-100000.0, lower=-100000.0, upper=0.0
252312
)
253313
await asyncio.sleep(0.0)
254314

@@ -258,8 +318,20 @@ async def test_setting_power( # pylint: disable=too-many-statements
258318
mocker.call(inv_ids[0], -10000.0),
259319
mocker.call(inv_ids[1], -20000.0),
260320
mocker.call(inv_ids[2], -30000.0),
261-
mocker.call(inv_ids[3], -30000.0),
321+
mocker.call(inv_ids[3], -40000.0),
262322
]
323+
dist_results = await dist_results_rx.receive()
324+
assert isinstance(
325+
dist_results, _power_distributing.Success
326+
), f"Expected a success, got {dist_results}"
327+
assert dist_results.succeeded_power == Power.from_watts(-100000.0)
328+
assert dist_results.excess_power == Power.zero()
329+
assert dist_results.succeeded_components == {
330+
ComponentId(8),
331+
ComponentId(18),
332+
ComponentId(28),
333+
ComponentId(38),
334+
}
263335

264336
# Setting 0 power should set all inverters to 0
265337
set_power.reset_mock()
@@ -281,6 +353,18 @@ async def test_setting_power( # pylint: disable=too-many-statements
281353
mocker.call(inv_ids[2], 0.0),
282354
mocker.call(inv_ids[3], 0.0),
283355
]
356+
dist_results = await dist_results_rx.receive()
357+
assert isinstance(
358+
dist_results, _power_distributing.Success
359+
), f"Expected a success, got {dist_results}"
360+
assert dist_results.succeeded_power == Power.zero()
361+
assert dist_results.excess_power == Power.zero()
362+
assert dist_results.succeeded_components == {
363+
ComponentId(8),
364+
ComponentId(18),
365+
ComponentId(28),
366+
ComponentId(38),
367+
}
284368

285369
# Resetting the power should lead to default (full) power getting set for all
286370
# inverters.
@@ -301,3 +385,15 @@ async def test_setting_power( # pylint: disable=too-many-statements
301385
mocker.call(inv_ids[2], -30_000.0),
302386
mocker.call(inv_ids[3], -40_000.0),
303387
]
388+
dist_results = await dist_results_rx.receive()
389+
assert isinstance(
390+
dist_results, _power_distributing.Success
391+
), f"Expected a success, got {dist_results}"
392+
assert dist_results.succeeded_power == Power.from_watts(-100000.0)
393+
assert dist_results.excess_power == Power.zero()
394+
assert dist_results.succeeded_components == {
395+
ComponentId(8),
396+
ComponentId(18),
397+
ComponentId(28),
398+
ComponentId(38),
399+
}

0 commit comments

Comments
 (0)