Revert "fix(IPsecUtil): Delete keywords no longer used"
[csit.git] / resources / libraries / python / MLRsearch / discrete_load.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 DiscreteLoad class."""
15
16 from __future__ import annotations
17
18 from dataclasses import dataclass, field
19 from functools import total_ordering
20 from typing import Callable, Optional, Union
21
22 from .load_rounding import LoadRounding
23 from .discrete_width import DiscreteWidth
24
25
26 @total_ordering
27 @dataclass
28 class DiscreteLoad:
29     """Structure to store load value together with its rounded integer form.
30
31     LoadRounding instance is needed to enable conversion between two forms.
32     Conversion methods and factories are added for convenience.
33
34     In general, the float form is allowed to differ from conversion from int.
35
36     Comparisons are supported, acting on the float load component.
37     Additive operations are supported, acting on int form.
38     Multiplication by a float constant is supported, acting on float form.
39
40     As for all user defined classes by default, all instances are truthy.
41     That is useful when dealing with Optional values, as None is falsy.
42
43     This dataclass is effectively frozen, but cannot be marked as such
44     as that would prevent LoadStats from being its subclass.
45     """
46
47     # For most debugs, rounding in repr just takes space.
48     rounding: LoadRounding = field(repr=False, compare=False)
49     """Rounding instance to use for conversion."""
50     float_load: float = None
51     """Float form of intended load [tps], usable for measurer."""
52     int_load: int = field(compare=False, default=None)
53     """Integer form, usable for exact computations."""
54
55     def __post_init__(self) -> None:
56         """Ensure types, compute missing information.
57
58         At this point, it is allowed for float load to differ from
59         conversion from int load. MLRsearch should round explicitly later,
60         based on its additional information.
61
62         :raises RuntimeError: If both init arguments are None.
63         """
64         if self.float_load is None and self.int_load is None:
65             raise RuntimeError("Float or int value is needed.")
66         if self.float_load is None:
67             self.int_load = int(self.int_load)
68             self.float_load = self.rounding.int2float(self.int_load)
69         else:
70             self.float_load = float(self.float_load)
71             self.int_load = self.rounding.float2int(self.float_load)
72
73     def __str__(self) -> str:
74         """Convert to a short human-readable string.
75
76         :returns: The short string.
77         :rtype: str
78         """
79         return f"int_load={int(self)}"
80
81     # Explicit comparison operators.
82     # Those generated with dataclass order=True do not allow subclass instances.
83
84     def __eq__(self, other: Optional[DiscreteLoad]) -> bool:
85         """Return whether the other instance has the same float form.
86
87         None is effectively considered to be an unequal instance.
88
89         :param other: Other instance to compare to, or None.
90         :type other: Optional[DiscreteLoad]
91         :returns: True only if float forms are exactly equal.
92         :rtype: bool
93         """
94         if other is None:
95             return False
96         return float(self) == float(other)
97
98     def __lt__(self, other: DiscreteLoad) -> bool:
99         """Return whether self has smaller float form than the other instance.
100
101         None is not supported, as MLRsearch does not need that
102         (so when None appears we want to raise).
103
104         :param other: Other instance to compare to.
105         :type other: DiscreteLoad
106         :returns: True only if float forms of self is strictly smaller.
107         :rtype: bool
108         """
109         return float(self) < float(other)
110
111     def __hash__(self) -> int:
112         """Return a hash based on the float value.
113
114         With this, the instance can be used as if it was immutable and hashable,
115         e.g. it can be a key in a dict.
116
117         :returns: Hash value for this instance.
118         :rtype: int
119         """
120         return hash(float(self))
121
122     @property
123     def is_round(self) -> bool:
124         """Return whether float load matches converted int load.
125
126         :returns: False if float load is not rounded.
127         :rtype: bool
128         """
129         expected = self.rounding.int2float(self.int_load)
130         return expected == self.float_load
131
132     def __int__(self) -> int:
133         """Return the int value.
134
135         :returns: The int field value.
136         :rtype: int
137         """
138         return self.int_load
139
140     def __float__(self) -> float:
141         """Return the float value.
142
143         :returns: The float field value [tps].
144         :rtype: float
145         """
146         return self.float_load
147
148     @staticmethod
149     def int_conver(rounding: LoadRounding) -> Callable[[int], DiscreteLoad]:
150         """Return a factory that turns an int load into a discrete load.
151
152         :param rounding: Rounding instance needed.
153         :type rounding: LoadRounding
154         :returns: Factory to use when converting from int.
155         :rtype: Callable[[int], DiscreteLoad]
156         """
157
158         def factory_int(int_load: int) -> DiscreteLoad:
159             """Use rounding and int load to create discrete load.
160
161             :param int_load: Intended load in integer form.
162             :type int_load: int
163             :returns: New discrete load instance matching the int load.
164             :rtype: DiscreteLoad
165             """
166             return DiscreteLoad(rounding=rounding, int_load=int_load)
167
168         return factory_int
169
170     @staticmethod
171     def float_conver(rounding: LoadRounding) -> Callable[[float], DiscreteLoad]:
172         """Return a factory that turns a float load into a discrete load.
173
174         :param rounding: Rounding instance needed.
175         :type rounding: LoadRounding
176         :returns: Factory to use when converting from float.
177         :rtype: Callable[[float], DiscreteLoad]
178         """
179
180         def factory_float(float_load: float) -> DiscreteLoad:
181             """Use rounding instance and float load to create discrete load.
182
183             The float form is not rounded yet.
184
185             :param int_load: Intended load in float form [tps].
186             :type int_load: float
187             :returns: New discrete load instance matching the float load.
188             :rtype: DiscreteLoad
189             """
190             return DiscreteLoad(rounding=rounding, float_load=float_load)
191
192         return factory_float
193
194     def rounded_down(self) -> DiscreteLoad:
195         """Create and return new instance with float form matching int.
196
197         :returns: New instance with same int form and float form rounded down.
198         :rtype: DiscreteLoad
199         """
200         return DiscreteLoad(rounding=self.rounding, int_load=int(self))
201
202     def hashable(self) -> DiscreteLoad:
203         """Return new equivalent instance.
204
205         This is mainly useful for conversion from unhashable subclasses,
206         such as LoadStats.
207         Rounding instance (reference) is copied from self.
208
209         :returns: New instance with values based on float form of self.
210         :rtype: DiscreteLoad
211         """
212         return DiscreteLoad(rounding=self.rounding, float_load=float(self))
213
214     def __add__(self, width: DiscreteWidth) -> DiscreteLoad:
215         """Return newly constructed instance with width added to int load.
216
217         Rounding instance (reference) is copied from self.
218
219         Argument type is checked, to avoid caller adding two loads by mistake
220         (or adding int to load and similar).
221
222         :param width: Value to add to int load.
223         :type width: DiscreteWidth
224         :returns: New instance.
225         :rtype: DiscreteLoad
226         :raises RuntimeError: When argument has unexpected type.
227         """
228         if not isinstance(width, DiscreteWidth):
229             raise RuntimeError(f"Not width: {width!r}")
230         return DiscreteLoad(
231             rounding=self.rounding,
232             int_load=self.int_load + int(width),
233         )
234
235     def __sub__(
236         self, other: Union[DiscreteWidth, DiscreteLoad]
237     ) -> Union[DiscreteLoad, DiscreteWidth]:
238         """Return result based on the argument type.
239
240         Load minus load is width, load minus width is load.
241         This allows the same operator to support both operations.
242
243         Rounding instance (reference) is copied from self.
244
245         :param other: Value to subtract from int load.
246         :type other: Union[DiscreteWidth, DiscreteLoad]
247         :returns: Resulting width or load.
248         :rtype: Union[DiscreteLoad, DiscreteWidth]
249         :raises RuntimeError: If the argument type is not supported.
250         """
251         if isinstance(other, DiscreteWidth):
252             return self._minus_width(other)
253         if isinstance(other, DiscreteLoad):
254             return self._minus_load(other)
255         raise RuntimeError(f"Unsupported type {other!r}")
256
257     def _minus_width(self, width: DiscreteWidth) -> DiscreteLoad:
258         """Return newly constructed instance, width subtracted from int load.
259
260         Rounding instance (reference) is copied from self.
261
262         :param width: Value to subtract from int load.
263         :type width: DiscreteWidth
264         :returns: New instance.
265         :rtype: DiscreteLoad
266         """
267         return DiscreteLoad(
268             rounding=self.rounding,
269             int_load=self.int_load - int(width),
270         )
271
272     def _minus_load(self, other: DiscreteLoad) -> DiscreteWidth:
273         """Return newly constructed width instance, difference of int loads.
274
275         Rounding instance (reference) is copied from self.
276
277         :param other: Value to subtract from int load.
278         :type other: DiscreteLoad
279         :returns: New instance.
280         :rtype: DiscreteWidth
281         """
282         return DiscreteWidth(
283             rounding=self.rounding,
284             int_width=self.int_load - int(other),
285         )
286
287     def __mul__(self, coefficient: float) -> DiscreteLoad:
288         """Return newly constructed instance, float load multiplied by argument.
289
290         Rounding instance (reference) is copied from self.
291
292         :param coefficient: Value to multiply float load with.
293         :type coefficient: float
294         :returns: New instance.
295         :rtype: DiscreteLoad
296         :raises RuntimeError: If argument is unsupported.
297         """
298         if not isinstance(coefficient, float):
299             raise RuntimeError(f"Not float: {coefficient!r}")
300         if coefficient <= 0.0:
301             raise RuntimeError(f"Not positive: {coefficient!r}")
302         return DiscreteLoad(
303             rounding=self.rounding,
304             float_load=self.float_load * coefficient,
305         )
306
307     def __truediv__(self, coefficient: float) -> DiscreteLoad:
308         """Call multiplication with inverse argument.
309
310         :param coefficient: Value to divide float load with.
311         :type coefficient: float
312         :returns: New instance.
313         :rtype: DiscreteLoad
314         :raises RuntimeError: If argument is unsupported.
315         """
316         return self * (1.0 / coefficient)