1 # Copyright (c) 2023 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
6 # http://www.apache.org/licenses/LICENSE-2.0
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
14 """Module defining LimitHandler class."""
16 from dataclasses import dataclass
17 from typing import Callable, Optional
19 from .dataclass import secondary_field
20 from .discrete_interval import DiscreteInterval
21 from .discrete_load import DiscreteLoad
22 from .discrete_width import DiscreteWidth
23 from .load_rounding import LoadRounding
28 """Encapsulated methods for logic around handling limits.
30 In multiple places within MLRsearch code, an intended load value
31 is only useful if it is far enough from possible known values.
32 All such places can be served with the handle method
33 with appropriate arguments.
36 rounding: LoadRounding
37 """Rounding instance to use."""
38 debug: Callable[[str], None]
39 """Injectable logging function."""
40 # The two fields below are derived, extracted from rounding as a shortcut.
41 min_load: DiscreteLoad = secondary_field()
42 """Minimal load, as prescribed by Config."""
43 max_load: DiscreteLoad = secondary_field()
44 """Maximal load, as prescribed by Config."""
46 def __post_init__(self) -> None:
47 """Initialize derived quantities."""
48 from_float = DiscreteLoad.float_conver(rounding=self.rounding)
49 self.min_load = from_float(self.rounding.min_load)
50 self.max_load = from_float(self.rounding.max_load)
56 clo: Optional[DiscreteLoad],
57 chi: Optional[DiscreteLoad],
58 ) -> Optional[DiscreteLoad]:
59 """Return new intended load after considering limits and bounds.
61 Not only we want to avoid measuring outside minmax interval,
62 we also want to avoid measuring too close to known limits and bounds.
63 We either round or return None, depending on hints from bound loads.
65 When rounding away from hard limits, we may end up being
66 too close to an already measured bound.
67 In this case, pick a midpoint between the bound and the limit.
69 The last two arguments are just loads (not full measurement results)
70 to allow callers to exclude some load without measuring them.
71 As a convenience, full results are also supported,
72 so that callers do not need to care about None when extracting load.
74 :param load: Intended load candidate, initial or from a load selector.
75 :param width: Relative width goal, considered narrow enough for now.
76 :param clo: Intended load of current relevant lower bound.
77 :param chi: Intended load of current relevant upper bound.
78 :type load: DiscreteLoad
79 :type width: DiscreteWidth
80 :type clo: Optional[DiscreteLoad]
81 :type chi: Optional[DiscreteLoad]
82 :return: Adjusted load to measure at, or None if narrow enough already.
83 :rtype: Optional[DiscreteLoad]
84 :raises RuntimeError: If unsupported corner case is detected.
87 raise RuntimeError("Got None load to handle.")
88 load = load.rounded_down()
89 min_load, max_load = self.min_load, self.max_load
90 if clo and not clo.is_round:
91 raise RuntimeError(f"Clo {clo} should have been round.")
92 if chi and not chi.is_round:
93 raise RuntimeError(f"Chi {chi} should have been round.")
94 if not clo and not chi:
95 load = self._handle_load_with_excludes(
96 load, width, min_load, max_load, min_ex=False, max_ex=False
98 # The "return load" lines are separate from load computation,
99 # so that logging can be added more easily when debugging.
103 # Expected when hitting the min load.
106 # This can happen when mrr2 forward rate is rounded to mrr2.
108 load = self._handle_load_with_excludes(
109 load, width, min_load, chi, min_ex=False, max_ex=True
114 raise RuntimeError("Lower load expected.")
116 raise RuntimeError("Higher load expected.")
117 load = self._handle_load_with_excludes(
118 load, width, clo, max_load, min_ex=True, max_ex=False
121 # We have both clo and chi defined.
122 if not clo < load < chi:
123 # Happens when bisect compares with bounded extend.
125 load = self._handle_load_with_excludes(
126 load, width, clo, chi, min_ex=True, max_ex=True
130 def _handle_load_with_excludes(
133 width: DiscreteWidth,
134 minimum: DiscreteLoad,
135 maximum: DiscreteLoad,
138 ) -> Optional[DiscreteLoad]:
139 """Adjust load if too close to limits, respecting exclusions.
141 This is a reusable block.
142 Limits may come from previous bounds or from hard load limits.
143 When coming from bounds, rounding to that is not allowed.
144 When coming from hard limits, rounding to the limit value
145 is allowed in general (given by the setting the _ex flag).
147 :param load: The candidate intended load before accounting for limits.
148 :param width: Relative width of area around the limits to avoid.
149 :param minimum: The lower limit to round around.
150 :param maximum: The upper limit to round around.
151 :param min_ex: If false, rounding to the minimum is allowed.
152 :param max_ex: If false, rounding to the maximum is allowed.
153 :type load: DiscreteLoad
154 :type width: DiscreteWidth
155 :type minimum: DiscreteLoad
156 :type maximum: DiscreteLoad
159 :returns: Adjusted load value, or None if narrow enough.
160 :rtype: Optional[DiscreteLoad]
161 :raises RuntimeError: If internal inconsistency is detected.
163 if not minimum <= load <= maximum:
165 "Internal error: load outside limits:"
166 f" load {load} min {minimum} max {maximum}"
168 max_width = maximum - minimum
169 if width >= max_width:
170 self.debug("Warning: Handling called with wide width.")
172 self.debug("Minimum not excluded, rounding to it.")
175 self.debug("Maximum not excluded, rounding to it.")
177 self.debug("Both limits excluded, narrow enough.")
179 soft_min = minimum + width
180 soft_max = maximum - width
181 if soft_min > soft_max:
182 self.debug("Whole interval is less than two goals.")
183 middle = DiscreteInterval(minimum, maximum).middle(width)
184 soft_min = soft_max = middle
187 self.debug("Min excluded, rounding to soft min.")
189 self.debug("Min not excluded, rounding to minimum.")
193 self.debug("Max excluded, rounding to soft max.")
195 self.debug("Max not excluded, rounding to maximum.")
197 # Far enough from all limits, no additional adjustment is needed.