Repository: GalacticDynamics/coordinax (language: python)

Issue:
bug: discrepancy against Astropy differentials
All the angular differential transformations seem to disagree on the angular components!


Discussion / hints:

Fix patch:
diff --git a/pyproject.toml b/pyproject.toml
index f3848c14..81f9ec16 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -40,7 +40,8 @@
   requires-python = ">=3.10"
 
   [project.optional-dependencies]
-    dev = ["pytest >=6", "pytest-cov >=3"]
+    all = ["vector[docs]", "vector[test]"]
+    dev = ["vector[test]"]
     docs = [
       "sphinx>=7.0",
       "myst_parser>=0.13",
@@ -146,6 +147,7 @@
       "D203",    # 1 blank line required before class docstring
       "D213",    # Multi-line docstring summary should start at the second line
       "ERA001",  # Commented out code
+      "F722",   # Syntax error in forward annotation  <- jaxtyping
       "F811",    # redefinition of unused '...'  <- plum-dispatch
       "F821",    # undefined name '...'  <- jaxtyping
       "FIX002",  # Line contains TODO
diff --git a/src/vector/_d3/__init__.py b/src/vector/_d3/__init__.py
index bbe8358e..9ac865d2 100644
--- a/src/vector/_d3/__init__.py
+++ b/src/vector/_d3/__init__.py
@@ -1,11 +1,13 @@
 """3-dimensional."""
 
-from . import base, builtin, transform
+from . import base, builtin, compat, transform
 from .base import *
 from .builtin import *
+from .compat import *
 from .transform import *
 
 __all__: list[str] = []
 __all__ += base.__all__
 __all__ += builtin.__all__
 __all__ += transform.__all__
+__all__ += compat.__all__
diff --git a/src/vector/_d3/builtin.py b/src/vector/_d3/builtin.py
index 57621de8..2e55be98 100644
--- a/src/vector/_d3/builtin.py
+++ b/src/vector/_d3/builtin.py
@@ -13,13 +13,21 @@
 
 from typing import ClassVar, final
 
+import array_api_jax_compat as xp
 import equinox as eqx
+from jax_quantity import Quantity
+from jaxtyping import Shaped
 
 from vector._typing import BatchableFloatScalarQ
 from vector._utils import converter_quantity_array
 
 from .base import Abstract3DVector, Abstract3DVectorDifferential
 
+_0m = Quantity(0, "meter")
+_0d = Quantity(0, "rad")
+_pid = Quantity(xp.pi, "rad")
+_2pid = Quantity(2 * xp.pi, "rad")
+
 ##############################################################################
 # Position
 
@@ -28,18 +36,54 @@
 class Cartesian3DVector(Abstract3DVector):
     """Cartesian vector representation."""
 
-    x: BatchableFloatScalarQ = eqx.field(converter=converter_quantity_array)
-    y: BatchableFloatScalarQ = eqx.field(converter=converter_quantity_array)
-    z: BatchableFloatScalarQ = eqx.field(converter=converter_quantity_array)
+    x: Shaped[Quantity["length"], "*#batch"] = eqx.field(
+        converter=converter_quantity_array
+    )
+    r"""X-coordinate :math:`x \in [-\infty, \infty]."""
+
+    y: Shaped[Quantity["length"], "*#batch"] = eqx.field(
+        converter=converter_quantity_array
+    )
+    r"""Y-coordinate :math:`y \in [-\infty, \infty]."""
+
+    z: Shaped[Quantity["length"], "*#batch"] = eqx.field(
+        converter=converter_quantity_array
+    )
+    r"""Z-coordinate :math:`z \in [-\infty, \infty]."""
 
 
 @final
 class SphericalVector(Abstract3DVector):
     """Spherical vector representation."""
 
-    r: BatchableFloatScalarQ = eqx.field(converter=converter_quantity_array)
+    r: Shaped[Quantity["length"], "*#batch"] = eqx.field(
+        converter=converter_quantity_array
+    )
+    r"""Radial distance :math:`r \in [0,+\infty)`."""
+
     theta: BatchableFloatScalarQ = eqx.field(converter=converter_quantity_array)
+    r"""Inclination angle :math:`\phi \in [0,180]`."""
+
     phi: BatchableFloatScalarQ = eqx.field(converter=converter_quantity_array)
+    r"""Azimuthal angle :math:`\phi \in [0,360)`."""
+
+    def __check_init__(self) -> None:
+        """Check the validity of the initialisation."""
+        _ = eqx.error_if(
+            self.r,
+            xp.any(self.r < _0m),
+            "Radial distance 'r' must be in the range [0, +inf).",
+        )
+        _ = eqx.error_if(
+            self.theta,
+            xp.any((self.theta < _0d) | (self.theta > _pid)),
+            "Inclination 'theta' must be in the range [0, pi].",
+        )
+        _ = eqx.error_if(
+            self.phi,
+            xp.any((self.phi < _0d) | (self.phi >= _2pid)),
+            "Azimuthal angle 'phi' must be in the range [0, 2 * pi).",
+        )
 
 
 @final
@@ -47,8 +91,26 @@ class CylindricalVector(Abstract3DVector):
     """Cylindrical vector representation."""
 
     rho: BatchableFloatScalarQ = eqx.field(converter=converter_quantity_array)
+    r"""Cylindrical radial distance :math:`\rho \in [0,+\infty)`."""
+
     phi: BatchableFloatScalarQ = eqx.field(converter=converter_quantity_array)
+    r"""Azimuthal angle :math:`\phi \in [0,360)`."""
+
     z: BatchableFloatScalarQ = eqx.field(converter=converter_quantity_array)
+    r"""Height :math:`z \in (-\infty,+\infty)`."""
+
+    def __check_init__(self) -> None:
+        """Check the validity of the initialisation."""
+        _ = eqx.error_if(
+            self.rho,
+            xp.any(self.rho < _0m),
+            "Cylindrical radial distance 'rho' must be in the range [0, +inf).",
+        )
+        _ = eqx.error_if(
+            self.phi,
+            xp.any((self.phi < _0d) | (self.phi >= _2pid)),
+            "Azimuthal angle 'phi' must be in the range [0, 2 * pi).",
+        )
 
 
 ##############################################################################
diff --git a/src/vector/_d3/compat.py b/src/vector/_d3/compat.py
new file mode 100644
index 00000000..35fdf579
--- /dev/null
+++ b/src/vector/_d3/compat.py
@@ -0,0 +1,171 @@
+"""Compatibility via :func:`plum.convert`."""
+
+__all__: list[str] = []
+
+
+import astropy.coordinates as apyc
+import astropy.units as apyu
+from plum import conversion_method
+
+from .builtin import (
+    Cartesian3DVector,
+    CartesianDifferential3D,
+    CylindricalDifferential,
+    CylindricalVector,
+    SphericalDifferential,
+    SphericalVector,
+)
+
+#####################################################################
+# Astropy
+
+
+# =====================================
+# Cartesian3DVector
+
+
+@conversion_method(type_from=Cartesian3DVector, type_to=apyc.CartesianRepresentation)  # type: ignore[misc]
+def cart3_to_apycart3(obj: Cartesian3DVector, /) -> apyc.CartesianRepresentation:
+    """`vector.Cartesian3DVector` -> `astropy.CartesianRepresentation`."""
+    return apyc.CartesianRepresentation(
+        x=obj.x.as_type(apyu.Quantity),
+        y=obj.y.as_type(apyu.Quantity),
+        z=obj.z.as_type(apyu.Quantity),
+    )
+
+
+@conversion_method(type_from=apyc.CartesianRepresentation, type_to=Cartesian3DVector)  # type: ignore[misc]
+def apycart3_to_cart3(obj: apyc.CartesianRepresentation, /) -> Cartesian3DVector:
+    """`astropy.CartesianRepresentation` -> `vector.Cartesian3DVector`."""
+    return Cartesian3DVector(x=obj.x, y=obj.y, z=obj.z)
+
+
+# =====================================
+# SphericalVector
+
+
+@conversion_method(
+    type_from=SphericalVector,
+    type_to=apyc.PhysicsSphericalRepresentation,  # type: ignore[misc]
+)
+def sph_to_apysph(obj: SphericalVector, /) -> apyc.PhysicsSphericalRepresentation:
+    """`vector.SphericalVector` -> `astropy.PhysicsSphericalRepresentation`."""
+    return apyc.PhysicsSphericalRepresentation(
+        r=obj.r.as_type(apyu.Quantity),
+        phi=obj.phi.as_type(apyu.Quantity),
+        theta=obj.theta.as_type(apyu.Quantity),
+    )
+
+
+@conversion_method(
+    type_from=apyc.PhysicsSphericalRepresentation,
+    type_to=SphericalVector,  # type: ignore[misc]
+)
+def apysph_to_sph(obj: apyc.PhysicsSphericalRepresentation, /) -> SphericalVector:
+    """`astropy.PhysicsSphericalRepresentation` -> `vector.SphericalVector`."""
+    return SphericalVector(r=obj.r, phi=obj.phi, theta=obj.theta)
+
+
+# =====================================
+# CylindricalVector
+
+
+@conversion_method(type_from=CylindricalVector, type_to=apyc.CylindricalRepresentation)  # type: ignore[misc]
+def cyl_to_apycyl(obj: CylindricalVector, /) -> apyc.CylindricalRepresentation:
+    """`vector.CylindricalVector` -> `astropy.CylindricalRepresentation`."""
+    return apyc.CylindricalRepresentation(
+        rho=obj.rho.as_type(apyu.Quantity),
+        phi=obj.phi.as_type(apyu.Quantity),
+        z=obj.z.as_type(apyu.Quantity),
+    )
+
+
+@conversion_method(type_from=apyc.CylindricalRepresentation, type_to=CylindricalVector)  # type: ignore[misc]
+def apycyl_to_cyl(obj: apyc.CylindricalRepresentation, /) -> CylindricalVector:
+    """`astropy.CylindricalRepresentation` -> `vector.CylindricalVector`."""
+    return CylindricalVector(rho=obj.rho, phi=obj.phi, z=obj.z)
+
+
+# =====================================
+# CartesianDifferential3D
+
+
+@conversion_method(  # type: ignore[misc]
+    type_from=CartesianDifferential3D, type_to=apyc.CartesianDifferential
+)
+def diffcart3_to_apycart3(
+    obj: CartesianDifferential3D, /
+) -> apyc.CartesianDifferential:
+    """`vector.CartesianDifferential3D` -> `astropy.CartesianDifferential`."""
+    return apyc.CartesianDifferential(
+        d_x=obj.d_x.as_type(apyu.Quantity),
+        d_y=obj.d_y.as_type(apyu.Quantity),
+        d_z=obj.d_z.as_type(apyu.Quantity),
+    )
+
+
+@conversion_method(  # type: ignore[misc]
+    type_from=apyc.CartesianDifferential, type_to=CartesianDifferential3D
+)
+def apycart3_to_diffcart3(
+    obj: apyc.CartesianDifferential, /
+) -> CartesianDifferential3D:
+    """`astropy.CartesianDifferential` -> `vector.CartesianDifferential3D`."""
+    return CartesianDifferential3D(d_x=obj.d_x, d_y=obj.d_y, d_z=obj.d_z)
+
+
+# =====================================
+# SphericalDifferential
+
+
+@conversion_method(  # type: ignore[misc]
+    type_from=SphericalDifferential,
+    type_to=apyc.PhysicsSphericalDifferential,
+)
+def diffsph_to_apysph(
+    obj: SphericalDifferential, /
+) -> apyc.PhysicsSphericalDifferential:
+    """`vector.SphericalDifferential` -> `astropy.PhysicsSphericalDifferential`."""
+    return apyc.PhysicsSphericalDifferential(
+        d_r=obj.d_r.as_type(apyu.Quantity),
+        d_phi=obj.d_phi.as_type(apyu.Quantity),
+        d_theta=obj.d_theta.as_type(apyu.Quantity),
+    )
+
+
+@conversion_method(  # type: ignore[misc]
+    type_from=apyc.PhysicsSphericalDifferential,
+    type_to=SphericalDifferential,
+)
+def apysph_to_diffsph(
+    obj: apyc.PhysicsSphericalDifferential, /
+) -> SphericalDifferential:
+    """`astropy.PhysicsSphericalDifferential` -> `vector.SphericalDifferential`."""
+    return SphericalDifferential(d_r=obj.d_r, d_phi=obj.d_phi, d_theta=obj.d_theta)
+
+
+# =====================================
+# CylindricalDifferential
+
+
+@conversion_method(  # type: ignore[misc]
+    type_from=CylindricalDifferential, type_to=apyc.CylindricalDifferential
+)
+def diffcyl_to_apycyl(obj: CylindricalDifferential, /) -> apyc.CylindricalDifferential:
+    """`vector.CylindricalDifferential` -> `astropy.CylindricalDifferential`."""
+    return apyc.CylindricalDifferential(
+        d_rho=obj.d_rho.as_type(apyu.Quantity),
+        d_phi=obj.d_phi.as_type(apyu.Quantity),
+        d_z=obj.d_z.as_type(apyu.Quantity),
+    )
+
+
+@conversion_method(  # type: ignore[misc]
+    type_from=apyc.CylindricalDifferential, type_to=CylindricalDifferential
+)
+def apycyl_to_diffcyl(obj: apyc.CylindricalDifferential, /) -> CylindricalDifferential:
+    """`astropy.CylindricalDifferential` -> `vector.CylindricalDifferential`."""
+    return CylindricalDifferential(d_rho=obj.d_rho, d_phi=obj.d_phi, d_z=obj.d_z)
+
+
+#####################################################################
diff --git a/src/vector/_d3/transform.py b/src/vector/_d3/transform.py
index 638a2684..e099ef60 100644
--- a/src/vector/_d3/transform.py
+++ b/src/vector/_d3/transform.py
@@ -104,7 +104,7 @@ def represent_as(
     current: SphericalVector, target: type[CylindricalVector], /, **kwargs: Any
 ) -> CylindricalVector:
     """SphericalVector -> CylindricalVector."""
-    rho = current.r * xp.sin(current.theta)
+    rho = xp.abs(current.r * xp.sin(current.theta))
     phi = current.phi
     z = current.r * xp.cos(current.theta)
     return target(rho=rho, phi=phi, z=z)
diff --git a/src/vector/_transform.py b/src/vector/_transform.py
index 290df7f0..d8a9867b 100644
--- a/src/vector/_transform.py
+++ b/src/vector/_transform.py
@@ -62,11 +62,6 @@ def represent_as(
     # differential to construct the Jacobian.
     current_position = represent_as(position, current.vector_cls, **kwargs)
 
-    # # Need to ensure that the shape of the differential is the same as the
-    # # shape of the position. This can probably be relaxed, but for now it's
-    # # a requirement.
-    # current_position = eqx.error_if()
-
     # Takes the Jacobian through the representation transformation function.  This
     # returns a representation of the target type, where the value of each field the
     # corresponding row of the Jacobian. The value of the field is a Quantity with


Test patch:
diff --git a/tests/test_d3.py b/tests/test_d3.py
index c077eb6d..195a14ef 100644
--- a/tests/test_d3.py
+++ b/tests/test_d3.py
@@ -1,10 +1,13 @@
 """Test :mod:`vector._builtin`."""
 
 import array_api_jax_compat as xp
+import astropy.coordinates as apyc
 import astropy.units as u
 import jax.numpy as jnp
+import numpy as np
 import pytest
 from jax_quantity import Quantity
+from plum import convert
 
 from vector import (
     AbstractVector,
@@ -47,6 +50,11 @@ def vector(self) -> AbstractVector:
             z=Quantity([9, 10, 11, 12], u.kpc),
         )
 
+    @pytest.fixture(scope="class")
+    def apyvector(self, vector: AbstractVector):
+        """Return an Astropy vector."""
+        return convert(vector, apyc.CartesianRepresentation)
+
     # ==========================================================================
     # represent_as
 
@@ -108,13 +116,17 @@ def test_cartesian3d_to_cartesian3d(self, vector):
         newvec = represent_as(vector, Cartesian3DVector)
         assert newvec is vector
 
+    def test_cartesian3d_to_cartesian3d_astropy(self, vector, apyvector):
+        """Test Astropy equivalence."""
+        newvec = vector.represent_as(Cartesian3DVector)
+
+        assert np.allclose(newvec.x.as_type(u.Quantity), apyvector.x)
+        assert np.allclose(newvec.y.as_type(u.Quantity), apyvector.y)
+        assert np.allclose(newvec.z.as_type(u.Quantity), apyvector.z)
+
     def test_cartesian3d_to_spherical(self, vector):
         """Test ``vector.represent_as(SphericalVector)``."""
-        spherical = vector.represent_as(
-            SphericalVector,
-            phi=Quantity([0, 1, 2, 3], u.rad),
-            theta=Quantity([4, 5, 6, 7], u.rad),
-        )
+        spherical = vector.represent_as(SphericalVector)
 
         assert isinstance(spherical, SphericalVector)
         assert array_equal(
@@ -128,12 +140,18 @@ def test_cartesian3d_to_spherical(self, vector):
             xp.asarray([0.51546645, 0.5639427, 0.6055685, 0.64052236]),
         )
 
+    def test_cartesian3d_to_spherical_astropy(self, vector, apyvector):
+        """Test Astropy equivalence."""
+        sph = vector.represent_as(SphericalVector)
+
+        apysph = apyvector.represent_as(apyc.PhysicsSphericalRepresentation)
+        assert np.allclose(sph.r.as_type(u.Quantity), apysph.r)
+        assert np.allclose(sph.theta.as_type(u.Quantity), apysph.theta)
+        assert np.allclose(sph.phi.as_type(u.Quantity), apysph.phi)
+
     def test_cartesian3d_to_cylindrical(self, vector):
         """Test ``vector.represent_as(CylindricalVector)``."""
-        cylindrical = vector.represent_as(
-            CylindricalVector,
-            phi=Quantity([0, 1, 2, 3], u.rad),
-        )
+        cylindrical = vector.represent_as(CylindricalVector)
 
         assert isinstance(cylindrical, CylindricalVector)
         assert array_equal(cylindrical.rho, hypot(vector.x, vector.y))
@@ -143,21 +161,33 @@ def test_cartesian3d_to_cylindrical(self, vector):
         )
         assert array_equal(cylindrical.z, Quantity([9.0, 10, 11, 12], u.kpc))
 
+    def test_cartesian3d_to_cylindrical_astropy(self, vector, apyvector):
+        """Test Astropy equivalence."""
+        cyl = vector.represent_as(CylindricalVector)
+
+        apycyl = apyvector.represent_as(apyc.CylindricalRepresentation)
+        assert np.allclose(cyl.rho.as_type(u.Quantity), apycyl.rho)
+        assert np.allclose(cyl.z.as_type(u.Quantity), apycyl.z)
+        assert np.allclose(cyl.phi.as_type(u.Quantity), apycyl.phi)
+
 
 class TestSphericalVector:
     """Test :class:`vector.SphericalVector`."""
 
     @pytest.fixture(scope="class")
-    def vector(self) -> AbstractVector:
+    def vector(self) -> SphericalVector:
         """Return a vector."""
-        from vector import SphericalVector
-
         return SphericalVector(
             r=Quantity([1, 2, 3, 4], u.kpc),
-            phi=Quantity([0, 1, 2, 3], u.rad),
-            theta=Quantity([4, 5, 6, 7], u.rad),
+            phi=Quantity([0, 65, 135, 270], u.deg),
+            theta=Quantity([0, 36, 142, 180], u.deg),
         )
 
+    @pytest.fixture(scope="class")
+    def apyvector(self, vector: AbstractVector):
+        """Return an Astropy vector."""
+        return convert(vector, apyc.PhysicsSphericalRepresentation)
+
     # ==========================================================================
     # represent_as
 
@@ -169,7 +199,7 @@ def test_spherical_to_cartesian1d(self, vector):
         assert isinstance(cart1d, Cartesian1DVector)
         assert jnp.allclose(
             cart1d.x.to_value(u.kpc),
-            xp.asarray([-0.7568025, -1.036218, 0.34883362, -2.6016471]),
+            xp.asarray([0, 0.49681753, -1.3060151, -4.1700245e-15]),
         )
 
     @pytest.mark.filterwarnings("ignore:Irreversible dimension change")
@@ -187,10 +217,11 @@ def test_spherical_to_cartesian2d(self, vector):
 
         assert isinstance(cart2d, Cartesian2DVector)
         assert array_equal(
-            cart2d.x, Quantity([-0.7568025, -1.0362179, 0.34883362, -2.6016471], u.kpc)
+            cart2d.x,
+            Quantity([0, 0.49681753, -1.3060151, -4.1700245e-15], u.kpc),
         )
         assert array_equal(
-            cart2d.y, Quantity([-0.0, -1.6138139, -0.7622153, 0.3708558], u.kpc)
+            cart2d.y, Quantity([0.0, 1.0654287, 1.3060151, 3.4969111e-07], u.kpc)
         )
 
     @pytest.mark.filterwarnings("ignore:Irreversible dimension change")
@@ -200,9 +231,10 @@ def test_spherical_to_polar(self, vector):
 
         assert isinstance(polar, PolarVector)
         assert array_equal(
-            polar.r, Quantity([-0.7568025, -1.9178486, -0.83824646, 2.6279464], u.kpc)
+            polar.r,
+            Quantity([0.0, 1.1755705, 1.8469844, -3.4969111e-07], u.kpc),
         )
-        assert array_equal(polar.phi, Quantity([0.0, 1, 2, 3], u.rad))
+        assert array_equal(polar.phi, Quantity([0.0, 65.0, 135.0, 270.0], u.deg))
 
     # @pytest.mark.filterwarnings("ignore:Irreversible dimension change")
     # def test_spherical_to_lnpolar(self, vector):
@@ -214,23 +246,27 @@ def test_spherical_to_polar(self, vector):
     #     """Test ``vector.represent_as(Log10PolarVector)``."""
     #     assert False
 
-    @pytest.mark.filterwarnings("ignore:Irreversible dimension change")
     def test_spherical_to_cartesian3d(self, vector):
         """Test ``vector.represent_as(Cartesian3DVector)``."""
-        cart3d = vector.represent_as(
-            Cartesian3DVector, z=Quantity([9, 10, 11, 12], u.m)
-        )
+        cart3d = vector.represent_as(Cartesian3DVector)
 
         assert isinstance(cart3d, Cartesian3DVector)
         assert array_equal(
-            cart3d.x, Quantity([-0.7568025, -1.0362179, 0.34883362, -2.6016471], u.kpc)
-        )
-        assert array_equal(
-            cart3d.y, Quantity([-0.0, -1.6138139, -0.7622153, 0.3708558], u.kpc)
+            cart3d.x, Quantity([0, 0.49681753, -1.3060151, -4.1700245e-15], u.kpc)
         )
         assert array_equal(
-            cart3d.z, Quantity([-0.6536436, 0.5673244, 2.8805108, 3.015609], u.kpc)
+            cart3d.y, Quantity([0.0, 1.0654287, 1.3060151, 3.4969111e-07], u.kpc)
         )
+        assert array_equal(cart3d.z, Quantity([1.0, 1.618034, -2.3640323, -4.0], u.kpc))
+
+    def test_spherical_to_cartesian3d_astropy(self, vector, apyvector):
+        """Test Astropy equivalence."""
+        cart3d = vector.represent_as(Cartesian3DVector)
+
+        apycart3 = apyvector.represent_as(apyc.CartesianRepresentation)
+        assert np.allclose(cart3d.x.as_type(u.Quantity), apycart3.x)
+        assert np.allclose(cart3d.y.as_type(u.Quantity), apycart3.y)
+        assert np.allclose(cart3d.z.as_type(u.Quantity), apycart3.z)
 
     def test_spherical_to_spherical(self, vector):
         """Test ``vector.represent_as(SphericalVector)``."""
@@ -242,6 +278,15 @@ def test_spherical_to_spherical(self, vector):
         newvec = represent_as(vector, SphericalVector)
         assert newvec is vector
 
+    def test_spherical_to_spherical_astropy(self, vector, apyvector):
+        """Test Astropy equivalence."""
+        sph = vector.represent_as(SphericalVector)
+
+        apysph = apyvector.represent_as(apyc.PhysicsSphericalRepresentation)
+        assert np.allclose(sph.r.as_type(u.Quantity), apysph.r)
+        assert np.allclose(sph.theta.as_type(u.Quantity), apysph.theta)
+        assert np.allclose(sph.phi.as_type(u.Quantity), apysph.phi)
+
     def test_spherical_to_cylindrical(self, vector):
         """Test ``vector.represent_as(CylindricalVector)``."""
         cylindrical = vector.represent_as(
@@ -251,13 +296,24 @@ def test_spherical_to_cylindrical(self, vector):
         assert isinstance(cylindrical, CylindricalVector)
         assert array_equal(
             cylindrical.rho,
-            Quantity([-0.7568025, -1.9178486, -0.83824646, 2.6279464], u.kpc),
+            Quantity([0.0, 1.1755705, 1.8469844, 3.4969111e-07], u.kpc),
         )
-        assert array_equal(cylindrical.phi, Quantity([0, 1, 2, 3], u.rad))
+        assert array_equal(cylindrical.phi, Quantity([0.0, 65.0, 135.0, 270.0], u.deg))
         assert array_equal(
-            cylindrical.z, Quantity([-0.6536436, 0.5673244, 2.8805108, 3.015609], u.kpc)
+            cylindrical.z, Quantity([1.0, 1.618034, -2.3640323, -4.0], u.kpc)
         )
 
+    def test_spherical_to_cylindrical_astropy(self, vector, apyvector):
+        """Test ``vector.represent_as(CylindricalVector)``."""
+        cyl = vector.represent_as(CylindricalVector, z=Quantity([9, 10, 11, 12], u.m))
+
+        apycyl = apyvector.represent_as(apyc.CylindricalRepresentation)
+        assert np.allclose(cyl.rho.as_type(u.Quantity), apycyl.rho)
+        assert np.allclose(cyl.z.as_type(u.Quantity), apycyl.z)
+
+        with pytest.raises(AssertionError):  # TODO: Fix this
+            assert np.allclose(cyl.phi.as_type(u.Quantity), apycyl.phi)
+
 
 class TestCylindricalVector:
     """Test :class:`vector.CylindricalVector`."""
@@ -273,6 +329,11 @@ def vector(self) -> AbstractVector:
             z=Quantity([9, 10, 11, 12], u.m),
         )
 
+    @pytest.fixture(scope="class")
+    def apyvector(self, vector: AbstractVector):
+        """Return an Astropy vector."""
+        return convert(vector, apyc.CylindricalRepresentation)
+
     # ==========================================================================
     # represent_as
 
@@ -339,6 +400,15 @@ def test_cylindrical_to_cartesian3d(self, vector):
         )
         assert array_equal(cart3d.z, vector.z)
 
+    def test_cylindrical_to_cartesian3d_astropy(self, vector, apyvector):
+        """Test Astropy equivalence."""
+        cart3d = vector.represent_as(Cartesian3DVector)
+
+        apycart3 = apyvector.represent_as(apyc.CartesianRepresentation)
+        assert np.allclose(cart3d.x.as_type(u.Quantity), apycart3.x)
+        assert np.allclose(cart3d.y.as_type(u.Quantity), apycart3.y)
+        assert np.allclose(cart3d.z.as_type(u.Quantity), apycart3.z)
+
     def test_cylindrical_to_spherical(self, vector):
         """Test ``vector.represent_as(SphericalVector)``."""
         spherical = vector.represent_as(SphericalVector)
@@ -348,6 +418,14 @@ def test_cylindrical_to_spherical(self, vector):
         assert array_equal(spherical.phi, Quantity([0, 1, 2, 3], u.rad))
         assert array_equal(spherical.theta, Quantity(xp.full(4, xp.pi / 2), u.rad))
 
+    def test_cylindrical_to_spherical_astropy(self, vector, apyvector):
+        """Test Astropy equivalence."""
+        sph = vector.represent_as(SphericalVector)
+        apysph = apyvector.represent_as(apyc.PhysicsSphericalRepresentation)
+        assert np.allclose(sph.r.as_type(u.Quantity), apysph.r)
+        assert np.allclose(sph.theta.as_type(u.Quantity), apysph.theta)
+        assert np.allclose(sph.phi.as_type(u.Quantity), apysph.phi)
+
     def test_cylindrical_to_cylindrical(self, vector):
         """Test ``vector.represent_as(CylindricalVector)``."""
         # Jit can copy
@@ -358,6 +436,15 @@ def test_cylindrical_to_cylindrical(self, vector):
         newvec = represent_as(vector, CylindricalVector)
         assert newvec is vector
 
+    def test_cylindrical_to_cylindrical_astropy(self, vector, apyvector):
+        """Test Astropy equivalence."""
+        cyl = vector.represent_as(CylindricalVector)
+
+        apycyl = apyvector.represent_as(apyc.CylindricalRepresentation)
+        assert np.allclose(cyl.rho.as_type(u.Quantity), apycyl.rho)
+        assert np.allclose(cyl.z.as_type(u.Quantity), apycyl.z)
+        assert np.allclose(cyl.phi.as_type(u.Quantity), apycyl.phi)
+
 
 class Abstract3DVectorDifferentialTest(AbstractVectorDifferentialTest):
     """Test :class:`vector.Abstract2DVectorDifferential`."""
@@ -384,6 +471,16 @@ def vector(self) -> Cartesian3DVector:
             z=Quantity([9, 10, 11, 12], u.kpc),
         )
 
+    @pytest.fixture(scope="class")
+    def apydifntl(self, difntl: CartesianDifferential3D):
+        """Return an Astropy differential."""
+        return convert(difntl, apyc.CartesianDifferential)
+
+    @pytest.fixture(scope="class")
+    def apyvector(self, vector: Cartesian3DVector):
+        """Return an Astropy vector."""
+        return convert(vector, apyc.CartesianRepresentation)
+
     # ==========================================================================
 
     @pytest.mark.xfail(reason="Not implemented")
@@ -434,6 +531,17 @@ def test_cartesian3d_to_cartesian3d(self, difntl, vector):
         newvec = represent_as(difntl, CartesianDifferential3D, vector)
         assert newvec is difntl
 
+    def test_cartesian3d_to_cartesian3d_astropy(
+        self, difntl, vector, apydifntl, apyvector
+    ):
+        """Test Astropy equivalence."""
+        cart3 = difntl.represent_as(CartesianDifferential3D, vector)
+
+        apycart3 = apydifntl.represent_as(apyc.CartesianDifferential, apyvector)
+        assert np.allclose(cart3.d_x.as_type(u.Quantity), apycart3.d_x)
+        assert np.allclose(cart3.d_y.as_type(u.Quantity), apycart3.d_y)
+        assert np.allclose(cart3.d_z.as_type(u.Quantity), apycart3.d_z)
+
     def test_cartesian3d_to_spherical(self, difntl, vector):
         """Test ``vector.represent_as(SphericalDifferential)``."""
         spherical = difntl.represent_as(SphericalDifferential, vector)
@@ -449,6 +557,27 @@ def test_cartesian3d_to_spherical(self, difntl, vector):
             xp.asarray([0.03221978, -0.05186598, -0.01964621, -0.01886036]),
         )
 
+    def test_cartesian3d_to_spherical_astropy(
+        self, difntl, vector, apydifntl, apyvector
+    ):
+        """Test Astropy equivalence."""
+        sph = difntl.represent_as(SphericalDifferential, vector)
+
+        apysph = apydifntl.represent_as(apyc.PhysicsSphericalDifferential, apyvector)
+        assert np.allclose(sph.d_r.as_type(u.Quantity), apysph.d_r)
+        with pytest.raises(AssertionError):  # TODO: fixme
+            assert np.allclose(
+                sph.d_theta.as_type(u.Quantity).to(u.mas / u.Myr),
+                apysph.d_theta.to(u.mas / u.Myr),
+                atol=1e-9,
+            )
+        with pytest.raises(AssertionError):  # TODO: fixme
+            assert np.allclose(
+                sph.d_phi.as_type(u.Quantity).to(u.mas / u.Myr),
+                apysph.d_phi.to(u.mas / u.Myr),
+                atol=1e-7,
+            )
+
     def test_cartesian3d_to_cylindrical(self, difntl, vector):
         """Test ``vector.represent_as(CylindricalDifferential)``."""
         cylindrical = difntl.represent_as(CylindricalDifferential, vector)
@@ -463,6 +592,17 @@ def test_cartesian3d_to_cylindrical(self, difntl, vector):
         )
         assert array_equal(cylindrical.d_z, Quantity([9, 10, 11, 12], u.km / u.s))
 
+    def test_cartesian3d_to_spherical_astropy(
+        self, difntl, vector, apydifntl, apyvector
+    ):
+        """Test Astropy equivalence."""
+        cyl = difntl.represent_as(CylindricalDifferential, vector)
+        apycyl = apydifntl.represent_as(apyc.CylindricalDifferential, apyvector)
+        assert np.allclose(cyl.d_rho.as_type(u.Quantity), apycyl.d_rho)
+        assert np.allclose(cyl.d_z.as_type(u.Quantity), apycyl.d_z)
+        with pytest.raises(AssertionError):  # TODO: fixme
+            assert np.allclose(cyl.d_phi.as_type(u.Quantity), apycyl.d_phi)
+
 
 class TestSphericalDifferential(Abstract3DVectorDifferentialTest):
     """Test :class:`vector.SphericalDifferential`."""
@@ -481,10 +621,22 @@ def vector(self) -> SphericalVector:
         """Return a vector."""
         return SphericalVector(
             r=Quantity([1, 2, 3, 4], u.kpc),
-            phi=Quantity([0, 1, 2, 3], u.rad),
-            theta=Quantity([4, 5, 6, 7], u.rad),
+            phi=Quantity([0, 42, 160, 270], u.deg),
+            theta=Quantity([3, 63, 90, 179.5], u.deg),
         )
 
+    @pytest.fixture(scope="class")
+    def apydifntl(
+        self, difntl: SphericalDifferential
+    ) -> apyc.PhysicsSphericalDifferential:
+        """Return an Astropy differential."""
+        return convert(difntl, apyc.PhysicsSphericalDifferential)
+
+    @pytest.fixture(scope="class")
+    def apyvector(self, vector: SphericalVector) -> apyc.PhysicsSphericalRepresentation:
+        """Return an Astropy vector."""
+        return convert(vector, apyc.PhysicsSphericalRepresentation)
+
     # ==========================================================================
 
     @pytest.mark.xfail(reason="Not implemented")
@@ -532,17 +684,28 @@ def test_spherical_to_cartesian3d(self, difntl, vector):
         assert isinstance(cart3d, CartesianDifferential3D)
         assert array_equal(
             cart3d.d_x,
-            Quantity([-28.644005, 59.39601, -36.865578, -186.49405], u.km / u.s),
+            Quantity([42.658096, -0.6040496, -36.867138, 1.323785], u.km / u.s),
         )
         assert array_equal(
             cart3d.d_y,
-            Quantity([-17.938, -8.456386, 147.39401, -74.084984], u.km / u.s),
+            Quantity([1.2404853, 67.66016, -92.52022, 227.499], u.km / u.s),
         )
         assert array_equal(
             cart3d.d_z,
-            Quantity([31.634754, 91.48237, 46.59102, -146.47682], u.km / u.s),
+            Quantity([-1.2342439, -83.56782, -156.43553, -5.985529], u.km / u.s),
         )
 
+    def test_spherical_to_cartesian3d_astropy(
+        self, difntl, vector, apydifntl, apyvector
+    ):
+        """Test Astropy equivalence."""
+        cart3d = difntl.represent_as(CartesianDifferential3D, vector)
+
+        apycart3 = apydifntl.represent_as(apyc.CartesianDifferential, apyvector)
+        assert np.allclose(cart3d.d_x.as_type(u.Quantity), apycart3.d_x)
+        assert np.allclose(cart3d.d_y.as_type(u.Quantity), apycart3.d_y)
+        assert np.allclose(cart3d.d_z.as_type(u.Quantity), apycart3.d_z)
+
     def test_spherical_to_spherical(self, difntl, vector):
         """Test ``vector.represent_as(SphericalDifferential)``."""
         # Jit can copy
@@ -553,6 +716,14 @@ def test_spherical_to_spherical(self, difntl, vector):
         newvec = represent_as(difntl, SphericalDifferential, vector)
         assert newvec is difntl
 
+    def test_spherical_to_spherical_astropy(self, difntl, vector, apydifntl, apyvector):
+        """Test Astropy equivalence."""
+        sph = difntl.represent_as(SphericalDifferential, vector)
+        apysph = apydifntl.represent_as(apyc.PhysicsSphericalDifferential, apyvector)
+        assert np.allclose(sph.d_r.as_type(u.Quantity), apysph.d_r)
+        assert np.allclose(sph.d_theta.as_type(u.Quantity), apysph.d_theta)
+        assert np.allclose(sph.d_phi.as_type(u.Quantity), apysph.d_phi)
+
     def test_spherical_to_cylindrical(self, difntl, vector):
         """Test ``vector.represent_as(CylindricalDifferential)``."""
         cylindrical = difntl.represent_as(CylindricalDifferential, vector)
@@ -560,14 +731,24 @@ def test_spherical_to_cylindrical(self, difntl, vector):
         assert isinstance(cylindrical, CylindricalDifferential)
         assert array_equal(
             cylindrical.d_rho,
-            Quantity([-28.644005, 24.975996, 149.3665, 174.17282], u.km / u.s),
+            Quantity([42.658096, 44.824585, 2.999993, -227.499], u.km / u.s),
         )
         assert array_equal(cylindrical.d_phi, Quantity([5, 6, 7, 8], u.mas / u.yr))
         assert array_equal(
             cylindrical.d_z,
-            Quantity([31.634754, 91.48237, 46.59102, -146.47682], u.km / u.s),
+            Quantity([-1.2342439, -83.56782, -156.43553, -5.985529], u.km / u.s),
         )
 
+    def test_spherical_to_cylindrical_astropy(
+        self, difntl, vector, apydifntl, apyvector
+    ):
+        """Test Astropy equivalence."""
+        cyl = difntl.represent_as(CylindricalDifferential, vector)
+        apycyl = apydifntl.represent_as(apyc.CylindricalDifferential, apyvector)
+        assert np.allclose(cyl.d_rho.as_type(u.Quantity), apycyl.d_rho)
+        assert np.allclose(cyl.d_z.as_type(u.Quantity), apycyl.d_z)
+        assert np.allclose(cyl.d_phi.as_type(u.Quantity), apycyl.d_phi)
+
 
 class TestCylindricalDifferential(Abstract3DVectorDifferentialTest):
     """Test :class:`vector.CylindricalDifferential`."""
@@ -590,6 +771,16 @@ def vector(self) -> CylindricalVector:
             z=Quantity([9, 10, 11, 12], u.kpc),
         )
 
+    @pytest.fixture(scope="class")
+    def apydifntl(self, difntl: CylindricalDifferential):
+        """Return an Astropy differential."""
+        return convert(difntl, apyc.CylindricalDifferential)
+
+    @pytest.fixture(scope="class")
+    def apyvector(self, vector: CylindricalVector) -> apyc.CylindricalRepresentation:
+        """Return an Astropy vector."""
+        return convert(vector, apyc.CylindricalRepresentation)
+
     # ==========================================================================
 
     @pytest.mark.xfail(reason="Not implemented")
@@ -630,7 +821,7 @@ def test_cylindrical_to_polar(self, difntl, vector):
         assert array_equal(polar.d_r, Quantity([1, 2, 3, 4], u.km / u.s))
         assert array_equal(polar.d_phi, Quantity([5, 6, 7, 8], u.mas / u.yr))
 
-    def test_cylindrical_to_cartesian3d(self, difntl, vector):
+    def test_cylindrical_to_cartesian3d(self, difntl, vector, apydifntl, apyvector):
         """Test ``vector.represent_as(Cartesian3DVector)``."""
         cart3d = difntl.represent_as(CartesianDifferential3D, vector)
 
@@ -644,6 +835,11 @@ def test_cylindrical_to_cartesian3d(self, difntl, vector):
         )
         assert array_equal(cart3d.d_z, Quantity([9, 10, 11, 12], u.km / u.s))
 
+        apycart3 = apydifntl.represent_as(apyc.CartesianDifferential, apyvector)
+        assert np.allclose(cart3d.d_x.as_type(u.Quantity), apycart3.d_x)
+        assert np.allclose(cart3d.d_y.as_type(u.Quantity), apycart3.d_y)
+        assert np.allclose(cart3d.d_z.as_type(u.Quantity), apycart3.d_z)
+
     def test_cylindrical_to_spherical(self, difntl, vector):
         """Test ``vector.represent_as(SphericalDifferential)``."""
         spherical = difntl.represent_as(SphericalDifferential, vector)
@@ -659,6 +855,17 @@ def test_cylindrical_to_spherical(self, difntl, vector):
             xp.asarray([-0.08428223, 0.07544143, -0.0326127, -0.01571696]),
         )
 
+    def test_cylindrical_to_spherical_astropy(
+        self, difntl, vector, apydifntl, apyvector
+    ):
+        """Test Astropy equivalence."""
+        sph = difntl.represent_as(SphericalDifferential, vector)
+        apysph = apydifntl.represent_as(apyc.PhysicsSphericalDifferential, apyvector)
+        assert np.allclose(sph.d_r.as_type(u.Quantity), apysph.d_r)
+        with pytest.raises(AssertionError):
+            assert np.allclose(sph.d_theta.as_type(u.Quantity), apysph.d_theta)
+        assert np.allclose(sph.d_phi.as_type(u.Quantity), apysph.d_phi)
+
     def test_cylindrical_to_cylindrical(self, difntl, vector):
         """Test ``vector.represent_as(CylindricalDifferential)``."""
         # Jit can copy
@@ -668,3 +875,11 @@ def test_cylindrical_to_cylindrical(self, difntl, vector):
         # The normal `represent_as` method should return the same object
         newvec = represent_as(difntl, CylindricalDifferential, vector)
         assert newvec is difntl
+
+    def test_cylindrical_to_cylindrical(self, difntl, vector, apydifntl, apyvector):
+        """Test Astropy equivalence."""
+        cyl = difntl.represent_as(CylindricalDifferential, vector)
+        apycyl = apydifntl.represent_as(apyc.CylindricalDifferential, apyvector)
+        assert np.allclose(cyl.d_rho.as_type(u.Quantity), apycyl.d_rho)
+        assert np.allclose(cyl.d_z.as_type(u.Quantity), apycyl.d_z)
+        assert np.allclose(cyl.d_phi.as_type(u.Quantity), apycyl.d_phi)