Let the type system catch a unit mistake#
In this tutorial we will write a function that can only be called with the right kind of quantity, and watch unxts.parametric reject a wrong one before the function body ever runs. Along the way you will see a dimension get inferred, get checked, and get carried through arithmetic.
You need unxt and unxts.parametric installed and nothing else.
Set up#
>>> import unxt as u
>>> import unxts.parametric as up
See what the default Quantity does not do#
Start with the plain unxt.Quantity. Subscript it with a dimension and give it a unit that contradicts the subscript — seconds where we asked for a length:
>>> u.Q["length"](1.0, "s")
Quantity(Array(1., dtype=float32, weak_type=True), unit='s')
Nothing happened. No error, no warning; we got a quantity of seconds back. The subscript on the default Quantity is accepted for compatibility and then ignored — it carries no dimension in its type, so it has nothing to check against.
That is the gap unxts.parametric fills.
Watch the same mistake get caught#
Now make the same call with up.PQ. We expect this one to fail, so let’s catch the error and print it:
>>> try:
... up.PQ["length"](1.0, "s")
... except ValueError as e:
... print(e)
Physical type mismatch.
There it is. ParametricQuantity["length"] is a genuine class that knows its dimension, so it compared the unit we passed against that dimension and refused. The wrong quantity never got built.
Give it a length and it is happy:
>>> up.PQ["length"](1.0, "m")
ParametricQuantity(Array(1., dtype=float32, weak_type=True), unit='m')
Let the dimension be inferred#
You do not have to write the subscript. Construct without one and the dimension is worked out from the unit:
>>> g = up.PQ(9.8, "m/s2")
>>> u.dimension_of(g)
PhysicalType('acceleration')
We never said “acceleration” anywhere. Look at what g actually is:
>>> type(g)
<class 'unxt...ParametricQuantity[PhysicalType('acceleration')]'>
The class itself is parametrized. That is the difference from the default Quantity, where every dimension shares one class — here, the dimension is part of the type, which is what made the check in the previous step possible.
A parametrized class carries its dimension even without an instance:
>>> u.dimension_of(up.PQ["length"])
PhysicalType('length')
Multiply two lengths and watch the type change#
Arithmetic re-parametrizes the result. Multiply two lengths:
>>> area = up.PQ(3.0, "m") * up.PQ(4.0, "m")
>>> area
ParametricQuantity(Array(12., dtype=float32, weak_type=True), unit='m2')
>>> u.dimension_of(area)
PhysicalType('area')
The result is no longer a length. Its type changed along with its unit:
>>> type(area) is up.PQ["area"]
True
The dimension was recomputed from the arithmetic, and the class followed.
Write a function that dispatches on dimension#
Now the payoff. Because up.PQ["length"] and up.PQ["time"] are real, distinct types, you can write two functions with the same name and let the argument’s dimension pick between them:
>>> from plum import dispatch
>>> @dispatch
... def describe(q: up.PQ["length"]) -> str:
... return "a length"
>>> @dispatch
... def describe(q: up.PQ["time"]) -> str:
... return "a duration"
Call it with a length:
>>> describe(up.PQ(5.0, "m"))
'a length'
and with a time:
>>> describe(up.PQ(5.0, "s"))
'a duration'
Same call, different implementation, chosen by the physical dimension of the argument. We never wrote an if.
Mix with plain quantities#
Parametric and plain quantities combine freely — the result is parametric, carrying the dimension:
>>> up.PQ(2.0, "m") + u.Q(3.0, "m")
ParametricQuantity(Array(5., dtype=float32, weak_type=True), unit='m')
What we built#
You have a describe function that selects its implementation from the physical dimension of its argument, and you have seen a wrong unit rejected at construction rather than producing a plausible wrong answer later. Both come from the same thing: the dimension living in the type.
That capability is not free — a class and a JAX pytree type per dimension — which is why it is opt-in rather than the default.
Where to go next#
ParametricQuantity— the full construction and dispatch surface.How to check dimensions at runtime — enforcing dimensions on function signatures with
jaxtyping.Why
Quantityis not parametric — what the per-dimension type actually costs.