scikit-hep / scikit-hep/vector

Code review: bugs, performance, simplifications, and modernizations

Open
#711 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
99
Forks
42
Avg merge
1d 13h
Merged PRs (30d)
6

Description

🤖 AI text below 🤖

A multi-agent code review of the full codebase (core, compute layer, all five backends, infrastructure). Every high-severity finding was reproduced by execution before being listed. Draft PRs addressing groups of these findings are linked below.

High-severity bugs (all reproduced)

  • != / not_equal uses AND instead of OR. src/vector/_compute/planar/not_equal.py:35, all spatial/not_equal.py signatures, lorentz/not_equal.py:146. xy_xy returns (x1 != x2) & (y1 != y2); the negation of equality needs |. The Lorentz version is doubly wrong: (t1 != t2) & spatial_equal(...). Reproduced: vector.obj(x=1.0, y=2.0) != vector.obj(x=1.0, y=3.0) returns False while == also returns False. Tests only cover identical vectors.
  • is_perpendicular missing absolute value. planar/is_perpendicular.py:56, spatial/is_perpendicular.py:66. The check is dot < tol·|v1|·|v2|, true for any obtuse pair. Reproduced: (1,0).is_perpendicular((-1,0))True. Needs |dot| < ….
  • NumPy backend v[mask] = w broken twice over. backends/numpy.py:186-190. (a) generic only bound in the if is_momentum: branch but used unconditionally → UnboundLocalError for non-momentum arrays (reproduced). (b) tofill = array[where] is a copy for boolean/fancy indices, so writes are silently discarded — only slice assignment works.
  • vector.array(existing_structured_array) always returns 2D. backends/numpy.py:2148-2169. Field-name detection only inspects dicts and dtype= kwargs; a pre-built (x,y,z,t) structured array silently becomes VectorNumpy2D (reproduced). Should fall back to asarray(args[0]).dtype.names.
  • NumPy coordinate classes shadow ndarray.dtype with mutable class state. numpy.py:465-472 and six siblings. AzimuthalNumpyXY.dtype = numpy.dtype(...) in __new__ makes every instance report whatever dtype was constructed last, anywhere in the process. Poisons __eq__'s dtype check, is test-order-dependent, and is a data race under free-threaded 3.14t.
  • from vector import * crashes without sympy installed. src/vector/__init__.py:84-97. __all__ unconditionally lists MomentumSympy2D/3D/4D, VectorSympy2D/3D/4D, but the except ImportError branch only defines VectorSympy = None. Same for awkward_transform without awkward. __dir__ (line 156) has the matching gap.
  • Numba is_antiparallel/is_perpendicular silently compute is_parallel for mixed dimensions. backends/_numba_object.py:1581,1591. Both mixed-dimension fallback branches hardcode is_parallel instead of using methodname. Wrong boolean, no error, untested.
  • Awkward _wrap_result leaks stale px/py fields. backends/awkward.py:711-719 and four sibling branches. The extra-field exclusion lists include pt, pz, E, mass, … but omit px/py. With register_awkward() + ak.zip({"px": …, "py": …}, with_name="Momentum3D"), rotateZ returns a record with both rotated x/y and the unrotated px/py. Five copies of the list, all missing the same two names (related to #705).

Medium-severity bugs

  • is_spacelike tolerance signlorentz/is_spacelike.py:63: dot < +tol instead of dot < -tol; with nonzero tolerance the light-cone predicates overlap. Reproduced: a timelike vector reports spacelike=True, lightlike=True, timelike=False at tolerance=1e-3.
  • Et sign convention differs by coordinate systemlorentz/Et.py:36-41: xy_z_t returns sqrt(Et2) (≥0) while every other signature returns sign-preserving t·sinθ. Reproduced: +13 vs −13 for the same physical vector. ROOT preserves sign.
  • Mt/Mt2 disagree between T and Tau coordinateslorentz/Mt2.py:37 vs :41: T-coords give Mt2=-96, Mt=nan (reproduced); Tau-coords clamp to 0. Neither matches the copysign convention used by tau.py.
  • Lorentz scale with negative factor flips tau's signlorentz/scale.py:40: negative tau encodes spacelike, so tau * factor mis-encodes; t-coordinates of the same vector behave differently.
  • vector.obj(E=…, e=…) silently accepts conflicting aliasesbackends/object.py:3230-3247: e overwrites E instead of raising "duplicate coordinates" (reproduced: returns E=99). Same for M=+m=.
  • ptau/pE/pe/pM/pm typo'd overloads — ~20 @typing.overload signatures in _methods.py (1933, 2053, 2173, 2413, 2533, …) and backends/object.py (2511, …) use nonexistent keyword names (a mechanical t→tau substitution also hit pt). The bogus spellings raise TypeError at runtime while valid pt= calls have no matching overload, so type checkers reject correct user code.
  • Momentum .view() mutates the source array's dtypenumpy.py:1366,1667,2050: self.dtype.names = (...) rewrites the dtype object shared with the caller's array; after arr.view(MomentumNumpy2D) the user's arr.dtype.names changed from ('px','py') to ('x','y').
  • NumPy coordinate __eq__ raises instead of returning Falsenumpy.py:481-489 ×7: other.dtype accessed before the isinstance check (az == 5AttributeError); zip(..., strict=True) raises on length mismatch.
  • Object-backend __array__ lacks NumPy 2 dtype/copy paramsobject.py:713,849,1115,1295,1843,2063: np.asarray(v, dtype=…) raises TypeError; np.array(v, copy=False) warns.
  • Awkward Record methods lose behaviorawkward.py:681-686: without global registration, calling a method on a vector ak.Record returns a behavior-less record; chaining fails.
  • vector.Array(dict_of_columns) crashesawkward_constructors.py:299: docstring promises all ak.Array signatures but only list/ndarray are converted.
  • SymPy _lib.sign delegates to numpy.signsympy.py:95: symbolic scale() on rho/phi vectors raises. Also maximum/minimum are byte-identical (both return val1 if symbolic) and copysign returns val1 unconditionally — wrong when both args are symbolic.
  • Numba vector.obj(x=, y=, t=) silently drops t_numba_object.py:865-916: the 2D branch doesn't require temporal is None; pure Python raises for this combination.
  • Numba backend still implements removed mixed-dimension semantics_numba_object.py:1390-1546, 2120: add/subtract/dot auto-project to min dimension and cross auto-demotes 4D, while pure Python now raises TypeError. Same code gives different results compiled vs. interpreted; like() (the recommended replacement) isn't available in numba either. Needs a decision: align with Python (breaking for JIT users) or document the divergence.
  • Numba missing lowercase momentum aliasese, e2, m, m2, et, et2, mt, mt2 work in Python, TypingError in JIT (_numba_object.py:2886-3058).
  • CI pass gate job is bypassable.github/workflows/ci.yml:100: no if: always(), so a failed upstream job skips the gate and branch protection treats it as green.
  • nox -s docs is brokennoxfile.py:93: session.install("-e.", doc_deps) passes the list un-unpacked → TypeError.
  • Free-threading check in noxfile inspects the wrong interpreternoxfile.py:51: checks the interpreter running nox, not the session venv, and leaks PYTHON_GIL=0 into subsequent sessions via os.environ.

Lower-severity / consistency

  • numpy.py:244-259: _getitem extracts longitudinal/temporal by hard-coded positional index — wrong for non-canonical field order.
  • _compute/planar/unit.py: zero-vector unit() gives (0,0) in XY but rho=1 in RhoPhi; spatial/eta.py:42 vs :64: nan_to_num guard present in xy_theta but not rhophi_theta.
  • Numba: 4D constructor typers check is_temporaltype twice and never check longitudinal (_numba_object.py:392,410); two TypingErrors constructed without raise (:2061,2085).
  • object.py:752,1167, numpy.py:1249, sympy.py:825,1029: operator-precedence bug in _wrap_result branch conditions — the isinstance guards bind only to one arm of the or (latent; numpy.py:1266 shows the intended parenthesization).
  • _methods.py:3163: Vector.like() treats any non-2D/3D argument as 4D instead of raising for non-vectors.
  • Docstring sweep: eta described with theta's range (_methods.py:858); rotate_euler lists the same six orders for proper Euler and Tait-Bryan (:997); "Momentum-synonyor" / broken VectorProtocolLorent2 cross-ref (:1507,1522); all twelve VectorObject4D.from_* say "VectorObject3D"; to_Vector4D's temporal-coordinate error message says "longitudinal" (:3258,3347); to_pxpythetamass says "energy" (:476); to_ptphietamass says "theta" (:593); assorted awkward.py docstring copy-paste nits (1263, 264, 1522, 1133, 1213).
  • sympy.py and _pytree.py are missing the BSD-3 header required on every module.
  • pyproject.toml: numba extra >=0.62 vs test-optional >=0.57; numpy>=1.19.3 unsatisfiable under requires-python>=3.10 (first 3.10 wheels were 1.21.3); blanket ignore::DeprecationWarning/ignore::UserWarning defeats filterwarnings=["error"]; slow marker declared but unused; cast_python_value duplicated in pylint disables.
  • awkward_constructors.py:330,407: __builtins__["zip"] relies on a CPython implementation detail (dict vs module); import builtins is the supported spelling. Also the behavior-mutation loop at :313-324 has no observable effect (overwritten by with_name(..., behavior=...)).

Performance

  • _handler_of re-walks the winning handler's MRO per operand on every dispatched binary op (_methods.py:4455); _aztype/_ltype/_ttype do hasattr+MRO scans per dispatch. The concrete type set is tiny and fixed — a per-type cache would make dispatch-key construction O(1) on the hottest path.
  • NumPy coordinate __eq__ compares element-by-element in a Python loop over numpy.void rows (numpy.py:484) — should be vectorized per-field comparisons.
  • MomentumNumpy*.__array_finalize__ re-runs alias renaming and dtype scans on every internal .view(), which fires inside every compute dispatch.
  • Object-backend operators route v1 + v2 through the NumPy ufunc protocol; calling self.add(other) directly in __add__ would skip that overhead.
  • boost_p4 theta/eta signatures compute 1/sin²θ and 1/tanθ independently; deltaphi.xy_xy uses two arctan2 + modulo where one arctan2(x2·y1−y2·x1, x1·x2+y1·y2) suffices (feeds deltaR2).

Simplifications (follow-ups, not PR'd yet)

Most of the bugs above live in hand-expanded near-duplicate code; the highest-leverage refactors:

  • A single module-level frozenset of all coordinate/momentum names shared by the five awkward _wrap_result branches (fixes the px/py class of bug structurally).
  • Loop-generate the numba momentum-alias overloads from _repr_momentum_to_generic.
  • A class factory for the seven NumPy coordinate classes (fixes dtype shadowing and __eq__ once instead of seven times).
  • boostX/Y/Z_{beta,gamma} hand-write 72 near-identical functions; sibling modules already use a make_conversion loop.
  • The ~60-line to_t1/to_t2 selection ladder is duplicated twice each in six lorentz modules (~700 lines); a lookup dict in lorentz/t.py would replace it.
  • Dead code: _numba.py:33 unused new_name; hasattr(operator, "matmul") guard; _array_repr's unused is_momentum param; unused TypeVar V in numpy.py; requirements-txt-fixer hook with no targets; commented-out ROOT CI job + environment.yml that exists only to serve it.

Modernizations

  • awkward.py leans on awkward private internals (ak._nplikes, ak._broadcasting.BROADCAST_RULE_TO_FACTORY_IMPL, ak._connect.numba.layout) — works on 2.9.1 but fragile.
  • typing.Callablecollections.abc.Callable; isinstance(x, (A | B | C)) one-element-tuple-wrapping-a-union hybrid at numpy.py:222.
  • blacken-docs pins black~=24.0 in additional_dependencies, which pre-commit.ci autoupdate never bumps.

Verified clean

All boost/rotation matrix math (verified algebraically and by roundtrip), momentum alias maps, rotate_euler conventions vs ROOT, awkward _reduce_sum axis handling, pytree roundtrips, _import_awkward's version guard, and the cd.yml trusted-publishing flow.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

This issue combines many independent findings across files such as src/vector/_compute/planar/not_equal.py, backends/numpy.py, numpy.py, and _numba_object.py. Start by selecting one unchecked bug, reproduce its stated example, and inspect the named implementation and existing tests. Done should be a focused fix with regression coverage for that finding, rather than an attempt to address the whole review.

Written by the indexing model from the issue text.

Assessment

Tech stack
numpy, python
Domain
backend, build-system, ci-cd, documentation, performance, testing-qa
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Needs clarification
Newbie friendliness
15/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.