unitx 0.6.0__tar.gz → 0.6.2__tar.gz
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.
- {unitx-0.6.0 → unitx-0.6.2}/CHANGELOG.md +21 -0
- {unitx-0.6.0 → unitx-0.6.2}/PKG-INFO +54 -21
- {unitx-0.6.0 → unitx-0.6.2}/README.md +53 -20
- {unitx-0.6.0 → unitx-0.6.2}/pyproject.toml +1 -1
- {unitx-0.6.0 → unitx-0.6.2}/src/unitx/factor.py +5 -1
- {unitx-0.6.0 → unitx-0.6.2}/src/unitx/quantity.py +31 -22
- {unitx-0.6.0 → unitx-0.6.2}/src/unitx/unit.py +67 -29
- {unitx-0.6.0 → unitx-0.6.2}/tests/test_quantity.py +56 -3
- {unitx-0.6.0 → unitx-0.6.2}/tests/test_si.py +5 -0
- {unitx-0.6.0 → unitx-0.6.2}/tests/test_unit.py +70 -55
- {unitx-0.6.0 → unitx-0.6.2}/tests/typing/check_quantity.py +28 -3
- {unitx-0.6.0 → unitx-0.6.2}/tests/typing/check_si.py +1 -0
- {unitx-0.6.0 → unitx-0.6.2}/.github/workflows/ci.yml +0 -0
- {unitx-0.6.0 → unitx-0.6.2}/.github/workflows/release.yml +0 -0
- {unitx-0.6.0 → unitx-0.6.2}/.gitignore +0 -0
- {unitx-0.6.0 → unitx-0.6.2}/LICENSE +0 -0
- {unitx-0.6.0 → unitx-0.6.2}/conftest.py +0 -0
- {unitx-0.6.0 → unitx-0.6.2}/pyrightconfig.json +0 -0
- {unitx-0.6.0 → unitx-0.6.2}/src/unitx/__init__.py +0 -0
- {unitx-0.6.0 → unitx-0.6.2}/src/unitx/_constclass.py +0 -0
- {unitx-0.6.0 → unitx-0.6.2}/src/unitx/_numerics.py +0 -0
- {unitx-0.6.0 → unitx-0.6.2}/src/unitx/_unicode.py +0 -0
- {unitx-0.6.0 → unitx-0.6.2}/src/unitx/_unit_data.py +0 -0
- {unitx-0.6.0 → unitx-0.6.2}/src/unitx/dimension.py +0 -0
- {unitx-0.6.0 → unitx-0.6.2}/src/unitx/exceptions.py +0 -0
- {unitx-0.6.0 → unitx-0.6.2}/src/unitx/monomial.py +0 -0
- {unitx-0.6.0 → unitx-0.6.2}/src/unitx/py.typed +0 -0
- {unitx-0.6.0 → unitx-0.6.2}/src/unitx/si/__init__.py +0 -0
- {unitx-0.6.0 → unitx-0.6.2}/src/unitx/si/constants.py +0 -0
- {unitx-0.6.0 → unitx-0.6.2}/src/unitx/si/units.py +0 -0
- {unitx-0.6.0 → unitx-0.6.2}/src/unitx/singleunit.py +0 -0
- {unitx-0.6.0 → unitx-0.6.2}/src/unitx/unitparser.py +0 -0
- {unitx-0.6.0 → unitx-0.6.2}/tests/__init__.py +0 -0
- {unitx-0.6.0 → unitx-0.6.2}/tests/test_dimension.py +0 -0
- {unitx-0.6.0 → unitx-0.6.2}/tests/test_factor.py +0 -0
- {unitx-0.6.0 → unitx-0.6.2}/tests/test_import.py +0 -0
- {unitx-0.6.0 → unitx-0.6.2}/tests/test_monomial.py +0 -0
- {unitx-0.6.0 → unitx-0.6.2}/tests/test_numeric.py +0 -0
- {unitx-0.6.0 → unitx-0.6.2}/tests/test_singleunit.py +0 -0
- {unitx-0.6.0 → unitx-0.6.2}/tests/test_typing.py +0 -0
- {unitx-0.6.0 → unitx-0.6.2}/tests/test_unicode.py +0 -0
- {unitx-0.6.0 → unitx-0.6.2}/tests/test_unit_data.py +0 -0
- {unitx-0.6.0 → unitx-0.6.2}/tests/test_unit_definition.py +0 -0
- {unitx-0.6.0 → unitx-0.6.2}/tests/test_unitparser.py +0 -0
- {unitx-0.6.0 → unitx-0.6.2}/tests/test_warnings.py +0 -0
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.6.2] - 2026-10-18
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Support multiplication and division between Quantity and Factor or SymbolicConstant in both operand orders. Multiplication and right-hand division preserve the magnitude and its type, combining the exact factor with the unit's scale; left-hand division takes the reciprocal magnitude and scales the reciprocal unit. Ordinary numeric operands continue to operate on the magnitude.
|
|
8
|
+
- Preserve generic magnitude types in the corresponding public type hints, with runtime and static regression coverage for exact scaling and reciprocal operations.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Combine the compact exact scale and formal units in ScaledUnit string output, grouping denominator factors into one product, such as `J⋅s/(2⋅π)`.
|
|
13
|
+
- Display quantities with scaled units using the combined product and multiplication dots between adjacent numeric factors, such as `3⋅60⋅s`, without evaluating the exact scale. String output remains readable display rather than parser input; `repr()` retains reconstructible state.
|
|
14
|
+
- Document exact-factor quantity arithmetic and the distinction between a symbolic expression such as `SI.h / (2 * PI)` and predefined `SI.hbar`, which continues to expose its numerical value in ordinary `J*s` units.
|
|
15
|
+
|
|
16
|
+
## [0.6.1] - 2026-10-17
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Make multiplication of ordinary numeric magnitudes with Unit and ScaledUnit symmetric: both operand orders construct a Quantity and preserve the magnitude type. Zero and negative magnitudes are supported; float magnitudes retain their stored values without Factor rationalization.
|
|
21
|
+
- Return Quantity when dividing Unit or ScaledUnit by an ordinary number. Integer, Fraction, and finite Decimal divisors produce exact Fraction reciprocals, so `si.deg / 60` preserves the exact arcminute definition; float divisors use native division.
|
|
22
|
+
- Keep Factor and SymbolicConstant multiplication and division as exact ScaledUnit operations. To retain the former explicit scaling behavior, use `unit * Factor(value)` or `unit / Factor(value)`.
|
|
23
|
+
|
|
3
24
|
## [0.6.0] - 2026-10-17
|
|
4
25
|
|
|
5
26
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: unitx
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.2
|
|
4
4
|
Summary: A modern, intuitive Python library for units, quantities, and dimensional analysis.
|
|
5
5
|
Project-URL: Homepage, https://github.com/DaitTHU/unitx
|
|
6
6
|
Project-URL: Issues, https://github.com/DaitTHU/unitx/issues
|
|
@@ -115,6 +115,7 @@ evaluated numerically.
|
|
|
115
115
|
For unit expressions and explicit construction, import the classes:
|
|
116
116
|
|
|
117
117
|
```pycon
|
|
118
|
+
>>> from unitx.factor import Factor
|
|
118
119
|
>>> from unitx.quantity import Quantity
|
|
119
120
|
>>> from unitx.unit import Unit
|
|
120
121
|
>>> Quantity(50, 'cm') == 50 * si.cm
|
|
@@ -129,33 +130,65 @@ True
|
|
|
129
130
|
with the reciprocal unit. Construction preserves the original magnitude and
|
|
130
131
|
its type.
|
|
131
132
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
`SymbolicConstant` on either side of a unit produces a `ScaledUnit`.
|
|
133
|
+
Multiplication is symmetric: `unit * value` and `value * unit` both construct
|
|
134
|
+
`Quantity` objects and preserve the magnitude's type. Dividing a unit by an
|
|
135
|
+
`int`, `Fraction`, or finite `Decimal` constructs a quantity with an exact
|
|
136
|
+
`Fraction` reciprocal, so `degree / 60` loses no precision. Float divisors use
|
|
137
|
+
native floating-point division without Factor's denominator limit. Booleans
|
|
138
|
+
are not accepted as right-hand numeric operands. A `Factor` or
|
|
139
|
+
`SymbolicConstant` on either side of a unit instead produces a `ScaledUnit`.
|
|
139
140
|
|
|
140
141
|
```pycon
|
|
141
142
|
>>> metre = Unit('m')
|
|
142
|
-
>>>
|
|
143
|
-
|
|
144
|
-
(2
|
|
145
|
-
|
|
143
|
+
>>> metre * 2 == 2 * metre
|
|
144
|
+
True
|
|
145
|
+
>>> (metre * 2).value
|
|
146
|
+
2
|
|
147
|
+
>>> double_metre = metre * Factor(2)
|
|
146
148
|
>>> print(double_metre)
|
|
147
|
-
2
|
|
149
|
+
2⋅m
|
|
148
150
|
>>> (3 * double_metre).to('m').value
|
|
149
151
|
6
|
|
150
|
-
>>>
|
|
151
|
-
|
|
152
|
+
>>> arcmin = si.deg / 60
|
|
153
|
+
>>> arcmin.value
|
|
154
|
+
Fraction(1, 60)
|
|
155
|
+
>>> arcmin.to(si.arcmin).value
|
|
156
|
+
Fraction(1, 1)
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Multiplying a `ScaledUnit` by an ordinary number also constructs a quantity,
|
|
161
|
+
keeping the exact scale in its unit and preserving the supplied magnitude.
|
|
162
|
+
Combining a Unit with a Quantity continues to return a Quantity.
|
|
163
|
+
|
|
164
|
+
Quantities also accept `Factor` and `SymbolicConstant` operands on either side
|
|
165
|
+
of multiplication or division. Multiplication and right-hand division preserve
|
|
166
|
+
the magnitude and its type, combining the exact factor with the unit instead.
|
|
167
|
+
Left-hand division takes the reciprocal magnitude and unit. Ordinary numeric
|
|
168
|
+
operands continue to act on the magnitude: `q * 2` and `q * Factor(2)` represent
|
|
169
|
+
equal physical quantities but can have different `.value` and `.unit` pairs.
|
|
170
|
+
|
|
171
|
+
```pycon
|
|
172
|
+
>>> from unitx.factor import PI
|
|
173
|
+
>>> hbar = SI.h / (2 * PI)
|
|
174
|
+
>>> print(hbar)
|
|
175
|
+
6.62607015E-34 J⋅s/(2⋅π)
|
|
176
|
+
>>> hbar.value == SI.h.value
|
|
177
|
+
True
|
|
178
|
+
>>> hbar * (2 * PI) == SI.h
|
|
179
|
+
True
|
|
180
|
+
>>> round(float(hbar.to('J*s').value) / 1e-34, 8)
|
|
181
|
+
1.05457182
|
|
152
182
|
|
|
153
183
|
```
|
|
154
184
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
185
|
+
The local `hbar` above retains the exact scale, whereas predefined `SI.hbar`
|
|
186
|
+
provides its numerical value in ordinary `J*s` units. `.value` is always the
|
|
187
|
+
magnitude paired with `.unit`, including its exact scale. Use
|
|
188
|
+
`.to('J*s').value` to evaluate the local expression in joule seconds;
|
|
189
|
+
conversion involving an irrational factor evaluates it approximately.
|
|
190
|
+
String output combines the scale and formal units into one product, with
|
|
191
|
+
grouped denominator factors and multiplication dots between adjacent numbers.
|
|
159
192
|
|
|
160
193
|
## Conversion and arithmetic
|
|
161
194
|
|
|
@@ -238,8 +271,8 @@ True
|
|
|
238
271
|
|
|
239
272
|
```
|
|
240
273
|
|
|
241
|
-
Multiplying or dividing a unit by a
|
|
242
|
-
`
|
|
274
|
+
Multiplying or dividing a unit by a `Factor` or `SymbolicConstant` creates a
|
|
275
|
+
`ScaledUnit`, which preserves an exact additional
|
|
243
276
|
scale through unit arithmetic. Its `scale`
|
|
244
277
|
is relative to its formal unit product; its `factor` is the total scale to the
|
|
245
278
|
coherent SI unit. An ordinary `Unit` has an implicit additional scale of one;
|
|
@@ -93,6 +93,7 @@ evaluated numerically.
|
|
|
93
93
|
For unit expressions and explicit construction, import the classes:
|
|
94
94
|
|
|
95
95
|
```pycon
|
|
96
|
+
>>> from unitx.factor import Factor
|
|
96
97
|
>>> from unitx.quantity import Quantity
|
|
97
98
|
>>> from unitx.unit import Unit
|
|
98
99
|
>>> Quantity(50, 'cm') == 50 * si.cm
|
|
@@ -107,33 +108,65 @@ True
|
|
|
107
108
|
with the reciprocal unit. Construction preserves the original magnitude and
|
|
108
109
|
its type.
|
|
109
110
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
`SymbolicConstant` on either side of a unit produces a `ScaledUnit`.
|
|
111
|
+
Multiplication is symmetric: `unit * value` and `value * unit` both construct
|
|
112
|
+
`Quantity` objects and preserve the magnitude's type. Dividing a unit by an
|
|
113
|
+
`int`, `Fraction`, or finite `Decimal` constructs a quantity with an exact
|
|
114
|
+
`Fraction` reciprocal, so `degree / 60` loses no precision. Float divisors use
|
|
115
|
+
native floating-point division without Factor's denominator limit. Booleans
|
|
116
|
+
are not accepted as right-hand numeric operands. A `Factor` or
|
|
117
|
+
`SymbolicConstant` on either side of a unit instead produces a `ScaledUnit`.
|
|
117
118
|
|
|
118
119
|
```pycon
|
|
119
120
|
>>> metre = Unit('m')
|
|
120
|
-
>>>
|
|
121
|
-
|
|
122
|
-
(2
|
|
123
|
-
|
|
121
|
+
>>> metre * 2 == 2 * metre
|
|
122
|
+
True
|
|
123
|
+
>>> (metre * 2).value
|
|
124
|
+
2
|
|
125
|
+
>>> double_metre = metre * Factor(2)
|
|
124
126
|
>>> print(double_metre)
|
|
125
|
-
2
|
|
127
|
+
2⋅m
|
|
126
128
|
>>> (3 * double_metre).to('m').value
|
|
127
129
|
6
|
|
128
|
-
>>>
|
|
129
|
-
|
|
130
|
+
>>> arcmin = si.deg / 60
|
|
131
|
+
>>> arcmin.value
|
|
132
|
+
Fraction(1, 60)
|
|
133
|
+
>>> arcmin.to(si.arcmin).value
|
|
134
|
+
Fraction(1, 1)
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Multiplying a `ScaledUnit` by an ordinary number also constructs a quantity,
|
|
139
|
+
keeping the exact scale in its unit and preserving the supplied magnitude.
|
|
140
|
+
Combining a Unit with a Quantity continues to return a Quantity.
|
|
141
|
+
|
|
142
|
+
Quantities also accept `Factor` and `SymbolicConstant` operands on either side
|
|
143
|
+
of multiplication or division. Multiplication and right-hand division preserve
|
|
144
|
+
the magnitude and its type, combining the exact factor with the unit instead.
|
|
145
|
+
Left-hand division takes the reciprocal magnitude and unit. Ordinary numeric
|
|
146
|
+
operands continue to act on the magnitude: `q * 2` and `q * Factor(2)` represent
|
|
147
|
+
equal physical quantities but can have different `.value` and `.unit` pairs.
|
|
148
|
+
|
|
149
|
+
```pycon
|
|
150
|
+
>>> from unitx.factor import PI
|
|
151
|
+
>>> hbar = SI.h / (2 * PI)
|
|
152
|
+
>>> print(hbar)
|
|
153
|
+
6.62607015E-34 J⋅s/(2⋅π)
|
|
154
|
+
>>> hbar.value == SI.h.value
|
|
155
|
+
True
|
|
156
|
+
>>> hbar * (2 * PI) == SI.h
|
|
157
|
+
True
|
|
158
|
+
>>> round(float(hbar.to('J*s').value) / 1e-34, 8)
|
|
159
|
+
1.05457182
|
|
130
160
|
|
|
131
161
|
```
|
|
132
162
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
163
|
+
The local `hbar` above retains the exact scale, whereas predefined `SI.hbar`
|
|
164
|
+
provides its numerical value in ordinary `J*s` units. `.value` is always the
|
|
165
|
+
magnitude paired with `.unit`, including its exact scale. Use
|
|
166
|
+
`.to('J*s').value` to evaluate the local expression in joule seconds;
|
|
167
|
+
conversion involving an irrational factor evaluates it approximately.
|
|
168
|
+
String output combines the scale and formal units into one product, with
|
|
169
|
+
grouped denominator factors and multiplication dots between adjacent numbers.
|
|
137
170
|
|
|
138
171
|
## Conversion and arithmetic
|
|
139
172
|
|
|
@@ -216,8 +249,8 @@ True
|
|
|
216
249
|
|
|
217
250
|
```
|
|
218
251
|
|
|
219
|
-
Multiplying or dividing a unit by a
|
|
220
|
-
`
|
|
252
|
+
Multiplying or dividing a unit by a `Factor` or `SymbolicConstant` creates a
|
|
253
|
+
`ScaledUnit`, which preserves an exact additional
|
|
221
254
|
scale through unit arithmetic. Its `scale`
|
|
222
255
|
is relative to its formal unit product; its `factor` is the total scale to the
|
|
223
256
|
coherent SI unit. An ordinary `Unit` has an implicit additional scale of one;
|
|
@@ -398,6 +398,10 @@ class Factor:
|
|
|
398
398
|
"""
|
|
399
399
|
if not compact:
|
|
400
400
|
return str(self._monomial)
|
|
401
|
+
return str(self._display_monomial())
|
|
402
|
+
|
|
403
|
+
def _display_monomial(self) -> Monomial[int | str]:
|
|
404
|
+
"""Collect numeric bases for compact display, without changing exact state."""
|
|
401
405
|
rational, irrational = self.decompose(rationalize_denominator=True)
|
|
402
406
|
display_components: dict[int | str, Fraction] = {}
|
|
403
407
|
if rational.numerator > 1:
|
|
@@ -417,7 +421,7 @@ class Factor:
|
|
|
417
421
|
# Numeric roots use string keys to stay distinct from rational coefficients.
|
|
418
422
|
# Symbol validation and Factor construction prevent collisions with symbols.
|
|
419
423
|
display_components[str(base)] = exponent
|
|
420
|
-
return
|
|
424
|
+
return Monomial._from_dict(display_components)
|
|
421
425
|
|
|
422
426
|
def __str__(self) -> str:
|
|
423
427
|
return self.format()
|
|
@@ -7,10 +7,11 @@ from fractions import Fraction
|
|
|
7
7
|
from typing import Any, Generic, TypeVar, cast, overload
|
|
8
8
|
|
|
9
9
|
from ._numerics import Fractionable, Magnitude, Number, Real, as_fraction, as_int_if_integral
|
|
10
|
+
from ._unicode import DOT
|
|
10
11
|
from .dimension import DIMENSIONLESS, Dimension
|
|
11
12
|
from .exceptions import DimensionError
|
|
12
|
-
from .factor import ONE, Factor
|
|
13
|
-
from .unit import
|
|
13
|
+
from .factor import ONE, Factor, SymbolicConstant
|
|
14
|
+
from .unit import Unit
|
|
14
15
|
|
|
15
16
|
# Magnitudes are exposed read-only; operations construct new quantities.
|
|
16
17
|
T = TypeVar('T', bound=Magnitude, covariant=True)
|
|
@@ -22,7 +23,9 @@ class Quantity(Generic[T]):
|
|
|
22
23
|
Construction preserves the magnitude's type. Conversion uses Factor's
|
|
23
24
|
scaling rules and can change that type. Arithmetic converts finite Decimal
|
|
24
25
|
operands exactly to Fraction; other magnitudes use native arithmetic.
|
|
25
|
-
Multiplication or division by a Unit
|
|
26
|
+
Multiplication or division by a Unit, Factor, or SymbolicConstant preserves
|
|
27
|
+
the magnitude unchanged. Exact factors are combined with the unit's scale,
|
|
28
|
+
while ordinary numeric operands participate in magnitude arithmetic.
|
|
26
29
|
|
|
27
30
|
Magnitudes must be scalars whose equality returns a bool. NumPy scalars,
|
|
28
31
|
arrays, and NumPy operations are not supported yet; NumPy integration is
|
|
@@ -33,9 +36,9 @@ class Quantity(Generic[T]):
|
|
|
33
36
|
Scalar multiplication and division accept int, float, Fraction, and Decimal.
|
|
34
37
|
|
|
35
38
|
Type hints preserve the magnitude type at construction and when combining
|
|
36
|
-
with a Unit. Numeric conversions and arithmetic use unions
|
|
37
|
-
depends on runtime units or values. In particular, float
|
|
38
|
-
accept int inputs, which may produce Fraction after scaling. Custom
|
|
39
|
+
with a Unit or exact factor. Numeric conversions and arithmetic use unions
|
|
40
|
+
where the result depends on runtime units or values. In particular, float
|
|
41
|
+
annotations also accept int inputs, which may produce Fraction after scaling. Custom
|
|
39
42
|
magnitudes retain their arithmetic protocol rather than an assumed Self.
|
|
40
43
|
|
|
41
44
|
Equality and hashing retain the exact rational coefficient and the unit's
|
|
@@ -50,9 +53,9 @@ class Quantity(Generic[T]):
|
|
|
50
53
|
Powers normalize the exponent once using as_fraction(), just as Unit does,
|
|
51
54
|
and use that exponent for both the magnitude and the unit. Nonintegral
|
|
52
55
|
magnitude powers use native numerical evaluation and can be approximate.
|
|
53
|
-
str()
|
|
54
|
-
|
|
55
|
-
Unit expression.
|
|
56
|
+
str() displays the magnitude followed by the unit's exact product expression
|
|
57
|
+
without evaluating its scale. Adjacent numeric factors use a multiplication
|
|
58
|
+
dot. repr() uses a reconstructible Unit expression.
|
|
56
59
|
|
|
57
60
|
>>> Quantity(1, 'm') + Quantity(50, 'cm')
|
|
58
61
|
Quantity(Fraction(3, 2), Unit('m'))
|
|
@@ -106,11 +109,13 @@ class Quantity(Generic[T]):
|
|
|
106
109
|
return f'{type(self).__name__}({self.value!r}, {self.unit!r})'
|
|
107
110
|
|
|
108
111
|
def __str__(self) -> str:
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
unit = str(self.unit)
|
|
113
|
+
if unit == '1':
|
|
114
|
+
return str(self.value)
|
|
115
|
+
if unit.startswith('1/'):
|
|
116
|
+
return f'{self.value}{unit[1:]}'
|
|
117
|
+
separator = DOT if unit[0].isdecimal() else ' '
|
|
118
|
+
return f'{self.value}{separator}{unit}'
|
|
114
119
|
|
|
115
120
|
def is_dimensionless(self) -> bool:
|
|
116
121
|
return self.dimension is DIMENSIONLESS
|
|
@@ -263,7 +268,7 @@ class Quantity(Generic[T]):
|
|
|
263
268
|
return NotImplemented
|
|
264
269
|
|
|
265
270
|
@overload
|
|
266
|
-
def __mul__(self, other: Unit, /) -> Quantity[T]: ...
|
|
271
|
+
def __mul__(self, other: Unit | Factor | SymbolicConstant, /) -> Quantity[T]: ...
|
|
267
272
|
|
|
268
273
|
@overload
|
|
269
274
|
def __mul__(self: Quantity[int], other: Quantity[int] | int, /) -> Quantity[int]: ...
|
|
@@ -281,7 +286,7 @@ class Quantity(Generic[T]):
|
|
|
281
286
|
quantity = cast(Quantity[Magnitude], other)
|
|
282
287
|
value = cast(Magnitude, quantity._arithmetic_value)
|
|
283
288
|
return Quantity(self._arithmetic_value * value, self.unit * quantity.unit)
|
|
284
|
-
if isinstance(other, Unit):
|
|
289
|
+
if isinstance(other, (Unit, Factor, SymbolicConstant)):
|
|
285
290
|
return Quantity(self.value, self.unit * other)
|
|
286
291
|
if isinstance(other, Fractionable):
|
|
287
292
|
value = cast(Magnitude, Fraction(other) if isinstance(other, Decimal) else other)
|
|
@@ -291,7 +296,7 @@ class Quantity(Generic[T]):
|
|
|
291
296
|
__rmul__ = __mul__
|
|
292
297
|
|
|
293
298
|
@overload
|
|
294
|
-
def __truediv__(self, other: Unit, /) -> Quantity[T]: ...
|
|
299
|
+
def __truediv__(self, other: Unit | Factor | SymbolicConstant, /) -> Quantity[T]: ...
|
|
295
300
|
|
|
296
301
|
@overload
|
|
297
302
|
def __truediv__(self: Quantity[int], other: Quantity[int] | int, /) -> Quantity[float]: ...
|
|
@@ -309,7 +314,7 @@ class Quantity(Generic[T]):
|
|
|
309
314
|
quantity = cast(Quantity[Magnitude], other)
|
|
310
315
|
value = cast(Magnitude, quantity._arithmetic_value)
|
|
311
316
|
return Quantity(self._arithmetic_value / value, self.unit / quantity.unit)
|
|
312
|
-
if isinstance(other, Unit):
|
|
317
|
+
if isinstance(other, (Unit, Factor, SymbolicConstant)):
|
|
313
318
|
return Quantity(self.value, self.unit / other)
|
|
314
319
|
if isinstance(other, Fractionable):
|
|
315
320
|
value = cast(Magnitude, Fraction(other) if isinstance(other, Decimal) else other)
|
|
@@ -317,18 +322,22 @@ class Quantity(Generic[T]):
|
|
|
317
322
|
return NotImplemented
|
|
318
323
|
|
|
319
324
|
@overload
|
|
320
|
-
def __rtruediv__(
|
|
325
|
+
def __rtruediv__(
|
|
326
|
+
self: Quantity[int], other: Unit | Factor | SymbolicConstant | int, /
|
|
327
|
+
) -> Quantity[float]: ...
|
|
321
328
|
|
|
322
329
|
@overload
|
|
323
330
|
def __rtruediv__[N: Fractionable](
|
|
324
|
-
self: Quantity[N], other: Unit | Fractionable, /
|
|
331
|
+
self: Quantity[N], other: Unit | Factor | SymbolicConstant | Fractionable, /
|
|
325
332
|
) -> Quantity[Real]: ...
|
|
326
333
|
|
|
327
334
|
@overload
|
|
328
|
-
def __rtruediv__(
|
|
335
|
+
def __rtruediv__(
|
|
336
|
+
self, other: Unit | Factor | SymbolicConstant | Fractionable, /
|
|
337
|
+
) -> Quantity[Magnitude]: ...
|
|
329
338
|
|
|
330
339
|
def __rtruediv__(self, other: object, /) -> Quantity[Magnitude]:
|
|
331
|
-
if isinstance(other, Unit):
|
|
340
|
+
if isinstance(other, (Unit, Factor, SymbolicConstant)):
|
|
332
341
|
return Quantity(1 / self._arithmetic_value, other / self.unit)
|
|
333
342
|
if isinstance(other, Fractionable):
|
|
334
343
|
value = Fraction(other) if isinstance(other, Decimal) else other
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from fractions import Fraction
|
|
3
5
|
from typing import TYPE_CHECKING, Self, overload
|
|
4
6
|
|
|
5
7
|
from ._numerics import Fractionable, Magnitude, as_fraction
|
|
@@ -32,18 +34,16 @@ class Unit:
|
|
|
32
34
|
exponents remain exact; float exponents may be approximated with an
|
|
33
35
|
`InexactFloatWarning`.
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
the
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
scales. NumPy scalars, arrays, and NumPy operations are not supported yet;
|
|
46
|
-
NumPy integration is planned.
|
|
37
|
+
Multiplying a Unit by an int, Fraction, Decimal, or float constructs a
|
|
38
|
+
Quantity, just as multiplying the magnitude by the Unit does. Construction
|
|
39
|
+
preserves the magnitude and its type. Dividing a magnitude by a Unit also
|
|
40
|
+
preserves it and uses the reciprocal unit. Dividing a Unit by an int,
|
|
41
|
+
Fraction, or finite Decimal constructs a Quantity with an exact Fraction
|
|
42
|
+
reciprocal; float divisors use native floating-point division. Booleans
|
|
43
|
+
are not accepted as right-hand numeric operands.
|
|
44
|
+
Factor and SymbolicConstant operands instead construct a ScaledUnit on
|
|
45
|
+
either side and preserve their exact scale. NumPy scalars, arrays, and
|
|
46
|
+
NumPy operations are not supported yet; NumPy integration is planned.
|
|
47
47
|
|
|
48
48
|
Equality and hashing compare canonical unit identities and rational
|
|
49
49
|
exponents, regardless of factor insertion order. Aliases compare equal,
|
|
@@ -133,7 +133,10 @@ class Unit:
|
|
|
133
133
|
return self._monomial == other._monomial
|
|
134
134
|
|
|
135
135
|
@overload
|
|
136
|
-
def __mul__(self, other:
|
|
136
|
+
def __mul__[T: Fractionable](self, other: T) -> Quantity[T]: ...
|
|
137
|
+
|
|
138
|
+
@overload
|
|
139
|
+
def __mul__(self, other: SymbolicConstant | Factor) -> ScaledUnit: ...
|
|
137
140
|
|
|
138
141
|
@overload
|
|
139
142
|
def __mul__(self, other: ScaledUnit) -> ScaledUnit: ...
|
|
@@ -141,8 +144,14 @@ class Unit:
|
|
|
141
144
|
@overload
|
|
142
145
|
def __mul__(self, other: Unit) -> Unit: ...
|
|
143
146
|
|
|
144
|
-
def __mul__(self, other: object) -> Unit:
|
|
145
|
-
|
|
147
|
+
def __mul__(self, other: object) -> Unit | Quantity[Magnitude]:
|
|
148
|
+
from .quantity import Quantity
|
|
149
|
+
|
|
150
|
+
if isinstance(other, Fractionable):
|
|
151
|
+
if isinstance(other, bool):
|
|
152
|
+
raise TypeError('Boolean numeric operands are not supported.')
|
|
153
|
+
return Quantity(other, self)
|
|
154
|
+
if isinstance(other, SymbolicConstant | Factor):
|
|
146
155
|
scale = other if isinstance(other, Factor) else Factor(other)
|
|
147
156
|
return ScaledUnit(self, scale)
|
|
148
157
|
if not isinstance(other, Unit):
|
|
@@ -150,7 +159,13 @@ class Unit:
|
|
|
150
159
|
return Unit._from_monomial(self._monomial * other._monomial)
|
|
151
160
|
|
|
152
161
|
@overload
|
|
153
|
-
def __truediv__(self, other:
|
|
162
|
+
def __truediv__(self, other: int | Fraction | Decimal) -> Quantity[Fraction]: ...
|
|
163
|
+
|
|
164
|
+
@overload
|
|
165
|
+
def __truediv__(self, other: float) -> Quantity[float]: ...
|
|
166
|
+
|
|
167
|
+
@overload
|
|
168
|
+
def __truediv__(self, other: SymbolicConstant | Factor) -> ScaledUnit: ...
|
|
154
169
|
|
|
155
170
|
@overload
|
|
156
171
|
def __truediv__(self, other: ScaledUnit) -> ScaledUnit: ...
|
|
@@ -158,8 +173,15 @@ class Unit:
|
|
|
158
173
|
@overload
|
|
159
174
|
def __truediv__(self, other: Unit) -> Unit: ...
|
|
160
175
|
|
|
161
|
-
def __truediv__(self, other: object) -> Unit:
|
|
162
|
-
|
|
176
|
+
def __truediv__(self, other: object) -> Unit | Quantity[Magnitude]:
|
|
177
|
+
from .quantity import Quantity
|
|
178
|
+
|
|
179
|
+
if isinstance(other, Fractionable):
|
|
180
|
+
if isinstance(other, bool):
|
|
181
|
+
raise TypeError('Boolean numeric operands are not supported.')
|
|
182
|
+
reciprocal = 1 / other if isinstance(other, float) else 1 / Fraction(other)
|
|
183
|
+
return Quantity(reciprocal, self)
|
|
184
|
+
if isinstance(other, SymbolicConstant | Factor):
|
|
163
185
|
scale = 1 / other if isinstance(other, Factor) else 1 / Factor(other)
|
|
164
186
|
return ScaledUnit(self, scale)
|
|
165
187
|
if not isinstance(other, Unit):
|
|
@@ -284,7 +306,8 @@ class ScaledUnit(Unit):
|
|
|
284
306
|
involving a ScaledUnit return ScaledUnit, even when the scale becomes one.
|
|
285
307
|
Equality compares the formal product and explicit scale, so a scale of one
|
|
286
308
|
compares equal to the unscaled Unit. `symbol` describes only the product;
|
|
287
|
-
str()
|
|
309
|
+
str() combines its compact exact scale and formal units into a single
|
|
310
|
+
product with a grouped denominator, and repr() reconstructs both.
|
|
288
311
|
|
|
289
312
|
Copying and pickling preserve the formal units and exact scale. Restoring
|
|
290
313
|
a unit requires its referenced unit definitions to be registered.
|
|
@@ -293,7 +316,7 @@ class ScaledUnit(Unit):
|
|
|
293
316
|
>>> minute
|
|
294
317
|
ScaledUnit('s', scale=Factor.from_dict({2: 2, 3: 1, 5: 1}))
|
|
295
318
|
>>> str(minute)
|
|
296
|
-
'60
|
|
319
|
+
'60⋅s'
|
|
297
320
|
>>> minute == Unit('s')
|
|
298
321
|
False
|
|
299
322
|
"""
|
|
@@ -336,9 +359,11 @@ class ScaledUnit(Unit):
|
|
|
336
359
|
def __str__(self) -> str:
|
|
337
360
|
if self.scale == ONE:
|
|
338
361
|
return self.symbol
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
362
|
+
components: dict[SingleUnit | int | str, Fraction] = dict(
|
|
363
|
+
self.scale._display_monomial().components()
|
|
364
|
+
)
|
|
365
|
+
components.update(self._monomial.components())
|
|
366
|
+
return str(Monomial(components))
|
|
342
367
|
|
|
343
368
|
def __hash__(self) -> int:
|
|
344
369
|
if self.scale == ONE:
|
|
@@ -351,19 +376,32 @@ class ScaledUnit(Unit):
|
|
|
351
376
|
other_scale = other.scale if isinstance(other, ScaledUnit) else ONE
|
|
352
377
|
return self._monomial == other._monomial and self.scale == other_scale
|
|
353
378
|
|
|
354
|
-
|
|
379
|
+
@overload
|
|
380
|
+
def __mul__[T: Fractionable](self, other: T) -> Quantity[T]: ...
|
|
381
|
+
|
|
382
|
+
@overload
|
|
383
|
+
def __mul__(self, other: Unit | SymbolicConstant | Factor) -> ScaledUnit: ...
|
|
384
|
+
|
|
385
|
+
def __mul__(self, other: object) -> ScaledUnit | Quantity[Magnitude]:
|
|
355
386
|
if isinstance(other, Fractionable | SymbolicConstant | Factor):
|
|
356
|
-
|
|
357
|
-
return ScaledUnit(self, scale)
|
|
387
|
+
return super().__mul__(other)
|
|
358
388
|
if not isinstance(other, Unit):
|
|
359
389
|
return NotImplemented
|
|
360
390
|
scale = self.scale * other.scale if isinstance(other, ScaledUnit) else self.scale
|
|
361
391
|
return ScaledUnit._from_monomial(self._monomial * other._monomial, scale)
|
|
362
392
|
|
|
363
|
-
|
|
393
|
+
@overload
|
|
394
|
+
def __truediv__(self, other: int | Fraction | Decimal) -> Quantity[Fraction]: ...
|
|
395
|
+
|
|
396
|
+
@overload
|
|
397
|
+
def __truediv__(self, other: float) -> Quantity[float]: ...
|
|
398
|
+
|
|
399
|
+
@overload
|
|
400
|
+
def __truediv__(self, other: Unit | SymbolicConstant | Factor) -> ScaledUnit: ...
|
|
401
|
+
|
|
402
|
+
def __truediv__(self, other: object) -> ScaledUnit | Quantity[Magnitude]:
|
|
364
403
|
if isinstance(other, Fractionable | SymbolicConstant | Factor):
|
|
365
|
-
|
|
366
|
-
return ScaledUnit(self, scale)
|
|
404
|
+
return super().__truediv__(other)
|
|
367
405
|
if not isinstance(other, Unit):
|
|
368
406
|
return NotImplemented
|
|
369
407
|
scale = self.scale / other.scale if isinstance(other, ScaledUnit) else self.scale
|
|
@@ -38,9 +38,14 @@ def test_quantity_str(unit: str, expected: str) -> None:
|
|
|
38
38
|
@pytest.mark.parametrize(
|
|
39
39
|
('unit', 'expected'),
|
|
40
40
|
[
|
|
41
|
-
(Factor(2) * Unit(), '3
|
|
42
|
-
(Factor(2) / Unit('s'), '3
|
|
43
|
-
(Factor(60) * Unit('s'), '3
|
|
41
|
+
(Factor(2) * Unit(), '3⋅2'),
|
|
42
|
+
(Factor(2) / Unit('s'), '3⋅2/s'),
|
|
43
|
+
(Factor(60) * Unit('s'), '3⋅60⋅s'),
|
|
44
|
+
(Unit('J*s') / (2 * PI), '3 J⋅s/(2⋅π)'),
|
|
45
|
+
(Unit('1/s') / (2 * PI), '3/(2⋅π⋅s)'),
|
|
46
|
+
(Unit() / PI, '3/π'),
|
|
47
|
+
(PI * Unit('s'), '3 π⋅s'),
|
|
48
|
+
(Factor(12) * Unit('s'), '3⋅12⋅s'),
|
|
44
49
|
(ScaledUnit(Unit(), ONE), '3'),
|
|
45
50
|
(ScaledUnit(Unit('1/s'), ONE), '3/s'),
|
|
46
51
|
],
|
|
@@ -52,6 +57,54 @@ def test_quantity_scaled_str(unit: ScaledUnit, expected: str) -> None:
|
|
|
52
57
|
assert (quantity.value, quantity.unit, quantity.unit.factor) == before
|
|
53
58
|
|
|
54
59
|
|
|
60
|
+
@pytest.mark.parametrize('value', [0, -2, 2**60 + 1, 0.1, Fraction(3, 7), Decimal('1.2345'), 2j])
|
|
61
|
+
@pytest.mark.parametrize('unit', [Unit('cm'), ScaledUnit('cm', PI), Unit('km/m')])
|
|
62
|
+
@pytest.mark.parametrize('factor', [ONE, Factor(2), PI, 1 / (2 * PI), Factor(2) ** Fraction(1, 2)])
|
|
63
|
+
def test_quantity_factor_operations(value, unit: Unit, factor) -> None:
|
|
64
|
+
quantity = Quantity(value, unit)
|
|
65
|
+
before = (repr(quantity), hash(quantity))
|
|
66
|
+
with localcontext() as context:
|
|
67
|
+
context.prec = 2
|
|
68
|
+
products = (quantity * factor, factor * quantity)
|
|
69
|
+
quotient = quantity / factor
|
|
70
|
+
for result in products:
|
|
71
|
+
assert result.value is value
|
|
72
|
+
assert result.unit == unit * factor
|
|
73
|
+
assert (result / factor).unit == unit
|
|
74
|
+
assert result == value * (unit * factor)
|
|
75
|
+
assert quotient.value is value
|
|
76
|
+
assert quotient.unit == unit / factor
|
|
77
|
+
assert (quotient * factor) == quantity
|
|
78
|
+
assert hash(quotient * factor) == hash(quantity)
|
|
79
|
+
assert {quantity: 'found'}[quotient * factor] == 'found'
|
|
80
|
+
assert (repr(quantity), hash(quantity)) == before
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@pytest.mark.parametrize('value', [2, Fraction(3, 7), Decimal('1.2345'), 2j])
|
|
84
|
+
@pytest.mark.parametrize('factor', [Factor(2), PI])
|
|
85
|
+
def test_quantity_factor_reciprocal(value, factor) -> None:
|
|
86
|
+
quantity = Quantity(value, ScaledUnit('cm', PI))
|
|
87
|
+
result = factor / quantity
|
|
88
|
+
reciprocal = 1 / quantity
|
|
89
|
+
assert result.value == reciprocal.value
|
|
90
|
+
assert type(result.value) is type(reciprocal.value)
|
|
91
|
+
assert result.unit == factor / quantity.unit
|
|
92
|
+
assert result == reciprocal * factor
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_quantity_factor_scaling() -> None:
|
|
96
|
+
quantity = Quantity(3, 'cm')
|
|
97
|
+
numeric = quantity * 2
|
|
98
|
+
exact = quantity * Factor(2)
|
|
99
|
+
assert numeric.value == 6
|
|
100
|
+
assert numeric.unit == quantity.unit
|
|
101
|
+
assert exact.value == 3
|
|
102
|
+
assert exact.unit == ScaledUnit('cm', Factor(2))
|
|
103
|
+
assert exact == numeric
|
|
104
|
+
assert hash(exact) == hash(numeric)
|
|
105
|
+
assert (exact.to('m').to(exact.unit)).value == exact.value
|
|
106
|
+
|
|
107
|
+
|
|
55
108
|
def test_quantity_scaled_operations() -> None:
|
|
56
109
|
minute = Factor(60) * Unit('s')
|
|
57
110
|
quantity = Quantity(1, minute)
|
|
@@ -132,6 +132,11 @@ def test_si_defining_constants(attribute: str, target: str, value: str) -> None:
|
|
|
132
132
|
assert result.value == Decimal(value)
|
|
133
133
|
|
|
134
134
|
|
|
135
|
+
def test_si_hbar_value() -> None:
|
|
136
|
+
assert SI.hbar.unit == SI.h.unit
|
|
137
|
+
assert SI.hbar.value == pytest.approx(1.0545718176461565e-34, rel=1e-15, abs=0)
|
|
138
|
+
|
|
139
|
+
|
|
135
140
|
@pytest.mark.parametrize(
|
|
136
141
|
('attribute', 'target'),
|
|
137
142
|
[('Phi0', si.Wb), ('G0', si.S), ('RK', si.ohm), ('a0', si.m), ('lambdaC', si.m), ('Eh', si.J)],
|
|
@@ -54,7 +54,7 @@ def test_unit_quantity_construction(value, unit: Unit, reciprocal: Unit) -> None
|
|
|
54
54
|
@pytest.mark.parametrize('unit', [Unit('cm'), ScaledUnit('cm', scale=PI), Unit('km/m')])
|
|
55
55
|
@pytest.mark.parametrize(
|
|
56
56
|
'value',
|
|
57
|
-
[
|
|
57
|
+
[Factor(2), PI, Factor(2) ** Fraction(1, 2)],
|
|
58
58
|
)
|
|
59
59
|
def test_unit_numeric_scaling(unit: Unit, value, operation) -> None:
|
|
60
60
|
before = (repr(unit), unit.factor, hash(unit))
|
|
@@ -74,12 +74,17 @@ def test_unit_numeric_scaling(unit: Unit, value, operation) -> None:
|
|
|
74
74
|
def test_unit_scaling_cancellation() -> None:
|
|
75
75
|
unit = Unit('cm')
|
|
76
76
|
scale = 2**54
|
|
77
|
-
scaled = unit * scale
|
|
77
|
+
scaled = unit * Factor(scale)
|
|
78
78
|
assert scaled.scale.value == scale
|
|
79
79
|
assert (scaled / Unit('s')).factor == Factor(Fraction(scale, 100))
|
|
80
80
|
assert ((scaled ** Fraction(1, 2)) ** 2) == scaled
|
|
81
81
|
assert (PI * unit / PI) == unit
|
|
82
|
-
for result in (
|
|
82
|
+
for result in (
|
|
83
|
+
scaled / Factor(scale),
|
|
84
|
+
unit / Factor(scale) * Factor(scale),
|
|
85
|
+
unit * ONE,
|
|
86
|
+
unit / ONE,
|
|
87
|
+
):
|
|
83
88
|
assert type(result) is ScaledUnit
|
|
84
89
|
assert result.scale == ONE
|
|
85
90
|
assert result.factor == unit.factor
|
|
@@ -96,17 +101,17 @@ def test_unit_quantity_grouping() -> None:
|
|
|
96
101
|
assert type(left) is type(right) is Quantity
|
|
97
102
|
assert left.value == 6
|
|
98
103
|
assert left.unit is metre
|
|
99
|
-
assert right.value ==
|
|
100
|
-
assert right.unit
|
|
104
|
+
assert right.value == 6
|
|
105
|
+
assert right.unit is metre
|
|
101
106
|
assert left == right
|
|
102
107
|
assert hash(left) == hash(right)
|
|
103
108
|
half = 3 * (metre / 2)
|
|
104
|
-
assert half.value == 3
|
|
109
|
+
assert half.value == Fraction(3, 2)
|
|
105
110
|
assert half.to(metre).value == Fraction(3, 2)
|
|
106
111
|
assert type(half.to(metre).value) is Fraction
|
|
107
112
|
|
|
108
113
|
|
|
109
|
-
@pytest.mark.parametrize('unit', [Unit('cm'), Unit('cm')
|
|
114
|
+
@pytest.mark.parametrize('unit', [Unit('cm'), Factor(2) * Unit('cm')])
|
|
110
115
|
def test_unit_quantity_dispatch(unit: Unit) -> None:
|
|
111
116
|
quantity = Quantity(Fraction(3, 2), 's')
|
|
112
117
|
for product in (unit * quantity, quantity * unit):
|
|
@@ -119,28 +124,50 @@ def test_unit_quantity_dispatch(unit: Unit) -> None:
|
|
|
119
124
|
assert quotient.unit == unit / quantity.unit
|
|
120
125
|
|
|
121
126
|
|
|
122
|
-
@pytest.mark.parametrize('
|
|
123
|
-
@pytest.mark.parametrize(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
assert len(recorded) == 1
|
|
128
|
-
assert recorded[0].filename == __file__
|
|
129
|
-
assert recorded[0].lineno == test_unit_scaling_warning.__code__.co_firstlineno + 4
|
|
130
|
-
assert result == operation(unit, Fraction(1, 10))
|
|
127
|
+
@pytest.mark.parametrize('unit', [Unit('cm'), ScaledUnit('cm', PI)])
|
|
128
|
+
@pytest.mark.parametrize(
|
|
129
|
+
'value', [0, -2, 2**60 + 1, Fraction(7, 3), Decimal('1.2345'), 0.1, 1e-300]
|
|
130
|
+
)
|
|
131
|
+
def test_unit_multiplication_symmetry(unit: Unit, value) -> None:
|
|
131
132
|
with warnings.catch_warnings():
|
|
132
133
|
warnings.simplefilter('error', InexactFloatWarning)
|
|
133
|
-
|
|
134
|
-
|
|
134
|
+
left, right = value * unit, unit * value
|
|
135
|
+
assert type(left) is type(right) is Quantity
|
|
136
|
+
assert left.value is right.value is value
|
|
137
|
+
assert left.unit is right.unit is unit
|
|
138
|
+
assert left == right
|
|
139
|
+
assert hash(left) == hash(right)
|
|
135
140
|
|
|
136
141
|
|
|
137
|
-
@pytest.mark.parametrize('
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
with
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
assert
|
|
142
|
+
@pytest.mark.parametrize('unit', [Unit('deg'), ScaledUnit('deg', PI)])
|
|
143
|
+
@pytest.mark.parametrize('divisor', [60, 2**60 + 1, -60, Fraction(7, 3), Decimal('1.2345')])
|
|
144
|
+
def test_unit_division_exact(unit: Unit, divisor) -> None:
|
|
145
|
+
with localcontext() as context:
|
|
146
|
+
context.prec = 2
|
|
147
|
+
result = unit / divisor
|
|
148
|
+
assert type(result) is Quantity
|
|
149
|
+
assert type(result.value) is Fraction
|
|
150
|
+
assert result.value == 1 / Fraction(divisor)
|
|
151
|
+
assert result.unit is unit
|
|
152
|
+
assert (result * divisor) == 1 * unit
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_unit_division_arcminute() -> None:
|
|
156
|
+
degree = Unit('deg')
|
|
157
|
+
arcmin = degree / 60
|
|
158
|
+
assert arcmin.value == Fraction(1, 60)
|
|
159
|
+
assert arcmin.unit is degree
|
|
160
|
+
assert arcmin.to('arcmin').value == 1
|
|
161
|
+
assert arcmin.to('arcmin').to(degree).value == Fraction(1, 60)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@pytest.mark.parametrize('value', [0.1, 1e-300, -0.5])
|
|
165
|
+
def test_unit_division_float(value: float) -> None:
|
|
166
|
+
with warnings.catch_warnings():
|
|
167
|
+
warnings.simplefilter('error', InexactFloatWarning)
|
|
168
|
+
result = Unit('m') / value
|
|
169
|
+
assert type(result.value) is float
|
|
170
|
+
assert result.value == 1 / value
|
|
144
171
|
|
|
145
172
|
|
|
146
173
|
@pytest.mark.parametrize(
|
|
@@ -417,37 +444,18 @@ def test_unit_invalid_operands(value: object) -> None:
|
|
|
417
444
|
unit / value # pyright: ignore[reportUnusedExpression, reportOperatorIssue]
|
|
418
445
|
|
|
419
446
|
|
|
420
|
-
@pytest.mark.parametrize('unit', [Unit('m'), ScaledUnit('m',
|
|
421
|
-
@pytest.mark.parametrize('
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
with pytest.raises(ValueError, match='Factor must be positive'):
|
|
426
|
-
operation(unit, value)
|
|
427
|
-
assert (repr(unit), unit.factor, hash(unit)) == before
|
|
447
|
+
@pytest.mark.parametrize('unit', [Unit('m'), ScaledUnit('m', Factor(2))])
|
|
448
|
+
@pytest.mark.parametrize('divisor', [0, 0.0, Fraction(0), Decimal(0)])
|
|
449
|
+
def test_unit_division_invalid(unit: Unit, divisor) -> None:
|
|
450
|
+
with pytest.raises(ZeroDivisionError):
|
|
451
|
+
unit / divisor
|
|
428
452
|
|
|
429
453
|
|
|
430
|
-
@pytest.mark.parametrize('unit', [Unit('m'),
|
|
454
|
+
@pytest.mark.parametrize('unit', [Unit('m'), ScaledUnit('m', Factor(2))])
|
|
431
455
|
@pytest.mark.parametrize('operation', [operator.mul, operator.truediv])
|
|
432
456
|
@pytest.mark.parametrize('value', [True, False])
|
|
433
|
-
def
|
|
434
|
-
with pytest.raises(TypeError, match='
|
|
435
|
-
operation(unit, value)
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
@pytest.mark.parametrize('unit', [Unit('m'), Unit('m') * 2])
|
|
439
|
-
@pytest.mark.parametrize('operation', [operator.mul, operator.truediv])
|
|
440
|
-
@pytest.mark.parametrize('value', [float('nan'), Decimal('NaN')])
|
|
441
|
-
def test_unit_invalid_nan_scale(unit: Unit, value, operation) -> None:
|
|
442
|
-
with pytest.raises(ValueError, match='cannot convert NaN'):
|
|
443
|
-
operation(unit, value)
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
@pytest.mark.parametrize('unit', [Unit('m'), Unit('m') * 2])
|
|
447
|
-
@pytest.mark.parametrize('operation', [operator.mul, operator.truediv])
|
|
448
|
-
@pytest.mark.parametrize('value', [float('inf'), -float('inf'), Decimal('Infinity')])
|
|
449
|
-
def test_unit_invalid_infinite_scale(unit: Unit, value, operation) -> None:
|
|
450
|
-
with pytest.raises(OverflowError, match='cannot convert Infinity'):
|
|
457
|
+
def test_unit_invalid_boolean(unit: Unit, value: bool, operation) -> None:
|
|
458
|
+
with pytest.raises(TypeError, match='Boolean numeric operands'):
|
|
451
459
|
operation(unit, value)
|
|
452
460
|
|
|
453
461
|
|
|
@@ -567,11 +575,16 @@ def test_scaled_unit_exact_power() -> None:
|
|
|
567
575
|
@pytest.mark.parametrize(
|
|
568
576
|
('base', 'scale', 'expected'),
|
|
569
577
|
[
|
|
570
|
-
('s', Factor(60), '60
|
|
578
|
+
('s', Factor(60), '60⋅s'),
|
|
571
579
|
('', Factor(2), '2'),
|
|
572
580
|
('1/s', Factor(2), '2/s'),
|
|
573
581
|
('1/(m*s)', Factor(2), '2/(m⋅s)'),
|
|
574
582
|
('cm', ONE, 'cm'),
|
|
583
|
+
('J*s', 1 / (2 * PI), 'J⋅s/(2⋅π)'),
|
|
584
|
+
('m/s', PI / 180, 'π⋅m/(180⋅s)'),
|
|
585
|
+
('1/(m*s)', 1 / (2 * PI), '1/(2⋅π⋅m⋅s)'),
|
|
586
|
+
('s', Factor(5040) ** Fraction(1, 2), '12⋅35¹ᐟ²⋅s'),
|
|
587
|
+
('m^(1/2)/s', PI ** Fraction(1, 2), 'π¹ᐟ²⋅m¹ᐟ²/s'),
|
|
575
588
|
],
|
|
576
589
|
)
|
|
577
590
|
def test_scaled_unit_str(base: str, scale: Factor, expected: str) -> None:
|
|
@@ -597,7 +610,7 @@ def test_scaled_unit_repr_roundtrip(text: str, scale: Factor | SymbolicConstant)
|
|
|
597
610
|
ScaledUnit('cm', scale=ONE),
|
|
598
611
|
ScaledUnit('cm^(1/2)/s', scale=PI / 180),
|
|
599
612
|
ScaledUnit('', scale=Factor(2) ** Fraction(1, 2)),
|
|
600
|
-
Unit('cm') * 3
|
|
613
|
+
Unit('cm') * Factor(Fraction(3, 2)),
|
|
601
614
|
],
|
|
602
615
|
)
|
|
603
616
|
def test_scaled_unit_copy(unit: ScaledUnit, rebuild) -> None:
|
|
@@ -613,7 +626,9 @@ def test_scaled_unit_copy(unit: ScaledUnit, rebuild) -> None:
|
|
|
613
626
|
|
|
614
627
|
|
|
615
628
|
@pytest.mark.parametrize('protocol', range(2, pickle.HIGHEST_PROTOCOL + 1))
|
|
616
|
-
@pytest.mark.parametrize(
|
|
629
|
+
@pytest.mark.parametrize(
|
|
630
|
+
'unit', [ScaledUnit('cm^(1/2)/s', scale=PI / 180), Unit('cm') * Factor(Fraction(3, 2))]
|
|
631
|
+
)
|
|
617
632
|
def test_scaled_unit_pickle(protocol: int, unit: ScaledUnit) -> None:
|
|
618
633
|
rebuilt = pickle.loads(pickle.dumps(unit, protocol=protocol))
|
|
619
634
|
assert type(rebuilt) is ScaledUnit
|
|
@@ -84,14 +84,26 @@ def test_scaled_unit_construction() -> None:
|
|
|
84
84
|
|
|
85
85
|
|
|
86
86
|
def test_unit_numeric_scaling(unit: Unit, scaled: ScaledUnit) -> None:
|
|
87
|
-
for value in (
|
|
87
|
+
for value in (Factor(2), PI):
|
|
88
88
|
assert_type(unit * value, ScaledUnit)
|
|
89
89
|
assert_type(unit / value, ScaledUnit)
|
|
90
90
|
assert_type(scaled * value, ScaledUnit)
|
|
91
91
|
assert_type(scaled / value, ScaledUnit)
|
|
92
|
-
assert_type(unit * 2
|
|
92
|
+
assert_type(unit * 2, Quantity[int])
|
|
93
|
+
assert_type(unit * 0.1, Quantity[float])
|
|
94
|
+
assert_type(unit * Fraction(1, 3), Quantity[Fraction])
|
|
95
|
+
assert_type(unit * Decimal('0.1'), Quantity[Decimal])
|
|
96
|
+
assert_type(scaled * 2, Quantity[int])
|
|
97
|
+
assert_type(scaled * Decimal('0.1'), Quantity[Decimal])
|
|
98
|
+
assert_type(unit / 60, Quantity[Fraction])
|
|
99
|
+
assert_type(unit / Fraction(3, 2), Quantity[Fraction])
|
|
100
|
+
assert_type(unit / Decimal('0.1'), Quantity[Fraction])
|
|
101
|
+
assert_type(unit / 0.1, Quantity[float])
|
|
102
|
+
assert_type(scaled / 60, Quantity[Fraction])
|
|
103
|
+
assert_type(scaled / 0.1, Quantity[float])
|
|
104
|
+
assert_type(unit * 2 / 3, Quantity[float])
|
|
93
105
|
assert_type(3 * (unit * 2), Quantity[int])
|
|
94
|
-
assert_type(Decimal(3) * (unit / 2), Quantity[
|
|
106
|
+
assert_type(Decimal(3) * (unit / 2), Quantity[Real])
|
|
95
107
|
assert_type(Factor(2) * unit, ScaledUnit)
|
|
96
108
|
assert_type(PI * scaled, ScaledUnit)
|
|
97
109
|
assert_type(unit * Quantity(2, 's'), Quantity[int])
|
|
@@ -131,6 +143,19 @@ def test_quantity_unit_operations[T: Magnitude](value: T) -> Quantity[T]:
|
|
|
131
143
|
return quantity
|
|
132
144
|
|
|
133
145
|
|
|
146
|
+
def test_quantity_factor_operations[T: Magnitude](value: T) -> Quantity[T]:
|
|
147
|
+
quantity = Quantity(value, 'm')
|
|
148
|
+
for factor in (Factor(2), PI):
|
|
149
|
+
assert_type(quantity * factor, Quantity[T])
|
|
150
|
+
assert_type(factor * quantity, Quantity[T])
|
|
151
|
+
assert_type(quantity / factor, Quantity[T])
|
|
152
|
+
assert_type(factor / quantity, Quantity[Magnitude])
|
|
153
|
+
assert_type(Factor(2) / Quantity(2, 's'), Quantity[float])
|
|
154
|
+
assert_type(PI / Quantity(Decimal(2), 's'), Quantity[Real])
|
|
155
|
+
assert_type(Quantity(Decimal(2), 's') / (2 * PI), Quantity[Decimal])
|
|
156
|
+
return quantity
|
|
157
|
+
|
|
158
|
+
|
|
134
159
|
def test_quantity_numeric_operations() -> None:
|
|
135
160
|
integer = Quantity(1, 'm')
|
|
136
161
|
rational = Quantity(Fraction(1), 'm')
|
|
@@ -20,5 +20,6 @@ def test_si_unit_types() -> None:
|
|
|
20
20
|
def test_si_constant_types() -> None:
|
|
21
21
|
assert_type(SI.c, Quantity[int])
|
|
22
22
|
assert_type(SI.h, Quantity[Decimal])
|
|
23
|
+
assert_type(SI.hbar, Quantity[Real])
|
|
23
24
|
assert_type(SI.e, Quantity[Decimal])
|
|
24
25
|
assert_type(SI.h.to(si.J * si.s), Quantity[Real])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|