Skip to content

Commit 4641bd9

Browse files
authored
Distinguish between grid meters and other meters (#1052)
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.
2 parents 16bb5cb + c08f7c9 commit 4641bd9

File tree

3 files changed

+222
-0
lines changed

3 files changed

+222
-0
lines changed

RELEASE_NOTES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,5 @@
1717
- Bump the `grpclib` dependency to pull a fix for using IPv6 addresses.
1818

1919
- Allow setting `api_power_request_timeout` in `microgrid.initialize()`.
20+
21+
- Fix an issue where in grid meters could be identified as {pv/ev/battery/chp} meters in some component graph configurations.

src/frequenz/sdk/microgrid/component_graph.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,20 @@ def successors(self, component_id: int) -> set[Component]:
120120
KeyError: if the specified `component_id` is not in the graph
121121
"""
122122

123+
@abstractmethod
124+
def is_grid_meter(self, component: Component) -> bool:
125+
"""Check if the specified component is a grid meter.
126+
127+
This is done by checking if the component is the only successor to the `Grid`
128+
component.
129+
130+
Args:
131+
component: component to check.
132+
133+
Returns:
134+
Whether the specified component is a grid meter.
135+
"""
136+
123137
@abstractmethod
124138
def is_pv_inverter(self, component: Component) -> bool:
125139
"""Check if the specified component is a PV inverter.
@@ -567,6 +581,32 @@ def validate(self) -> None:
567581
self._validate_intermediary_components()
568582
self._validate_leaf_components()
569583

584+
def is_grid_meter(self, component: Component) -> bool:
585+
"""Check if the specified component is a grid meter.
586+
587+
This is done by checking if the component is the only successor to the `Grid`
588+
component.
589+
590+
Args:
591+
component: component to check.
592+
593+
Returns:
594+
Whether the specified component is a grid meter.
595+
"""
596+
if component.category != ComponentCategory.METER:
597+
return False
598+
599+
predecessors = self.predecessors(component.component_id)
600+
if len(predecessors) != 1:
601+
return False
602+
603+
predecessor = next(iter(predecessors))
604+
if predecessor.category != ComponentCategory.GRID:
605+
return False
606+
607+
grid_successors = self.successors(predecessor.component_id)
608+
return len(grid_successors) == 1
609+
570610
def is_pv_inverter(self, component: Component) -> bool:
571611
"""Check if the specified component is a PV inverter.
572612
@@ -596,6 +636,7 @@ def is_pv_meter(self, component: Component) -> bool:
596636
successors = self.successors(component.component_id)
597637
return (
598638
component.category == ComponentCategory.METER
639+
and not self.is_grid_meter(component)
599640
and len(successors) > 0
600641
and all(
601642
self.is_pv_inverter(successor)
@@ -643,6 +684,7 @@ def is_ev_charger_meter(self, component: Component) -> bool:
643684
successors = self.successors(component.component_id)
644685
return (
645686
component.category == ComponentCategory.METER
687+
and not self.is_grid_meter(component)
646688
and len(successors) > 0
647689
and all(self.is_ev_charger(successor) for successor in successors)
648690
)
@@ -690,6 +732,7 @@ def is_battery_meter(self, component: Component) -> bool:
690732
successors = self.successors(component.component_id)
691733
return (
692734
component.category == ComponentCategory.METER
735+
and not self.is_grid_meter(component)
693736
and len(successors) > 0
694737
and all(self.is_battery_inverter(successor) for successor in successors)
695738
)
@@ -734,6 +777,7 @@ def is_chp_meter(self, component: Component) -> bool:
734777
successors = self.successors(component.component_id)
735778
return (
736779
component.category == ComponentCategory.METER
780+
and not self.is_grid_meter(component)
737781
and len(successors) > 0
738782
and all(self.is_chp(successor) for successor in successors)
739783
)

tests/microgrid/test_graph.py

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,29 @@ def test_dfs_search_grid_meter(self) -> None:
465465
result = graph.dfs(grid, set(), graph.is_pv_chain)
466466
assert result == pv_meters
467467

468+
def test_dfs_search_grid_meter_no_pv_meter(self) -> None:
469+
"""Test DFS searching PV components in a graph with a single grid meter."""
470+
grid = Component(1, ComponentCategory.GRID)
471+
pv_inverters = {
472+
Component(3, ComponentCategory.INVERTER, InverterType.SOLAR),
473+
Component(4, ComponentCategory.INVERTER, InverterType.SOLAR),
474+
}
475+
476+
graph = gr._MicrogridComponentGraph(
477+
components={
478+
grid,
479+
Component(2, ComponentCategory.METER),
480+
}.union(pv_inverters),
481+
connections={
482+
Connection(1, 2),
483+
Connection(2, 3),
484+
Connection(2, 4),
485+
},
486+
)
487+
488+
result = graph.dfs(grid, set(), graph.is_pv_chain)
489+
assert result == pv_inverters
490+
468491
def test_dfs_search_no_grid_meter(self) -> None:
469492
"""Test DFS searching PV components in a graph with no grid meter."""
470493
grid = Component(1, ComponentCategory.GRID)
@@ -1477,3 +1500,156 @@ def test_graph_correction(self) -> None:
14771500
assert set(graph.components()) == expected
14781501

14791502
assert list(graph.connections()) == [Connection(0, 8)]
1503+
1504+
1505+
class TestComponentTypeIdentification:
1506+
"""Test the component type identification methods in the component graph."""
1507+
1508+
def test_no_comp_meters_pv(self) -> None:
1509+
"""Test the case where there are no meters in the graph."""
1510+
grid = Component(1, ComponentCategory.GRID)
1511+
grid_meter = Component(2, ComponentCategory.METER)
1512+
pv_inv_1 = Component(3, ComponentCategory.INVERTER, InverterType.SOLAR)
1513+
pv_inv_2 = Component(4, ComponentCategory.INVERTER, InverterType.SOLAR)
1514+
1515+
graph = gr._MicrogridComponentGraph(
1516+
components={
1517+
grid,
1518+
grid_meter,
1519+
pv_inv_1,
1520+
pv_inv_2,
1521+
},
1522+
connections={
1523+
Connection(1, 2),
1524+
Connection(2, 3),
1525+
Connection(2, 4),
1526+
},
1527+
)
1528+
1529+
assert graph.is_grid_meter(grid_meter)
1530+
assert not graph.is_pv_meter(grid_meter)
1531+
assert not graph.is_pv_chain(grid_meter)
1532+
1533+
assert graph.is_pv_inverter(pv_inv_1) and graph.is_pv_chain(pv_inv_1)
1534+
assert graph.is_pv_inverter(pv_inv_2) and graph.is_pv_chain(pv_inv_2)
1535+
1536+
def test_no_comp_meters_mixed(self) -> None:
1537+
"""Test the case where there are no meters in the graph."""
1538+
grid = Component(1, ComponentCategory.GRID)
1539+
grid_meter = Component(2, ComponentCategory.METER)
1540+
pv_inv = Component(3, ComponentCategory.INVERTER, InverterType.SOLAR)
1541+
battery_inv = Component(4, ComponentCategory.INVERTER, InverterType.BATTERY)
1542+
battery = Component(5, ComponentCategory.BATTERY)
1543+
1544+
graph = gr._MicrogridComponentGraph(
1545+
components={
1546+
grid,
1547+
grid_meter,
1548+
pv_inv,
1549+
battery_inv,
1550+
battery,
1551+
},
1552+
connections={
1553+
Connection(1, 2),
1554+
Connection(2, 3),
1555+
Connection(2, 4),
1556+
Connection(4, 5),
1557+
},
1558+
)
1559+
1560+
assert graph.is_grid_meter(grid_meter)
1561+
assert not graph.is_pv_meter(grid_meter)
1562+
assert not graph.is_pv_chain(grid_meter)
1563+
1564+
assert graph.is_pv_inverter(pv_inv) and graph.is_pv_chain(pv_inv)
1565+
assert not graph.is_battery_inverter(pv_inv) and not graph.is_battery_chain(
1566+
pv_inv
1567+
)
1568+
1569+
assert graph.is_battery_inverter(battery_inv) and graph.is_battery_chain(
1570+
battery_inv
1571+
)
1572+
assert not graph.is_pv_inverter(battery_inv) and not graph.is_pv_chain(
1573+
battery_inv
1574+
)
1575+
1576+
def test_with_meters(self) -> None:
1577+
"""Test the case where there are meters in the graph."""
1578+
grid = Component(1, ComponentCategory.GRID)
1579+
grid_meter = Component(2, ComponentCategory.METER)
1580+
pv_meter = Component(3, ComponentCategory.METER)
1581+
pv_inv = Component(4, ComponentCategory.INVERTER, InverterType.SOLAR)
1582+
battery_meter = Component(5, ComponentCategory.METER)
1583+
battery_inv = Component(6, ComponentCategory.INVERTER, InverterType.BATTERY)
1584+
battery = Component(7, ComponentCategory.BATTERY)
1585+
1586+
graph = gr._MicrogridComponentGraph(
1587+
components={
1588+
grid,
1589+
grid_meter,
1590+
pv_meter,
1591+
pv_inv,
1592+
battery_meter,
1593+
battery_inv,
1594+
battery,
1595+
},
1596+
connections={
1597+
Connection(1, 2),
1598+
Connection(2, 3),
1599+
Connection(3, 4),
1600+
Connection(2, 5),
1601+
Connection(5, 6),
1602+
Connection(6, 7),
1603+
},
1604+
)
1605+
1606+
assert graph.is_grid_meter(grid_meter)
1607+
assert not graph.is_pv_meter(grid_meter)
1608+
assert not graph.is_pv_chain(grid_meter)
1609+
1610+
assert graph.is_pv_meter(pv_meter)
1611+
assert graph.is_pv_chain(pv_meter)
1612+
assert graph.is_pv_chain(pv_inv)
1613+
assert graph.is_pv_inverter(pv_inv)
1614+
1615+
assert graph.is_battery_meter(battery_meter)
1616+
assert graph.is_battery_chain(battery_meter)
1617+
assert graph.is_battery_chain(battery_inv)
1618+
assert graph.is_battery_inverter(battery_inv)
1619+
1620+
def test_without_grid_meters(self) -> None:
1621+
"""Test the case where there are no grid meters in the graph."""
1622+
grid = Component(1, ComponentCategory.GRID)
1623+
ev_meter = Component(2, ComponentCategory.METER)
1624+
ev_charger = Component(3, ComponentCategory.EV_CHARGER)
1625+
chp_meter = Component(4, ComponentCategory.METER)
1626+
chp = Component(5, ComponentCategory.CHP)
1627+
1628+
graph = gr._MicrogridComponentGraph(
1629+
components={
1630+
grid,
1631+
ev_meter,
1632+
ev_charger,
1633+
chp_meter,
1634+
chp,
1635+
},
1636+
connections={
1637+
Connection(1, 2),
1638+
Connection(2, 3),
1639+
Connection(1, 4),
1640+
Connection(4, 5),
1641+
},
1642+
)
1643+
1644+
assert not graph.is_grid_meter(ev_meter)
1645+
assert not graph.is_grid_meter(chp_meter)
1646+
1647+
assert graph.is_ev_charger_meter(ev_meter)
1648+
assert graph.is_ev_charger(ev_charger)
1649+
assert graph.is_ev_charger_chain(ev_meter)
1650+
assert graph.is_ev_charger_chain(ev_charger)
1651+
1652+
assert graph.is_chp_meter(chp_meter)
1653+
assert graph.is_chp(chp)
1654+
assert graph.is_chp_chain(chp_meter)
1655+
assert graph.is_chp_chain(chp)

0 commit comments

Comments
 (0)