Repository: scikit-hep/vector

Issue:
Allow momentum type coordinates in `to_Vector*D` methods
If so, then a user might justifiably expect to pass `pz` instead or `z`, but only in the momentum case. Just something to consider, at this point.

When this gets extended to `to_Vector4D`, the time component has a lot of momentum synonyms. The geometric names for the two possible temporal coordinates are `t` and `tau`, but the momentum names are `energy` and `mass`, `E` and `M`, and (I think) `e` and `m`. There was a discussion once about including the lowercase as well as the uppercase.

_Originally posted by @jpivarski in https://github.com/scikit-hep/vector/pull/319#discussion_r1115886750_
            

Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/scikit-hep/vector/pull/423?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-hep) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`ea3c018`)](https://app.codecov.io/gh/scikit-hep/vector/commit/ea3c0187c672a5604d6ea4292b523f3bc60a872e?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-hep) 82.70% compared to head [(`e16643d`)](https://app.codecov.io/gh/scikit-hep/vector/pull/423?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-hep) 82.89%.


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #423      +/-   ##
==========================================
+ Coverage   82.70%   82.89%   +0.19%     
==========================================
  Files          96       96              
  Lines       11453    11453              
==========================================
+ Hits         9472     9494      +22     
+ Misses       1981     1959      -22     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/scikit-hep/vector/pull/423?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-hep).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-hep).


Patch:
diff --git a/.gitignore b/.gitignore
index 1c09a8d..28e662a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -142,3 +142,6 @@ dmypy.json
 /node_modules
 /package.json
 /yarn.lock
+
+# MacOS
+.DS_Store
diff --git a/src/vector/_methods.py b/src/vector/_methods.py
index d8febb3..b60123e 100644
--- a/src/vector/_methods.py
+++ b/src/vector/_methods.py
@@ -184,7 +184,9 @@ class VectorProtocol:
         Projects this vector/these vectors onto azimuthal and longitudinal
         coordinates only.
 
-        If 2D, a $z$ component of $0$ is imputed.
+        If 2D, a default $z$ component of $0$ is imputed.
+
+        The longitudinal coordinate can be passed as a named argument.
         """
         raise AssertionError
 
@@ -193,9 +195,12 @@ class VectorProtocol:
         Projects this vector/these vectors onto azimuthal, longitudinal,
         and temporal coordinates.
 
-        If 2D or 3D, a $t$ component of $0$ is imputed.
+        If 3D, a default $t$ component of $0$ is imputed.
+
+        If 2D, a $z$ component of $0$ is imputed along with a default
+        $t$ component of $0$.
 
-        If 2D, a $z$ component of $0$ is imputed.
+        The longitudinal and temporal coordinates can be passed as named arguments.
         """
         raise AssertionError
 
@@ -3163,6 +3168,7 @@ class Vector2D(Vector, VectorProtocolPlanar):
         self,
         *,
         z: float | None = None,
+        pz: float | None = None,
         theta: float | None = None,
         eta: float | None = None,
     ) -> VectorProtocolSpatial:
@@ -3170,8 +3176,7 @@ class Vector2D(Vector, VectorProtocolPlanar):
         Converts a 2D vector to 3D vector.
 
         The scalar longitudinal coordinate is broadcasted for NumPy and Awkward
-        vectors. Only a single longitudinal coordinate should be provided. Generic
-        coordinate counterparts should be provided for the momentum coordinates.
+        vectors. Only a single longitudinal coordinate should be provided.
 
         Examples:
             >>> import vector
@@ -3179,18 +3184,18 @@ class Vector2D(Vector, VectorProtocolPlanar):
             >>> vec.to_Vector3D(z=1)
             VectorObject3D(x=1, y=2, z=1)
             >>> vec = vector.MomentumObject2D(px=1, py=2)
-            >>> vec.to_Vector3D(z=4)
+            >>> vec.to_Vector3D(pz=4)
             MomentumObject3D(px=1, py=2, pz=4)
         """
-        if sum(x is not None for x in (z, theta, eta)) > 1:
+        if sum(x is not None for x in (z, pz, theta, eta)) > 1:
             raise TypeError(
-                "At most one longitudinal coordinate (`z`, `theta`, or `eta`) may be assigned (non-None)"
+                "At most one longitudinal coordinate (`z`/`pz`, `theta`, or `eta`) may be assigned (non-None)"
             )
 
         l_value = 0.0
         l_type: type[Longitudinal] = LongitudinalZ
-        if z is not None:
-            l_value = z
+        if any(coord is not None for coord in (z, pz)):
+            l_value = next(coord for coord in (z, pz) if coord is not None)
         elif eta is not None:
             l_value = eta
             l_type = LongitudinalEta
@@ -3209,18 +3214,24 @@ class Vector2D(Vector, VectorProtocolPlanar):
         self,
         *,
         z: float | None = None,
+        pz: float | None = None,
         theta: float | None = None,
         eta: float | None = None,
         t: float | None = None,
+        e: float | None = None,
+        E: float | None = None,
+        energy: float | None = None,
         tau: float | None = None,
+        m: float | None = None,
+        M: float | None = None,
+        mass: float | None = None,
     ) -> VectorProtocolLorentz:
         """
         Converts a 2D vector to 4D vector.
 
         The scalar longitudinal and temporal coordinates are broadcasted for NumPy and
         Awkward vectors. Only a single longitudinal and temporal coordinate should be
-        provided. Generic coordinate counterparts should be provided for the momentum
-        coordinates.
+        provided.
 
         Examples:
             >>> import vector
@@ -3228,30 +3239,30 @@ class Vector2D(Vector, VectorProtocolPlanar):
             >>> vec.to_Vector4D(z=3, t=4)
             VectorObject4D(x=1, y=2, z=3, t=4)
             >>> vec = vector.MomentumObject2D(px=1, py=2)
-            >>> vec.to_Vector4D(z=4, t=4)
+            >>> vec.to_Vector4D(pz=4, energy=4)
             MomentumObject4D(px=1, py=2, pz=4, E=4)
         """
-        if sum(x is not None for x in (z, theta, eta)) > 1:
+        if sum(x is not None for x in (z, pz, theta, eta)) > 1:
             raise TypeError(
-                "At most one longitudinal coordinate (`z`, `theta`, or `eta`) may be assigned (non-None)"
+                "At most one longitudinal coordinate (`z`/`pz`, `theta`, or `eta`) may be assigned (non-None)"
             )
-        elif sum(x is not None for x in (t, tau)) > 1:
+        elif sum(x is not None for x in (t, tau, m, M, mass, e, E, energy)) > 1:
             raise TypeError(
-                "At most one longitudinal coordinate (`t`, `tau`) may be assigned (non-None)"
+                "At most one longitudinal coordinate (`t`/`e`/`E`/`energy`, `tau`/`m`/`M`/`mass`) may be assigned (non-None)"
             )
 
         t_value = 0.0
         t_type: type[Temporal] = TemporalT
-        if t is not None:
-            t_value = t
-        elif tau is not None:
-            t_value = tau
+        if any(coord is not None for coord in (tau, m, M, mass)):
             t_type = TemporalTau
+            t_value = next(coord for coord in (tau, m, M, mass) if coord is not None)
+        elif any(coord is not None for coord in (t, e, E, energy)):
+            t_value = next(coord for coord in (t, e, E, energy) if coord is not None)
 
         l_value = 0.0
         l_type: type[Longitudinal] = LongitudinalZ
-        if z is not None:
-            l_value = z
+        if any(coord is not None for coord in (z, pz)):
+            l_value = next(coord for coord in (z, pz) if coord is not None)
         elif eta is not None:
             l_value = eta
             l_type = LongitudinalEta
@@ -3283,14 +3294,19 @@ class Vector3D(Vector, VectorProtocolSpatial):
         self,
         *,
         t: float | None = None,
+        e: float | None = None,
+        E: float | None = None,
+        energy: float | None = None,
         tau: float | None = None,
+        m: float | None = None,
+        M: float | None = None,
+        mass: float | None = None,
     ) -> VectorProtocolLorentz:
         """
         Converts a 3D vector to 4D vector.
 
         The scalar temporal coordinate are broadcasted for NumPy and Awkward vectors.
-        Only a single temporal coordinate should be provided. Generic coordinate
-        counterparts should be provided for the momentum coordinates.
+        Only a single temporal coordinate should be provided.
 
         Examples:
             >>> import vector
@@ -3298,21 +3314,21 @@ class Vector3D(Vector, VectorProtocolSpatial):
             >>> vec.to_Vector4D(t=4)
             VectorObject4D(x=1, y=2, z=3, t=4)
             >>> vec = vector.MomentumObject3D(px=1, py=2, pz=3)
-            >>> vec.to_Vector4D(tau=4)
+            >>> vec.to_Vector4D(M=4)
             MomentumObject4D(px=1, py=2, pz=3, mass=4)
         """
-        if sum(x is not None for x in (t, tau)) > 1:
+        if sum(x is not None for x in (t, tau, m, M, mass, e, E, energy)) > 1:
             raise TypeError(
-                "At most one longitudinal coordinate (`t`, `tau`) may be assigned (non-None)"
+                "At most one longitudinal coordinate (`t`/`e`/`E`/`energy`, `tau`/`m`/`M`/`mass`) may be assigned (non-None)"
             )
 
         t_value = 0.0
         t_type: type[Temporal] = TemporalT
-        if t is not None:
-            t_value = t
-        elif tau is not None:
-            t_value = tau
+        if any(coord is not None for coord in (tau, m, M, mass)):
             t_type = TemporalTau
+            t_value = next(coord for coord in (tau, m, M, mass) if coord is not None)
+        elif any(coord is not None for coord in (t, e, E, energy)):
+            t_value = next(coord for coord in (t, e, E, energy) if coord is not None)
 
         return self._wrap_result(
             type(self),


Test patch:
diff --git a/tests/backends/test_awkward.py b/tests/backends/test_awkward.py
index 1c1ca93..c6b7f40 100644
--- a/tests/backends/test_awkward.py
+++ b/tests/backends/test_awkward.py
@@ -70,6 +70,36 @@ def test_dimension_conversion():
     assert ak.all(vec.to_Vector4D(t=1).y == vec.y)
     assert ak.all(vec.to_Vector4D(t=1).z == vec.z)
 
+    # check if momentum coords work
+    vec = vector.Array(
+        [
+            [{"px": 1, "py": 1.1}, {"px": 2, "py": 2.1}],
+            [],
+        ]
+    )
+    assert ak.all(vec.to_Vector3D(pz=1).pz == 1)
+
+    assert ak.all(vec.to_Vector4D(pz=1, m=1).pz == 1)
+    assert ak.all(vec.to_Vector4D(pz=1, m=1).m == 1)
+    assert ak.all(vec.to_Vector4D(pz=1, mass=1).mass == 1)
+    assert ak.all(vec.to_Vector4D(pz=1, M=1).M == 1)
+    assert ak.all(vec.to_Vector4D(pz=1, e=1).e == 1)
+    assert ak.all(vec.to_Vector4D(pz=1, energy=1).energy == 1)
+    assert ak.all(vec.to_Vector4D(pz=1, E=1).E == 1)
+
+    vec = vector.Array(
+        [
+            [{"px": 1, "py": 1.1, "pz": 1.2}, {"px": 2, "py": 2.1, "pz": 2.2}],
+            [],
+        ]
+    )
+    assert ak.all(vec.to_Vector4D(m=1).m == 1)
+    assert ak.all(vec.to_Vector4D(mass=1).mass == 1)
+    assert ak.all(vec.to_Vector4D(M=1).M == 1)
+    assert ak.all(vec.to_Vector4D(e=1).e == 1)
+    assert ak.all(vec.to_Vector4D(energy=1).energy == 1)
+    assert ak.all(vec.to_Vector4D(E=1).E == 1)
+
 
 def test_type_checks():
     with pytest.raises(TypeError):
diff --git a/tests/backends/test_numpy.py b/tests/backends/test_numpy.py
index e1a389d..431e5ce 100644
--- a/tests/backends/test_numpy.py
+++ b/tests/backends/test_numpy.py
@@ -14,49 +14,6 @@ import pytest
 import vector.backends.numpy
 
 
-def test_dimension_conversion():
-    # 2D -> 3D
-    vec = vector.VectorNumpy2D(
-        [(1.0, 1.0), (2.0, 2.0)],
-        dtype=[("x", float), ("y", float)],
-    )
-    assert all(vec.to_Vector3D(z=1).z == 1)
-    assert all(vec.to_Vector3D(eta=1).eta == 1)
-    assert all(vec.to_Vector3D(theta=1).theta == 1)
-
-    assert all(vec.to_Vector3D(z=1).x == vec.x)
-    assert all(vec.to_Vector3D(z=1).y == vec.y)
-
-    # 2D -> 4D
-    assert all(vec.to_Vector4D(z=1, t=1).t == 1)
-    assert all(vec.to_Vector4D(z=1, t=1).z == 1)
-    assert all(vec.to_Vector4D(eta=1, t=1).eta == 1)
-    assert all(vec.to_Vector4D(eta=1, t=1).t == 1)
-    assert all(vec.to_Vector4D(theta=1, t=1).theta == 1)
-    assert all(vec.to_Vector4D(theta=1, t=1).t == 1)
-    assert all(vec.to_Vector4D(z=1, tau=1).z == 1)
-    assert all(vec.to_Vector4D(z=1, tau=1).tau == 1)
-    assert all(vec.to_Vector4D(eta=1, tau=1).eta == 1)
-    assert all(vec.to_Vector4D(eta=1, tau=1).tau == 1)
-    assert all(vec.to_Vector4D(theta=1, tau=1).theta == 1)
-    assert all(vec.to_Vector4D(theta=1, tau=1).tau == 1)
-
-    assert all(vec.to_Vector4D(z=1, t=1).x == vec.x)
-    assert all(vec.to_Vector4D(z=1, t=1).y == vec.y)
-
-    # 3D -> 4D
-    vec = vector.VectorNumpy3D(
-        [(1.0, 1.0, 1.0), (2.0, 2.0, 2.0)],
-        dtype=[("x", float), ("y", float), ("z", float)],
-    )
-    assert all(vec.to_Vector4D(t=1).t == 1)
-    assert all(vec.to_Vector4D(tau=1).tau == 1)
-
-    assert all(vec.to_Vector4D(t=1).x == vec.x)
-    assert all(vec.to_Vector4D(t=1).y == vec.y)
-    assert all(vec.to_Vector4D(t=1).z == vec.z)
-
-
 def test_type_checks():
     with pytest.raises(TypeError):
         vector.backends.numpy.VectorNumpy2D(
@@ -486,121 +443,3 @@ def test_count_nonzero_4d():
     assert numpy.count_nonzero(v2, axis=1, keepdims=True).tolist() == [[3], [2]]
     assert numpy.count_nonzero(v2, axis=0).tolist() == [2, 2, 1]
     assert numpy.count_nonzero(v2, axis=0, keepdims=True).tolist() == [[2, 2, 1]]
-
-
-def test_demotion():
-    v1 = vector.array(
-        {
-            "x": [10.0, 20.0, 30.0],
-            "y": [-10.0, 20.0, 30.0],
-        },
-    )
-    v2 = vector.array(
-        {
-            "x": [10.0, 20.0, 30.0],
-            "y": [-10.0, 20.0, 30.0],
-            "z": [5.0, 1.0, 1.0],
-        },
-    )
-    v3 = vector.array(
-        {
-            "x": [10.0, 20.0, 30.0],
-            "y": [-10.0, 20.0, 30.0],
-            "z": [5.0, 1.0, 1.0],
-            "t": [16.0, 31.0, 46.0],
-        },
-    )
-
-    v1_v2 = vector.array(
-        {
-            "x": [20.0, 40.0, 60.0],
-            "y": [-20.0, 40.0, 60.0],
-        },
-    )
-    v2_v3 = vector.array(
-        {
-            "x": [20.0, 40.0, 60.0],
-            "y": [-20.0, 40.0, 60.0],
-            "z": [10.0, 2.0, 2.0],
-        },
-    )
-    v1_v3 = vector.array(
-        {
-            "x": [20.0, 40.0, 60.0],
-            "y": [-20.0, 40.0, 60.0],
-        },
-    )
-
-    # order should not matter
-    assert all(v1 + v2 == v1_v2)
-    assert all(v2 + v1 == v1_v2)
-    assert all(v1 + v3 == v1_v3)
-    assert all(v3 + v1 == v1_v3)
-    assert all(v2 + v3 == v2_v3)
-    assert all(v3 + v2 == v2_v3)
-
-    v1 = vector.array(
-        {
-            "px": [10.0, 20.0, 30.0],
-            "py": [-10.0, 20.0, 30.0],
-        },
-    )
-    v2 = vector.array(
-        {
-            "px": [10.0, 20.0, 30.0],
-            "py": [-10.0, 20.0, 30.0],
-            "pz": [5.0, 1.0, 1.0],
-        },
-    )
-    v3 = vector.array(
-        {
-            "px": [10.0, 20.0, 30.0],
-            "py": [-10.0, 20.0, 30.0],
-            "pz": [5.0, 1.0, 1.0],
-            "t": [16.0, 31.0, 46.0],
-        },
-    )
-
-    p_v1_v2 = vector.array(
-        {
-            "px": [20.0, 40.0, 60.0],
-            "py": [-20.0, 40.0, 60.0],
-        },
-    )
-    p_v2_v3 = vector.array(
-        {
-            "px": [20.0, 40.0, 60.0],
-            "py": [-20.0, 40.0, 60.0],
-            "pz": [10.0, 2.0, 2.0],
-        },
-    )
-    p_v1_v3 = vector.array(
-        {
-            "px": [20.0, 40.0, 60.0],
-            "py": [-20.0, 40.0, 60.0],
-        },
-    )
-
-    # order should not matter
-    assert all(v1 + v2 == p_v1_v2)
-    assert all(v2 + v1 == p_v1_v2)
-    assert all(v1 + v3 == p_v1_v3)
-    assert all(v3 + v1 == p_v1_v3)
-    assert all(v2 + v3 == p_v2_v3)
-    assert all(v3 + v2 == p_v2_v3)
-
-    v2 = vector.array(
-        {
-            "x": [10.0, 20.0, 30.0],
-            "y": [-10.0, 20.0, 30.0],
-            "z": [5.0, 1.0, 1.0],
-        },
-    )
-
-    # momentum + generic = generic
-    assert all(v1 + v2 == v1_v2)
-    assert all(v2 + v1 == v1_v2)
-    assert all(v1 + v3 == v1_v3)
-    assert all(v3 + v1 == v1_v3)
-    assert all(v2 + v3 == v2_v3)
-    assert all(v3 + v2 == v2_v3)
diff --git a/tests/backends/test_object.py b/tests/backends/test_object.py
index c50598c..bad9574 100644
--- a/tests/backends/test_object.py
+++ b/tests/backends/test_object.py
@@ -11,43 +11,6 @@ import pytest
 import vector
 
 
-def test_dimension_conversion():
-    # 2D -> 3D
-    vec = vector.VectorObject2D(x=1, y=2)
-    assert vec.to_Vector3D(z=1).z == 1
-    assert vec.to_Vector3D(eta=1).eta == 1
-    assert vec.to_Vector3D(theta=1).theta == 1
-
-    assert vec.to_Vector3D(z=1).x == vec.x
-    assert vec.to_Vector3D(z=1).y == vec.y
-
-    # 2D -> 4D
-    assert vec.to_Vector4D(z=1, t=1).z == 1
-    assert vec.to_Vector4D(z=1, t=1).t == 1
-    assert vec.to_Vector4D(eta=1, t=1).eta == 1
-    assert vec.to_Vector4D(eta=1, t=1).t == 1
-    assert vec.to_Vector4D(theta=1, t=1).theta == 1
-    assert vec.to_Vector4D(theta=1, t=1).t == 1
-    assert vec.to_Vector4D(z=1, tau=1).z == 1
-    assert vec.to_Vector4D(z=1, tau=1).tau == 1
-    assert vec.to_Vector4D(eta=1, tau=1).eta == 1
-    assert vec.to_Vector4D(eta=1, tau=1).tau == 1
-    assert vec.to_Vector4D(theta=1, tau=1).theta == 1
-    assert vec.to_Vector4D(theta=1, tau=1).tau == 1
-
-    assert vec.to_Vector4D(z=1, t=1).x == vec.x
-    assert vec.to_Vector4D(z=1, t=1).y == vec.y
-
-    # 3D -> 4D
-    vec = vector.VectorObject3D(x=1, y=2, z=3)
-    assert vec.to_Vector4D(t=1).t == 1
-    assert vec.to_Vector4D(tau=1).tau == 1
-
-    assert vec.to_Vector4D(t=1).x == vec.x
-    assert vec.to_Vector4D(t=1).y == vec.y
-    assert vec.to_Vector4D(t=1).z == vec.z
-
-
 def test_constructors_2D():
     vec = vector.VectorObject2D(x=1, y=2)
     assert vec.x == 1
@@ -257,41 +220,3 @@ def test_array_casting():
 
     with pytest.raises(TypeError):
         vector.obj(x=1, y=False)
-
-
-def test_demotion():
-    v1 = vector.obj(x=0.1, y=0.2)
-    v2 = vector.obj(x=1, y=2, z=3)
-    v3 = vector.obj(x=10, y=20, z=30, t=40)
-
-    # order should not matter
-    assert v1 + v2 == vector.obj(x=1.1, y=2.2)
-    assert v2 + v1 == vector.obj(x=1.1, y=2.2)
-    assert v1 + v3 == vector.obj(x=10.1, y=20.2)
-    assert v3 + v1 == vector.obj(x=10.1, y=20.2)
-    assert v2 + v3 == vector.obj(x=11, y=22, z=33)
-    assert v3 + v2 == vector.obj(x=11, y=22, z=33)
-
-    v1 = vector.obj(px=0.1, py=0.2)
-    v2 = vector.obj(px=1, py=2, pz=3)
-    v3 = vector.obj(px=10, py=20, pz=30, t=40)
-
-    # order should not matter
-    assert v1 + v2 == vector.obj(px=1.1, py=2.2)
-    assert v2 + v1 == vector.obj(px=1.1, py=2.2)
-    assert v1 + v3 == vector.obj(px=10.1, py=20.2)
-    assert v3 + v1 == vector.obj(px=10.1, py=20.2)
-    assert v2 + v3 == vector.obj(px=11, py=22, pz=33)
-    assert v3 + v2 == vector.obj(px=11, py=22, pz=33)
-
-    v1 = vector.obj(px=0.1, py=0.2)
-    v2 = vector.obj(x=1, y=2, z=3)
-    v3 = vector.obj(px=10, py=20, pz=30, t=40)
-
-    # momentum + generic = generic
-    assert v1 + v2 == vector.obj(x=1.1, y=2.2)
-    assert v2 + v1 == vector.obj(x=1.1, y=2.2)
-    assert v1 + v3 == vector.obj(px=10.1, py=20.2)
-    assert v3 + v1 == vector.obj(px=10.1, py=20.2)
-    assert v2 + v3 == vector.obj(x=11, y=22, z=33)
-    assert v3 + v2 == vector.obj(x=11, y=22, z=33)
diff --git a/tests/compute/test_conversions.py b/tests/compute/test_conversions.py
index d6889e0..c87f78e 100644
--- a/tests/compute/test_conversions.py
+++ b/tests/compute/test_conversions.py
@@ -491,3 +491,129 @@ def test_MomentumNumpy4D():
                 assert tv.y[0] == pytest.approx(2)
                 assert tv.z[0] == pytest.approx(3)
                 assert tv.t[0] == pytest.approx(4)
+
+
+def test_conversion_with_coords_object():
+    # 2D -> 3D
+    vec = vector.VectorObject2D(x=1, y=2)
+    assert vec.to_Vector3D(z=1).z == 1
+    assert vec.to_Vector3D(eta=1).eta == 1
+    assert vec.to_Vector3D(theta=1).theta == 1
+
+    assert vec.to_Vector3D(z=1).x == vec.x
+    assert vec.to_Vector3D(z=1).y == vec.y
+
+    # 2D -> 4D
+    assert vec.to_Vector4D(z=1, t=1).z == 1
+    assert vec.to_Vector4D(z=1, t=1).t == 1
+    assert vec.to_Vector4D(eta=1, t=1).eta == 1
+    assert vec.to_Vector4D(eta=1, t=1).t == 1
+    assert vec.to_Vector4D(theta=1, t=1).theta == 1
+    assert vec.to_Vector4D(theta=1, t=1).t == 1
+    assert vec.to_Vector4D(z=1, tau=1).z == 1
+    assert vec.to_Vector4D(z=1, tau=1).tau == 1
+    assert vec.to_Vector4D(eta=1, tau=1).eta == 1
+    assert vec.to_Vector4D(eta=1, tau=1).tau == 1
+    assert vec.to_Vector4D(theta=1, tau=1).theta == 1
+    assert vec.to_Vector4D(theta=1, tau=1).tau == 1
+
+    assert vec.to_Vector4D(z=1, t=1).x == vec.x
+    assert vec.to_Vector4D(z=1, t=1).y == vec.y
+
+    # 3D -> 4D
+    vec = vector.VectorObject3D(x=1, y=2, z=3)
+    assert vec.to_Vector4D(t=1).t == 1
+    assert vec.to_Vector4D(tau=1).tau == 1
+
+    assert vec.to_Vector4D(t=1).x == vec.x
+    assert vec.to_Vector4D(t=1).y == vec.y
+    assert vec.to_Vector4D(t=1).z == vec.z
+
+    # check if momentum coords work
+    vec = vector.MomentumObject2D(px=1, py=2)
+    assert vec.to_Vector3D(pz=1).pz == 1
+
+    assert vec.to_Vector4D(pz=1, m=1).pz == 1
+    assert vec.to_Vector4D(pz=1, m=1).m == 1
+    assert vec.to_Vector4D(pz=1, mass=1).mass == 1
+    assert vec.to_Vector4D(pz=1, M=1).M == 1
+    assert vec.to_Vector4D(pz=1, e=1).e == 1
+    assert vec.to_Vector4D(pz=1, energy=1).energy == 1
+    assert vec.to_Vector4D(pz=1, E=1).E == 1
+
+    vec = vector.MomentumObject3D(px=1, py=2, pz=3)
+    assert vec.to_Vector4D(m=1).m == 1
+    assert vec.to_Vector4D(mass=1).mass == 1
+    assert vec.to_Vector4D(M=1).M == 1
+    assert vec.to_Vector4D(e=1).e == 1
+    assert vec.to_Vector4D(energy=1).energy == 1
+    assert vec.to_Vector4D(E=1).E == 1
+
+
+def test_conversion_with_coords_numpy():
+    # 2D -> 3D
+    vec = vector.VectorNumpy2D(
+        [(1.0, 1.0), (2.0, 2.0)],
+        dtype=[("x", float), ("y", float)],
+    )
+    assert all(vec.to_Vector3D(z=1).z == 1)
+    assert all(vec.to_Vector3D(eta=1).eta == 1)
+    assert all(vec.to_Vector3D(theta=1).theta == 1)
+
+    assert all(vec.to_Vector3D(z=1).x == vec.x)
+    assert all(vec.to_Vector3D(z=1).y == vec.y)
+
+    # 2D -> 4D
+    assert all(vec.to_Vector4D(z=1, t=1).t == 1)
+    assert all(vec.to_Vector4D(z=1, t=1).z == 1)
+    assert all(vec.to_Vector4D(eta=1, t=1).eta == 1)
+    assert all(vec.to_Vector4D(eta=1, t=1).t == 1)
+    assert all(vec.to_Vector4D(theta=1, t=1).theta == 1)
+    assert all(vec.to_Vector4D(theta=1, t=1).t == 1)
+    assert all(vec.to_Vector4D(z=1, tau=1).z == 1)
+    assert all(vec.to_Vector4D(z=1, tau=1).tau == 1)
+    assert all(vec.to_Vector4D(eta=1, tau=1).eta == 1)
+    assert all(vec.to_Vector4D(eta=1, tau=1).tau == 1)
+    assert all(vec.to_Vector4D(theta=1, tau=1).theta == 1)
+    assert all(vec.to_Vector4D(theta=1, tau=1).tau == 1)
+
+    assert all(vec.to_Vector4D(z=1, t=1).x == vec.x)
+    assert all(vec.to_Vector4D(z=1, t=1).y == vec.y)
+
+    # 3D -> 4D
+    vec = vector.VectorNumpy3D(
+        [(1.0, 1.0, 1.0), (2.0, 2.0, 2.0)],
+        dtype=[("x", float), ("y", float), ("z", float)],
+    )
+    assert all(vec.to_Vector4D(t=1).t == 1)
+    assert all(vec.to_Vector4D(tau=1).tau == 1)
+
+    assert all(vec.to_Vector4D(t=1).x == vec.x)
+    assert all(vec.to_Vector4D(t=1).y == vec.y)
+    assert all(vec.to_Vector4D(t=1).z == vec.z)
+
+    # check if momentum coords work
+    vec = vector.MomentumNumpy2D(
+        [(1.0, 1.0), (2.0, 2.0)],
+        dtype=[("px", float), ("py", float)],
+    )
+    assert all(vec.to_Vector3D(pz=1).pz == 1)
+
+    assert all(vec.to_Vector4D(pz=1, m=1).pz == 1)
+    assert all(vec.to_Vector4D(pz=1, m=1).m == 1)
+    assert all(vec.to_Vector4D(pz=1, mass=1).mass == 1)
+    assert all(vec.to_Vector4D(pz=1, M=1).M == 1)
+    assert all(vec.to_Vector4D(pz=1, e=1).e == 1)
+    assert all(vec.to_Vector4D(pz=1, energy=1).energy == 1)
+    assert all(vec.to_Vector4D(pz=1, E=1).E == 1)
+
+    vec = vector.MomentumNumpy3D(
+        [(1.0, 1.0, 1.0), (2.0, 2.0, 2.0)],
+        dtype=[("px", float), ("py", float), ("pz", float)],
+    )
+    assert all(vec.to_Vector4D(m=1).m == 1)
+    assert all(vec.to_Vector4D(mass=1).mass == 1)
+    assert all(vec.to_Vector4D(M=1).M == 1)
+    assert all(vec.to_Vector4D(e=1).e == 1)
+    assert all(vec.to_Vector4D(energy=1).energy == 1)
+    assert all(vec.to_Vector4D(E=1).E == 1)
diff --git a/tests/compute/test_demotion.py b/tests/compute/test_demotion.py
new file mode 100644
index 0000000..6397c29
--- /dev/null
+++ b/tests/compute/test_demotion.py
@@ -0,0 +1,164 @@
+# Copyright (c) 2019-2024, Jonas Eschle, Jim Pivarski, Eduardo Rodrigues, and Henry Schreiner.
+#
+# Distributed under the 3-clause BSD license, see accompanying file LICENSE
+# or https://github.com/scikit-hep/vector for details.
+
+from __future__ import annotations
+
+import vector
+
+
+def test_demotion_object():
+    v1 = vector.obj(x=0.1, y=0.2)
+    v2 = vector.obj(x=1, y=2, z=3)
+    v3 = vector.obj(x=10, y=20, z=30, t=40)
+
+    # order should not matter
+    assert v1 + v2 == vector.obj(x=1.1, y=2.2)
+    assert v2 + v1 == vector.obj(x=1.1, y=2.2)
+    assert v1 + v3 == vector.obj(x=10.1, y=20.2)
+    assert v3 + v1 == vector.obj(x=10.1, y=20.2)
+    assert v2 + v3 == vector.obj(x=11, y=22, z=33)
+    assert v3 + v2 == vector.obj(x=11, y=22, z=33)
+
+    v1 = vector.obj(px=0.1, py=0.2)
+    v2 = vector.obj(px=1, py=2, pz=3)
+    v3 = vector.obj(px=10, py=20, pz=30, t=40)
+
+    # order should not matter
+    assert v1 + v2 == vector.obj(px=1.1, py=2.2)
+    assert v2 + v1 == vector.obj(px=1.1, py=2.2)
+    assert v1 + v3 == vector.obj(px=10.1, py=20.2)
+    assert v3 + v1 == vector.obj(px=10.1, py=20.2)
+    assert v2 + v3 == vector.obj(px=11, py=22, pz=33)
+    assert v3 + v2 == vector.obj(px=11, py=22, pz=33)
+
+    v1 = vector.obj(px=0.1, py=0.2)
+    v2 = vector.obj(x=1, y=2, z=3)
+    v3 = vector.obj(px=10, py=20, pz=30, t=40)
+
+    # momentum + generic = generic
+    assert v1 + v2 == vector.obj(x=1.1, y=2.2)
+    assert v2 + v1 == vector.obj(x=1.1, y=2.2)
+    assert v1 + v3 == vector.obj(px=10.1, py=20.2)
+    assert v3 + v1 == vector.obj(px=10.1, py=20.2)
+    assert v2 + v3 == vector.obj(x=11, y=22, z=33)
+    assert v3 + v2 == vector.obj(x=11, y=22, z=33)
+
+
+def test_demotion_numpy():
+    v1 = vector.array(
+        {
+            "x": [10.0, 20.0, 30.0],
+            "y": [-10.0, 20.0, 30.0],
+        },
+    )
+    v2 = vector.array(
+        {
+            "x": [10.0, 20.0, 30.0],
+            "y": [-10.0, 20.0, 30.0],
+            "z": [5.0, 1.0, 1.0],
+        },
+    )
+    v3 = vector.array(
+        {
+            "x": [10.0, 20.0, 30.0],
+            "y": [-10.0, 20.0, 30.0],
+            "z": [5.0, 1.0, 1.0],
+            "t": [16.0, 31.0, 46.0],
+        },
+    )
+
+    v1_v2 = vector.array(
+        {
+            "x": [20.0, 40.0, 60.0],
+            "y": [-20.0, 40.0, 60.0],
+        },
+    )
+    v2_v3 = vector.array(
+        {
+            "x": [20.0, 40.0, 60.0],
+            "y": [-20.0, 40.0, 60.0],
+            "z": [10.0, 2.0, 2.0],
+        },
+    )
+    v1_v3 = vector.array(
+        {
+            "x": [20.0, 40.0, 60.0],
+            "y": [-20.0, 40.0, 60.0],
+        },
+    )
+
+    # order should not matter
+    assert all(v1 + v2 == v1_v2)
+    assert all(v2 + v1 == v1_v2)
+    assert all(v1 + v3 == v1_v3)
+    assert all(v3 + v1 == v1_v3)
+    assert all(v2 + v3 == v2_v3)
+    assert all(v3 + v2 == v2_v3)
+
+    v1 = vector.array(
+        {
+            "px": [10.0, 20.0, 30.0],
+            "py": [-10.0, 20.0, 30.0],
+        },
+    )
+    v2 = vector.array(
+        {
+            "px": [10.0, 20.0, 30.0],
+            "py": [-10.0, 20.0, 30.0],
+            "pz": [5.0, 1.0, 1.0],
+        },
+    )
+    v3 = vector.array(
+        {
+            "px": [10.0, 20.0, 30.0],
+            "py": [-10.0, 20.0, 30.0],
+            "pz": [5.0, 1.0, 1.0],
+            "t": [16.0, 31.0, 46.0],
+        },
+    )
+
+    p_v1_v2 = vector.array(
+        {
+            "px": [20.0, 40.0, 60.0],
+            "py": [-20.0, 40.0, 60.0],
+        },
+    )
+    p_v2_v3 = vector.array(
+        {
+            "px": [20.0, 40.0, 60.0],
+            "py": [-20.0, 40.0, 60.0],
+            "pz": [10.0, 2.0, 2.0],
+        },
+    )
+    p_v1_v3 = vector.array(
+        {
+            "px": [20.0, 40.0, 60.0],
+            "py": [-20.0, 40.0, 60.0],
+        },
+    )
+
+    # order should not matter
+    assert all(v1 + v2 == p_v1_v2)
+    assert all(v2 + v1 == p_v1_v2)
+    assert all(v1 + v3 == p_v1_v3)
+    assert all(v3 + v1 == p_v1_v3)
+    assert all(v2 + v3 == p_v2_v3)
+    assert all(v3 + v2 == p_v2_v3)
+
+    v2 = vector.array(
+        {
+            "x": [10.0, 20.0, 30.0],
+            "y": [-10.0, 20.0, 30.0],
+            "z": [5.0, 1.0, 1.0],
+        },
+    )
+
+    # momentum + generic = generic
+    assert all(v1 + v2 == v1_v2)
+    assert all(v2 + v1 == v1_v2)
+    assert all(v1 + v3 == v1_v3)
+    assert all(v3 + v1 == v1_v3)
+    assert all(v2 + v3 == v2_v3)
+    assert all(v3 + v2 == v2_v3)