Scalar numerical behavior
Exact unit structure and numerical magnitude evaluation have separate contracts. Quantity handles units; the magnitude determines the numerical domain used by arithmetic. Array calculations use the separate NumPy conversion rules.
>>> from decimal import Decimal, localcontext
>>> from fractions import Fraction
>>> from unitx import Quantity, Unit
>>> from unitx.si import si
Arithmetic and result types
Construction preserves the magnitude object and type. Multiplying or dividing by a Unit only changes the unit product. Numerical operations apply the magnitudes' operators in operand order, preserving their promotion, rounding, signals and exceptions.
| Operation/domain | Behavior |
|---|---|
| int true division | Python float result |
| Fraction arithmetic | Exact rational result when the other operand supports it |
| Decimal arithmetic | Decimal result under the active context |
| Decimal with float/Fraction arithmetic | Native TypeError; choose a common domain explicitly |
Unary + and - |
Apply the native magnitude operator; keep the unit |
bool(q) |
Delegate to bool(q.value); a zero magnitude is false regardless of unit |
>>> (Quantity(Fraction(1), 'm') / Quantity(3, 's')).value
Fraction(1, 3)
>>> with localcontext() as context:
... context.prec = 2
... q = Quantity(Decimal('1.2345'), 'm')
... print((+q).value, (-q).value, (q * 2).value)
1.2 -1.2 2.5
>>> bool(Quantity(0, 'm'))
False
Custom scalar magnitudes are responsible for their own operator domain and
must defer Unit operands to reflected construction. Structural protocol
compatibility alone is not a promise that every third-party number works.
The supported real inputs of unitx.math are narrower: its Python scalar
backend returns float magnitudes, as described in standard-library enhancements.
Conversion and addition
Rational unit scales keep int/Fraction magnitudes exact. Decimal conversion combines the magnitude and rational scale exactly internally, then rounds once to Decimal under the active context. A scale of one returns the original magnitude without arithmetic or rounding. Irrational residual scales require numerical approximation; Decimal conversion uses the float approximation's shortest decimal representation.
Addition and subtraction first convert the right operand to the left quantity's unit. When an int operand is aligned with a Decimal magnitude, it is scaled in the Decimal domain. Explicit Fraction and float operands keep their own domains. Conversion and arithmetic can each round; changing the calculation's unit can therefore change rounding errors.
>>> Quantity(1, 'cm').value_in('m')
Fraction(1, 100)
>>> Quantity(Decimal('0.1'), 'm').to('cm').value
Decimal('10')
>>> (Decimal('1') * si.m + 50 * si.cm).value
Decimal('1.5')
>>> (Quantity(1, 'km/m') + 1).value_in('')
Fraction(1001, 1)
.value always means the magnitude in the stored unit, including an extra
Unit multiplier. Plain numbers used with dimensionless quantities are
coherent, unscaled values; they do not inherit the quantity's stored scale.
Exponents
Formal exponents share one rule across Monomial, Dimension, Factor, Unit and
Quantity. int/Fraction/Decimal exponents remain exact. Float exponents use
their shortest decimal text without a denominator limit: 0.1 means 1/10,
and 1e-10 remains nonzero. Use Fraction(1, 3) for exactly one third;
the float 1 / 3 instead denotes the finite decimal 0.3333333333333333.
Quantity passes the original exponent object to its magnitude, so numerical powers still follow native Python rules. For example, an int raised to 2.0 returns float; Decimal magnitudes require int or Decimal exponents. Exponents must be finite real scalars other than bool. Numerical powers may round or produce complex values, even though the unit exponent remains rational.
>>> (Unit('m') ** 0.1) ** 10 == Unit('m')
True
>>> Unit('m') ** Fraction(1, 3) == Unit('m^(1/3)')
True
>>> (Quantity(2, 'm') ** 2.0).value
4.0
This exponent rule is distinct from Factor's numeric scale input, whose float rationalization uses a denominator limit.
Equality, ordering and hashing
Equality has no tolerance. Finite floats retain their stored binary rational
values; they are not replaced by intended decimal fractions. With equal unit
factors, comparison uses the native magnitude operators, including independent
!=, Decimal signals and complex ordering errors.
With different factors, builtin real values and complex components are aligned exactly for equality and hashing. Ordinary numeric subclasses follow their base types' alignment. Cross-factor rational comparison does not perform an extra native Decimal/float comparison merely to reproduce its signals. Decimal/Fraction comparison is supported natively. Ordering cancels common irrational factors before approximating any remaining irrational ratio.
>>> Quantity(0.1, '') == Quantity(Fraction(1, 10), '')
False
>>> Quantity(1, 'm') == Quantity(100, 'cm')
True
>>> len({Quantity(1, 'm'), Quantity(100, 'cm')})
1
Equal scalar quantities have matching hashes when their magnitudes are hashable. Quantities with any NumPy magnitude are unhashable. Array comparisons use numerical broadcasting, not this exact scalar comparison path.
Close comparisons
q.isclose(other, rel_tol=1e-9, abs_tol=0) uses the symmetric condition in
coherent SI units:
abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)
Finite rational coefficients remain exact; residual irrational unit factors are approximated. Zero tolerances select exact equality. Tolerances must be finite and nonnegative. Relative tolerance is dimensionless; a nonzero absolute tolerance for dimensional quantities must carry matching units. Plain operands and plain absolute tolerances are coherent dimensionless values. Different dimensions raise DimensionError. NaNs are never close; infinities are close only when they have the same sign.
>>> Quantity(0.1 + 0.2, 'm').isclose(Quantity(0.3, 'm'))
True
>>> a, b = Quantity(1, 'm'), Quantity(100.05, 'cm')
>>> a.isclose(b, rel_tol=0, abs_tol=1 * si.mm)
True
>>> b.isclose(a, rel_tol=0, abs_tol=1 * si.mm)
True
NumPy isclose uses its own asymmetric formula and defaults; see
array comparisons. AffineQuantity point tolerances
have a separate origin-independent contract.
Symbolic factors and pi
Factor represents a positive multiplicative expression with rational
exponents. SymbolicConstant introduces an independent positive mathematical
constant under its documented construction assumptions; it does not infer
mathematical independence from numerical values. Known products, roots and
rational scales belong in Factor, rather than new constant identities.
>>> import math as stdmath
>>> from unitx.factor import Factor, pi
>>> pi / pi == Factor(1)
True
>>> Quantity(180, 'deg') == Quantity(stdmath.pi, 'rad')
False
>>> Quantity(180, 'deg') == Quantity(1, pi * si.rad)
True
>>> Quantity(180, 'deg').isclose(Quantity(stdmath.pi, 'rad'))
True
>>> Factor(Fraction(1, 3))(Decimal('1.5'))
Fraction(1, 2)
The degree factor retains symbolic pi/180. Standard-library math.pi is a
finite float whose stored value is rational; it is not exact mathematical pi.
unitx.pi.value currently supplies the same float approximation for numerical
evaluation. Symbolic cancellation remains exact; arbitrary-precision numerical
evaluation is not provided.
Factor accepts positive int/Fraction/Decimal/float scale inputs. Fraction and finite Decimal inputs remain exact. Numeric float scale inputs use denominator limiting and can warn, or round a tiny positive value to zero and fail. Factor evaluation returns int/Fraction when rational and float otherwise. Calling a Factor on a finite Decimal magnitude converts that magnitude exactly to Fraction, independently of Decimal context; this direct Factor API differs from Quantity conversion, which retains Decimal.
The built-in integer factorization backend is bounded. Unresolved factors
emit IncompleteFactorWarning and may limit simplification and comparisons.
SymPy is an optional, more capable factorization backend:
python -m pip install sympy