Skip to content

Commit f850760

Browse files
committed
Move bounds methods to a separate module
This allows them to be reused in the battery pool, without leading to circular imports. Signed-off-by: Sahas Subramanian <[email protected]>
1 parent c3076b6 commit f850760

File tree

2 files changed

+148
-137
lines changed

2 files changed

+148
-137
lines changed
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
# License: MIT
2+
# Copyright © 2023 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Methods for checking and clamping bounds and power values to exclusion bounds."""
5+
6+
from ...timeseries import Bounds, Power
7+
8+
9+
def _check_exclusion_bounds_overlap(
10+
lower_bound: Power,
11+
upper_bound: Power,
12+
exclusion_bounds: Bounds[Power] | None,
13+
) -> tuple[bool, bool]:
14+
"""Check if the given bounds overlap with the given exclusion bounds.
15+
16+
When only the upper bound overlaps with exclusion bounds, the usable range is
17+
between the lower bound and the lower exclusion bound, like below.
18+
19+
===lb+++++++ex----ub-------ex===
20+
21+
When only the lower bound overlaps with exclusion bounds, the usable range is
22+
between the upper exclusion bound and the upper bound.
23+
24+
===ex------lb------ex++++++ub===
25+
26+
Both bounds overlapping with exclusion bounds (or given bounds are fully contained
27+
within exclusion bounds). In this case, there is no usable range.
28+
29+
===ex------lb------ub------ex===
30+
31+
Args:
32+
lower_bound: The lower bound to check.
33+
upper_bound: The upper bound to check.
34+
exclusion_bounds: The exclusion bounds to check against.
35+
36+
Returns:
37+
A tuple containing a boolean indicating if the lower bound is bounded by the
38+
exclusion bounds, and a boolean indicating if the upper bound is bounded by
39+
the exclusion bounds.
40+
"""
41+
if exclusion_bounds is None:
42+
return False, False
43+
44+
bounded_lower = False
45+
bounded_upper = False
46+
47+
if exclusion_bounds.lower < lower_bound < exclusion_bounds.upper:
48+
bounded_lower = True
49+
if exclusion_bounds.lower < upper_bound < exclusion_bounds.upper:
50+
bounded_upper = True
51+
52+
return bounded_lower, bounded_upper
53+
54+
55+
def _adjust_exclusion_bounds(
56+
lower_bound: Power,
57+
upper_bound: Power,
58+
exclusion_bounds: Bounds[Power] | None,
59+
) -> tuple[Power, Power]:
60+
"""Adjust the given bounds to exclude the given exclusion bounds.
61+
62+
Args:
63+
lower_bound: The lower bound to adjust.
64+
upper_bound: The upper bound to adjust.
65+
exclusion_bounds: The exclusion bounds to adjust to.
66+
67+
Returns:
68+
The adjusted lower and upper bounds.
69+
"""
70+
if exclusion_bounds is None:
71+
return lower_bound, upper_bound
72+
73+
# If the given bounds are within the exclusion bounds, there's no room to adjust,
74+
# so return zero.
75+
#
76+
# And if the given bounds overlap with the exclusion bounds on one side, then clamp
77+
# the given bounds on that side.
78+
match _check_exclusion_bounds_overlap(lower_bound, upper_bound, exclusion_bounds):
79+
case (True, True):
80+
return Power.zero(), Power.zero()
81+
case (False, True):
82+
return lower_bound, exclusion_bounds.lower
83+
case (True, False):
84+
return exclusion_bounds.upper, upper_bound
85+
return lower_bound, upper_bound
86+
87+
88+
# Just 20 lines of code in this function, but unfortunately 8 of those are return
89+
# statements, and that's too many for pylint.
90+
def _clamp_to_bounds( # pylint: disable=too-many-return-statements
91+
value: Power,
92+
lower_bound: Power,
93+
upper_bound: Power,
94+
exclusion_bounds: Bounds[Power] | None,
95+
) -> tuple[Power | None, Power | None]:
96+
"""Clamp the given value to the given bounds.
97+
98+
When the given value can falls within the exclusion zone, and can be clamped to
99+
both sides, both options will be returned.
100+
101+
When the given value falls outside the usable bounds and can be clamped only to
102+
one side, only that option will be returned.
103+
104+
Args:
105+
value: The value to clamp.
106+
lower_bound: The lower bound to clamp to.
107+
upper_bound: The upper bound to clamp to.
108+
exclusion_bounds: The exclusion bounds to clamp outside of.
109+
110+
Returns:
111+
The clamped value.
112+
"""
113+
# If the given bounds are within the exclusion bounds, return zero.
114+
#
115+
# And if the given bounds overlap with the exclusion bounds on one side, and the
116+
# given power is in that overlap region, clamp it to the exclusion bounds on that
117+
# side.
118+
if exclusion_bounds is not None:
119+
match _check_exclusion_bounds_overlap(
120+
lower_bound, upper_bound, exclusion_bounds
121+
):
122+
case (True, True):
123+
return None, None
124+
case (True, False):
125+
if value < exclusion_bounds.upper:
126+
return None, exclusion_bounds.upper
127+
case (False, True):
128+
if value > exclusion_bounds.lower:
129+
return exclusion_bounds.lower, None
130+
131+
# If the given value is outside the given bounds, clamp it to the closest bound.
132+
if value < lower_bound:
133+
return lower_bound, None
134+
if value > upper_bound:
135+
return None, upper_bound
136+
137+
# If the given value is within the exclusion bounds and the exclusion bounds are
138+
# within the given bounds, clamp the given value to the closest exclusion bound.
139+
if exclusion_bounds is not None:
140+
if exclusion_bounds.lower < value < exclusion_bounds.upper:
141+
return exclusion_bounds.lower, exclusion_bounds.upper
142+
143+
return value, value

src/frequenz/sdk/actor/_power_managing/_matryoshka.py

Lines changed: 5 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@
2727
from ... import timeseries
2828
from ...timeseries import Power
2929
from ._base_classes import BaseAlgorithm, Proposal, Report
30+
from ._bounds_methods import (
31+
_adjust_exclusion_bounds,
32+
_check_exclusion_bounds_overlap,
33+
_clamp_to_bounds,
34+
)
3035
from ._sorted_set import SortedSet
3136

3237
if typing.TYPE_CHECKING:
@@ -36,143 +41,6 @@
3641
_logger = logging.getLogger(__name__)
3742

3843

39-
# Just 20 lines of code in this function, but unfortunately 8 of those are return
40-
# statements, and that's too many for pylint.
41-
def _clamp_to_bounds( # pylint: disable=too-many-return-statements
42-
value: Power,
43-
lower_bound: Power,
44-
upper_bound: Power,
45-
exclusion_bounds: timeseries.Bounds[Power] | None,
46-
) -> tuple[Power | None, Power | None]:
47-
"""Clamp the given value to the given bounds.
48-
49-
When the given value can falls within the exclusion zone, and can be clamped to
50-
both sides, both options will be returned.
51-
52-
When the given value falls outside the usable bounds and can be clamped only to
53-
one side, only that option will be returned.
54-
55-
Args:
56-
value: The value to clamp.
57-
lower_bound: The lower bound to clamp to.
58-
upper_bound: The upper bound to clamp to.
59-
exclusion_bounds: The exclusion bounds to clamp outside of.
60-
61-
Returns:
62-
The clamped value.
63-
"""
64-
# If the given bounds are within the exclusion bounds, return zero.
65-
#
66-
# And if the given bounds overlap with the exclusion bounds on one side, and the
67-
# given power is in that overlap region, clamp it to the exclusion bounds on that
68-
# side.
69-
if exclusion_bounds is not None:
70-
match _check_exclusion_bounds_overlap(
71-
lower_bound, upper_bound, exclusion_bounds
72-
):
73-
case (True, True):
74-
return None, None
75-
case (True, False):
76-
if value < exclusion_bounds.upper:
77-
return None, exclusion_bounds.upper
78-
case (False, True):
79-
if value > exclusion_bounds.lower:
80-
return exclusion_bounds.lower, None
81-
82-
# If the given value is outside the given bounds, clamp it to the closest bound.
83-
if value < lower_bound:
84-
return lower_bound, None
85-
if value > upper_bound:
86-
return None, upper_bound
87-
88-
# If the given value is within the exclusion bounds and the exclusion bounds are
89-
# within the given bounds, clamp the given value to the closest exclusion bound.
90-
if exclusion_bounds is not None:
91-
if exclusion_bounds.lower < value < exclusion_bounds.upper:
92-
return exclusion_bounds.lower, exclusion_bounds.upper
93-
94-
return value, value
95-
96-
97-
def _check_exclusion_bounds_overlap(
98-
lower_bound: Power,
99-
upper_bound: Power,
100-
exclusion_bounds: timeseries.Bounds[Power] | None,
101-
) -> tuple[bool, bool]:
102-
"""Check if the given bounds overlap with the given exclusion bounds.
103-
104-
When only the upper bound overlaps with exclusion bounds, the usable range is
105-
between the lower bound and the lower exclusion bound, like below.
106-
107-
===lb+++++++ex----ub-------ex===
108-
109-
When only the lower bound overlaps with exclusion bounds, the usable range is
110-
between the upper exclusion bound and the upper bound.
111-
112-
===ex------lb------ex++++++ub===
113-
114-
Both bounds overlapping with exclusion bounds (or given bounds are fully contained
115-
within exclusion bounds). In this case, there is no usable range.
116-
117-
===ex------lb------ub------ex===
118-
119-
Args:
120-
lower_bound: The lower bound to check.
121-
upper_bound: The upper bound to check.
122-
exclusion_bounds: The exclusion bounds to check against.
123-
124-
Returns:
125-
A tuple containing a boolean indicating if the lower bound is bounded by the
126-
exclusion bounds, and a boolean indicating if the upper bound is bounded by
127-
the exclusion bounds.
128-
"""
129-
if exclusion_bounds is None:
130-
return False, False
131-
132-
bounded_lower = False
133-
bounded_upper = False
134-
135-
if exclusion_bounds.lower < lower_bound < exclusion_bounds.upper:
136-
bounded_lower = True
137-
if exclusion_bounds.lower < upper_bound < exclusion_bounds.upper:
138-
bounded_upper = True
139-
140-
return bounded_lower, bounded_upper
141-
142-
143-
def _adjust_exclusion_bounds(
144-
lower_bound: Power,
145-
upper_bound: Power,
146-
exclusion_bounds: timeseries.Bounds[Power] | None,
147-
) -> tuple[Power, Power]:
148-
"""Adjust the given bounds to exclude the given exclusion bounds.
149-
150-
Args:
151-
lower_bound: The lower bound to adjust.
152-
upper_bound: The upper bound to adjust.
153-
exclusion_bounds: The exclusion bounds to adjust to.
154-
155-
Returns:
156-
The adjusted lower and upper bounds.
157-
"""
158-
if exclusion_bounds is None:
159-
return lower_bound, upper_bound
160-
161-
# If the given bounds are within the exclusion bounds, there's no room to adjust,
162-
# so return zero.
163-
#
164-
# And if the given bounds overlap with the exclusion bounds on one side, then clamp
165-
# the given bounds on that side.
166-
match _check_exclusion_bounds_overlap(lower_bound, upper_bound, exclusion_bounds):
167-
case (True, True):
168-
return Power.zero(), Power.zero()
169-
case (False, True):
170-
return lower_bound, exclusion_bounds.lower
171-
case (True, False):
172-
return exclusion_bounds.upper, upper_bound
173-
return lower_bound, upper_bound
174-
175-
17644
class Matryoshka(BaseAlgorithm):
17745
"""The matryoshka algorithm."""
17846

0 commit comments

Comments
 (0)