Skip to content

Commit 8c9bcae

Browse files
authored
Quantity improvements (#533)
Closes #531, Closes #532
2 parents 520406f + bc37e7c commit 8c9bcae

File tree

3 files changed

+83
-11
lines changed

3 files changed

+83
-11
lines changed

RELEASE_NOTES.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@
99
<!-- Here goes notes on how to upgrade from previous versions, including deprecations and what they should be replaced with -->
1010

1111
- `Channels` has been upgraded to version 0.16.0, for information on how to upgrade visit https://github.com/frequenz-floss/frequenz-channels-python/releases/tag/v0.16.0
12+
- `Quantity` objects are no longer hashable. This is because of the pitfalls of hashing `float` values.
1213

1314
## New Features
1415

1516
- Add `abs()` support for quantities.
1617
- Add quantity class `Frequency` for frequency values.
1718
- Quantities can now be multiplied with `Percentage` types.
18-
- `FormulaEngine` arithmetics now supports scalar multiplication with floats and addition with Quantities
19+
- `FormulaEngine` arithmetics now supports scalar multiplication with floats and addition with Quantities.
20+
- Add a `isclose()` method on quantities to compare them to other values of the same type. Because `Quantity` types are just wrappers around `float`s, direct comparison might not always be desirable.
1921

2022
## Bug Fixes
2123

src/frequenz/sdk/timeseries/_quantities.py

Lines changed: 74 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,23 @@ def isinf(self) -> bool:
100100
"""
101101
return math.isinf(self._base_value)
102102

103-
def __hash__(self) -> int:
104-
"""Return a hash of this object.
103+
def isclose(self, other: Self, rel_tol: float = 1e-9, abs_tol: float = 0.0) -> bool:
104+
"""Return whether this quantity is close to another.
105+
106+
Args:
107+
other: The quantity to compare to.
108+
rel_tol: The relative tolerance.
109+
abs_tol: The absolute tolerance.
105110
106111
Returns:
107-
A hash of this object.
112+
Whether this quantity is close to another.
108113
"""
109-
return hash((type(self), self._base_value))
114+
return math.isclose(
115+
self._base_value,
116+
other._base_value, # pylint: disable=protected-access
117+
rel_tol=rel_tol,
118+
abs_tol=abs_tol,
119+
)
110120

111121
def __repr__(self) -> str:
112122
"""Return a representation of this quantity.
@@ -401,7 +411,16 @@ class Power(
401411
6: "MW",
402412
},
403413
):
404-
"""A power quantity."""
414+
"""A power quantity.
415+
416+
Objects of this type are wrappers around `float` values.
417+
418+
The constructors accept a single `float` value, the `as_*()` methods return a
419+
`float` value, and each of the arithmetic operators supported by this type are
420+
actually implemented using floating-point arithmetic.
421+
422+
So all considerations about floating-point arithmetic apply to this type as well.
423+
"""
405424

406425
@classmethod
407426
def from_watts(cls, watts: float) -> Self:
@@ -565,7 +584,16 @@ class Current(
565584
0: "A",
566585
},
567586
):
568-
"""A current quantity."""
587+
"""A current quantity.
588+
589+
Objects of this type are wrappers around `float` values.
590+
591+
The constructors accept a single `float` value, the `as_*()` methods return a
592+
`float` value, and each of the arithmetic operators supported by this type are
593+
actually implemented using floating-point arithmetic.
594+
595+
So all considerations about floating-point arithmetic apply to this type as well.
596+
"""
569597

570598
@classmethod
571599
def from_amperes(cls, amperes: float) -> Self:
@@ -652,7 +680,16 @@ class Voltage(
652680
metaclass=_NoDefaultConstructible,
653681
exponent_unit_map={0: "V", -3: "mV", 3: "kV"},
654682
):
655-
"""A voltage quantity."""
683+
"""A voltage quantity.
684+
685+
Objects of this type are wrappers around `float` values.
686+
687+
The constructors accept a single `float` value, the `as_*()` methods return a
688+
`float` value, and each of the arithmetic operators supported by this type are
689+
actually implemented using floating-point arithmetic.
690+
691+
So all considerations about floating-point arithmetic apply to this type as well.
692+
"""
656693

657694
@classmethod
658695
def from_volts(cls, volts: float) -> Self:
@@ -765,7 +802,16 @@ class Energy(
765802
6: "MWh",
766803
},
767804
):
768-
"""An energy quantity."""
805+
"""An energy quantity.
806+
807+
Objects of this type are wrappers around `float` values.
808+
809+
The constructors accept a single `float` value, the `as_*()` methods return a
810+
`float` value, and each of the arithmetic operators supported by this type are
811+
actually implemented using floating-point arithmetic.
812+
813+
So all considerations about floating-point arithmetic apply to this type as well.
814+
"""
769815

770816
@classmethod
771817
def from_watt_hours(cls, watt_hours: float) -> Self:
@@ -875,7 +921,16 @@ class Frequency(
875921
metaclass=_NoDefaultConstructible,
876922
exponent_unit_map={0: "Hz", 3: "kHz", 6: "MHz", 9: "GHz"},
877923
):
878-
"""A frequency quantity."""
924+
"""A frequency quantity.
925+
926+
Objects of this type are wrappers around `float` values.
927+
928+
The constructors accept a single `float` value, the `as_*()` methods return a
929+
`float` value, and each of the arithmetic operators supported by this type are
930+
actually implemented using floating-point arithmetic.
931+
932+
So all considerations about floating-point arithmetic apply to this type as well.
933+
"""
879934

880935
@classmethod
881936
def from_hertz(cls, hertz: float) -> Self:
@@ -979,7 +1034,16 @@ class Percentage(
9791034
metaclass=_NoDefaultConstructible,
9801035
exponent_unit_map={0: "%"},
9811036
):
982-
"""A percentage quantity."""
1037+
"""A percentage quantity.
1038+
1039+
Objects of this type are wrappers around `float` values.
1040+
1041+
The constructors accept a single `float` value, the `as_*()` methods return a
1042+
`float` value, and each of the arithmetic operators supported by this type are
1043+
actually implemented using floating-point arithmetic.
1044+
1045+
So all considerations about floating-point arithmetic apply to this type as well.
1046+
"""
9831047

9841048
@classmethod
9851049
def from_percent(cls, percent: float) -> Self:

tests/timeseries/test_quantities.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@ def test_string_representation() -> None:
9191
assert f"{Fz1(-20000)}" == "-20 kHz"
9292

9393

94+
def test_isclose() -> None:
95+
"""Test the isclose method of the quantities."""
96+
assert Fz1(1.024445).isclose(Fz1(1.024445))
97+
assert not Fz1(1.024445).isclose(Fz1(1.0))
98+
99+
94100
def test_addition_subtraction() -> None:
95101
"""Test the addition and subtraction of the quantities."""
96102
assert Quantity(1) + Quantity(1, exponent=0) == Quantity(2, exponent=0)

0 commit comments

Comments
 (0)