unitx 0.6.2__tar.gz → 0.6.3__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.2 → unitx-0.6.3}/CHANGELOG.md +25 -0
- {unitx-0.6.2 → unitx-0.6.3}/PKG-INFO +92 -22
- {unitx-0.6.2 → unitx-0.6.3}/README.md +91 -21
- {unitx-0.6.2 → unitx-0.6.3}/pyproject.toml +1 -1
- {unitx-0.6.2 → unitx-0.6.3}/src/unitx/_numerics.py +16 -5
- {unitx-0.6.2 → unitx-0.6.3}/src/unitx/dimension.py +5 -4
- {unitx-0.6.2 → unitx-0.6.3}/src/unitx/factor.py +30 -11
- {unitx-0.6.2 → unitx-0.6.3}/src/unitx/monomial.py +5 -5
- unitx-0.6.3/src/unitx/quantity.py +682 -0
- {unitx-0.6.2 → unitx-0.6.3}/src/unitx/si/constants.py +10 -6
- {unitx-0.6.2 → unitx-0.6.3}/src/unitx/unit.py +6 -5
- {unitx-0.6.2 → unitx-0.6.3}/tests/test_dimension.py +10 -8
- {unitx-0.6.2 → unitx-0.6.3}/tests/test_factor.py +87 -5
- {unitx-0.6.2 → unitx-0.6.3}/tests/test_monomial.py +8 -11
- unitx-0.6.3/tests/test_quantity.py +1158 -0
- {unitx-0.6.2 → unitx-0.6.3}/tests/test_si.py +10 -1
- {unitx-0.6.2 → unitx-0.6.3}/tests/test_unit.py +36 -9
- {unitx-0.6.2 → unitx-0.6.3}/tests/test_warnings.py +19 -19
- {unitx-0.6.2 → unitx-0.6.3}/tests/typing/check_quantity.py +72 -15
- {unitx-0.6.2 → unitx-0.6.3}/tests/typing/check_si.py +2 -3
- unitx-0.6.2/src/unitx/quantity.py +0 -393
- unitx-0.6.2/tests/test_quantity.py +0 -505
- {unitx-0.6.2 → unitx-0.6.3}/.github/workflows/ci.yml +0 -0
- {unitx-0.6.2 → unitx-0.6.3}/.github/workflows/release.yml +0 -0
- {unitx-0.6.2 → unitx-0.6.3}/.gitignore +0 -0
- {unitx-0.6.2 → unitx-0.6.3}/LICENSE +0 -0
- {unitx-0.6.2 → unitx-0.6.3}/conftest.py +0 -0
- {unitx-0.6.2 → unitx-0.6.3}/pyrightconfig.json +0 -0
- {unitx-0.6.2 → unitx-0.6.3}/src/unitx/__init__.py +0 -0
- {unitx-0.6.2 → unitx-0.6.3}/src/unitx/_constclass.py +0 -0
- {unitx-0.6.2 → unitx-0.6.3}/src/unitx/_unicode.py +0 -0
- {unitx-0.6.2 → unitx-0.6.3}/src/unitx/_unit_data.py +0 -0
- {unitx-0.6.2 → unitx-0.6.3}/src/unitx/exceptions.py +0 -0
- {unitx-0.6.2 → unitx-0.6.3}/src/unitx/py.typed +0 -0
- {unitx-0.6.2 → unitx-0.6.3}/src/unitx/si/__init__.py +0 -0
- {unitx-0.6.2 → unitx-0.6.3}/src/unitx/si/units.py +0 -0
- {unitx-0.6.2 → unitx-0.6.3}/src/unitx/singleunit.py +0 -0
- {unitx-0.6.2 → unitx-0.6.3}/src/unitx/unitparser.py +0 -0
- {unitx-0.6.2 → unitx-0.6.3}/tests/__init__.py +0 -0
- {unitx-0.6.2 → unitx-0.6.3}/tests/test_import.py +0 -0
- {unitx-0.6.2 → unitx-0.6.3}/tests/test_numeric.py +0 -0
- {unitx-0.6.2 → unitx-0.6.3}/tests/test_singleunit.py +0 -0
- {unitx-0.6.2 → unitx-0.6.3}/tests/test_typing.py +0 -0
- {unitx-0.6.2 → unitx-0.6.3}/tests/test_unicode.py +0 -0
- {unitx-0.6.2 → unitx-0.6.3}/tests/test_unit_data.py +0 -0
- {unitx-0.6.2 → unitx-0.6.3}/tests/test_unit_definition.py +0 -0
- {unitx-0.6.2 → unitx-0.6.3}/tests/test_unitparser.py +0 -0
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.6.3] - 2026-10-18
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Add `Quantity.value_in(unit)` to return the same magnitude and concrete type as `.to(unit).value` without constructing a temporary quantity. The target unit is required and accepts a string or Unit, including ScaledUnit.
|
|
8
|
+
- Add `Quantity.isclose()` for symmetric real scalar comparisons with relative and absolute tolerances in coherent SI units. Nonzero absolute tolerances for dimensional quantities must carry matching units. Rational coefficients remain exact; residual irrational factors are approximated. Zero tolerances use exact equality.
|
|
9
|
+
- Make quantity truth values follow their magnitudes, so zero quantities are false regardless of unit.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Apply native magnitude operators in operand order for arithmetic, unary operations, and numerical powers, preserving numerical promotion, Decimal contexts, signals, and exceptions. Support the scalar operand types accepted by the magnitude, including complex values and custom scalars.
|
|
14
|
+
- Keep Decimal conversions and arithmetic in Decimal. Rational conversions combine the magnitude and scale exactly before rounding once in the active context; identity conversions preserve the original magnitude. Addition and subtraction align integer operands in the Decimal domain when paired with Decimal magnitudes. Derived SI constants use Decimal arithmetic and `Decimal(str(math.pi))` where an approximate pi is required, with evaluation in the context active at import.
|
|
15
|
+
- Interpret float formal exponents by their shortest decimal text, without a denominator limit, consistently across Monomial, Dimension, Factor, SymbolicConstant, Unit, and Quantity. For example, `0.1` represents `1/10` and `1e-10` remains nonzero. Numeric scale inputs to Factor retain their existing denominator-limit and warning rules.
|
|
16
|
+
- Cache rationalized Factor decompositions lazily, independently of numerical value evaluation, while preserving equality, hashing, copying, and serialization.
|
|
17
|
+
- Update public type hints, examples, and regression coverage for conversion, native arithmetic, comparisons, and formal exponents.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Preserve native comparisons, including independent `!=`, Decimal signals, and complex ordering errors, when unit factors agree. Across different factors, compare builtin real values and complex components using exact coefficients, retaining consistent hashes for equal quantities and native unhashability for custom magnitudes.
|
|
22
|
+
|
|
23
|
+
### Migration
|
|
24
|
+
|
|
25
|
+
- Decimal arithmetic no longer converts operands to Fraction automatically. Mixing Decimal with float or Fraction in arithmetic now raises the native TypeError; explicitly choose a common numerical type when needed. Decimal conversion and derived SI constants can therefore have different result types and rounding from earlier releases.
|
|
26
|
+
- Quantity numerical powers use the original exponent object; for example, an integer magnitude raised to `2.0` produces a float. Use `Fraction(1, 3)` for an exactly one-third formal exponent: the float `1 / 3` now represents the decimal `0.3333333333333333` and does not simplify as exactly one third.
|
|
27
|
+
|
|
3
28
|
## [0.6.2] - 2026-10-18
|
|
4
29
|
|
|
5
30
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: unitx
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.3
|
|
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
|
|
@@ -107,8 +107,9 @@ text spellings without adding an attribute to `si`.
|
|
|
107
107
|
|
|
108
108
|
SI defining constants retain their exact decimal values. Measured constants
|
|
109
109
|
are stored nominal values; measurement uncertainties are not propagated.
|
|
110
|
-
Derived constants
|
|
111
|
-
|
|
110
|
+
Derived Decimal constants are evaluated in the Decimal context active at
|
|
111
|
+
import. Formulas involving pi use `Decimal(str(math.pi))` as an approximation;
|
|
112
|
+
additional Decimal precision does not add precision to pi.
|
|
112
113
|
|
|
113
114
|
## Constructing units and quantities
|
|
114
115
|
|
|
@@ -192,16 +193,23 @@ grouped denominator factors and multiplication dots between adjacent numbers.
|
|
|
192
193
|
|
|
193
194
|
## Conversion and arithmetic
|
|
194
195
|
|
|
195
|
-
Use `.to()` to
|
|
196
|
+
Use `.to()` to obtain a new quantity in a unit with the same physical dimensions,
|
|
197
|
+
or `.value_in(unit)` to obtain only its numerical value. The target unit is
|
|
198
|
+
required and accepts a string or `Unit`, including `ScaledUnit`.
|
|
199
|
+
`.value_in(unit)` returns the same value and concrete type as `.to(unit).value`,
|
|
200
|
+
without constructing a temporary quantity. Both follow the conversion rules
|
|
201
|
+
below. A conversion with scale one preserves the original magnitude; Decimal
|
|
202
|
+
magnitudes stay Decimal and other conversions can change the numerical type.
|
|
203
|
+
`.value` reads the original stored magnitude.
|
|
196
204
|
Addition and subtraction convert the right operand to the left operand's unit:
|
|
197
205
|
|
|
198
206
|
```pycon
|
|
199
207
|
>>> length = Quantity(1, 'm')
|
|
200
208
|
>>> print(length + Quantity(50, 'cm'))
|
|
201
209
|
3/2 m
|
|
202
|
-
>>> length.
|
|
210
|
+
>>> length.value_in('cm')
|
|
203
211
|
100
|
|
204
|
-
>>> Quantity(1, 'cm').
|
|
212
|
+
>>> Quantity(1, 'cm').value_in('m')
|
|
205
213
|
Fraction(1, 100)
|
|
206
214
|
|
|
207
215
|
```
|
|
@@ -368,29 +376,90 @@ Fraction(1001, 1)
|
|
|
368
376
|
|
|
369
377
|
Exact unit algebra and numerical magnitude evaluation have different rules:
|
|
370
378
|
|
|
371
|
-
-
|
|
372
|
-
|
|
373
|
-
`
|
|
374
|
-
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
-
|
|
378
|
-
|
|
379
|
-
|
|
379
|
+
- Formal exponents use one rule in Monomial, Dimension, Factor, Unit, and
|
|
380
|
+
Quantity: int/Fraction/Decimal values stay exact; floats use their shortest
|
|
381
|
+
decimal text without a denominator limit. Thus `0.1` means `1/10` and
|
|
382
|
+
`1e-10` remains nonzero. Use `Fraction(1, 3)` for exactly one third:
|
|
383
|
+
the float `1 / 3` means the decimal `0.3333333333333333` in formal exponents.
|
|
384
|
+
Factor's numeric scale inputs retain their separate denominator-limit rules.
|
|
385
|
+
- Quantity handles units; the magnitudes determine numerical arithmetic.
|
|
386
|
+
Operators use the magnitudes in operand order and preserve native promotion,
|
|
387
|
+
rounding, signals, and exceptions. Decimal arithmetic returns Decimal and
|
|
388
|
+
follows its active context. Mixing Decimal with float or Fraction raises
|
|
389
|
+
`TypeError`, as it does for the bare numbers; choose a common numerical type
|
|
390
|
+
explicitly when needed.
|
|
391
|
+
- Conversion with a rational scale keeps int/Fraction values exact. Decimal
|
|
392
|
+
conversion combines the value and scale exactly internally, then rounds once
|
|
393
|
+
to Decimal in the active context. Irrational unit scales are approximated as
|
|
394
|
+
floats, using their shortest decimal representations for Decimal conversion.
|
|
395
|
+
A scale of one preserves the magnitude without arithmetic or rounding.
|
|
396
|
+
Direct `Factor(value)` retains its separate exact Fraction evaluation rules.
|
|
397
|
+
- Addition and subtraction align the right operand with the left quantity's
|
|
398
|
+
unit before numerical arithmetic. Integers being aligned with a Decimal
|
|
399
|
+
magnitude are scaled in the Decimal domain, so `Decimal('1') * si.m + 50 *
|
|
400
|
+
si.cm` returns a Decimal magnitude of `1.5`. Explicit Fraction/float operands
|
|
401
|
+
keep their own numerical domains. Finite-precision conversion and arithmetic
|
|
402
|
+
can round; changing the result unit can therefore change rounding errors.
|
|
403
|
+
- Unary `+` and `-` apply the magnitude's native operators and preserve the unit.
|
|
404
|
+
Decimal results remain Decimal and follow its context precision, rounding,
|
|
405
|
+
signals, and exceptions; unary `+` can therefore change the numerical value.
|
|
406
|
+
- Integer division produces a float; Fraction arithmetic remains rational.
|
|
407
|
+
Use Fraction magnitudes explicitly for exact rational results. Powers pass
|
|
408
|
+
the original exponent to the magnitude: `2 ** 2.0` returns float and
|
|
409
|
+
`2 ** Decimal('0.5')` returns Decimal. Decimal magnitudes require int or
|
|
410
|
+
Decimal exponents, as in Python. The unit follows the common formal exponent
|
|
411
|
+
rule above; numerical evaluation still uses the original exponent object.
|
|
412
|
+
Exponents must be finite real scalars other than bool. Numerical powers can
|
|
413
|
+
round or produce complex values, following the magnitude's rules.
|
|
414
|
+
- Truth values follow the magnitude, so zero quantities are false regardless
|
|
415
|
+
of unit. Comparisons with equal unit factors use native magnitude operators,
|
|
416
|
+
including independent `!=`, Decimal signals/exceptions and complex ordering
|
|
417
|
+
errors. For different unit factors, builtin real values and complex
|
|
418
|
+
components are aligned exactly to avoid rounding-dependent equality or
|
|
419
|
+
hashes. Ordinary numeric subclasses use the same alignment as their base
|
|
420
|
+
types. Decimal/float signals apply to native comparisons with equal factors;
|
|
421
|
+
cross-factor alignment compares exact rational coefficients and does not
|
|
422
|
+
perform an additional comparison merely to reproduce signals. Decimal/Fraction
|
|
423
|
+
comparisons are supported natively.
|
|
380
424
|
- Equality is exact, without a tolerance. Finite floats are compared as their
|
|
381
425
|
stored binary rational values. Floating-point conversions and irrational
|
|
382
|
-
powers may round;
|
|
383
|
-
|
|
426
|
+
powers may round; use `isclose()` for approximate real scalar comparisons.
|
|
427
|
+
It uses the symmetric condition
|
|
428
|
+
`abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)` in coherent SI
|
|
429
|
+
units, with defaults `rel_tol=1e-9` and `abs_tol=0`. Swapping quantities
|
|
430
|
+
preserves the result. Nonzero absolute tolerances for dimensional quantities
|
|
431
|
+
must carry matching units; plain operands and plain absolute tolerances
|
|
432
|
+
represent unscaled dimensionless values. Tolerances must be finite and
|
|
433
|
+
nonnegative. Different dimensions raise `DimensionError`. NaNs are never
|
|
434
|
+
close, and infinities are close only to infinities of the same sign.
|
|
435
|
+
Finite rational coefficients stay exact; residual irrational unit factors
|
|
436
|
+
are approximated. With both tolerances zero, `isclose()` uses exact equality.
|
|
384
437
|
|
|
385
438
|
```pycon
|
|
386
|
-
>>> from decimal import Decimal
|
|
439
|
+
>>> from decimal import Decimal, localcontext
|
|
387
440
|
>>> from fractions import Fraction
|
|
441
|
+
>>> with localcontext() as context:
|
|
442
|
+
... context.prec = 2
|
|
443
|
+
... q = Quantity(Decimal('1.2345'), 'm')
|
|
444
|
+
... print((+q).value, (-q).value, (q * 2).value)
|
|
445
|
+
1.2 -1.2 2.5
|
|
388
446
|
>>> Quantity(Decimal('0.1'), 'm').to('cm').value
|
|
389
|
-
10
|
|
447
|
+
Decimal('10')
|
|
390
448
|
>>> (Quantity(Fraction(1), 'm') / Quantity(3, 's')).value
|
|
391
449
|
Fraction(1, 3)
|
|
450
|
+
>>> (Quantity(2, 'm') ** 2.0).value
|
|
451
|
+
4.0
|
|
452
|
+
>>> bool(Quantity(0, 'm'))
|
|
453
|
+
False
|
|
392
454
|
>>> Quantity(0.1, '') == Quantity(Fraction(1, 10), '')
|
|
393
455
|
False
|
|
456
|
+
>>> Quantity(0.1 + 0.2, 'm').isclose(Quantity(0.3, 'm'))
|
|
457
|
+
True
|
|
458
|
+
>>> a, b = Quantity(1, 'm'), Quantity(100.05, 'cm')
|
|
459
|
+
>>> a.isclose(b, rel_tol=0, abs_tol=Quantity(1, 'mm'))
|
|
460
|
+
True
|
|
461
|
+
>>> b.isclose(a, rel_tol=0, abs_tol=Quantity(1, 'mm'))
|
|
462
|
+
True
|
|
394
463
|
|
|
395
464
|
```
|
|
396
465
|
|
|
@@ -410,9 +479,10 @@ python -m pip install sympy
|
|
|
410
479
|
- Custom magnitudes must support scalar arithmetic, return a Python `bool`
|
|
411
480
|
from equality, and defer raw operations with `Unit` operands to its reflected
|
|
412
481
|
operators.
|
|
413
|
-
- Scalar
|
|
414
|
-
|
|
415
|
-
|
|
482
|
+
- Scalar operations use the magnitude's supported operand types, including
|
|
483
|
+
complex numbers and custom scalar magnitudes. Custom magnitudes must also
|
|
484
|
+
defer operations with Quantity operands so that Quantity can handle units.
|
|
485
|
+
Ordering follows the magnitude's native rules.
|
|
416
486
|
- Unit conversion is multiplicative. Offset temperature scales such as Celsius
|
|
417
487
|
and Fahrenheit are not supported.
|
|
418
488
|
|
|
@@ -85,8 +85,9 @@ text spellings without adding an attribute to `si`.
|
|
|
85
85
|
|
|
86
86
|
SI defining constants retain their exact decimal values. Measured constants
|
|
87
87
|
are stored nominal values; measurement uncertainties are not propagated.
|
|
88
|
-
Derived constants
|
|
89
|
-
|
|
88
|
+
Derived Decimal constants are evaluated in the Decimal context active at
|
|
89
|
+
import. Formulas involving pi use `Decimal(str(math.pi))` as an approximation;
|
|
90
|
+
additional Decimal precision does not add precision to pi.
|
|
90
91
|
|
|
91
92
|
## Constructing units and quantities
|
|
92
93
|
|
|
@@ -170,16 +171,23 @@ grouped denominator factors and multiplication dots between adjacent numbers.
|
|
|
170
171
|
|
|
171
172
|
## Conversion and arithmetic
|
|
172
173
|
|
|
173
|
-
Use `.to()` to
|
|
174
|
+
Use `.to()` to obtain a new quantity in a unit with the same physical dimensions,
|
|
175
|
+
or `.value_in(unit)` to obtain only its numerical value. The target unit is
|
|
176
|
+
required and accepts a string or `Unit`, including `ScaledUnit`.
|
|
177
|
+
`.value_in(unit)` returns the same value and concrete type as `.to(unit).value`,
|
|
178
|
+
without constructing a temporary quantity. Both follow the conversion rules
|
|
179
|
+
below. A conversion with scale one preserves the original magnitude; Decimal
|
|
180
|
+
magnitudes stay Decimal and other conversions can change the numerical type.
|
|
181
|
+
`.value` reads the original stored magnitude.
|
|
174
182
|
Addition and subtraction convert the right operand to the left operand's unit:
|
|
175
183
|
|
|
176
184
|
```pycon
|
|
177
185
|
>>> length = Quantity(1, 'm')
|
|
178
186
|
>>> print(length + Quantity(50, 'cm'))
|
|
179
187
|
3/2 m
|
|
180
|
-
>>> length.
|
|
188
|
+
>>> length.value_in('cm')
|
|
181
189
|
100
|
|
182
|
-
>>> Quantity(1, 'cm').
|
|
190
|
+
>>> Quantity(1, 'cm').value_in('m')
|
|
183
191
|
Fraction(1, 100)
|
|
184
192
|
|
|
185
193
|
```
|
|
@@ -346,29 +354,90 @@ Fraction(1001, 1)
|
|
|
346
354
|
|
|
347
355
|
Exact unit algebra and numerical magnitude evaluation have different rules:
|
|
348
356
|
|
|
349
|
-
-
|
|
350
|
-
|
|
351
|
-
`
|
|
352
|
-
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
-
|
|
356
|
-
|
|
357
|
-
|
|
357
|
+
- Formal exponents use one rule in Monomial, Dimension, Factor, Unit, and
|
|
358
|
+
Quantity: int/Fraction/Decimal values stay exact; floats use their shortest
|
|
359
|
+
decimal text without a denominator limit. Thus `0.1` means `1/10` and
|
|
360
|
+
`1e-10` remains nonzero. Use `Fraction(1, 3)` for exactly one third:
|
|
361
|
+
the float `1 / 3` means the decimal `0.3333333333333333` in formal exponents.
|
|
362
|
+
Factor's numeric scale inputs retain their separate denominator-limit rules.
|
|
363
|
+
- Quantity handles units; the magnitudes determine numerical arithmetic.
|
|
364
|
+
Operators use the magnitudes in operand order and preserve native promotion,
|
|
365
|
+
rounding, signals, and exceptions. Decimal arithmetic returns Decimal and
|
|
366
|
+
follows its active context. Mixing Decimal with float or Fraction raises
|
|
367
|
+
`TypeError`, as it does for the bare numbers; choose a common numerical type
|
|
368
|
+
explicitly when needed.
|
|
369
|
+
- Conversion with a rational scale keeps int/Fraction values exact. Decimal
|
|
370
|
+
conversion combines the value and scale exactly internally, then rounds once
|
|
371
|
+
to Decimal in the active context. Irrational unit scales are approximated as
|
|
372
|
+
floats, using their shortest decimal representations for Decimal conversion.
|
|
373
|
+
A scale of one preserves the magnitude without arithmetic or rounding.
|
|
374
|
+
Direct `Factor(value)` retains its separate exact Fraction evaluation rules.
|
|
375
|
+
- Addition and subtraction align the right operand with the left quantity's
|
|
376
|
+
unit before numerical arithmetic. Integers being aligned with a Decimal
|
|
377
|
+
magnitude are scaled in the Decimal domain, so `Decimal('1') * si.m + 50 *
|
|
378
|
+
si.cm` returns a Decimal magnitude of `1.5`. Explicit Fraction/float operands
|
|
379
|
+
keep their own numerical domains. Finite-precision conversion and arithmetic
|
|
380
|
+
can round; changing the result unit can therefore change rounding errors.
|
|
381
|
+
- Unary `+` and `-` apply the magnitude's native operators and preserve the unit.
|
|
382
|
+
Decimal results remain Decimal and follow its context precision, rounding,
|
|
383
|
+
signals, and exceptions; unary `+` can therefore change the numerical value.
|
|
384
|
+
- Integer division produces a float; Fraction arithmetic remains rational.
|
|
385
|
+
Use Fraction magnitudes explicitly for exact rational results. Powers pass
|
|
386
|
+
the original exponent to the magnitude: `2 ** 2.0` returns float and
|
|
387
|
+
`2 ** Decimal('0.5')` returns Decimal. Decimal magnitudes require int or
|
|
388
|
+
Decimal exponents, as in Python. The unit follows the common formal exponent
|
|
389
|
+
rule above; numerical evaluation still uses the original exponent object.
|
|
390
|
+
Exponents must be finite real scalars other than bool. Numerical powers can
|
|
391
|
+
round or produce complex values, following the magnitude's rules.
|
|
392
|
+
- Truth values follow the magnitude, so zero quantities are false regardless
|
|
393
|
+
of unit. Comparisons with equal unit factors use native magnitude operators,
|
|
394
|
+
including independent `!=`, Decimal signals/exceptions and complex ordering
|
|
395
|
+
errors. For different unit factors, builtin real values and complex
|
|
396
|
+
components are aligned exactly to avoid rounding-dependent equality or
|
|
397
|
+
hashes. Ordinary numeric subclasses use the same alignment as their base
|
|
398
|
+
types. Decimal/float signals apply to native comparisons with equal factors;
|
|
399
|
+
cross-factor alignment compares exact rational coefficients and does not
|
|
400
|
+
perform an additional comparison merely to reproduce signals. Decimal/Fraction
|
|
401
|
+
comparisons are supported natively.
|
|
358
402
|
- Equality is exact, without a tolerance. Finite floats are compared as their
|
|
359
403
|
stored binary rational values. Floating-point conversions and irrational
|
|
360
|
-
powers may round;
|
|
361
|
-
|
|
404
|
+
powers may round; use `isclose()` for approximate real scalar comparisons.
|
|
405
|
+
It uses the symmetric condition
|
|
406
|
+
`abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)` in coherent SI
|
|
407
|
+
units, with defaults `rel_tol=1e-9` and `abs_tol=0`. Swapping quantities
|
|
408
|
+
preserves the result. Nonzero absolute tolerances for dimensional quantities
|
|
409
|
+
must carry matching units; plain operands and plain absolute tolerances
|
|
410
|
+
represent unscaled dimensionless values. Tolerances must be finite and
|
|
411
|
+
nonnegative. Different dimensions raise `DimensionError`. NaNs are never
|
|
412
|
+
close, and infinities are close only to infinities of the same sign.
|
|
413
|
+
Finite rational coefficients stay exact; residual irrational unit factors
|
|
414
|
+
are approximated. With both tolerances zero, `isclose()` uses exact equality.
|
|
362
415
|
|
|
363
416
|
```pycon
|
|
364
|
-
>>> from decimal import Decimal
|
|
417
|
+
>>> from decimal import Decimal, localcontext
|
|
365
418
|
>>> from fractions import Fraction
|
|
419
|
+
>>> with localcontext() as context:
|
|
420
|
+
... context.prec = 2
|
|
421
|
+
... q = Quantity(Decimal('1.2345'), 'm')
|
|
422
|
+
... print((+q).value, (-q).value, (q * 2).value)
|
|
423
|
+
1.2 -1.2 2.5
|
|
366
424
|
>>> Quantity(Decimal('0.1'), 'm').to('cm').value
|
|
367
|
-
10
|
|
425
|
+
Decimal('10')
|
|
368
426
|
>>> (Quantity(Fraction(1), 'm') / Quantity(3, 's')).value
|
|
369
427
|
Fraction(1, 3)
|
|
428
|
+
>>> (Quantity(2, 'm') ** 2.0).value
|
|
429
|
+
4.0
|
|
430
|
+
>>> bool(Quantity(0, 'm'))
|
|
431
|
+
False
|
|
370
432
|
>>> Quantity(0.1, '') == Quantity(Fraction(1, 10), '')
|
|
371
433
|
False
|
|
434
|
+
>>> Quantity(0.1 + 0.2, 'm').isclose(Quantity(0.3, 'm'))
|
|
435
|
+
True
|
|
436
|
+
>>> a, b = Quantity(1, 'm'), Quantity(100.05, 'cm')
|
|
437
|
+
>>> a.isclose(b, rel_tol=0, abs_tol=Quantity(1, 'mm'))
|
|
438
|
+
True
|
|
439
|
+
>>> b.isclose(a, rel_tol=0, abs_tol=Quantity(1, 'mm'))
|
|
440
|
+
True
|
|
372
441
|
|
|
373
442
|
```
|
|
374
443
|
|
|
@@ -388,9 +457,10 @@ python -m pip install sympy
|
|
|
388
457
|
- Custom magnitudes must support scalar arithmetic, return a Python `bool`
|
|
389
458
|
from equality, and defer raw operations with `Unit` operands to its reflected
|
|
390
459
|
operators.
|
|
391
|
-
- Scalar
|
|
392
|
-
|
|
393
|
-
|
|
460
|
+
- Scalar operations use the magnitude's supported operand types, including
|
|
461
|
+
complex numbers and custom scalar magnitudes. Custom magnitudes must also
|
|
462
|
+
defer operations with Quantity operands so that Quantity can handle units.
|
|
463
|
+
Ordering follows the magnitude's native rules.
|
|
394
464
|
- Unit conversion is multiplicative. Offset temperature scales such as Celsius
|
|
395
465
|
and Fahrenheit are not supported.
|
|
396
466
|
|
|
@@ -39,17 +39,18 @@ class Magnitude(Protocol):
|
|
|
39
39
|
can produce complex values. NumPy scalars, arrays, and operation dispatch
|
|
40
40
|
are not supported yet; NumPy integration is planned.
|
|
41
41
|
|
|
42
|
-
Forward
|
|
42
|
+
Forward binary arithmetic has an unspecified operand domain at
|
|
43
43
|
this protocol boundary. `Never` lets concrete numeric methods satisfy the
|
|
44
44
|
protocol without claiming that they accept every operand. In particular,
|
|
45
|
-
raw magnitudes must defer Unit operands to Unit's reflected
|
|
46
|
-
which construct quantities and preserve the magnitude's type.
|
|
45
|
+
raw magnitudes must defer Unit operands to Unit's reflected multiplication
|
|
46
|
+
and division, which construct quantities and preserve the magnitude's type.
|
|
47
|
+
They must also defer Quantity operands so that Quantity can handle units.
|
|
47
48
|
"""
|
|
48
49
|
|
|
49
50
|
def __pos__(self, /) -> Magnitude: ...
|
|
50
51
|
def __neg__(self, /) -> Magnitude: ...
|
|
51
|
-
def __add__(self, other:
|
|
52
|
-
def __sub__(self, other:
|
|
52
|
+
def __add__(self, other: Never, /) -> Magnitude: ...
|
|
53
|
+
def __sub__(self, other: Never, /) -> Magnitude: ...
|
|
53
54
|
def __mul__(self, other: Never, /) -> Magnitude: ...
|
|
54
55
|
def __truediv__(self, other: Never, /) -> Magnitude: ...
|
|
55
56
|
def __pow__(self, other: Any, /) -> Magnitude: ...
|
|
@@ -120,6 +121,16 @@ def as_int_if_integral(value: Fraction, /) -> int | Fraction:
|
|
|
120
121
|
return value.numerator if value.denominator == 1 else value
|
|
121
122
|
|
|
122
123
|
|
|
124
|
+
def as_exponent(value: Fractionable, /) -> Fraction:
|
|
125
|
+
"""Normalize formal exponents; floats use their shortest decimal text."""
|
|
126
|
+
if not isinstance(value, Fractionable) or isinstance(value, bool):
|
|
127
|
+
raise TypeError(f'Cannot convert {type(value).__name__} to Fraction.')
|
|
128
|
+
# Nonfinite floats still use Fraction's standard ValueError/OverflowError.
|
|
129
|
+
return Fraction(
|
|
130
|
+
str(float(value)) if isinstance(value, float) and math.isfinite(value) else value
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
123
134
|
# Number theory functions: prime factorization and primality testing.
|
|
124
135
|
# These are implemented here to avoid a dependency on `sympy`.
|
|
125
136
|
# However, the implementation is limited and may not be as efficient or
|
|
@@ -4,7 +4,7 @@ from collections.abc import Iterable
|
|
|
4
4
|
from fractions import Fraction
|
|
5
5
|
from typing import Literal
|
|
6
6
|
|
|
7
|
-
from ._numerics import Fractionable,
|
|
7
|
+
from ._numerics import Fractionable, as_exponent, as_int_if_integral
|
|
8
8
|
from ._unicode import superscript
|
|
9
9
|
|
|
10
10
|
__all__ = ['DIMENSIONLESS', 'Dimension']
|
|
@@ -26,7 +26,8 @@ class Dimension:
|
|
|
26
26
|
- `J`: luminous
|
|
27
27
|
|
|
28
28
|
Zero exponents represent the `DIMENSIONLESS` singleton. Use Fraction or
|
|
29
|
-
Decimal for exact exponents; floats
|
|
29
|
+
Decimal for exact exponents; floats use their shortest decimal text,
|
|
30
|
+
without a denominator limit, consistently with Monomial, Factor, and Unit.
|
|
30
31
|
|
|
31
32
|
>>> dim_velocity = Dimension(L=1, T=-1)
|
|
32
33
|
>>> dim_force = Dimension(M=1, L=1, T=-2)
|
|
@@ -58,7 +59,7 @@ class Dimension:
|
|
|
58
59
|
N: Fractionable = 0,
|
|
59
60
|
J: Fractionable = 0,
|
|
60
61
|
) -> Dimension:
|
|
61
|
-
exponents = tuple(
|
|
62
|
+
exponents = tuple(as_exponent(exp) for exp in (T, L, M, I, Theta, N, J))
|
|
62
63
|
if not any(exponents):
|
|
63
64
|
return DIMENSIONLESS
|
|
64
65
|
obj = object.__new__(cls)
|
|
@@ -125,7 +126,7 @@ class Dimension:
|
|
|
125
126
|
return self._from_iter(a - b for a, b in zip(self._exponents, other._exponents))
|
|
126
127
|
|
|
127
128
|
def __pow__(self, power: Fractionable) -> Dimension:
|
|
128
|
-
power_as_fraction =
|
|
129
|
+
power_as_fraction = as_exponent(power)
|
|
129
130
|
return self._from_iter(exp * power_as_fraction for exp in self._exponents)
|
|
130
131
|
|
|
131
132
|
def __rtruediv__(self, other: Literal[1]) -> Dimension:
|
|
@@ -9,7 +9,14 @@ from decimal import MAX_EMAX, MIN_EMIN, Context, Decimal, Overflow, localcontext
|
|
|
9
9
|
from fractions import Fraction
|
|
10
10
|
from typing import Any, overload
|
|
11
11
|
|
|
12
|
-
from ._numerics import
|
|
12
|
+
from ._numerics import (
|
|
13
|
+
Fractionable,
|
|
14
|
+
Magnitude,
|
|
15
|
+
UnfactoredInteger,
|
|
16
|
+
as_exponent,
|
|
17
|
+
as_fraction,
|
|
18
|
+
as_int_if_integral,
|
|
19
|
+
)
|
|
13
20
|
from .exceptions import _WARNING_SKIP_PREFIXES, IncompleteFactorWarning
|
|
14
21
|
from .monomial import Monomial
|
|
15
22
|
|
|
@@ -162,7 +169,7 @@ class SymbolicConstant:
|
|
|
162
169
|
def __pow__(self, other: Fractionable) -> Factor:
|
|
163
170
|
if not isinstance(other, Fractionable):
|
|
164
171
|
return NotImplemented
|
|
165
|
-
return Factor._from_monomial(Monomial({self:
|
|
172
|
+
return Factor._from_monomial(Monomial({self: as_exponent(other)}))
|
|
166
173
|
|
|
167
174
|
@property
|
|
168
175
|
def symbol(self) -> str:
|
|
@@ -198,11 +205,13 @@ class Factor:
|
|
|
198
205
|
and classification may be incomplete, and equivalent expressions may compare
|
|
199
206
|
unequal. Install SymPy for more complete support.
|
|
200
207
|
|
|
201
|
-
Construction and
|
|
202
|
-
with denominators at most 1,000,000. Inexact conversions emit
|
|
203
|
-
InexactFloatWarning. Small positive
|
|
204
|
-
positivity check: Factor(1e-10) warns and raises ValueError. Use Decimal
|
|
205
|
-
Fraction for exact
|
|
208
|
+
Construction and ordinary multiplication/division convert float scales to
|
|
209
|
+
Fractions with denominators at most 1,000,000. Inexact conversions emit
|
|
210
|
+
InexactFloatWarning. Small positive float scales can round to zero and fail
|
|
211
|
+
the positivity check: Factor(1e-10) warns and raises ValueError. Use Decimal
|
|
212
|
+
or Fraction for exact scales. Exponents, including from_dict(), instead use
|
|
213
|
+
a float's shortest decimal text without a denominator limit, consistently
|
|
214
|
+
with Monomial, Dimension, and Unit. Use Fraction(1, 3) for exactly one third.
|
|
206
215
|
|
|
207
216
|
Factor equality compares stored bases and exponents. Numeric operands are
|
|
208
217
|
compared as exact rational values, including the stored value of floats.
|
|
@@ -226,11 +235,13 @@ class Factor:
|
|
|
226
235
|
Fraction(127, 50)
|
|
227
236
|
"""
|
|
228
237
|
|
|
229
|
-
__slots__ = ('_monomial', '_value_cache')
|
|
238
|
+
__slots__ = ('_monomial', '_value_cache', '_decompose_cache')
|
|
230
239
|
_monomial: Monomial[int | SymbolicConstant]
|
|
231
240
|
_value_cache: int | Fraction | float | None
|
|
241
|
+
_decompose_cache: tuple[Fraction, Factor] | None
|
|
232
242
|
|
|
233
243
|
def __init__(self, value: Fractionable | SymbolicConstant, /) -> None:
|
|
244
|
+
self._decompose_cache = None
|
|
234
245
|
if isinstance(value, SymbolicConstant):
|
|
235
246
|
self._monomial = Monomial({value: 1})
|
|
236
247
|
self._value_cache = float(value)
|
|
@@ -264,6 +275,7 @@ class Factor:
|
|
|
264
275
|
obj = object.__new__(cls)
|
|
265
276
|
obj._monomial = monomial
|
|
266
277
|
obj._value_cache = None
|
|
278
|
+
obj._decompose_cache = None
|
|
267
279
|
return obj
|
|
268
280
|
|
|
269
281
|
@classmethod
|
|
@@ -279,7 +291,7 @@ class Factor:
|
|
|
279
291
|
raise TypeError(error_message)
|
|
280
292
|
normalized: dict[int | SymbolicConstant, Fraction] = {}
|
|
281
293
|
for base, exponent in components.items():
|
|
282
|
-
normalized[base] =
|
|
294
|
+
normalized[base] = as_exponent(exponent)
|
|
283
295
|
monomial = Monomial(normalized)
|
|
284
296
|
if not all(isinstance(base, (int, SymbolicConstant)) for base in monomial.bases()):
|
|
285
297
|
raise TypeError('All bases must be int or SymbolicConstant.')
|
|
@@ -317,6 +329,8 @@ class Factor:
|
|
|
317
329
|
The rational part is a Fraction, and the irrational part is a Factor
|
|
318
330
|
containing only SymbolicConstant bases and integer bases with *proper*
|
|
319
331
|
fraction exponents. The product of the two parts equals the original factor.
|
|
332
|
+
Rationalized decompositions are cached lazily on this factor; default
|
|
333
|
+
decompositions are computed on each call, independently of the value cache.
|
|
320
334
|
|
|
321
335
|
Parameters
|
|
322
336
|
----------
|
|
@@ -337,6 +351,8 @@ class Factor:
|
|
|
337
351
|
>>> (Factor(2) ** Fraction(-3, 2)).decompose(rationalize_denominator=True)
|
|
338
352
|
(Fraction(1, 4), Factor.from_dict({2: Fraction(1, 2)}))
|
|
339
353
|
"""
|
|
354
|
+
if rationalize_denominator and self._decompose_cache is not None:
|
|
355
|
+
return self._decompose_cache
|
|
340
356
|
rational = Fraction(1)
|
|
341
357
|
irrational: dict[int | SymbolicConstant, Fraction] = {}
|
|
342
358
|
for base, exponent in self._monomial.components():
|
|
@@ -348,7 +364,10 @@ class Factor:
|
|
|
348
364
|
integer_part = math.floor(exponent) if rationalize_denominator else int(exponent)
|
|
349
365
|
rational *= Fraction(base) ** integer_part
|
|
350
366
|
irrational[base] = exponent - integer_part
|
|
351
|
-
|
|
367
|
+
result = rational, Factor._from_monomial(Monomial._from_dict(irrational))
|
|
368
|
+
if rationalize_denominator:
|
|
369
|
+
self._decompose_cache = result
|
|
370
|
+
return result
|
|
352
371
|
|
|
353
372
|
@property
|
|
354
373
|
def value(self) -> int | Fraction | float:
|
|
@@ -490,7 +509,7 @@ class Factor:
|
|
|
490
509
|
return NotImplemented
|
|
491
510
|
|
|
492
511
|
def __pow__(self, other: Fractionable) -> Factor:
|
|
493
|
-
return Factor._from_monomial(self._monomial **
|
|
512
|
+
return Factor._from_monomial(self._monomial ** as_exponent(other))
|
|
494
513
|
|
|
495
514
|
@overload
|
|
496
515
|
def __call__(self, magnitude: Fraction) -> Fraction | float: ...
|
|
@@ -5,7 +5,7 @@ from fractions import Fraction
|
|
|
5
5
|
from types import MappingProxyType
|
|
6
6
|
from typing import Generic, Literal, TypeVar
|
|
7
7
|
|
|
8
|
-
from ._numerics import Fractionable,
|
|
8
|
+
from ._numerics import Fractionable, as_exponent
|
|
9
9
|
from ._unicode import DOT, superscript
|
|
10
10
|
|
|
11
11
|
__all__ = ['Monomial']
|
|
@@ -31,8 +31,8 @@ class Monomial(Generic[BaseT]):
|
|
|
31
31
|
components : Mapping[BaseT, Fractionable], optional
|
|
32
32
|
Bases and exponents. The mapping is copied and zero exponents are
|
|
33
33
|
removed. Defaults to an empty monomial. Use int, Fraction, or Decimal
|
|
34
|
-
for exact exponents. Float exponents
|
|
35
|
-
|
|
34
|
+
for exact exponents. Float exponents use their shortest decimal text,
|
|
35
|
+
so 0.1 means Fraction(1, 10); no denominator limit is applied.
|
|
36
36
|
|
|
37
37
|
Notes
|
|
38
38
|
-----
|
|
@@ -76,7 +76,7 @@ class Monomial(Generic[BaseT]):
|
|
|
76
76
|
raise TypeError(error_message)
|
|
77
77
|
normalized: dict[BaseT, Fraction] = {}
|
|
78
78
|
for base, exp in components.items():
|
|
79
|
-
if exponent :=
|
|
79
|
+
if exponent := as_exponent(exp):
|
|
80
80
|
normalized[base] = exponent
|
|
81
81
|
obj = super().__new__(cls)
|
|
82
82
|
object.__setattr__(obj, '_components', MappingProxyType(normalized))
|
|
@@ -193,7 +193,7 @@ class Monomial(Generic[BaseT]):
|
|
|
193
193
|
def __pow__(self, other: Fractionable) -> Monomial[BaseT]:
|
|
194
194
|
if not isinstance(other, Fractionable):
|
|
195
195
|
return NotImplemented
|
|
196
|
-
other_as_fraction =
|
|
196
|
+
other_as_fraction = as_exponent(other)
|
|
197
197
|
if other_as_fraction == 0:
|
|
198
198
|
return self._from_dict({})
|
|
199
199
|
return self._from_dict({base: exp * other_as_fraction for base, exp in self.components()})
|