Skip to content

Commit 759228e

Browse files
committed
Merge remote-tracking branch 'origin/v0.x.x' into battery-pool-temp
2 parents 18036e4 + 82084a6 commit 759228e

File tree

4 files changed

+100
-55
lines changed

4 files changed

+100
-55
lines changed

RELEASE_NOTES.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,15 @@
1313

1414
## New Features
1515

16-
- Add `abs()` support for quantities.
17-
- Add quantity class `Frequency` for frequency values.
18-
- Quantities can now be multiplied with `Percentage` types.
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.
16+
- Quantities
17+
18+
* Add `abs()`.
19+
* 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.
20+
* Add `zero()` constructor (which returns a singleton) to easily get a zero value.
21+
* Add multiplication by `Percentage` types.
22+
* Add a new quantity class `Frequency` for frequency values.
23+
24+
- `FormulaEngine` arithmetics now supports scalar multiplication with floats and addition with Quantities
2125
- Add a new method for streaming average temperature values for the battery pool.
2226

2327
## Bug Fixes

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ dev-docstrings = [
5454
"darglint == 1.8.1",
5555
"tomli == 2.0.1", # Needed by pydocstyle to read pyproject.toml
5656
]
57-
dev-examples = ["polars == 0.18.8"]
57+
dev-examples = ["polars == 0.18.11"]
5858
dev-formatting = ["black == 23.7.0", "isort == 5.12.0"]
5959
dev-mkdocs = [
6060
"mike == 1.1.2",

src/frequenz/sdk/timeseries/_quantities.py

Lines changed: 34 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@
2525

2626

2727
class Quantity:
28-
"""A quantity with a unit."""
28+
"""A quantity with a unit.
29+
30+
Quantities try to behave like float and are also immutable.
31+
"""
2932

3033
_base_value: float
3134
"""The value of this quantity in the base unit."""
@@ -63,6 +66,30 @@ def __init_subclass__(cls, exponent_unit_map: dict[int, str]) -> None:
6366
cls._exponent_unit_map = exponent_unit_map
6467
super().__init_subclass__()
6568

69+
_zero_cache: dict[type, Quantity] = {}
70+
"""Cache for zero singletons.
71+
72+
This is a workaround for mypy getting confused when using @functools.cache and
73+
@classmethod combined with returning Self. It believes the resulting type of this
74+
method is Self and complains that members of the actual class don't exist in Self,
75+
so we need to implement the cache ourselves.
76+
"""
77+
78+
@classmethod
79+
def zero(cls) -> Self:
80+
"""Return a quantity with value 0.
81+
82+
Returns:
83+
A quantity with value 0.
84+
"""
85+
_zero = cls._zero_cache.get(cls, None)
86+
if _zero is None:
87+
_zero = cls.__new__(cls)
88+
_zero._base_value = 0
89+
cls._zero_cache[cls] = _zero
90+
assert isinstance(_zero, cls)
91+
return _zero
92+
6693
@property
6794
def base_value(self) -> float:
6895
"""Return the value of this quantity in the base unit.
@@ -250,48 +277,6 @@ def __mul__(self, percent: Percentage) -> Self:
250277
product._base_value = self._base_value * percent.as_fraction()
251278
return product
252279

253-
def __iadd__(self, other: Self) -> Self:
254-
"""Add another quantity to this one.
255-
256-
Args:
257-
other: The other quantity.
258-
259-
Returns:
260-
This quantity.
261-
"""
262-
if not type(other) is type(self):
263-
return NotImplemented
264-
self._base_value += other._base_value
265-
return self
266-
267-
def __isub__(self, other: Self) -> Self:
268-
"""Subtract another quantity from this one.
269-
270-
Args:
271-
other: The other quantity.
272-
273-
Returns:
274-
This quantity.
275-
"""
276-
if not type(other) is type(self):
277-
return NotImplemented
278-
self._base_value -= other._base_value
279-
return self
280-
281-
def __imul__(self, percent: Percentage) -> Self:
282-
"""Multiply this quantity by a percentage.
283-
284-
Args:
285-
percent: The percentage.
286-
287-
Returns:
288-
This quantity.
289-
"""
290-
if not isinstance(percent, Percentage):
291-
return NotImplemented
292-
self._base_value *= percent.as_fraction()
293-
return self
294-
295280
def __gt__(self, other: Self) -> bool:
296281
"""Return whether this quantity is greater than another.
297282
@@ -446,7 +431,7 @@ class Power(
446431
):
447432
"""A power quantity.
448433
449-
Objects of this type are wrappers around `float` values.
434+
Objects of this type are wrappers around `float` values and are immutable.
450435
451436
The constructors accept a single `float` value, the `as_*()` methods return a
452437
`float` value, and each of the arithmetic operators supported by this type are
@@ -619,7 +604,7 @@ class Current(
619604
):
620605
"""A current quantity.
621606
622-
Objects of this type are wrappers around `float` values.
607+
Objects of this type are wrappers around `float` values and are immutable.
623608
624609
The constructors accept a single `float` value, the `as_*()` methods return a
625610
`float` value, and each of the arithmetic operators supported by this type are
@@ -715,7 +700,7 @@ class Voltage(
715700
):
716701
"""A voltage quantity.
717702
718-
Objects of this type are wrappers around `float` values.
703+
Objects of this type are wrappers around `float` values and are immutable.
719704
720705
The constructors accept a single `float` value, the `as_*()` methods return a
721706
`float` value, and each of the arithmetic operators supported by this type are
@@ -837,7 +822,7 @@ class Energy(
837822
):
838823
"""An energy quantity.
839824
840-
Objects of this type are wrappers around `float` values.
825+
Objects of this type are wrappers around `float` values and are immutable.
841826
842827
The constructors accept a single `float` value, the `as_*()` methods return a
843828
`float` value, and each of the arithmetic operators supported by this type are
@@ -956,7 +941,7 @@ class Frequency(
956941
):
957942
"""A frequency quantity.
958943
959-
Objects of this type are wrappers around `float` values.
944+
Objects of this type are wrappers around `float` values and are immutable.
960945
961946
The constructors accept a single `float` value, the `as_*()` methods return a
962947
`float` value, and each of the arithmetic operators supported by this type are
@@ -1069,7 +1054,7 @@ class Percentage(
10691054
):
10701055
"""A percentage quantity.
10711056
1072-
Objects of this type are wrappers around `float` values.
1057+
Objects of this type are wrappers around `float` values and are immutable.
10731058
10741059
The constructors accept a single `float` value, the `as_*()` methods return a
10751060
`float` value, and each of the arithmetic operators supported by this type are

tests/timeseries/test_quantities.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,62 @@ class Fz2(
4343
"""Frequency quantity with broad exponent unit map."""
4444

4545

46+
def test_zero() -> None:
47+
"""Test the zero value for quantity."""
48+
assert Quantity(0.0) == Quantity.zero()
49+
assert Quantity(0.0, exponent=100) == Quantity.zero()
50+
assert Quantity.zero() is Quantity.zero() # It is a "singleton"
51+
assert Quantity.zero().base_value == 0.0
52+
53+
# Test the singleton is immutable
54+
one = Quantity.zero()
55+
one += Quantity(1.0)
56+
assert one != Quantity.zero()
57+
assert Quantity.zero() == Quantity(0.0)
58+
59+
assert Power.from_watts(0.0) == Power.zero()
60+
assert Power.from_kilowatts(0.0) == Power.zero()
61+
assert isinstance(Power.zero(), Power)
62+
assert Power.zero().as_watts() == 0.0
63+
assert Power.zero().as_kilowatts() == 0.0
64+
assert Power.zero() is Power.zero() # It is a "singleton"
65+
66+
assert Current.from_amperes(0.0) == Current.zero()
67+
assert Current.from_milliamperes(0.0) == Current.zero()
68+
assert isinstance(Current.zero(), Current)
69+
assert Current.zero().as_amperes() == 0.0
70+
assert Current.zero().as_milliamperes() == 0.0
71+
assert Current.zero() is Current.zero() # It is a "singleton"
72+
73+
assert Voltage.from_volts(0.0) == Voltage.zero()
74+
assert Voltage.from_kilovolts(0.0) == Voltage.zero()
75+
assert isinstance(Voltage.zero(), Voltage)
76+
assert Voltage.zero().as_volts() == 0.0
77+
assert Voltage.zero().as_kilovolts() == 0.0
78+
assert Voltage.zero() is Voltage.zero() # It is a "singleton"
79+
80+
assert Energy.from_kilowatt_hours(0.0) == Energy.zero()
81+
assert Energy.from_megawatt_hours(0.0) == Energy.zero()
82+
assert isinstance(Energy.zero(), Energy)
83+
assert Energy.zero().as_kilowatt_hours() == 0.0
84+
assert Energy.zero().as_megawatt_hours() == 0.0
85+
assert Energy.zero() is Energy.zero() # It is a "singleton"
86+
87+
assert Frequency.from_hertz(0.0) == Frequency.zero()
88+
assert Frequency.from_megahertz(0.0) == Frequency.zero()
89+
assert isinstance(Frequency.zero(), Frequency)
90+
assert Frequency.zero().as_hertz() == 0.0
91+
assert Frequency.zero().as_megahertz() == 0.0
92+
assert Frequency.zero() is Frequency.zero() # It is a "singleton"
93+
94+
assert Percentage.from_percent(0.0) == Percentage.zero()
95+
assert Percentage.from_fraction(0.0) == Percentage.zero()
96+
assert isinstance(Percentage.zero(), Percentage)
97+
assert Percentage.zero().as_percent() == 0.0
98+
assert Percentage.zero().as_fraction() == 0.0
99+
assert Percentage.zero() is Percentage.zero() # It is a "singleton"
100+
101+
46102
def test_string_representation() -> None:
47103
"""Test the string representation of the quantities."""
48104
assert str(Quantity(1.024445, exponent=0)) == "1.024"

0 commit comments

Comments
 (0)