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 DiscreteLoad class."""
16 from __future__ import annotations
18 from dataclasses import dataclass, field
19 from functools import total_ordering
20 from typing import Callable, Optional, Union
22 from .load_rounding import LoadRounding
23 from .discrete_width import DiscreteWidth
29 """Structure to store load value together with its rounded integer form.
31 LoadRounding instance is needed to enable conversion between two forms.
32 Conversion methods and factories are added for convenience.
34 In general, the float form is allowed to differ from conversion from int.
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.
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.
43 This dataclass is effectively frozen, but cannot be marked as such
44 as that would prevent LoadStats from being its subclass.
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."""
55 def __post_init__(self) -> None:
56 """Ensure types, compute missing information.
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.
62 :raises RuntimeError: If both init arguments are None.
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)
70 self.float_load = float(self.float_load)
71 self.int_load = self.rounding.float2int(self.float_load)
73 def __str__(self) -> str:
74 """Convert to a short human-readable string.
76 :returns: The short string.
79 return f"int_load={int(self)}"
81 # Explicit comparison operators.
82 # Those generated with dataclass order=True do not allow subclass instances.
84 def __eq__(self, other: Optional[DiscreteLoad]) -> bool:
85 """Return whether the other instance has the same float form.
87 None is effectively considered to be an unequal instance.
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.
96 return float(self) == float(other)
98 def __lt__(self, other: DiscreteLoad) -> bool:
99 """Return whether self has smaller float form than the other instance.
101 None is not supported, as MLRsearch does not need that
102 (so when None appears we want to raise).
104 :param other: Other instance to compare to.
105 :type other: DiscreteLoad
106 :returns: True only if float forms of self is strictly smaller.
109 return float(self) < float(other)
111 def __hash__(self) -> int:
112 """Return a hash based on the float value.
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.
117 :returns: Hash value for this instance.
120 return hash(float(self))
123 def is_round(self) -> bool:
124 """Return whether float load matches converted int load.
126 :returns: False if float load is not rounded.
129 expected = self.rounding.int2float(self.int_load)
130 return expected == self.float_load
132 def __int__(self) -> int:
133 """Return the int value.
135 :returns: The int field value.
140 def __float__(self) -> float:
141 """Return the float value.
143 :returns: The float field value [tps].
146 return self.float_load
149 def int_conver(rounding: LoadRounding) -> Callable[[int], DiscreteLoad]:
150 """Return a factory that turns an int load into a discrete load.
152 :param rounding: Rounding instance needed.
153 :type rounding: LoadRounding
154 :returns: Factory to use when converting from int.
155 :rtype: Callable[[int], DiscreteLoad]
158 def factory_int(int_load: int) -> DiscreteLoad:
159 """Use rounding and int load to create discrete load.
161 :param int_load: Intended load in integer form.
163 :returns: New discrete load instance matching the int load.
166 return DiscreteLoad(rounding=rounding, int_load=int_load)
171 def float_conver(rounding: LoadRounding) -> Callable[[float], DiscreteLoad]:
172 """Return a factory that turns a float load into a discrete load.
174 :param rounding: Rounding instance needed.
175 :type rounding: LoadRounding
176 :returns: Factory to use when converting from float.
177 :rtype: Callable[[float], DiscreteLoad]
180 def factory_float(float_load: float) -> DiscreteLoad:
181 """Use rounding instance and float load to create discrete load.
183 The float form is not rounded yet.
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.
190 return DiscreteLoad(rounding=rounding, float_load=float_load)
194 def rounded_down(self) -> DiscreteLoad:
195 """Create and return new instance with float form matching int.
197 :returns: New instance with same int form and float form rounded down.
200 return DiscreteLoad(rounding=self.rounding, int_load=int(self))
202 def hashable(self) -> DiscreteLoad:
203 """Return new equivalent instance.
205 This is mainly useful for conversion from unhashable subclasses,
207 Rounding instance (reference) is copied from self.
209 :returns: New instance with values based on float form of self.
212 return DiscreteLoad(rounding=self.rounding, float_load=float(self))
214 def __add__(self, width: DiscreteWidth) -> DiscreteLoad:
215 """Return newly constructed instance with width added to int load.
217 Rounding instance (reference) is copied from self.
219 Argument type is checked, to avoid caller adding two loads by mistake
220 (or adding int to load and similar).
222 :param width: Value to add to int load.
223 :type width: DiscreteWidth
224 :returns: New instance.
226 :raises RuntimeError: When argument has unexpected type.
228 if not isinstance(width, DiscreteWidth):
229 raise RuntimeError(f"Not width: {width!r}")
231 rounding=self.rounding,
232 int_load=self.int_load + int(width),
236 self, other: Union[DiscreteWidth, DiscreteLoad]
237 ) -> Union[DiscreteLoad, DiscreteWidth]:
238 """Return result based on the argument type.
240 Load minus load is width, load minus width is load.
241 This allows the same operator to support both operations.
243 Rounding instance (reference) is copied from self.
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.
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}")
257 def _minus_width(self, width: DiscreteWidth) -> DiscreteLoad:
258 """Return newly constructed instance, width subtracted from int load.
260 Rounding instance (reference) is copied from self.
262 :param width: Value to subtract from int load.
263 :type width: DiscreteWidth
264 :returns: New instance.
268 rounding=self.rounding,
269 int_load=self.int_load - int(width),
272 def _minus_load(self, other: DiscreteLoad) -> DiscreteWidth:
273 """Return newly constructed width instance, difference of int loads.
275 Rounding instance (reference) is copied from self.
277 :param other: Value to subtract from int load.
278 :type other: DiscreteLoad
279 :returns: New instance.
280 :rtype: DiscreteWidth
282 return DiscreteWidth(
283 rounding=self.rounding,
284 int_width=self.int_load - int(other),
287 def __mul__(self, coefficient: float) -> DiscreteLoad:
288 """Return newly constructed instance, float load multiplied by argument.
290 Rounding instance (reference) is copied from self.
292 :param coefficient: Value to multiply float load with.
293 :type coefficient: float
294 :returns: New instance.
296 :raises RuntimeError: If argument is unsupported.
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}")
303 rounding=self.rounding,
304 float_load=self.float_load * coefficient,
307 def __truediv__(self, coefficient: float) -> DiscreteLoad:
308 """Call multiplication with inverse argument.
310 :param coefficient: Value to divide float load with.
311 :type coefficient: float
312 :returns: New instance.
314 :raises RuntimeError: If argument is unsupported.
316 return self * (1.0 / coefficient)