From bb7977d597ba57f10dafc254909f3f245dda6231 Mon Sep 17 00:00:00 2001 From: Elzbieta Kotulska Date: Sun, 13 Oct 2024 09:48:01 +0200 Subject: [PATCH 1/5] Add ReactivePower quantity Signed-off-by: Elzbieta Kotulska --- src/frequenz/quantities/__init__.py | 2 + src/frequenz/quantities/_energy.py | 19 +- src/frequenz/quantities/_reactive_power.py | 252 +++++++++++++++++++++ tests/test_quantities.py | 80 ++++++- 4 files changed, 344 insertions(+), 9 deletions(-) create mode 100644 src/frequenz/quantities/_reactive_power.py diff --git a/src/frequenz/quantities/__init__.py b/src/frequenz/quantities/__init__.py index fcc68b1..39945ef 100644 --- a/src/frequenz/quantities/__init__.py +++ b/src/frequenz/quantities/__init__.py @@ -83,6 +83,7 @@ from ._percentage import Percentage from ._power import Power from ._quantity import Quantity +from ._reactive_power import ReactivePower from ._temperature import Temperature from ._voltage import Voltage @@ -93,6 +94,7 @@ "Percentage", "Power", "Quantity", + "ReactivePower", "Temperature", "Voltage", ] diff --git a/src/frequenz/quantities/_energy.py b/src/frequenz/quantities/_energy.py index a0795e9..0c6586c 100644 --- a/src/frequenz/quantities/_energy.py +++ b/src/frequenz/quantities/_energy.py @@ -14,6 +14,7 @@ if TYPE_CHECKING: from ._percentage import Percentage from ._power import Power + from ._reactive_power import ReactivePower class Energy( @@ -160,8 +161,19 @@ def __truediv__(self, power: Power, /) -> timedelta: A duration from dividing this energy by the given power. """ + @overload + def __truediv__(self, power: ReactivePower, /) -> timedelta: + """Return a duration from dividing this energy by the given power. + + Args: + power: The power to divide by. + + Returns: + A duration from dividing this energy by the given power. + """ + def __truediv__( - self, other: float | Self | timedelta | Power, / + self, other: float | Self | timedelta | Power | ReactivePower, / ) -> Self | float | Power | timedelta: """Return a power or duration from dividing this energy by the given value. @@ -172,6 +184,9 @@ def __truediv__( A power or duration from dividing this energy by the given value. """ from ._power import Power # pylint: disable=import-outside-toplevel + from ._reactive_power import ( + ReactivePower, + ) # pylint: disable=import-outside-toplevel match other: case float(): @@ -180,7 +195,7 @@ def __truediv__( return self._base_value / other._base_value case timedelta(): return Power._new(self._base_value / (other.total_seconds() / 3600.0)) - case Power(): + case Power() | ReactivePower(): return timedelta( seconds=(self._base_value / other._base_value) * 3600.0 ) diff --git a/src/frequenz/quantities/_reactive_power.py b/src/frequenz/quantities/_reactive_power.py new file mode 100644 index 0000000..3f7260c --- /dev/null +++ b/src/frequenz/quantities/_reactive_power.py @@ -0,0 +1,252 @@ +# License: MIT +# Copyright © 2024 Frequenz Energy-as-a-Service GmbH + +"""Types for holding reactive power quantities with units.""" + + +from __future__ import annotations + +from datetime import timedelta +from typing import TYPE_CHECKING, Self, overload + +from ._quantity import NoDefaultConstructible, Quantity + +if TYPE_CHECKING: + from ._current import Current + from ._energy import Energy + from ._percentage import Percentage + from ._voltage import Voltage + + +class ReactivePower( + Quantity, + metaclass=NoDefaultConstructible, + exponent_unit_map={ + -3: "mVAR", + 0: "VAR", + 3: "kVAR", + 6: "MVAR", + }, +): + """A reactive power quantity. + + Objects of this type are wrappers around `float` values and are immutable. + + The constructors accept a single `float` value, the `as_*()` methods return a + `float` value, and each of the arithmetic operators supported by this type are + actually implemented using floating-point arithmetic. + + So all considerations about floating-point arithmetic apply to this type as well. + """ + + @classmethod + def from_volt_amperes_reactive(cls, value: float) -> Self: + """Initialize a new reactive power quantity. + + Args: + value: The reactive power in volt-amperes reactive (VAR). + + Returns: + A new reactive power quantity. + """ + return cls._new(value) + + @classmethod + def from_milli_volt_amperes_reactive(cls, mvars: float) -> Self: + """Initialize a new reactive power quantity. + + Args: + mvars: The reactive power in millivolt-amperes reactive (mVAR). + + Returns: + A new reactive power quantity. + """ + return cls._new(mvars, exponent=-3) + + @classmethod + def from_kilo_volt_amperes_reactive(cls, kvars: float) -> Self: + """Initialize a new reactive power quantity. + + Args: + kvars: The reactive power in kilovolt-amperes reactive (kVAR). + + Returns: + A new reactive power quantity. + """ + return cls._new(kvars, exponent=3) + + @classmethod + def from_mega_volt_amperes_reactive(cls, mvars: float) -> Self: + """Initialize a new reactive power quantity. + + Args: + mvars: The reactive power in megavolt-amperes reactive (MVAR). + + Returns: + A new reactive power quantity. + """ + return cls._new(mvars, exponent=6) + + def as_volt_amperes_reactive(self) -> float: + """Return the reactive power in volt-amperes reactive (VAR). + + Returns: + The reactive power in volt-amperes reactive (VAR). + """ + return self._base_value + + def as_milli_volt_amperes_reactive(self) -> float: + """Return the reactive power in millivolt-amperes reactive (mVAR). + + Returns: + The reactive power in millivolt-amperes reactive (mVAR). + """ + return self._base_value * 1e3 + + def as_kilo_volt_amperes_reactive(self) -> float: + """Return the reactive power in kilovolt-amperes reactive (kVAR). + + Returns: + The reactive power in kilovolt-amperes reactive (kVAR). + """ + return self._base_value / 1e3 + + def as_mega_volt_amperes_reactive(self) -> float: + """Return the reactive power in megavolt-amperes reactive (MVAR). + + Returns: + The reactive power in megavolt-amperes reactive (MVAR). + """ + return self._base_value / 1e6 + + # We need the ignore here because otherwise mypy will give this error: + # > Overloaded operator methods can't have wider argument types in overrides + # The problem seems to be when the other type implements an **incompatible** + # __rmul__ method, which is not the case here, so we should be safe. + # Please see this example: + # https://github.com/python/mypy/blob/c26f1297d4f19d2d1124a30efc97caebb8c28616/test-data/unit/check-overloading.test#L4738C1-L4769C55 + # And a discussion in a mypy issue here: + # https://github.com/python/mypy/issues/4985#issuecomment-389692396 + @overload # type: ignore[override] + def __mul__(self, scalar: float, /) -> Self: + """Scale this power by a scalar. + + Args: + scalar: The scalar by which to scale this power. + + Returns: + The scaled power. + """ + + @overload + def __mul__(self, percent: Percentage, /) -> Self: + """Scale this power by a percentage. + + Args: + percent: The percentage by which to scale this power. + + Returns: + The scaled power. + """ + + @overload + def __mul__(self, other: timedelta, /) -> Energy: + """Return an energy from multiplying this power by the given duration. + + Args: + other: The duration to multiply by. + + Returns: + The calculated energy. + """ + + def __mul__(self, other: float | Percentage | timedelta, /) -> Self | Energy: + """Return a power or energy from multiplying this power by the given value. + + Args: + other: The scalar, percentage or duration to multiply by. + + Returns: + A power or energy. + """ + from ._energy import Energy # pylint: disable=import-outside-toplevel + from ._percentage import Percentage # pylint: disable=import-outside-toplevel + + match other: + case float() | Percentage(): + return super().__mul__(other) + case timedelta(): + return Energy._new(self._base_value * other.total_seconds() / 3600.0) + case _: + return NotImplemented + + # See the comment for ReactivePower.__mul__ for why we need the ignore here. + @overload # type: ignore[override] + def __truediv__(self, other: float, /) -> Self: + """Divide this power by a scalar. + + Args: + other: The scalar to divide this power by. + + Returns: + The divided power. + """ + + @overload + def __truediv__(self, other: Self, /) -> float: + """Return the ratio of this power to another. + + Args: + other: The other power. + + Returns: + The ratio of this power to another. + """ + + @overload + def __truediv__(self, current: Current, /) -> Voltage: + """Return a voltage from dividing this power by the given current. + + Args: + current: The current to divide by. + + Returns: + A voltage from dividing this power by the a current. + """ + + @overload + def __truediv__(self, voltage: Voltage, /) -> Current: + """Return a current from dividing this power by the given voltage. + + Args: + voltage: The voltage to divide by. + + Returns: + A current from dividing this power by a voltage. + """ + + def __truediv__( + self, other: float | Self | Current | Voltage, / + ) -> Self | float | Voltage | Current: + """Return a current or voltage from dividing this power by the given value. + + Args: + other: The scalar, power, current or voltage to divide by. + + Returns: + A current or voltage from dividing this power by the given value. + """ + from ._current import Current # pylint: disable=import-outside-toplevel + from ._voltage import Voltage # pylint: disable=import-outside-toplevel + + match other: + case float(): + return super().__truediv__(other) + case ReactivePower(): + return self._base_value / other._base_value + case Current(): + return Voltage._new(self._base_value / other._base_value) + case Voltage(): + return Current._new(self._base_value / other._base_value) + case _: + return NotImplemented diff --git a/tests/test_quantities.py b/tests/test_quantities.py index ce9ffdb..fa6200b 100644 --- a/tests/test_quantities.py +++ b/tests/test_quantities.py @@ -19,6 +19,7 @@ Percentage, Power, Quantity, + ReactivePower, Temperature, Voltage, ) @@ -90,7 +91,7 @@ class Fz2( assert len(_QUANTITY_CTORS) >= _SANITFY_NUM_CLASSES -def test_zero() -> None: +def test_zero() -> None: # pylint: disable=too-many-statements """Test the zero value for quantity.""" assert Quantity(0.0) == Quantity.zero() assert Quantity(0.0, exponent=100) == Quantity.zero() @@ -110,6 +111,13 @@ def test_zero() -> None: assert Power.zero().as_kilowatts() == 0.0 assert Power.zero() is Power.zero() # It is a "singleton" + assert ReactivePower.from_volt_amperes_reactive(0.0) == ReactivePower.zero() + assert ReactivePower.from_kilo_volt_amperes_reactive(0.0) == ReactivePower.zero() + assert isinstance(ReactivePower.zero(), ReactivePower) + assert ReactivePower.zero().as_volt_amperes_reactive() == 0.0 + assert ReactivePower.zero().as_kilo_volt_amperes_reactive() == 0.0 + assert ReactivePower.zero() is ReactivePower.zero() # It is a "singleton" + assert Current.from_amperes(0.0) == Current.zero() assert Current.from_milliamperes(0.0) == Current.zero() assert isinstance(Current.zero(), Current) @@ -223,8 +231,11 @@ def test_string_representation() -> None: assert f"{Power.from_watts(0.000124445):.0}" == "0 W" assert f"{Energy.from_watt_hours(0.124445):.0}" == "0 Wh" + assert f"{ReactivePower.from_volt_amperes_reactive(0.000124445):.0}" == "0 VAR" assert f"{Power.from_watts(-0.0):.0}" == "-0 W" assert f"{Power.from_watts(0.0):.0}" == "0 W" + assert f"{ReactivePower.from_volt_amperes_reactive(-0.0):.0}" == "-0 VAR" + assert f"{ReactivePower.from_volt_amperes_reactive(0.0):.0}" == "0 VAR" assert f"{Voltage.from_volts(999.9999850988388)}" == "1 kV" @@ -359,6 +370,26 @@ def test_power() -> None: Power(1.0, exponent=0) +def test_reactive_power() -> None: + """Test the reactive power class.""" + power = ReactivePower.from_milli_volt_amperes_reactive(0.0000002) + assert f"{power:.9}" == "0.0000002 mVAR" + power = ReactivePower.from_kilo_volt_amperes_reactive(10000000.2) + assert f"{power}" == "10000 MVAR" + + power = ReactivePower.from_kilo_volt_amperes_reactive(1.2) + assert power.as_volt_amperes_reactive() == 1200.0 + assert power.as_mega_volt_amperes_reactive() == 0.0012 + assert power.as_kilo_volt_amperes_reactive() == 1.2 + assert power == ReactivePower.from_milli_volt_amperes_reactive(1200000.0) + assert power == ReactivePower.from_mega_volt_amperes_reactive(0.0012) + assert power != ReactivePower.from_volt_amperes_reactive(1000.0) + + with pytest.raises(TypeError): + # using the default constructor should raise. + ReactivePower(1.0, exponent=0) + + def test_current() -> None: """Test the current class.""" current = Current.from_milliamperes(0.0000002) @@ -432,22 +463,31 @@ def test_temperature() -> None: Temperature(1.0, exponent=0) -def test_quantity_compositions() -> None: +@pytest.mark.parametrize( + "power", + [ + Power.from_watts(1000.0), + ReactivePower.from_volt_amperes_reactive(1000.0), + ], + ids=lambda q: q.__class__.__name__, +) +def test_quantity_compositions(power: Power | ReactivePower | ApparentPower) -> None: """Test the composition of quantities.""" - power = Power.from_watts(1000.0) voltage = Voltage.from_volts(230.0) current = Current.from_amperes(4.3478260869565215) energy = Energy.from_kilowatt_hours(6.2) assert power / voltage == current assert power / current == voltage - assert power == voltage * current - assert power == current * voltage assert energy / power == timedelta(hours=6.2) - assert energy / timedelta(hours=6.2) == power assert energy == power * timedelta(hours=6.2) + if isinstance(power, Power): + assert power == voltage * current + assert power == current * voltage + assert energy / timedelta(hours=6.2) == power + def test_frequency() -> None: """Test the frequency class.""" @@ -484,6 +524,10 @@ def test_neg() -> None: assert -power == Power.from_watts(-1000.0) assert -(-power) == power + reactive_power = ReactivePower.from_volt_amperes_reactive(1000.0) + assert -reactive_power == ReactivePower.from_volt_amperes_reactive(-1000.0) + assert -(-reactive_power) == reactive_power + voltage = Voltage.from_volts(230.0) assert -voltage == Voltage.from_volts(-230.0) assert -(-voltage) == voltage @@ -510,6 +554,10 @@ def test_pos() -> None: assert +power == power assert +(+power) == power + reactive_power = ReactivePower.from_volt_amperes_reactive(1000.0) + assert +reactive_power == reactive_power + assert +(+reactive_power) == reactive_power + voltage = Voltage.from_volts(230.0) assert +voltage == voltage assert +(+voltage) == voltage @@ -536,6 +584,9 @@ def test_inf() -> None: assert f"{Power.from_watts(float('inf'))}" == "inf W" assert f"{Power.from_watts(float('-inf'))}" == "-inf W" + assert f"{ReactivePower.from_volt_amperes_reactive(float('inf'))}" == "inf VAR" + assert f"{ReactivePower.from_volt_amperes_reactive(float('-inf'))}" == "-inf VAR" + assert f"{Voltage.from_volts(float('inf'))}" == "inf V" assert f"{Voltage.from_volts(float('-inf'))}" == "-inf V" @@ -555,6 +606,7 @@ def test_inf() -> None: def test_nan() -> None: """Test proper formating when using nan in quantities.""" assert f"{Power.from_watts(float('nan'))}" == "nan W" + assert f"{ReactivePower.from_volt_amperes_reactive(float('nan'))}" == "nan VAR" assert f"{Voltage.from_volts(float('nan'))}" == "nan V" assert f"{Current.from_amperes(float('nan'))}" == "nan A" assert f"{Energy.from_watt_hours(float('nan'))}" == "nan Wh" @@ -568,6 +620,10 @@ def test_abs() -> None: assert abs(power) == Power.from_watts(1000.0) assert abs(-power) == Power.from_watts(1000.0) + reactive_power = ReactivePower.from_volt_amperes_reactive(1000.0) + assert abs(reactive_power) == ReactivePower.from_volt_amperes_reactive(1000.0) + assert abs(-reactive_power) == ReactivePower.from_volt_amperes_reactive(1000.0) + voltage = Voltage.from_volts(230.0) assert abs(voltage) == Voltage.from_volts(230.0) assert abs(-voltage) == Voltage.from_volts(230.0) @@ -765,6 +821,7 @@ def test_quantity_divided_by_self( Energy.from_kilowatt_hours(500.0), Frequency.from_hertz(50), Power.from_watts(1000.0), + ReactivePower.from_volt_amperes_reactive(1000.0), Quantity(30.0), Temperature.from_celsius(30), Voltage.from_volts(230.0), @@ -789,6 +846,7 @@ def test_invalid_current_divisions(divisor: Quantity) -> None: Quantity(30.0), Temperature.from_celsius(30), Voltage.from_volts(230.0), + ApparentPower.from_volt_amperes(1000.0), ], ids=lambda q: q.__class__.__name__, ) @@ -808,6 +866,7 @@ def test_invalid_energy_divisions(divisor: Quantity) -> None: Current.from_amperes(2), Energy.from_kilowatt_hours(500.0), Power.from_watts(1000.0), + ReactivePower.from_volt_amperes_reactive(1000.0), Quantity(30.0), Temperature.from_celsius(30), Voltage.from_volts(230.0), @@ -831,6 +890,7 @@ def test_invalid_frequency_divisions(divisor: Quantity) -> None: Energy.from_kilowatt_hours(500.0), Frequency.from_hertz(50), Power.from_watts(1000.0), + ReactivePower.from_volt_amperes_reactive(1000.0), Quantity(30.0), Temperature.from_celsius(30), Voltage.from_volts(230.0), @@ -854,6 +914,7 @@ def test_invalid_percentage_divisions(divisor: Quantity) -> None: Frequency.from_hertz(50), Quantity(30.0), Temperature.from_celsius(30), + ReactivePower.from_volt_amperes_reactive(1000.0), ], ids=lambda q: q.__class__.__name__, ) @@ -874,6 +935,7 @@ def test_invalid_power_divisions(divisor: Quantity) -> None: Energy.from_kilowatt_hours(500.0), Frequency.from_hertz(50), Power.from_watts(1000.0), + ReactivePower.from_volt_amperes_reactive(1000.0), Temperature.from_celsius(30), Voltage.from_volts(230.0), ], @@ -896,6 +958,7 @@ def test_invalid_quantity_divisions(divisor: Quantity) -> None: Energy.from_kilowatt_hours(500.0), Frequency.from_hertz(50), Power.from_watts(1000.0), + ReactivePower.from_volt_amperes_reactive(1000.0), Quantity(30.0), Voltage.from_volts(230.0), ], @@ -918,6 +981,7 @@ def test_invalid_temperature_divisions(divisor: Quantity) -> None: Energy.from_kilowatt_hours(500.0), Frequency.from_hertz(50), Power.from_watts(1000.0), + ReactivePower.from_volt_amperes_reactive(1000.0), Quantity(30.0), Temperature.from_celsius(30), ], @@ -936,7 +1000,9 @@ def test_invalid_voltage_divisions(divisor: Quantity) -> None: # We can't use _QUANTITY_TYPES here, because it will break the tests, as hypothesis # will generate more values, some of which are unsupported by the quantities. See the # test comment for more details. -@pytest.mark.parametrize("quantity_type", [Power, Voltage, Current, Energy, Frequency]) +@pytest.mark.parametrize( + "quantity_type", [Power, Voltage, Current, Energy, Frequency, ReactivePower] +) @pytest.mark.parametrize("exponent", [0, 3, 6, 9]) @hypothesis.settings( max_examples=1000 From 349f38059318ec2f9d65f6c95b48a1b99af230a6 Mon Sep 17 00:00:00 2001 From: Elzbieta Kotulska Date: Sun, 13 Oct 2024 10:14:33 +0200 Subject: [PATCH 2/5] Add ApparentPower quantity Signed-off-by: Elzbieta Kotulska --- src/frequenz/quantities/__init__.py | 2 + src/frequenz/quantities/_apparent_power.py | 251 +++++++++++++++++++++ src/frequenz/quantities/_energy.py | 23 +- tests/test_quantities.py | 59 ++++- 4 files changed, 329 insertions(+), 6 deletions(-) create mode 100644 src/frequenz/quantities/_apparent_power.py diff --git a/src/frequenz/quantities/__init__.py b/src/frequenz/quantities/__init__.py index 39945ef..f282f0d 100644 --- a/src/frequenz/quantities/__init__.py +++ b/src/frequenz/quantities/__init__.py @@ -77,6 +77,7 @@ """ +from ._apparent_power import ApparentPower from ._current import Current from ._energy import Energy from ._frequency import Frequency @@ -95,6 +96,7 @@ "Power", "Quantity", "ReactivePower", + "ApparentPower", "Temperature", "Voltage", ] diff --git a/src/frequenz/quantities/_apparent_power.py b/src/frequenz/quantities/_apparent_power.py new file mode 100644 index 0000000..b9f41b1 --- /dev/null +++ b/src/frequenz/quantities/_apparent_power.py @@ -0,0 +1,251 @@ +# License: MIT +# Copyright © 2024 Frequenz Energy-as-a-Service GmbH + +"""Types for holding apparent power quantities with units.""" + +from __future__ import annotations + +from datetime import timedelta +from typing import TYPE_CHECKING, Self, overload + +from ._quantity import NoDefaultConstructible, Quantity + +if TYPE_CHECKING: + from ._current import Current + from ._energy import Energy + from ._percentage import Percentage + from ._voltage import Voltage + + +class ApparentPower( + Quantity, + metaclass=NoDefaultConstructible, + exponent_unit_map={ + -3: "mVA", + 0: "VA", + 3: "kVA", + 6: "MVA", + }, +): + """A apparent power quantity. + + Objects of this type are wrappers around `float` values and are immutable. + + The constructors accept a single `float` value, the `as_*()` methods return a + `float` value, and each of the arithmetic operators supported by this type are + actually implemented using floating-point arithmetic. + + So all considerations about floating-point arithmetic apply to this type as well. + """ + + @classmethod + def from_volt_amperes(cls, value: float) -> Self: + """Initialize a new apparent power quantity. + + Args: + value: The apparent power in volt-amperes (VA). + + Returns: + A new apparent power quantity. + """ + return cls._new(value) + + @classmethod + def from_milli_volt_amperes(cls, mva: float) -> Self: + """Initialize a new apparent power quantity. + + Args: + mva: The apparent power in millivolt-amperes (mVA). + + Returns: + A new apparent power quantity. + """ + return cls._new(mva, exponent=-3) + + @classmethod + def from_kilo_volt_amperes(cls, kva: float) -> Self: + """Initialize a new apparent power quantity. + + Args: + kva: The apparent power in kilovolt-amperes (kVA). + + Returns: + A new apparent power quantity. + """ + return cls._new(kva, exponent=3) + + @classmethod + def from_mega_volt_amperes(cls, mva: float) -> Self: + """Initialize a new apparent power quantity. + + Args: + mva: The apparent power in megavolt-amperes (MVA). + + Returns: + A new apparent power quantity. + """ + return cls._new(mva, exponent=6) + + def as_volt_amperes(self) -> float: + """Return the apparent power in volt-amperes (VA). + + Returns: + The apparent power in volt-amperes (VA). + """ + return self._base_value + + def as_milli_volt_amperes(self) -> float: + """Return the apparent power in millivolt-amperes (mVA). + + Returns: + The apparent power in millivolt-amperes (mVA). + """ + return self._base_value * 1e3 + + def as_kilo_volt_amperes(self) -> float: + """Return the apparent power in kilovolt-amperes (kVA). + + Returns: + The apparent power in kilovolt-amperes (kVA). + """ + return self._base_value / 1e3 + + def as_mega_volt_amperes(self) -> float: + """Return the apparent power in megavolt-amperes (MVA). + + Returns: + The apparent power in megavolt-amperes (MVA). + """ + return self._base_value / 1e6 + + # We need the ignore here because otherwise mypy will give this error: + # > Overloaded operator methods can't have wider argument types in overrides + # The problem seems to be when the other type implements an **incompatible** + # __rmul__ method, which is not the case here, so we should be safe. + # Please see this example: + # https://github.com/python/mypy/blob/c26f1297d4f19d2d1124a30efc97caebb8c28616/test-data/unit/check-overloading.test#L4738C1-L4769C55 + # And a discussion in a mypy issue here: + # https://github.com/python/mypy/issues/4985#issuecomment-389692396 + @overload # type: ignore[override] + def __mul__(self, scalar: float, /) -> Self: + """Scale this power by a scalar. + + Args: + scalar: The scalar by which to scale this power. + + Returns: + The scaled power. + """ + + @overload + def __mul__(self, percent: Percentage, /) -> Self: + """Scale this power by a percentage. + + Args: + percent: The percentage by which to scale this power. + + Returns: + The scaled power. + """ + + @overload + def __mul__(self, other: timedelta, /) -> Energy: + """Return an energy from multiplying this power by the given duration. + + Args: + other: The duration to multiply by. + + Returns: + The calculated energy. + """ + + def __mul__(self, other: float | Percentage | timedelta, /) -> Self | Energy: + """Return a power or energy from multiplying this power by the given value. + + Args: + other: The scalar, percentage or duration to multiply by. + + Returns: + A power or energy. + """ + from ._energy import Energy # pylint: disable=import-outside-toplevel + from ._percentage import Percentage # pylint: disable=import-outside-toplevel + + match other: + case float() | Percentage(): + return super().__mul__(other) + case timedelta(): + return Energy._new(self._base_value * other.total_seconds() / 3600.0) + case _: + return NotImplemented + + # See the comment for ApparentPower.__mul__ for why we need the ignore here. + @overload # type: ignore[override] + def __truediv__(self, other: float, /) -> Self: + """Divide this power by a scalar. + + Args: + other: The scalar to divide this power by. + + Returns: + The divided power. + """ + + @overload + def __truediv__(self, other: Self, /) -> float: + """Return the ratio of this power to another. + + Args: + other: The other power. + + Returns: + The ratio of this power to another. + """ + + @overload + def __truediv__(self, current: Current, /) -> Voltage: + """Return a voltage from dividing this power by the given current. + + Args: + current: The current to divide by. + + Returns: + A voltage from dividing this power by the a current. + """ + + @overload + def __truediv__(self, voltage: Voltage, /) -> Current: + """Return a current from dividing this power by the given voltage. + + Args: + voltage: The voltage to divide by. + + Returns: + A current from dividing this power by a voltage. + """ + + def __truediv__( + self, other: float | Self | Current | Voltage, / + ) -> Self | float | Voltage | Current: + """Return a current or voltage from dividing this power by the given value. + + Args: + other: The scalar, power, current or voltage to divide by. + + Returns: + A current or voltage from dividing this power by the given value. + """ + from ._current import Current # pylint: disable=import-outside-toplevel + from ._voltage import Voltage # pylint: disable=import-outside-toplevel + + match other: + case float(): + return super().__truediv__(other) + case ApparentPower(): + return self._base_value / other._base_value + case Current(): + return Voltage._new(self._base_value / other._base_value) + case Voltage(): + return Current._new(self._base_value / other._base_value) + case _: + return NotImplemented diff --git a/src/frequenz/quantities/_energy.py b/src/frequenz/quantities/_energy.py index 0c6586c..72f42cc 100644 --- a/src/frequenz/quantities/_energy.py +++ b/src/frequenz/quantities/_energy.py @@ -12,6 +12,7 @@ from ._quantity import NoDefaultConstructible, Quantity if TYPE_CHECKING: + from ._apparent_power import ApparentPower from ._percentage import Percentage from ._power import Power from ._reactive_power import ReactivePower @@ -172,8 +173,19 @@ def __truediv__(self, power: ReactivePower, /) -> timedelta: A duration from dividing this energy by the given power. """ + @overload + def __truediv__(self, power: ApparentPower, /) -> timedelta: + """Return a duration from dividing this energy by the given power. + + Args: + power: The power to divide by. + + Returns: + A duration from dividing this energy by the given power. + """ + def __truediv__( - self, other: float | Self | timedelta | Power | ReactivePower, / + self, other: float | Self | timedelta | Power | ReactivePower | ApparentPower, / ) -> Self | float | Power | timedelta: """Return a power or duration from dividing this energy by the given value. @@ -183,10 +195,13 @@ def __truediv__( Returns: A power or duration from dividing this energy by the given value. """ + from ._apparent_power import ( # pylint: disable=import-outside-toplevel + ApparentPower, + ) from ._power import Power # pylint: disable=import-outside-toplevel - from ._reactive_power import ( + from ._reactive_power import ( # pylint: disable=import-outside-toplevel ReactivePower, - ) # pylint: disable=import-outside-toplevel + ) match other: case float(): @@ -195,7 +210,7 @@ def __truediv__( return self._base_value / other._base_value case timedelta(): return Power._new(self._base_value / (other.total_seconds() / 3600.0)) - case Power() | ReactivePower(): + case Power() | ReactivePower() | ApparentPower(): return timedelta( seconds=(self._base_value / other._base_value) * 3600.0 ) diff --git a/tests/test_quantities.py b/tests/test_quantities.py index fa6200b..9e2a4aa 100644 --- a/tests/test_quantities.py +++ b/tests/test_quantities.py @@ -13,6 +13,7 @@ from frequenz import quantities from frequenz.quantities import ( + ApparentPower, Current, Energy, Frequency, @@ -118,6 +119,13 @@ def test_zero() -> None: # pylint: disable=too-many-statements assert ReactivePower.zero().as_kilo_volt_amperes_reactive() == 0.0 assert ReactivePower.zero() is ReactivePower.zero() # It is a "singleton" + assert ApparentPower.from_volt_amperes(0.0) == ApparentPower.zero() + assert ApparentPower.from_kilo_volt_amperes(0.0) == ApparentPower.zero() + assert isinstance(ApparentPower.zero(), ApparentPower) + assert ApparentPower.zero().as_volt_amperes() == 0.0 + assert ApparentPower.zero().as_kilo_volt_amperes() == 0.0 + assert ApparentPower.zero() is ApparentPower.zero() # It is a "singleton" + assert Current.from_amperes(0.0) == Current.zero() assert Current.from_milliamperes(0.0) == Current.zero() assert isinstance(Current.zero(), Current) @@ -232,10 +240,13 @@ def test_string_representation() -> None: assert f"{Power.from_watts(0.000124445):.0}" == "0 W" assert f"{Energy.from_watt_hours(0.124445):.0}" == "0 Wh" assert f"{ReactivePower.from_volt_amperes_reactive(0.000124445):.0}" == "0 VAR" + assert f"{ApparentPower.from_volt_amperes(0.000124445):.0}" == "0 VA" assert f"{Power.from_watts(-0.0):.0}" == "-0 W" assert f"{Power.from_watts(0.0):.0}" == "0 W" assert f"{ReactivePower.from_volt_amperes_reactive(-0.0):.0}" == "-0 VAR" assert f"{ReactivePower.from_volt_amperes_reactive(0.0):.0}" == "0 VAR" + assert f"{ApparentPower.from_volt_amperes(-0.0):.0}" == "-0 VA" + assert f"{ApparentPower.from_volt_amperes(0.0):.0}" == "0 VA" assert f"{Voltage.from_volts(999.9999850988388)}" == "1 kV" @@ -390,6 +401,26 @@ def test_reactive_power() -> None: ReactivePower(1.0, exponent=0) +def test_apparent_power() -> None: + """Test the apparent power class.""" + power = ApparentPower.from_milli_volt_amperes(0.0000002) + assert f"{power:.9}" == "0.0000002 mVA" + power = ApparentPower.from_kilo_volt_amperes(10000000.2) + assert f"{power}" == "10000 MVA" + + power = ApparentPower.from_kilo_volt_amperes(1.2) + assert power.as_volt_amperes() == 1200.0 + assert power.as_mega_volt_amperes() == 0.0012 + assert power.as_kilo_volt_amperes() == 1.2 + assert power == ApparentPower.from_milli_volt_amperes(1200000.0) + assert power == ApparentPower.from_mega_volt_amperes(0.0012) + assert power != ApparentPower.from_volt_amperes(1000.0) + + with pytest.raises(TypeError): + # using the default constructor should raise. + ApparentPower(1.0, exponent=0) + + def test_current() -> None: """Test the current class.""" current = Current.from_milliamperes(0.0000002) @@ -468,6 +499,7 @@ def test_temperature() -> None: [ Power.from_watts(1000.0), ReactivePower.from_volt_amperes_reactive(1000.0), + ApparentPower.from_volt_amperes(1000.0), ], ids=lambda q: q.__class__.__name__, ) @@ -528,6 +560,10 @@ def test_neg() -> None: assert -reactive_power == ReactivePower.from_volt_amperes_reactive(-1000.0) assert -(-reactive_power) == reactive_power + apparent_power = ApparentPower.from_volt_amperes(1000.0) + assert -apparent_power == ApparentPower.from_volt_amperes(-1000.0) + assert -(-apparent_power) == apparent_power + voltage = Voltage.from_volts(230.0) assert -voltage == Voltage.from_volts(-230.0) assert -(-voltage) == voltage @@ -558,6 +594,10 @@ def test_pos() -> None: assert +reactive_power == reactive_power assert +(+reactive_power) == reactive_power + apparent_power = ApparentPower.from_volt_amperes(1000.0) + assert +apparent_power == apparent_power + assert +(+apparent_power) == apparent_power + voltage = Voltage.from_volts(230.0) assert +voltage == voltage assert +(+voltage) == voltage @@ -587,6 +627,9 @@ def test_inf() -> None: assert f"{ReactivePower.from_volt_amperes_reactive(float('inf'))}" == "inf VAR" assert f"{ReactivePower.from_volt_amperes_reactive(float('-inf'))}" == "-inf VAR" + assert f"{ApparentPower.from_volt_amperes(float('inf'))}" == "inf VA" + assert f"{ApparentPower.from_volt_amperes(float('-inf'))}" == "-inf VA" + assert f"{Voltage.from_volts(float('inf'))}" == "inf V" assert f"{Voltage.from_volts(float('-inf'))}" == "-inf V" @@ -607,6 +650,7 @@ def test_nan() -> None: """Test proper formating when using nan in quantities.""" assert f"{Power.from_watts(float('nan'))}" == "nan W" assert f"{ReactivePower.from_volt_amperes_reactive(float('nan'))}" == "nan VAR" + assert f"{ApparentPower.from_volt_amperes(float('nan'))}" == "nan VA" assert f"{Voltage.from_volts(float('nan'))}" == "nan V" assert f"{Current.from_amperes(float('nan'))}" == "nan A" assert f"{Energy.from_watt_hours(float('nan'))}" == "nan Wh" @@ -624,6 +668,10 @@ def test_abs() -> None: assert abs(reactive_power) == ReactivePower.from_volt_amperes_reactive(1000.0) assert abs(-reactive_power) == ReactivePower.from_volt_amperes_reactive(1000.0) + apparent_power = ApparentPower.from_volt_amperes(1000.0) + assert abs(apparent_power) == ApparentPower.from_volt_amperes(1000.0) + assert abs(-apparent_power) == ApparentPower.from_volt_amperes(1000.0) + voltage = Voltage.from_volts(230.0) assert abs(voltage) == Voltage.from_volts(230.0) assert abs(-voltage) == Voltage.from_volts(230.0) @@ -821,6 +869,7 @@ def test_quantity_divided_by_self( Energy.from_kilowatt_hours(500.0), Frequency.from_hertz(50), Power.from_watts(1000.0), + ApparentPower.from_volt_amperes(1000.0), ReactivePower.from_volt_amperes_reactive(1000.0), Quantity(30.0), Temperature.from_celsius(30), @@ -846,7 +895,6 @@ def test_invalid_current_divisions(divisor: Quantity) -> None: Quantity(30.0), Temperature.from_celsius(30), Voltage.from_volts(230.0), - ApparentPower.from_volt_amperes(1000.0), ], ids=lambda q: q.__class__.__name__, ) @@ -866,6 +914,7 @@ def test_invalid_energy_divisions(divisor: Quantity) -> None: Current.from_amperes(2), Energy.from_kilowatt_hours(500.0), Power.from_watts(1000.0), + ApparentPower.from_volt_amperes(1000.0), ReactivePower.from_volt_amperes_reactive(1000.0), Quantity(30.0), Temperature.from_celsius(30), @@ -890,6 +939,7 @@ def test_invalid_frequency_divisions(divisor: Quantity) -> None: Energy.from_kilowatt_hours(500.0), Frequency.from_hertz(50), Power.from_watts(1000.0), + ApparentPower.from_volt_amperes(1000.0), ReactivePower.from_volt_amperes_reactive(1000.0), Quantity(30.0), Temperature.from_celsius(30), @@ -915,6 +965,7 @@ def test_invalid_percentage_divisions(divisor: Quantity) -> None: Quantity(30.0), Temperature.from_celsius(30), ReactivePower.from_volt_amperes_reactive(1000.0), + ApparentPower.from_volt_amperes(1000.0), ], ids=lambda q: q.__class__.__name__, ) @@ -936,6 +987,7 @@ def test_invalid_power_divisions(divisor: Quantity) -> None: Frequency.from_hertz(50), Power.from_watts(1000.0), ReactivePower.from_volt_amperes_reactive(1000.0), + ApparentPower.from_volt_amperes(1000.0), Temperature.from_celsius(30), Voltage.from_volts(230.0), ], @@ -959,6 +1011,7 @@ def test_invalid_quantity_divisions(divisor: Quantity) -> None: Frequency.from_hertz(50), Power.from_watts(1000.0), ReactivePower.from_volt_amperes_reactive(1000.0), + ApparentPower.from_volt_amperes(1000.0), Quantity(30.0), Voltage.from_volts(230.0), ], @@ -982,6 +1035,7 @@ def test_invalid_temperature_divisions(divisor: Quantity) -> None: Frequency.from_hertz(50), Power.from_watts(1000.0), ReactivePower.from_volt_amperes_reactive(1000.0), + ApparentPower.from_volt_amperes(1000.0), Quantity(30.0), Temperature.from_celsius(30), ], @@ -1001,7 +1055,8 @@ def test_invalid_voltage_divisions(divisor: Quantity) -> None: # will generate more values, some of which are unsupported by the quantities. See the # test comment for more details. @pytest.mark.parametrize( - "quantity_type", [Power, Voltage, Current, Energy, Frequency, ReactivePower] + "quantity_type", + [Power, Voltage, Current, Energy, Frequency, ReactivePower, ApparentPower], ) @pytest.mark.parametrize("exponent", [0, 3, 6, 9]) @hypothesis.settings( From 4e6e49dbc21a6437a3183374843af7966528abea Mon Sep 17 00:00:00 2001 From: Elzbieta Kotulska Date: Mon, 14 Oct 2024 07:57:07 +0200 Subject: [PATCH 3/5] Update release notes Signed-off-by: Elzbieta Kotulska --- RELEASE_NOTES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index c3fbeea..5e747f4 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -7,3 +7,5 @@ This is the initial release, extracted from the [SDK v1.0.0rc601](https://github ## New Features - Added support for `__round__` (`round(quantity)`), `__pos__` (`+quantity`) and `__mod__` (`quantity % quantity`) operators. +- Add `ReactivePower` quantity. +- Add `ApparentPower` quantity. From 7b16aec37d538a640f4b530aca80686bb1cd4e84 Mon Sep 17 00:00:00 2001 From: Elzbieta Kotulska Date: Wed, 16 Oct 2024 13:31:34 +0200 Subject: [PATCH 4/5] Disable pylint warning in unit tests This file looks like for now. If number of quantities increase then we should consider separating this file into many quantities. Signed-off-by: Elzbieta Kotulska --- tests/test_quantities.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_quantities.py b/tests/test_quantities.py index 9e2a4aa..771c0b3 100644 --- a/tests/test_quantities.py +++ b/tests/test_quantities.py @@ -3,6 +3,7 @@ """Tests for quantity types.""" +# pylint: disable=too-many-lines import inspect from datetime import timedelta from typing import Callable From 7df70cff96753fa09aee23794e4b17f77e5c7f56 Mon Sep 17 00:00:00 2001 From: Elzbieta Kotulska Date: Wed, 16 Oct 2024 13:37:56 +0200 Subject: [PATCH 5/5] Remove the Energy interactions for ReactivePower and ApparentPower If necessary, they can be added later. Signed-off-by: Elzbieta Kotulska --- src/frequenz/quantities/_apparent_power.py | 37 ++++++---------------- src/frequenz/quantities/_energy.py | 34 ++------------------ src/frequenz/quantities/_reactive_power.py | 37 ++++++---------------- tests/test_quantities.py | 22 ++++--------- 4 files changed, 29 insertions(+), 101 deletions(-) diff --git a/src/frequenz/quantities/_apparent_power.py b/src/frequenz/quantities/_apparent_power.py index b9f41b1..e5df26a 100644 --- a/src/frequenz/quantities/_apparent_power.py +++ b/src/frequenz/quantities/_apparent_power.py @@ -5,14 +5,12 @@ from __future__ import annotations -from datetime import timedelta from typing import TYPE_CHECKING, Self, overload from ._quantity import NoDefaultConstructible, Quantity if TYPE_CHECKING: from ._current import Current - from ._energy import Energy from ._percentage import Percentage from ._voltage import Voltage @@ -118,15 +116,7 @@ def as_mega_volt_amperes(self) -> float: """ return self._base_value / 1e6 - # We need the ignore here because otherwise mypy will give this error: - # > Overloaded operator methods can't have wider argument types in overrides - # The problem seems to be when the other type implements an **incompatible** - # __rmul__ method, which is not the case here, so we should be safe. - # Please see this example: - # https://github.com/python/mypy/blob/c26f1297d4f19d2d1124a30efc97caebb8c28616/test-data/unit/check-overloading.test#L4738C1-L4769C55 - # And a discussion in a mypy issue here: - # https://github.com/python/mypy/issues/4985#issuecomment-389692396 - @overload # type: ignore[override] + @overload def __mul__(self, scalar: float, /) -> Self: """Scale this power by a scalar. @@ -148,18 +138,7 @@ def __mul__(self, percent: Percentage, /) -> Self: The scaled power. """ - @overload - def __mul__(self, other: timedelta, /) -> Energy: - """Return an energy from multiplying this power by the given duration. - - Args: - other: The duration to multiply by. - - Returns: - The calculated energy. - """ - - def __mul__(self, other: float | Percentage | timedelta, /) -> Self | Energy: + def __mul__(self, other: float | Percentage, /) -> Self: """Return a power or energy from multiplying this power by the given value. Args: @@ -168,18 +147,22 @@ def __mul__(self, other: float | Percentage | timedelta, /) -> Self | Energy: Returns: A power or energy. """ - from ._energy import Energy # pylint: disable=import-outside-toplevel from ._percentage import Percentage # pylint: disable=import-outside-toplevel match other: case float() | Percentage(): return super().__mul__(other) - case timedelta(): - return Energy._new(self._base_value * other.total_seconds() / 3600.0) case _: return NotImplemented - # See the comment for ApparentPower.__mul__ for why we need the ignore here. + # We need the ignore here because otherwise mypy will give this error: + # > Overloaded operator methods can't have wider argument types in overrides + # The problem seems to be when the other type implements an **incompatible** + # __rmul__ method, which is not the case here, so we should be safe. + # Please see this example: + # https://github.com/python/mypy/blob/c26f1297d4f19d2d1124a30efc97caebb8c28616/test-data/unit/check-overloading.test#L4738C1-L4769C55 + # And a discussion in a mypy issue here: + # https://github.com/python/mypy/issues/4985#issuecomment-389692396 @overload # type: ignore[override] def __truediv__(self, other: float, /) -> Self: """Divide this power by a scalar. diff --git a/src/frequenz/quantities/_energy.py b/src/frequenz/quantities/_energy.py index 72f42cc..a0795e9 100644 --- a/src/frequenz/quantities/_energy.py +++ b/src/frequenz/quantities/_energy.py @@ -12,10 +12,8 @@ from ._quantity import NoDefaultConstructible, Quantity if TYPE_CHECKING: - from ._apparent_power import ApparentPower from ._percentage import Percentage from ._power import Power - from ._reactive_power import ReactivePower class Energy( @@ -162,30 +160,8 @@ def __truediv__(self, power: Power, /) -> timedelta: A duration from dividing this energy by the given power. """ - @overload - def __truediv__(self, power: ReactivePower, /) -> timedelta: - """Return a duration from dividing this energy by the given power. - - Args: - power: The power to divide by. - - Returns: - A duration from dividing this energy by the given power. - """ - - @overload - def __truediv__(self, power: ApparentPower, /) -> timedelta: - """Return a duration from dividing this energy by the given power. - - Args: - power: The power to divide by. - - Returns: - A duration from dividing this energy by the given power. - """ - def __truediv__( - self, other: float | Self | timedelta | Power | ReactivePower | ApparentPower, / + self, other: float | Self | timedelta | Power, / ) -> Self | float | Power | timedelta: """Return a power or duration from dividing this energy by the given value. @@ -195,13 +171,7 @@ def __truediv__( Returns: A power or duration from dividing this energy by the given value. """ - from ._apparent_power import ( # pylint: disable=import-outside-toplevel - ApparentPower, - ) from ._power import Power # pylint: disable=import-outside-toplevel - from ._reactive_power import ( # pylint: disable=import-outside-toplevel - ReactivePower, - ) match other: case float(): @@ -210,7 +180,7 @@ def __truediv__( return self._base_value / other._base_value case timedelta(): return Power._new(self._base_value / (other.total_seconds() / 3600.0)) - case Power() | ReactivePower() | ApparentPower(): + case Power(): return timedelta( seconds=(self._base_value / other._base_value) * 3600.0 ) diff --git a/src/frequenz/quantities/_reactive_power.py b/src/frequenz/quantities/_reactive_power.py index 3f7260c..8a43f47 100644 --- a/src/frequenz/quantities/_reactive_power.py +++ b/src/frequenz/quantities/_reactive_power.py @@ -6,14 +6,12 @@ from __future__ import annotations -from datetime import timedelta from typing import TYPE_CHECKING, Self, overload from ._quantity import NoDefaultConstructible, Quantity if TYPE_CHECKING: from ._current import Current - from ._energy import Energy from ._percentage import Percentage from ._voltage import Voltage @@ -119,15 +117,7 @@ def as_mega_volt_amperes_reactive(self) -> float: """ return self._base_value / 1e6 - # We need the ignore here because otherwise mypy will give this error: - # > Overloaded operator methods can't have wider argument types in overrides - # The problem seems to be when the other type implements an **incompatible** - # __rmul__ method, which is not the case here, so we should be safe. - # Please see this example: - # https://github.com/python/mypy/blob/c26f1297d4f19d2d1124a30efc97caebb8c28616/test-data/unit/check-overloading.test#L4738C1-L4769C55 - # And a discussion in a mypy issue here: - # https://github.com/python/mypy/issues/4985#issuecomment-389692396 - @overload # type: ignore[override] + @overload def __mul__(self, scalar: float, /) -> Self: """Scale this power by a scalar. @@ -149,18 +139,7 @@ def __mul__(self, percent: Percentage, /) -> Self: The scaled power. """ - @overload - def __mul__(self, other: timedelta, /) -> Energy: - """Return an energy from multiplying this power by the given duration. - - Args: - other: The duration to multiply by. - - Returns: - The calculated energy. - """ - - def __mul__(self, other: float | Percentage | timedelta, /) -> Self | Energy: + def __mul__(self, other: float | Percentage, /) -> Self: """Return a power or energy from multiplying this power by the given value. Args: @@ -169,18 +148,22 @@ def __mul__(self, other: float | Percentage | timedelta, /) -> Self | Energy: Returns: A power or energy. """ - from ._energy import Energy # pylint: disable=import-outside-toplevel from ._percentage import Percentage # pylint: disable=import-outside-toplevel match other: case float() | Percentage(): return super().__mul__(other) - case timedelta(): - return Energy._new(self._base_value * other.total_seconds() / 3600.0) case _: return NotImplemented - # See the comment for ReactivePower.__mul__ for why we need the ignore here. + # We need the ignore here because otherwise mypy will give this error: + # > Overloaded operator methods can't have wider argument types in overrides + # The problem seems to be when the other type implements an **incompatible** + # __rmul__ method, which is not the case here, so we should be safe. + # Please see this example: + # https://github.com/python/mypy/blob/c26f1297d4f19d2d1124a30efc97caebb8c28616/test-data/unit/check-overloading.test#L4738C1-L4769C55 + # And a discussion in a mypy issue here: + # https://github.com/python/mypy/issues/4985#issuecomment-389692396 @overload # type: ignore[override] def __truediv__(self, other: float, /) -> Self: """Divide this power by a scalar. diff --git a/tests/test_quantities.py b/tests/test_quantities.py index 771c0b3..f316e04 100644 --- a/tests/test_quantities.py +++ b/tests/test_quantities.py @@ -495,31 +495,21 @@ def test_temperature() -> None: Temperature(1.0, exponent=0) -@pytest.mark.parametrize( - "power", - [ - Power.from_watts(1000.0), - ReactivePower.from_volt_amperes_reactive(1000.0), - ApparentPower.from_volt_amperes(1000.0), - ], - ids=lambda q: q.__class__.__name__, -) -def test_quantity_compositions(power: Power | ReactivePower | ApparentPower) -> None: +def test_quantity_compositions() -> None: """Test the composition of quantities.""" + power = Power.from_watts(1000.0) voltage = Voltage.from_volts(230.0) current = Current.from_amperes(4.3478260869565215) energy = Energy.from_kilowatt_hours(6.2) assert power / voltage == current assert power / current == voltage + assert power == voltage * current + assert power == current * voltage assert energy / power == timedelta(hours=6.2) assert energy == power * timedelta(hours=6.2) - - if isinstance(power, Power): - assert power == voltage * current - assert power == current * voltage - assert energy / timedelta(hours=6.2) == power + assert energy / timedelta(hours=6.2) == power def test_frequency() -> None: @@ -896,6 +886,8 @@ def test_invalid_current_divisions(divisor: Quantity) -> None: Quantity(30.0), Temperature.from_celsius(30), Voltage.from_volts(230.0), + ReactivePower.from_volt_amperes_reactive(1000.0), + ApparentPower.from_volt_amperes(1000.0), ], ids=lambda q: q.__class__.__name__, )