diff options
Diffstat (limited to 'resources/libraries/python/MLRsearch/candidate.py')
-rw-r--r-- | resources/libraries/python/MLRsearch/candidate.py | 153 |
1 files changed, 153 insertions, 0 deletions
diff --git a/resources/libraries/python/MLRsearch/candidate.py b/resources/libraries/python/MLRsearch/candidate.py new file mode 100644 index 0000000000..16bbe60bae --- /dev/null +++ b/resources/libraries/python/MLRsearch/candidate.py @@ -0,0 +1,153 @@ +# 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 Candidate class.""" + +from __future__ import annotations + +from dataclasses import dataclass +from functools import total_ordering +from typing import Optional + +from .discrete_load import DiscreteLoad +from .discrete_width import DiscreteWidth +from .selector import Selector + + +@total_ordering +@dataclass(frozen=True) +class Candidate: + """Class describing next trial inputs, as nominated by a selector. + + As each selector is notified by the controller when its nominated load + becomes the winner, a reference to the selector is also included here. + + The rest of the code focuses on defining the ordering between candidates. + When two instances are compared, the lesser has higher priority + for choosing which trial is actually performed next. + + As Python implicitly converts values to bool in many places + (e.g. in "if" statement), any instance is called "truthy" if it converts + to True, and "falsy" if it converts to False. + To make such places nice and readable, __bool__ method is implemented + in a way that a candidate instance is falsy if its load is None. + As a falsy candidate never gets measured, + other fields of a falsy instance are irrelevant. + """ + + load: Optional[DiscreteLoad] = None + """Measure at this intended load. None if no load nominated by selector.""" + duration: float = None + """Trial duration as chosen by the selector.""" + width: Optional[DiscreteWidth] = None + """Set the global width to this when this candidate becomes the winner.""" + selector: Selector = None + """Reference to the selector instance which nominated this candidate.""" + + def __str__(self) -> str: + """Convert trial inputs into a short human-readable string. + + :returns: The short string. + :rtype: str + """ + return f"d={self.duration},l={self.load}" + + def __eq__(self, other: Candidate) -> bool: + """Return wheter self is identical to the other candidate. + + This is just a pretense for total ordering wrapper to work. + In reality, MLRsearch shall never test equivalence, + so we save space by just raising RuntimeError if this is ever called. + + :param other: The other instance to compare to. + :type other: Candidate + :returns: True if the instances are equivalent. + :rtype: bool + :raises RuntimeError: Always, to prevent unintended usage. + """ + raise RuntimeError("Candidate equality comparison shall not be needed.") + + def __lt__(self, other: Candidate) -> bool: + """Return whether self should be measured before other. + + In the decreasing order of importance: + Non-None load is preferred. + Self is less than other when both loads are None. + Lower offered load is preferred. + Longer trial duration is preferred. + Non-none width is preferred. + Larger width is preferred. + Self is preferred. + + The logic comes from the desire to save time and being conservative. + + :param other: The other instance to compare to. + :type other: Candidate + :returns: True if self should be measured sooner. + :rtype: bool + """ + if not self.load: + if other.load: + return False + return True + if not other.load: + return True + if self.load < other.load: + return True + if self.load > other.load: + return False + if self.duration > other.duration: + return True + if self.duration < other.duration: + return False + if not self.width: + if other.width: + return False + return True + if not other.width: + return True + return self.width >= other.width + + def __bool__(self) -> bool: + """Does this candidate choose to perform any trial measurement? + + :returns: True if yes, it does choose to perform. + :rtype: bool + """ + return bool(self.load) + + @staticmethod + def nomination_from(selector: Selector) -> Candidate: + """Call nominate on selector, wrap into Candidate instance to return. + + We avoid dependency cycle while letting candidate depend on selector, + therefore selector cannot know how to wrap its nomination + into a full candidate instance. + This factory method finishes the wrapping. + + :param selector: Selector to call. + :type selector: Selector + :returns: Newly created Candidate instance with nominated trial inputs. + :rtype: Candidate + """ + load, duration, width = selector.nominate() + return Candidate( + load=load, + duration=duration, + width=width, + selector=selector, + ) + + def won(self) -> None: + """Inform selector its candidate became a winner.""" + self.selector.won(self.load) |