From 0f2c1c0980c0de74f12ecda06ac7f4ee8b25ee71 Mon Sep 17 00:00:00 2001 From: Iason Krommydas Date: Thu, 30 Oct 2025 17:07:53 -0500 Subject: [PATCH 1/4] first try --- src/vector/backends/awkward_constructors.py | 20 +++ src/vector/backends/numpy.py | 165 ++++++++++++++++++++ 2 files changed, 185 insertions(+) diff --git a/src/vector/backends/awkward_constructors.py b/src/vector/backends/awkward_constructors.py index 625b1fae..14c53aa8 100644 --- a/src/vector/backends/awkward_constructors.py +++ b/src/vector/backends/awkward_constructors.py @@ -199,6 +199,26 @@ def _check_names( if dimension == 0: raise TypeError(complaint1 if is_momentum else complaint2) + # Check if any remaining fieldnames would conflict with already-processed coordinates + # when mapped to generic names (e.g., pt was processed, rho shouldn't remain) + if fieldnames: + from vector._methods import _repr_momentum_to_generic + + # Reconstruct original fieldnames from names already processed + # to check against remaining fieldnames + original_fieldnames = list(names) # Start with processed generic names + original_fieldnames.extend(fieldnames) # Add remaining fieldnames + + # Map to generic names - but we need the original input fieldnames + # Actually, we need to check if remaining fieldnames conflict with processed ones + # The processed ones are in 'names' (generic form), remaining are in 'fieldnames' + + # Check each remaining fieldname to see if its generic form was already used + for fname in fieldnames: + generic = _repr_momentum_to_generic.get(fname, fname) + if generic in names: + raise TypeError(complaint1 if is_momentum else complaint2) + for name in fieldnames: names.append(name) columns.append(projectable[name]) diff --git a/src/vector/backends/numpy.py b/src/vector/backends/numpy.py index d05588b9..6f91300d 100644 --- a/src/vector/backends/numpy.py +++ b/src/vector/backends/numpy.py @@ -2071,6 +2071,168 @@ def __setitem__(self, where: typing.Any, what: typing.Any) -> None: return _setitem(self, where, what, True) +def _validate_numpy_coordinates(fieldnames: tuple[str, ...]) -> None: + """ + Validate coordinate field names using dimension-guard pattern. + + This follows the same logic as _check_names in awkward_constructors to ensure + consistent validation across backends. + + Raises TypeError if duplicate or conflicting coordinates are detected. + """ + complaint1 = "duplicate coordinates (through momentum-aliases): " + ", ".join( + repr(x) for x in fieldnames + ) + complaint2 = ( + "unrecognized combination of coordinates, allowed combinations are:\n\n" + " (2D) x= y=\n" + " (2D) rho= phi=\n" + " (3D) x= y= z=\n" + " (3D) x= y= theta=\n" + " (3D) x= y= eta=\n" + " (3D) rho= phi= z=\n" + " (3D) rho= phi= theta=\n" + " (3D) rho= phi= eta=\n" + " (4D) x= y= z= t=\n" + " (4D) x= y= z= tau=\n" + " (4D) x= y= theta= t=\n" + " (4D) x= y= theta= tau=\n" + " (4D) x= y= eta= t=\n" + " (4D) x= y= eta= tau=\n" + " (4D) rho= phi= z= t=\n" + " (4D) rho= phi= z= tau=\n" + " (4D) rho= phi= theta= t=\n" + " (4D) rho= phi= theta= tau=\n" + " (4D) rho= phi= eta= t=\n" + " (4D) rho= phi= eta= tau=" + ) + + is_momentum = False + dimension = 0 + fieldnames_copy = list(fieldnames) + + # 2D azimuthal coordinates + if "x" in fieldnames_copy and "y" in fieldnames_copy: + if dimension != 0: + raise TypeError(complaint1 if is_momentum else complaint2) + dimension = 2 + fieldnames_copy.remove("x") + fieldnames_copy.remove("y") + if "rho" in fieldnames_copy and "phi" in fieldnames_copy: + if dimension != 0: + raise TypeError(complaint1 if is_momentum else complaint2) + dimension = 2 + fieldnames_copy.remove("rho") + fieldnames_copy.remove("phi") + if "x" in fieldnames_copy and "py" in fieldnames_copy: + is_momentum = True + if dimension != 0: + raise TypeError(complaint1 if is_momentum else complaint2) + dimension = 2 + fieldnames_copy.remove("x") + fieldnames_copy.remove("py") + if "px" in fieldnames_copy and "y" in fieldnames_copy: + is_momentum = True + if dimension != 0: + raise TypeError(complaint1 if is_momentum else complaint2) + dimension = 2 + fieldnames_copy.remove("px") + fieldnames_copy.remove("y") + if "px" in fieldnames_copy and "py" in fieldnames_copy: + is_momentum = True + if dimension != 0: + raise TypeError(complaint1 if is_momentum else complaint2) + dimension = 2 + fieldnames_copy.remove("px") + fieldnames_copy.remove("py") + if "pt" in fieldnames_copy and "phi" in fieldnames_copy: + is_momentum = True + if dimension != 0: + raise TypeError(complaint1 if is_momentum else complaint2) + dimension = 2 + fieldnames_copy.remove("pt") + fieldnames_copy.remove("phi") + + # 3D longitudinal coordinates + if "z" in fieldnames_copy: + if dimension != 2: + raise TypeError(complaint1 if is_momentum else complaint2) + dimension = 3 + fieldnames_copy.remove("z") + if "theta" in fieldnames_copy: + if dimension != 2: + raise TypeError(complaint1 if is_momentum else complaint2) + dimension = 3 + fieldnames_copy.remove("theta") + if "eta" in fieldnames_copy: + if dimension != 2: + raise TypeError(complaint1 if is_momentum else complaint2) + dimension = 3 + fieldnames_copy.remove("eta") + if "pz" in fieldnames_copy: + is_momentum = True + if dimension != 2: + raise TypeError(complaint1 if is_momentum else complaint2) + dimension = 3 + fieldnames_copy.remove("pz") + + # 4D temporal coordinates + if "t" in fieldnames_copy: + if dimension != 3: + raise TypeError(complaint1 if is_momentum else complaint2) + dimension = 4 + fieldnames_copy.remove("t") + if "tau" in fieldnames_copy: + if dimension != 3: + raise TypeError(complaint1 if is_momentum else complaint2) + dimension = 4 + fieldnames_copy.remove("tau") + if "E" in fieldnames_copy: + is_momentum = True + if dimension != 3: + raise TypeError(complaint1 if is_momentum else complaint2) + dimension = 4 + fieldnames_copy.remove("E") + if "e" in fieldnames_copy: + is_momentum = True + if dimension != 3: + raise TypeError(complaint1 if is_momentum else complaint2) + dimension = 4 + fieldnames_copy.remove("e") + if "energy" in fieldnames_copy: + is_momentum = True + if dimension != 3: + raise TypeError(complaint1 if is_momentum else complaint2) + dimension = 4 + fieldnames_copy.remove("energy") + if "M" in fieldnames_copy: + is_momentum = True + if dimension != 3: + raise TypeError(complaint1 if is_momentum else complaint2) + dimension = 4 + fieldnames_copy.remove("M") + if "m" in fieldnames_copy: + is_momentum = True + if dimension != 3: + raise TypeError(complaint1 if is_momentum else complaint2) + dimension = 4 + fieldnames_copy.remove("m") + if "mass" in fieldnames_copy: + is_momentum = True + if dimension != 3: + raise TypeError(complaint1 if is_momentum else complaint2) + dimension = 4 + fieldnames_copy.remove("mass") + + # Check if any remaining fieldnames would conflict with already-processed coordinates + # when mapped to generic names (e.g., pt was processed, rho shouldn't remain) + if fieldnames_copy: + # Map all original fieldnames to generic names to detect conflicts + generic_names = [_repr_momentum_to_generic.get(x, x) for x in fieldnames] + if len(generic_names) != len(set(generic_names)): + raise TypeError(complaint1 if is_momentum else complaint2) + + def array(*args: typing.Any, **kwargs: typing.Any) -> VectorNumpy: """ Constructs a NumPy array of vectors, whose type is determined by the dtype @@ -2138,6 +2300,9 @@ def array(*args: typing.Any, **kwargs: typing.Any) -> VectorNumpy: is_momentum = any(x in _repr_momentum_to_generic for x in names) + # Validate coordinates using dimension-guard pattern (same as awkward _check_names) + _validate_numpy_coordinates(names) + if any(x in ("t", "E", "e", "energy", "tau", "M", "m", "mass") for x in names): cls = MomentumNumpy4D if is_momentum else VectorNumpy4D elif any(x in ("z", "pz", "theta", "eta") for x in names): From b04d5cfde0c1bb2ff3beb76fc8593c02913901fd Mon Sep 17 00:00:00 2001 From: Iason Krommydas Date: Thu, 30 Oct 2025 17:27:16 -0500 Subject: [PATCH 2/4] import at the top --- src/vector/backends/awkward_constructors.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vector/backends/awkward_constructors.py b/src/vector/backends/awkward_constructors.py index 14c53aa8..95fadea2 100644 --- a/src/vector/backends/awkward_constructors.py +++ b/src/vector/backends/awkward_constructors.py @@ -9,6 +9,8 @@ import numpy +from vector._methods import _repr_momentum_to_generic + def _recname(is_momentum: bool, dimension: int) -> str: name = "Momentum" if is_momentum else "Vector" @@ -202,8 +204,6 @@ def _check_names( # Check if any remaining fieldnames would conflict with already-processed coordinates # when mapped to generic names (e.g., pt was processed, rho shouldn't remain) if fieldnames: - from vector._methods import _repr_momentum_to_generic - # Reconstruct original fieldnames from names already processed # to check against remaining fieldnames original_fieldnames = list(names) # Start with processed generic names From 96d9c7ce80453a94050d75d9c4036ae0da0e7d66 Mon Sep 17 00:00:00 2001 From: Iason Krommydas Date: Thu, 30 Oct 2025 19:04:03 -0500 Subject: [PATCH 3/4] check leftovers --- src/vector/backends/awkward_constructors.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/vector/backends/awkward_constructors.py b/src/vector/backends/awkward_constructors.py index 95fadea2..3c2409d8 100644 --- a/src/vector/backends/awkward_constructors.py +++ b/src/vector/backends/awkward_constructors.py @@ -202,23 +202,19 @@ def _check_names( raise TypeError(complaint1 if is_momentum else complaint2) # Check if any remaining fieldnames would conflict with already-processed coordinates - # when mapped to generic names (e.g., pt was processed, rho shouldn't remain) + # or with each other when mapped to generic names (e.g., "x" and "px" both map to "x") if fieldnames: - # Reconstruct original fieldnames from names already processed - # to check against remaining fieldnames - original_fieldnames = list(names) # Start with processed generic names - original_fieldnames.extend(fieldnames) # Add remaining fieldnames - - # Map to generic names - but we need the original input fieldnames - # Actually, we need to check if remaining fieldnames conflict with processed ones - # The processed ones are in 'names' (generic form), remaining are in 'fieldnames' - - # Check each remaining fieldname to see if its generic form was already used + # Check leftovers against already-processed coordinates for fname in fieldnames: generic = _repr_momentum_to_generic.get(fname, fname) if generic in names: raise TypeError(complaint1 if is_momentum else complaint2) + # Check leftovers against each other for duplicates + leftover_generics = [_repr_momentum_to_generic.get(x, x) for x in fieldnames] + if len(leftover_generics) != len(set(leftover_generics)): + raise TypeError(complaint1 if is_momentum else complaint2) + for name in fieldnames: names.append(name) columns.append(projectable[name]) From a500b6fca19becf48cfff0ceff2eee1def67b3ac Mon Sep 17 00:00:00 2001 From: Iason Krommydas Date: Thu, 30 Oct 2025 20:35:15 -0500 Subject: [PATCH 4/4] add test --- src/vector/backends/object.py | 4 +- tests/backends/test_coordinate_validation.py | 1414 ++++++++++++++++++ 2 files changed, 1416 insertions(+), 2 deletions(-) create mode 100644 tests/backends/test_coordinate_validation.py diff --git a/src/vector/backends/object.py b/src/vector/backends/object.py index 16cb8101..d1bde4b7 100644 --- a/src/vector/backends/object.py +++ b/src/vector/backends/object.py @@ -3211,7 +3211,7 @@ def obj(**coordinates: float) -> VectorObject: if "E" in coordinates: is_momentum = True generic_coordinates["t"] = coordinates.pop("E") - if "e" in coordinates: + if "e" in coordinates and "t" not in generic_coordinates: is_momentum = True generic_coordinates["t"] = coordinates.pop("e") if "energy" in coordinates and "t" not in generic_coordinates: @@ -3220,7 +3220,7 @@ def obj(**coordinates: float) -> VectorObject: if "M" in coordinates: is_momentum = True generic_coordinates["tau"] = coordinates.pop("M") - if "m" in coordinates: + if "m" in coordinates and "tau" not in generic_coordinates: is_momentum = True generic_coordinates["tau"] = coordinates.pop("m") if "mass" in coordinates and "tau" not in generic_coordinates: diff --git a/tests/backends/test_coordinate_validation.py b/tests/backends/test_coordinate_validation.py new file mode 100644 index 00000000..3f524b7e --- /dev/null +++ b/tests/backends/test_coordinate_validation.py @@ -0,0 +1,1414 @@ +# Copyright (c) 2019-2025, Saransh Chopra, Henry Schreiner, Eduardo Rodrigues, Jonas Eschle, and Jim Pivarski. +# +# 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 numpy as np +import pytest + +import vector + +ak = pytest.importorskip("awkward") + +pytestmark = pytest.mark.awkward + + +# ============================================================================ +# Duplicate temporal coordinates (t-like vs tau-like) +# ============================================================================ +# Temporal coordinates: t, E, e, energy (all map to 't') +# tau, M, m, mass (all map to 'tau') +# These are mutually exclusive + + +def test_duplicate_E_e_object(): + """vector.obj should reject E + e""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.obj(pt=1.0, phi=0.5, eta=1.0, E=5.0, e=5.0) + + +def test_duplicate_E_e_numpy(): + """vector.array should reject E + e""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.array( + { + "pt": np.array([1.0, 2.0]), + "phi": np.array([0.5, 1.0]), + "eta": np.array([1.0, 1.5]), + "E": np.array([5.0, 6.0]), + "e": np.array([5.0, 6.0]), + } + ) + + +def test_duplicate_E_e_awkward(): + """vector.Array should reject E + e""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.Array( + ak.Array( + { + "pt": np.array([1.0, 2.0]), + "phi": np.array([0.5, 1.0]), + "eta": np.array([1.0, 1.5]), + "E": np.array([5.0, 6.0]), + "e": np.array([5.0, 6.0]), + } + ) + ) + + +def test_duplicate_E_e_zip(): + """vector.zip should reject E + e""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.zip( + { + "pt": np.array([1.0, 2.0]), + "phi": np.array([0.5, 1.0]), + "eta": np.array([1.0, 1.5]), + "E": np.array([5.0, 6.0]), + "e": np.array([5.0, 6.0]), + } + ) + + +def test_duplicate_E_energy_object(): + """vector.obj should reject E + energy""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.obj(pt=1.0, phi=0.5, eta=1.0, E=5.0, energy=5.0) + + +def test_duplicate_E_energy_numpy(): + """vector.array should reject E + energy""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.array( + { + "pt": np.array([1.0, 2.0]), + "phi": np.array([0.5, 1.0]), + "eta": np.array([1.0, 1.5]), + "E": np.array([5.0, 6.0]), + "energy": np.array([5.0, 6.0]), + } + ) + + +def test_duplicate_E_energy_awkward(): + """vector.Array should reject E + energy""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.Array( + ak.Array( + { + "pt": np.array([1.0, 2.0]), + "phi": np.array([0.5, 1.0]), + "eta": np.array([1.0, 1.5]), + "E": np.array([5.0, 6.0]), + "energy": np.array([5.0, 6.0]), + } + ) + ) + + +def test_duplicate_E_energy_zip(): + """vector.zip should reject E + energy""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.zip( + { + "pt": np.array([1.0, 2.0]), + "phi": np.array([0.5, 1.0]), + "eta": np.array([1.0, 1.5]), + "E": np.array([5.0, 6.0]), + "energy": np.array([5.0, 6.0]), + } + ) + + +def test_duplicate_e_energy_object(): + """vector.obj should reject e + energy""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.obj(pt=1.0, phi=0.5, eta=1.0, e=5.0, energy=5.0) + + +def test_duplicate_e_energy_numpy(): + """vector.array should reject e + energy""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.array( + { + "pt": np.array([1.0, 2.0]), + "phi": np.array([0.5, 1.0]), + "eta": np.array([1.0, 1.5]), + "e": np.array([5.0, 6.0]), + "energy": np.array([5.0, 6.0]), + } + ) + + +def test_duplicate_e_energy_awkward(): + """vector.Array should reject e + energy""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.Array( + ak.Array( + { + "pt": np.array([1.0, 2.0]), + "phi": np.array([0.5, 1.0]), + "eta": np.array([1.0, 1.5]), + "e": np.array([5.0, 6.0]), + "energy": np.array([5.0, 6.0]), + } + ) + ) + + +def test_duplicate_e_energy_zip(): + """vector.zip should reject e + energy""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.zip( + { + "pt": np.array([1.0, 2.0]), + "phi": np.array([0.5, 1.0]), + "eta": np.array([1.0, 1.5]), + "e": np.array([5.0, 6.0]), + "energy": np.array([5.0, 6.0]), + } + ) + + +def test_duplicate_M_m_object(): + """vector.obj should reject M + m""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.obj(pt=1.0, phi=0.5, eta=1.0, M=0.5, m=0.5) + + +def test_duplicate_M_m_numpy(): + """vector.array should reject M + m""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.array( + { + "pt": np.array([1.0, 2.0]), + "phi": np.array([0.5, 1.0]), + "eta": np.array([1.0, 1.5]), + "M": np.array([0.5, 0.5]), + "m": np.array([0.5, 0.5]), + } + ) + + +def test_duplicate_M_m_awkward(): + """vector.Array should reject M + m""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.Array( + ak.Array( + { + "pt": np.array([1.0, 2.0]), + "phi": np.array([0.5, 1.0]), + "eta": np.array([1.0, 1.5]), + "M": np.array([0.5, 0.5]), + "m": np.array([0.5, 0.5]), + } + ) + ) + + +def test_duplicate_M_m_zip(): + """vector.zip should reject M + m""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.zip( + { + "pt": np.array([1.0, 2.0]), + "phi": np.array([0.5, 1.0]), + "eta": np.array([1.0, 1.5]), + "M": np.array([0.5, 0.5]), + "m": np.array([0.5, 0.5]), + } + ) + + +def test_duplicate_M_mass_object(): + """vector.obj should reject M + mass""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.obj(pt=1.0, phi=0.5, eta=1.0, M=0.5, mass=0.5) + + +def test_duplicate_M_mass_numpy(): + """vector.array should reject M + mass""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.array( + { + "pt": np.array([1.0, 2.0]), + "phi": np.array([0.5, 1.0]), + "eta": np.array([1.0, 1.5]), + "M": np.array([0.5, 0.5]), + "mass": np.array([0.5, 0.5]), + } + ) + + +def test_duplicate_M_mass_awkward(): + """vector.Array should reject M + mass""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.Array( + ak.Array( + { + "pt": np.array([1.0, 2.0]), + "phi": np.array([0.5, 1.0]), + "eta": np.array([1.0, 1.5]), + "M": np.array([0.5, 0.5]), + "mass": np.array([0.5, 0.5]), + } + ) + ) + + +def test_duplicate_M_mass_zip(): + """vector.zip should reject M + mass""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.zip( + { + "pt": np.array([1.0, 2.0]), + "phi": np.array([0.5, 1.0]), + "eta": np.array([1.0, 1.5]), + "M": np.array([0.5, 0.5]), + "mass": np.array([0.5, 0.5]), + } + ) + + +def test_duplicate_m_mass_object(): + """vector.obj should reject m + mass""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.obj(pt=1.0, phi=0.5, eta=1.0, m=0.5, mass=0.5) + + +def test_duplicate_m_mass_numpy(): + """vector.array should reject m + mass""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.array( + { + "pt": np.array([1.0, 2.0]), + "phi": np.array([0.5, 1.0]), + "eta": np.array([1.0, 1.5]), + "m": np.array([0.5, 0.5]), + "mass": np.array([0.5, 0.5]), + } + ) + + +def test_duplicate_m_mass_awkward(): + """vector.Array should reject m + mass""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.Array( + ak.Array( + { + "pt": np.array([1.0, 2.0]), + "phi": np.array([0.5, 1.0]), + "eta": np.array([1.0, 1.5]), + "m": np.array([0.5, 0.5]), + "mass": np.array([0.5, 0.5]), + } + ) + ) + + +def test_duplicate_m_mass_zip(): + """vector.zip should reject m + mass""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.zip( + { + "pt": np.array([1.0, 2.0]), + "phi": np.array([0.5, 1.0]), + "eta": np.array([1.0, 1.5]), + "m": np.array([0.5, 0.5]), + "mass": np.array([0.5, 0.5]), + } + ) + + +def test_conflicting_energy_mass_object(): + """vector.obj should reject energy + mass (t-like + tau-like)""" + with pytest.raises(TypeError, match="specify t= or tau=, but not more than one"): + vector.obj(pt=1.0, phi=0.5, eta=1.0, energy=5.0, mass=0.5) + + +def test_conflicting_energy_mass_numpy(): + """vector.array should reject energy + mass (t-like + tau-like)""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.array( + { + "pt": np.array([1.0, 2.0]), + "phi": np.array([0.5, 1.0]), + "eta": np.array([1.0, 1.5]), + "energy": np.array([5.0, 6.0]), + "mass": np.array([0.5, 0.5]), + } + ) + + +def test_conflicting_energy_mass_awkward(): + """vector.Array should reject energy + mass (t-like + tau-like)""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.Array( + ak.Array( + { + "pt": np.array([1.0, 2.0]), + "phi": np.array([0.5, 1.0]), + "eta": np.array([1.0, 1.5]), + "energy": np.array([5.0, 6.0]), + "mass": np.array([0.5, 0.5]), + } + ) + ) + + +def test_conflicting_energy_mass_zip(): + """vector.zip should reject energy + mass (t-like + tau-like)""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.zip( + { + "pt": np.array([1.0, 2.0]), + "phi": np.array([0.5, 1.0]), + "eta": np.array([1.0, 1.5]), + "energy": np.array([5.0, 6.0]), + "mass": np.array([0.5, 0.5]), + } + ) + + +def test_conflicting_t_tau_object(): + """vector.obj should reject t + tau""" + with pytest.raises(TypeError, match="specify t= or tau="): + vector.obj(x=1.0, y=2.0, z=3.0, t=5.0, tau=0.5) + + +def test_conflicting_t_tau_numpy(): + """vector.array should reject t + tau""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.array( + { + "x": np.array([1.0, 2.0]), + "y": np.array([2.0, 3.0]), + "z": np.array([3.0, 4.0]), + "t": np.array([5.0, 6.0]), + "tau": np.array([0.5, 0.5]), + } + ) + + +def test_conflicting_t_tau_awkward(): + """vector.Array should reject t + tau""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.Array( + ak.Array( + { + "x": np.array([1.0, 2.0]), + "y": np.array([2.0, 3.0]), + "z": np.array([3.0, 4.0]), + "t": np.array([5.0, 6.0]), + "tau": np.array([0.5, 0.5]), + } + ) + ) + + +def test_conflicting_t_tau_zip(): + """vector.zip should reject t + tau""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.zip( + { + "x": np.array([1.0, 2.0]), + "y": np.array([2.0, 3.0]), + "z": np.array([3.0, 4.0]), + "t": np.array([5.0, 6.0]), + "tau": np.array([0.5, 0.5]), + } + ) + + +def test_conflicting_E_mass_object(): + """vector.obj should reject E + mass""" + with pytest.raises(TypeError, match="specify t= or tau=, but not more than one"): + vector.obj(pt=1.0, phi=0.5, eta=1.0, E=5.0, mass=0.5) + + +def test_conflicting_E_mass_numpy(): + """vector.array should reject E + mass""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.array( + { + "pt": np.array([1.0, 2.0]), + "phi": np.array([0.5, 1.0]), + "eta": np.array([1.0, 1.5]), + "E": np.array([5.0, 6.0]), + "mass": np.array([0.5, 0.5]), + } + ) + + +def test_conflicting_E_mass_awkward(): + """vector.Array should reject E + mass""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.Array( + ak.Array( + { + "pt": np.array([1.0, 2.0]), + "phi": np.array([0.5, 1.0]), + "eta": np.array([1.0, 1.5]), + "E": np.array([5.0, 6.0]), + "mass": np.array([0.5, 0.5]), + } + ) + ) + + +def test_conflicting_E_mass_zip(): + """vector.zip should reject E + mass""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.zip( + { + "pt": np.array([1.0, 2.0]), + "phi": np.array([0.5, 1.0]), + "eta": np.array([1.0, 1.5]), + "E": np.array([5.0, 6.0]), + "mass": np.array([0.5, 0.5]), + } + ) + + +def test_conflicting_t_mass_object(): + """vector.obj should reject t + mass""" + with pytest.raises(TypeError, match="specify t= or tau=, but not more than one"): + vector.obj(x=1.0, y=2.0, z=3.0, t=5.0, mass=0.5) + + +def test_conflicting_t_mass_numpy(): + """vector.array should reject t + mass""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.array( + { + "x": np.array([1.0, 2.0]), + "y": np.array([2.0, 3.0]), + "z": np.array([3.0, 4.0]), + "t": np.array([5.0, 6.0]), + "mass": np.array([0.5, 0.5]), + } + ) + + +def test_conflicting_t_mass_awkward(): + """vector.Array should reject t + mass""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.Array( + ak.Array( + { + "x": np.array([1.0, 2.0]), + "y": np.array([2.0, 3.0]), + "z": np.array([3.0, 4.0]), + "t": np.array([5.0, 6.0]), + "mass": np.array([0.5, 0.5]), + } + ) + ) + + +def test_conflicting_t_mass_zip(): + """vector.zip should reject t + mass""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.zip( + { + "x": np.array([1.0, 2.0]), + "y": np.array([2.0, 3.0]), + "z": np.array([3.0, 4.0]), + "t": np.array([5.0, 6.0]), + "mass": np.array([0.5, 0.5]), + } + ) + + +def test_conflicting_energy_tau_object(): + """vector.obj should reject energy + tau""" + with pytest.raises(TypeError, match="specify t= or tau=, but not more than one"): + vector.obj(x=1.0, y=2.0, z=3.0, energy=5.0, tau=0.5) + + +def test_conflicting_energy_tau_numpy(): + """vector.array should reject energy + tau""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.array( + { + "x": np.array([1.0, 2.0]), + "y": np.array([2.0, 3.0]), + "z": np.array([3.0, 4.0]), + "energy": np.array([5.0, 6.0]), + "tau": np.array([0.5, 0.5]), + } + ) + + +def test_conflicting_energy_tau_awkward(): + """vector.Array should reject energy + tau""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.Array( + ak.Array( + { + "x": np.array([1.0, 2.0]), + "y": np.array([2.0, 3.0]), + "z": np.array([3.0, 4.0]), + "energy": np.array([5.0, 6.0]), + "tau": np.array([0.5, 0.5]), + } + ) + ) + + +def test_conflicting_energy_tau_zip(): + """vector.zip should reject energy + tau""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.zip( + { + "x": np.array([1.0, 2.0]), + "y": np.array([2.0, 3.0]), + "z": np.array([3.0, 4.0]), + "energy": np.array([5.0, 6.0]), + "tau": np.array([0.5, 0.5]), + } + ) + + +# ============================================================================ +# Duplicate azimuthal coordinates +# ============================================================================ +# x <-> px, y <-> py, rho <-> pt + + +def test_duplicate_px_x_object(): + """vector.obj should reject px + x""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.obj(px=1.0, x=1.0, y=2.0, z=3.0, t=5.0) + + +def test_duplicate_px_x_numpy(): + """vector.array should reject px + x""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.array( + { + "px": np.array([1.0, 2.0]), + "x": np.array([1.0, 2.0]), + "y": np.array([2.0, 3.0]), + "z": np.array([3.0, 4.0]), + "t": np.array([5.0, 6.0]), + } + ) + + +def test_duplicate_px_x_awkward(): + """vector.Array should reject px + x""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.Array( + ak.Array( + { + "px": np.array([1.0, 2.0]), + "x": np.array([1.0, 2.0]), + "y": np.array([2.0, 3.0]), + "z": np.array([3.0, 4.0]), + "t": np.array([5.0, 6.0]), + } + ) + ) + + +def test_duplicate_px_x_zip(): + """vector.zip should reject px + x""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.zip( + { + "px": np.array([1.0, 2.0]), + "x": np.array([1.0, 2.0]), + "y": np.array([2.0, 3.0]), + "z": np.array([3.0, 4.0]), + "t": np.array([5.0, 6.0]), + } + ) + + +def test_duplicate_py_y_object(): + """vector.obj should reject py + y""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.obj(x=1.0, py=2.0, y=2.0, z=3.0, t=5.0) + + +def test_duplicate_py_y_numpy(): + """vector.array should reject py + y""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.array( + { + "x": np.array([1.0, 2.0]), + "py": np.array([2.0, 3.0]), + "y": np.array([2.0, 3.0]), + "z": np.array([3.0, 4.0]), + "t": np.array([5.0, 6.0]), + } + ) + + +def test_duplicate_py_y_awkward(): + """vector.Array should reject py + y""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.Array( + ak.Array( + { + "x": np.array([1.0, 2.0]), + "py": np.array([2.0, 3.0]), + "y": np.array([2.0, 3.0]), + "z": np.array([3.0, 4.0]), + "t": np.array([5.0, 6.0]), + } + ) + ) + + +def test_duplicate_py_y_zip(): + """vector.zip should reject py + y""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.zip( + { + "x": np.array([1.0, 2.0]), + "py": np.array([2.0, 3.0]), + "y": np.array([2.0, 3.0]), + "z": np.array([3.0, 4.0]), + "t": np.array([5.0, 6.0]), + } + ) + + +def test_duplicate_pt_rho_object(): + """vector.obj should reject pt + rho""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.obj(pt=1.0, rho=1.0, phi=0.5, eta=1.0, mass=0.5) + + +def test_duplicate_pt_rho_numpy(): + """vector.array should reject pt + rho""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.array( + { + "pt": np.array([1.0, 2.0]), + "rho": np.array([1.0, 2.0]), + "phi": np.array([0.5, 1.0]), + "eta": np.array([1.0, 1.5]), + "mass": np.array([0.5, 0.5]), + } + ) + + +def test_duplicate_pt_rho_awkward(): + """vector.Array should reject pt + rho""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.Array( + ak.Array( + { + "pt": np.array([1.0, 2.0]), + "rho": np.array([1.0, 2.0]), + "phi": np.array([0.5, 1.0]), + "eta": np.array([1.0, 1.5]), + "mass": np.array([0.5, 0.5]), + } + ) + ) + + +def test_duplicate_pt_rho_zip(): + """vector.zip should reject pt + rho""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.zip( + { + "pt": np.array([1.0, 2.0]), + "rho": np.array([1.0, 2.0]), + "phi": np.array([0.5, 1.0]), + "eta": np.array([1.0, 1.5]), + "mass": np.array([0.5, 0.5]), + } + ) + + +# ============================================================================ +# Duplicate longitudinal coordinates +# ============================================================================ +# z <-> pz + + +def test_duplicate_pz_z_object(): + """vector.obj should reject pz + z""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.obj(x=1.0, y=2.0, pz=3.0, z=3.0, t=5.0) + + +def test_duplicate_pz_z_numpy(): + """vector.array should reject pz + z""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.array( + { + "x": np.array([1.0, 2.0]), + "y": np.array([2.0, 3.0]), + "pz": np.array([3.0, 4.0]), + "z": np.array([3.0, 4.0]), + "t": np.array([5.0, 6.0]), + } + ) + + +def test_duplicate_pz_z_awkward(): + """vector.Array should reject pz + z""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.Array( + ak.Array( + { + "x": np.array([1.0, 2.0]), + "y": np.array([2.0, 3.0]), + "pz": np.array([3.0, 4.0]), + "z": np.array([3.0, 4.0]), + "t": np.array([5.0, 6.0]), + } + ) + ) + + +def test_duplicate_pz_z_zip(): + """vector.zip should reject pz + z""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.zip( + { + "x": np.array([1.0, 2.0]), + "y": np.array([2.0, 3.0]), + "pz": np.array([3.0, 4.0]), + "z": np.array([3.0, 4.0]), + "t": np.array([5.0, 6.0]), + } + ) + + +# ============================================================================ +# Mixed azimuthal coordinate systems (from _gather_coordinates) +# ============================================================================ + + +def test_mixed_xy_with_rho_object(): + """vector.obj should reject x+y with rho""" + with pytest.raises(TypeError, match="specify x= and y= or rho= and phi="): + vector.obj(x=1.0, y=2.0, rho=1.0, z=3.0) + + +def test_mixed_xy_with_phi_object(): + """vector.obj should reject x+y with phi""" + with pytest.raises(TypeError, match="specify x= and y= or rho= and phi="): + vector.obj(x=1.0, y=2.0, phi=0.5, z=3.0) + + +def test_mixed_rhophi_with_x_object(): + """vector.obj should reject rho+phi with x""" + with pytest.raises(TypeError, match="specify x= and y= or rho= and phi="): + vector.obj(rho=1.0, phi=0.5, x=1.0, z=3.0) + + +def test_mixed_rhophi_with_y_object(): + """vector.obj should reject rho+phi with y""" + with pytest.raises(TypeError, match="specify x= and y= or rho= and phi="): + vector.obj(rho=1.0, phi=0.5, y=2.0, z=3.0) + + +# ============================================================================ +# Mixed longitudinal coordinates (from _gather_coordinates) +# ============================================================================ + + +def test_mixed_z_theta_object(): + """vector.obj should reject z with theta""" + with pytest.raises(TypeError, match="specify z= or theta= or eta="): + vector.obj(x=1.0, y=2.0, z=3.0, theta=1.0) + + +def test_mixed_z_eta_object(): + """vector.obj should reject z with eta""" + with pytest.raises(TypeError, match="specify z= or theta= or eta="): + vector.obj(x=1.0, y=2.0, z=3.0, eta=1.0) + + +def test_mixed_theta_eta_object(): + """vector.obj should reject theta with eta""" + with pytest.raises(TypeError, match="specify z= or theta= or eta="): + vector.obj(x=1.0, y=2.0, theta=1.0, eta=1.0) + + +# ============================================================================ +# Valid combinations (ensure validation doesn't reject valid inputs) +# ============================================================================ + + +def test_valid_pt_phi_eta_mass_object(): + """vector.obj should accept pt, phi, eta, mass""" + vec = vector.obj(pt=1.0, phi=0.5, eta=1.0, mass=0.5) + assert vec.pt == 1.0 + assert vec.phi == 0.5 + assert vec.eta == 1.0 + assert vec.mass == 0.5 + + +def test_valid_pt_phi_eta_mass_numpy(): + """vector.array should accept pt, phi, eta, mass""" + arr = vector.array( + { + "pt": np.array([1.0, 2.0]), + "phi": np.array([0.5, 1.0]), + "eta": np.array([1.0, 1.5]), + "mass": np.array([0.5, 0.5]), + } + ) + assert np.allclose(arr.pt, [1.0, 2.0]) + assert np.allclose(arr.phi, [0.5, 1.0]) + assert np.allclose(arr.eta, [1.0, 1.5]) + assert np.allclose(arr.mass, [0.5, 0.5]) + + +def test_valid_pt_phi_eta_mass_awkward(): + """vector.Array should accept pt, phi, eta, mass""" + arr = vector.Array( + ak.Array( + { + "pt": np.array([1.0, 2.0]), + "phi": np.array([0.5, 1.0]), + "eta": np.array([1.0, 1.5]), + "mass": np.array([0.5, 0.5]), + } + ) + ) + assert ak.all(arr.pt == ak.Array([1.0, 2.0])) + assert ak.all(arr.phi == ak.Array([0.5, 1.0])) + assert ak.all(arr.eta == ak.Array([1.0, 1.5])) + assert ak.all(arr.mass == ak.Array([0.5, 0.5])) + + +def test_valid_pt_phi_eta_mass_zip(): + """vector.zip should accept pt, phi, eta, mass""" + arr = vector.zip( + { + "pt": np.array([1.0, 2.0]), + "phi": np.array([0.5, 1.0]), + "eta": np.array([1.0, 1.5]), + "mass": np.array([0.5, 0.5]), + } + ) + assert ak.all(arr.pt == ak.Array([1.0, 2.0])) + assert ak.all(arr.phi == ak.Array([0.5, 1.0])) + assert ak.all(arr.eta == ak.Array([1.0, 1.5])) + assert ak.all(arr.mass == ak.Array([0.5, 0.5])) + + +def test_valid_x_y_z_energy_object(): + """vector.obj should accept x, y, z, energy""" + vec = vector.obj(x=1.0, y=2.0, z=3.0, energy=5.0) + assert vec.x == 1.0 + assert vec.y == 2.0 + assert vec.z == 3.0 + assert vec.energy == 5.0 + + +def test_valid_x_y_z_energy_numpy(): + """vector.array should accept x, y, z, energy""" + arr = vector.array( + { + "x": np.array([1.0, 2.0]), + "y": np.array([2.0, 3.0]), + "z": np.array([3.0, 4.0]), + "energy": np.array([5.0, 6.0]), + } + ) + assert np.allclose(arr.x, [1.0, 2.0]) + assert np.allclose(arr.y, [2.0, 3.0]) + assert np.allclose(arr.z, [3.0, 4.0]) + assert np.allclose(arr.energy, [5.0, 6.0]) + + +def test_valid_x_y_z_energy_awkward(): + """vector.Array should accept x, y, z, energy""" + arr = vector.Array( + ak.Array( + { + "x": np.array([1.0, 2.0]), + "y": np.array([2.0, 3.0]), + "z": np.array([3.0, 4.0]), + "energy": np.array([5.0, 6.0]), + } + ) + ) + assert ak.all(arr.x == ak.Array([1.0, 2.0])) + assert ak.all(arr.y == ak.Array([2.0, 3.0])) + assert ak.all(arr.z == ak.Array([3.0, 4.0])) + assert ak.all(arr.energy == ak.Array([5.0, 6.0])) + + +def test_valid_x_y_z_energy_zip(): + """vector.zip should accept x, y, z, energy""" + arr = vector.zip( + { + "x": np.array([1.0, 2.0]), + "y": np.array([2.0, 3.0]), + "z": np.array([3.0, 4.0]), + "energy": np.array([5.0, 6.0]), + } + ) + assert ak.all(arr.x == ak.Array([1.0, 2.0])) + assert ak.all(arr.y == ak.Array([2.0, 3.0])) + assert ak.all(arr.z == ak.Array([3.0, 4.0])) + assert ak.all(arr.energy == ak.Array([5.0, 6.0])) + + +def test_valid_px_py_pz_E_object(): + """vector.obj should accept px, py, pz, E""" + vec = vector.obj(px=1.0, py=2.0, pz=3.0, E=5.0) + assert vec.px == 1.0 + assert vec.py == 2.0 + assert vec.pz == 3.0 + assert vec.E == 5.0 + + +def test_valid_px_py_pz_E_numpy(): + """vector.array should accept px, py, pz, E""" + arr = vector.array( + { + "px": np.array([1.0, 2.0]), + "py": np.array([2.0, 3.0]), + "pz": np.array([3.0, 4.0]), + "E": np.array([5.0, 6.0]), + } + ) + assert np.allclose(arr.px, [1.0, 2.0]) + assert np.allclose(arr.py, [2.0, 3.0]) + assert np.allclose(arr.pz, [3.0, 4.0]) + assert np.allclose(arr.E, [5.0, 6.0]) + + +def test_valid_px_py_pz_E_awkward(): + """vector.Array should accept px, py, pz, E""" + arr = vector.Array( + ak.Array( + { + "px": np.array([1.0, 2.0]), + "py": np.array([2.0, 3.0]), + "pz": np.array([3.0, 4.0]), + "E": np.array([5.0, 6.0]), + } + ) + ) + assert ak.all(arr.px == ak.Array([1.0, 2.0])) + assert ak.all(arr.py == ak.Array([2.0, 3.0])) + assert ak.all(arr.pz == ak.Array([3.0, 4.0])) + assert ak.all(ak.Array([5.0, 6.0]) == arr.E) + + +def test_valid_px_py_pz_E_zip(): + """vector.zip should accept px, py, pz, E""" + arr = vector.zip( + { + "px": np.array([1.0, 2.0]), + "py": np.array([2.0, 3.0]), + "pz": np.array([3.0, 4.0]), + "E": np.array([5.0, 6.0]), + } + ) + assert ak.all(arr.px == ak.Array([1.0, 2.0])) + assert ak.all(arr.py == ak.Array([2.0, 3.0])) + assert ak.all(arr.pz == ak.Array([3.0, 4.0])) + assert ak.all(ak.Array([5.0, 6.0]) == arr.E) + + +# ============================================================================ +# Incomplete azimuthal coordinate pairs +# ============================================================================ + + +def test_incomplete_x_without_y_object(): + """vector.obj should reject x without y""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.obj(x=1.0, z=3.0) + + +def test_incomplete_x_without_y_numpy(): + """vector.array should reject x without y""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.array( + { + "x": np.array([1.0, 2.0]), + "z": np.array([3.0, 4.0]), + } + ) + + +def test_incomplete_x_without_y_awkward(): + """vector.Array should reject x without y""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.Array( + ak.Array( + { + "x": np.array([1.0, 2.0]), + "z": np.array([3.0, 4.0]), + } + ) + ) + + +def test_incomplete_x_without_y_zip(): + """vector.zip should reject x without y""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.zip( + { + "x": np.array([1.0, 2.0]), + "z": np.array([3.0, 4.0]), + } + ) + + +def test_incomplete_rho_without_phi_object(): + """vector.obj should reject rho without phi""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.obj(rho=1.0, z=3.0) + + +def test_incomplete_rho_without_phi_numpy(): + """vector.array should reject rho without phi""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.array( + { + "rho": np.array([1.0, 2.0]), + "z": np.array([3.0, 4.0]), + } + ) + + +def test_incomplete_rho_without_phi_awkward(): + """vector.Array should reject rho without phi""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.Array( + ak.Array( + { + "rho": np.array([1.0, 2.0]), + "z": np.array([3.0, 4.0]), + } + ) + ) + + +def test_incomplete_rho_without_phi_zip(): + """vector.zip should reject rho without phi""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.zip( + { + "rho": np.array([1.0, 2.0]), + "z": np.array([3.0, 4.0]), + } + ) + + +# ============================================================================ +# Mixed azimuthal coordinate components +# ============================================================================ + + +def test_mixed_x_phi_object(): + """vector.obj should reject x with phi (mixed systems)""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.obj(x=1.0, phi=0.5, z=3.0) + + +def test_mixed_x_phi_numpy(): + """vector.array should reject x with phi (mixed systems)""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.array( + { + "x": np.array([1.0, 2.0]), + "phi": np.array([0.5, 1.0]), + "z": np.array([3.0, 4.0]), + } + ) + + +def test_mixed_x_phi_awkward(): + """vector.Array should reject x with phi (mixed systems)""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.Array( + ak.Array( + { + "x": np.array([1.0, 2.0]), + "phi": np.array([0.5, 1.0]), + "z": np.array([3.0, 4.0]), + } + ) + ) + + +def test_mixed_x_phi_zip(): + """vector.zip should reject x with phi (mixed systems)""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.zip( + { + "x": np.array([1.0, 2.0]), + "phi": np.array([0.5, 1.0]), + "z": np.array([3.0, 4.0]), + } + ) + + +def test_mixed_y_rho_object(): + """vector.obj should reject y with rho (mixed systems)""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.obj(y=2.0, rho=1.0, z=3.0) + + +def test_mixed_y_rho_numpy(): + """vector.array should reject y with rho (mixed systems)""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.array( + { + "y": np.array([2.0, 3.0]), + "rho": np.array([1.0, 2.0]), + "z": np.array([3.0, 4.0]), + } + ) + + +def test_mixed_y_rho_awkward(): + """vector.Array should reject y with rho (mixed systems)""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.Array( + ak.Array( + { + "y": np.array([2.0, 3.0]), + "rho": np.array([1.0, 2.0]), + "z": np.array([3.0, 4.0]), + } + ) + ) + + +def test_mixed_y_rho_zip(): + """vector.zip should reject y with rho (mixed systems)""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.zip( + { + "y": np.array([2.0, 3.0]), + "rho": np.array([1.0, 2.0]), + "z": np.array([3.0, 4.0]), + } + ) + + +# ============================================================================ +# Temporal without proper 3D base +# ============================================================================ + + +def test_temporal_without_longitudinal_object(): + """vector.obj should reject x+y+t (temporal without longitudinal)""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.obj(x=1.0, y=2.0, t=5.0) + + +def test_temporal_without_longitudinal_numpy(): + """vector.array should reject x+y+t (temporal without longitudinal)""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.array( + { + "x": np.array([1.0, 2.0]), + "y": np.array([2.0, 3.0]), + "t": np.array([5.0, 6.0]), + } + ) + + +def test_temporal_without_longitudinal_awkward(): + """vector.Array should reject x+y+t (temporal without longitudinal)""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.Array( + ak.Array( + { + "x": np.array([1.0, 2.0]), + "y": np.array([2.0, 3.0]), + "t": np.array([5.0, 6.0]), + } + ) + ) + + +def test_temporal_without_longitudinal_zip(): + """vector.zip should reject x+y+t (temporal without longitudinal)""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.zip( + { + "x": np.array([1.0, 2.0]), + "y": np.array([2.0, 3.0]), + "t": np.array([5.0, 6.0]), + } + ) + + +def test_mass_without_longitudinal_object(): + """vector.obj should reject pt+phi+mass (temporal without longitudinal)""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.obj(pt=1.0, phi=0.5, mass=0.5) + + +def test_mass_without_longitudinal_numpy(): + """vector.array should reject pt+phi+mass (temporal without longitudinal)""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.array( + { + "pt": np.array([1.0, 2.0]), + "phi": np.array([0.5, 1.0]), + "mass": np.array([0.5, 0.5]), + } + ) + + +def test_mass_without_longitudinal_awkward(): + """vector.Array should reject pt+phi+mass (temporal without longitudinal)""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.Array( + ak.Array( + { + "pt": np.array([1.0, 2.0]), + "phi": np.array([0.5, 1.0]), + "mass": np.array([0.5, 0.5]), + } + ) + ) + + +def test_mass_without_longitudinal_zip(): + """vector.zip should reject pt+phi+mass (temporal without longitudinal)""" + with pytest.raises(TypeError, match="duplicate coordinates"): + vector.zip( + { + "pt": np.array([1.0, 2.0]), + "phi": np.array([0.5, 1.0]), + "mass": np.array([0.5, 0.5]), + } + ) + + +# ============================================================================ +# Missing required coordinates +# ============================================================================ + + +def test_only_temporal_object(): + """vector.obj should reject only t (missing spatial coords)""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.obj(t=5.0) + + +def test_only_temporal_numpy(): + """vector.array should reject only t (missing spatial coords)""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.array( + { + "t": np.array([5.0, 6.0]), + } + ) + + +def test_only_temporal_awkward(): + """vector.Array should reject only t (missing spatial coords)""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.Array( + ak.Array( + { + "t": np.array([5.0, 6.0]), + } + ) + ) + + +def test_only_temporal_zip(): + """vector.zip should reject only t (missing spatial coords)""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.zip( + { + "t": np.array([5.0, 6.0]), + } + ) + + +def test_only_longitudinal_object(): + """vector.obj should reject only z (missing azimuthal coords)""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.obj(z=3.0) + + +def test_only_longitudinal_numpy(): + """vector.array should reject only z (missing azimuthal coords)""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.array( + { + "z": np.array([3.0, 4.0]), + } + ) + + +def test_only_longitudinal_awkward(): + """vector.Array should reject only z (missing azimuthal coords)""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.Array( + ak.Array( + { + "z": np.array([3.0, 4.0]), + } + ) + ) + + +def test_only_longitudinal_zip(): + """vector.zip should reject only z (missing azimuthal coords)""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.zip( + { + "z": np.array([3.0, 4.0]), + } + ) + + +def test_longitudinal_temporal_without_azimuthal_object(): + """vector.obj should reject z+t without azimuthal coords""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.obj(z=3.0, t=5.0) + + +def test_longitudinal_temporal_without_azimuthal_numpy(): + """vector.array should reject z+t without azimuthal coords""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.array( + { + "z": np.array([3.0, 4.0]), + "t": np.array([5.0, 6.0]), + } + ) + + +def test_longitudinal_temporal_without_azimuthal_awkward(): + """vector.Array should reject z+t without azimuthal coords""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.Array( + ak.Array( + { + "z": np.array([3.0, 4.0]), + "t": np.array([5.0, 6.0]), + } + ) + ) + + +def test_longitudinal_temporal_without_azimuthal_zip(): + """vector.zip should reject z+t without azimuthal coords""" + with pytest.raises(TypeError, match="unrecognized combination"): + vector.zip( + { + "z": np.array([3.0, 4.0]), + "t": np.array([5.0, 6.0]), + } + )