feat(MLRsearch): MLRsearch v7
[csit.git] / resources / libraries / python / MLRsearch / candidate.py
diff --git a/resources/libraries/python/MLRsearch/candidate.py b/resources/libraries/python/MLRsearch/candidate.py
new file mode 100644 (file)
index 0000000..16bbe60
--- /dev/null
@@ -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)