Linear differences and affine points

Unit and Quantity describe linear scales and differences. AffineUnit describes a linear coordinate's physical origin and difference scale; AffineQuantity represents a reading in that coordinate. Logarithmic coordinates have separate LogUnit and LogQuantity types.

>>> from decimal import Decimal
>>> from fractions import Fraction
>>> from unitx import AffineQuantity, AffineUnit, Quantity, Unit
>>> from unitx.imperial import imperial
>>> from unitx.si import si
>>> type(Unit('degC')) is Unit
True
>>> 20 * si.degC == Quantity(20, 'K')
True
>>> type(20 @ si.degC) is AffineQuantity
True
>>> print((20 @ si.degC).to(imperial.degF))
68 °F

value @ coordinate constructs a point; value * coordinate constructs a Quantity difference without applying the origin. The syntax is directional: coordinate @ value is unsupported. AffineQuantity(value, coordinate) is the explicit point constructor. Quantity(value, coordinate) and ordinary Quantity conversions use the descriptor's linear difference identity. Quantity matrix multiplication retains its ordinary numerical meaning.

Point identity belongs to the quantity class, including zero-origin coordinates: 300 * si.K is a difference and 300 @ si.K is a point. They do not compare equal.

Point conversion and physical exit

.to(coordinate) returns an AffineQuantity, including conversion to K. It accepts only linear affine coordinates. .value_in(coordinate) returns the new reading. .to_quantity(unit) explicitly evaluates a physical Quantity for multiplication or other ordinary quantity arithmetic.

>>> point = 0 @ si.degC
>>> type(point.to(si.K)) is AffineQuantity
True
>>> point.value_in(si.K)
Fraction(5463, 20)
>>> point.to_quantity(si.K) == Quantity(Fraction(5463, 20), si.K)
True
>>> point.to_quantity(si.K) * 2 == Quantity(Fraction(5463, 10), si.K)
True

The target of .to_quantity() is always interpreted as a linear difference unit, including a temperature descriptor. Thus (20 @ si.degC).to('degC') is a point reading 20 °C, whereas .to_quantity('degC') evaluates its physical value using the degree-Celsius difference scale, whose step equals one kelvin.

String coordinates use bound definition metadata. degC, (degC) and [degree Celsius] select the same origin. Prefixes and compound expressions have their ordinary linear meaning; they do not create new coordinate origins.

Point and difference arithmetic

Operation Result
point - compatible point Quantity in the left coordinate's difference unit
point + difference; difference + point AffineQuantity in the point's coordinate
point - difference AffineQuantity in the point's coordinate
point + point; difference - point TypeError
point multiplication, division, powers, negation, absolute value TypeError
>>> hot, cold = 300 @ si.K, 20 @ si.degC
>>> hot - cold == Quantity(Fraction(137, 20), si.K)
True
>>> cold + (hot - cold) == hot
True
>>> (30 @ si.degC) - (68 @ imperial.degF) == Quantity(10, si.K)
True

Negative readings can be constructed directly, for example (-20) @ si.degC; negating an existing point is undefined. No absolute-zero restriction is imposed. Complex readings and booleans are unsupported.

Defining coordinates

AffineUnit(reference, /, *, at=0, delta_unit=None) means that reading at represents reference. A reference is a physical Quantity or an AffineQuantity point. The difference scale defaults to the reference's difference unit.

>>> celsius = AffineUnit(Decimal('273.15') * si.K)
>>> fahrenheit = AffineUnit(0 @ celsius, at=32, delta_unit=imperial.degR)
>>> (212 @ fahrenheit).value_in(celsius)
Fraction(100, 1)
>>> fahrenheit.origin == Quantity(Fraction(45967, 180), si.K)
True

The descriptor stores only its normalized physical origin and complete delta_unit, including exact additional scales. Defining values and at must be finite real Python scalars excluding bool. Stored float values are preserved as their exact binary rationals; Decimal/Fraction definitions do not round in the active Decimal context. No reference-coordinate chain is retained.

For nonzero at, the difference scale must have a rational factor relative to the physical reference unit. Otherwise normalization would require a symbolic sum that a single scalar Quantity cannot retain exactly. Construction raises TypeError instead of approximating that origin. Supply the physical origin directly with at=0 to use an irrational difference scale.

Descriptors are immutable and hashable, with no multiplicative factor property. Multiplication, division and powers on a descriptor operate on its linear side. Name a coordinate with .define(symbol, name, ...). A definition remains standalone unless collected by a UnitCatalog or bound through a Registry.

>>> from unitx import Registry
>>> local_kelvin = si.K.define('localK', 'local kelvin')
>>> epoch = AffineUnit(10 * local_kelvin).define('localT', 'local temperature')
>>> receiver = Registry([epoch])
>>> AffineQuantity(3, receiver.parse('localT')).value_in(si.K)
Fraction(13, 1)

Definitions capture bound identities, not expressions reinterpreted in the default registry. Named projections retain their own difference identity; anonymous coordinates use their explicit delta_unit. Reconstruction requires matching definitions to be loaded in the receiving process.

Comparison, tolerances and arrays

Finite scalar equality respects exact stored values and symbolic factors. For example, float -273.15 need not equal the exact Celsius reading of 0 K. Use Decimal/Fraction definitions or a difference tolerance where appropriate.

point.isclose(other, rel_tol=0, abs_tol=difference) uses coordinate differences; relative tolerance must be zero. The absolute tolerance is a compatible Quantity or real zero. NaNs compare unequal; unsupported exact symbolic sums require an explicit numerical conversion before comparison.

Numeric NumPy arrays/scalars are supported. Comparisons produce masks; np.asarray(point) rejects implicit unit removal. Mean/median preserve the point coordinate, standard deviation returns a difference, and variance returns its squared unit. Arithmetic reductions such as summing points are unsupported. Indexed assignment and supported output operations stage conversion and casting before writing fixed-unit, fixed-dtype buffers. See NumPy support.