Revert "fix(IPsecUtil): Delete keywords no longer used"
[csit.git] / resources / libraries / python / MLRsearch / candidate.py
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:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
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.
13
14 """Module defining Candidate class."""
15
16 from __future__ import annotations
17
18 from dataclasses import dataclass
19 from functools import total_ordering
20 from typing import Optional
21
22 from .discrete_load import DiscreteLoad
23 from .discrete_width import DiscreteWidth
24 from .selector import Selector
25
26
27 @total_ordering
28 @dataclass(frozen=True)
29 class Candidate:
30     """Class describing next trial inputs, as nominated by a selector.
31
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.
34
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.
38
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.
46     """
47
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."""
56
57     def __str__(self) -> str:
58         """Convert trial inputs into a short human-readable string.
59
60         :returns: The short string.
61         :rtype: str
62         """
63         return f"d={self.duration},l={self.load}"
64
65     def __eq__(self, other: Candidate) -> bool:
66         """Return wheter self is identical to the other candidate.
67
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.
71
72         :param other: The other instance to compare to.
73         :type other: Candidate
74         :returns: True if the instances are equivalent.
75         :rtype: bool
76         :raises RuntimeError: Always, to prevent unintended usage.
77         """
78         raise RuntimeError("Candidate equality comparison shall not be needed.")
79
80     def __lt__(self, other: Candidate) -> bool:
81         """Return whether self should be measured before other.
82
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.
90         Self is preferred.
91
92         The logic comes from the desire to save time and being conservative.
93
94         :param other: The other instance to compare to.
95         :type other: Candidate
96         :returns: True if self should be measured sooner.
97         :rtype: bool
98         """
99         if not self.load:
100             if other.load:
101                 return False
102             return True
103         if not other.load:
104             return True
105         if self.load < other.load:
106             return True
107         if self.load > other.load:
108             return False
109         if self.duration > other.duration:
110             return True
111         if self.duration < other.duration:
112             return False
113         if not self.width:
114             if other.width:
115                 return False
116             return True
117         if not other.width:
118             return True
119         return self.width >= other.width
120
121     def __bool__(self) -> bool:
122         """Does this candidate choose to perform any trial measurement?
123
124         :returns: True if yes, it does choose to perform.
125         :rtype: bool
126         """
127         return bool(self.load)
128
129     @staticmethod
130     def nomination_from(selector: Selector) -> Candidate:
131         """Call nominate on selector, wrap into Candidate instance to return.
132
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.
137
138         :param selector: Selector to call.
139         :type selector: Selector
140         :returns: Newly created Candidate instance with nominated trial inputs.
141         :rtype: Candidate
142         """
143         load, duration, width = selector.nominate()
144         return Candidate(
145             load=load,
146             duration=duration,
147             width=width,
148             selector=selector,
149         )
150
151     def won(self) -> None:
152         """Inform selector its candidate became a winner."""
153         self.selector.won(self.load)