-
Notifications
You must be signed in to change notification settings - Fork 20
Implement the ShiftingMatryoshka algorithm for the PowerManager
#1146
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
649caf6 to
3c38ef5
Compare
There was a problem hiding this 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.
src/frequenz/sdk/microgrid/_power_managing/_power_managing_actor.py
Outdated
Show resolved
Hide resolved
3c38ef5 to
7f405bb
Compare
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. |
We should definitely not remove code because pylint complains about duplication :D Can't we just add a |
7f405bb to
509c528
Compare
eff0cf3 to
6e13739
Compare
…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]>
6e13739 to
4b858b9
Compare
There was a problem hiding this 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]]] = {}
There was a problem hiding this 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?
| """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. |
There was a problem hiding this comment.
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/.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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]]: |
There was a problem hiding this comment.
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 😱
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| 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) |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.0besidesNoneor was the intention to only matchNone
I don't understand what that means. Of course, I can add comments for these, though.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 😆 )
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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:
frequenz-sdk-python/src/frequenz/sdk/microgrid/_power_managing/_shifting_matryoshka.py
Line 136 in 6d106d1
| if proposal_power is not None: |
So nothing to do.
| | 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 | |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Signed-off-by: Sahas Subramanian <[email protected]>
Signed-off-by: Sahas Subramanian <[email protected]>
Signed-off-by: Sahas Subramanian <[email protected]>
Signed-off-by: Sahas Subramanian <[email protected]>
Signed-off-by: Sahas Subramanian <[email protected]>
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]>
68c72cd to
9cb09d7
Compare
Instead the proposed powers need to be clamped to the available bounds. Signed-off-by: Sahas Subramanian <[email protected]>
Signed-off-by: Sahas Subramanian <[email protected]>
9cb09d7 to
95e326c
Compare
There was a problem hiding this 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:
- target_power is bigger then any power proposal
- 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.
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.
The original matryoshka algorithm would have produced 100 here. The new algorithm adds the request powers from all the actors.
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.
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.
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. |
This PR removes the
set_operating_pointfeature and replaces the power manager's originalMatryoshkaalgorithm with the newShiftingMatryoshkaalgorithm.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.