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.
- frequenz/quantities/__init__.py +112 -0
- frequenz/quantities/_apparent_power.py +234 -0
- frequenz/quantities/_current.py +131 -0
- frequenz/quantities/_energy.py +188 -0
- frequenz/quantities/_frequency.py +115 -0
- frequenz/quantities/_percentage.py +66 -0
- frequenz/quantities/_power.py +244 -0
- frequenz/quantities/_quantity.py +541 -0
- frequenz/quantities/_reactive_power.py +235 -0
- frequenz/quantities/_temperature.py +39 -0
- frequenz/quantities/_voltage.py +148 -0
- frequenz/quantities/conftest.py +13 -0
- frequenz/quantities/experimental/__init__.py +11 -0
- frequenz/quantities/experimental/marshmallow.py +271 -0
- frequenz/quantities/py.typed +0 -0
- frequenz_quantities-1.0.0.dist-info/LICENSE +21 -0
- frequenz_quantities-1.0.0.dist-info/METADATA +103 -0
- frequenz_quantities-1.0.0.dist-info/RECORD +20 -0
- frequenz_quantities-1.0.0.dist-info/WHEEL +5 -0
- frequenz_quantities-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -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
|