diff options
author | Vratko Polak <vrpolak@cisco.com> | 2023-10-17 16:31:35 +0200 |
---|---|---|
committer | Vratko Polak <vrpolak@cisco.com> | 2023-10-18 08:10:06 +0000 |
commit | e5dbe10d9599b9a53fa07e6fadfaf427ba6d69e3 (patch) | |
tree | 147b7972bea35a093f6644e63c5f1fb4e4b2c9a0 /resources/libraries/python/MLRsearch/limit_handler.py | |
parent | c6dfb6c09c5dafd1d522f96b4b86c5ec5efc1c83 (diff) |
feat(MLRsearch): MLRsearch v7
Replaces MLRv2, suitable for "big bang" upgrade across CSIT.
PyPI metadata updated only partially (full edits will come separately).
Pylint wants less complexity, but the differences are only minor.
+ Use the same (new CSIT) defaults everywhere, also in Python library.
+ Update also PLRsearch to use the new result class.
+ Make upper bound optional in UTI.
+ Fix ASTF approximate duration detection.
+ Do not keep approximated_receive_rate (for MRR) in result structure.
Change-Id: I03406f32d5c93f56b527cb3f93791b61955dfd74
Signed-off-by: Vratko Polak <vrpolak@cisco.com>
Diffstat (limited to 'resources/libraries/python/MLRsearch/limit_handler.py')
-rw-r--r-- | resources/libraries/python/MLRsearch/limit_handler.py | 198 |
1 files changed, 198 insertions, 0 deletions
diff --git a/resources/libraries/python/MLRsearch/limit_handler.py b/resources/libraries/python/MLRsearch/limit_handler.py new file mode 100644 index 0000000000..5919f398f3 --- /dev/null +++ b/resources/libraries/python/MLRsearch/limit_handler.py @@ -0,0 +1,198 @@ +# Copyright (c) 2023 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Module defining LimitHandler class.""" + +from dataclasses import dataclass +from typing import Callable, Optional + +from .dataclass import secondary_field +from .discrete_interval import DiscreteInterval +from .discrete_load import DiscreteLoad +from .discrete_width import DiscreteWidth +from .load_rounding import LoadRounding + + +@dataclass +class LimitHandler: + """Encapsulated methods for logic around handling limits. + + In multiple places within MLRsearch code, an intended load value + is only useful if it is far enough from possible known values. + All such places can be served with the handle method + with appropriate arguments. + """ + + rounding: LoadRounding + """Rounding instance to use.""" + debug: Callable[[str], None] + """Injectable logging function.""" + # The two fields below are derived, extracted from rounding as a shortcut. + min_load: DiscreteLoad = secondary_field() + """Minimal load, as prescribed by Config.""" + max_load: DiscreteLoad = secondary_field() + """Maximal load, as prescribed by Config.""" + + def __post_init__(self) -> None: + """Initialize derived quantities.""" + from_float = DiscreteLoad.float_conver(rounding=self.rounding) + self.min_load = from_float(self.rounding.min_load) + self.max_load = from_float(self.rounding.max_load) + + def handle( + self, + load: DiscreteLoad, + width: DiscreteWidth, + clo: Optional[DiscreteLoad], + chi: Optional[DiscreteLoad], + ) -> Optional[DiscreteLoad]: + """Return new intended load after considering limits and bounds. + + Not only we want to avoid measuring outside minmax interval, + we also want to avoid measuring too close to known limits and bounds. + We either round or return None, depending on hints from bound loads. + + When rounding away from hard limits, we may end up being + too close to an already measured bound. + In this case, pick a midpoint between the bound and the limit. + + The last two arguments are just loads (not full measurement results) + to allow callers to exclude some load without measuring them. + As a convenience, full results are also supported, + so that callers do not need to care about None when extracting load. + + :param load: Intended load candidate, initial or from a load selector. + :param width: Relative width goal, considered narrow enough for now. + :param clo: Intended load of current relevant lower bound. + :param chi: Intended load of current relevant upper bound. + :type load: DiscreteLoad + :type width: DiscreteWidth + :type clo: Optional[DiscreteLoad] + :type chi: Optional[DiscreteLoad] + :return: Adjusted load to measure at, or None if narrow enough already. + :rtype: Optional[DiscreteLoad] + :raises RuntimeError: If unsupported corner case is detected. + """ + if not load: + raise RuntimeError("Got None load to handle.") + load = load.rounded_down() + min_load, max_load = self.min_load, self.max_load + if clo and not clo.is_round: + raise RuntimeError(f"Clo {clo} should have been round.") + if chi and not chi.is_round: + raise RuntimeError(f"Chi {chi} should have been round.") + if not clo and not chi: + load = self._handle_load_with_excludes( + load, width, min_load, max_load, min_ex=False, max_ex=False + ) + # The "return load" lines are separate from load computation, + # so that logging can be added more easily when debugging. + return load + if chi and not clo: + if chi <= min_load: + # Expected when hitting the min load. + return None + if load >= chi: + # This can happen when mrr2 forward rate is rounded to mrr2. + return None + load = self._handle_load_with_excludes( + load, width, min_load, chi, min_ex=False, max_ex=True + ) + return load + if clo and not chi: + if clo >= max_load: + raise RuntimeError("Lower load expected.") + if load <= clo: + raise RuntimeError("Higher load expected.") + load = self._handle_load_with_excludes( + load, width, clo, max_load, min_ex=True, max_ex=False + ) + return load + # We have both clo and chi defined. + if not clo < load < chi: + # Happens when bisect compares with bounded extend. + return None + load = self._handle_load_with_excludes( + load, width, clo, chi, min_ex=True, max_ex=True + ) + return load + + def _handle_load_with_excludes( + self, + load: DiscreteLoad, + width: DiscreteWidth, + minimum: DiscreteLoad, + maximum: DiscreteLoad, + min_ex: bool, + max_ex: bool, + ) -> Optional[DiscreteLoad]: + """Adjust load if too close to limits, respecting exclusions. + + This is a reusable block. + Limits may come from previous bounds or from hard load limits. + When coming from bounds, rounding to that is not allowed. + When coming from hard limits, rounding to the limit value + is allowed in general (given by the setting the _ex flag). + + :param load: The candidate intended load before accounting for limits. + :param width: Relative width of area around the limits to avoid. + :param minimum: The lower limit to round around. + :param maximum: The upper limit to round around. + :param min_ex: If false, rounding to the minimum is allowed. + :param max_ex: If false, rounding to the maximum is allowed. + :type load: DiscreteLoad + :type width: DiscreteWidth + :type minimum: DiscreteLoad + :type maximum: DiscreteLoad + :type min_ex: bool + :type max_ex: bool + :returns: Adjusted load value, or None if narrow enough. + :rtype: Optional[DiscreteLoad] + :raises RuntimeError: If internal inconsistency is detected. + """ + if not minimum <= load <= maximum: + raise RuntimeError( + "Internal error: load outside limits:" + f" load {load} min {minimum} max {maximum}" + ) + max_width = maximum - minimum + if width >= max_width: + self.debug("Warning: Handling called with wide width.") + if not min_ex: + self.debug("Minimum not excluded, rounding to it.") + return minimum + if not max_ex: + self.debug("Maximum not excluded, rounding to it.") + return maximum + self.debug("Both limits excluded, narrow enough.") + return None + soft_min = minimum + width + soft_max = maximum - width + if soft_min > soft_max: + self.debug("Whole interval is less than two goals.") + middle = DiscreteInterval(minimum, maximum).middle(width) + soft_min = soft_max = middle + if load < soft_min: + if min_ex: + self.debug("Min excluded, rounding to soft min.") + return soft_min + self.debug("Min not excluded, rounding to minimum.") + return minimum + if load > soft_max: + if max_ex: + self.debug("Max excluded, rounding to soft max.") + return soft_max + self.debug("Max not excluded, rounding to maximum.") + return maximum + # Far enough from all limits, no additional adjustment is needed. + return load |