diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 0ed3b86e0..b4e9d02b9 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -55,3 +55,5 @@ | EV Chargers | Maximum power (aka max consumption power) | - 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. + +- 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. diff --git a/src/frequenz/sdk/microgrid/component_graph.py b/src/frequenz/sdk/microgrid/component_graph.py index 1b0be619e..51bb8c023 100644 --- a/src/frequenz/sdk/microgrid/component_graph.py +++ b/src/frequenz/sdk/microgrid/component_graph.py @@ -311,16 +311,11 @@ def dfs( def find_first_descendant_component( self, *, - root_category: ComponentCategory, descendant_categories: Iterable[ComponentCategory], ) -> Component: """Find the first descendant component given root and descendant categories. - This method searches for the root component within the provided root - category. If multiple components share the same root category, the - first found one is considered as the root component. - - Subsequently, it looks for the first descendant component from the root + This method looks for the first descendant component from the GRID component, considering only the immediate descendants. The priority of the component to search for is determined by the order @@ -328,13 +323,12 @@ def find_first_descendant_component( highest priority. Args: - root_category: The class of the root component to search for. descendant_categories: The descendant classes to search for the first descendant component in. Returns: The first descendant component found in the component graph, - considering the specified `root` and `descendants` categories. + considering the specified `descendants` categories. """ @@ -824,16 +818,11 @@ def dfs( def find_first_descendant_component( self, *, - root_category: ComponentCategory, descendant_categories: Iterable[ComponentCategory], ) -> Component: """Find the first descendant component given root and descendant categories. - This method searches for the root component within the provided root - category. If multiple components share the same root category, the - first found one is considered as the root component. - - Subsequently, it looks for the first descendant component from the root + This method looks for the first descendant component from the GRID component, considering only the immediate descendants. The priority of the component to search for is determined by the order @@ -841,25 +830,28 @@ def find_first_descendant_component( highest priority. Args: - root_category: The class of the root component to search for. descendant_categories: The descendant classes to search for the first descendant component in. Returns: The first descendant component found in the component graph, - considering the specified `root` and `descendants` categories. + considering the specified `descendants` categories. Raises: - ValueError: When the root component is not found in the component - graph or when no component is found in the given categories. + InvalidGraphError: When no GRID component is found in the graph. + ValueError: When no component is found in the given categories. """ root_component = next( - (comp for comp in self.components(component_categories={root_category})), + ( + comp + for comp in self.components( + component_categories={ComponentCategory.GRID} + ) + ), None, ) - if root_component is None: - raise ValueError(f"Root component not found for {root_category.name}") + raise InvalidGraphError("No GRID component found in the component graph!") # Sort by component ID to ensure consistent results. successors = sorted( diff --git a/src/frequenz/sdk/timeseries/_grid_frequency.py b/src/frequenz/sdk/timeseries/_grid_frequency.py index e645f4f69..54ea1a96b 100644 --- a/src/frequenz/sdk/timeseries/_grid_frequency.py +++ b/src/frequenz/sdk/timeseries/_grid_frequency.py @@ -53,7 +53,6 @@ def __init__( if not source: component_graph = connection_manager.get().component_graph source = component_graph.find_first_descendant_component( - root_category=ComponentCategory.GRID, descendant_categories=( ComponentCategory.METER, ComponentCategory.INVERTER, diff --git a/src/frequenz/sdk/timeseries/_voltage_streamer.py b/src/frequenz/sdk/timeseries/_voltage_streamer.py index 193988401..bf8b489f2 100644 --- a/src/frequenz/sdk/timeseries/_voltage_streamer.py +++ b/src/frequenz/sdk/timeseries/_voltage_streamer.py @@ -81,7 +81,6 @@ def __init__( if not source_component: component_graph = connection_manager.get().component_graph source_component = component_graph.find_first_descendant_component( - root_category=ComponentCategory.GRID, descendant_categories=[ ComponentCategory.METER, ComponentCategory.INVERTER, diff --git a/tests/microgrid/test_graph.py b/tests/microgrid/test_graph.py index cd522a788..fe865851c 100644 --- a/tests/microgrid/test_graph.py +++ b/tests/microgrid/test_graph.py @@ -590,45 +590,21 @@ def test_find_first_descendant_component(self) -> None: # Find the first descendant component of the grid endpoint. result = graph.find_first_descendant_component( - root_category=ComponentCategory.GRID, descendant_categories=(ComponentCategory.METER,), ) assert result == Component(2, ComponentCategory.METER) - # Find the first descendant component of the first meter found. - result = graph.find_first_descendant_component( - root_category=ComponentCategory.METER, - descendant_categories=(ComponentCategory.INVERTER,), - ) - assert result == Component(4, ComponentCategory.INVERTER, InverterType.BATTERY) - # Find the first descendant component of the grid, # considering meter or inverter categories. result = graph.find_first_descendant_component( - root_category=ComponentCategory.GRID, descendant_categories=(ComponentCategory.METER, ComponentCategory.INVERTER), ) assert result == Component(2, ComponentCategory.METER) - # Find the first descendant component of the first meter with nested meters. - result = graph.find_first_descendant_component( - root_category=ComponentCategory.METER, - descendant_categories=(ComponentCategory.METER,), - ) - assert result == Component(3, ComponentCategory.METER) - - # Verify behavior when root component is not found. - with pytest.raises(ValueError): - graph.find_first_descendant_component( - root_category=ComponentCategory.CHP, - descendant_categories=(ComponentCategory.INVERTER,), - ) - # Verify behavior when component is not found in immediate descendant # categories for the first meter. with pytest.raises(ValueError): graph.find_first_descendant_component( - root_category=ComponentCategory.METER, descendant_categories=( ComponentCategory.EV_CHARGER, ComponentCategory.BATTERY, @@ -639,7 +615,6 @@ def test_find_first_descendant_component(self) -> None: # categories from the grid component as root. with pytest.raises(ValueError): graph.find_first_descendant_component( - root_category=ComponentCategory.GRID, descendant_categories=(ComponentCategory.INVERTER,), )