diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 91e1059f1..2b413800f 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -6,7 +6,11 @@ ## Upgrading - +- Replace `Quantity` and its sub-classes (`Power`, `Current`, etc.) in the `frequenz.sdk.timeseries` module with the external +[`frequenz-quantities`](https://pypi.org/project/frequenz-quantities/) package. Please add the new library as a dependency +and adapt your imports if you are using these types. +- The `QuantityT` has been moved to the `frequenz.sdk.timeseries._base_types` module. +- The `QuantityT` doesn't include itself (`QuantityT`) anymore. ## New Features diff --git a/benchmarks/power_distribution/power_distributor.py b/benchmarks/power_distribution/power_distributor.py index dfbd1459b..6dccaf6fc 100644 --- a/benchmarks/power_distribution/power_distributor.py +++ b/benchmarks/power_distribution/power_distributor.py @@ -13,6 +13,7 @@ from frequenz.channels import Broadcast from frequenz.client.microgrid import Component, ComponentCategory +from frequenz.quantities import Power from frequenz.sdk import microgrid from frequenz.sdk.actor import ResamplerConfig @@ -27,7 +28,6 @@ Result, Success, ) -from frequenz.sdk.timeseries._quantities import Power HOST = "microgrid.sandbox.api.frequenz.io" PORT = 62060 diff --git a/benchmarks/timeseries/periodic_feature_extractor.py b/benchmarks/timeseries/periodic_feature_extractor.py index 5add994fa..9a5bc765e 100644 --- a/benchmarks/timeseries/periodic_feature_extractor.py +++ b/benchmarks/timeseries/periodic_feature_extractor.py @@ -20,11 +20,11 @@ import numpy as np from frequenz.channels import Broadcast +from frequenz.quantities import Quantity from numpy.random import default_rng from numpy.typing import NDArray from frequenz.sdk.timeseries import MovingWindow, PeriodicFeatureExtractor, Sample -from frequenz.sdk.timeseries._quantities import Quantity @contextlib.asynccontextmanager diff --git a/benchmarks/timeseries/resampling.py b/benchmarks/timeseries/resampling.py index a31449150..53c70aa60 100644 --- a/benchmarks/timeseries/resampling.py +++ b/benchmarks/timeseries/resampling.py @@ -7,8 +7,9 @@ from datetime import datetime, timedelta, timezone from timeit import timeit +from frequenz.quantities import Quantity + from frequenz.sdk.timeseries import Sample -from frequenz.sdk.timeseries._quantities import Quantity from frequenz.sdk.timeseries._resampling import ( ResamplerConfig, SourceProperties, diff --git a/benchmarks/timeseries/ringbuffer_memusage.py b/benchmarks/timeseries/ringbuffer_memusage.py index 10cb4866f..c34c30dc9 100644 --- a/benchmarks/timeseries/ringbuffer_memusage.py +++ b/benchmarks/timeseries/ringbuffer_memusage.py @@ -9,9 +9,9 @@ from datetime import datetime, timedelta, timezone import numpy as np +from frequenz.quantities import Quantity from frequenz.sdk.timeseries import Sample -from frequenz.sdk.timeseries._quantities import Quantity from frequenz.sdk.timeseries._ringbuffer import OrderedRingBuffer FIVE_MINUTES = timedelta(minutes=5) diff --git a/benchmarks/timeseries/ringbuffer_serialization.py b/benchmarks/timeseries/ringbuffer_serialization.py index 47a20dc03..38385bf1a 100644 --- a/benchmarks/timeseries/ringbuffer_serialization.py +++ b/benchmarks/timeseries/ringbuffer_serialization.py @@ -11,10 +11,10 @@ from typing import Any import numpy as np +from frequenz.quantities import Quantity import frequenz.sdk.timeseries._ringbuffer as rb from frequenz.sdk.timeseries import Sample -from frequenz.sdk.timeseries._quantities import Quantity FILE_NAME = "ringbuffer.pkl" FIVE_MINUTES = timedelta(minutes=5) diff --git a/pyproject.toml b/pyproject.toml index 09fc14aec..8284e9835 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ dependencies = [ # (plugins.mkdocstrings.handlers.python.import) "frequenz-client-microgrid >= 0.5.1, < 0.6.0", "frequenz-channels >= 1.2.0, < 2.0.0", + "frequenz-quantities == 1.0.0rc1", "networkx >= 2.8, < 4", "numpy >= 1.26.4, < 2", "typing_extensions >= 4.6.1, < 5", diff --git a/src/frequenz/sdk/microgrid/_data_sourcing/microgrid_api_source.py b/src/frequenz/sdk/microgrid/_data_sourcing/microgrid_api_source.py index 34486cfcc..b2d5c29ff 100644 --- a/src/frequenz/sdk/microgrid/_data_sourcing/microgrid_api_source.py +++ b/src/frequenz/sdk/microgrid/_data_sourcing/microgrid_api_source.py @@ -17,12 +17,12 @@ InverterData, MeterData, ) +from frequenz.quantities import Quantity from ..._internal._asyncio import run_forever from ..._internal._channels import ChannelRegistry from ...microgrid import connection_manager from ...timeseries import Sample -from ...timeseries._quantities import Quantity from ._component_metric_request import ComponentMetricRequest _logger = logging.getLogger(__name__) diff --git a/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_battery_manager.py b/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_battery_manager.py index 3a473cc38..d676faa46 100644 --- a/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_battery_manager.py +++ b/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_battery_manager.py @@ -18,10 +18,10 @@ InverterData, OperationOutOfRange, ) +from frequenz.quantities import Power from typing_extensions import override from ...._internal._math import is_close_to_zero -from ....timeseries._quantities import Power from ... import connection_manager from .._component_pool_status_tracker import ComponentPoolStatusTracker from .._component_status import BatteryStatusTracker, ComponentPoolStatus diff --git a/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_ev_charger_manager/_config.py b/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_ev_charger_manager/_config.py index 7b66a9f70..f57a55c2d 100644 --- a/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_ev_charger_manager/_config.py +++ b/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_ev_charger_manager/_config.py @@ -7,7 +7,7 @@ from dataclasses import dataclass, field from datetime import timedelta -from .....timeseries import Current +from frequenz.quantities import Current @dataclass(frozen=True) diff --git a/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_ev_charger_manager/_ev_charger_manager.py b/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_ev_charger_manager/_ev_charger_manager.py index 62a33c37a..19ad9b146 100644 --- a/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_ev_charger_manager/_ev_charger_manager.py +++ b/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_ev_charger_manager/_ev_charger_manager.py @@ -22,11 +22,12 @@ ComponentCategory, EVChargerData, ) +from frequenz.quantities import Power, Voltage from typing_extensions import override from ....._internal._asyncio import run_forever from ....._internal._math import is_close_to_zero -from .....timeseries import Power, Sample3Phase, Voltage +from .....timeseries import Sample3Phase from .... import _data_pipeline, connection_manager from ..._component_pool_status_tracker import ComponentPoolStatusTracker from ..._component_status import ComponentPoolStatus, EVChargerStatusTracker diff --git a/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_ev_charger_manager/_states.py b/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_ev_charger_manager/_states.py index c698bbaa2..44b62fe65 100644 --- a/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_ev_charger_manager/_states.py +++ b/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_ev_charger_manager/_states.py @@ -9,8 +9,7 @@ from typing import Iterable from frequenz.client.microgrid import EVChargerData - -from .....timeseries import Power +from frequenz.quantities import Power @dataclass diff --git a/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_pv_inverter_manager/_pv_inverter_manager.py b/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_pv_inverter_manager/_pv_inverter_manager.py index 709cbb896..53ddc8495 100644 --- a/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_pv_inverter_manager/_pv_inverter_manager.py +++ b/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_pv_inverter_manager/_pv_inverter_manager.py @@ -15,10 +15,10 @@ InverterData, InverterType, ) +from frequenz.quantities import Power from typing_extensions import override from ....._internal._math import is_close_to_zero -from .....timeseries import Power from .... import connection_manager from ..._component_pool_status_tracker import ComponentPoolStatusTracker from ..._component_status import ComponentPoolStatus, PVInverterStatusTracker diff --git a/src/frequenz/sdk/microgrid/_power_distributing/request.py b/src/frequenz/sdk/microgrid/_power_distributing/request.py index ccbd3d46e..e7f3081c7 100644 --- a/src/frequenz/sdk/microgrid/_power_distributing/request.py +++ b/src/frequenz/sdk/microgrid/_power_distributing/request.py @@ -6,7 +6,7 @@ import dataclasses from collections import abc -from ...timeseries._quantities import Power +from frequenz.quantities import Power @dataclasses.dataclass diff --git a/src/frequenz/sdk/microgrid/_power_distributing/result.py b/src/frequenz/sdk/microgrid/_power_distributing/result.py index 04f2db140..9a9fe1c8b 100644 --- a/src/frequenz/sdk/microgrid/_power_distributing/result.py +++ b/src/frequenz/sdk/microgrid/_power_distributing/result.py @@ -7,7 +7,7 @@ import dataclasses from collections import abc -from frequenz.sdk.timeseries._quantities import Power +from frequenz.quantities import Power from .request import Request @@ -117,7 +117,7 @@ class OutOfBounds(_BaseResultMixin): ) from frequenz.sdk.actor.power_distributing.request import Request from frequenz.sdk.actor.power_distributing.result import PowerBounds - from frequenz.sdk.timeseries._quantities import Power + from frequenz.quantities import Power def handle_power_request_result(result: Result) -> None: match result: diff --git a/src/frequenz/sdk/microgrid/_power_managing/_base_classes.py b/src/frequenz/sdk/microgrid/_power_managing/_base_classes.py index 46b328903..f4f2ed5ae 100644 --- a/src/frequenz/sdk/microgrid/_power_managing/_base_classes.py +++ b/src/frequenz/sdk/microgrid/_power_managing/_base_classes.py @@ -10,8 +10,9 @@ import enum import typing +from frequenz.quantities import Power + from ... import timeseries -from ...timeseries import Power from . import _bounds if typing.TYPE_CHECKING: diff --git a/src/frequenz/sdk/microgrid/_power_managing/_bounds.py b/src/frequenz/sdk/microgrid/_power_managing/_bounds.py index 20b9765e3..267aae9cb 100644 --- a/src/frequenz/sdk/microgrid/_power_managing/_bounds.py +++ b/src/frequenz/sdk/microgrid/_power_managing/_bounds.py @@ -3,7 +3,9 @@ """Utilities for checking and clamping bounds and power values to exclusion bounds.""" -from ...timeseries import Bounds, Power +from frequenz.quantities import Power + +from ...timeseries import Bounds def check_exclusion_bounds_overlap( diff --git a/src/frequenz/sdk/microgrid/_power_managing/_matryoshka.py b/src/frequenz/sdk/microgrid/_power_managing/_matryoshka.py index 006d451d7..af3f0af3b 100644 --- a/src/frequenz/sdk/microgrid/_power_managing/_matryoshka.py +++ b/src/frequenz/sdk/microgrid/_power_managing/_matryoshka.py @@ -23,10 +23,10 @@ import typing from datetime import timedelta +from frequenz.quantities import Power from typing_extensions import override from ... import timeseries -from ...timeseries import Power from . import _bounds from ._base_classes import BaseAlgorithm, Proposal, _Report diff --git a/src/frequenz/sdk/microgrid/_power_managing/_power_managing_actor.py b/src/frequenz/sdk/microgrid/_power_managing/_power_managing_actor.py index ed7f01c45..2211f1a3a 100644 --- a/src/frequenz/sdk/microgrid/_power_managing/_power_managing_actor.py +++ b/src/frequenz/sdk/microgrid/_power_managing/_power_managing_actor.py @@ -13,12 +13,12 @@ from frequenz.channels import Receiver, Sender, select, selected_from from frequenz.channels.timer import SkipMissedAndDrift, Timer from frequenz.client.microgrid import ComponentCategory, ComponentType, InverterType +from frequenz.quantities import Power from typing_extensions import override from ..._internal._asyncio import run_forever from ..._internal._channels import ChannelRegistry from ...actor import Actor -from ...timeseries import Power from ...timeseries._base_types import Bounds, SystemBounds from .. import _data_pipeline, _power_distributing from ._base_classes import Algorithm, BaseAlgorithm, Proposal, ReportRequest, _Report diff --git a/src/frequenz/sdk/microgrid/_resampling.py b/src/frequenz/sdk/microgrid/_resampling.py index d9c5f9e2c..fa403d53c 100644 --- a/src/frequenz/sdk/microgrid/_resampling.py +++ b/src/frequenz/sdk/microgrid/_resampling.py @@ -9,12 +9,12 @@ import logging from frequenz.channels import Receiver, Sender +from frequenz.quantities import Quantity from .._internal._asyncio import cancel_and_await from .._internal._channels import ChannelRegistry from ..actor import Actor from ..timeseries import Sample -from ..timeseries._quantities import Quantity from ..timeseries._resampling import Resampler, ResamplerConfig, ResamplingError from ._data_sourcing import ComponentMetricRequest diff --git a/src/frequenz/sdk/timeseries/__init__.py b/src/frequenz/sdk/timeseries/__init__.py index 43afd08ce..99d30c746 100644 --- a/src/frequenz/sdk/timeseries/__init__.py +++ b/src/frequenz/sdk/timeseries/__init__.py @@ -40,16 +40,6 @@ from ._fuse import Fuse from ._moving_window import MovingWindow from ._periodic_feature_extractor import PeriodicFeatureExtractor -from ._quantities import ( - Current, - Energy, - Frequency, - Percentage, - Power, - Quantity, - Temperature, - Voltage, -) from ._resampling import ResamplerConfig __all__ = [ @@ -62,15 +52,4 @@ "Sample", "Sample3Phase", "UNIX_EPOCH", - # - # Quantities - # - "Quantity", - "Current", - "Energy", - "Power", - "Temperature", - "Voltage", - "Frequency", - "Percentage", ] diff --git a/src/frequenz/sdk/timeseries/_base_types.py b/src/frequenz/sdk/timeseries/_base_types.py index f6732fb27..eff275fa1 100644 --- a/src/frequenz/sdk/timeseries/_base_types.py +++ b/src/frequenz/sdk/timeseries/_base_types.py @@ -10,11 +10,14 @@ from datetime import datetime, timezone from typing import Any, Generic, Protocol, Self, TypeVar, cast, overload -from ._quantities import Power, QuantityT +from frequenz.quantities import Power, Quantity UNIX_EPOCH = datetime.fromtimestamp(0.0, tz=timezone.utc) """The UNIX epoch (in UTC).""" +QuantityT = TypeVar("QuantityT", bound=Quantity) +"""Type variable for representing various quantity types.""" + @dataclass(frozen=True, order=True) class Sample(Generic[QuantityT]): diff --git a/src/frequenz/sdk/timeseries/_fuse.py b/src/frequenz/sdk/timeseries/_fuse.py index 6fb495db6..43f77b0c3 100644 --- a/src/frequenz/sdk/timeseries/_fuse.py +++ b/src/frequenz/sdk/timeseries/_fuse.py @@ -5,11 +5,8 @@ from __future__ import annotations from dataclasses import dataclass -from typing import TYPE_CHECKING -if TYPE_CHECKING: - # Break circular import - from . import Current +from frequenz.quantities import Current @dataclass(frozen=True) diff --git a/src/frequenz/sdk/timeseries/_grid_frequency.py b/src/frequenz/sdk/timeseries/_grid_frequency.py index f6cce97f6..e645f4f69 100644 --- a/src/frequenz/sdk/timeseries/_grid_frequency.py +++ b/src/frequenz/sdk/timeseries/_grid_frequency.py @@ -10,12 +10,12 @@ from frequenz.channels import Receiver, Sender from frequenz.client.microgrid import Component, ComponentCategory, ComponentMetricId +from frequenz.quantities import Frequency, Quantity from .._internal._channels import ChannelRegistry from ..microgrid import connection_manager from ..microgrid._data_sourcing import ComponentMetricRequest from ..timeseries._base_types import Sample -from ..timeseries._quantities import Frequency, Quantity _logger = logging.getLogger(__name__) diff --git a/src/frequenz/sdk/timeseries/_moving_window.py b/src/frequenz/sdk/timeseries/_moving_window.py index 79e8bd040..c1cefb9de 100644 --- a/src/frequenz/sdk/timeseries/_moving_window.py +++ b/src/frequenz/sdk/timeseries/_moving_window.py @@ -13,11 +13,11 @@ import numpy as np from frequenz.channels import Broadcast, Receiver, Sender +from frequenz.quantities import Quantity from numpy.typing import ArrayLike from ..actor._background_service import BackgroundService from ._base_types import UNIX_EPOCH, Sample -from ._quantities import Quantity from ._resampling import Resampler, ResamplerConfig from ._ringbuffer import OrderedRingBuffer diff --git a/src/frequenz/sdk/timeseries/_quantities.py b/src/frequenz/sdk/timeseries/_quantities.py deleted file mode 100644 index 1e824515f..000000000 --- a/src/frequenz/sdk/timeseries/_quantities.py +++ /dev/null @@ -1,1341 +0,0 @@ -# License: MIT -# Copyright © 2022 Frequenz Energy-as-a-Service GmbH - -"""Types for holding quantities with units.""" - -# pylint: disable=too-many-lines - -from __future__ import annotations - -import math -from datetime import timedelta -from typing import Any, NoReturn, Self, TypeVar, overload - -QuantityT = TypeVar( - "QuantityT", - "Quantity", - "Power", - "Current", - "Voltage", - "Energy", - "Frequency", - "Percentage", - "Temperature", -) -"""Type variable for representing various quantity types.""" - - -class Quantity: - """A quantity with a unit. - - Quantities try to behave like float and are also immutable. - """ - - _base_value: float - """The value of this quantity in the base unit.""" - - _exponent_unit_map: dict[int, str] | None = None - """A mapping from the exponent of the base unit to the unit symbol. - - If None, this quantity has no unit. None is possible only when using the base - class. Sub-classes must define this. - """ - - def __init__(self, value: float, exponent: int = 0) -> None: - """Initialize a new quantity. - - Args: - value: The value of this quantity in a given exponent of the base unit. - exponent: The exponent of the base unit the given value is in. - """ - self._base_value = value * 10.0**exponent - - @classmethod - def _new(cls, value: float, *, exponent: int = 0) -> Self: - """Instantiate a new quantity subclass instance. - - Args: - value: The value of this quantity in a given exponent of the base unit. - exponent: The exponent of the base unit the given value is in. - - Returns: - A new quantity subclass instance. - """ - self = cls.__new__(cls) - self._base_value = value * 10.0**exponent - return self - - def __init_subclass__(cls, exponent_unit_map: dict[int, str]) -> None: - """Initialize a new subclass of Quantity. - - Args: - exponent_unit_map: A mapping from the exponent of the base unit to the unit - symbol. - - Raises: - TypeError: If the given exponent_unit_map is not a dict. - ValueError: If the given exponent_unit_map does not contain a base unit - (exponent 0). - """ - if 0 not in exponent_unit_map: - raise ValueError("Expected a base unit for the type (for exponent 0)") - cls._exponent_unit_map = exponent_unit_map - super().__init_subclass__() - - _zero_cache: dict[type, Quantity] = {} - """Cache for zero singletons. - - This is a workaround for mypy getting confused when using @functools.cache and - @classmethod combined with returning Self. It believes the resulting type of this - method is Self and complains that members of the actual class don't exist in Self, - so we need to implement the cache ourselves. - """ - - @classmethod - def zero(cls) -> Self: - """Return a quantity with value 0.0. - - Returns: - A quantity with value 0.0. - """ - _zero = cls._zero_cache.get(cls, None) - if _zero is None: - _zero = cls.__new__(cls) - _zero._base_value = 0.0 - cls._zero_cache[cls] = _zero - assert isinstance(_zero, cls) - return _zero - - @classmethod - def from_string(cls, string: str) -> Self: - """Return a quantity from a string representation. - - Args: - string: The string representation of the quantity. - - Returns: - A quantity object with the value given in the string. - - Raises: - ValueError: If the string does not match the expected format. - - """ - split_string = string.split(" ") - - if len(split_string) != 2: - raise ValueError( - f"Expected a string of the form 'value unit', got {string}" - ) - - assert cls._exponent_unit_map is not None - exp_map = cls._exponent_unit_map - - for exponent, unit in exp_map.items(): - if unit == split_string[1]: - instance = cls.__new__(cls) - try: - instance._base_value = float(split_string[0]) * 10**exponent - except ValueError as error: - raise ValueError(f"Failed to parse string '{string}'.") from error - - return instance - - raise ValueError(f"Unknown unit {split_string[1]}") - - @property - def base_value(self) -> float: - """Return the value of this quantity in the base unit. - - Returns: - The value of this quantity in the base unit. - """ - return self._base_value - - @property - def base_unit(self) -> str | None: - """Return the base unit of this quantity. - - None if this quantity has no unit. - - Returns: - The base unit of this quantity. - """ - if not self._exponent_unit_map: - return None - return self._exponent_unit_map[0] - - def isnan(self) -> bool: - """Return whether this quantity is NaN. - - Returns: - Whether this quantity is NaN. - """ - return math.isnan(self._base_value) - - def isinf(self) -> bool: - """Return whether this quantity is infinite. - - Returns: - Whether this quantity is infinite. - """ - return math.isinf(self._base_value) - - def isclose(self, other: Self, rel_tol: float = 1e-9, abs_tol: float = 0.0) -> bool: - """Return whether this quantity is close to another. - - Args: - other: The quantity to compare to. - rel_tol: The relative tolerance. - abs_tol: The absolute tolerance. - - Returns: - Whether this quantity is close to another. - """ - return math.isclose( - self._base_value, - other._base_value, # pylint: disable=protected-access - rel_tol=rel_tol, - abs_tol=abs_tol, - ) - - def __repr__(self) -> str: - """Return a representation of this quantity. - - Returns: - A representation of this quantity. - """ - return f"{type(self).__name__}(value={self._base_value}, exponent=0)" - - def __str__(self) -> str: - """Return a string representation of this quantity. - - Returns: - A string representation of this quantity. - """ - return self.__format__("") - - # pylint: disable=too-many-branches - def __format__(self, __format_spec: str) -> str: - """Return a formatted string representation of this quantity. - - If specified, must be of this form: `[0].{precision}`. If a 0 is not given, the - trailing zeros will be omitted. If no precision is given, the default is 3. - - The returned string will use the unit that will result in the maximum precision, - based on the magnitude of the value. - - Example: - ```python - from frequenz.sdk.timeseries import Current - c = Current.from_amperes(0.2345) - assert f"{c:.2}" == "234.5 mA" - c = Current.from_amperes(1.2345) - assert f"{c:.2}" == "1.23 A" - c = Current.from_milliamperes(1.2345) - assert f"{c:.6}" == "1.2345 mA" - ``` - - Args: - __format_spec: The format specifier. - - Returns: - A string representation of this quantity. - - Raises: - ValueError: If the given format specifier is invalid. - """ - keep_trailing_zeros = False - if __format_spec != "": - fspec_parts = __format_spec.split(".") - if ( - len(fspec_parts) != 2 - or fspec_parts[0] not in ("", "0") - or not fspec_parts[1].isdigit() - ): - raise ValueError( - "Invalid format specifier. Must be empty or `[0].{precision}`" - ) - if fspec_parts[0] == "0": - keep_trailing_zeros = True - precision = int(fspec_parts[1]) - else: - precision = 3 - if not self._exponent_unit_map: - return f"{self._base_value:.{precision}f}" - - if math.isinf(self._base_value) or math.isnan(self._base_value): - return f"{self._base_value} {self._exponent_unit_map[0]}" - - if abs_value := abs(self._base_value): - precision_pow = 10 ** (precision) - # Prevent numbers like 999.999999 being rendered as 1000 V - # instead of 1 kV. - # This could happen because the str formatting function does - # rounding as well. - # This is an imperfect solution that works for _most_ cases. - # isclose parameters were chosen according to the observed cases - if math.isclose(abs_value, precision_pow, abs_tol=1e-4, rel_tol=0.01): - # If the value is close to the precision, round it - exponent = math.ceil(math.log10(precision_pow)) - else: - exponent = math.floor(math.log10(abs_value)) - else: - exponent = 0 - - unit_place = exponent - exponent % 3 - if unit_place < min(self._exponent_unit_map): - unit = self._exponent_unit_map[min(self._exponent_unit_map.keys())] - unit_place = min(self._exponent_unit_map) - elif unit_place > max(self._exponent_unit_map): - unit = self._exponent_unit_map[max(self._exponent_unit_map.keys())] - unit_place = max(self._exponent_unit_map) - else: - unit = self._exponent_unit_map[unit_place] - - value_str = f"{self._base_value / 10 ** unit_place:.{precision}f}" - - if value_str in ("-0", "0"): - stripped = value_str - else: - stripped = value_str.rstrip("0").rstrip(".") - - if not keep_trailing_zeros: - value_str = stripped - unit_str = unit if stripped not in ("-0", "0") else self._exponent_unit_map[0] - return f"{value_str} {unit_str}" - - def __add__(self, other: Self) -> Self: - """Return the sum of this quantity and another. - - Args: - other: The other quantity. - - Returns: - The sum of this quantity and another. - """ - if not type(other) is type(self): - return NotImplemented - summe = type(self).__new__(type(self)) - summe._base_value = self._base_value + other._base_value - return summe - - def __sub__(self, other: Self) -> Self: - """Return the difference of this quantity and another. - - Args: - other: The other quantity. - - Returns: - The difference of this quantity and another. - """ - if not type(other) is type(self): - return NotImplemented - difference = type(self).__new__(type(self)) - difference._base_value = self._base_value - other._base_value - return difference - - @overload - def __mul__(self, scalar: float, /) -> Self: - """Scale this quantity by a scalar. - - Args: - scalar: The scalar by which to scale this quantity. - - Returns: - The scaled quantity. - """ - - @overload - def __mul__(self, percent: Percentage, /) -> Self: - """Scale this quantity by a percentage. - - Args: - percent: The percentage by which to scale this quantity. - - Returns: - The scaled quantity. - """ - - def __mul__(self, value: float | Percentage, /) -> Self: - """Scale this quantity by a scalar or percentage. - - Args: - value: The scalar or percentage by which to scale this quantity. - - Returns: - The scaled quantity. - """ - match value: - case float(): - return type(self)._new(self._base_value * value) - case Percentage(): - return type(self)._new(self._base_value * value.as_fraction()) - case _: - return NotImplemented - - @overload - def __truediv__(self, other: float, /) -> Self: - """Divide this quantity by a scalar. - - Args: - other: The scalar or percentage to divide this quantity by. - - Returns: - The divided quantity. - """ - - @overload - def __truediv__(self, other: Self, /) -> float: - """Return the ratio of this quantity to another. - - Args: - other: The other quantity. - - Returns: - The ratio of this quantity to another. - """ - - def __truediv__(self, value: float | Self, /) -> Self | float: - """Divide this quantity by a scalar or another quantity. - - Args: - value: The scalar or quantity to divide this quantity by. - - Returns: - The divided quantity or the ratio of this quantity to another. - """ - match value: - case float(): - return type(self)._new(self._base_value / value) - case Quantity() if type(value) is type(self): - return self._base_value / value._base_value - case _: - return NotImplemented - - def __gt__(self, other: Self) -> bool: - """Return whether this quantity is greater than another. - - Args: - other: The other quantity. - - Returns: - Whether this quantity is greater than another. - """ - if not type(other) is type(self): - return NotImplemented - return self._base_value > other._base_value - - def __ge__(self, other: Self) -> bool: - """Return whether this quantity is greater than or equal to another. - - Args: - other: The other quantity. - - Returns: - Whether this quantity is greater than or equal to another. - """ - if not type(other) is type(self): - return NotImplemented - return self._base_value >= other._base_value - - def __lt__(self, other: Self) -> bool: - """Return whether this quantity is less than another. - - Args: - other: The other quantity. - - Returns: - Whether this quantity is less than another. - """ - if not type(other) is type(self): - return NotImplemented - return self._base_value < other._base_value - - def __le__(self, other: Self) -> bool: - """Return whether this quantity is less than or equal to another. - - Args: - other: The other quantity. - - Returns: - Whether this quantity is less than or equal to another. - """ - if not type(other) is type(self): - return NotImplemented - return self._base_value <= other._base_value - - def __eq__(self, other: object) -> bool: - """Return whether this quantity is equal to another. - - Args: - other: The other quantity. - - Returns: - Whether this quantity is equal to another. - """ - if not type(other) is type(self): - return NotImplemented - # The above check ensures that both quantities are the exact same type, because - # `isinstance` returns true for subclasses and superclasses. But the above check - # doesn't help mypy identify the type of other, so the below line is necessary. - assert isinstance(other, self.__class__) - return self._base_value == other._base_value - - def __neg__(self) -> Self: - """Return the negation of this quantity. - - Returns: - The negation of this quantity. - """ - negation = type(self).__new__(type(self)) - negation._base_value = -self._base_value - return negation - - def __abs__(self) -> Self: - """Return the absolute value of this quantity. - - Returns: - The absolute value of this quantity. - """ - absolute = type(self).__new__(type(self)) - absolute._base_value = abs(self._base_value) - return absolute - - -class _NoDefaultConstructible(type): - """A metaclass that disables the default constructor.""" - - def __call__(cls, *_args: Any, **_kwargs: Any) -> NoReturn: - """Raise a TypeError when the default constructor is called. - - Args: - *_args: ignored positional arguments. - **_kwargs: ignored keyword arguments. - - Raises: - TypeError: Always. - """ - raise TypeError( - "Use of default constructor NOT allowed for " - f"{cls.__module__}.{cls.__qualname__}, " - f"use one of the `{cls.__name__}.from_*()` methods instead." - ) - - -class Temperature( - Quantity, - metaclass=_NoDefaultConstructible, - exponent_unit_map={ - 0: "°C", - }, -): - """A temperature quantity (in degrees Celsius).""" - - @classmethod - def from_celsius(cls, value: float) -> Self: - """Initialize a new temperature quantity. - - Args: - value: The temperature in degrees Celsius. - - Returns: - A new temperature quantity. - """ - return cls._new(value) - - def as_celsius(self) -> float: - """Return the temperature in degrees Celsius. - - Returns: - The temperature in degrees Celsius. - """ - return self._base_value - - -class Power( - Quantity, - metaclass=_NoDefaultConstructible, - exponent_unit_map={ - -3: "mW", - 0: "W", - 3: "kW", - 6: "MW", - }, -): - """A 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_watts(cls, watts: float) -> Self: - """Initialize a new power quantity. - - Args: - watts: The power in watts. - - Returns: - A new power quantity. - """ - return cls._new(watts) - - @classmethod - def from_milliwatts(cls, milliwatts: float) -> Self: - """Initialize a new power quantity. - - Args: - milliwatts: The power in milliwatts. - - Returns: - A new power quantity. - """ - return cls._new(milliwatts, exponent=-3) - - @classmethod - def from_kilowatts(cls, kilowatts: float) -> Self: - """Initialize a new power quantity. - - Args: - kilowatts: The power in kilowatts. - - Returns: - A new power quantity. - """ - return cls._new(kilowatts, exponent=3) - - @classmethod - def from_megawatts(cls, megawatts: float) -> Self: - """Initialize a new power quantity. - - Args: - megawatts: The power in megawatts. - - Returns: - A new power quantity. - """ - return cls._new(megawatts, exponent=6) - - def as_watts(self) -> float: - """Return the power in watts. - - Returns: - The power in watts. - """ - return self._base_value - - def as_kilowatts(self) -> float: - """Return the power in kilowatts. - - Returns: - The power in kilowatts. - """ - return self._base_value / 1e3 - - def as_megawatts(self) -> float: - """Return the power in megawatts. - - Returns: - The power in megawatts. - """ - 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. - """ - 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 Power.__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. - """ - match other: - case float(): - return super().__truediv__(other) - case Power(): - 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 - - -class Current( - Quantity, - metaclass=_NoDefaultConstructible, - exponent_unit_map={ - -3: "mA", - 0: "A", - }, -): - """A current 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_amperes(cls, amperes: float) -> Self: - """Initialize a new current quantity. - - Args: - amperes: The current in amperes. - - Returns: - A new current quantity. - """ - return cls._new(amperes) - - @classmethod - def from_milliamperes(cls, milliamperes: float) -> Self: - """Initialize a new current quantity. - - Args: - milliamperes: The current in milliamperes. - - Returns: - A new current quantity. - """ - return cls._new(milliamperes, exponent=-3) - - def as_amperes(self) -> float: - """Return the current in amperes. - - Returns: - The current in amperes. - """ - return self._base_value - - def as_milliamperes(self) -> float: - """Return the current in milliamperes. - - Returns: - The current in milliamperes. - """ - return self._base_value * 1e3 - - # See comment for Power.__mul__ for why we need the ignore here. - @overload # type: ignore[override] - def __mul__(self, scalar: float, /) -> Self: - """Scale this current by a scalar. - - Args: - scalar: The scalar by which to scale this current. - - Returns: - The scaled current. - """ - - @overload - def __mul__(self, percent: Percentage, /) -> Self: - """Scale this current by a percentage. - - Args: - percent: The percentage by which to scale this current. - - Returns: - The scaled current. - """ - - @overload - def __mul__(self, other: Voltage, /) -> Power: - """Multiply the current by a voltage to get a power. - - Args: - other: The voltage. - - Returns: - The calculated power. - """ - - def __mul__(self, other: float | Percentage | Voltage, /) -> Self | Power: - """Return a current or power from multiplying this current by the given value. - - Args: - other: The scalar, percentage or voltage to multiply by. - - Returns: - A current or power. - """ - match other: - case float() | Percentage(): - return super().__mul__(other) - case Voltage(): - return Power._new(self._base_value * other._base_value) - case _: - return NotImplemented - - -class Voltage( - Quantity, - metaclass=_NoDefaultConstructible, - exponent_unit_map={0: "V", -3: "mV", 3: "kV"}, -): - """A voltage 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_volts(cls, volts: float) -> Self: - """Initialize a new voltage quantity. - - Args: - volts: The voltage in volts. - - Returns: - A new voltage quantity. - """ - return cls._new(volts) - - @classmethod - def from_millivolts(cls, millivolts: float) -> Self: - """Initialize a new voltage quantity. - - Args: - millivolts: The voltage in millivolts. - - Returns: - A new voltage quantity. - """ - return cls._new(millivolts, exponent=-3) - - @classmethod - def from_kilovolts(cls, kilovolts: float) -> Self: - """Initialize a new voltage quantity. - - Args: - kilovolts: The voltage in kilovolts. - - Returns: - A new voltage quantity. - """ - return cls._new(kilovolts, exponent=3) - - def as_volts(self) -> float: - """Return the voltage in volts. - - Returns: - The voltage in volts. - """ - return self._base_value - - def as_millivolts(self) -> float: - """Return the voltage in millivolts. - - Returns: - The voltage in millivolts. - """ - return self._base_value * 1e3 - - def as_kilovolts(self) -> float: - """Return the voltage in kilovolts. - - Returns: - The voltage in kilovolts. - """ - return self._base_value / 1e3 - - # See comment for Power.__mul__ for why we need the ignore here. - @overload # type: ignore[override] - def __mul__(self, scalar: float, /) -> Self: - """Scale this voltage by a scalar. - - Args: - scalar: The scalar by which to scale this voltage. - - Returns: - The scaled voltage. - """ - - @overload - def __mul__(self, percent: Percentage, /) -> Self: - """Scale this voltage by a percentage. - - Args: - percent: The percentage by which to scale this voltage. - - Returns: - The scaled voltage. - """ - - @overload - def __mul__(self, other: Current, /) -> Power: - """Multiply the voltage by the current to get the power. - - Args: - other: The current to multiply the voltage with. - - Returns: - The calculated power. - """ - - def __mul__(self, other: float | Percentage | Current, /) -> Self | Power: - """Return a voltage or power from multiplying this voltage by the given value. - - Args: - other: The scalar, percentage or current to multiply by. - - Returns: - The calculated voltage or power. - """ - match other: - case float() | Percentage(): - return super().__mul__(other) - case Current(): - return Power._new(self._base_value * other._base_value) - case _: - return NotImplemented - - -class Energy( - Quantity, - metaclass=_NoDefaultConstructible, - exponent_unit_map={ - 0: "Wh", - 3: "kWh", - 6: "MWh", - }, -): - """An energy 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_watt_hours(cls, watt_hours: float) -> Self: - """Initialize a new energy quantity. - - Args: - watt_hours: The energy in watt hours. - - Returns: - A new energy quantity. - """ - return cls._new(watt_hours) - - @classmethod - def from_kilowatt_hours(cls, kilowatt_hours: float) -> Self: - """Initialize a new energy quantity. - - Args: - kilowatt_hours: The energy in kilowatt hours. - - Returns: - A new energy quantity. - """ - return cls._new(kilowatt_hours, exponent=3) - - @classmethod - def from_megawatt_hours(cls, megawatt_hours: float) -> Self: - """Initialize a new energy quantity. - - Args: - megawatt_hours: The energy in megawatt hours. - - Returns: - A new energy quantity. - """ - return cls._new(megawatt_hours, exponent=6) - - def as_watt_hours(self) -> float: - """Return the energy in watt hours. - - Returns: - The energy in watt hours. - """ - return self._base_value - - def as_kilowatt_hours(self) -> float: - """Return the energy in kilowatt hours. - - Returns: - The energy in kilowatt hours. - """ - return self._base_value / 1e3 - - def as_megawatt_hours(self) -> float: - """Return the energy in megawatt hours. - - Returns: - The energy in megawatt hours. - """ - return self._base_value / 1e6 - - def __mul__(self, other: float | Percentage) -> Self: - """Scale this energy by a percentage. - - Args: - other: The percentage by which to scale this energy. - - Returns: - The scaled energy. - """ - match other: - case float(): - return self._new(self._base_value * other) - case Percentage(): - return self._new(self._base_value * other.as_fraction()) - case _: - return NotImplemented - - # See the comment for Power.__mul__ for why we need the ignore here. - @overload # type: ignore[override] - def __truediv__(self, other: float, /) -> Self: - """Divide this energy by a scalar. - - Args: - other: The scalar to divide this energy by. - - Returns: - The divided energy. - """ - - @overload - def __truediv__(self, other: Self, /) -> float: - """Return the ratio of this energy to another. - - Args: - other: The other energy. - - Returns: - The ratio of this energy to another. - """ - - @overload - def __truediv__(self, duration: timedelta, /) -> Power: - """Return a power from dividing this energy by the given duration. - - Args: - duration: The duration to divide by. - - Returns: - A power from dividing this energy by the given duration. - """ - - @overload - def __truediv__(self, power: Power, /) -> 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 | float | Power | timedelta: - """Return a power or duration from dividing this energy by the given value. - - Args: - other: The scalar, energy, power or duration to divide by. - - Returns: - A power or duration from dividing this energy by the given value. - """ - match other: - case float(): - return super().__truediv__(other) - case Energy(): - return self._base_value / other._base_value - case timedelta(): - return Power._new(self._base_value / (other.total_seconds() / 3600.0)) - case Power(): - return timedelta( - seconds=(self._base_value / other._base_value) * 3600.0 - ) - case _: - return NotImplemented - - -class Frequency( - Quantity, - metaclass=_NoDefaultConstructible, - exponent_unit_map={0: "Hz", 3: "kHz", 6: "MHz", 9: "GHz"}, -): - """A frequency 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_hertz(cls, hertz: float) -> Self: - """Initialize a new frequency quantity. - - Args: - hertz: The frequency in hertz. - - Returns: - A new frequency quantity. - """ - return cls._new(hertz) - - @classmethod - def from_kilohertz(cls, kilohertz: float) -> Self: - """Initialize a new frequency quantity. - - Args: - kilohertz: The frequency in kilohertz. - - Returns: - A new frequency quantity. - """ - return cls._new(kilohertz, exponent=3) - - @classmethod - def from_megahertz(cls, megahertz: float) -> Self: - """Initialize a new frequency quantity. - - Args: - megahertz: The frequency in megahertz. - - Returns: - A new frequency quantity. - """ - return cls._new(megahertz, exponent=6) - - @classmethod - def from_gigahertz(cls, gigahertz: float) -> Self: - """Initialize a new frequency quantity. - - Args: - gigahertz: The frequency in gigahertz. - - Returns: - A new frequency quantity. - """ - return cls._new(gigahertz, exponent=9) - - def as_hertz(self) -> float: - """Return the frequency in hertz. - - Returns: - The frequency in hertz. - """ - return self._base_value - - def as_kilohertz(self) -> float: - """Return the frequency in kilohertz. - - Returns: - The frequency in kilohertz. - """ - return self._base_value / 1e3 - - def as_megahertz(self) -> float: - """Return the frequency in megahertz. - - Returns: - The frequency in megahertz. - """ - return self._base_value / 1e6 - - def as_gigahertz(self) -> float: - """Return the frequency in gigahertz. - - Returns: - The frequency in gigahertz. - """ - return self._base_value / 1e9 - - def period(self) -> timedelta: - """Return the period of the frequency. - - Returns: - The period of the frequency. - """ - return timedelta(seconds=1.0 / self._base_value) - - -class Percentage( - Quantity, - metaclass=_NoDefaultConstructible, - exponent_unit_map={0: "%"}, -): - """A percentage 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_percent(cls, percent: float) -> Self: - """Initialize a new percentage quantity from a percent value. - - Args: - percent: The percent value, normally in the 0.0-100.0 range. - - Returns: - A new percentage quantity. - """ - return cls._new(percent) - - @classmethod - def from_fraction(cls, fraction: float) -> Self: - """Initialize a new percentage quantity from a fraction. - - Args: - fraction: The fraction, normally in the 0.0-1.0 range. - - Returns: - A new percentage quantity. - """ - return cls._new(fraction * 100) - - def as_percent(self) -> float: - """Return this quantity as a percentage. - - Returns: - This quantity as a percentage. - """ - return self._base_value - - def as_fraction(self) -> float: - """Return this quantity as a fraction. - - Returns: - This quantity as a fraction. - """ - return self._base_value / 100 diff --git a/src/frequenz/sdk/timeseries/_resampling.py b/src/frequenz/sdk/timeseries/_resampling.py index d6266962b..c23dc0888 100644 --- a/src/frequenz/sdk/timeseries/_resampling.py +++ b/src/frequenz/sdk/timeseries/_resampling.py @@ -17,10 +17,10 @@ from typing import cast from frequenz.channels.timer import Timer, TriggerAllMissed, _to_microseconds +from frequenz.quantities import Quantity from .._internal._asyncio import cancel_and_await -from ._base_types import UNIX_EPOCH, Sample -from ._quantities import Quantity, QuantityT +from ._base_types import UNIX_EPOCH, QuantityT, Sample _logger = logging.getLogger(__name__) diff --git a/src/frequenz/sdk/timeseries/_ringbuffer/buffer.py b/src/frequenz/sdk/timeseries/_ringbuffer/buffer.py index bac39a46d..6f81498fd 100644 --- a/src/frequenz/sdk/timeseries/_ringbuffer/buffer.py +++ b/src/frequenz/sdk/timeseries/_ringbuffer/buffer.py @@ -12,8 +12,7 @@ import numpy as np import numpy.typing as npt -from .._base_types import UNIX_EPOCH, Sample -from .._quantities import QuantityT +from .._base_types import UNIX_EPOCH, QuantityT, Sample FloatArray = TypeVar("FloatArray", list[float], npt.NDArray[np.float64]) """Type variable of the buffer container.""" diff --git a/src/frequenz/sdk/timeseries/_voltage_streamer.py b/src/frequenz/sdk/timeseries/_voltage_streamer.py index bb124aa1b..193988401 100644 --- a/src/frequenz/sdk/timeseries/_voltage_streamer.py +++ b/src/frequenz/sdk/timeseries/_voltage_streamer.py @@ -15,10 +15,10 @@ from frequenz.channels import Receiver, Sender from frequenz.client.microgrid import Component, ComponentCategory, ComponentMetricId +from frequenz.quantities import Quantity, Voltage from .._internal._channels import ChannelRegistry from ..timeseries._base_types import Sample, Sample3Phase -from ..timeseries._quantities import Quantity, Voltage if TYPE_CHECKING: # Imported here to avoid a circular import. diff --git a/src/frequenz/sdk/timeseries/battery_pool/_battery_pool.py b/src/frequenz/sdk/timeseries/battery_pool/_battery_pool.py index 9fba7ab47..e3bc76241 100644 --- a/src/frequenz/sdk/timeseries/battery_pool/_battery_pool.py +++ b/src/frequenz/sdk/timeseries/battery_pool/_battery_pool.py @@ -12,10 +12,12 @@ import uuid from collections import abc +from frequenz.quantities import Energy, Percentage, Power, Temperature + from ... import timeseries from ..._internal._channels import MappingReceiverFetcher, ReceiverFetcher from ...microgrid import _power_distributing, _power_managing -from ...timeseries import Energy, Percentage, Power, Sample, Temperature +from ...timeseries import Sample from .._base_types import SystemBounds from ..formula_engine import FormulaEngine from ..formula_engine._formula_generators import ( diff --git a/src/frequenz/sdk/timeseries/battery_pool/_metric_calculator.py b/src/frequenz/sdk/timeseries/battery_pool/_metric_calculator.py index a4691173c..f9cb703d8 100644 --- a/src/frequenz/sdk/timeseries/battery_pool/_metric_calculator.py +++ b/src/frequenz/sdk/timeseries/battery_pool/_metric_calculator.py @@ -12,6 +12,7 @@ from typing import Generic, TypeVar from frequenz.client.microgrid import ComponentMetricId +from frequenz.quantities import Energy, Percentage, Power, Temperature from ... import timeseries from ..._internal import _math @@ -23,7 +24,6 @@ ) from ...microgrid._power_distributing.result import PowerBounds from .._base_types import Sample, SystemBounds -from .._quantities import Energy, Percentage, Power, Temperature from ._component_metrics import ComponentMetricsData _logger = logging.getLogger(__name__) diff --git a/src/frequenz/sdk/timeseries/battery_pool/messages.py b/src/frequenz/sdk/timeseries/battery_pool/messages.py index 704409717..653fee6ff 100644 --- a/src/frequenz/sdk/timeseries/battery_pool/messages.py +++ b/src/frequenz/sdk/timeseries/battery_pool/messages.py @@ -6,6 +6,8 @@ import abc import typing +from frequenz.quantities import Power + from ...microgrid._power_distributing import ( Error, OutOfBounds, @@ -14,7 +16,6 @@ Success, ) from .._base_types import Bounds -from .._quantities import Power # This class is used to expose the generic reports from the PowerManager with specific diff --git a/src/frequenz/sdk/timeseries/consumer.py b/src/frequenz/sdk/timeseries/consumer.py index ecb137efb..9b85b3c2a 100644 --- a/src/frequenz/sdk/timeseries/consumer.py +++ b/src/frequenz/sdk/timeseries/consumer.py @@ -6,10 +6,10 @@ import uuid from frequenz.channels import Sender +from frequenz.quantities import Power from .._internal._channels import ChannelRegistry from ..microgrid._data_sourcing import ComponentMetricRequest -from ._quantities import Power from .formula_engine import FormulaEngine from .formula_engine._formula_engine_pool import FormulaEnginePool from .formula_engine._formula_generators import ConsumerPowerFormula diff --git a/src/frequenz/sdk/timeseries/ev_charger_pool/_ev_charger_pool.py b/src/frequenz/sdk/timeseries/ev_charger_pool/_ev_charger_pool.py index 6ae51df9d..5026892a6 100644 --- a/src/frequenz/sdk/timeseries/ev_charger_pool/_ev_charger_pool.py +++ b/src/frequenz/sdk/timeseries/ev_charger_pool/_ev_charger_pool.py @@ -8,11 +8,12 @@ import uuid from collections import abc +from frequenz.quantities import Current, Power + from ..._internal._channels import MappingReceiverFetcher, ReceiverFetcher from ...microgrid import _power_distributing, _power_managing from ...timeseries import Bounds from .._base_types import SystemBounds -from .._quantities import Current, Power from ..formula_engine import FormulaEngine, FormulaEngine3Phase from ..formula_engine._formula_generators import ( EVChargerCurrentFormula, diff --git a/src/frequenz/sdk/timeseries/ev_charger_pool/_result_types.py b/src/frequenz/sdk/timeseries/ev_charger_pool/_result_types.py index ff9a5ee3e..32d1c90f1 100644 --- a/src/frequenz/sdk/timeseries/ev_charger_pool/_result_types.py +++ b/src/frequenz/sdk/timeseries/ev_charger_pool/_result_types.py @@ -5,8 +5,9 @@ import typing +from frequenz.quantities import Power + from .._base_types import Bounds -from .._quantities import Power class EVChargerPoolReport(typing.Protocol): diff --git a/src/frequenz/sdk/timeseries/ev_charger_pool/_system_bounds_tracker.py b/src/frequenz/sdk/timeseries/ev_charger_pool/_system_bounds_tracker.py index a4e7cb53c..c8dc85576 100644 --- a/src/frequenz/sdk/timeseries/ev_charger_pool/_system_bounds_tracker.py +++ b/src/frequenz/sdk/timeseries/ev_charger_pool/_system_bounds_tracker.py @@ -9,12 +9,12 @@ from frequenz.channels import Receiver, Sender, merge, select, selected_from from frequenz.client.microgrid import EVChargerData +from frequenz.quantities import Power from ..._internal._asyncio import run_forever from ...actor import BackgroundService from ...microgrid import connection_manager from ...microgrid._power_distributing._component_status import ComponentPoolStatus -from .. import Power from .._base_types import Bounds, SystemBounds diff --git a/src/frequenz/sdk/timeseries/formula_engine/_formula_engine.py b/src/frequenz/sdk/timeseries/formula_engine/_formula_engine.py index eabd83e77..8ce0f047d 100644 --- a/src/frequenz/sdk/timeseries/formula_engine/_formula_engine.py +++ b/src/frequenz/sdk/timeseries/formula_engine/_formula_engine.py @@ -15,10 +15,10 @@ from typing import Any, Generic, Self, TypeVar from frequenz.channels import Broadcast, Receiver +from frequenz.quantities import Quantity from ..._internal._asyncio import cancel_and_await -from .. import Sample, Sample3Phase -from .._quantities import Quantity, QuantityT +from .._base_types import QuantityT, Sample, Sample3Phase from ._formula_evaluator import FormulaEvaluator from ._formula_formatter import format_formula from ._formula_steps import ( @@ -137,7 +137,7 @@ def from_receiver( Example: ```python from frequenz.sdk import microgrid - from frequenz.sdk.timeseries import Power + from frequenz.quantities import Power async def run() -> None: producer_power_engine = microgrid.producer().power @@ -626,7 +626,7 @@ class FormulaBuilder(Generic[QuantityT]): following calls need to be made: ```python - from frequenz.sdk.timeseries import Power + from frequenz.quantities import Power channel = Broadcast[Sample[Power]](name="channel") receiver_1 = channel.new_receiver(name="receiver_1") @@ -746,7 +746,7 @@ def push_clipper(self, min_value: float | None, max_value: float | None) -> None For example, this clips the output of the entire expression: ```python - from frequenz.sdk.timeseries import Power + from frequenz.quantities import Power builder = FormulaBuilder("example", Power) channel = Broadcast[Sample[Power]](name="channel") @@ -764,7 +764,7 @@ def push_clipper(self, min_value: float | None, max_value: float | None) -> None And this clips the output of metric_2 only, and not the final result: ```python - from frequenz.sdk.timeseries import Power + from frequenz.quantities import Power builder = FormulaBuilder("example", Power) channel = Broadcast[Sample[Power]](name="channel") @@ -858,7 +858,7 @@ def __init__( TokenType, FormulaEngine[QuantityT] | FormulaEngine3Phase[QuantityT] - | QuantityT + | Quantity | float | str, ] diff --git a/src/frequenz/sdk/timeseries/formula_engine/_formula_engine_pool.py b/src/frequenz/sdk/timeseries/formula_engine/_formula_engine_pool.py index 5658af21c..9d26f0118 100644 --- a/src/frequenz/sdk/timeseries/formula_engine/_formula_engine_pool.py +++ b/src/frequenz/sdk/timeseries/formula_engine/_formula_engine_pool.py @@ -9,10 +9,10 @@ from frequenz.channels import Sender from frequenz.client.microgrid import ComponentMetricId +from frequenz.quantities import Current, Power, Quantity from ..._internal._channels import ChannelRegistry from ...microgrid._data_sourcing import ComponentMetricRequest -from .._quantities import Current, Power, Quantity from ._formula_generators._formula_generator import ( FormulaGenerator, FormulaGeneratorConfig, diff --git a/src/frequenz/sdk/timeseries/formula_engine/_formula_evaluator.py b/src/frequenz/sdk/timeseries/formula_engine/_formula_evaluator.py index 3e32f9d4f..aa26430ed 100644 --- a/src/frequenz/sdk/timeseries/formula_engine/_formula_evaluator.py +++ b/src/frequenz/sdk/timeseries/formula_engine/_formula_evaluator.py @@ -9,8 +9,7 @@ from math import isinf, isnan from typing import Generic -from .. import Sample -from .._quantities import QuantityT +from .._base_types import QuantityT, Sample from ._formula_steps import FormulaStep, MetricFetcher diff --git a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_battery_power_formula.py b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_battery_power_formula.py index ae1c3bd01..6080d9932 100644 --- a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_battery_power_formula.py +++ b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_battery_power_formula.py @@ -7,9 +7,9 @@ import logging from frequenz.client.microgrid import Component, ComponentCategory, ComponentMetricId +from frequenz.quantities import Power from ....microgrid import connection_manager -from ..._quantities import Power from ...formula_engine import FormulaEngine from ._fallback_formula_metric_fetcher import FallbackFormulaMetricFetcher from ._formula_generator import ( diff --git a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_chp_power_formula.py b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_chp_power_formula.py index 755022d57..b8cf51d74 100644 --- a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_chp_power_formula.py +++ b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_chp_power_formula.py @@ -8,9 +8,9 @@ from collections import abc from frequenz.client.microgrid import ComponentCategory, ComponentMetricId +from frequenz.quantities import Power from ....microgrid import connection_manager -from ..._quantities import Power from ...formula_engine import FormulaEngine from ._formula_generator import ( NON_EXISTING_COMPONENT_ID, diff --git a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_consumer_power_formula.py b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_consumer_power_formula.py index 67dcc582c..f2dcb2016 100644 --- a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_consumer_power_formula.py +++ b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_consumer_power_formula.py @@ -6,9 +6,9 @@ import logging from frequenz.client.microgrid import Component, ComponentCategory, ComponentMetricId +from frequenz.quantities import Power from ....microgrid import connection_manager -from ..._quantities import Power from .._formula_engine import FormulaEngine from .._resampled_formula_builder import ResampledFormulaBuilder from ._fallback_formula_metric_fetcher import FallbackFormulaMetricFetcher diff --git a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_ev_charger_current_formula.py b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_ev_charger_current_formula.py index bbcd17655..4b53d360f 100644 --- a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_ev_charger_current_formula.py +++ b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_ev_charger_current_formula.py @@ -8,8 +8,8 @@ from collections import abc from frequenz.client.microgrid import ComponentMetricId +from frequenz.quantities import Current -from ..._quantities import Current from .._formula_engine import FormulaEngine, FormulaEngine3Phase from ._formula_generator import NON_EXISTING_COMPONENT_ID, FormulaGenerator diff --git a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_ev_charger_power_formula.py b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_ev_charger_power_formula.py index 80ce5c624..cfa29667f 100644 --- a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_ev_charger_power_formula.py +++ b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_ev_charger_power_formula.py @@ -6,8 +6,8 @@ import logging from frequenz.client.microgrid import ComponentMetricId +from frequenz.quantities import Power -from ..._quantities import Power from .._formula_engine import FormulaEngine from ._formula_generator import NON_EXISTING_COMPONENT_ID, FormulaGenerator diff --git a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_fallback_formula_metric_fetcher.py b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_fallback_formula_metric_fetcher.py index fababba4f..362cf8c18 100644 --- a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_fallback_formula_metric_fetcher.py +++ b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_fallback_formula_metric_fetcher.py @@ -5,8 +5,7 @@ from frequenz.channels import Receiver -from ... import Sample -from ..._quantities import QuantityT +from ..._base_types import QuantityT, Sample from .. import FormulaEngine from .._formula_steps import FallbackMetricFetcher from ._formula_generator import FormulaGenerator diff --git a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_formula_generator.py b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_formula_generator.py index 1283f9e78..2fe011c45 100644 --- a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_formula_generator.py +++ b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_formula_generator.py @@ -18,7 +18,7 @@ from ...._internal._channels import ChannelRegistry from ....microgrid import connection_manager from ....microgrid._data_sourcing import ComponentMetricRequest -from ..._quantities import QuantityT +from ..._base_types import QuantityT from .._formula_engine import FormulaEngine, FormulaEngine3Phase from .._resampled_formula_builder import ResampledFormulaBuilder diff --git a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_grid_current_formula.py b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_grid_current_formula.py index 59e37309c..420e5151c 100644 --- a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_grid_current_formula.py +++ b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_grid_current_formula.py @@ -4,8 +4,8 @@ """Formula generator from component graph for 3-phase Grid Current.""" from frequenz.client.microgrid import Component, ComponentCategory, ComponentMetricId +from frequenz.quantities import Current -from ..._quantities import Current from .._formula_engine import FormulaEngine, FormulaEngine3Phase from ._formula_generator import FormulaGenerator diff --git a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_grid_power_3_phase_formula.py b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_grid_power_3_phase_formula.py index e6fd279b8..4f298d921 100644 --- a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_grid_power_3_phase_formula.py +++ b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_grid_power_3_phase_formula.py @@ -4,8 +4,8 @@ """Formula generator from component graph for 3-phase Grid Power.""" from frequenz.client.microgrid import Component, ComponentCategory, ComponentMetricId +from frequenz.quantities import Power -from ..._quantities import Power from .._formula_engine import FormulaEngine, FormulaEngine3Phase from ._formula_generator import FormulaGenerator diff --git a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_grid_power_formula.py b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_grid_power_formula.py index ce6093623..47a21c502 100644 --- a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_grid_power_formula.py +++ b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_grid_power_formula.py @@ -4,8 +4,8 @@ """Formula generator from component graph for Grid Power.""" from frequenz.client.microgrid import Component, ComponentCategory, ComponentMetricId +from frequenz.quantities import Power -from ..._quantities import Power from .._formula_engine import FormulaEngine from ._fallback_formula_metric_fetcher import FallbackFormulaMetricFetcher from ._formula_generator import ( diff --git a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_producer_power_formula.py b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_producer_power_formula.py index 7710a69a8..df952883a 100644 --- a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_producer_power_formula.py +++ b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_producer_power_formula.py @@ -7,9 +7,9 @@ from typing import Callable from frequenz.client.microgrid import Component, ComponentCategory, ComponentMetricId +from frequenz.quantities import Power from ....microgrid import connection_manager -from ..._quantities import Power from .._formula_engine import FormulaEngine from ._fallback_formula_metric_fetcher import FallbackFormulaMetricFetcher from ._formula_generator import ( diff --git a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_pv_power_formula.py b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_pv_power_formula.py index 98bd87337..ea65e4738 100644 --- a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_pv_power_formula.py +++ b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_pv_power_formula.py @@ -6,9 +6,9 @@ import logging from frequenz.client.microgrid import Component, ComponentCategory, ComponentMetricId +from frequenz.quantities import Power from ....microgrid import connection_manager -from ..._quantities import Power from .._formula_engine import FormulaEngine from ._fallback_formula_metric_fetcher import FallbackFormulaMetricFetcher from ._formula_generator import ( diff --git a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_simple_power_formula.py b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_simple_power_formula.py index 79b2fb305..c41bdabc8 100644 --- a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_simple_power_formula.py +++ b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_simple_power_formula.py @@ -4,9 +4,9 @@ """Formula generator from component graph.""" from frequenz.client.microgrid import ComponentCategory, ComponentMetricId +from frequenz.quantities import Power from ....microgrid import connection_manager -from ..._quantities import Power from .._formula_engine import FormulaEngine from ._formula_generator import FormulaGenerator diff --git a/src/frequenz/sdk/timeseries/formula_engine/_formula_steps.py b/src/frequenz/sdk/timeseries/formula_engine/_formula_steps.py index d6c47852b..2e18424c9 100644 --- a/src/frequenz/sdk/timeseries/formula_engine/_formula_steps.py +++ b/src/frequenz/sdk/timeseries/formula_engine/_formula_steps.py @@ -12,8 +12,7 @@ from frequenz.channels import Receiver, ReceiverError -from .. import Sample -from .._quantities import QuantityT +from .._base_types import QuantityT, Sample _logger = logging.getLogger(__name__) diff --git a/src/frequenz/sdk/timeseries/formula_engine/_resampled_formula_builder.py b/src/frequenz/sdk/timeseries/formula_engine/_resampled_formula_builder.py index 9c980eff0..39b2fc76a 100644 --- a/src/frequenz/sdk/timeseries/formula_engine/_resampled_formula_builder.py +++ b/src/frequenz/sdk/timeseries/formula_engine/_resampled_formula_builder.py @@ -9,11 +9,11 @@ from frequenz.channels import Receiver, Sender from frequenz.client.microgrid import ComponentMetricId +from frequenz.quantities import Quantity from ..._internal._channels import ChannelRegistry from ...microgrid._data_sourcing import ComponentMetricRequest -from .. import Sample -from .._quantities import Quantity, QuantityT +from .._base_types import QuantityT, Sample from ._formula_engine import FormulaBuilder, FormulaEngine from ._formula_steps import FallbackMetricFetcher from ._tokenizer import Tokenizer, TokenType @@ -53,7 +53,7 @@ def __init__( # pylint: disable=too-many-arguments self._namespace: str = namespace self._metric_id: ComponentMetricId = metric_id self._resampler_requests: list[ComponentMetricRequest] = [] - super().__init__(formula_name, create_method) # type: ignore[arg-type] + super().__init__(formula_name, create_method) def _get_resampled_receiver( self, component_id: int, metric_id: ComponentMetricId diff --git a/src/frequenz/sdk/timeseries/grid.py b/src/frequenz/sdk/timeseries/grid.py index 3fb721323..a9b0abe00 100644 --- a/src/frequenz/sdk/timeseries/grid.py +++ b/src/frequenz/sdk/timeseries/grid.py @@ -14,12 +14,12 @@ from frequenz.channels import Sender from frequenz.client.microgrid._component import ComponentCategory +from frequenz.quantities import Current, Power from .._internal._channels import ChannelRegistry from ..microgrid import connection_manager from ..microgrid._data_sourcing import ComponentMetricRequest from ._fuse import Fuse -from ._quantities import Current, Power from .formula_engine import FormulaEngine, FormulaEngine3Phase from .formula_engine._formula_engine_pool import FormulaEnginePool from .formula_engine._formula_generators import ( diff --git a/src/frequenz/sdk/timeseries/logical_meter/_logical_meter.py b/src/frequenz/sdk/timeseries/logical_meter/_logical_meter.py index 2e4ca53ba..1efe3056a 100644 --- a/src/frequenz/sdk/timeseries/logical_meter/_logical_meter.py +++ b/src/frequenz/sdk/timeseries/logical_meter/_logical_meter.py @@ -7,10 +7,10 @@ import uuid from frequenz.channels import Sender +from frequenz.quantities import Power, Quantity from ..._internal._channels import ChannelRegistry from ...microgrid._data_sourcing import ComponentMetricId, ComponentMetricRequest -from .._quantities import Power, Quantity from ..formula_engine import FormulaEngine from ..formula_engine._formula_engine_pool import FormulaEnginePool from ..formula_engine._formula_generators import CHPPowerFormula diff --git a/src/frequenz/sdk/timeseries/producer.py b/src/frequenz/sdk/timeseries/producer.py index 22ddf4b61..4e5103399 100644 --- a/src/frequenz/sdk/timeseries/producer.py +++ b/src/frequenz/sdk/timeseries/producer.py @@ -6,10 +6,10 @@ import uuid from frequenz.channels import Sender +from frequenz.quantities import Power from .._internal._channels import ChannelRegistry from ..microgrid._data_sourcing import ComponentMetricRequest -from ._quantities import Power from .formula_engine import FormulaEngine from .formula_engine._formula_engine_pool import FormulaEnginePool from .formula_engine._formula_generators import ProducerPowerFormula diff --git a/src/frequenz/sdk/timeseries/pv_pool/_pv_pool.py b/src/frequenz/sdk/timeseries/pv_pool/_pv_pool.py index 8d7bf173e..3f7fb2c2c 100644 --- a/src/frequenz/sdk/timeseries/pv_pool/_pv_pool.py +++ b/src/frequenz/sdk/timeseries/pv_pool/_pv_pool.py @@ -7,11 +7,12 @@ import uuid from collections import abc +from frequenz.quantities import Power + from ..._internal._channels import MappingReceiverFetcher, ReceiverFetcher from ...microgrid import _power_distributing, _power_managing from ...timeseries import Bounds from .._base_types import SystemBounds -from .._quantities import Power from ..formula_engine import FormulaEngine from ..formula_engine._formula_generators import FormulaGeneratorConfig, PVPowerFormula from ._pv_pool_reference_store import PVPoolReferenceStore diff --git a/src/frequenz/sdk/timeseries/pv_pool/_result_types.py b/src/frequenz/sdk/timeseries/pv_pool/_result_types.py index ad6fb59fd..da8ddb2f4 100644 --- a/src/frequenz/sdk/timeseries/pv_pool/_result_types.py +++ b/src/frequenz/sdk/timeseries/pv_pool/_result_types.py @@ -5,8 +5,9 @@ import typing +from frequenz.quantities import Power + from .._base_types import Bounds -from .._quantities import Power class PVPoolReport(typing.Protocol): diff --git a/src/frequenz/sdk/timeseries/pv_pool/_system_bounds_tracker.py b/src/frequenz/sdk/timeseries/pv_pool/_system_bounds_tracker.py index 8a8a5c7f5..b996f42da 100644 --- a/src/frequenz/sdk/timeseries/pv_pool/_system_bounds_tracker.py +++ b/src/frequenz/sdk/timeseries/pv_pool/_system_bounds_tracker.py @@ -8,13 +8,13 @@ from frequenz.channels import Receiver, Sender, merge, select, selected_from from frequenz.client.microgrid import InverterData +from frequenz.quantities import Power from ..._internal._asyncio import run_forever from ...actor import BackgroundService from ...microgrid import connection_manager from ...microgrid._power_distributing._component_status import ComponentPoolStatus from .._base_types import Bounds, SystemBounds -from .._quantities import Power class PVSystemBoundsTracker(BackgroundService): diff --git a/tests/actor/_power_managing/test_matryoshka.py b/tests/actor/_power_managing/test_matryoshka.py index df45c9182..771783f18 100644 --- a/tests/actor/_power_managing/test_matryoshka.py +++ b/tests/actor/_power_managing/test_matryoshka.py @@ -6,10 +6,12 @@ import asyncio from datetime import datetime, timedelta, timezone +from frequenz.quantities import Power + from frequenz.sdk import timeseries from frequenz.sdk.microgrid._power_managing import Proposal from frequenz.sdk.microgrid._power_managing._matryoshka import Matryoshka -from frequenz.sdk.timeseries import Power, _base_types +from frequenz.sdk.timeseries import _base_types class StatefulTester: diff --git a/tests/actor/_power_managing/test_report.py b/tests/actor/_power_managing/test_report.py index 1dc50e42e..05b8645e8 100644 --- a/tests/actor/_power_managing/test_report.py +++ b/tests/actor/_power_managing/test_report.py @@ -3,8 +3,10 @@ """Tests for methods provided by the PowerManager's reports.""" +from frequenz.quantities import Power + from frequenz.sdk.microgrid._power_managing import _Report -from frequenz.sdk.timeseries import Bounds, Power +from frequenz.sdk.timeseries import Bounds class BoundsTester: diff --git a/tests/actor/power_distributing/test_power_distributing.py b/tests/actor/power_distributing/test_power_distributing.py index feb270d7b..cf357d2dc 100644 --- a/tests/actor/power_distributing/test_power_distributing.py +++ b/tests/actor/power_distributing/test_power_distributing.py @@ -14,6 +14,7 @@ from frequenz.channels import Broadcast from frequenz.client.microgrid import ComponentCategory +from frequenz.quantities import Power from pytest_mock import MockerFixture from frequenz.sdk.microgrid._power_distributing import ( @@ -35,7 +36,6 @@ Result, Success, ) -from frequenz.sdk.timeseries import Power from ...conftest import SAFETY_TIMEOUT from ...microgrid.fixtures import _Mocks, _mocks diff --git a/tests/actor/test_data_sourcing.py b/tests/actor/test_data_sourcing.py index 191dd576e..1ee207ce1 100644 --- a/tests/actor/test_data_sourcing.py +++ b/tests/actor/test_data_sourcing.py @@ -27,13 +27,14 @@ InverterData, MeterData, ) +from frequenz.quantities import Quantity from frequenz.sdk._internal._channels import ChannelRegistry from frequenz.sdk.microgrid._data_sourcing import ( ComponentMetricRequest, DataSourcingActor, ) -from frequenz.sdk.timeseries import Quantity, Sample +from frequenz.sdk.timeseries import Sample T = TypeVar("T", bound=ComponentData) diff --git a/tests/actor/test_resampling.py b/tests/actor/test_resampling.py index ba5f3c913..d98bd2aa1 100644 --- a/tests/actor/test_resampling.py +++ b/tests/actor/test_resampling.py @@ -11,12 +11,12 @@ import time_machine from frequenz.channels import Broadcast from frequenz.client.microgrid import ComponentMetricId +from frequenz.quantities import Quantity from frequenz.sdk._internal._channels import ChannelRegistry from frequenz.sdk.microgrid._data_sourcing import ComponentMetricRequest from frequenz.sdk.microgrid._resampling import ComponentMetricsResamplingActor from frequenz.sdk.timeseries import ResamplerConfig, Sample -from frequenz.sdk.timeseries._quantities import Quantity @pytest.fixture(autouse=True) diff --git a/tests/microgrid/test_grid.py b/tests/microgrid/test_grid.py index d877b8d7f..357bc008b 100644 --- a/tests/microgrid/test_grid.py +++ b/tests/microgrid/test_grid.py @@ -7,11 +7,12 @@ import frequenz.client.microgrid as client from frequenz.client.microgrid import ComponentCategory +from frequenz.quantities import Current, Power, Quantity from pytest_mock import MockerFixture import frequenz.sdk.microgrid.component_graph as gr from frequenz.sdk import microgrid -from frequenz.sdk.timeseries import Current, Fuse, Power, Quantity +from frequenz.sdk.timeseries import Fuse from tests.utils.graph_generator import GraphGenerator from ..timeseries._formula_engine.utils import equal_float_lists, get_resampled_stream diff --git a/tests/timeseries/_battery_pool/test_battery_pool.py b/tests/timeseries/_battery_pool/test_battery_pool.py index af65a6e7d..df5191e49 100644 --- a/tests/timeseries/_battery_pool/test_battery_pool.py +++ b/tests/timeseries/_battery_pool/test_battery_pool.py @@ -21,6 +21,7 @@ import time_machine from frequenz.channels import Receiver, Sender from frequenz.client.microgrid import ComponentCategory +from frequenz.quantities import Energy, Percentage, Power, Temperature from pytest_mock import MockerFixture from frequenz.sdk import microgrid @@ -33,14 +34,7 @@ from frequenz.sdk.microgrid._power_distributing._component_managers._battery_manager import ( _get_battery_inverter_mappings, ) -from frequenz.sdk.timeseries import ( - Bounds, - Energy, - Percentage, - Power, - Sample, - Temperature, -) +from frequenz.sdk.timeseries import Bounds, Sample from frequenz.sdk.timeseries._base_types import SystemBounds from frequenz.sdk.timeseries.battery_pool import BatteryPool from frequenz.sdk.timeseries.formula_engine._formula_generators._formula_generator import ( diff --git a/tests/timeseries/_battery_pool/test_battery_pool_control_methods.py b/tests/timeseries/_battery_pool/test_battery_pool_control_methods.py index 6e3afbb3b..9632b1fc7 100644 --- a/tests/timeseries/_battery_pool/test_battery_pool_control_methods.py +++ b/tests/timeseries/_battery_pool/test_battery_pool_control_methods.py @@ -12,6 +12,7 @@ import async_solipsism import pytest from frequenz.channels import LatestValueCache, Sender +from frequenz.quantities import Power from pytest_mock import MockerFixture from frequenz.sdk import microgrid, timeseries @@ -21,7 +22,6 @@ from frequenz.sdk.microgrid._power_distributing._component_pool_status_tracker import ( ComponentPoolStatusTracker, ) -from frequenz.sdk.timeseries import Power from frequenz.sdk.timeseries.battery_pool.messages import BatteryPoolReport from ...utils.component_data_streamer import MockComponentDataStreamer diff --git a/tests/timeseries/_ev_charger_pool/test_ev_charger_pool.py b/tests/timeseries/_ev_charger_pool/test_ev_charger_pool.py index f8b647ede..ac6e0b7ef 100644 --- a/tests/timeseries/_ev_charger_pool/test_ev_charger_pool.py +++ b/tests/timeseries/_ev_charger_pool/test_ev_charger_pool.py @@ -4,10 +4,10 @@ """Tests for the `EVChargerPool`.""" +from frequenz.quantities import Power from pytest_mock import MockerFixture from frequenz.sdk import microgrid -from frequenz.sdk.timeseries._quantities import Power from tests.timeseries.mock_microgrid import MockMicrogrid diff --git a/tests/timeseries/_ev_charger_pool/test_ev_charger_pool_control_methods.py b/tests/timeseries/_ev_charger_pool/test_ev_charger_pool_control_methods.py index 5747bb95d..bb720ab9d 100644 --- a/tests/timeseries/_ev_charger_pool/test_ev_charger_pool_control_methods.py +++ b/tests/timeseries/_ev_charger_pool/test_ev_charger_pool_control_methods.py @@ -13,6 +13,7 @@ import time_machine from frequenz.channels import Receiver from frequenz.client.microgrid import EVChargerCableState, EVChargerComponentState +from frequenz.quantities import Power, Voltage from pytest_mock import MockerFixture from frequenz.sdk import microgrid @@ -22,7 +23,7 @@ from frequenz.sdk.microgrid._power_distributing._component_pool_status_tracker import ( ComponentPoolStatusTracker, ) -from frequenz.sdk.timeseries import Power, ResamplerConfig, Sample3Phase, Voltage +from frequenz.sdk.timeseries import ResamplerConfig, Sample3Phase from frequenz.sdk.timeseries.ev_charger_pool import EVChargerPool, EVChargerPoolReport from ...microgrid.fixtures import _Mocks diff --git a/tests/timeseries/_formula_engine/test_formula_composition.py b/tests/timeseries/_formula_engine/test_formula_composition.py index 3f4491922..d7b649cc2 100644 --- a/tests/timeseries/_formula_engine/test_formula_composition.py +++ b/tests/timeseries/_formula_engine/test_formula_composition.py @@ -9,10 +9,11 @@ import pytest from frequenz.client.microgrid import ComponentMetricId +from frequenz.quantities import Power from pytest_mock import MockerFixture from frequenz.sdk import microgrid -from frequenz.sdk.timeseries import Power, Sample +from frequenz.sdk.timeseries import Sample from ..mock_microgrid import MockMicrogrid from .utils import get_resampled_stream diff --git a/tests/timeseries/_formula_engine/utils.py b/tests/timeseries/_formula_engine/utils.py index 3bbcd828f..b1e8f8230 100644 --- a/tests/timeseries/_formula_engine/utils.py +++ b/tests/timeseries/_formula_engine/utils.py @@ -11,8 +11,7 @@ from frequenz.client.microgrid import ComponentMetricId from frequenz.sdk.microgrid import _data_pipeline -from frequenz.sdk.timeseries import Sample -from frequenz.sdk.timeseries._quantities import QuantityT +from frequenz.sdk.timeseries._base_types import QuantityT, Sample from frequenz.sdk.timeseries.formula_engine._resampled_formula_builder import ( ResampledFormulaBuilder, ) diff --git a/tests/timeseries/_pv_pool/test_pv_pool_control_methods.py b/tests/timeseries/_pv_pool/test_pv_pool_control_methods.py index 3b76d877f..24543a918 100644 --- a/tests/timeseries/_pv_pool/test_pv_pool_control_methods.py +++ b/tests/timeseries/_pv_pool/test_pv_pool_control_methods.py @@ -12,13 +12,13 @@ import pytest from frequenz.channels import Receiver from frequenz.client.microgrid import InverterComponentState +from frequenz.quantities import Power from pytest_mock import MockerFixture from frequenz.sdk import microgrid from frequenz.sdk.actor import ResamplerConfig from frequenz.sdk.microgrid import _power_distributing from frequenz.sdk.microgrid._data_pipeline import _DataPipeline -from frequenz.sdk.timeseries import Power from frequenz.sdk.timeseries.pv_pool import PVPoolReport from ...microgrid.fixtures import _Mocks diff --git a/tests/timeseries/mock_resampler.py b/tests/timeseries/mock_resampler.py index 1993a361b..9c5765999 100644 --- a/tests/timeseries/mock_resampler.py +++ b/tests/timeseries/mock_resampler.py @@ -10,13 +10,13 @@ from frequenz.channels import Broadcast, Receiver, Sender from frequenz.client.microgrid import ComponentMetricId +from frequenz.quantities import Quantity from pytest_mock import MockerFixture from frequenz.sdk._internal._asyncio import cancel_and_await from frequenz.sdk.microgrid._data_pipeline import _DataPipeline from frequenz.sdk.microgrid._data_sourcing import ComponentMetricRequest from frequenz.sdk.timeseries import ResamplerConfig, Sample -from frequenz.sdk.timeseries._quantities import Quantity from frequenz.sdk.timeseries.formula_engine._formula_generators._formula_generator import ( NON_EXISTING_COMPONENT_ID, ) diff --git a/tests/timeseries/test_base_types.py b/tests/timeseries/test_base_types.py index 37ebee078..7371f74e8 100644 --- a/tests/timeseries/test_base_types.py +++ b/tests/timeseries/test_base_types.py @@ -6,8 +6,9 @@ from datetime import datetime +from frequenz.quantities import Power + from frequenz.sdk.timeseries._base_types import Bounds, SystemBounds -from frequenz.sdk.timeseries._quantities import Power def test_bounds_contains() -> None: diff --git a/tests/timeseries/test_consumer.py b/tests/timeseries/test_consumer.py index 64d93ef63..063e6a8f1 100644 --- a/tests/timeseries/test_consumer.py +++ b/tests/timeseries/test_consumer.py @@ -5,10 +5,10 @@ from contextlib import AsyncExitStack +from frequenz.quantities import Power from pytest_mock import MockerFixture from frequenz.sdk import microgrid -from frequenz.sdk.timeseries._quantities import Power from .mock_microgrid import MockMicrogrid diff --git a/tests/timeseries/test_formula_engine.py b/tests/timeseries/test_formula_engine.py index b3fbe36ac..bf51c166e 100644 --- a/tests/timeseries/test_formula_engine.py +++ b/tests/timeseries/test_formula_engine.py @@ -8,9 +8,9 @@ from datetime import datetime from frequenz.channels import Broadcast, Receiver +from frequenz.quantities import Power, Quantity from frequenz.sdk.timeseries import Sample -from frequenz.sdk.timeseries._quantities import Power, Quantity from frequenz.sdk.timeseries.formula_engine._formula_engine import ( FormulaBuilder, FormulaEngine, diff --git a/tests/timeseries/test_formula_formatter.py b/tests/timeseries/test_formula_formatter.py index 3f32dc795..784c15f8c 100644 --- a/tests/timeseries/test_formula_formatter.py +++ b/tests/timeseries/test_formula_formatter.py @@ -7,11 +7,11 @@ from contextlib import AsyncExitStack from frequenz.channels import Broadcast +from frequenz.quantities import Quantity from pytest_mock import MockerFixture from frequenz.sdk import microgrid from frequenz.sdk.timeseries import Sample -from frequenz.sdk.timeseries._quantities import Quantity from frequenz.sdk.timeseries.formula_engine._formula_engine import FormulaBuilder from frequenz.sdk.timeseries.formula_engine._formula_formatter import format_formula from frequenz.sdk.timeseries.formula_engine._formula_steps import ( diff --git a/tests/timeseries/test_frequency_streaming.py b/tests/timeseries/test_frequency_streaming.py index 1332aec4b..c6fe99b8b 100644 --- a/tests/timeseries/test_frequency_streaming.py +++ b/tests/timeseries/test_frequency_streaming.py @@ -7,10 +7,10 @@ import asyncio from datetime import datetime, timezone +from frequenz.quantities import Frequency from pytest_mock import MockerFixture from frequenz.sdk import microgrid -from frequenz.sdk.timeseries._quantities import Frequency from tests.utils import component_data_wrapper from ._formula_engine.utils import equal_float_lists diff --git a/tests/timeseries/test_logical_meter.py b/tests/timeseries/test_logical_meter.py index df7aba107..b33e18248 100644 --- a/tests/timeseries/test_logical_meter.py +++ b/tests/timeseries/test_logical_meter.py @@ -6,10 +6,10 @@ from contextlib import AsyncExitStack +from frequenz.quantities import Power from pytest_mock import MockerFixture from frequenz.sdk import microgrid -from frequenz.sdk.timeseries._quantities import Power from .mock_microgrid import MockMicrogrid diff --git a/tests/timeseries/test_moving_window.py b/tests/timeseries/test_moving_window.py index d1d601897..f1ae49673 100644 --- a/tests/timeseries/test_moving_window.py +++ b/tests/timeseries/test_moving_window.py @@ -12,10 +12,10 @@ import pytest import time_machine from frequenz.channels import Broadcast, Sender +from frequenz.quantities import Quantity from frequenz.sdk.timeseries import UNIX_EPOCH, Sample from frequenz.sdk.timeseries._moving_window import MovingWindow -from frequenz.sdk.timeseries._quantities import Quantity from frequenz.sdk.timeseries._resampling import ResamplerConfig diff --git a/tests/timeseries/test_periodic_feature_extractor.py b/tests/timeseries/test_periodic_feature_extractor.py index c32d60739..a298b0324 100644 --- a/tests/timeseries/test_periodic_feature_extractor.py +++ b/tests/timeseries/test_periodic_feature_extractor.py @@ -10,6 +10,7 @@ import numpy as np import pytest from frequenz.channels import Broadcast +from frequenz.quantities import Quantity from frequenz.sdk.timeseries import ( UNIX_EPOCH, @@ -17,7 +18,6 @@ PeriodicFeatureExtractor, Sample, ) -from frequenz.sdk.timeseries._quantities import Quantity from tests.timeseries.test_moving_window import ( init_moving_window, push_logical_meter_data, diff --git a/tests/timeseries/test_producer.py b/tests/timeseries/test_producer.py index c881ddabb..83290636c 100644 --- a/tests/timeseries/test_producer.py +++ b/tests/timeseries/test_producer.py @@ -5,10 +5,10 @@ from contextlib import AsyncExitStack +from frequenz.quantities import Power from pytest_mock import MockerFixture from frequenz.sdk import microgrid -from frequenz.sdk.timeseries._quantities import Power from .mock_microgrid import MockMicrogrid diff --git a/tests/timeseries/test_quantities.py b/tests/timeseries/test_quantities.py deleted file mode 100644 index 2babbea56..000000000 --- a/tests/timeseries/test_quantities.py +++ /dev/null @@ -1,934 +0,0 @@ -# License: MIT -# Copyright © 2022 Frequenz Energy-as-a-Service GmbH - -"""Tests for quantity types.""" - -import inspect -from datetime import timedelta -from typing import Callable - -import hypothesis -import pytest -from hypothesis import strategies as st - -from frequenz.sdk.timeseries import _quantities -from frequenz.sdk.timeseries._quantities import ( - Current, - Energy, - Frequency, - Percentage, - Power, - Quantity, - Temperature, - Voltage, -) - - -class Fz1( - Quantity, - exponent_unit_map={ - 0: "Hz", - 3: "kHz", - }, -): - """Frequency quantity with narrow exponent unit map.""" - - -class Fz2( - Quantity, - exponent_unit_map={ - -6: "uHz", - -3: "mHz", - 0: "Hz", - 3: "kHz", - 6: "MHz", - 9: "GHz", - }, -): - """Frequency quantity with broad exponent unit map.""" - - -_CtorType = Callable[[float], Quantity] - -# This is the current number of subclasses. This probably will get outdated, but it will -# provide at least some safety against something going really wrong and end up testing -# an empty list. With this we should at least make sure we are not testing less classes -# than before. We don't get the actual number using len(_QUANTITY_SUBCLASSES) because it -# would defeat the purpose of the test. -_SANITFY_NUM_CLASSES = 7 - -_QUANTITY_SUBCLASSES = [ - cls - for _, cls in inspect.getmembers( - _quantities, - lambda m: inspect.isclass(m) and issubclass(m, Quantity) and m is not Quantity, - ) -] - -# A very basic sanity check that are messing up the introspection -assert len(_QUANTITY_SUBCLASSES) >= _SANITFY_NUM_CLASSES - -_QUANTITY_BASE_UNIT_STRINGS = [ - cls._new(0).base_unit # pylint: disable=protected-access - for cls in _QUANTITY_SUBCLASSES -] -for unit in _QUANTITY_BASE_UNIT_STRINGS: - assert unit is not None - -_QUANTITY_CTORS = [ - method - for cls in _QUANTITY_SUBCLASSES - for _, method in inspect.getmembers( - cls, - lambda m: inspect.ismethod(m) - and m.__name__.startswith("from_") - and m.__name__ != ("from_string"), - ) -] -# A very basic sanity check that are messing up the introspection. There are actually -# many more constructors than classes, but this still works as a very basic check. -assert len(_QUANTITY_CTORS) >= _SANITFY_NUM_CLASSES - - -def test_zero() -> None: - """Test the zero value for quantity.""" - assert Quantity(0.0) == Quantity.zero() - assert Quantity(0.0, exponent=100) == Quantity.zero() - assert Quantity.zero() is Quantity.zero() # It is a "singleton" - assert Quantity.zero().base_value == 0.0 - - # Test the singleton is immutable - one = Quantity.zero() - one += Quantity(1.0) - assert one != Quantity.zero() - assert Quantity.zero() == Quantity(0.0) - - assert Power.from_watts(0.0) == Power.zero() - assert Power.from_kilowatts(0.0) == Power.zero() - assert isinstance(Power.zero(), Power) - assert Power.zero().as_watts() == 0.0 - assert Power.zero().as_kilowatts() == 0.0 - assert Power.zero() is Power.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) - assert Current.zero().as_amperes() == 0.0 - assert Current.zero().as_milliamperes() == 0.0 - assert Current.zero() is Current.zero() # It is a "singleton" - - assert Voltage.from_volts(0.0) == Voltage.zero() - assert Voltage.from_kilovolts(0.0) == Voltage.zero() - assert isinstance(Voltage.zero(), Voltage) - assert Voltage.zero().as_volts() == 0.0 - assert Voltage.zero().as_kilovolts() == 0.0 - assert Voltage.zero() is Voltage.zero() # It is a "singleton" - - assert Energy.from_kilowatt_hours(0.0) == Energy.zero() - assert Energy.from_megawatt_hours(0.0) == Energy.zero() - assert isinstance(Energy.zero(), Energy) - assert Energy.zero().as_kilowatt_hours() == 0.0 - assert Energy.zero().as_megawatt_hours() == 0.0 - assert Energy.zero() is Energy.zero() # It is a "singleton" - - assert Frequency.from_hertz(0.0) == Frequency.zero() - assert Frequency.from_megahertz(0.0) == Frequency.zero() - assert isinstance(Frequency.zero(), Frequency) - assert Frequency.zero().as_hertz() == 0.0 - assert Frequency.zero().as_megahertz() == 0.0 - assert Frequency.zero() is Frequency.zero() # It is a "singleton" - - assert Percentage.from_percent(0.0) == Percentage.zero() - assert Percentage.from_fraction(0.0) == Percentage.zero() - assert isinstance(Percentage.zero(), Percentage) - assert Percentage.zero().as_percent() == 0.0 - assert Percentage.zero().as_fraction() == 0.0 - assert Percentage.zero() is Percentage.zero() # It is a "singleton" - - -@pytest.mark.parametrize("quantity_ctor", _QUANTITY_CTORS) -def test_base_value_from_ctor_is_float(quantity_ctor: _CtorType) -> None: - """Test that the base value always is a float.""" - quantity = quantity_ctor(1) - assert isinstance(quantity.base_value, float) - - -@pytest.mark.parametrize("quantity_type", _QUANTITY_SUBCLASSES + [Quantity]) -def test_base_value_from_zero_is_float(quantity_type: type[Quantity]) -> None: - """Test that the base value always is a float.""" - quantity = quantity_type.zero() - assert isinstance(quantity.base_value, float) - - -@pytest.mark.parametrize( - "quantity_type, unit", zip(_QUANTITY_SUBCLASSES, _QUANTITY_BASE_UNIT_STRINGS) -) -def test_base_value_from_string_is_float( - quantity_type: type[Quantity], unit: str -) -> None: - """Test that the base value always is a float.""" - quantity = quantity_type.from_string(f"1 {unit}") - assert isinstance(quantity.base_value, float) - - -def test_string_representation() -> None: - """Test the string representation of the quantities.""" - assert str(Quantity(1.024445, exponent=0)) == "1.024" - assert ( - repr(Quantity(1.024445, exponent=0)) == "Quantity(value=1.024445, exponent=0)" - ) - assert f"{Quantity(0.50001, exponent=0):.0}" == "1" - assert f"{Quantity(1.024445, exponent=0)}" == "1.024" - assert f"{Quantity(1.024445, exponent=0):.0}" == "1" - assert f"{Quantity(0.124445, exponent=0):.0}" == "0" - assert f"{Quantity(0.50001, exponent=0):.0}" == "1" - assert f"{Quantity(1.024445, exponent=0):.6}" == "1.024445" - - assert f"{Quantity(1.024445, exponent=3)}" == "1024.445" - - assert str(Fz1(1.024445, exponent=0)) == "1.024 Hz" - assert repr(Fz1(1.024445, exponent=0)) == "Fz1(value=1.024445, exponent=0)" - assert f"{Fz1(1.024445, exponent=0)}" == "1.024 Hz" - assert f"{Fz1(1.024445, exponent=0):.0}" == "1 Hz" - assert f"{Fz1(1.024445, exponent=0):.1}" == "1 Hz" - assert f"{Fz1(1.024445, exponent=0):.2}" == "1.02 Hz" - assert f"{Fz1(1.024445, exponent=0):.9}" == "1.024445 Hz" - assert f"{Fz1(1.024445, exponent=0):0.0}" == "1 Hz" - assert f"{Fz1(1.024445, exponent=0):0.1}" == "1.0 Hz" - assert f"{Fz1(1.024445, exponent=0):0.2}" == "1.02 Hz" - assert f"{Fz1(1.024445, exponent=0):0.9}" == "1.024445000 Hz" - - assert f"{Fz1(1.024445, exponent=3)}" == "1.024 kHz" - assert f"{Fz2(1.024445, exponent=3)}" == "1.024 kHz" - - assert f"{Fz1(1.024445, exponent=6)}" == "1024.445 kHz" - assert f"{Fz2(1.024445, exponent=6)}" == "1.024 MHz" - assert f"{Fz1(1.024445, exponent=9)}" == "1024445 kHz" - assert f"{Fz2(1.024445, exponent=9)}" == "1.024 GHz" - - assert f"{Fz1(1.024445, exponent=-3)}" == "0.001 Hz" - assert f"{Fz2(1.024445, exponent=-3)}" == "1.024 mHz" - - assert f"{Fz1(1.024445, exponent=-6)}" == "0 Hz" - assert f"{Fz1(1.024445, exponent=-6):.6}" == "0.000001 Hz" - assert f"{Fz2(1.024445, exponent=-6)}" == "1.024 uHz" - - assert f"{Fz1(1.024445, exponent=-12)}" == "0 Hz" - assert f"{Fz2(1.024445, exponent=-12)}" == "0 Hz" - - assert f"{Fz1(0)}" == "0 Hz" - - assert f"{Fz1(-20)}" == "-20 Hz" - assert f"{Fz1(-20000)}" == "-20 kHz" - - assert f"{Power.from_watts(0.000124445):.0}" == "0 W" - assert f"{Energy.from_watt_hours(0.124445):.0}" == "0 Wh" - assert f"{Power.from_watts(-0.0):.0}" == "-0 W" - assert f"{Power.from_watts(0.0):.0}" == "0 W" - assert f"{Voltage.from_volts(999.9999850988388)}" == "1 kV" - - -def test_isclose() -> None: - """Test the isclose method of the quantities.""" - assert Fz1(1.024445).isclose(Fz1(1.024445)) - assert not Fz1(1.024445).isclose(Fz1(1.0)) - - -def test_addition_subtraction() -> None: - """Test the addition and subtraction of the quantities.""" - assert Quantity(1) + Quantity(1, exponent=0) == Quantity(2, exponent=0) - assert Quantity(1) + Quantity(1, exponent=3) == Quantity(1001, exponent=0) - assert Quantity(1) - Quantity(1, exponent=0) == Quantity(0, exponent=0) - - assert Fz1(1) + Fz1(1) == Fz1(2) - with pytest.raises(TypeError) as excinfo: - assert Fz1(1) + Fz2(1) # type: ignore - assert excinfo.value.args[0] == "unsupported operand type(s) for +: 'Fz1' and 'Fz2'" - with pytest.raises(TypeError) as excinfo: - assert Fz1(1) - Fz2(1) # type: ignore - assert excinfo.value.args[0] == "unsupported operand type(s) for -: 'Fz1' and 'Fz2'" - - fz1 = Fz1(1.0) - fz1 += Fz1(4.0) - assert fz1 == Fz1(5.0) - fz1 -= Fz1(9.0) - assert fz1 == Fz1(-4.0) - - with pytest.raises(TypeError) as excinfo: - fz1 += Fz2(1.0) # type: ignore - - -def test_comparison() -> None: - """Test the comparison of the quantities.""" - assert Quantity(1.024445, exponent=0) == Quantity(1.024445, exponent=0) - assert Quantity(1.024445, exponent=0) != Quantity(1.024445, exponent=3) - assert Quantity(1.024445, exponent=0) < Quantity(1.024445, exponent=3) - assert Quantity(1.024445, exponent=0) <= Quantity(1.024445, exponent=3) - assert Quantity(1.024445, exponent=0) <= Quantity(1.024445, exponent=0) - assert Quantity(1.024445, exponent=0) > Quantity(1.024445, exponent=-3) - assert Quantity(1.024445, exponent=0) >= Quantity(1.024445, exponent=-3) - assert Quantity(1.024445, exponent=0) >= Quantity(1.024445, exponent=0) - - assert Fz1(1.024445, exponent=0) == Fz1(1.024445, exponent=0) - assert Fz1(1.024445, exponent=0) != Fz1(1.024445, exponent=3) - assert Fz1(1.024445, exponent=0) < Fz1(1.024445, exponent=3) - assert Fz1(1.024445, exponent=0) <= Fz1(1.024445, exponent=3) - assert Fz1(1.024445, exponent=0) <= Fz1(1.024445, exponent=0) - assert Fz1(1.024445, exponent=0) > Fz1(1.024445, exponent=-3) - assert Fz1(1.024445, exponent=0) >= Fz1(1.024445, exponent=-3) - assert Fz1(1.024445, exponent=0) >= Fz1(1.024445, exponent=0) - - assert Fz1(1.024445, exponent=0) != Fz2(1.024445, exponent=0) - with pytest.raises(TypeError) as excinfo: - # unfortunately, mypy does not identify this as an error, when comparing a child - # type against a base type, but they should still fail, because base-type - # instances are being used as dimension-less quantities, whereas all child types - # have dimensions/units. - assert Fz1(1.024445, exponent=0) <= Quantity(1.024445, exponent=0) - assert ( - excinfo.value.args[0] - == "'<=' not supported between instances of 'Fz1' and 'Quantity'" - ) - with pytest.raises(TypeError) as excinfo: - assert Quantity(1.024445, exponent=0) <= Fz1(1.024445, exponent=0) - assert ( - excinfo.value.args[0] - == "'<=' not supported between instances of 'Quantity' and 'Fz1'" - ) - with pytest.raises(TypeError) as excinfo: - assert Fz1(1.024445, exponent=0) < Fz2(1.024445, exponent=3) # type: ignore - assert ( - excinfo.value.args[0] - == "'<' not supported between instances of 'Fz1' and 'Fz2'" - ) - with pytest.raises(TypeError) as excinfo: - assert Fz1(1.024445, exponent=0) <= Fz2(1.024445, exponent=3) # type: ignore - assert ( - excinfo.value.args[0] - == "'<=' not supported between instances of 'Fz1' and 'Fz2'" - ) - with pytest.raises(TypeError) as excinfo: - assert Fz1(1.024445, exponent=0) > Fz2(1.024445, exponent=-3) # type: ignore - assert ( - excinfo.value.args[0] - == "'>' not supported between instances of 'Fz1' and 'Fz2'" - ) - with pytest.raises(TypeError) as excinfo: - assert Fz1(1.024445, exponent=0) >= Fz2(1.024445, exponent=-3) # type: ignore - assert ( - excinfo.value.args[0] - == "'>=' not supported between instances of 'Fz1' and 'Fz2'" - ) - - -def test_power() -> None: - """Test the power class.""" - power = Power.from_milliwatts(0.0000002) - assert f"{power:.9}" == "0.0000002 mW" - power = Power.from_kilowatts(10000000.2) - assert f"{power}" == "10000 MW" - - power = Power.from_kilowatts(1.2) - assert power.as_watts() == 1200.0 - assert power.as_megawatts() == 0.0012 - assert power.as_kilowatts() == 1.2 - assert power == Power.from_milliwatts(1200000.0) - assert power == Power.from_megawatts(0.0012) - assert power != Power.from_watts(1000.0) - - with pytest.raises(TypeError): - # using the default constructor should raise. - Power(1.0, exponent=0) - - -def test_current() -> None: - """Test the current class.""" - current = Current.from_milliamperes(0.0000002) - assert f"{current:.9}" == "0.0000002 mA" - current = Current.from_amperes(600000.0) - assert f"{current}" == "600000 A" - - current = Current.from_amperes(6.0) - assert current.as_amperes() == 6.0 - assert current.as_milliamperes() == 6000.0 - assert current == Current.from_milliamperes(6000.0) - assert current == Current.from_amperes(6.0) - assert current != Current.from_amperes(5.0) - - with pytest.raises(TypeError): - # using the default constructor should raise. - Current(1.0, exponent=0) - - -def test_voltage() -> None: - """Test the voltage class.""" - voltage = Voltage.from_millivolts(0.0000002) - assert f"{voltage:.9}" == "0.0000002 mV" - voltage = Voltage.from_kilovolts(600000.0) - assert f"{voltage}" == "600000 kV" - - voltage = Voltage.from_volts(6.0) - assert voltage.as_volts() == 6.0 - assert voltage.as_millivolts() == 6000.0 - assert voltage.as_kilovolts() == 0.006 - assert voltage == Voltage.from_millivolts(6000.0) - assert voltage == Voltage.from_kilovolts(0.006) - assert voltage == Voltage.from_volts(6.0) - assert voltage != Voltage.from_volts(5.0) - - with pytest.raises(TypeError): - # using the default constructor should raise. - Voltage(1.0, exponent=0) - - -def test_energy() -> None: - """Test the energy class.""" - energy = Energy.from_watt_hours(0.0000002) - assert f"{energy:.9}" == "0.0000002 Wh" - energy = Energy.from_megawatt_hours(600000.0) - assert f"{energy}" == "600000 MWh" - - energy = Energy.from_kilowatt_hours(6.0) - assert energy.as_watt_hours() == 6000.0 - assert energy.as_kilowatt_hours() == 6.0 - assert energy.as_megawatt_hours() == 0.006 - assert energy == Energy.from_megawatt_hours(0.006) - assert energy == Energy.from_kilowatt_hours(6.0) - assert energy != Energy.from_kilowatt_hours(5.0) - - with pytest.raises(TypeError): - # using the default constructor should raise. - Energy(1.0, exponent=0) - - -def test_temperature() -> None: - """Test the temperature class.""" - temp = Temperature.from_celsius(30.4) - assert f"{temp}" == "30.4 °C" - - assert temp.as_celsius() == 30.4 - assert temp != Temperature.from_celsius(5.0) - - with pytest.raises(TypeError): - # using the default constructor should raise. - Temperature(1.0, exponent=0) - - -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 / timedelta(hours=6.2) == power - assert energy == power * timedelta(hours=6.2) - - -def test_frequency() -> None: - """Test the frequency class.""" - freq = Frequency.from_hertz(0.0000002) - assert f"{freq:.9}" == "0.0000002 Hz" - freq = Frequency.from_kilohertz(600_000.0) - assert f"{freq}" == "600 MHz" - - freq = Frequency.from_hertz(6.0) - assert freq.as_hertz() == 6.0 - assert freq.as_kilohertz() == 0.006 - assert freq == Frequency.from_kilohertz(0.006) - assert freq == Frequency.from_hertz(6.0) - assert freq != Frequency.from_hertz(5.0) - - with pytest.raises(TypeError): - # using the default constructor should raise. - Frequency(1.0, exponent=0) - - -def test_percentage() -> None: - """Test the percentage class.""" - pct = Percentage.from_fraction(0.204) - assert f"{pct}" == "20.4 %" - pct = Percentage.from_percent(20.4) - assert f"{pct}" == "20.4 %" - assert pct.as_percent() == 20.4 - assert pct.as_fraction() == 0.204 - - -def test_neg() -> None: - """Test the negation of quantities.""" - power = Power.from_watts(1000.0) - assert -power == Power.from_watts(-1000.0) - assert -(-power) == power - - voltage = Voltage.from_volts(230.0) - assert -voltage == Voltage.from_volts(-230.0) - assert -(-voltage) == voltage - - current = Current.from_amperes(2) - assert -current == Current.from_amperes(-2) - assert -(-current) == current - - energy = Energy.from_kilowatt_hours(6.2) - assert -energy == Energy.from_kilowatt_hours(-6.2) - - freq = Frequency.from_hertz(50) - assert -freq == Frequency.from_hertz(-50) - assert -(-freq) == freq - - pct = Percentage.from_fraction(30) - assert -pct == Percentage.from_fraction(-30) - assert -(-pct) == pct - - -def test_inf() -> None: - """Test proper formating when using inf in quantities.""" - assert f"{Power.from_watts(float('inf'))}" == "inf W" - assert f"{Power.from_watts(float('-inf'))}" == "-inf W" - - assert f"{Voltage.from_volts(float('inf'))}" == "inf V" - assert f"{Voltage.from_volts(float('-inf'))}" == "-inf V" - - assert f"{Current.from_amperes(float('inf'))}" == "inf A" - assert f"{Current.from_amperes(float('-inf'))}" == "-inf A" - - assert f"{Energy.from_watt_hours(float('inf'))}" == "inf Wh" - assert f"{Energy.from_watt_hours(float('-inf'))}" == "-inf Wh" - - assert f"{Frequency.from_hertz(float('inf'))}" == "inf Hz" - assert f"{Frequency.from_hertz(float('-inf'))}" == "-inf Hz" - - assert f"{Percentage.from_fraction(float('inf'))}" == "inf %" - assert f"{Percentage.from_fraction(float('-inf'))}" == "-inf %" - - -def test_nan() -> None: - """Test proper formating when using nan in quantities.""" - assert f"{Power.from_watts(float('nan'))}" == "nan W" - 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" - assert f"{Frequency.from_hertz(float('nan'))}" == "nan Hz" - assert f"{Percentage.from_fraction(float('nan'))}" == "nan %" - - -def test_abs() -> None: - """Test the absolute value of quantities.""" - power = Power.from_watts(1000.0) - assert abs(power) == Power.from_watts(1000.0) - assert abs(-power) == Power.from_watts(1000.0) - - voltage = Voltage.from_volts(230.0) - assert abs(voltage) == Voltage.from_volts(230.0) - assert abs(-voltage) == Voltage.from_volts(230.0) - - current = Current.from_amperes(2) - assert abs(current) == Current.from_amperes(2) - assert abs(-current) == Current.from_amperes(2) - - energy = Energy.from_kilowatt_hours(6.2) - assert abs(energy) == Energy.from_kilowatt_hours(6.2) - assert abs(-energy) == Energy.from_kilowatt_hours(6.2) - - freq = Frequency.from_hertz(50) - assert abs(freq) == Frequency.from_hertz(50) - assert abs(-freq) == Frequency.from_hertz(50) - - pct = Percentage.from_fraction(30) - assert abs(pct) == Percentage.from_fraction(30) - assert abs(-pct) == Percentage.from_fraction(30) - - -@pytest.mark.parametrize("quantity_ctor", _QUANTITY_CTORS + [Quantity]) -# Use a small amount to avoid long running tests, we have too many combinations -@hypothesis.settings(max_examples=10) -@hypothesis.given( - quantity_value=st.floats( - allow_infinity=False, - allow_nan=False, - allow_subnormal=False, - # We need to set this because otherwise constructors with big exponents will - # cause the value to be too big for the float type, and the test will fail. - max_value=1e298, - min_value=-1e298, - ), - percent=st.floats(allow_infinity=False, allow_nan=False, allow_subnormal=False), -) -def test_quantity_multiplied_with_precentage( - quantity_ctor: type[Quantity], quantity_value: float, percent: float -) -> None: - """Test the multiplication of all quantities with percentage.""" - percentage = Percentage.from_percent(percent) - quantity = quantity_ctor(quantity_value) - expected_value = quantity.base_value * (percent / 100.0) - print(f"{quantity=}, {percentage=}, {expected_value=}") - - product = quantity * percentage - print(f"{product=}") - assert product.base_value == expected_value - - quantity *= percentage - print(f"*{quantity=}") - assert quantity.base_value == expected_value - - -@pytest.mark.parametrize("quantity_ctor", _QUANTITY_CTORS + [Quantity]) -# Use a small amount to avoid long running tests, we have too many combinations -@hypothesis.settings(max_examples=10) -@hypothesis.given( - quantity_value=st.floats( - allow_infinity=False, - allow_nan=False, - allow_subnormal=False, - # We need to set this because otherwise constructors with big exponents will - # cause the value to be too big for the float type, and the test will fail. - max_value=1e298, - min_value=-1e298, - ), - scalar=st.floats(allow_infinity=False, allow_nan=False, allow_subnormal=False), -) -def test_quantity_multiplied_with_float( - quantity_ctor: type[Quantity], quantity_value: float, scalar: float -) -> None: - """Test the multiplication of all quantities with a float.""" - quantity = quantity_ctor(quantity_value) - expected_value = quantity.base_value * scalar - print(f"{quantity=}, {expected_value=}") - - product = quantity * scalar - print(f"{product=}") - assert product.base_value == expected_value - - quantity *= scalar - print(f"*{quantity=}") - assert quantity.base_value == expected_value - - -def test_invalid_multiplications() -> None: - """Test the multiplication of quantities with invalid quantities.""" - power = Power.from_watts(1000.0) - voltage = Voltage.from_volts(230.0) - current = Current.from_amperes(2) - energy = Energy.from_kilowatt_hours(12) - - for quantity in [power, voltage, current, energy]: - with pytest.raises(TypeError): - _ = power * quantity # type: ignore - with pytest.raises(TypeError): - power *= quantity # type: ignore - - for quantity in [voltage, power, energy]: - with pytest.raises(TypeError): - _ = voltage * quantity # type: ignore - with pytest.raises(TypeError): - voltage *= quantity # type: ignore - - for quantity in [current, power, energy]: - with pytest.raises(TypeError): - _ = current * quantity # type: ignore - with pytest.raises(TypeError): - current *= quantity # type: ignore - - for quantity in [energy, power, voltage, current]: - with pytest.raises(TypeError): - _ = energy * quantity # type: ignore - with pytest.raises(TypeError): - energy *= quantity # type: ignore - - -@pytest.mark.parametrize("quantity_ctor", _QUANTITY_CTORS + [Quantity]) -# Use a small amount to avoid long running tests, we have too many combinations -@hypothesis.settings(max_examples=10) -@hypothesis.given( - quantity_value=st.floats( - allow_infinity=False, - allow_nan=False, - allow_subnormal=False, - # We need to set this because otherwise constructors with big exponents will - # cause the value to be too big for the float type, and the test will fail. - max_value=1e298, - min_value=-1e298, - ), - scalar=st.floats(allow_infinity=False, allow_nan=False, allow_subnormal=False), -) -def test_quantity_divided_by_float( - quantity_ctor: type[Quantity], quantity_value: float, scalar: float -) -> None: - """Test the division of all quantities by a float.""" - hypothesis.assume(scalar != 0.0) - quantity = quantity_ctor(quantity_value) - expected_value = quantity.base_value / scalar - print(f"{quantity=}, {expected_value=}") - - quotient = quantity / scalar - print(f"{quotient=}") - assert quotient.base_value == expected_value - - quantity /= scalar - print(f"*{quantity=}") - assert quantity.base_value == expected_value - - -@pytest.mark.parametrize("quantity_ctor", _QUANTITY_CTORS + [Quantity]) -# Use a small amount to avoid long running tests, we have too many combinations -@hypothesis.settings(max_examples=10) -@hypothesis.given( - quantity_value=st.floats( - allow_infinity=False, - allow_nan=False, - allow_subnormal=False, - # We need to set this because otherwise constructors with big exponents will - # cause the value to be too big for the float type, and the test will fail. - max_value=1e298, - min_value=-1e298, - ), - divisor_value=st.floats( - allow_infinity=False, allow_nan=False, allow_subnormal=False - ), -) -def test_quantity_divided_by_self( - quantity_ctor: type[Quantity], quantity_value: float, divisor_value: float -) -> None: - """Test the division of all quantities by a float.""" - hypothesis.assume(divisor_value != 0.0) - # We need to have float here because quantity /= divisor will return a float - quantity: Quantity | float = quantity_ctor(quantity_value) - divisor = quantity_ctor(divisor_value) - assert isinstance(quantity, Quantity) - expected_value = quantity.base_value / divisor.base_value - print(f"{quantity=}, {expected_value=}") - - quotient = quantity / divisor - print(f"{quotient=}") - assert isinstance(quotient, float) - assert quotient == expected_value - - quantity /= divisor - print(f"*{quantity=}") - assert isinstance(quantity, float) - assert quantity == expected_value - - -@pytest.mark.parametrize( - "divisor", - [ - Energy.from_kilowatt_hours(500.0), - Frequency.from_hertz(50), - Power.from_watts(1000.0), - Quantity(30.0), - Temperature.from_celsius(30), - Voltage.from_volts(230.0), - ], - ids=lambda q: q.__class__.__name__, -) -def test_invalid_current_divisions(divisor: Quantity) -> None: - """Test the divisions of current with invalid quantities.""" - current = Current.from_amperes(2) - - with pytest.raises(TypeError): - _ = current / divisor # type: ignore - with pytest.raises(TypeError): - current /= divisor # type: ignore - - -@pytest.mark.parametrize( - "divisor", - [ - Current.from_amperes(2), - Frequency.from_hertz(50), - Quantity(30.0), - Temperature.from_celsius(30), - Voltage.from_volts(230.0), - ], - ids=lambda q: q.__class__.__name__, -) -def test_invalid_energy_divisions(divisor: Quantity) -> None: - """Test the divisions of energy with invalid quantities.""" - energy = Energy.from_kilowatt_hours(500.0) - - with pytest.raises(TypeError): - _ = energy / divisor # type: ignore - with pytest.raises(TypeError): - energy /= divisor # type: ignore - - -@pytest.mark.parametrize( - "divisor", - [ - Current.from_amperes(2), - Energy.from_kilowatt_hours(500.0), - Power.from_watts(1000.0), - Quantity(30.0), - Temperature.from_celsius(30), - Voltage.from_volts(230.0), - ], - ids=lambda q: q.__class__.__name__, -) -def test_invalid_frequency_divisions(divisor: Quantity) -> None: - """Test the divisions of frequency with invalid quantities.""" - frequency = Frequency.from_hertz(50) - - with pytest.raises(TypeError): - _ = frequency / divisor # type: ignore - with pytest.raises(TypeError): - frequency /= divisor # type: ignore - - -@pytest.mark.parametrize( - "divisor", - [ - Current.from_amperes(2), - Energy.from_kilowatt_hours(500.0), - Frequency.from_hertz(50), - Power.from_watts(1000.0), - Quantity(30.0), - Temperature.from_celsius(30), - Voltage.from_volts(230.0), - ], - ids=lambda q: q.__class__.__name__, -) -def test_invalid_percentage_divisions(divisor: Quantity) -> None: - """Test the divisions of percentage with invalid quantities.""" - percentage = Percentage.from_percent(50.0) - - with pytest.raises(TypeError): - _ = percentage / divisor # type: ignore - with pytest.raises(TypeError): - percentage /= divisor # type: ignore - - -@pytest.mark.parametrize( - "divisor", - [ - Energy.from_kilowatt_hours(500.0), - Frequency.from_hertz(50), - Quantity(30.0), - Temperature.from_celsius(30), - ], - ids=lambda q: q.__class__.__name__, -) -def test_invalid_power_divisions(divisor: Quantity) -> None: - """Test the divisions of power with invalid quantities.""" - power = Power.from_watts(1000.0) - - with pytest.raises(TypeError): - _ = power / divisor # type: ignore - with pytest.raises(TypeError): - power /= divisor # type: ignore - - -@pytest.mark.parametrize( - "divisor", - [ - Current.from_amperes(2), - Energy.from_kilowatt_hours(500.0), - Frequency.from_hertz(50), - Power.from_watts(1000.0), - Temperature.from_celsius(30), - Voltage.from_volts(230.0), - ], - ids=lambda q: q.__class__.__name__, -) -def test_invalid_quantity_divisions(divisor: Quantity) -> None: - """Test the divisions of quantity with invalid quantities.""" - quantity = Quantity(30.0) - - with pytest.raises(TypeError): - _ = quantity / divisor - with pytest.raises(TypeError): - quantity /= divisor # type: ignore[assignment] - - -@pytest.mark.parametrize( - "divisor", - [ - Current.from_amperes(2), - Energy.from_kilowatt_hours(500.0), - Frequency.from_hertz(50), - Power.from_watts(1000.0), - Quantity(30.0), - Voltage.from_volts(230.0), - ], - ids=lambda q: q.__class__.__name__, -) -def test_invalid_temperature_divisions(divisor: Quantity) -> None: - """Test the divisions of temperature with invalid quantities.""" - temperature = Temperature.from_celsius(30) - - with pytest.raises(TypeError): - _ = temperature / divisor # type: ignore - with pytest.raises(TypeError): - temperature /= divisor # type: ignore - - -@pytest.mark.parametrize( - "divisor", - [ - Current.from_amperes(2), - Energy.from_kilowatt_hours(500.0), - Frequency.from_hertz(50), - Power.from_watts(1000.0), - Quantity(30.0), - Temperature.from_celsius(30), - ], - ids=lambda q: q.__class__.__name__, -) -def test_invalid_voltage_divisions(divisor: Quantity) -> None: - """Test the divisions of voltage with invalid quantities.""" - voltage = Voltage.from_volts(230.0) - - with pytest.raises(TypeError): - _ = voltage / divisor # type: ignore - with pytest.raises(TypeError): - voltage /= divisor # type: ignore - - -# 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("exponent", [0, 3, 6, 9]) -@hypothesis.settings( - max_examples=1000 -) # Set to have a decent amount of examples (default is 100) -@hypothesis.seed(42) # Seed that triggers a lot of problematic edge cases -@hypothesis.given(value=st.floats(min_value=-1.0, max_value=1.0)) -def test_to_and_from_string( - quantity_type: type[Quantity], exponent: int, value: float -) -> None: - """Test string parsing and formatting. - - The parameters for this test are constructed to stay deterministic. - - With a different (or random) seed or different max_examples the - test will show failing examples. - - Fixing those cases was considered an unreasonable amount of work - at the time of writing. - - For the future, one idea was to parse the string number after the first - generation and regenerate it with the more appropriate unit and precision. - """ - quantity = quantity_type.__new__(quantity_type) - quantity._base_value = value * 10**exponent # pylint: disable=protected-access - # The above should be replaced with: - # quantity = quantity_type._new( # pylint: disable=protected-access - # value, exponent=exponent - # ) - # But we can't do that now, because, you guessed it, it will also break the tests - # (_new() will use 10.0**exponent instead of 10**exponent, which seems to have some - # effect on the tests. - quantity_str = f"{quantity:.{exponent}}" - from_string = quantity_type.from_string(quantity_str) - try: - assert f"{from_string:.{exponent}}" == quantity_str - except AssertionError as error: - pytest.fail( - f"Failed for {quantity.base_value} != from_string({from_string.base_value}) " - + f"with exponent {exponent} and source value '{value}': {error}" - ) diff --git a/tests/timeseries/test_resampling.py b/tests/timeseries/test_resampling.py index b982d2064..81598fa30 100644 --- a/tests/timeseries/test_resampling.py +++ b/tests/timeseries/test_resampling.py @@ -14,9 +14,9 @@ import pytest import time_machine from frequenz.channels import Broadcast, SenderError +from frequenz.quantities import Quantity from frequenz.sdk.timeseries import Sample -from frequenz.sdk.timeseries._quantities import Quantity from frequenz.sdk.timeseries._resampling import ( DEFAULT_BUFFER_LEN_MAX, DEFAULT_BUFFER_LEN_WARN, diff --git a/tests/timeseries/test_ringbuffer.py b/tests/timeseries/test_ringbuffer.py index 04b1012bc..f4667ea78 100644 --- a/tests/timeseries/test_ringbuffer.py +++ b/tests/timeseries/test_ringbuffer.py @@ -11,9 +11,9 @@ import numpy as np import pytest +from frequenz.quantities import Quantity from frequenz.sdk.timeseries import Sample -from frequenz.sdk.timeseries._quantities import Quantity from frequenz.sdk.timeseries._ringbuffer import Gap, OrderedRingBuffer from frequenz.sdk.timeseries._ringbuffer.buffer import FloatArray diff --git a/tests/timeseries/test_ringbuffer_serialization.py b/tests/timeseries/test_ringbuffer_serialization.py index d86fb4d8e..60136e47d 100644 --- a/tests/timeseries/test_ringbuffer_serialization.py +++ b/tests/timeseries/test_ringbuffer_serialization.py @@ -10,10 +10,10 @@ import numpy as np import pytest +from frequenz.quantities import Quantity import frequenz.sdk.timeseries._ringbuffer as rb from frequenz.sdk.timeseries import Sample -from frequenz.sdk.timeseries._quantities import Quantity FIVE_MINUTES = timedelta(minutes=5) _29_DAYS = 60 * 24 * 29