aboutsummaryrefslogtreecommitdiffstats
path: root/resources/libraries/python/MLRsearch/candidate.py
diff options
context:
space:
mode:
Diffstat (limited to 'resources/libraries/python/MLRsearch/candidate.py')
-rw-r--r--resources/libraries/python/MLRsearch/candidate.py153
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)