Skip to content

Commit

Permalink
Distinguish between grid meters and other meters (#1052)
Browse files Browse the repository at this point in the history
The component graph methods for identifying meters as
{pv/ev/battery/chp} meters were sometimes incorrectly identifying grid
meters as one of {pv/ev/battery/chp} meters.

This PR fixes that issue.
  • Loading branch information
shsms committed Aug 26, 2024
2 parents 16bb5cb + c08f7c9 commit 4641bd9
Show file tree
Hide file tree
Showing 3 changed files with 222 additions and 0 deletions.
2 changes: 2 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@
- Bump the `grpclib` dependency to pull a fix for using IPv6 addresses.

- Allow setting `api_power_request_timeout` in `microgrid.initialize()`.

- Fix an issue where in grid meters could be identified as {pv/ev/battery/chp} meters in some component graph configurations.
44 changes: 44 additions & 0 deletions src/frequenz/sdk/microgrid/component_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,20 @@ def successors(self, component_id: int) -> set[Component]:
KeyError: if the specified `component_id` is not in the graph
"""

@abstractmethod
def is_grid_meter(self, component: Component) -> bool:
"""Check if the specified component is a grid meter.
This is done by checking if the component is the only successor to the `Grid`
component.
Args:
component: component to check.
Returns:
Whether the specified component is a grid meter.
"""

@abstractmethod
def is_pv_inverter(self, component: Component) -> bool:
"""Check if the specified component is a PV inverter.
Expand Down Expand Up @@ -567,6 +581,32 @@ def validate(self) -> None:
self._validate_intermediary_components()
self._validate_leaf_components()

def is_grid_meter(self, component: Component) -> bool:
"""Check if the specified component is a grid meter.
This is done by checking if the component is the only successor to the `Grid`
component.
Args:
component: component to check.
Returns:
Whether the specified component is a grid meter.
"""
if component.category != ComponentCategory.METER:
return False

predecessors = self.predecessors(component.component_id)
if len(predecessors) != 1:
return False

predecessor = next(iter(predecessors))
if predecessor.category != ComponentCategory.GRID:
return False

grid_successors = self.successors(predecessor.component_id)
return len(grid_successors) == 1

def is_pv_inverter(self, component: Component) -> bool:
"""Check if the specified component is a PV inverter.
Expand Down Expand Up @@ -596,6 +636,7 @@ def is_pv_meter(self, component: Component) -> bool:
successors = self.successors(component.component_id)
return (
component.category == ComponentCategory.METER
and not self.is_grid_meter(component)
and len(successors) > 0
and all(
self.is_pv_inverter(successor)
Expand Down Expand Up @@ -643,6 +684,7 @@ def is_ev_charger_meter(self, component: Component) -> bool:
successors = self.successors(component.component_id)
return (
component.category == ComponentCategory.METER
and not self.is_grid_meter(component)
and len(successors) > 0
and all(self.is_ev_charger(successor) for successor in successors)
)
Expand Down Expand Up @@ -690,6 +732,7 @@ def is_battery_meter(self, component: Component) -> bool:
successors = self.successors(component.component_id)
return (
component.category == ComponentCategory.METER
and not self.is_grid_meter(component)
and len(successors) > 0
and all(self.is_battery_inverter(successor) for successor in successors)
)
Expand Down Expand Up @@ -734,6 +777,7 @@ def is_chp_meter(self, component: Component) -> bool:
successors = self.successors(component.component_id)
return (
component.category == ComponentCategory.METER
and not self.is_grid_meter(component)
and len(successors) > 0
and all(self.is_chp(successor) for successor in successors)
)
Expand Down
176 changes: 176 additions & 0 deletions tests/microgrid/test_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,29 @@ def test_dfs_search_grid_meter(self) -> None:
result = graph.dfs(grid, set(), graph.is_pv_chain)
assert result == pv_meters

def test_dfs_search_grid_meter_no_pv_meter(self) -> None:
"""Test DFS searching PV components in a graph with a single grid meter."""
grid = Component(1, ComponentCategory.GRID)
pv_inverters = {
Component(3, ComponentCategory.INVERTER, InverterType.SOLAR),
Component(4, ComponentCategory.INVERTER, InverterType.SOLAR),
}

graph = gr._MicrogridComponentGraph(
components={
grid,
Component(2, ComponentCategory.METER),
}.union(pv_inverters),
connections={
Connection(1, 2),
Connection(2, 3),
Connection(2, 4),
},
)

result = graph.dfs(grid, set(), graph.is_pv_chain)
assert result == pv_inverters

def test_dfs_search_no_grid_meter(self) -> None:
"""Test DFS searching PV components in a graph with no grid meter."""
grid = Component(1, ComponentCategory.GRID)
Expand Down Expand Up @@ -1477,3 +1500,156 @@ def test_graph_correction(self) -> None:
assert set(graph.components()) == expected

assert list(graph.connections()) == [Connection(0, 8)]


class TestComponentTypeIdentification:
"""Test the component type identification methods in the component graph."""

def test_no_comp_meters_pv(self) -> None:
"""Test the case where there are no meters in the graph."""
grid = Component(1, ComponentCategory.GRID)
grid_meter = Component(2, ComponentCategory.METER)
pv_inv_1 = Component(3, ComponentCategory.INVERTER, InverterType.SOLAR)
pv_inv_2 = Component(4, ComponentCategory.INVERTER, InverterType.SOLAR)

graph = gr._MicrogridComponentGraph(
components={
grid,
grid_meter,
pv_inv_1,
pv_inv_2,
},
connections={
Connection(1, 2),
Connection(2, 3),
Connection(2, 4),
},
)

assert graph.is_grid_meter(grid_meter)
assert not graph.is_pv_meter(grid_meter)
assert not graph.is_pv_chain(grid_meter)

assert graph.is_pv_inverter(pv_inv_1) and graph.is_pv_chain(pv_inv_1)
assert graph.is_pv_inverter(pv_inv_2) and graph.is_pv_chain(pv_inv_2)

def test_no_comp_meters_mixed(self) -> None:
"""Test the case where there are no meters in the graph."""
grid = Component(1, ComponentCategory.GRID)
grid_meter = Component(2, ComponentCategory.METER)
pv_inv = Component(3, ComponentCategory.INVERTER, InverterType.SOLAR)
battery_inv = Component(4, ComponentCategory.INVERTER, InverterType.BATTERY)
battery = Component(5, ComponentCategory.BATTERY)

graph = gr._MicrogridComponentGraph(
components={
grid,
grid_meter,
pv_inv,
battery_inv,
battery,
},
connections={
Connection(1, 2),
Connection(2, 3),
Connection(2, 4),
Connection(4, 5),
},
)

assert graph.is_grid_meter(grid_meter)
assert not graph.is_pv_meter(grid_meter)
assert not graph.is_pv_chain(grid_meter)

assert graph.is_pv_inverter(pv_inv) and graph.is_pv_chain(pv_inv)
assert not graph.is_battery_inverter(pv_inv) and not graph.is_battery_chain(
pv_inv
)

assert graph.is_battery_inverter(battery_inv) and graph.is_battery_chain(
battery_inv
)
assert not graph.is_pv_inverter(battery_inv) and not graph.is_pv_chain(
battery_inv
)

def test_with_meters(self) -> None:
"""Test the case where there are meters in the graph."""
grid = Component(1, ComponentCategory.GRID)
grid_meter = Component(2, ComponentCategory.METER)
pv_meter = Component(3, ComponentCategory.METER)
pv_inv = Component(4, ComponentCategory.INVERTER, InverterType.SOLAR)
battery_meter = Component(5, ComponentCategory.METER)
battery_inv = Component(6, ComponentCategory.INVERTER, InverterType.BATTERY)
battery = Component(7, ComponentCategory.BATTERY)

graph = gr._MicrogridComponentGraph(
components={
grid,
grid_meter,
pv_meter,
pv_inv,
battery_meter,
battery_inv,
battery,
},
connections={
Connection(1, 2),
Connection(2, 3),
Connection(3, 4),
Connection(2, 5),
Connection(5, 6),
Connection(6, 7),
},
)

assert graph.is_grid_meter(grid_meter)
assert not graph.is_pv_meter(grid_meter)
assert not graph.is_pv_chain(grid_meter)

assert graph.is_pv_meter(pv_meter)
assert graph.is_pv_chain(pv_meter)
assert graph.is_pv_chain(pv_inv)
assert graph.is_pv_inverter(pv_inv)

assert graph.is_battery_meter(battery_meter)
assert graph.is_battery_chain(battery_meter)
assert graph.is_battery_chain(battery_inv)
assert graph.is_battery_inverter(battery_inv)

def test_without_grid_meters(self) -> None:
"""Test the case where there are no grid meters in the graph."""
grid = Component(1, ComponentCategory.GRID)
ev_meter = Component(2, ComponentCategory.METER)
ev_charger = Component(3, ComponentCategory.EV_CHARGER)
chp_meter = Component(4, ComponentCategory.METER)
chp = Component(5, ComponentCategory.CHP)

graph = gr._MicrogridComponentGraph(
components={
grid,
ev_meter,
ev_charger,
chp_meter,
chp,
},
connections={
Connection(1, 2),
Connection(2, 3),
Connection(1, 4),
Connection(4, 5),
},
)

assert not graph.is_grid_meter(ev_meter)
assert not graph.is_grid_meter(chp_meter)

assert graph.is_ev_charger_meter(ev_meter)
assert graph.is_ev_charger(ev_charger)
assert graph.is_ev_charger_chain(ev_meter)
assert graph.is_ev_charger_chain(ev_charger)

assert graph.is_chp_meter(chp_meter)
assert graph.is_chp(chp)
assert graph.is_chp_chain(chp_meter)
assert graph.is_chp_chain(chp)

0 comments on commit 4641bd9

Please sign in to comment.