Skip to content

Commit 31aa32b

Browse files
Add depth first search for components (#595)
In this PR we add a dfs component search to the component graph. The search continues to go into depth until the condition is fulfilled. The condition has to be provided as a lambda. Furthermore we add function that returns the grid component such that it can be used in the dfs search, whenever the search should begin at the `GRID` component.
2 parents cac5473 + 61e993e commit 31aa32b

File tree

4 files changed

+187
-6
lines changed

4 files changed

+187
-6
lines changed

RELEASE_NOTES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
## New Features
1212

13-
<!-- Here goes the main new features and examples or instructions on how to use them -->
13+
- DFS for compentent graph
1414

1515
## Bug Fixes
1616

src/frequenz/sdk/microgrid/_graph.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,29 @@ def is_chp_chain(self, component: Component) -> bool:
266266
Whether the specified component is part of a CHP chain.
267267
"""
268268

269+
@abstractmethod
270+
def dfs(
271+
self,
272+
current_node: Component,
273+
visited: Set[Component],
274+
condition: Callable[[Component], bool],
275+
) -> Set[Component]:
276+
"""
277+
Search for components that fulfill the condition in the Graph.
278+
279+
DFS is used for searching the graph. The graph travarsal is stopped
280+
once a component fulfills the condition.
281+
282+
Args:
283+
current_node: The current node to search from.
284+
visited: The set of visited nodes.
285+
condition: The condition function to check for.
286+
287+
Returns:
288+
A set of component ids where the coresponding components fulfill
289+
the condition function.
290+
"""
291+
269292

270293
class _MicrogridComponentGraph(ComponentGraph):
271294
"""ComponentGraph implementation designed to work with the microgrid API.
@@ -688,6 +711,42 @@ def is_chp_chain(self, component: Component) -> bool:
688711
"""
689712
return self.is_chp(component) or self.is_chp_meter(component)
690713

714+
def dfs(
715+
self,
716+
current_node: Component,
717+
visited: Set[Component],
718+
condition: Callable[[Component], bool],
719+
) -> Set[Component]:
720+
"""
721+
Search for components that fulfill the condition in the Graph.
722+
723+
DFS is used for searching the graph. The graph travarsal is stopped
724+
once a component fulfills the condition.
725+
726+
Args:
727+
current_node: The current node to search from.
728+
visited: The set of visited nodes.
729+
condition: The condition function to check for.
730+
731+
Returns:
732+
A set of component ids where the coresponding components fulfill
733+
the condition function.
734+
"""
735+
if current_node in visited:
736+
return set()
737+
738+
visited.add(current_node)
739+
740+
if condition(current_node):
741+
return {current_node}
742+
743+
component: Set[Component] = set()
744+
745+
for successor in self.successors(current_node.component_id):
746+
component.update(self.dfs(successor, visited, condition))
747+
748+
return component
749+
691750
def _validate_graph(self) -> None:
692751
"""Check that the underlying graph data is valid.
693752

src/frequenz/sdk/timeseries/_formula_engine/_formula_generators/_formula_generator.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,15 +101,15 @@ def _get_builder(
101101
)
102102
return builder
103103

104-
def _get_grid_component_successors(self) -> set[component.Component]:
105-
"""Get the set of grid component successors in the component graph.
104+
def _get_grid_component(self) -> component.Component:
105+
"""
106+
Get the grid component in the component graph.
106107
107108
Returns:
108-
A set of grid component successors.
109+
The first grid component found in the graph.
109110
110111
Raises:
111112
ComponentNotFound: If the grid component is not found in the component graph.
112-
ComponentNotFound: If no successor components are found in the component graph.
113113
"""
114114
component_graph = connection_manager.get().component_graph
115115
grid_component = next(
@@ -120,10 +120,22 @@ def _get_grid_component_successors(self) -> set[component.Component]:
120120
),
121121
None,
122122
)
123-
124123
if grid_component is None:
125124
raise ComponentNotFound("Grid component not found in the component graph.")
126125

126+
return grid_component
127+
128+
def _get_grid_component_successors(self) -> set[component.Component]:
129+
"""Get the set of grid component successors in the component graph.
130+
131+
Returns:
132+
A set of grid component successors.
133+
134+
Raises:
135+
ComponentNotFound: If no successor components are found in the component graph.
136+
"""
137+
grid_component = self._get_grid_component()
138+
component_graph = connection_manager.get().component_graph
127139
grid_successors = component_graph.successors(grid_component.component_id)
128140

129141
if not grid_successors:

tests/microgrid/test_graph.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,116 @@ def test_connection_filters(self) -> None:
403403
Connection(2, 6),
404404
}
405405

406+
def test_dfs_search_two_grid_meters(self) -> None:
407+
"""Test DFS searching PV components in a graph with two grid meters."""
408+
grid = Component(1, ComponentCategory.GRID)
409+
pv_inverters = {
410+
Component(4, ComponentCategory.INVERTER, InverterType.SOLAR),
411+
Component(5, ComponentCategory.INVERTER, InverterType.SOLAR),
412+
}
413+
414+
graph = gr._MicrogridComponentGraph(
415+
components={
416+
grid,
417+
Component(2, ComponentCategory.METER),
418+
Component(3, ComponentCategory.METER),
419+
}.union(pv_inverters),
420+
connections={
421+
Connection(1, 2),
422+
Connection(1, 3),
423+
Connection(2, 4),
424+
Connection(2, 5),
425+
},
426+
)
427+
428+
result = graph.dfs(grid, set(), graph.is_pv_inverter)
429+
assert result == pv_inverters
430+
431+
def test_dfs_search_grid_meter(self) -> None:
432+
"""Test DFS searching PV components in a graph with a single grid meter."""
433+
grid = Component(1, ComponentCategory.GRID)
434+
pv_meters = {
435+
Component(3, ComponentCategory.METER),
436+
Component(4, ComponentCategory.METER),
437+
}
438+
439+
graph = gr._MicrogridComponentGraph(
440+
components={
441+
grid,
442+
Component(2, ComponentCategory.METER),
443+
Component(5, ComponentCategory.INVERTER, InverterType.SOLAR),
444+
Component(6, ComponentCategory.INVERTER, InverterType.SOLAR),
445+
}.union(pv_meters),
446+
connections={
447+
Connection(1, 2),
448+
Connection(2, 3),
449+
Connection(2, 4),
450+
Connection(3, 5),
451+
Connection(4, 6),
452+
},
453+
)
454+
455+
result = graph.dfs(grid, set(), graph.is_pv_chain)
456+
assert result == pv_meters
457+
458+
def test_dfs_search_no_grid_meter(self) -> None:
459+
"""Test DFS searching PV components in a graph with no grid meter."""
460+
grid = Component(1, ComponentCategory.GRID)
461+
pv_meters = {
462+
Component(3, ComponentCategory.METER),
463+
Component(4, ComponentCategory.METER),
464+
}
465+
466+
graph = gr._MicrogridComponentGraph(
467+
components={
468+
grid,
469+
Component(2, ComponentCategory.METER),
470+
Component(5, ComponentCategory.INVERTER, InverterType.SOLAR),
471+
Component(6, ComponentCategory.INVERTER, InverterType.SOLAR),
472+
}.union(pv_meters),
473+
connections={
474+
Connection(1, 2),
475+
Connection(1, 3),
476+
Connection(1, 4),
477+
Connection(3, 5),
478+
Connection(4, 6),
479+
},
480+
)
481+
482+
result = graph.dfs(grid, set(), graph.is_pv_chain)
483+
assert result == pv_meters
484+
485+
def test_dfs_search_nested_components(self) -> None:
486+
"""Test DFS searching PV components in a graph with nested components."""
487+
grid = Component(1, ComponentCategory.GRID)
488+
battery_components = {
489+
Component(4, ComponentCategory.METER),
490+
Component(5, ComponentCategory.METER),
491+
Component(6, ComponentCategory.INVERTER, InverterType.BATTERY),
492+
}
493+
494+
graph = gr._MicrogridComponentGraph(
495+
components={
496+
grid,
497+
Component(2, ComponentCategory.METER),
498+
Component(3, ComponentCategory.METER),
499+
Component(7, ComponentCategory.INVERTER, InverterType.BATTERY),
500+
Component(8, ComponentCategory.INVERTER, InverterType.BATTERY),
501+
}.union(battery_components),
502+
connections={
503+
Connection(1, 2),
504+
Connection(2, 3),
505+
Connection(2, 6),
506+
Connection(3, 4),
507+
Connection(3, 5),
508+
Connection(4, 7),
509+
Connection(5, 8),
510+
},
511+
)
512+
513+
assert set() == graph.dfs(grid, set(), graph.is_pv_chain)
514+
assert battery_components == graph.dfs(grid, set(), graph.is_battery_chain)
515+
406516

407517
class Test_MicrogridComponentGraph:
408518
"""Test cases for the package-internal implementation of the ComponentGraph.

0 commit comments

Comments
 (0)