Skip to content

Conversation

@shsms
Copy link
Contributor

@shsms shsms commented Jan 15, 2025

This PR removes the set_operating_point feature and replaces the power manager's original Matryoshka algorithm with the new ShiftingMatryoshka algorithm.

With the new algorithm, power proposals from actors are added to get the target power for the components, and higher-priority actors can limit the bounds available to lower-priority actors.

@shsms shsms requested a review from a team as a code owner January 15, 2025 14:58
@shsms shsms requested review from daniel-zullo-frequenz and removed request for a team January 15, 2025 14:58
@github-actions github-actions bot added part:docs Affects the documentation part:tests Affects the unit, integration and performance (benchmarks) tests part:data-pipeline Affects the data pipeline part:microgrid Affects the interactions with the microgrid labels Jan 15, 2025
@shsms shsms force-pushed the power-manager-shift-limit branch from 649caf6 to 3c38ef5 Compare January 15, 2025 14:59
@shsms shsms requested a review from llucax January 15, 2025 15:08
@shsms shsms linked an issue Jan 15, 2025 that may be closed by this pull request
@shsms shsms self-assigned this Jan 15, 2025
Copy link
Contributor

@llucax llucax left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just gave this a quick look, only to the structural changes.

I wonder why removing the old algorithm if we can support more than one. I can understand if it is to remove the maintenance cost, but I would delay the removal until we collected more experience.

@shsms shsms force-pushed the power-manager-shift-limit branch from 3c38ef5 to 7f405bb Compare January 20, 2025 13:00
@shsms
Copy link
Contributor Author

shsms commented Jan 20, 2025

I wonder why removing the old algorithm if we can support more than one. I can understand if it is to remove the maintenance cost, but I would delay the removal until we collected more experience.

This was because pylint was complaining about duplicate lines, plus it is a separate commit, which should be easy to revert if necessary. If you prefer, I can drop that commit, but we'll need to find a way to silence pylint either by refactoring the tests or pylint config.

But if we remove the current PR's limiting-only option like I mentioned in some comments above, this would stop being an issue as well, so we can revisit this after we decide that.

@llucax
Copy link
Contributor

llucax commented Jan 22, 2025

This was because pylint was complaining about duplicate lines, plus it is a separate commit, which should be easy to revert if necessary. If you prefer, I can drop that commit, but we'll need to find a way to silence pylint either by refactoring the tests or pylint config.

We should definitely not remove code because pylint complains about duplication :D

Can't we just add a # pylint: disable=<duplication> at the beginning of the file(s) reporting duplication?

@shsms shsms force-pushed the power-manager-shift-limit branch from 7f405bb to 509c528 Compare January 28, 2025 10:39
@shsms shsms force-pushed the power-manager-shift-limit branch 6 times, most recently from eff0cf3 to 6e13739 Compare March 20, 2025 16:49
shsms added 4 commits March 21, 2025 10:09
…requenz-floss#970)"

This reverts commit 6a54a0b, reversing
changes made to 4c71140.

Signed-off-by: Sahas Subramanian <[email protected]>
…er (frequenz-floss#957)"

This reverts commit d5d74a3, reversing
changes made to 8e5d65e.

Signed-off-by: Sahas Subramanian <[email protected]>
This makes the function easier to distinguish from per-priority bounds
produced by the power manager.

Signed-off-by: Sahas Subramanian <[email protected]>
The shifting logic will be implemented in the next commit.

Signed-off-by: Sahas Subramanian <[email protected]>
@shsms shsms force-pushed the power-manager-shift-limit branch from 6e13739 to 4b858b9 Compare March 21, 2025 09:10
@llucax llucax requested a review from Copilot March 24, 2025 08:22
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR replaces the original Matryoshka algorithm with the new ShiftingMatryoshka algorithm in the power manager while removing the “set_operating_point” parameter from pool APIs and associated documentation.

  • Introduces a new algorithm implementation in _shifting_matryoshka.py that aggregates proposals using shifted bounds.
  • Updates documentation, tests, and pool creation methods to reflect the removal of the set_operating_point parameter.
  • Refactors the power managing actor to use a unified subscriptions structure and maps the algorithm selection accordingly.

Reviewed Changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/frequenz/sdk/microgrid/_power_managing/_shifting_matryoshka.py New algorithm implementation and handling of bounds via power shifting.
src/frequenz/sdk/microgrid/init.py Updated documentation to describe the new handling of power proposals.
src/frequenz/sdk/timeseries/* Removed obsolete set_operating_point parameter and cleaned up related docstrings.
tests/* Updated tests to match changes in target power calculation and bounds shifting.
RELEASE_NOTES.md Updated to document removal of the set_operating_point parameter and details of the new algorithm.
src/frequenz/sdk/microgrid/_power_managing/_power_managing_actor.py Refactored to use the new algorithm selection and unified subscriptions mapping.
src/frequenz/sdk/microgrid/_data_pipeline.py Removed extra set_operating_point parameter from pool creation methods.
Comments suppressed due to low confidence (2)

src/frequenz/sdk/microgrid/_power_managing/_shifting_matryoshka.py:147

  • [nitpick] Verify that subtracting the proposal power from the lower bound produces the intended result in all edge cases. Consider adding an inline comment to explain the rationale behind shifting the bounds in this manner.
lower_bound = lower_bound - proposal_power

src/frequenz/sdk/microgrid/_power_managing/_power_managing_actor.py:79

  • [nitpick] Consider renaming '_subscriptions' to a more descriptive name (e.g. '_report_subscriptions') to clarify that these senders are used for reporting status.
self._subscriptions: dict[frozenset[int], dict[int, Sender[_Report]]] = {}

@llucax llucax self-requested a review March 24, 2025 08:25
Copy link
Contributor

@llucax llucax left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't do a super in-depth review, in particular I didn't try to validate or fully understand the algorithm code, but I did follow the examples/new tests to understand the algorithm and it looks good to me and it was easier to follow (not sure if because I'm now more familiar with the algorithm or because the new wording focused on shifting :D).

Just a few minor comments, otherwise LGTM.

One final question though. The old matryoshka algorithm still works as before the operating point setting/group/shifting was added, right? The removal of the set_operating_point option only affects that?

Comment on lines 4 to 17
"""A power manager implementation that uses the matryoshka algorithm.
When there are multiple proposals from different actors for the same set of components,
the matryoshka algorithm will consider the priority of the actors, the bounds they set
and their preferred power to determine the target power for the components.
The preferred power of lower priority actors will take precedence as long as they
respect the bounds set by higher priority actors. If lower priority actors request
power values outside the bounds set by higher priority actors, the target power will
be the closest value to the preferred power that is within the bounds.
When there is only a single proposal for a set of components, its preferred power would
be the target power, as long as it falls within the system power bounds for the
components.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would move this docs to the class, as the module is private, it will never be rendered in the documentation website otherwise (unless you are explicitly including it in docs/.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh wow, I forgot to update these docs. These are for the old matryoshka algorithm. I will do that and move it to the class.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hehe, right, I was reviewing commit by commit and commented on this when I saw you added the file but then didn't realized you didn't update it afterwards.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

proposals: set[Proposal],
system_bounds: SystemBounds,
priority: int | None = None,
) -> tuple[Power | None, Bounds[Power]]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm shaking while about bounds due to PTSD after fighting with bounds updates for the v0.17 microgrid API. I hope it is not too hard to merge 😱

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it should be fine because you're handling it in the compatibility layer from the client right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Half and half, what was hard was how to cope with bounds being None/NaN/0, as sometimes 0 means None/NaN because we were blindly reading the protobuf value, and the default in protobuf (if the value is missing) is 0 for int. So these semantic differences I think are not handled centrally in the compatibility layer, but I'm not sure right now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should expect that the API's bounds are always correct. If we get None/NaN/0 from the API, then those should be treated as 0.

Comment on lines 116 to 134
proposal_lower = next_proposal.bounds.lower or lower_bound
proposal_upper = next_proposal.bounds.upper or upper_bound
proposal_power = next_proposal.preferred_power

if proposal_upper < proposal_lower:
continue

if proposal_power and (
proposal_power < proposal_lower or proposal_power > proposal_upper
):
continue

if proposal_lower >= upper_bound:
proposal_lower = upper_bound
proposal_upper = upper_bound
elif proposal_upper <= lower_bound:
proposal_lower = lower_bound
proposal_upper = lower_bound

lower_bound = max(lower_bound, proposal_lower)
upper_bound = min(upper_bound, proposal_upper)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are all these checks for proposal_power, proposal_lower and proposal_upper intentionally covering for an eventual 0.0 besides None or was the intention to only match None? Since in the code below, you are actually checking for None explicitly, I guess it is intentional, but maybe it would be best to add some comment to say it explicitly (either way).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

intentionally covering for an eventual 0.0 besides None or was the intention to only match None

I don't understand what that means. Of course, I can add comments for these, though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added more comments, also fixed a bug.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was talking specifically about using a nullable int as a boolean implicitly, the next_proposal.bounds.lower or lower_bound, f proposal_power and ..., etc. In these cases both 0 and None will be considered False, so the question was if this was considered. Sometimes I feel it might be good to have a linting rule that requires writing if (proposal_power == 0 or proposal_power is None) and (... (but only sometimes, because other times I guess it can be overkill 😆 )

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh damn, you're right, we can't continue here if proposal_power == 0. Stupid Python and its weird implicit booleans. I will make a PR next week.

Sometimes I feel it might be good to have a linting rule that requires writing if (proposal_power == 0 or proposal_power is None) and (... (but only sometimes, because other times I guess it can be overkill 😆 )

That sounds like a great idea. It is not overkill. Expressing our intentions clearly is always the way to go.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Upvote so it is ready when we switch to ruff :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like I had already changed this before merging:

So nothing to do.

Comment on lines 204 to 210
| Actor | Priority | System Bounds | Requested Bounds | Requested | Aggregate |
| | | | | Power | Power |
|-------|----------|-----------------|------------------|--------------|-----------|
| A | 3 | -100kW .. 100kW | None | 20kW | 20kW |
| B | 2 | -120kW .. 80kW | None | 50kW | 70kW |
| C | 1 | -170kW .. 30kW | None | 50kW | 100kW |
| | | | | target power | 100kW |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe like in the test, adding a column with the "adjusted power" could add some extra clarity.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

individual batteries, to keep the batteries in balance.
### Resolving conflicting power proposals
### How to work with other actors
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All (or at least some of) the old documentation here should still be useful for people interested in the old Matryoshka algorithm, so not sure if replacing it is the best choice. I wonder if the docs shouldn't be moved to the algorithms themselves, and either referenced here (or see if there is a way to include it with mkdocs, but this won't work/look nice when reading the docs from the editor). Maybe in here we can just say more generically that there are 2 available algorithms, say which is the default, and then just link to the algorithm docs for details? If the plan is to remove the old algo soon, I guess it is fine to move forward as it is, but if we really want to support more than one algorithm, the docs should be structured in a way that we can explain all of them and switch the default easily without rewriting the whole __init__.py docs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code for the old algorithm is still there, but users cannot choose it. Chances are we'll not need the old algorithm anymore. We could introduce new algorithms in the future just for specific component types, like EVs, etc. But we don't have to change the docs structure until we offer multiple algorithms for them to choose from.

shsms added 9 commits March 27, 2025 10:14
With this we only have to write the details on how the power manager
works only once.

Signed-off-by: Sahas Subramanian <[email protected]>
Signed-off-by: Sahas Subramanian <[email protected]>
Earlier, the exclusion bounds were applied to every proposal, and in
some cases, that was leading to much higher target powers than what
was necessary.

Signed-off-by: Sahas Subramanian <[email protected]>
And refactor the _calc_targets method to use the new function to
eliminate code duplication.

Signed-off-by: Sahas Subramanian <[email protected]>
@shsms shsms force-pushed the power-manager-shift-limit branch from 68c72cd to 9cb09d7 Compare March 27, 2025 10:01
@shsms shsms requested a review from llucax March 27, 2025 10:03
shsms added 2 commits March 27, 2025 11:04
Instead the proposed powers need to be clamped to the available
bounds.

Signed-off-by: Sahas Subramanian <[email protected]>
Copy link
Contributor

@ela-kotulska-frequenz ela-kotulska-frequenz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am sorry but I still don't understand new approach fully. :D

For example (test based on test_shifting_matryoshka.py):

    system_bounds = _base_types.SystemBounds(
        timestamp=datetime.now(tz=timezone.utc),
        inclusion_bounds=timeseries.Bounds(
            lower=Power.from_watts(-1000.0), upper=Power.from_watts(1000.0)
        ),
        exclusion_bounds=None,
    )

    tester = StatefulTester(batteries, system_bounds)
    tester.tgt_power(priority=7, power=150.0, bounds=(100.0, 200.0), expected=150.0)
    tester.tgt_power(priority=6, power=50.0, bounds=(0, 150.0), expected=200.0)

Two actors (not aware of each other and not aware of any bounds) sets power and bounds.
I expected: target_power = 100 and bounds = (100..150) ,
Actual result: target_power = 200.
So two things happen:

  1. target_power is bigger then any power proposal
  2. Actors with priority 6 should narrow the upper bound to 150 but it doesn't.

Is it expected and why?

Another question: How actor should should be aware of other actor and sdk bounds? I am asking because some actors don't look at any bounds, now. They assume PowerManager will update bounds.
But with this algorithm actor should be aware of the power proposed by another actors , because his proposed power will be combined with it.

@shsms
Copy link
Contributor Author

shsms commented Apr 4, 2025

I am sorry but I still don't understand new approach fully. :D

Okay, that's good because now I know I was right to be skeptical about there being no questions when I explained it last Tuesday. But it was a nice few days when I thought that I must have done a really good job explaining and that we finally have created a fully consistent, easy-to-understand algorithm. :D We can go through it again on Monday.

Two actors (not aware of each other and not aware of any bounds) sets power and bounds. I expected: target_power = 100 and bounds = (100..150) , Actual result: target_power = 200.

The original matryoshka algorithm would have produced 100 here. The new algorithm adds the request powers from all the actors.

So two things happen:

  1. target_power is bigger then any power proposal

Yes, it is the sum of the two actors. That is how the shifting matryoshka algorithm differs from the older algorithm, which was just limiting.

  1. Actors with priority 6 should narrow the upper bound to 150 but it doesn't.

Actor pri 6 narrows the bounds only for itself and for actors with a lower pri than 6. Actor pri 7 sets 150. And that becomes the 0 point for Actor pri 6. Because Actor 7 limits bounds to 100..200, according to the new zero point, the available range for Actor 6 becomes -50..50. When Actor 6 sets power to 50, it has it is within its available range.

You could try setting other powers from 6. higher than 50, lower than 50, and negative values.

Is it expected and why?

Another question: How actor should should be aware of other actor and sdk bounds? I am asking because some actors don't look at any bounds, now. They assume PowerManager will update bounds. But with this algorithm actor should be aware of the power proposed by another actors , because his proposed power will be combined with it.

No, they are still independent. They don't have access to what's going on with the actual batteries, or what other actors are setting.

@github-project-automation github-project-automation bot moved this from To do to Review approved in Python SDK Roadmap Apr 7, 2025
@ela-kotulska-frequenz ela-kotulska-frequenz self-requested a review April 7, 2025 08:08
@shsms shsms added this pull request to the merge queue Apr 8, 2025
Merged via the queue into frequenz-floss:v1.x.x with commit 87c2e35 Apr 8, 2025
5 checks passed
@shsms shsms deleted the power-manager-shift-limit branch April 8, 2025 08:48
@github-project-automation github-project-automation bot moved this from Review approved to Done in Python SDK Roadmap Apr 8, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

part:data-pipeline Affects the data pipeline part:docs Affects the documentation part:microgrid Affects the interactions with the microgrid part:tests Affects the unit, integration and performance (benchmarks) tests

Projects

Development

Successfully merging this pull request may close these issues.

PowerManager algorithm that can shift or limit bounds

4 participants