diff --git a/pyproject.toml b/pyproject.toml index 4d4fe9d3..94b5d378 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,6 +53,7 @@ astro = ["coordinaxs.astro>=0.24"] benchmark = ["pytest-benchmark>=5.2.3", "pytest-codspeed>=5.0.3"] curveframes = ["coordinaxs.curveframes>=0.24"] interop-astropy = ["coordinaxs.interop.astropy>=0.24"] +parametric = ["unxts.parametric>=2.0"] workspace = [ "coordinaxs.api>=0.24", "coordinaxs.astro>=0.24", diff --git a/src/coordinax/distances/_src/register_parametric.py b/src/coordinax/distances/_src/register_parametric.py index cd347edf..50effc57 100644 --- a/src/coordinax/distances/_src/register_parametric.py +++ b/src/coordinax/distances/_src/register_parametric.py @@ -1,18 +1,24 @@ -"""`from_` overloads for `unxts.parametric`; imported only when it is installed. +"""Registrations for `unxts.parametric`; imported only when it is installed. A `ParametricQuantity["length"|"angle"|"mag"]` carries its physical type in the *type*, so plum can pick the branch statically and prefers these over the `AbstractQuantity` catch-all in `measures`, which has to read `u.dimension_of(q)` at runtime. Plain `unxt.Quantity` still takes that path. + +It is also not a `unxt.Q` subclass, so the `AbstractDistance`/`Q` promotion +rules in `base` never reach it; the rule below closes that. """ __all__: tuple[str, ...] = () from typing import Any +from plum import add_promotion_rule + # Optional dependency: absent from the lint environment by design. from unxts.parametric import PQ # ty: ignore[unresolved-import] +from .base import AbstractDistance from .measures import Distance, _from_angle, _from_length, _from_mag @@ -32,3 +38,8 @@ def from_(cls: type[Distance], q: PQ["angle"], /, **kw: Any) -> Distance: def from_(cls: type[Distance], q: PQ["mag"], /, **kw: Any) -> Distance: """Construct a distance from a parametric magnitude quantity.""" return _from_mag(cls, q, **kw) + + +# Degrade to the parametric quantity, as the `AbstractDistance`/`Q` rules in +# `base` do. +add_promotion_rule(AbstractDistance, PQ, PQ) diff --git a/tests/unit/distances/test_parametric.py b/tests/unit/distances/test_parametric.py index cc4d48cf..72d8b999 100644 --- a/tests/unit/distances/test_parametric.py +++ b/tests/unit/distances/test_parametric.py @@ -5,6 +5,7 @@ """ import jax.numpy as jnp +import plum import pytest import unxt as u @@ -69,3 +70,21 @@ def test_dispatches_by_type_not_by_dimension(self, value: float, unit: str) -> N parametric = _resolved_from_(cxd.Distance, PQ(value, unit)) plain = _resolved_from_(cxd.Distance, u.Q(value, unit)) assert parametric is not plain + + +class TestParametricPromotion: + """A distance and a `PQ` promote to the `PQ`.""" + + def test_promotes_to_parametric_quantity(self) -> None: + """`plum.promote` degrades the distance, as it does for `Q`.""" + promoted = plum.promote(cxd.Distance(1, "pc"), PQ(1.0, "rad")) + assert all(isinstance(x, PQ) for x in promoted) + + def test_arithmetic_is_order_independent(self) -> None: + """Both operand orders give the same `PQ`; `Distance * PQ` used to raise.""" + d, pq = cxd.Distance(1, "pc"), PQ(1.0, "rad") + forward, reverse = d * pq, pq * d + assert isinstance(forward, PQ) + assert isinstance(reverse, PQ) + assert forward.unit == reverse.unit + assert jnp.allclose(forward.value, reverse.value) diff --git a/uv.lock b/uv.lock index 45dd457f..cc1c4f8a 100644 --- a/uv.lock +++ b/uv.lock @@ -487,6 +487,9 @@ curveframes = [ interop-astropy = [ { name = "coordinaxs-interop-astropy" }, ] +parametric = [ + { name = "unxts-parametric" }, +] workspace = [ { name = "coordinaxs-api" }, { name = "coordinaxs-astro" }, @@ -627,10 +630,11 @@ requires-dist = [ { name = "typing-extensions", specifier = ">=4.13.2" }, { name = "unxt", specifier = ">=2.0" }, { name = "unxts-linalg", specifier = ">=2.0.3" }, + { name = "unxts-parametric", marker = "extra == 'parametric'", specifier = ">=2.0" }, { name = "wadler-lindig", specifier = ">=0.1.6" }, { name = "xmmutablemap", specifier = ">=0.1" }, ] -provides-extras = ["astro", "benchmark", "curveframes", "interop-astropy", "workspace"] +provides-extras = ["astro", "benchmark", "curveframes", "interop-astropy", "parametric", "workspace"] [package.metadata.requires-dev] build = [{ name = "build", specifier = ">=1.3.0" }]