Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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"
1 change: 1 addition & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
16 changes: 16 additions & 0 deletions python/frequenz/microgrid_component_graph/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
6 changes: 6 additions & 0 deletions src/category.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
Expand All @@ -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")?,
Expand Down Expand Up @@ -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)
{
Expand Down
12 changes: 12 additions & 0 deletions src/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,18 @@ impl ComponentGraph {
.map_err(|e| PyErr::new::<FormulaGenerationError, _>(e.to_string()))
}

#[pyo3(signature = (wind_turbine_ids=None))]
fn wind_turbine_formula(
&self,
py: Python<'_>,
wind_turbine_ids: Option<Bound<'_, PyAny>>,
) -> PyResult<String> {
self.graph
.wind_turbine_formula(extract_ids(py, wind_turbine_ids)?)
.map(|f| f.to_string())
.map_err(|e| PyErr::new::<FormulaGenerationError, _>(e.to_string()))
}

fn grid_coalesce_formula(&self) -> PyResult<String> {
self.graph
.grid_coalesce_formula()
Expand Down
47 changes: 47 additions & 0 deletions tests/test_microgrid_component_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
GridConnectionPoint,
Meter,
SolarInverter,
WindTurbine,
)

from frequenz import microgrid_component_graph
Expand Down Expand Up @@ -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))
}