diff --git a/Cargo.lock b/Cargo.lock index 77c5fad..0822a17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -22,9 +22,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "frequenz-microgrid-component-graph" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1995604d1b936b8d3c626e26f883b926ee4ade301703cabd2254a513dc6e42a5" +checksum = "96b3e6a856da39ea69dcfa516398e87c1e1b655f33df0530fefd35dcb7ab724f" dependencies = [ "petgraph", "tracing", @@ -32,7 +32,7 @@ dependencies = [ [[package]] name = "frequenz-microgrid-component-graph-python-bindings" -version = "0.2.0" +version = "0.3.0" dependencies = [ "frequenz-microgrid-component-graph", "pyo3", diff --git a/Cargo.toml b/Cargo.toml index 10890ab..edfc238 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "frequenz-microgrid-component-graph-python-bindings" -version = "0.2.0" +version = "0.3.0" edition = "2024" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -10,4 +10,4 @@ crate-type = ["cdylib"] [dependencies] pyo3 = "0.27.1" -frequenz-microgrid-component-graph = "0.2.0" +frequenz-microgrid-component-graph = "0.3.0" diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 46df7f8..f8d9f9e 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -3,3 +3,4 @@ ## New Features - Grid formulas now use single successor meters as fallback components for meters attached to the grid. +- Adds wind turbine bindings \ No newline at end of file diff --git a/python/frequenz/microgrid_component_graph/__init__.pyi b/python/frequenz/microgrid_component_graph/__init__.pyi index ac2d05c..6d77221 100644 --- a/python/frequenz/microgrid_component_graph/__init__.pyi +++ b/python/frequenz/microgrid_component_graph/__init__.pyi @@ -292,6 +292,22 @@ class ComponentGraph(Generic[ComponentT, ConnectionT, ComponentIdT]): are not EV chargers. """ + def wind_turbine_formula(self, wind_turbine_ids: Set[ComponentIdT] | None) -> str: + """Generate the wind turbine formula for this component graph. + + Args: + wind_turbine_ids: The set of wind turbine component IDs to include in + the formula. If `None`, all wind turbines in the graph will be + included. + + Returns: + The wind turbine formula as a string. + + Raises: + FormulaGenerationError: if the given component IDs don't exist or + are not wind turbines. + """ + def grid_coalesce_formula(self) -> str: """Generate the grid coalesce formula for this component graph. diff --git a/src/category.rs b/src/category.rs index 68827f3..cef7de1 100644 --- a/src/category.rs +++ b/src/category.rs @@ -10,6 +10,7 @@ struct ComponentClasses<'py> { battery: Bound<'py, PyAny>, ev_charger: Bound<'py, PyAny>, chp: Bound<'py, PyAny>, + wind_turbine: Bound<'py, PyAny>, battery_inverter: Bound<'py, PyAny>, solar_inverter: Bound<'py, PyAny>, hybrid_inverter: Bound<'py, PyAny>, @@ -33,6 +34,7 @@ impl<'py> ComponentClasses<'py> { battery: module.getattr("Battery")?, ev_charger: module.getattr("EvCharger")?, chp: module.getattr("Chp")?, + wind_turbine: module.getattr("WindTurbine")?, battery_inverter: module.getattr("BatteryInverter")?, solar_inverter: module.getattr("SolarInverter")?, hybrid_inverter: module.getattr("HybridInverter")?, @@ -82,6 +84,10 @@ pub(crate) fn category_from_python_component( || object.is(&comp_classes.hybrid_inverter) { Ok(cg::ComponentCategory::Inverter(cg::InverterType::Hybrid)) + } else if object.is_instance(&comp_classes.wind_turbine)? + || object.is(&comp_classes.wind_turbine) + { + Ok(cg::ComponentCategory::WindTurbine) } else if object.is_instance(&comp_classes.unspecified_component)? || object.is(&comp_classes.unspecified_component) { diff --git a/src/graph.rs b/src/graph.rs index 2c074cf..488d10b 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -278,6 +278,18 @@ impl ComponentGraph { .map_err(|e| PyErr::new::(e.to_string())) } + #[pyo3(signature = (wind_turbine_ids=None))] + fn wind_turbine_formula( + &self, + py: Python<'_>, + wind_turbine_ids: Option>, + ) -> PyResult { + self.graph + .wind_turbine_formula(extract_ids(py, wind_turbine_ids)?) + .map(|f| f.to_string()) + .map_err(|e| PyErr::new::(e.to_string())) + } + fn grid_coalesce_formula(&self) -> PyResult { self.graph .grid_coalesce_formula() diff --git a/tests/test_microgrid_component_graph.py b/tests/test_microgrid_component_graph.py index 4815bbf..86f8c92 100644 --- a/tests/test_microgrid_component_graph.py +++ b/tests/test_microgrid_component_graph.py @@ -11,6 +11,7 @@ GridConnectionPoint, Meter, SolarInverter, + WindTurbine, ) from frequenz import microgrid_component_graph @@ -77,3 +78,49 @@ def test_graph_creation() -> None: Meter(id=ComponentId(3), microgrid_id=MicrogridId(1)), SolarInverter(id=ComponentId(4), microgrid_id=MicrogridId(1)), } + + +def test_wind_turbine_graph() -> None: + """Test graph creation and formula generation for Wind Turbines.""" + graph: microgrid_component_graph.ComponentGraph[ + Component, ComponentConnection, ComponentId + ] = microgrid_component_graph.ComponentGraph( + components={ + GridConnectionPoint( + id=ComponentId(1), + microgrid_id=MicrogridId(1), + rated_fuse_current=100, + ), + Meter(id=ComponentId(2), microgrid_id=MicrogridId(1)), + WindTurbine(id=ComponentId(3), microgrid_id=MicrogridId(1)), + }, + connections={ + # Grid -> Meter -> Wind Turbine + ComponentConnection(source=ComponentId(1), destination=ComponentId(2)), + ComponentConnection(source=ComponentId(2), destination=ComponentId(3)), + }, + ) + + # 1. Test Component Retrieval + assert graph.components(matching_types=WindTurbine) == { + WindTurbine(id=ComponentId(3), microgrid_id=MicrogridId(1)) + } + + # 2. Test Combined Retrieval (Meter + Wind) + assert graph.components(matching_types=[Meter, WindTurbine]) == { + Meter(id=ComponentId(2), microgrid_id=MicrogridId(1)), + WindTurbine(id=ComponentId(3), microgrid_id=MicrogridId(1)), + } + + # 3. Test Formula Generation + # References the Meter (ID 2) measuring the Turbine (ID 3). + assert ( + graph.wind_turbine_formula(wind_turbine_ids={ComponentId(3)}) + == "COALESCE(#3, #2, 0.0)" + ) + + # 4. Test Topology (Successors/Predecessors) + # The predecessor of the Wind Turbine (3) should be the Meter (2) + assert graph.predecessors(ComponentId(3)) == { + Meter(id=ComponentId(2), microgrid_id=MicrogridId(1)) + }