frequenz-quantities 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- frequenz/quantities/__init__.py +112 -0
- frequenz/quantities/_apparent_power.py +234 -0
- frequenz/quantities/_current.py +131 -0
- frequenz/quantities/_energy.py +188 -0
- frequenz/quantities/_frequency.py +115 -0
- frequenz/quantities/_percentage.py +66 -0
- frequenz/quantities/_power.py +244 -0
- frequenz/quantities/_quantity.py +541 -0
- frequenz/quantities/_reactive_power.py +235 -0
- frequenz/quantities/_temperature.py +39 -0
- frequenz/quantities/_voltage.py +148 -0
- frequenz/quantities/conftest.py +13 -0
- frequenz/quantities/experimental/__init__.py +11 -0
- frequenz/quantities/experimental/marshmallow.py +271 -0
- frequenz/quantities/py.typed +0 -0
- frequenz_quantities-1.0.0.dist-info/LICENSE +21 -0
- frequenz_quantities-1.0.0.dist-info/METADATA +103 -0
- frequenz_quantities-1.0.0.dist-info/RECORD +20 -0
- frequenz_quantities-1.0.0.dist-info/WHEEL +5 -0
- frequenz_quantities-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
# License: MIT
|
|
2
|
+
# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
|
|
3
|
+
|
|
4
|
+
"""Types for holding quantities with units."""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import math
|
|
10
|
+
from typing import TYPE_CHECKING, Any, NoReturn, Self, overload
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from ._percentage import Percentage
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Quantity:
|
|
17
|
+
"""A quantity with a unit.
|
|
18
|
+
|
|
19
|
+
Quantities try to behave like float and are also immutable.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
_base_value: float
|
|
23
|
+
"""The value of this quantity in the base unit."""
|
|
24
|
+
|
|
25
|
+
_exponent_unit_map: dict[int, str] | None = None
|
|
26
|
+
"""A mapping from the exponent of the base unit to the unit symbol.
|
|
27
|
+
|
|
28
|
+
If None, this quantity has no unit. None is possible only when using the base
|
|
29
|
+
class. Sub-classes must define this.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, value: float, exponent: int = 0) -> None:
|
|
33
|
+
"""Initialize a new quantity.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
value: The value of this quantity in a given exponent of the base unit.
|
|
37
|
+
exponent: The exponent of the base unit the given value is in.
|
|
38
|
+
"""
|
|
39
|
+
self._base_value = value * 10.0**exponent
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def _new(cls, value: float, *, exponent: int = 0) -> Self:
|
|
43
|
+
"""Instantiate a new quantity subclass instance.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
value: The value of this quantity in a given exponent of the base unit.
|
|
47
|
+
exponent: The exponent of the base unit the given value is in.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
A new quantity subclass instance.
|
|
51
|
+
"""
|
|
52
|
+
self = cls.__new__(cls)
|
|
53
|
+
self._base_value = value * 10.0**exponent
|
|
54
|
+
return self
|
|
55
|
+
|
|
56
|
+
def __init_subclass__(cls, exponent_unit_map: dict[int, str]) -> None:
|
|
57
|
+
"""Initialize a new subclass of Quantity.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
exponent_unit_map: A mapping from the exponent of the base unit to the unit
|
|
61
|
+
symbol.
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
ValueError: If the given exponent_unit_map does not contain a base unit
|
|
65
|
+
(exponent 0).
|
|
66
|
+
"""
|
|
67
|
+
if 0 not in exponent_unit_map:
|
|
68
|
+
raise ValueError("Expected a base unit for the type (for exponent 0)")
|
|
69
|
+
cls._exponent_unit_map = exponent_unit_map
|
|
70
|
+
super().__init_subclass__()
|
|
71
|
+
|
|
72
|
+
_zero_cache: dict[type, Quantity] = {}
|
|
73
|
+
"""Cache for zero singletons.
|
|
74
|
+
|
|
75
|
+
This is a workaround for mypy getting confused when using @functools.cache and
|
|
76
|
+
@classmethod combined with returning Self. It believes the resulting type of this
|
|
77
|
+
method is Self and complains that members of the actual class don't exist in Self,
|
|
78
|
+
so we need to implement the cache ourselves.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def zero(cls) -> Self:
|
|
83
|
+
"""Return a quantity with value 0.0.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
A quantity with value 0.0.
|
|
87
|
+
"""
|
|
88
|
+
_zero = cls._zero_cache.get(cls, None)
|
|
89
|
+
if _zero is None:
|
|
90
|
+
_zero = cls.__new__(cls)
|
|
91
|
+
_zero._base_value = 0.0
|
|
92
|
+
cls._zero_cache[cls] = _zero
|
|
93
|
+
assert isinstance(_zero, cls)
|
|
94
|
+
return _zero
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def from_string(cls, string: str) -> Self:
|
|
98
|
+
"""Return a quantity from a string representation.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
string: The string representation of the quantity.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
A quantity object with the value given in the string.
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
ValueError: If the string does not match the expected format.
|
|
108
|
+
|
|
109
|
+
"""
|
|
110
|
+
split_string = string.split(" ")
|
|
111
|
+
|
|
112
|
+
if len(split_string) != 2:
|
|
113
|
+
raise ValueError(
|
|
114
|
+
f"Expected a string of the form 'value unit', got {string}"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
assert cls._exponent_unit_map is not None
|
|
118
|
+
exp_map = cls._exponent_unit_map
|
|
119
|
+
|
|
120
|
+
for exponent, unit in exp_map.items():
|
|
121
|
+
if unit == split_string[1]:
|
|
122
|
+
instance = cls.__new__(cls)
|
|
123
|
+
try:
|
|
124
|
+
instance._base_value = float(split_string[0]) * 10**exponent
|
|
125
|
+
except ValueError as error:
|
|
126
|
+
raise ValueError(f"Failed to parse string '{string}'.") from error
|
|
127
|
+
|
|
128
|
+
return instance
|
|
129
|
+
|
|
130
|
+
raise ValueError(f"Unknown unit {split_string[1]}")
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def base_value(self) -> float:
|
|
134
|
+
"""Return the value of this quantity in the base unit.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
The value of this quantity in the base unit.
|
|
138
|
+
"""
|
|
139
|
+
return self._base_value
|
|
140
|
+
|
|
141
|
+
def __round__(self, ndigits: int | None = None) -> Self:
|
|
142
|
+
"""Round this quantity to the given number of digits.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
ndigits: The number of digits to round to.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
The rounded quantity.
|
|
149
|
+
"""
|
|
150
|
+
return self._new(round(self._base_value, ndigits))
|
|
151
|
+
|
|
152
|
+
def __pos__(self) -> Self:
|
|
153
|
+
"""Return this quantity.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
This quantity.
|
|
157
|
+
"""
|
|
158
|
+
return self
|
|
159
|
+
|
|
160
|
+
def __mod__(self, other: Self) -> Self:
|
|
161
|
+
"""Return the remainder of this quantity and another.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
other: The other quantity.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
The remainder of this quantity and another.
|
|
168
|
+
"""
|
|
169
|
+
return self._new(self._base_value % other._base_value)
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def base_unit(self) -> str | None:
|
|
173
|
+
"""Return the base unit of this quantity.
|
|
174
|
+
|
|
175
|
+
None if this quantity has no unit.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
The base unit of this quantity.
|
|
179
|
+
"""
|
|
180
|
+
if not self._exponent_unit_map:
|
|
181
|
+
return None
|
|
182
|
+
return self._exponent_unit_map[0]
|
|
183
|
+
|
|
184
|
+
def isnan(self) -> bool:
|
|
185
|
+
"""Return whether this quantity is NaN.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Whether this quantity is NaN.
|
|
189
|
+
"""
|
|
190
|
+
return math.isnan(self._base_value)
|
|
191
|
+
|
|
192
|
+
def isinf(self) -> bool:
|
|
193
|
+
"""Return whether this quantity is infinite.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Whether this quantity is infinite.
|
|
197
|
+
"""
|
|
198
|
+
return math.isinf(self._base_value)
|
|
199
|
+
|
|
200
|
+
def isclose(self, other: Self, rel_tol: float = 1e-9, abs_tol: float = 0.0) -> bool:
|
|
201
|
+
"""Return whether this quantity is close to another.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
other: The quantity to compare to.
|
|
205
|
+
rel_tol: The relative tolerance.
|
|
206
|
+
abs_tol: The absolute tolerance.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Whether this quantity is close to another.
|
|
210
|
+
"""
|
|
211
|
+
return math.isclose(
|
|
212
|
+
self._base_value,
|
|
213
|
+
other._base_value, # pylint: disable=protected-access
|
|
214
|
+
rel_tol=rel_tol,
|
|
215
|
+
abs_tol=abs_tol,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
def __repr__(self) -> str:
|
|
219
|
+
"""Return a representation of this quantity.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
A representation of this quantity.
|
|
223
|
+
"""
|
|
224
|
+
return f"{type(self).__name__}(value={self._base_value}, exponent=0)"
|
|
225
|
+
|
|
226
|
+
def __str__(self) -> str:
|
|
227
|
+
"""Return a string representation of this quantity.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
A string representation of this quantity.
|
|
231
|
+
"""
|
|
232
|
+
return self.__format__("")
|
|
233
|
+
|
|
234
|
+
# pylint: disable=too-many-branches
|
|
235
|
+
def __format__(self, __format_spec: str) -> str:
|
|
236
|
+
"""Return a formatted string representation of this quantity.
|
|
237
|
+
|
|
238
|
+
If specified, must be of this form: `[0].{precision}`. If a 0 is not given, the
|
|
239
|
+
trailing zeros will be omitted. If no precision is given, the default is 3.
|
|
240
|
+
|
|
241
|
+
The returned string will use the unit that will result in the maximum precision,
|
|
242
|
+
based on the magnitude of the value.
|
|
243
|
+
|
|
244
|
+
Example:
|
|
245
|
+
```python
|
|
246
|
+
from frequenz.quantities import Current
|
|
247
|
+
c = Current.from_amperes(0.2345)
|
|
248
|
+
assert f"{c:.2}" == "234.5 mA"
|
|
249
|
+
c = Current.from_amperes(1.2345)
|
|
250
|
+
assert f"{c:.2}" == "1.23 A"
|
|
251
|
+
c = Current.from_milliamperes(1.2345)
|
|
252
|
+
assert f"{c:.6}" == "1.2345 mA"
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
__format_spec: The format specifier.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
A string representation of this quantity.
|
|
260
|
+
|
|
261
|
+
Raises:
|
|
262
|
+
ValueError: If the given format specifier is invalid.
|
|
263
|
+
"""
|
|
264
|
+
keep_trailing_zeros = False
|
|
265
|
+
if __format_spec != "":
|
|
266
|
+
fspec_parts = __format_spec.split(".")
|
|
267
|
+
if (
|
|
268
|
+
len(fspec_parts) != 2
|
|
269
|
+
or fspec_parts[0] not in ("", "0")
|
|
270
|
+
or not fspec_parts[1].isdigit()
|
|
271
|
+
):
|
|
272
|
+
raise ValueError(
|
|
273
|
+
"Invalid format specifier. Must be empty or `[0].{precision}`"
|
|
274
|
+
)
|
|
275
|
+
if fspec_parts[0] == "0":
|
|
276
|
+
keep_trailing_zeros = True
|
|
277
|
+
precision = int(fspec_parts[1])
|
|
278
|
+
else:
|
|
279
|
+
precision = 3
|
|
280
|
+
if not self._exponent_unit_map:
|
|
281
|
+
return f"{self._base_value:.{precision}f}"
|
|
282
|
+
|
|
283
|
+
if math.isinf(self._base_value) or math.isnan(self._base_value):
|
|
284
|
+
return f"{self._base_value} {self._exponent_unit_map[0]}"
|
|
285
|
+
|
|
286
|
+
if abs_value := abs(self._base_value):
|
|
287
|
+
precision_pow = 10 ** (precision)
|
|
288
|
+
# Prevent numbers like 999.999999 being rendered as 1000 V
|
|
289
|
+
# instead of 1 kV.
|
|
290
|
+
# This could happen because the str formatting function does
|
|
291
|
+
# rounding as well.
|
|
292
|
+
# This is an imperfect solution that works for _most_ cases.
|
|
293
|
+
# isclose parameters were chosen according to the observed cases
|
|
294
|
+
if math.isclose(abs_value, precision_pow, abs_tol=1e-4, rel_tol=0.01):
|
|
295
|
+
# If the value is close to the precision, round it
|
|
296
|
+
exponent = math.ceil(math.log10(precision_pow))
|
|
297
|
+
else:
|
|
298
|
+
exponent = math.floor(math.log10(abs_value))
|
|
299
|
+
else:
|
|
300
|
+
exponent = 0
|
|
301
|
+
|
|
302
|
+
unit_place = exponent - exponent % 3
|
|
303
|
+
if unit_place < min(self._exponent_unit_map):
|
|
304
|
+
unit = self._exponent_unit_map[min(self._exponent_unit_map.keys())]
|
|
305
|
+
unit_place = min(self._exponent_unit_map)
|
|
306
|
+
elif unit_place > max(self._exponent_unit_map):
|
|
307
|
+
unit = self._exponent_unit_map[max(self._exponent_unit_map.keys())]
|
|
308
|
+
unit_place = max(self._exponent_unit_map)
|
|
309
|
+
else:
|
|
310
|
+
unit = self._exponent_unit_map[unit_place]
|
|
311
|
+
|
|
312
|
+
value_str = f"{self._base_value / 10 ** unit_place:.{precision}f}"
|
|
313
|
+
|
|
314
|
+
if value_str in ("-0", "0"):
|
|
315
|
+
stripped = value_str
|
|
316
|
+
else:
|
|
317
|
+
stripped = value_str.rstrip("0").rstrip(".")
|
|
318
|
+
|
|
319
|
+
if not keep_trailing_zeros:
|
|
320
|
+
value_str = stripped
|
|
321
|
+
unit_str = unit if stripped not in ("-0", "0") else self._exponent_unit_map[0]
|
|
322
|
+
return f"{value_str} {unit_str}"
|
|
323
|
+
|
|
324
|
+
def __add__(self, other: Self) -> Self:
|
|
325
|
+
"""Return the sum of this quantity and another.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
other: The other quantity.
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
The sum of this quantity and another.
|
|
332
|
+
"""
|
|
333
|
+
if not type(other) is type(self):
|
|
334
|
+
return NotImplemented
|
|
335
|
+
summe = type(self).__new__(type(self))
|
|
336
|
+
summe._base_value = self._base_value + other._base_value
|
|
337
|
+
return summe
|
|
338
|
+
|
|
339
|
+
def __sub__(self, other: Self) -> Self:
|
|
340
|
+
"""Return the difference of this quantity and another.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
other: The other quantity.
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
The difference of this quantity and another.
|
|
347
|
+
"""
|
|
348
|
+
if not type(other) is type(self):
|
|
349
|
+
return NotImplemented
|
|
350
|
+
difference = type(self).__new__(type(self))
|
|
351
|
+
difference._base_value = self._base_value - other._base_value
|
|
352
|
+
return difference
|
|
353
|
+
|
|
354
|
+
@overload
|
|
355
|
+
def __mul__(self, scalar: float, /) -> Self:
|
|
356
|
+
"""Scale this quantity by a scalar.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
scalar: The scalar by which to scale this quantity.
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
The scaled quantity.
|
|
363
|
+
"""
|
|
364
|
+
|
|
365
|
+
@overload
|
|
366
|
+
def __mul__(self, percent: Percentage, /) -> Self:
|
|
367
|
+
"""Scale this quantity by a percentage.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
percent: The percentage by which to scale this quantity.
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
The scaled quantity.
|
|
374
|
+
"""
|
|
375
|
+
|
|
376
|
+
def __mul__(self, value: float | Percentage, /) -> Self:
|
|
377
|
+
"""Scale this quantity by a scalar or percentage.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
value: The scalar or percentage by which to scale this quantity.
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
The scaled quantity.
|
|
384
|
+
"""
|
|
385
|
+
from ._percentage import Percentage # pylint: disable=import-outside-toplevel
|
|
386
|
+
|
|
387
|
+
match value:
|
|
388
|
+
case float():
|
|
389
|
+
return type(self)._new(self._base_value * value)
|
|
390
|
+
case Percentage():
|
|
391
|
+
return type(self)._new(self._base_value * value.as_fraction())
|
|
392
|
+
case _:
|
|
393
|
+
return NotImplemented
|
|
394
|
+
|
|
395
|
+
@overload
|
|
396
|
+
def __truediv__(self, other: float, /) -> Self:
|
|
397
|
+
"""Divide this quantity by a scalar.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
other: The scalar or percentage to divide this quantity by.
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
The divided quantity.
|
|
404
|
+
"""
|
|
405
|
+
|
|
406
|
+
@overload
|
|
407
|
+
def __truediv__(self, other: Self, /) -> float:
|
|
408
|
+
"""Return the ratio of this quantity to another.
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
other: The other quantity.
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
The ratio of this quantity to another.
|
|
415
|
+
"""
|
|
416
|
+
|
|
417
|
+
def __truediv__(self, value: float | Self, /) -> Self | float:
|
|
418
|
+
"""Divide this quantity by a scalar or another quantity.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
value: The scalar or quantity to divide this quantity by.
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
The divided quantity or the ratio of this quantity to another.
|
|
425
|
+
"""
|
|
426
|
+
match value:
|
|
427
|
+
case float():
|
|
428
|
+
return type(self)._new(self._base_value / value)
|
|
429
|
+
case Quantity() if type(value) is type(self):
|
|
430
|
+
return self._base_value / value._base_value
|
|
431
|
+
case _:
|
|
432
|
+
return NotImplemented
|
|
433
|
+
|
|
434
|
+
def __gt__(self, other: Self) -> bool:
|
|
435
|
+
"""Return whether this quantity is greater than another.
|
|
436
|
+
|
|
437
|
+
Args:
|
|
438
|
+
other: The other quantity.
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
Whether this quantity is greater than another.
|
|
442
|
+
"""
|
|
443
|
+
if not type(other) is type(self):
|
|
444
|
+
return NotImplemented
|
|
445
|
+
return self._base_value > other._base_value
|
|
446
|
+
|
|
447
|
+
def __ge__(self, other: Self) -> bool:
|
|
448
|
+
"""Return whether this quantity is greater than or equal to another.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
other: The other quantity.
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
Whether this quantity is greater than or equal to another.
|
|
455
|
+
"""
|
|
456
|
+
if not type(other) is type(self):
|
|
457
|
+
return NotImplemented
|
|
458
|
+
return self._base_value >= other._base_value
|
|
459
|
+
|
|
460
|
+
def __lt__(self, other: Self) -> bool:
|
|
461
|
+
"""Return whether this quantity is less than another.
|
|
462
|
+
|
|
463
|
+
Args:
|
|
464
|
+
other: The other quantity.
|
|
465
|
+
|
|
466
|
+
Returns:
|
|
467
|
+
Whether this quantity is less than another.
|
|
468
|
+
"""
|
|
469
|
+
if not type(other) is type(self):
|
|
470
|
+
return NotImplemented
|
|
471
|
+
return self._base_value < other._base_value
|
|
472
|
+
|
|
473
|
+
def __le__(self, other: Self) -> bool:
|
|
474
|
+
"""Return whether this quantity is less than or equal to another.
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
other: The other quantity.
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
Whether this quantity is less than or equal to another.
|
|
481
|
+
"""
|
|
482
|
+
if not type(other) is type(self):
|
|
483
|
+
return NotImplemented
|
|
484
|
+
return self._base_value <= other._base_value
|
|
485
|
+
|
|
486
|
+
def __eq__(self, other: object) -> bool:
|
|
487
|
+
"""Return whether this quantity is equal to another.
|
|
488
|
+
|
|
489
|
+
Args:
|
|
490
|
+
other: The other quantity.
|
|
491
|
+
|
|
492
|
+
Returns:
|
|
493
|
+
Whether this quantity is equal to another.
|
|
494
|
+
"""
|
|
495
|
+
if not type(other) is type(self):
|
|
496
|
+
return NotImplemented
|
|
497
|
+
# The above check ensures that both quantities are the exact same type, because
|
|
498
|
+
# `isinstance` returns true for subclasses and superclasses. But the above check
|
|
499
|
+
# doesn't help mypy identify the type of other, so the below line is necessary.
|
|
500
|
+
assert isinstance(other, self.__class__)
|
|
501
|
+
return self._base_value == other._base_value
|
|
502
|
+
|
|
503
|
+
def __neg__(self) -> Self:
|
|
504
|
+
"""Return the negation of this quantity.
|
|
505
|
+
|
|
506
|
+
Returns:
|
|
507
|
+
The negation of this quantity.
|
|
508
|
+
"""
|
|
509
|
+
negation = type(self).__new__(type(self))
|
|
510
|
+
negation._base_value = -self._base_value
|
|
511
|
+
return negation
|
|
512
|
+
|
|
513
|
+
def __abs__(self) -> Self:
|
|
514
|
+
"""Return the absolute value of this quantity.
|
|
515
|
+
|
|
516
|
+
Returns:
|
|
517
|
+
The absolute value of this quantity.
|
|
518
|
+
"""
|
|
519
|
+
absolute = type(self).__new__(type(self))
|
|
520
|
+
absolute._base_value = abs(self._base_value)
|
|
521
|
+
return absolute
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
class NoDefaultConstructible(type):
|
|
525
|
+
"""A metaclass that disables the default constructor."""
|
|
526
|
+
|
|
527
|
+
def __call__(cls, *_args: Any, **_kwargs: Any) -> NoReturn:
|
|
528
|
+
"""Raise a TypeError when the default constructor is called.
|
|
529
|
+
|
|
530
|
+
Args:
|
|
531
|
+
*_args: ignored positional arguments.
|
|
532
|
+
**_kwargs: ignored keyword arguments.
|
|
533
|
+
|
|
534
|
+
Raises:
|
|
535
|
+
TypeError: Always.
|
|
536
|
+
"""
|
|
537
|
+
raise TypeError(
|
|
538
|
+
"Use of default constructor NOT allowed for "
|
|
539
|
+
f"{cls.__module__}.{cls.__qualname__}, "
|
|
540
|
+
f"use one of the `{cls.__name__}.from_*()` methods instead."
|
|
541
|
+
)
|