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 Candidate class."""
16 from __future__ import annotations
18 from dataclasses import dataclass
19 from functools import total_ordering
20 from typing import Optional
22 from .discrete_load import DiscreteLoad
23 from .discrete_width import DiscreteWidth
24 from .selector import Selector
28 @dataclass(frozen=True)
30 """Class describing next trial inputs, as nominated by a selector.
32 As each selector is notified by the controller when its nominated load
33 becomes the winner, a reference to the selector is also included here.
35 The rest of the code focuses on defining the ordering between candidates.
36 When two instances are compared, the lesser has higher priority
37 for choosing which trial is actually performed next.
39 As Python implicitly converts values to bool in many places
40 (e.g. in "if" statement), any instance is called "truthy" if it converts
41 to True, and "falsy" if it converts to False.
42 To make such places nice and readable, __bool__ method is implemented
43 in a way that a candidate instance is falsy if its load is None.
44 As a falsy candidate never gets measured,
45 other fields of a falsy instance are irrelevant.
48 load: Optional[DiscreteLoad] = None
49 """Measure at this intended load. None if no load nominated by selector."""
50 duration: float = None
51 """Trial duration as chosen by the selector."""
52 width: Optional[DiscreteWidth] = None
53 """Set the global width to this when this candidate becomes the winner."""
54 selector: Selector = None
55 """Reference to the selector instance which nominated this candidate."""
57 def __str__(self) -> str:
58 """Convert trial inputs into a short human-readable string.
60 :returns: The short string.
63 return f"d={self.duration},l={self.load}"
65 def __eq__(self, other: Candidate) -> bool:
66 """Return wheter self is identical to the other candidate.
68 This is just a pretense for total ordering wrapper to work.
69 In reality, MLRsearch shall never test equivalence,
70 so we save space by just raising RuntimeError if this is ever called.
72 :param other: The other instance to compare to.
73 :type other: Candidate
74 :returns: True if the instances are equivalent.
76 :raises RuntimeError: Always, to prevent unintended usage.
78 raise RuntimeError("Candidate equality comparison shall not be needed.")
80 def __lt__(self, other: Candidate) -> bool:
81 """Return whether self should be measured before other.
83 In the decreasing order of importance:
84 Non-None load is preferred.
85 Self is less than other when both loads are None.
86 Lower offered load is preferred.
87 Longer trial duration is preferred.
88 Non-none width is preferred.
89 Larger width is preferred.
92 The logic comes from the desire to save time and being conservative.
94 :param other: The other instance to compare to.
95 :type other: Candidate
96 :returns: True if self should be measured sooner.
105 if self.load < other.load:
107 if self.load > other.load:
109 if self.duration > other.duration:
111 if self.duration < other.duration:
119 return self.width >= other.width
121 def __bool__(self) -> bool:
122 """Does this candidate choose to perform any trial measurement?
124 :returns: True if yes, it does choose to perform.
127 return bool(self.load)
130 def nomination_from(selector: Selector) -> Candidate:
131 """Call nominate on selector, wrap into Candidate instance to return.
133 We avoid dependency cycle while letting candidate depend on selector,
134 therefore selector cannot know how to wrap its nomination
135 into a full candidate instance.
136 This factory method finishes the wrapping.
138 :param selector: Selector to call.
139 :type selector: Selector
140 :returns: Newly created Candidate instance with nominated trial inputs.
143 load, duration, width = selector.nominate()
151 def won(self) -> None:
152 """Inform selector its candidate became a winner."""
153 self.selector.won(self.load)