frequenz-quantities 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,112 @@
1
+ # License: MIT
2
+ # Copyright © 2024 Frequenz Energy-as-a-Service GmbH
3
+
4
+ """Types for holding quantities with units.
5
+
6
+ This library provide types for holding quantities with units. The main goal is to avoid
7
+ mistakes while working with different types of quantities, for example avoiding adding
8
+ a length to a time.
9
+
10
+ It also prevents mistakes when operating between the same quantity but in different
11
+ units, like adding a power in Joules to a power in Watts without converting one of them.
12
+
13
+ Quantities store the value in a base unit, and then provide methods to get that quantity
14
+ as a particular unit. They can only be constructed using special constructors with the
15
+ form `Quantity.from_<unit>`, for example
16
+ [`Power.from_watts(10.0)`][frequenz.quantities.Power.from_watts].
17
+
18
+ Internally quantities store values as `float`s, so regular [float issues and limitations
19
+ apply](https://docs.python.org/3/tutorial/floatingpoint.html), although some of them are
20
+ tried to be mitigated.
21
+
22
+ Quantities are also immutable, so operations between quantities return a new instance of
23
+ the quantity.
24
+
25
+ This library provides the following types:
26
+
27
+ - [ApparentPower][frequenz.quantities.ApparentPower]: A quantity representing apparent
28
+ power.
29
+ - [Current][frequenz.quantities.Current]: A quantity representing an electric current.
30
+ - [Energy][frequenz.quantities.Energy]: A quantity representing energy.
31
+ - [Frequency][frequenz.quantities.Frequency]: A quantity representing frequency.
32
+ - [Percentage][frequenz.quantities.Percentage]: A quantity representing a percentage.
33
+ - [Power][frequenz.quantities.Power]: A quantity representing power.
34
+ - [ReactivePower][frequenz.quantities.ReactivePower]: A quantity representing reactive
35
+ power.
36
+ - [Temperature][frequenz.quantities.Temperature]: A quantity representing temperature.
37
+ - [Voltage][frequenz.quantities.Voltage]: A quantity representing electric voltage.
38
+
39
+ There is also the unitless [Quantity][frequenz.quantities.Quantity] class. All
40
+ quantities are subclasses of this class and it can be used as a base to create new
41
+ quantities. Using the `Quantity` class directly is discouraged, as it doesn't provide
42
+ any unit conversion methods.
43
+
44
+ Example:
45
+ ```python
46
+ from datetime import timedelta
47
+ from frequenz.quantities import Power, Voltage, Current, Energy
48
+
49
+ # Create a power quantity
50
+ power = Power.from_watts(230.0)
51
+
52
+ # Printing uses a unit to make the string as short as possible
53
+ print(f"Power: {power}") # Power: 230.0 W
54
+ # The precision can be changed
55
+ print(f"Power: {power:0.3}") # Power: 230.000 W
56
+ # The conversion methods can be used to get the value in a particular unit
57
+ print(f"Power in MW: {power.as_megawatt()}") # Power in MW: 0.00023 MW
58
+
59
+ # Create a voltage quantity
60
+ voltage = Voltage.from_volts(230.0)
61
+
62
+ # Calculate the current
63
+ current = power / voltage
64
+ assert isinstance(current, Current)
65
+ print(f"Current: {current}") # Current: 1.0 A
66
+ assert current.isclose(Current.from_amperes(1.0))
67
+
68
+ # Calculate the energy
69
+ energy = power * timedelta(hours=1)
70
+ assert isinstance(energy, Energy)
71
+ print(f"Energy: {energy}") # Energy: 230.0 Wh
72
+ print(f"Energy in kWh: {energy.as_kilowatt_hours()}") # Energy in kWh: 0.23
73
+
74
+ # Invalid operations are not permitted
75
+ # (when using a type hinting linter like mypy, this will be caught at linting time)
76
+ try:
77
+ power + voltage
78
+ except TypeError as e:
79
+ print(f"Error: {e}") # Error: unsupported operand type(s) for +: 'Power' and 'Voltage'
80
+ ```
81
+
82
+ This library also provides an [**experimental** module with marshmallow fields and
83
+ a base schema][frequenz.quantities.experimental.marshmallow] to serialize and
84
+ deserialize quantities using the marshmallow library. To use it, you need to make sure
85
+ to install this package with the `marshmallow` optional dependencies (e.g.
86
+ `pip install frequenz-quantities[marshmallow]`).
87
+ """
88
+
89
+
90
+ from ._apparent_power import ApparentPower
91
+ from ._current import Current
92
+ from ._energy import Energy
93
+ from ._frequency import Frequency
94
+ from ._percentage import Percentage
95
+ from ._power import Power
96
+ from ._quantity import Quantity
97
+ from ._reactive_power import ReactivePower
98
+ from ._temperature import Temperature
99
+ from ._voltage import Voltage
100
+
101
+ __all__ = [
102
+ "ApparentPower",
103
+ "Current",
104
+ "Energy",
105
+ "Frequency",
106
+ "Percentage",
107
+ "Power",
108
+ "Quantity",
109
+ "ReactivePower",
110
+ "Temperature",
111
+ "Voltage",
112
+ ]
@@ -0,0 +1,234 @@
1
+ # License: MIT
2
+ # Copyright © 2024 Frequenz Energy-as-a-Service GmbH
3
+
4
+ """Types for holding apparent power quantities with units."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import TYPE_CHECKING, Self, overload
9
+
10
+ from ._quantity import NoDefaultConstructible, Quantity
11
+
12
+ if TYPE_CHECKING:
13
+ from ._current import Current
14
+ from ._percentage import Percentage
15
+ from ._voltage import Voltage
16
+
17
+
18
+ class ApparentPower(
19
+ Quantity,
20
+ metaclass=NoDefaultConstructible,
21
+ exponent_unit_map={
22
+ -3: "mVA",
23
+ 0: "VA",
24
+ 3: "kVA",
25
+ 6: "MVA",
26
+ },
27
+ ):
28
+ """A apparent power quantity.
29
+
30
+ Objects of this type are wrappers around `float` values and are immutable.
31
+
32
+ The constructors accept a single `float` value, the `as_*()` methods return a
33
+ `float` value, and each of the arithmetic operators supported by this type are
34
+ actually implemented using floating-point arithmetic.
35
+
36
+ So all considerations about floating-point arithmetic apply to this type as well.
37
+ """
38
+
39
+ @classmethod
40
+ def from_volt_amperes(cls, value: float) -> Self:
41
+ """Initialize a new apparent power quantity.
42
+
43
+ Args:
44
+ value: The apparent power in volt-amperes (VA).
45
+
46
+ Returns:
47
+ A new apparent power quantity.
48
+ """
49
+ return cls._new(value)
50
+
51
+ @classmethod
52
+ def from_milli_volt_amperes(cls, mva: float) -> Self:
53
+ """Initialize a new apparent power quantity.
54
+
55
+ Args:
56
+ mva: The apparent power in millivolt-amperes (mVA).
57
+
58
+ Returns:
59
+ A new apparent power quantity.
60
+ """
61
+ return cls._new(mva, exponent=-3)
62
+
63
+ @classmethod
64
+ def from_kilo_volt_amperes(cls, kva: float) -> Self:
65
+ """Initialize a new apparent power quantity.
66
+
67
+ Args:
68
+ kva: The apparent power in kilovolt-amperes (kVA).
69
+
70
+ Returns:
71
+ A new apparent power quantity.
72
+ """
73
+ return cls._new(kva, exponent=3)
74
+
75
+ @classmethod
76
+ def from_mega_volt_amperes(cls, mva: float) -> Self:
77
+ """Initialize a new apparent power quantity.
78
+
79
+ Args:
80
+ mva: The apparent power in megavolt-amperes (MVA).
81
+
82
+ Returns:
83
+ A new apparent power quantity.
84
+ """
85
+ return cls._new(mva, exponent=6)
86
+
87
+ def as_volt_amperes(self) -> float:
88
+ """Return the apparent power in volt-amperes (VA).
89
+
90
+ Returns:
91
+ The apparent power in volt-amperes (VA).
92
+ """
93
+ return self._base_value
94
+
95
+ def as_milli_volt_amperes(self) -> float:
96
+ """Return the apparent power in millivolt-amperes (mVA).
97
+
98
+ Returns:
99
+ The apparent power in millivolt-amperes (mVA).
100
+ """
101
+ return self._base_value * 1e3
102
+
103
+ def as_kilo_volt_amperes(self) -> float:
104
+ """Return the apparent power in kilovolt-amperes (kVA).
105
+
106
+ Returns:
107
+ The apparent power in kilovolt-amperes (kVA).
108
+ """
109
+ return self._base_value / 1e3
110
+
111
+ def as_mega_volt_amperes(self) -> float:
112
+ """Return the apparent power in megavolt-amperes (MVA).
113
+
114
+ Returns:
115
+ The apparent power in megavolt-amperes (MVA).
116
+ """
117
+ return self._base_value / 1e6
118
+
119
+ @overload
120
+ def __mul__(self, scalar: float, /) -> Self:
121
+ """Scale this power by a scalar.
122
+
123
+ Args:
124
+ scalar: The scalar by which to scale this power.
125
+
126
+ Returns:
127
+ The scaled power.
128
+ """
129
+
130
+ @overload
131
+ def __mul__(self, percent: Percentage, /) -> Self:
132
+ """Scale this power by a percentage.
133
+
134
+ Args:
135
+ percent: The percentage by which to scale this power.
136
+
137
+ Returns:
138
+ The scaled power.
139
+ """
140
+
141
+ def __mul__(self, other: float | Percentage, /) -> Self:
142
+ """Return a power or energy from multiplying this power by the given value.
143
+
144
+ Args:
145
+ other: The scalar, percentage or duration to multiply by.
146
+
147
+ Returns:
148
+ A power or energy.
149
+ """
150
+ from ._percentage import Percentage # pylint: disable=import-outside-toplevel
151
+
152
+ match other:
153
+ case float() | Percentage():
154
+ return super().__mul__(other)
155
+ case _:
156
+ return NotImplemented
157
+
158
+ # We need the ignore here because otherwise mypy will give this error:
159
+ # > Overloaded operator methods can't have wider argument types in overrides
160
+ # The problem seems to be when the other type implements an **incompatible**
161
+ # __rmul__ method, which is not the case here, so we should be safe.
162
+ # Please see this example:
163
+ # https://github.com/python/mypy/blob/c26f1297d4f19d2d1124a30efc97caebb8c28616/test-data/unit/check-overloading.test#L4738C1-L4769C55
164
+ # And a discussion in a mypy issue here:
165
+ # https://github.com/python/mypy/issues/4985#issuecomment-389692396
166
+ @overload # type: ignore[override]
167
+ def __truediv__(self, other: float, /) -> Self:
168
+ """Divide this power by a scalar.
169
+
170
+ Args:
171
+ other: The scalar to divide this power by.
172
+
173
+ Returns:
174
+ The divided power.
175
+ """
176
+
177
+ @overload
178
+ def __truediv__(self, other: Self, /) -> float:
179
+ """Return the ratio of this power to another.
180
+
181
+ Args:
182
+ other: The other power.
183
+
184
+ Returns:
185
+ The ratio of this power to another.
186
+ """
187
+
188
+ @overload
189
+ def __truediv__(self, current: Current, /) -> Voltage:
190
+ """Return a voltage from dividing this power by the given current.
191
+
192
+ Args:
193
+ current: The current to divide by.
194
+
195
+ Returns:
196
+ A voltage from dividing this power by the a current.
197
+ """
198
+
199
+ @overload
200
+ def __truediv__(self, voltage: Voltage, /) -> Current:
201
+ """Return a current from dividing this power by the given voltage.
202
+
203
+ Args:
204
+ voltage: The voltage to divide by.
205
+
206
+ Returns:
207
+ A current from dividing this power by a voltage.
208
+ """
209
+
210
+ def __truediv__(
211
+ self, other: float | Self | Current | Voltage, /
212
+ ) -> Self | float | Voltage | Current:
213
+ """Return a current or voltage from dividing this power by the given value.
214
+
215
+ Args:
216
+ other: The scalar, power, current or voltage to divide by.
217
+
218
+ Returns:
219
+ A current or voltage from dividing this power by the given value.
220
+ """
221
+ from ._current import Current # pylint: disable=import-outside-toplevel
222
+ from ._voltage import Voltage # pylint: disable=import-outside-toplevel
223
+
224
+ match other:
225
+ case float():
226
+ return super().__truediv__(other)
227
+ case ApparentPower():
228
+ return self._base_value / other._base_value
229
+ case Current():
230
+ return Voltage._new(self._base_value / other._base_value)
231
+ case Voltage():
232
+ return Current._new(self._base_value / other._base_value)
233
+ case _:
234
+ return NotImplemented
@@ -0,0 +1,131 @@
1
+ # License: MIT
2
+ # Copyright © 2022 Frequenz Energy-as-a-Service GmbH
3
+
4
+ """Types for holding quantities with units."""
5
+
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING, Self, overload
10
+
11
+ from ._quantity import NoDefaultConstructible, Quantity
12
+
13
+ if TYPE_CHECKING:
14
+ from ._percentage import Percentage
15
+ from ._power import Power
16
+ from ._voltage import Voltage
17
+
18
+
19
+ class Current(
20
+ Quantity,
21
+ metaclass=NoDefaultConstructible,
22
+ exponent_unit_map={
23
+ -3: "mA",
24
+ 0: "A",
25
+ },
26
+ ):
27
+ """A current quantity.
28
+
29
+ Objects of this type are wrappers around `float` values and are immutable.
30
+
31
+ The constructors accept a single `float` value, the `as_*()` methods return a
32
+ `float` value, and each of the arithmetic operators supported by this type are
33
+ actually implemented using floating-point arithmetic.
34
+
35
+ So all considerations about floating-point arithmetic apply to this type as well.
36
+ """
37
+
38
+ @classmethod
39
+ def from_amperes(cls, amperes: float) -> Self:
40
+ """Initialize a new current quantity.
41
+
42
+ Args:
43
+ amperes: The current in amperes.
44
+
45
+ Returns:
46
+ A new current quantity.
47
+ """
48
+ return cls._new(amperes)
49
+
50
+ @classmethod
51
+ def from_milliamperes(cls, milliamperes: float) -> Self:
52
+ """Initialize a new current quantity.
53
+
54
+ Args:
55
+ milliamperes: The current in milliamperes.
56
+
57
+ Returns:
58
+ A new current quantity.
59
+ """
60
+ return cls._new(milliamperes, exponent=-3)
61
+
62
+ def as_amperes(self) -> float:
63
+ """Return the current in amperes.
64
+
65
+ Returns:
66
+ The current in amperes.
67
+ """
68
+ return self._base_value
69
+
70
+ def as_milliamperes(self) -> float:
71
+ """Return the current in milliamperes.
72
+
73
+ Returns:
74
+ The current in milliamperes.
75
+ """
76
+ return self._base_value * 1e3
77
+
78
+ # See comment for Power.__mul__ for why we need the ignore here.
79
+ @overload # type: ignore[override]
80
+ def __mul__(self, scalar: float, /) -> Self:
81
+ """Scale this current by a scalar.
82
+
83
+ Args:
84
+ scalar: The scalar by which to scale this current.
85
+
86
+ Returns:
87
+ The scaled current.
88
+ """
89
+
90
+ @overload
91
+ def __mul__(self, percent: Percentage, /) -> Self:
92
+ """Scale this current by a percentage.
93
+
94
+ Args:
95
+ percent: The percentage by which to scale this current.
96
+
97
+ Returns:
98
+ The scaled current.
99
+ """
100
+
101
+ @overload
102
+ def __mul__(self, other: Voltage, /) -> Power:
103
+ """Multiply the current by a voltage to get a power.
104
+
105
+ Args:
106
+ other: The voltage.
107
+
108
+ Returns:
109
+ The calculated power.
110
+ """
111
+
112
+ def __mul__(self, other: float | Percentage | Voltage, /) -> Self | Power:
113
+ """Return a current or power from multiplying this current by the given value.
114
+
115
+ Args:
116
+ other: The scalar, percentage or voltage to multiply by.
117
+
118
+ Returns:
119
+ A current or power.
120
+ """
121
+ from ._percentage import Percentage # pylint: disable=import-outside-toplevel
122
+ from ._power import Power # pylint: disable=import-outside-toplevel
123
+ from ._voltage import Voltage # pylint: disable=import-outside-toplevel
124
+
125
+ match other:
126
+ case float() | Percentage():
127
+ return super().__mul__(other)
128
+ case Voltage():
129
+ return Power._new(self._base_value * other._base_value)
130
+ case _:
131
+ return NotImplemented
@@ -0,0 +1,188 @@
1
+ # License: MIT
2
+ # Copyright © 2022 Frequenz Energy-as-a-Service GmbH
3
+
4
+ """Types for holding quantities with units."""
5
+
6
+
7
+ from __future__ import annotations
8
+
9
+ from datetime import timedelta
10
+ from typing import TYPE_CHECKING, Self, overload
11
+
12
+ from ._quantity import NoDefaultConstructible, Quantity
13
+
14
+ if TYPE_CHECKING:
15
+ from ._percentage import Percentage
16
+ from ._power import Power
17
+
18
+
19
+ class Energy(
20
+ Quantity,
21
+ metaclass=NoDefaultConstructible,
22
+ exponent_unit_map={
23
+ 0: "Wh",
24
+ 3: "kWh",
25
+ 6: "MWh",
26
+ },
27
+ ):
28
+ """An energy quantity.
29
+
30
+ Objects of this type are wrappers around `float` values and are immutable.
31
+
32
+ The constructors accept a single `float` value, the `as_*()` methods return a
33
+ `float` value, and each of the arithmetic operators supported by this type are
34
+ actually implemented using floating-point arithmetic.
35
+
36
+ So all considerations about floating-point arithmetic apply to this type as well.
37
+ """
38
+
39
+ @classmethod
40
+ def from_watt_hours(cls, watt_hours: float) -> Self:
41
+ """Initialize a new energy quantity.
42
+
43
+ Args:
44
+ watt_hours: The energy in watt hours.
45
+
46
+ Returns:
47
+ A new energy quantity.
48
+ """
49
+ return cls._new(watt_hours)
50
+
51
+ @classmethod
52
+ def from_kilowatt_hours(cls, kilowatt_hours: float) -> Self:
53
+ """Initialize a new energy quantity.
54
+
55
+ Args:
56
+ kilowatt_hours: The energy in kilowatt hours.
57
+
58
+ Returns:
59
+ A new energy quantity.
60
+ """
61
+ return cls._new(kilowatt_hours, exponent=3)
62
+
63
+ @classmethod
64
+ def from_megawatt_hours(cls, megawatt_hours: float) -> Self:
65
+ """Initialize a new energy quantity.
66
+
67
+ Args:
68
+ megawatt_hours: The energy in megawatt hours.
69
+
70
+ Returns:
71
+ A new energy quantity.
72
+ """
73
+ return cls._new(megawatt_hours, exponent=6)
74
+
75
+ def as_watt_hours(self) -> float:
76
+ """Return the energy in watt hours.
77
+
78
+ Returns:
79
+ The energy in watt hours.
80
+ """
81
+ return self._base_value
82
+
83
+ def as_kilowatt_hours(self) -> float:
84
+ """Return the energy in kilowatt hours.
85
+
86
+ Returns:
87
+ The energy in kilowatt hours.
88
+ """
89
+ return self._base_value / 1e3
90
+
91
+ def as_megawatt_hours(self) -> float:
92
+ """Return the energy in megawatt hours.
93
+
94
+ Returns:
95
+ The energy in megawatt hours.
96
+ """
97
+ return self._base_value / 1e6
98
+
99
+ def __mul__(self, other: float | Percentage) -> Self:
100
+ """Scale this energy by a percentage.
101
+
102
+ Args:
103
+ other: The percentage by which to scale this energy.
104
+
105
+ Returns:
106
+ The scaled energy.
107
+ """
108
+ from ._percentage import Percentage # pylint: disable=import-outside-toplevel
109
+
110
+ match other:
111
+ case float():
112
+ return self._new(self._base_value * other)
113
+ case Percentage():
114
+ return self._new(self._base_value * other.as_fraction())
115
+ case _:
116
+ return NotImplemented
117
+
118
+ # See the comment for Power.__mul__ for why we need the ignore here.
119
+ @overload # type: ignore[override]
120
+ def __truediv__(self, other: float, /) -> Self:
121
+ """Divide this energy by a scalar.
122
+
123
+ Args:
124
+ other: The scalar to divide this energy by.
125
+
126
+ Returns:
127
+ The divided energy.
128
+ """
129
+
130
+ @overload
131
+ def __truediv__(self, other: Self, /) -> float:
132
+ """Return the ratio of this energy to another.
133
+
134
+ Args:
135
+ other: The other energy.
136
+
137
+ Returns:
138
+ The ratio of this energy to another.
139
+ """
140
+
141
+ @overload
142
+ def __truediv__(self, duration: timedelta, /) -> Power:
143
+ """Return a power from dividing this energy by the given duration.
144
+
145
+ Args:
146
+ duration: The duration to divide by.
147
+
148
+ Returns:
149
+ A power from dividing this energy by the given duration.
150
+ """
151
+
152
+ @overload
153
+ def __truediv__(self, power: Power, /) -> timedelta:
154
+ """Return a duration from dividing this energy by the given power.
155
+
156
+ Args:
157
+ power: The power to divide by.
158
+
159
+ Returns:
160
+ A duration from dividing this energy by the given power.
161
+ """
162
+
163
+ def __truediv__(
164
+ self, other: float | Self | timedelta | Power, /
165
+ ) -> Self | float | Power | timedelta:
166
+ """Return a power or duration from dividing this energy by the given value.
167
+
168
+ Args:
169
+ other: The scalar, energy, power or duration to divide by.
170
+
171
+ Returns:
172
+ A power or duration from dividing this energy by the given value.
173
+ """
174
+ from ._power import Power # pylint: disable=import-outside-toplevel
175
+
176
+ match other:
177
+ case float():
178
+ return super().__truediv__(other)
179
+ case Energy():
180
+ return self._base_value / other._base_value
181
+ case timedelta():
182
+ return Power._new(self._base_value / (other.total_seconds() / 3600.0))
183
+ case Power():
184
+ return timedelta(
185
+ seconds=(self._base_value / other._base_value) * 3600.0
186
+ )
187
+ case _:
188
+ return NotImplemented