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
12 changes: 1 addition & 11 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,11 @@
# Frequenz Python SDK Release Notes

## Summary

<!-- Here goes a general summary of what this release is about -->

## Upgrading

<!-- Here goes notes on how to upgrade from previous versions, including deprecations and what they should be replaced with -->

- The `FormulaEngine` is now replaced by a newly implemented `Formula` type. This doesn't affect the high level interfaces.
- The `FormulaEngine` is now replaced by a newly implemented `Formula` type. This doesn't affect the high level interfaces. `FormulaEngine` is now a deprecated wrapper to `Formula`.

- The `ComponentGraph` has been replaced by the `frequenz-microgrid-component-graph` package, which provides python bindings for the rust implementation.

## New Features

- The power manager algorithm for batteries can now be changed from the default ShiftingMatryoshka, by passing it as an argument to `microgrid.initialize()`

## Bug Fixes

<!-- Here goes notable bug fixes that are worth a special mention or explanation -->
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ dependencies = [
# Make sure to update the mkdocs.yml file when
# changing the version
# (plugins.mkdocstrings.handlers.python.import)
"frequenz-client-microgrid >= 0.18.0, < 0.19.0",
"frequenz-microgrid-component-graph >= 0.2.0, < 0.3",
"frequenz-client-microgrid >= 0.18.1, < 0.19.0",
"frequenz-microgrid-component-graph >= 0.3.2, < 0.4",
"frequenz-client-common >= 0.3.6, < 0.4.0",
"frequenz-channels >= 1.6.1, < 2.0.0",
"frequenz-quantities[marshmallow] >= 1.0.0, < 2.0.0",
Expand Down
175 changes: 175 additions & 0 deletions src/frequenz/sdk/microgrid/component_graph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# License: MIT
# Copyright © 2025 Frequenz Energy-as-a-Service GmbH

"""Component graph representation for a microgrid."""

from frequenz.client.common.microgrid.components import ComponentId
from frequenz.client.microgrid.component import (
BatteryInverter,
Chp,
Component,
ComponentConnection,
EvCharger,
SolarInverter,
)
from frequenz.microgrid_component_graph import ComponentGraph as BaseComponentGraph
from typing_extensions import override


class ComponentGraph(BaseComponentGraph[Component, ComponentConnection, ComponentId]):
"""A representation of a microgrid's component graph."""

def is_pv_inverter(self, component: Component) -> bool:
"""Check if the specified component is a PV inverter.

Args:
component: The component to check.

Returns:
Whether the specified component is a PV inverter.
"""
return isinstance(component, SolarInverter)

def is_pv_chain(self, component: Component) -> bool:
"""Check if the specified component is part of a PV chain.

A component is part of a PV chain if it is either a PV inverter or a PV
meter.

Args:
component: The component to check.

Returns:
Whether the specified component is part of a PV chain.
"""
return self.is_pv_inverter(component) or self.is_pv_meter(component)

@override
def is_pv_meter(self, component: Component | ComponentId) -> bool:
"""Check if the specified component is a PV meter.

Args:
component: The component or component ID to check.

Returns:
Whether the specified component is a PV meter.
"""
if isinstance(component, Component):
return super().is_pv_meter(component.id)
return super().is_pv_meter(component)

def is_ev_charger(self, component: Component) -> bool:
"""Check if the specified component is an EV charger.

Args:
component: The component to check.

Returns:
Whether the specified component is an EV charger.
"""
return isinstance(component, EvCharger)

def is_ev_charger_chain(self, component: Component) -> bool:
"""Check if the specified component is part of an EV charger chain.

A component is part of an EV charger chain if it is either an EV charger or an
EV charger meter.

Args:
component: The component to check.

Returns:
Whether the specified component is part of an EV charger chain.
"""
return self.is_ev_charger(component) or self.is_ev_charger_meter(component)

@override
def is_ev_charger_meter(self, component: Component | ComponentId) -> bool:
"""Check if the specified component is an EV charger meter.

Args:
component: The component or component ID to check.

Returns:
Whether the specified component is an EV charger meter.
"""
if isinstance(component, Component):
return super().is_ev_charger_meter(component.id)
return super().is_ev_charger_meter(component)

def is_battery_inverter(self, component: Component) -> bool:
"""Check if the specified component is a battery inverter.

Args:
component: The component to check.

Returns:
Whether the specified component is a battery inverter.
"""
return isinstance(component, BatteryInverter)

def is_battery_chain(self, component: Component) -> bool:
"""Check if the specified component is part of a battery chain.

A component is part of a battery chain if it is either a battery inverter or a
battery meter.

Args:
component: The component to check.

Returns:
Whether the specified component is part of a battery chain.
"""
return self.is_battery_inverter(component) or self.is_battery_meter(component)

@override
def is_battery_meter(self, component: Component | ComponentId) -> bool:
"""Check if the specified component is a battery meter.

Args:
component: The component or component ID to check.

Returns:
Whether the specified component is a battery meter.
"""
if isinstance(component, Component):
return super().is_battery_meter(component.id)
return super().is_battery_meter(component)

def is_chp(self, component: Component) -> bool:
"""Check if the specified component is a CHP.

Args:
component: The component to check.

Returns:
Whether the specified component is a CHP.
"""
return isinstance(component, Chp)

def is_chp_chain(self, component: Component) -> bool:
"""Check if the specified component is part of a CHP chain.

A component is part of a CHP chain if it is either a CHP or a CHP meter.

Args:
component: The component to check.

Returns:
Whether the specified component is part of a CHP chain.
"""
return self.is_chp(component) or self.is_chp_meter(component)

@override
def is_chp_meter(self, component: Component | ComponentId) -> bool:
"""Check if the specified component is a CHP meter.

Args:
component: The component or component ID to check.

Returns:
Whether the specified component is a CHP meter.
"""
if isinstance(component, Component):
return super().is_chp_meter(component.id)
return super().is_chp_meter(component)
15 changes: 5 additions & 10 deletions src/frequenz/sdk/microgrid/connection_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,13 @@
from abc import ABC, abstractmethod

from frequenz.client.common.microgrid import MicrogridId
from frequenz.client.common.microgrid.components import ComponentId
from frequenz.client.microgrid import (
Location,
MicrogridApiClient,
MicrogridInfo,
)
from frequenz.client.microgrid.component import Component, ComponentConnection
from frequenz.microgrid_component_graph import ComponentGraph

from .component_graph import ComponentGraph

_logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -59,7 +58,7 @@ def api_client(self) -> MicrogridApiClient:
@abstractmethod
def component_graph(
self,
) -> ComponentGraph[Component, ComponentConnection, ComponentId]:
) -> ComponentGraph:
"""Get component graph.

Returns:
Expand Down Expand Up @@ -109,9 +108,7 @@ def __init__(self, server_url: str) -> None:
self._client = MicrogridApiClient(server_url)
# To create graph from the API client we need await.
# So create empty graph here, and update it in `run` method.
self._graph: (
ComponentGraph[Component, ComponentConnection, ComponentId] | None
) = None
self._graph: ComponentGraph | None = None

self._microgrid: MicrogridInfo
"""The microgrid information."""
Expand Down Expand Up @@ -140,9 +137,7 @@ def location(self) -> Location | None:
return self._microgrid.location

@property
def component_graph(
self,
) -> ComponentGraph[Component, ComponentConnection, ComponentId]:
def component_graph(self) -> ComponentGraph:
"""Get component graph.

Returns:
Expand Down
38 changes: 38 additions & 0 deletions src/frequenz/sdk/timeseries/formula_engine/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# License: MIT
# Copyright © 2025 Frequenz Energy-as-a-Service GmbH

"""Deprecated Formula Engine."""


from typing_extensions import deprecated

from .._base_types import QuantityT
from ..formulas._formula import Formula
from ..formulas._formula_3_phase import Formula3Phase


@deprecated(
"The FormulaEngine class is deprecated and will be removed in a future release. "
+ "Please use the Formula class instead."
)
class FormulaEngine(Formula[QuantityT]):
"""Deprecated Formula Engine class.

This class is deprecated and will be removed in a future release.
Please use the `Formula` and `Formula3Phase` classes directly.
"""


@deprecated(
"The FormulaEngine3Phase class is deprecated and will be removed in a future release. "
+ "Please use the Formula3Phase class instead."
)
class FormulaEngine3Phase(Formula3Phase[QuantityT]):
"""Deprecated FormulaEngine3Phase class.

This class is deprecated and will be removed in a future release.
Please use the `Formula3Phase` class directly.
"""


__all__ = ["FormulaEngine", "FormulaEngine3Phase"]
27 changes: 13 additions & 14 deletions src/frequenz/sdk/timeseries/formulas/_formula.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
)

from ...actor import BackgroundService
from .. import ReceiverFetcher, Sample
from .. import Sample
from .._base_types import QuantityT
from . import _ast
from ._base_ast_node import AstNode
Expand All @@ -39,7 +39,7 @@ async def fetcher(f: Formula[QuantityT]) -> Receiver[Sample[QuantityT]]:
return lambda: fetcher(formula)


class Formula(BackgroundService, ReceiverFetcher[Sample[QuantityT]]):
class Formula(BackgroundService, Generic[QuantityT]):
"""A formula represented as an AST."""

def __init__( # pylint: disable=too-many-arguments
Expand Down Expand Up @@ -84,12 +84,11 @@ def __str__(self) -> str:
"""Return a string representation of the formula."""
return f"[{self._name}]({self._root})"

@override
def new_receiver(self, *, limit: int = 50) -> Receiver[Sample[QuantityT]]:
def new_receiver(self, *, max_size: int = 50) -> Receiver[Sample[QuantityT]]:
"""Subscribe to the formula evaluator to get evaluated samples."""
if not self._evaluator.is_running:
self.start()
return self._channel.new_receiver(limit=limit)
return self._channel.new_receiver(limit=max_size)

@override
def start(self) -> None:
Expand Down Expand Up @@ -128,24 +127,24 @@ def __truediv__(self, other: float) -> FormulaBuilder[QuantityT]:

def coalesce(
self,
other: list[FormulaBuilder[QuantityT] | QuantityT | Formula[QuantityT]],
*other: FormulaBuilder[QuantityT] | QuantityT | Formula[QuantityT],
) -> FormulaBuilder[QuantityT]:
"""Create a coalesce operation node."""
return FormulaBuilder(self, self._create_method).coalesce(other)
return FormulaBuilder(self, self._create_method).coalesce(*other)

def min(
self,
other: list[FormulaBuilder[QuantityT] | QuantityT | Formula[QuantityT]],
*other: FormulaBuilder[QuantityT] | QuantityT | Formula[QuantityT],
) -> FormulaBuilder[QuantityT]:
"""Create a min operation node."""
return FormulaBuilder(self, self._create_method).min(other)
return FormulaBuilder(self, self._create_method).min(*other)

def max(
self,
other: list[FormulaBuilder[QuantityT] | QuantityT | Formula[QuantityT]],
*other: FormulaBuilder[QuantityT] | QuantityT | Formula[QuantityT],
) -> FormulaBuilder[QuantityT]:
"""Create a max operation node."""
return FormulaBuilder(self, self._create_method).max(other)
return FormulaBuilder(self, self._create_method).max(*other)


class FormulaBuilder(Generic[QuantityT]):
Expand Down Expand Up @@ -266,7 +265,7 @@ def __truediv__(

def coalesce(
self,
other: list[FormulaBuilder[QuantityT] | QuantityT | Formula[QuantityT]],
*other: FormulaBuilder[QuantityT] | QuantityT | Formula[QuantityT],
) -> FormulaBuilder[QuantityT]:
"""Create a coalesce operation node."""
right_nodes: list[AstNode[QuantityT]] = []
Expand Down Expand Up @@ -299,7 +298,7 @@ def coalesce(

def min(
self,
other: list[FormulaBuilder[QuantityT] | QuantityT | Formula[QuantityT]],
*other: FormulaBuilder[QuantityT] | QuantityT | Formula[QuantityT],
) -> FormulaBuilder[QuantityT]:
"""Create a min operation node."""
right_nodes: list[AstNode[QuantityT]] = []
Expand Down Expand Up @@ -332,7 +331,7 @@ def min(

def max(
self,
other: list[FormulaBuilder[QuantityT] | QuantityT | Formula[QuantityT]],
*other: FormulaBuilder[QuantityT] | QuantityT | Formula[QuantityT],
) -> FormulaBuilder[QuantityT]:
"""Create a max operation node."""
right_nodes: list[AstNode[QuantityT]] = []
Expand Down
Loading