1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
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)
|