python-units 0.3.0__py3-none-any.whl → 0.4.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.
- api/public.py +50 -0
- api/si.py +51 -1
- core/__init__.py +8 -0
- core/quantity.py +226 -12
- core/unit_definitions.py +170 -17
- {python_units-0.3.0.dist-info → python_units-0.4.0.dist-info}/METADATA +183 -2
- {python_units-0.3.0.dist-info → python_units-0.4.0.dist-info}/RECORD +11 -11
- units/unit.py +16 -0
- {python_units-0.3.0.dist-info → python_units-0.4.0.dist-info}/WHEEL +0 -0
- {python_units-0.3.0.dist-info → python_units-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {python_units-0.3.0.dist-info → python_units-0.4.0.dist-info}/top_level.txt +0 -0
api/public.py
CHANGED
|
@@ -5,20 +5,40 @@ from api.si import (
|
|
|
5
5
|
ampere,
|
|
6
6
|
becquerel,
|
|
7
7
|
candela,
|
|
8
|
+
centimetre,
|
|
8
9
|
coulomb,
|
|
9
10
|
degree_celcius,
|
|
10
11
|
farad,
|
|
12
|
+
gram,
|
|
11
13
|
gray,
|
|
12
14
|
henry,
|
|
13
15
|
hertz,
|
|
16
|
+
hour,
|
|
14
17
|
joule,
|
|
15
18
|
katal,
|
|
16
19
|
kelvin,
|
|
17
20
|
kilogram,
|
|
21
|
+
kiloampere,
|
|
22
|
+
kilometre,
|
|
23
|
+
kilovolt,
|
|
24
|
+
kilowatt,
|
|
18
25
|
lumen,
|
|
19
26
|
lux,
|
|
27
|
+
megawatt,
|
|
20
28
|
metre,
|
|
29
|
+
microgram,
|
|
30
|
+
micrometre,
|
|
31
|
+
microsecond,
|
|
32
|
+
milliampere,
|
|
33
|
+
milligram,
|
|
34
|
+
millimetre,
|
|
35
|
+
millisecond,
|
|
36
|
+
millivolt,
|
|
37
|
+
milliwatt,
|
|
38
|
+
minute,
|
|
21
39
|
mole,
|
|
40
|
+
nanometre,
|
|
41
|
+
nanosecond,
|
|
22
42
|
newton,
|
|
23
43
|
ohm,
|
|
24
44
|
pascal,
|
|
@@ -28,6 +48,7 @@ from api.si import (
|
|
|
28
48
|
sievert,
|
|
29
49
|
steradian,
|
|
30
50
|
tesla,
|
|
51
|
+
tonne,
|
|
31
52
|
volt,
|
|
32
53
|
watt,
|
|
33
54
|
weber,
|
|
@@ -43,12 +64,16 @@ from core.quantity import (
|
|
|
43
64
|
Quantity,
|
|
44
65
|
complex_quantity,
|
|
45
66
|
complex_unit,
|
|
67
|
+
convert,
|
|
46
68
|
float_quantity,
|
|
47
69
|
float_unit,
|
|
48
70
|
int_quantity,
|
|
49
71
|
int_unit,
|
|
50
72
|
long_quantity,
|
|
51
73
|
long_unit,
|
|
74
|
+
multiplier,
|
|
75
|
+
unit,
|
|
76
|
+
value,
|
|
52
77
|
)
|
|
53
78
|
from core.unit_definitions import BaseUnit, CustomUnitBase, DerivedUnit, SIUnit
|
|
54
79
|
from models.dimension import Dimension, DimensionSystem
|
|
@@ -73,28 +98,50 @@ __all__ = [
|
|
|
73
98
|
"ampere",
|
|
74
99
|
"becquerel",
|
|
75
100
|
"candela",
|
|
101
|
+
"centimetre",
|
|
76
102
|
"complex_quantity",
|
|
77
103
|
"complex_unit",
|
|
104
|
+
"convert",
|
|
78
105
|
"coulomb",
|
|
79
106
|
"degree_celcius",
|
|
80
107
|
"farad",
|
|
81
108
|
"float_quantity",
|
|
82
109
|
"float_unit",
|
|
110
|
+
"gram",
|
|
83
111
|
"gray",
|
|
84
112
|
"henry",
|
|
85
113
|
"hertz",
|
|
114
|
+
"hour",
|
|
86
115
|
"int_quantity",
|
|
87
116
|
"int_unit",
|
|
88
117
|
"joule",
|
|
89
118
|
"katal",
|
|
90
119
|
"kelvin",
|
|
91
120
|
"kilogram",
|
|
121
|
+
"kiloampere",
|
|
122
|
+
"kilometre",
|
|
123
|
+
"kilovolt",
|
|
124
|
+
"kilowatt",
|
|
92
125
|
"long_quantity",
|
|
93
126
|
"long_unit",
|
|
94
127
|
"lumen",
|
|
95
128
|
"lux",
|
|
129
|
+
"megawatt",
|
|
96
130
|
"metre",
|
|
131
|
+
"microgram",
|
|
132
|
+
"micrometre",
|
|
133
|
+
"microsecond",
|
|
134
|
+
"milliampere",
|
|
135
|
+
"milligram",
|
|
136
|
+
"millimetre",
|
|
137
|
+
"millisecond",
|
|
138
|
+
"millivolt",
|
|
139
|
+
"milliwatt",
|
|
140
|
+
"minute",
|
|
141
|
+
"multiplier",
|
|
97
142
|
"mole",
|
|
143
|
+
"nanometre",
|
|
144
|
+
"nanosecond",
|
|
98
145
|
"newton",
|
|
99
146
|
"ohm",
|
|
100
147
|
"pascal",
|
|
@@ -104,6 +151,9 @@ __all__ = [
|
|
|
104
151
|
"sievert",
|
|
105
152
|
"steradian",
|
|
106
153
|
"tesla",
|
|
154
|
+
"tonne",
|
|
155
|
+
"unit",
|
|
156
|
+
"value",
|
|
107
157
|
"volt",
|
|
108
158
|
"watt",
|
|
109
159
|
"weber",
|
api/si.py
CHANGED
|
@@ -29,7 +29,11 @@ siemens = DerivedUnit.define("S", ampere / volt)
|
|
|
29
29
|
weber = DerivedUnit.define("Wb", volt * second)
|
|
30
30
|
tesla = DerivedUnit.define("T", weber / metre / metre)
|
|
31
31
|
henry = DerivedUnit.define("H", weber / ampere)
|
|
32
|
-
degree_celcius = DerivedUnit.define(
|
|
32
|
+
degree_celcius = DerivedUnit.define(
|
|
33
|
+
"°C",
|
|
34
|
+
kelvin,
|
|
35
|
+
supports_multiplicative_conversion=False,
|
|
36
|
+
)
|
|
33
37
|
lumen = DerivedUnit.define("lm", candela * steradian)
|
|
34
38
|
lux = DerivedUnit.define("lx", lumen / metre / metre)
|
|
35
39
|
becquerel = DerivedUnit.define("Bq", SIUnit() / second)
|
|
@@ -37,6 +41,31 @@ gray = DerivedUnit.define("Gy", joule / kilogram)
|
|
|
37
41
|
sievert = DerivedUnit.define("Sv", joule / kilogram)
|
|
38
42
|
katal = DerivedUnit.define("kat", mole / second)
|
|
39
43
|
|
|
44
|
+
kilometre = DerivedUnit.define("km", metre, conversion_factor=1000.0)
|
|
45
|
+
centimetre = DerivedUnit.define("cm", metre, conversion_factor=0.01)
|
|
46
|
+
millimetre = DerivedUnit.define("mm", metre, conversion_factor=0.001)
|
|
47
|
+
micrometre = DerivedUnit.define("µm", metre, conversion_factor=0.000001)
|
|
48
|
+
nanometre = DerivedUnit.define("nm", metre, conversion_factor=0.000000001)
|
|
49
|
+
|
|
50
|
+
gram = DerivedUnit.define("g", kilogram, conversion_factor=0.001)
|
|
51
|
+
milligram = DerivedUnit.define("mg", kilogram, conversion_factor=0.000001)
|
|
52
|
+
microgram = DerivedUnit.define("µg", kilogram, conversion_factor=0.000000001)
|
|
53
|
+
tonne = DerivedUnit.define("t", kilogram, conversion_factor=1000.0)
|
|
54
|
+
|
|
55
|
+
minute = DerivedUnit.define("min", second, conversion_factor=60.0)
|
|
56
|
+
hour = DerivedUnit.define("h", second, conversion_factor=3600.0)
|
|
57
|
+
millisecond = DerivedUnit.define("ms", second, conversion_factor=0.001)
|
|
58
|
+
microsecond = DerivedUnit.define("µs", second, conversion_factor=0.000001)
|
|
59
|
+
nanosecond = DerivedUnit.define("ns", second, conversion_factor=0.000000001)
|
|
60
|
+
|
|
61
|
+
milliampere = DerivedUnit.define("mA", ampere, conversion_factor=0.001)
|
|
62
|
+
kiloampere = DerivedUnit.define("kA", ampere, conversion_factor=1000.0)
|
|
63
|
+
millivolt = DerivedUnit.define("mV", volt, conversion_factor=0.001)
|
|
64
|
+
kilovolt = DerivedUnit.define("kV", volt, conversion_factor=1000.0)
|
|
65
|
+
milliwatt = DerivedUnit.define("mW", watt, conversion_factor=0.001)
|
|
66
|
+
kilowatt = DerivedUnit.define("kW", watt, conversion_factor=1000.0)
|
|
67
|
+
megawatt = DerivedUnit.define("MW", watt, conversion_factor=1000000.0)
|
|
68
|
+
|
|
40
69
|
for unit in (
|
|
41
70
|
newton,
|
|
42
71
|
pascal,
|
|
@@ -59,20 +88,40 @@ __all__ = [
|
|
|
59
88
|
"ampere",
|
|
60
89
|
"becquerel",
|
|
61
90
|
"candela",
|
|
91
|
+
"centimetre",
|
|
62
92
|
"coulomb",
|
|
63
93
|
"degree_celcius",
|
|
64
94
|
"farad",
|
|
95
|
+
"gram",
|
|
65
96
|
"gray",
|
|
66
97
|
"henry",
|
|
67
98
|
"hertz",
|
|
99
|
+
"hour",
|
|
68
100
|
"joule",
|
|
69
101
|
"katal",
|
|
70
102
|
"kelvin",
|
|
71
103
|
"kilogram",
|
|
104
|
+
"kiloampere",
|
|
105
|
+
"kilometre",
|
|
106
|
+
"kilovolt",
|
|
107
|
+
"kilowatt",
|
|
72
108
|
"lumen",
|
|
73
109
|
"lux",
|
|
110
|
+
"megawatt",
|
|
74
111
|
"metre",
|
|
112
|
+
"microgram",
|
|
113
|
+
"micrometre",
|
|
114
|
+
"microsecond",
|
|
115
|
+
"milliampere",
|
|
116
|
+
"milligram",
|
|
117
|
+
"millimetre",
|
|
118
|
+
"millisecond",
|
|
119
|
+
"millivolt",
|
|
120
|
+
"milliwatt",
|
|
121
|
+
"minute",
|
|
75
122
|
"mole",
|
|
123
|
+
"nanometre",
|
|
124
|
+
"nanosecond",
|
|
76
125
|
"newton",
|
|
77
126
|
"ohm",
|
|
78
127
|
"pascal",
|
|
@@ -82,6 +131,7 @@ __all__ = [
|
|
|
82
131
|
"sievert",
|
|
83
132
|
"steradian",
|
|
84
133
|
"tesla",
|
|
134
|
+
"tonne",
|
|
85
135
|
"volt",
|
|
86
136
|
"watt",
|
|
87
137
|
"weber",
|
core/__init__.py
CHANGED
|
@@ -11,12 +11,16 @@ from .quantity import (
|
|
|
11
11
|
Quantity,
|
|
12
12
|
complex_quantity,
|
|
13
13
|
complex_unit,
|
|
14
|
+
convert,
|
|
14
15
|
float_quantity,
|
|
15
16
|
float_unit,
|
|
16
17
|
int_quantity,
|
|
17
18
|
int_unit,
|
|
18
19
|
long_quantity,
|
|
19
20
|
long_unit,
|
|
21
|
+
multiplier,
|
|
22
|
+
unit,
|
|
23
|
+
value,
|
|
20
24
|
)
|
|
21
25
|
from .unit_definitions import BaseUnit, CustomUnitBase, DerivedUnit, SIUnit
|
|
22
26
|
|
|
@@ -33,10 +37,14 @@ __all__ = [
|
|
|
33
37
|
"UnitsError",
|
|
34
38
|
"complex_quantity",
|
|
35
39
|
"complex_unit",
|
|
40
|
+
"convert",
|
|
36
41
|
"float_quantity",
|
|
37
42
|
"float_unit",
|
|
38
43
|
"int_quantity",
|
|
39
44
|
"int_unit",
|
|
40
45
|
"long_quantity",
|
|
41
46
|
"long_unit",
|
|
47
|
+
"multiplier",
|
|
48
|
+
"unit",
|
|
49
|
+
"value",
|
|
42
50
|
]
|
core/quantity.py
CHANGED
|
@@ -5,7 +5,13 @@ from __future__ import annotations
|
|
|
5
5
|
|
|
6
6
|
from core.deprecations import deprecated_call
|
|
7
7
|
from core.errors import InvalidValueError, UnitCompatibilityError, UnitOperandError
|
|
8
|
-
from core.unit_definitions import
|
|
8
|
+
from core.unit_definitions import (
|
|
9
|
+
BaseUnit,
|
|
10
|
+
DerivedUnit,
|
|
11
|
+
SIUnit,
|
|
12
|
+
clone_unit,
|
|
13
|
+
require_unit_instance,
|
|
14
|
+
)
|
|
9
15
|
from models.dimension import SI_DIMENSION_SYSTEM
|
|
10
16
|
from utils.numbers import Scalar, is_number, is_real_number, validate_numeric_value
|
|
11
17
|
|
|
@@ -18,6 +24,49 @@ def require_quantity_operand(operand: object, operation: str) -> None:
|
|
|
18
24
|
)
|
|
19
25
|
|
|
20
26
|
|
|
27
|
+
def normalize_scalar(value: Scalar) -> Scalar:
|
|
28
|
+
"""Return ``int`` for exact integer floats, otherwise return ``value``."""
|
|
29
|
+
if isinstance(value, float) and value.is_integer():
|
|
30
|
+
return int(value)
|
|
31
|
+
return value
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def normalize_product(value: Scalar, left: Scalar, right: Scalar) -> Scalar:
|
|
35
|
+
"""Normalize exact integer products only when both inputs were integers."""
|
|
36
|
+
if isinstance(left, int) and isinstance(right, int):
|
|
37
|
+
return normalize_scalar(value)
|
|
38
|
+
return value
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def normalize_reverse_floor(value: Scalar, denominator: "Quantity") -> Scalar:
|
|
42
|
+
"""Preserve legacy int display for unscaled integer reverse floor division."""
|
|
43
|
+
if (
|
|
44
|
+
denominator.unit.conversion_factor == 1.0
|
|
45
|
+
and isinstance(denominator.value, int)
|
|
46
|
+
):
|
|
47
|
+
return normalize_scalar(value)
|
|
48
|
+
return value
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def normalize_result_unit(result_unit: BaseUnit) -> BaseUnit:
|
|
52
|
+
"""Return a renderable result unit with no hidden anonymous scale."""
|
|
53
|
+
if isinstance(result_unit, DerivedUnit) or result_unit.conversion_factor == 1.0:
|
|
54
|
+
return result_unit
|
|
55
|
+
try:
|
|
56
|
+
return result_unit.__class__(
|
|
57
|
+
dimension=result_unit.dimension,
|
|
58
|
+
supports_multiplicative_conversion=(
|
|
59
|
+
result_unit.supports_multiplicative_conversion
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
except TypeError:
|
|
63
|
+
unit = result_unit.__class__(dimension=result_unit.dimension)
|
|
64
|
+
unit._supports_multiplicative_conversion = (
|
|
65
|
+
result_unit.supports_multiplicative_conversion
|
|
66
|
+
)
|
|
67
|
+
return unit
|
|
68
|
+
|
|
69
|
+
|
|
21
70
|
class Quantity:
|
|
22
71
|
"""A numeric value coupled to a unit definition."""
|
|
23
72
|
|
|
@@ -81,6 +130,44 @@ class Quantity:
|
|
|
81
130
|
"unsupported scalar for {}: {}".format(operation, type(value).__name__)
|
|
82
131
|
)
|
|
83
132
|
|
|
133
|
+
def _base_value(self) -> Scalar:
|
|
134
|
+
return self.value * self.unit.conversion_factor
|
|
135
|
+
|
|
136
|
+
def to(self, target_unit: BaseUnit) -> "Quantity":
|
|
137
|
+
"""
|
|
138
|
+
Convert this quantity to a compatible scale-only target unit.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
target_unit: Unit definition with the same dimension.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Quantity expressed in ``target_unit``.
|
|
145
|
+
|
|
146
|
+
Raises:
|
|
147
|
+
InvalidUnitError: If ``target_unit`` is not a unit definition.
|
|
148
|
+
UnitCompatibilityError: If dimensions differ or either unit cannot
|
|
149
|
+
be converted with a multiplicative scale factor.
|
|
150
|
+
"""
|
|
151
|
+
require_unit_instance(target_unit)
|
|
152
|
+
if self.unit.dimension != target_unit.dimension:
|
|
153
|
+
raise UnitCompatibilityError(
|
|
154
|
+
"cannot convert {} to {}".format(self.unit, target_unit)
|
|
155
|
+
)
|
|
156
|
+
if (
|
|
157
|
+
not self.unit.supports_multiplicative_conversion
|
|
158
|
+
or not target_unit.supports_multiplicative_conversion
|
|
159
|
+
):
|
|
160
|
+
raise UnitCompatibilityError(
|
|
161
|
+
"units require a non-multiplicative conversion: {} and {}".format(
|
|
162
|
+
self.unit,
|
|
163
|
+
target_unit,
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
return self.__class__(
|
|
167
|
+
normalize_scalar(self._base_value() / target_unit.conversion_factor),
|
|
168
|
+
target_unit,
|
|
169
|
+
)
|
|
170
|
+
|
|
84
171
|
def __add__(self, quantity2: object) -> "Quantity":
|
|
85
172
|
self._require_compatible_quantity(quantity2, "addition")
|
|
86
173
|
return self.__class__(self.value + quantity2.value, self.unit)
|
|
@@ -98,9 +185,22 @@ class Quantity:
|
|
|
98
185
|
|
|
99
186
|
def __mul__(self, quantity2: object) -> "Quantity":
|
|
100
187
|
if isinstance(quantity2, Quantity):
|
|
101
|
-
|
|
188
|
+
result_unit = self.unit * quantity2.unit
|
|
189
|
+
result_value = self._base_value() * quantity2._base_value()
|
|
190
|
+
return self.__class__(
|
|
191
|
+
normalize_product(result_value, self.value, quantity2.value),
|
|
192
|
+
normalize_result_unit(result_unit),
|
|
193
|
+
)
|
|
102
194
|
if isinstance(quantity2, BaseUnit):
|
|
103
|
-
|
|
195
|
+
result_unit = self.unit * quantity2
|
|
196
|
+
result_value = (
|
|
197
|
+
self._base_value()
|
|
198
|
+
* quantity2.conversion_factor
|
|
199
|
+
)
|
|
200
|
+
return self.__class__(
|
|
201
|
+
normalize_scalar(result_value),
|
|
202
|
+
normalize_result_unit(result_unit),
|
|
203
|
+
)
|
|
104
204
|
self._require_numeric_scalar(quantity2, "multiplication")
|
|
105
205
|
return self.__class__(self.value * quantity2, self.unit)
|
|
106
206
|
|
|
@@ -109,29 +209,55 @@ class Quantity:
|
|
|
109
209
|
|
|
110
210
|
def __truediv__(self, quantity2: object) -> "Quantity":
|
|
111
211
|
if isinstance(quantity2, Quantity):
|
|
112
|
-
|
|
212
|
+
result_unit = self.unit / quantity2.unit
|
|
213
|
+
result_value = self._base_value() / quantity2._base_value()
|
|
214
|
+
return self.__class__(result_value, normalize_result_unit(result_unit))
|
|
113
215
|
if isinstance(quantity2, BaseUnit):
|
|
114
|
-
|
|
216
|
+
result_unit = self.unit / quantity2
|
|
217
|
+
result_value = (
|
|
218
|
+
self._base_value()
|
|
219
|
+
/ quantity2.conversion_factor
|
|
220
|
+
)
|
|
221
|
+
return self.__class__(
|
|
222
|
+
normalize_scalar(result_value),
|
|
223
|
+
normalize_result_unit(result_unit),
|
|
224
|
+
)
|
|
115
225
|
self._require_numeric_scalar(quantity2, "division")
|
|
116
226
|
return self.__class__(self.value / quantity2, self.unit)
|
|
117
227
|
|
|
118
228
|
def __rtruediv__(self, quantity2: object) -> "Quantity":
|
|
119
229
|
if isinstance(quantity2, Quantity):
|
|
120
|
-
|
|
230
|
+
result_unit = quantity2.unit / self.unit
|
|
231
|
+
result_value = quantity2._base_value() / self._base_value()
|
|
232
|
+
return self.__class__(result_value, normalize_result_unit(result_unit))
|
|
121
233
|
self._require_numeric_scalar(quantity2, "division")
|
|
122
|
-
|
|
234
|
+
result_unit = self._dimensionless_unit() / self.unit
|
|
235
|
+
result_value = quantity2 / self._base_value()
|
|
236
|
+
return self.__class__(result_value, normalize_result_unit(result_unit))
|
|
123
237
|
|
|
124
238
|
def __floordiv__(self, quantity2: object) -> "Quantity":
|
|
125
239
|
if isinstance(quantity2, Quantity):
|
|
126
|
-
|
|
240
|
+
result_unit = self.unit / quantity2.unit
|
|
241
|
+
result_value = self._base_value() // quantity2._base_value()
|
|
242
|
+
return self.__class__(result_value, normalize_result_unit(result_unit))
|
|
127
243
|
self._require_real_scalar(quantity2, "floor division")
|
|
128
244
|
return self.__class__(self.value // quantity2, self.unit)
|
|
129
245
|
|
|
130
246
|
def __rfloordiv__(self, quantity2: object) -> "Quantity":
|
|
131
247
|
if isinstance(quantity2, Quantity):
|
|
132
|
-
|
|
248
|
+
result_unit = quantity2.unit / self.unit
|
|
249
|
+
result_value = quantity2._base_value() // self._base_value()
|
|
250
|
+
return self.__class__(
|
|
251
|
+
normalize_scalar(result_value),
|
|
252
|
+
normalize_result_unit(result_unit),
|
|
253
|
+
)
|
|
133
254
|
self._require_real_scalar(quantity2, "floor division")
|
|
134
|
-
|
|
255
|
+
result_unit = self._dimensionless_unit() / self.unit
|
|
256
|
+
result_value = quantity2 // self._base_value()
|
|
257
|
+
return self.__class__(
|
|
258
|
+
normalize_reverse_floor(result_value, self),
|
|
259
|
+
normalize_result_unit(result_unit),
|
|
260
|
+
)
|
|
135
261
|
|
|
136
262
|
def __mod__(self, quantity2: object) -> "Quantity":
|
|
137
263
|
if isinstance(quantity2, Quantity):
|
|
@@ -145,7 +271,12 @@ class Quantity:
|
|
|
145
271
|
self._require_compatible_quantity(quantity2, "modulo")
|
|
146
272
|
return self.__class__(quantity2.value % self.value, self.unit)
|
|
147
273
|
self._require_real_scalar(quantity2, "modulo")
|
|
148
|
-
|
|
274
|
+
result_unit = self._dimensionless_unit() / self.unit
|
|
275
|
+
result_value = quantity2 % self._base_value()
|
|
276
|
+
return self.__class__(
|
|
277
|
+
normalize_scalar(result_value),
|
|
278
|
+
normalize_result_unit(result_unit),
|
|
279
|
+
)
|
|
149
280
|
|
|
150
281
|
def __divmod__(self, quantity2: object) -> tuple["Quantity", "Quantity"]:
|
|
151
282
|
return self.__floordiv__(quantity2), self.__mod__(quantity2)
|
|
@@ -163,7 +294,12 @@ class Quantity:
|
|
|
163
294
|
)
|
|
164
295
|
if self.is_unitless:
|
|
165
296
|
return self.__class__(self.value ** exponent, self.unit)
|
|
166
|
-
|
|
297
|
+
result_unit = self.unit ** exponent
|
|
298
|
+
result_value = self._base_value() ** exponent
|
|
299
|
+
return self.__class__(
|
|
300
|
+
normalize_scalar(result_value),
|
|
301
|
+
normalize_result_unit(result_unit),
|
|
302
|
+
)
|
|
167
303
|
|
|
168
304
|
def __neg__(self) -> "Quantity":
|
|
169
305
|
return self.__class__(-self.value, self.unit)
|
|
@@ -215,6 +351,84 @@ def complex_quantity(quantity: Quantity) -> Quantity:
|
|
|
215
351
|
return Quantity(complex(quantity.value), quantity.unit)
|
|
216
352
|
|
|
217
353
|
|
|
354
|
+
def convert(quantity: Quantity, target_unit: BaseUnit) -> Quantity:
|
|
355
|
+
"""
|
|
356
|
+
Convert a quantity to a compatible scale-only target unit.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
quantity: Quantity to convert.
|
|
360
|
+
target_unit: Unit definition with the same dimension.
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
Quantity expressed in ``target_unit``.
|
|
364
|
+
|
|
365
|
+
Raises:
|
|
366
|
+
UnitOperandError: If ``quantity`` is not a Quantity.
|
|
367
|
+
InvalidUnitError: If ``target_unit`` is not a unit definition.
|
|
368
|
+
UnitCompatibilityError: If conversion is not scale-only compatible.
|
|
369
|
+
"""
|
|
370
|
+
require_quantity_operand(quantity, "conversion")
|
|
371
|
+
return quantity.to(target_unit)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def value(quantity: Quantity) -> Scalar:
|
|
375
|
+
"""
|
|
376
|
+
Return the numeric value of a quantity without converting it.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
quantity: Quantity to inspect.
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
Numeric value stored on the quantity.
|
|
383
|
+
|
|
384
|
+
Raises:
|
|
385
|
+
UnitOperandError: If ``quantity`` is not a Quantity.
|
|
386
|
+
"""
|
|
387
|
+
require_quantity_operand(quantity, "value extraction")
|
|
388
|
+
return quantity.value
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def unit(quantity: Quantity) -> BaseUnit:
|
|
392
|
+
"""
|
|
393
|
+
Return the unit definition of a quantity.
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
quantity: Quantity to inspect.
|
|
397
|
+
|
|
398
|
+
Returns:
|
|
399
|
+
Unit definition stored on the quantity.
|
|
400
|
+
|
|
401
|
+
Raises:
|
|
402
|
+
UnitOperandError: If ``quantity`` is not a Quantity.
|
|
403
|
+
"""
|
|
404
|
+
require_quantity_operand(quantity, "unit extraction")
|
|
405
|
+
return quantity.unit
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def multiplier(quantity_or_unit: Quantity | BaseUnit) -> float:
|
|
409
|
+
"""
|
|
410
|
+
Return the multiplicative factor to the canonical base dimension.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
quantity_or_unit: Quantity or unit definition to inspect.
|
|
414
|
+
|
|
415
|
+
Returns:
|
|
416
|
+
Multiplicative factor for the unit carried by the input.
|
|
417
|
+
|
|
418
|
+
Raises:
|
|
419
|
+
UnitOperandError: If the input is neither Quantity nor BaseUnit.
|
|
420
|
+
"""
|
|
421
|
+
if isinstance(quantity_or_unit, Quantity):
|
|
422
|
+
return quantity_or_unit.unit.conversion_factor
|
|
423
|
+
if isinstance(quantity_or_unit, BaseUnit):
|
|
424
|
+
return quantity_or_unit.conversion_factor
|
|
425
|
+
raise UnitOperandError(
|
|
426
|
+
"unsupported operand for multiplier extraction: {}".format(
|
|
427
|
+
type(quantity_or_unit).__name__
|
|
428
|
+
)
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
|
|
218
432
|
def int_unit(quantity: Quantity) -> Quantity:
|
|
219
433
|
"""Legacy compatibility helper equivalent to ``int_quantity``."""
|
|
220
434
|
return deprecated_call(
|
core/unit_definitions.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
-
from numbers import Number
|
|
6
|
+
from numbers import Number, Real
|
|
7
7
|
from types import MappingProxyType
|
|
8
8
|
from typing import Dict, Mapping
|
|
9
9
|
|
|
@@ -36,13 +36,50 @@ def require_unit_instance(unit: object) -> None:
|
|
|
36
36
|
)
|
|
37
37
|
|
|
38
38
|
|
|
39
|
+
def validate_conversion_factor(conversion_factor: object) -> float:
|
|
40
|
+
"""Validate a multiplicative conversion factor and return it as float."""
|
|
41
|
+
if (
|
|
42
|
+
not isinstance(conversion_factor, Real)
|
|
43
|
+
or isinstance(conversion_factor, bool)
|
|
44
|
+
or conversion_factor <= 0
|
|
45
|
+
):
|
|
46
|
+
raise InvalidValueError(
|
|
47
|
+
"conversion factor must be a positive real scalar, got {}".format(
|
|
48
|
+
type(conversion_factor).__name__
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
return float(conversion_factor)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def validate_conversion_support(supports_conversion: object) -> bool:
|
|
55
|
+
"""Validate a conversion support flag and return it."""
|
|
56
|
+
if not isinstance(supports_conversion, bool):
|
|
57
|
+
raise InvalidValueError(
|
|
58
|
+
"conversion support flag must be bool, got {}".format(
|
|
59
|
+
type(supports_conversion).__name__
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
return supports_conversion
|
|
63
|
+
|
|
64
|
+
|
|
39
65
|
def clone_unit(unit: "BaseUnit | None") -> "BaseUnit":
|
|
40
66
|
"""Clone a unit definition while preserving its type."""
|
|
41
67
|
if unit is None:
|
|
42
68
|
return SIUnit()
|
|
43
69
|
require_unit_instance(unit)
|
|
44
70
|
|
|
45
|
-
|
|
71
|
+
try:
|
|
72
|
+
cloned_unit = unit.__class__(
|
|
73
|
+
dimension=unit.dimension,
|
|
74
|
+
conversion_factor=unit.conversion_factor,
|
|
75
|
+
supports_multiplicative_conversion=unit.supports_multiplicative_conversion,
|
|
76
|
+
)
|
|
77
|
+
except TypeError:
|
|
78
|
+
cloned_unit = unit.__class__(dimension=unit.dimension)
|
|
79
|
+
cloned_unit._conversion_factor = unit.conversion_factor
|
|
80
|
+
cloned_unit._supports_multiplicative_conversion = (
|
|
81
|
+
unit.supports_multiplicative_conversion
|
|
82
|
+
)
|
|
46
83
|
if isinstance(unit, DerivedUnit):
|
|
47
84
|
cloned_unit.name = unit.name
|
|
48
85
|
return cloned_unit
|
|
@@ -61,11 +98,20 @@ class BaseUnit:
|
|
|
61
98
|
|
|
62
99
|
dimension_system = SI_DIMENSION_SYSTEM
|
|
63
100
|
|
|
64
|
-
def __init__(
|
|
101
|
+
def __init__(
|
|
102
|
+
self,
|
|
103
|
+
dimension: Dimension | None = None,
|
|
104
|
+
conversion_factor: float = 1.0,
|
|
105
|
+
supports_multiplicative_conversion: bool = True,
|
|
106
|
+
) -> None:
|
|
65
107
|
self._dimension = dimension or Dimension(
|
|
66
108
|
system=self.dimension_system,
|
|
67
109
|
exponents=(0,) * len(self.dimension_system.symbols),
|
|
68
110
|
)
|
|
111
|
+
self._conversion_factor = validate_conversion_factor(conversion_factor)
|
|
112
|
+
self._supports_multiplicative_conversion = validate_conversion_support(
|
|
113
|
+
supports_multiplicative_conversion
|
|
114
|
+
)
|
|
69
115
|
|
|
70
116
|
@property
|
|
71
117
|
def unit_dict(self) -> Dict[str, int]:
|
|
@@ -85,16 +131,32 @@ class BaseUnit:
|
|
|
85
131
|
def dimension(self, dimension: Dimension) -> None:
|
|
86
132
|
self._dimension = dimension
|
|
87
133
|
|
|
134
|
+
@property
|
|
135
|
+
def conversion_factor(self) -> float:
|
|
136
|
+
"""Return the multiplicative factor to this dimension's base unit."""
|
|
137
|
+
return self._conversion_factor
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def supports_multiplicative_conversion(self) -> bool:
|
|
141
|
+
"""Return whether this unit can use scale-only conversion."""
|
|
142
|
+
return self._supports_multiplicative_conversion
|
|
143
|
+
|
|
88
144
|
def __eq__(self, unit2: object) -> bool:
|
|
89
145
|
if not isinstance(unit2, BaseUnit):
|
|
90
146
|
return False
|
|
91
147
|
if self.dimension != unit2.dimension:
|
|
92
148
|
return False
|
|
149
|
+
if self.conversion_factor != unit2.conversion_factor:
|
|
150
|
+
return False
|
|
93
151
|
if isinstance(self, DerivedUnit) or isinstance(unit2, DerivedUnit):
|
|
94
152
|
return (
|
|
95
153
|
isinstance(self, DerivedUnit)
|
|
96
154
|
and isinstance(unit2, DerivedUnit)
|
|
97
155
|
and self.name == unit2.name
|
|
156
|
+
and (
|
|
157
|
+
self.supports_multiplicative_conversion
|
|
158
|
+
== unit2.supports_multiplicative_conversion
|
|
159
|
+
)
|
|
98
160
|
)
|
|
99
161
|
return True
|
|
100
162
|
|
|
@@ -109,15 +171,48 @@ class BaseUnit:
|
|
|
109
171
|
)
|
|
110
172
|
if operator_name == "mul":
|
|
111
173
|
dimension = self.dimension * unit2.dimension
|
|
174
|
+
conversion_factor = self.conversion_factor * unit2.conversion_factor
|
|
112
175
|
else:
|
|
113
176
|
dimension = self.dimension / unit2.dimension
|
|
177
|
+
conversion_factor = self.conversion_factor / unit2.conversion_factor
|
|
178
|
+
supports_conversion = (
|
|
179
|
+
self.supports_multiplicative_conversion
|
|
180
|
+
and unit2.supports_multiplicative_conversion
|
|
181
|
+
)
|
|
114
182
|
if dimension.system != SI_DIMENSION_SYSTEM:
|
|
115
|
-
return self.__class__(
|
|
116
|
-
|
|
183
|
+
return self.__class__(
|
|
184
|
+
dimension=dimension,
|
|
185
|
+
conversion_factor=conversion_factor,
|
|
186
|
+
supports_multiplicative_conversion=supports_conversion,
|
|
187
|
+
)
|
|
188
|
+
if supports_conversion and conversion_factor == 1.0:
|
|
189
|
+
return resolve_unit(dimension)
|
|
190
|
+
return BaseUnit(
|
|
191
|
+
dimension=dimension,
|
|
192
|
+
conversion_factor=conversion_factor,
|
|
193
|
+
supports_multiplicative_conversion=supports_conversion,
|
|
194
|
+
)
|
|
117
195
|
|
|
118
196
|
def _quantity_from_scalar(self, value: Number) -> object:
|
|
119
|
-
from core.quantity import Quantity
|
|
120
|
-
|
|
197
|
+
from core.quantity import Quantity, normalize_scalar
|
|
198
|
+
|
|
199
|
+
if not isinstance(self, DerivedUnit) and self.conversion_factor != 1.0:
|
|
200
|
+
try:
|
|
201
|
+
unit = self.__class__(
|
|
202
|
+
dimension=self.dimension,
|
|
203
|
+
supports_multiplicative_conversion=(
|
|
204
|
+
self.supports_multiplicative_conversion
|
|
205
|
+
),
|
|
206
|
+
)
|
|
207
|
+
except TypeError:
|
|
208
|
+
unit = self.__class__(dimension=self.dimension)
|
|
209
|
+
unit._supports_multiplicative_conversion = (
|
|
210
|
+
self.supports_multiplicative_conversion
|
|
211
|
+
)
|
|
212
|
+
result_value = value * self.conversion_factor
|
|
213
|
+
if self.conversion_factor.is_integer():
|
|
214
|
+
result_value = normalize_scalar(result_value)
|
|
215
|
+
return Quantity(result_value, unit)
|
|
121
216
|
return Quantity(value, self)
|
|
122
217
|
|
|
123
218
|
def __mul__(self, unit2: object) -> object:
|
|
@@ -152,13 +247,37 @@ class BaseUnit:
|
|
|
152
247
|
system=self.dimension.system,
|
|
153
248
|
exponents=tuple(value * exponent for value in self.dimension.exponents),
|
|
154
249
|
)
|
|
155
|
-
|
|
156
|
-
|
|
250
|
+
conversion_factor = self.conversion_factor**exponent
|
|
251
|
+
if (
|
|
252
|
+
isinstance(self, DerivedUnit)
|
|
253
|
+
and self.name
|
|
254
|
+
and exponent != 1
|
|
255
|
+
and self.conversion_factor == 1.0
|
|
256
|
+
):
|
|
257
|
+
derived = DerivedUnit(
|
|
258
|
+
dimension=dimension,
|
|
259
|
+
conversion_factor=1.0,
|
|
260
|
+
supports_multiplicative_conversion=(
|
|
261
|
+
self.supports_multiplicative_conversion
|
|
262
|
+
),
|
|
263
|
+
)
|
|
157
264
|
derived.name = "{}^{}".format(self.name, exponent)
|
|
158
265
|
return derived
|
|
159
266
|
if dimension.system != SI_DIMENSION_SYSTEM:
|
|
160
|
-
return self.__class__(
|
|
161
|
-
|
|
267
|
+
return self.__class__(
|
|
268
|
+
dimension=dimension,
|
|
269
|
+
conversion_factor=conversion_factor,
|
|
270
|
+
supports_multiplicative_conversion=(
|
|
271
|
+
self.supports_multiplicative_conversion
|
|
272
|
+
),
|
|
273
|
+
)
|
|
274
|
+
if self.supports_multiplicative_conversion and conversion_factor == 1.0:
|
|
275
|
+
return resolve_unit(dimension)
|
|
276
|
+
return BaseUnit(
|
|
277
|
+
dimension=dimension,
|
|
278
|
+
conversion_factor=conversion_factor,
|
|
279
|
+
supports_multiplicative_conversion=self.supports_multiplicative_conversion,
|
|
280
|
+
)
|
|
162
281
|
|
|
163
282
|
def __str__(self) -> str:
|
|
164
283
|
return self.dimension.render()
|
|
@@ -168,7 +287,12 @@ class SIUnit(BaseUnit):
|
|
|
168
287
|
"""Template class for SI units."""
|
|
169
288
|
|
|
170
289
|
@classmethod
|
|
171
|
-
def define(
|
|
290
|
+
def define(
|
|
291
|
+
cls,
|
|
292
|
+
key: str,
|
|
293
|
+
value: int = 1,
|
|
294
|
+
conversion_factor: float = 1.0,
|
|
295
|
+
) -> "SIUnit":
|
|
172
296
|
"""Define an SI unit or exponentiated SI dimension."""
|
|
173
297
|
if key not in cls.dimension_system.symbols:
|
|
174
298
|
raise InvalidUnitError("unknown SI unit key: {}".format(key))
|
|
@@ -176,7 +300,10 @@ class SIUnit(BaseUnit):
|
|
|
176
300
|
raise InvalidValueError(
|
|
177
301
|
"unit exponent must be an integer, got {}".format(type(value).__name__)
|
|
178
302
|
)
|
|
179
|
-
return cls(
|
|
303
|
+
return cls(
|
|
304
|
+
dimension=Dimension.from_mapping({key: value}, system=cls.dimension_system),
|
|
305
|
+
conversion_factor=conversion_factor,
|
|
306
|
+
)
|
|
180
307
|
|
|
181
308
|
|
|
182
309
|
class CustomUnitBase(BaseUnit):
|
|
@@ -185,7 +312,12 @@ class CustomUnitBase(BaseUnit):
|
|
|
185
312
|
dimension_system = DimensionSystem("custom", ())
|
|
186
313
|
|
|
187
314
|
@classmethod
|
|
188
|
-
def define(
|
|
315
|
+
def define(
|
|
316
|
+
cls,
|
|
317
|
+
key: str,
|
|
318
|
+
value: int = 1,
|
|
319
|
+
conversion_factor: float = 1.0,
|
|
320
|
+
) -> "CustomUnitBase":
|
|
189
321
|
"""Define a custom base unit within the subclass dimension system."""
|
|
190
322
|
if key not in cls.dimension_system.symbols:
|
|
191
323
|
raise InvalidUnitError("unknown custom unit key: {}".format(key))
|
|
@@ -193,7 +325,10 @@ class CustomUnitBase(BaseUnit):
|
|
|
193
325
|
raise InvalidValueError(
|
|
194
326
|
"unit exponent must be an integer, got {}".format(type(value).__name__)
|
|
195
327
|
)
|
|
196
|
-
return cls(
|
|
328
|
+
return cls(
|
|
329
|
+
dimension=Dimension.from_mapping({key: value}, system=cls.dimension_system),
|
|
330
|
+
conversion_factor=conversion_factor,
|
|
331
|
+
)
|
|
197
332
|
|
|
198
333
|
|
|
199
334
|
class DerivedUnit(BaseUnit):
|
|
@@ -218,10 +353,28 @@ class DerivedUnit(BaseUnit):
|
|
|
218
353
|
return super().__str__()
|
|
219
354
|
|
|
220
355
|
@classmethod
|
|
221
|
-
def define(
|
|
356
|
+
def define(
|
|
357
|
+
cls,
|
|
358
|
+
name: str,
|
|
359
|
+
unit: BaseUnit,
|
|
360
|
+
conversion_factor: float | None = None,
|
|
361
|
+
supports_multiplicative_conversion: bool | None = None,
|
|
362
|
+
) -> "DerivedUnit":
|
|
222
363
|
"""Define a named derived unit."""
|
|
223
364
|
require_unit_instance(unit)
|
|
224
|
-
obj = cls(
|
|
365
|
+
obj = cls(
|
|
366
|
+
dimension=unit.dimension,
|
|
367
|
+
conversion_factor=(
|
|
368
|
+
unit.conversion_factor
|
|
369
|
+
if conversion_factor is None
|
|
370
|
+
else conversion_factor
|
|
371
|
+
),
|
|
372
|
+
supports_multiplicative_conversion=(
|
|
373
|
+
unit.supports_multiplicative_conversion
|
|
374
|
+
if supports_multiplicative_conversion is None
|
|
375
|
+
else supports_multiplicative_conversion
|
|
376
|
+
),
|
|
377
|
+
)
|
|
225
378
|
obj.name = name
|
|
226
379
|
return obj
|
|
227
380
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-units
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Python library to represent numbers with units
|
|
5
5
|
Author-email: "Paul K. Korir, PhD" <paul.korir@gmail.com>
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -30,7 +30,74 @@ Dynamic: license-file
|
|
|
30
30
|
[](https://pypi.org/project/python-units/)
|
|
31
31
|
[](/Users/paulkorir/PycharmProjects/python-units/tests/unit/test_units.py)
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
# The Price of Unitless Arithmetic
|
|
34
|
+
|
|
35
|
+
On September 23, 1999, flight controllers expected NASA's Mars Climate Orbiter
|
|
36
|
+
to pass behind Mars, fire its engine, and come back into radio contact after
|
|
37
|
+
orbit insertion. It never came back. When engineers reviewed the final hours of
|
|
38
|
+
flight data, the trajectory was not where the navigation system thought it was:
|
|
39
|
+
the spacecraft had approached Mars far lower than planned. The investigation
|
|
40
|
+
traced the loss to a unit boundary that software had failed to defend. One side
|
|
41
|
+
of the system handled "small forces" data in English units; the navigation side
|
|
42
|
+
expected metric units.
|
|
43
|
+
|
|
44
|
+
That is the kind of bug this package is meant to stop. Without units, the
|
|
45
|
+
mistake is just arithmetic:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
# A navigation routine expects impulse in newton-seconds.
|
|
49
|
+
expected_impulse_ns = 120
|
|
50
|
+
|
|
51
|
+
# A supplier routine accidentally sends a value in a different force unit.
|
|
52
|
+
# The number is still just a number, so Python accepts it.
|
|
53
|
+
reported_impulse_other_units = 120
|
|
54
|
+
|
|
55
|
+
trajectory_impulse = expected_impulse_ns + reported_impulse_other_units
|
|
56
|
+
print(trajectory_impulse) # 240
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
There is nothing in `240` that tells you a spacecraft trajectory may now be
|
|
60
|
+
wrong. With units attached, the mismatch stops at the boundary:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from units import CustomUnitBase
|
|
64
|
+
from units.dimension import DimensionSystem
|
|
65
|
+
from units.si import newton, second
|
|
66
|
+
|
|
67
|
+
class EnglishImpulseUnit(CustomUnitBase):
|
|
68
|
+
dimension_system = DimensionSystem("english-impulse", ("lbf_s",))
|
|
69
|
+
|
|
70
|
+
pound_force_second = EnglishImpulseUnit.define("lbf_s")
|
|
71
|
+
|
|
72
|
+
expected_impulse = 120 * newton * second
|
|
73
|
+
reported_impulse = 120 * pound_force_second
|
|
74
|
+
|
|
75
|
+
trajectory_impulse = expected_impulse + reported_impulse
|
|
76
|
+
# UnitCompatibilityError: units mismatch: m·kg·s^-1 and lbf_s
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
That failure is the feature. A bug that would otherwise move through a program
|
|
80
|
+
as an ordinary number is stopped before it contaminates mission-critical
|
|
81
|
+
calculations.
|
|
82
|
+
|
|
83
|
+
Background: NASA/JPL describe the Mars Climate Orbiter loss as a navigation
|
|
84
|
+
error caused by a failure to translate English units to metric, sending the
|
|
85
|
+
spacecraft too close to Mars.
|
|
86
|
+
|
|
87
|
+
Source: https://en.wikipedia.org/wiki/Mars_Climate_Orbiter
|
|
88
|
+
|
|
89
|
+
# About
|
|
90
|
+
|
|
91
|
+
`python-units` is a Python package for unit-aware arithmetic. It provides:
|
|
92
|
+
- a `Quantity` type that combines numeric values with unit information
|
|
93
|
+
- a registry of SI base and derived units
|
|
94
|
+
- algebraic unit manipulation and compatibility checks
|
|
95
|
+
- explicit multiplicative conversions between compatible units
|
|
96
|
+
- a public API that prioritizes scalar-by-unit construction and SI unit imports
|
|
97
|
+
- a migration path from the legacy `Unit` constructor and compatibility helpers
|
|
98
|
+
- a Python 3-only codebase with no Python 2 compatibility shims
|
|
99
|
+
- a project structure that separates public API, core logic, data models, and utilities
|
|
100
|
+
- comprehensive unit tests and documentation
|
|
34
101
|
|
|
35
102
|
Supported Python versions: 3.10+
|
|
36
103
|
|
|
@@ -139,12 +206,18 @@ Stable top-level imports:
|
|
|
139
206
|
|
|
140
207
|
* `Quantity`
|
|
141
208
|
* `Unit` (compatibility alias for `Quantity`)
|
|
209
|
+
* `convert`
|
|
210
|
+
* `value`
|
|
211
|
+
* `unit`
|
|
212
|
+
* `multiplier`
|
|
142
213
|
* `UnitsError`, `InvalidUnitError`, `InvalidValueError`,
|
|
143
214
|
`UnitCompatibilityError`, `UnitOperandError`
|
|
144
215
|
|
|
145
216
|
Canonical unit imports:
|
|
146
217
|
|
|
147
218
|
* `from units.si import metre, second, newton`
|
|
219
|
+
* prefixed and scaled units such as `kilometre`, `centimetre`, `gram`,
|
|
220
|
+
`minute`, `hour`, `kilowatt`, and `millivolt`
|
|
148
221
|
|
|
149
222
|
Legacy compatibility helpers:
|
|
150
223
|
|
|
@@ -166,11 +239,119 @@ deprecated compatibility paths are scheduled for removal in `1.0.0`.
|
|
|
166
239
|
|
|
167
240
|
* Addition and subtraction require identical units.
|
|
168
241
|
* Multiplication and division combine units algebraically.
|
|
242
|
+
* Explicit scale-only conversions are available through `quantity.to(unit)` and
|
|
243
|
+
`convert(quantity, unit)`.
|
|
169
244
|
* Integer powers of units and unit-bearing quantities are supported.
|
|
170
245
|
* Unitless quantities are supported explicitly.
|
|
246
|
+
* Affine conversions, such as `degree_celcius <-> kelvin`, are intentionally not
|
|
247
|
+
implemented yet.
|
|
171
248
|
* The core quantity model allows signed values. Domain-specific constraints such
|
|
172
249
|
as non-negative lengths should be enforced by higher-level types or validators.
|
|
173
250
|
|
|
251
|
+
# Conversion foundations
|
|
252
|
+
|
|
253
|
+
`0.4.0` adds explicit multiplicative conversions. Conversion never happens
|
|
254
|
+
silently during addition or subtraction; you choose the target unit.
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
from units import convert, multiplier, unit, value
|
|
258
|
+
from units.si import gram, hour, kilogram, kilometre, metre, minute, second
|
|
259
|
+
|
|
260
|
+
distance = 1.5 * kilometre
|
|
261
|
+
print(distance.to(metre)) # 1500 m
|
|
262
|
+
print(convert(2500 * metre, kilometre)) # 2.5 km
|
|
263
|
+
|
|
264
|
+
duration = 2 * hour
|
|
265
|
+
print(duration.to(minute)) # 120 min
|
|
266
|
+
print((1500 * gram).to(kilogram)) # 1.5 kg
|
|
267
|
+
|
|
268
|
+
speed = (72 * kilometre) / (2 * hour)
|
|
269
|
+
print(speed) # 10.0 m·s^-1
|
|
270
|
+
|
|
271
|
+
print(value(distance)) # 1.5
|
|
272
|
+
print(unit(distance)) # km
|
|
273
|
+
print(multiplier(kilometre)) # 1000.0
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
The conversion model is scale-only in this release. Celsius is a named
|
|
277
|
+
temperature unit, but converting between Celsius and kelvin requires an offset
|
|
278
|
+
and is reserved for a later affine-conversion release.
|
|
279
|
+
|
|
280
|
+
# Prefixed and scaled units
|
|
281
|
+
|
|
282
|
+
Common SI prefixes and practical time units are available from `units.si`:
|
|
283
|
+
|
|
284
|
+
```python
|
|
285
|
+
from units.si import (
|
|
286
|
+
centimetre,
|
|
287
|
+
gram,
|
|
288
|
+
hour,
|
|
289
|
+
kiloampere,
|
|
290
|
+
kilometre,
|
|
291
|
+
kilovolt,
|
|
292
|
+
kilowatt,
|
|
293
|
+
megawatt,
|
|
294
|
+
micrometre,
|
|
295
|
+
microsecond,
|
|
296
|
+
milliampere,
|
|
297
|
+
milligram,
|
|
298
|
+
millimetre,
|
|
299
|
+
millisecond,
|
|
300
|
+
millivolt,
|
|
301
|
+
milliwatt,
|
|
302
|
+
minute,
|
|
303
|
+
nanometre,
|
|
304
|
+
nanosecond,
|
|
305
|
+
tonne,
|
|
306
|
+
)
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Scaled units participate correctly in multiplication, division, and powers:
|
|
310
|
+
|
|
311
|
+
```python
|
|
312
|
+
from units.si import hour, kilometre, metre
|
|
313
|
+
|
|
314
|
+
area = (2 * kilometre) * (3 * metre)
|
|
315
|
+
print(area) # 6000 m^2
|
|
316
|
+
|
|
317
|
+
square = (2 * kilometre) ** 2
|
|
318
|
+
print(square) # 4000000 m^2
|
|
319
|
+
|
|
320
|
+
speed = (72 * kilometre) / (2 * hour)
|
|
321
|
+
print(speed) # 10.0 m·s^-1
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
# Familiar composite units
|
|
325
|
+
|
|
326
|
+
Composite unit expressions such as `kilometre / hour` are algebraic unit
|
|
327
|
+
definitions. They carry the correct scale factor, but anonymous composite units
|
|
328
|
+
render in canonical SI base form:
|
|
329
|
+
|
|
330
|
+
```python
|
|
331
|
+
from units.si import hour, kilometre
|
|
332
|
+
|
|
333
|
+
speed = 30 * kilometre / hour
|
|
334
|
+
print(speed) # 8.333333333333334 m·s^-1
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
When you want a semantically familiar display unit, give that composite unit an
|
|
338
|
+
explicit name and convert to it:
|
|
339
|
+
|
|
340
|
+
```python
|
|
341
|
+
from units import DerivedUnit, convert
|
|
342
|
+
from units.si import hour, kilometre
|
|
343
|
+
|
|
344
|
+
kilometres_per_hour = DerivedUnit.define("km·hr^-1", kilometre / hour)
|
|
345
|
+
|
|
346
|
+
speed = 30 * kilometre / hour
|
|
347
|
+
print(convert(speed, kilometres_per_hour)) # 30 km·hr^-1
|
|
348
|
+
print(30 * kilometres_per_hour) # 30 km·hr^-1
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
This keeps the arithmetic deterministic while letting application code choose
|
|
352
|
+
domain-specific display names such as `km·hr^-1`, `N·m`, or any other familiar
|
|
353
|
+
derived unit form.
|
|
354
|
+
|
|
174
355
|
# Real-world examples
|
|
175
356
|
|
|
176
357
|
## Electrical engineering: from resistance to power dissipation
|
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
adapters/__init__.py,sha256=CSemt7d9mX9of0YQijrkQJ9FXwzXqN0TcnCeWGhXXC4,64
|
|
2
2
|
api/__init__.py,sha256=EVoYM25GfHSWZnMigUDRsAnnyMnMUoeVZnlxlgQRfUI,72
|
|
3
|
-
api/public.py,sha256=
|
|
4
|
-
api/si.py,sha256=
|
|
5
|
-
core/__init__.py,sha256=
|
|
3
|
+
api/public.py,sha256=sNxV6ZQowrVsqYbMztx7tx_gtjfU6QG7ym9RZQVaAtE,2521
|
|
4
|
+
api/si.py,sha256=kNRegHG1HI87reYFhA3C3zVzLjehVOD8yX2dBIUS7PM,4141
|
|
5
|
+
core/__init__.py,sha256=f3NNOUAbLL9mECMoLLuYoEPbPYmPsP-aL3V1lzitZTA,919
|
|
6
6
|
core/deprecations.py,sha256=SQf13eA5dJt78CvIKuXi3vI1bBknwEPHT3FkDWh0Xmc,1436
|
|
7
7
|
core/errors.py,sha256=iwA73z53DPMPmdoGg-T_PYpyfETqbxwGpxcDYOrsxLY,609
|
|
8
|
-
core/quantity.py,sha256=
|
|
9
|
-
core/unit_definitions.py,sha256=
|
|
8
|
+
core/quantity.py,sha256=aOJNjXfdV01MIg1KqyuJdVhyTt1sPED8kqxoLd3B4s0,17018
|
|
9
|
+
core/unit_definitions.py,sha256=kdof8fIVrV6KGIsDPisEjsS7RHLKPBHXOENyvwQysJw,15887
|
|
10
10
|
models/__init__.py,sha256=8-39jReNyDmbbih6K5IHeqwoWfagjXzjy7nIfJBpPMI,189
|
|
11
11
|
models/dimension.py,sha256=UNlb234AaUim7iXWzc_2RZ5aZCUefZTvOGTNizoQiPc,3789
|
|
12
|
-
python_units-0.
|
|
12
|
+
python_units-0.4.0.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
13
|
services/__init__.py,sha256=tSot9S3F7fHezy3Lj4DNRMQK1ATptRaN0efKRJSgrKs,65
|
|
14
14
|
units/__init__.py,sha256=Tyj9-XgWDco8J7s_wx2jaEP3fylc2Tc6VZIbsZ1ron4,251
|
|
15
15
|
units/dimension.py,sha256=v-aiG3bDV7tJUV_xuxZSAw8UHQe0GnbybI2UkiEwuQM,221
|
|
16
16
|
units/errors.py,sha256=e24NaDCrked0wqWL4kLFLYS0Q1-VB-9QxfHzqrrkoRU,351
|
|
17
17
|
units/quantity.py,sha256=hMYeiQo4EVLjg76Bx88jdNP8b_hXM6shSrXKUXBY3jA,513
|
|
18
18
|
units/si.py,sha256=2wqvff8wxQP1w-fWPYolWuIeBGl942jyX8N6dQyGwDU,118
|
|
19
|
-
units/unit.py,sha256=
|
|
19
|
+
units/unit.py,sha256=r10068WecU2qrE_D8CL8CEK9_tbJvop11pL-7GBmyQU,845
|
|
20
20
|
utils/__init__.py,sha256=zTkrwGlYAf6IRcS8e_9nRyACmwJO5Ni-Lwoy55K3qCw,191
|
|
21
21
|
utils/numbers.py,sha256=_wzMOCoU2hOybqVcT-x__7eY6WkwHa97Ahhr9L5reQE,884
|
|
22
|
-
python_units-0.
|
|
23
|
-
python_units-0.
|
|
24
|
-
python_units-0.
|
|
25
|
-
python_units-0.
|
|
22
|
+
python_units-0.4.0.dist-info/METADATA,sha256=NE6ev6wLQzw_V0yyXeay7cNwfdOykovG0HhXpH7rZPc,13290
|
|
23
|
+
python_units-0.4.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
24
|
+
python_units-0.4.0.dist-info/top_level.txt,sha256=xVEgUUcetmpTHsYk3A-xD9qAxU5S2yD0n8YW1r08tn0,46
|
|
25
|
+
python_units-0.4.0.dist-info/RECORD,,
|
units/unit.py
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
"""Compatibility exports for unit definitions."""
|
|
3
3
|
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
from types import ModuleType
|
|
8
|
+
|
|
9
|
+
from core.quantity import unit as _extract_unit
|
|
4
10
|
from core.unit_definitions import (
|
|
5
11
|
BaseUnit,
|
|
6
12
|
CustomUnitBase,
|
|
@@ -22,3 +28,13 @@ __all__ = [
|
|
|
22
28
|
"require_unit_instance",
|
|
23
29
|
"resolve_unit",
|
|
24
30
|
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class _CallableUnitModule(ModuleType):
|
|
34
|
+
"""Module wrapper that preserves the public ``units.unit(...)`` helper."""
|
|
35
|
+
|
|
36
|
+
def __call__(self, quantity: object) -> BaseUnit:
|
|
37
|
+
return _extract_unit(quantity)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
sys.modules[__name__].__class__ = _CallableUnitModule
|
|
File without changes
|
|
File without changes
|
|
File without changes
|