Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
2b9547c
Clear release notes
llucax Nov 24, 2025
08e6ed4
Bump dependency for frequenz-client-microgrid to v0.18.0
llucax Feb 17, 2025
49363a9
Remove test for grid connection without a fuse
llucax Feb 21, 2025
f2fd68f
Skip battery pool bounds tests
llucax Mar 14, 2025
aecd763
Add some missing `@override`
llucax Oct 30, 2025
0084ef5
Add old component data wrappers
llucax Oct 30, 2025
67a91be
Migrate metric identifiers from ComponentMetricId to Metric
llucax Oct 30, 2025
c382d37
Use new API v0.18 methods
llucax Oct 31, 2025
3126e23
Rename some "api" to "client" or "api_client"
llucax Oct 31, 2025
7bb0c1c
Update `.component_id` -> `.id`
llucax Oct 31, 2025
5b948e7
Use the new `MicrogridInfo` object
llucax Nov 3, 2025
f5d0f60
Replace uses of `ComponentCategory` with the new types
llucax Nov 3, 2025
2d69465
Improve component graph's filtering
llucax Nov 6, 2025
3b24560
Allow for unknown `ComponentCategory`s
llucax Nov 4, 2025
dd2136a
Update `Connection` to `ComponentConnection`
llucax Nov 7, 2025
a604f74
Allow passing single elements to component graph `connections()`
llucax Nov 7, 2025
9245356
Replace old `Component`s with specific classes in tests
llucax Nov 7, 2025
38d40c2
Use new component states and state snapshots
llucax Nov 7, 2025
1ee88ff
Update release notes
llucax Mar 22, 2025
5c23d96
Add AST nodes for the new tree walking Formula implementation
shsms Nov 20, 2025
fce8e8f
Introduce a `Peekable` wrapper around `Iterator`
shsms Nov 20, 2025
41687bd
Implement a lexer for the new component graph formulas
shsms Nov 20, 2025
6696a36
Implement a `ResampledStreamFetcher`
shsms Nov 20, 2025
058b2b6
Implement a `FormulaEvaluatingActor`
shsms Nov 20, 2025
1d37dab
Implement the `Formula` type
shsms Nov 20, 2025
fd7848c
Implement a parser for string formulas
shsms Nov 20, 2025
8a39aba
Add tests for formulas
shsms Nov 20, 2025
3e60eca
Add a 3-phase formula type that wraps 3 1-phase formulas
shsms Nov 20, 2025
61df097
Add a formula pool for storing and reusing formulas
shsms Nov 20, 2025
a25adf0
Add `frequenz-microgrid-component-graph` as a dependency
shsms Nov 20, 2025
0f9af10
Remove test for island-mode
shsms Nov 20, 2025
2d5c053
Switch to use the external component graph
shsms Nov 20, 2025
f2a513e
Delete the old component graph
shsms Nov 20, 2025
0a1320c
Replace FormulaEngine with the new Formula
shsms Nov 20, 2025
707b1cc
Remove tests for the old fallback mechanism
shsms Nov 20, 2025
1f8636e
Send test data from secondary components
shsms Nov 20, 2025
1fa2b7f
Test priority of component powers in formulas over meter powers
shsms Nov 20, 2025
f48230c
Drop old formula engine
shsms Nov 20, 2025
d23723b
Document the new Formula implementation
shsms Nov 20, 2025
09b029b
Remove all remaining references to FormulaEngines
shsms Nov 20, 2025
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
34 changes: 31 additions & 3 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,35 @@
# Frequenz Python SDK Release Notes

## Bug Fixes
## Summary

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

## Upgrading

- The SDK now depends on the `frequenz-client-microgrid` v0.18.x series.

* Check the release notes for the client [v0.18].
* There were a lot of changes, so it might be also worth having a quick look at the microgrid API [v0.17][api-v0.17] and [v0.18][api-v0.17] releases.
* Checking out the API common releases [v0.6][common-v0.6], [v0.7][common-v0.7], [v0.8][common-v0.8] might also be worthwhile, at least if you find any errors about renamed or missing types.
* Although many of the changes in lower layer are hidden by the SDK, there are some changes that can't be hidden away. For example the `Metric` enum had some renames, and `Component.component_id` was renamed to `Component.id`. Also, there is a new component class hierarchy.

- `ComponentGraph` methods arguments were renamed to better reflect what they expect.

- The log level for when components are transitioning to a `WORKING` state is lowered to `INFO`, and the log message has been improved.
* The `components()` method now uses `matching_ids` and `matching_types` instead of `component_ids` and `component_categories` respectively. `matching_types` takes types inheriting from `Component` instead of categories, for example `Battery` or `BatteryInverter`.
* The `connections()` methods now take `matching_sources` and `matching_destinations` instead of `start` and `end` respectively. This is to match the new names in `ComponentConnection`.
* All arguments for both methods can now receiver arbitrary iterables instead of `set`s, and can also accept a single value.

[v0.18]: https://github.com/frequenz-floss/frequenz-client-microgrid-python/releases/tag/v0.18.0
[api-v0.17]: https://github.com/frequenz-floss/frequenz-api-microgrid/releases/tag/v0.17.0
[api-v0.18]: https://github.com/frequenz-floss/frequenz-api-microgrid/releases/tag/v0.18.0
[common-v0.6]: https://github.com/frequenz-floss/frequenz-api-common/releases/tag/v0.6.0
[common-v0.7]: https://github.com/frequenz-floss/frequenz-api-common/releases/tag/v0.7.0
[common-v0.8]: https://github.com/frequenz-floss/frequenz-api-common/releases/tag/v0.8.0

## New Features

<!-- Here goes the main new features and examples or instructions on how to use them -->

## Bug Fixes

- This fixes a bug in the power manager, that was causing proposals to be ignored when they were proposing bounds that were fully outside the available bounds, under some cases.
<!-- Here goes notable bug fixes that are worth a special mention or explanation -->
11 changes: 5 additions & 6 deletions benchmarks/power_distribution/power_distributor.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

from frequenz.channels import Broadcast
from frequenz.client.common.microgrid.components import ComponentId
from frequenz.client.microgrid import Component, ComponentCategory
from frequenz.client.microgrid.component import Battery
from frequenz.quantities import Power

from frequenz.sdk import microgrid
Expand Down Expand Up @@ -116,8 +116,7 @@ async def run_test( # pylint: disable=too-many-locals
battery_status_channel = Broadcast[ComponentPoolStatus](name="battery-status")
power_result_channel = Broadcast[Result](name="power-result")
async with PowerDistributingActor(
component_category=ComponentCategory.BATTERY,
component_type=None,
component_type=Battery,
requests_receiver=power_request_channel.new_receiver(),
results_sender=power_result_channel.new_sender(),
component_pool_status_sender=battery_status_channel.new_sender(),
Expand All @@ -143,10 +142,10 @@ async def run() -> None:
ResamplerConfig2(resampling_period=timedelta(seconds=1.0)),
)

all_batteries: set[Component] = connection_manager.get().component_graph.components(
component_categories={ComponentCategory.BATTERY}
all_batteries = connection_manager.get().component_graph.components(
matching_types=Battery
)
batteries_ids = {c.component_id for c in all_batteries}
batteries_ids = {c.id for c in all_batteries}
# Take some time to get data from components
await asyncio.sleep(4)
with open("/dev/stdout", "w", encoding="utf-8") as csvfile:
Expand Down
8 changes: 4 additions & 4 deletions benchmarks/timeseries/benchmark_datasourcing.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from typing import Any

from frequenz.channels import Broadcast, Receiver, ReceiverStoppedError
from frequenz.client.microgrid import ComponentMetricId
from frequenz.client.microgrid.metrics import Metric

from frequenz.sdk import microgrid
from frequenz.sdk._internal._channels import ChannelRegistry
Expand All @@ -37,9 +37,9 @@
sys.exit(1)

COMPONENT_METRIC_IDS = [
ComponentMetricId.CURRENT_PHASE_1,
ComponentMetricId.CURRENT_PHASE_2,
ComponentMetricId.CURRENT_PHASE_3,
Metric.AC_CURRENT_PHASE_1,
Metric.AC_CURRENT_PHASE_2,
Metric.AC_CURRENT_PHASE_3,
]


Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Formula Engine
# Formulas

::: frequenz.sdk.timeseries.formula_engine
::: frequenz.sdk.timeseries.formulas
options:
members: None
show_bases: false
Expand Down
2 changes: 1 addition & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ plugins:
- https://docs.python.org/3/objects.inv
- https://frequenz-floss.github.io/frequenz-channels-python/v1/objects.inv
- https://frequenz-floss.github.io/frequenz-client-common-python/v0.3/objects.inv
- https://frequenz-floss.github.io/frequenz-client-microgrid-python/v0.9/objects.inv
- https://frequenz-floss.github.io/frequenz-client-microgrid-python/v0.18/objects.inv
- https://frequenz-floss.github.io/frequenz-core-python/v1/objects.inv
- https://frequenz-floss.github.io/frequenz-quantities-python/v1/objects.inv
- https://lovasoa.github.io/marshmallow_dataclass/html/objects.inv
Expand Down
16 changes: 5 additions & 11 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ dependencies = [
# Make sure to update the mkdocs.yml file when
# changing the version
# (plugins.mkdocstrings.handlers.python.import)
"frequenz-client-microgrid >= 0.9.0, < 0.10.0",
"frequenz-client-common >= 0.3.2, < 0.4.0",
"frequenz-client-microgrid >= 0.18.0, < 0.19.0",
"frequenz-microgrid-component-graph >= 0.2.0, < 0.3",
"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",
"networkx >= 2.8, < 4",
"numpy >= 2.1.0, < 3",
"typing_extensions >= 4.13.0, < 5",
"typing_extensions >= 4.14.1, < 5",
"marshmallow >= 3.19.0, < 5",
"marshmallow_dataclass >= 8.7.1, < 9",
]
Expand Down Expand Up @@ -204,13 +204,7 @@ files = ["src", "tests", "examples", "benchmarks", "docs", "noxfile.py"]
strict = true

[[tool.mypy.overrides]]
module = [
"async_solipsism",
"mkdocs_macros.*",
# The available stubs packages are outdated or incomplete (WIP/experimental):
# https://github.com/frequenz-floss/frequenz-sdk-python/issues/430
"networkx",
]
module = ["async_solipsism", "mkdocs_macros.*"]
ignore_missing_imports = true

[tool.setuptools_scm]
Expand Down
241 changes: 241 additions & 0 deletions src/frequenz/sdk/_internal/_graph_traversal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
# License: MIT
# Copyright © 2025 Frequenz Energy-as-a-Service GmbH

"""Graph traversal helpers."""

from __future__ import annotations

from collections.abc import Iterable
from typing import Callable

from frequenz.client.common.microgrid.components import ComponentId
from frequenz.client.microgrid.component import (
BatteryInverter,
Chp,
Component,
ComponentConnection,
EvCharger,
GridConnectionPoint,
SolarInverter,
)
from frequenz.microgrid_component_graph import ComponentGraph, InvalidGraphError


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

Args:
component: The component to check.

Returns:
`True` if the component is a PV inverter, `False` otherwise.
"""
return isinstance(component, SolarInverter)


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

Args:
component: The component to check.

Returns:
`True` if the component is a battery inverter, `False` otherwise.
"""
return isinstance(component, BatteryInverter)


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

Args:
component: The component to check.

Returns:
`True` if the component is a CHP, `False` otherwise.
"""
return isinstance(component, Chp)


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

Args:
component: The component to check.

Returns:
`True` if the component is an EV charger, `False` otherwise.
"""
return isinstance(component, EvCharger)


def is_battery_chain(
graph: ComponentGraph[Component, ComponentConnection, ComponentId],
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:
graph: The component graph.
component: component to check.

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


def is_pv_chain(
graph: ComponentGraph[Component, ComponentConnection, ComponentId],
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:
graph: The component graph.
component: component to check.

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


def is_ev_charger_chain(
graph: ComponentGraph[Component, ComponentConnection, ComponentId],
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:
graph: The component graph.
component: component to check.

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


def is_chp_chain(
graph: ComponentGraph[Component, ComponentConnection, ComponentId],
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:
graph: The component graph.
component: component to check.

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


def dfs(
graph: ComponentGraph[Component, ComponentConnection, ComponentId],
current_node: Component,
visited: set[Component],
condition: Callable[[Component], bool],
) -> set[Component]:
"""
Search for components that fulfill the condition in the Graph.

DFS is used for searching the graph. The graph traversal is stopped
once a component fulfills the condition.

Args:
graph: The component graph.
current_node: The current node to search from.
visited: The set of visited nodes.
condition: The condition function to check for.

Returns:
A set of component ids where the corresponding components fulfill
the condition function.
"""
if current_node in visited:
return set()

visited.add(current_node)

if condition(current_node):
return {current_node}

component: set[Component] = set()

for successor in graph.successors(current_node.id):
component.update(dfs(graph, successor, visited, condition))

return component


def find_first_descendant_component(
graph: ComponentGraph[Component, ComponentConnection, ComponentId],
*,
descendants: Iterable[type[Component]],
) -> Component:
"""Find the first descendant component given root and descendant categories.

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
of the descendant categories, with the first category having the
highest priority.

Args:
graph: The component graph to search.
descendants: The descendant classes to search for the first
descendant component in.

Returns:
The first descendant component found in the component graph,
considering the specified `descendants` categories.

Raises:
InvalidGraphError: When no GRID component is found in the graph.
ValueError: When no component is found in the given categories.
"""
# We always sort by component ID to ensure consistent results

def sorted_by_id(components: Iterable[Component]) -> Iterable[Component]:
return sorted(components, key=lambda c: c.id)

root_component = next(
iter(sorted_by_id(graph.components(matching_types={GridConnectionPoint}))),
None,
)
if root_component is None:
raise InvalidGraphError(
"No GridConnectionPoint component found in the component graph!"
)

successors = sorted_by_id(graph.successors(root_component.id))

def find_component(component_class: type[Component]) -> Component | None:
return next(
(comp for comp in successors if isinstance(comp, component_class)),
None,
)

# Find the first component that matches the given descendant categories
# in the order of the categories list.
component = next(filter(None, map(find_component, descendants)), None)

if component is None:
raise ValueError("Component not found in any of the descendant categories.")

return component
2 changes: 1 addition & 1 deletion src/frequenz/sdk/microgrid/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@
## Streaming component data

All pools have a `power` property, which is a
[`FormulaEngine`][frequenz.sdk.timeseries.formula_engine.FormulaEngine] that can
[`Formula`][frequenz.sdk.timeseries.formulas.Formula] that can

- provide a stream of resampled power values, which correspond to the sum of the
power measured from all the components in the pool together.
Expand Down
Loading