scikit-hep / scikit-hep/vector
Code review: bugs, performance, simplifications, and modernizations
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_equaluses AND instead of OR.src/vector/_compute/planar/not_equal.py:35, allspatial/not_equal.pysignatures,lorentz/not_equal.py:146.xy_xyreturns(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)returnsFalsewhile==also returnsFalse. Tests only cover identical vectors. -
is_perpendicularmissing absolute value.planar/is_perpendicular.py:56,spatial/is_perpendicular.py:66. The check isdot < tol·|v1|·|v2|, true for any obtuse pair. Reproduced:(1,0).is_perpendicular((-1,0))→True. Needs|dot| < …. - NumPy backend
v[mask] = wbroken twice over.backends/numpy.py:186-190. (a)genericonly bound in theif is_momentum:branch but used unconditionally →UnboundLocalErrorfor 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 anddtype=kwargs; a pre-built(x,y,z,t)structured array silently becomesVectorNumpy2D(reproduced). Should fall back toasarray(args[0]).dtype.names. - NumPy coordinate classes shadow
ndarray.dtypewith mutable class state.numpy.py:465-472and 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 listsMomentumSympy2D/3D/4D,VectorSympy2D/3D/4D, but theexcept ImportErrorbranch only definesVectorSympy = None. Same forawkward_transformwithout awkward.__dir__(line 156) has the matching gap. - Numba
is_antiparallel/is_perpendicularsilently computeis_parallelfor mixed dimensions.backends/_numba_object.py:1581,1591. Both mixed-dimension fallback branches hardcodeis_parallelinstead of usingmethodname. Wrong boolean, no error, untested. - Awkward
_wrap_resultleaks stalepx/pyfields.backends/awkward.py:711-719and four sibling branches. The extra-field exclusion lists includept,pz,E,mass, … but omitpx/py. Withregister_awkward()+ak.zip({"px": …, "py": …}, with_name="Momentum3D"),rotateZreturns a record with both rotatedx/yand the unrotatedpx/py. Five copies of the list, all missing the same two names (related to #705).
Medium-severity bugs
-
is_spaceliketolerance sign —lorentz/is_spacelike.py:63:dot < +tolinstead ofdot < -tol; with nonzero tolerance the light-cone predicates overlap. Reproduced: a timelike vector reportsspacelike=True, lightlike=True, timelike=Falseattolerance=1e-3. -
Etsign convention differs by coordinate system —lorentz/Et.py:36-41:xy_z_treturnssqrt(Et2)(≥0) while every other signature returns sign-preservingt·sinθ. Reproduced:+13vs−13for the same physical vector. ROOT preserves sign. -
Mt/Mt2disagree between T and Tau coordinates —lorentz/Mt2.py:37vs:41: T-coords giveMt2=-96, Mt=nan(reproduced); Tau-coords clamp to 0. Neither matches the copysign convention used bytau.py. - Lorentz
scalewith negative factor flipstau's sign —lorentz/scale.py:40: negativetauencodes spacelike, sotau * factormis-encodes; t-coordinates of the same vector behave differently. -
vector.obj(E=…, e=…)silently accepts conflicting aliases —backends/object.py:3230-3247:eoverwritesEinstead of raising "duplicate coordinates" (reproduced: returnsE=99). Same forM=+m=. -
ptau/pE/pe/pM/pmtypo'd overloads — ~20@typing.overloadsignatures in_methods.py(1933, 2053, 2173, 2413, 2533, …) andbackends/object.py(2511, …) use nonexistent keyword names (a mechanicalt→tausubstitution also hitpt). The bogus spellings raiseTypeErrorat runtime while validpt=calls have no matching overload, so type checkers reject correct user code. - Momentum
.view()mutates the source array's dtype —numpy.py:1366,1667,2050:self.dtype.names = (...)rewrites the dtype object shared with the caller's array; afterarr.view(MomentumNumpy2D)the user'sarr.dtype.nameschanged from('px','py')to('x','y'). - NumPy coordinate
__eq__raises instead of returning False —numpy.py:481-489×7:other.dtypeaccessed before the isinstance check (az == 5→AttributeError);zip(..., strict=True)raises on length mismatch. - Object-backend
__array__lacks NumPy 2dtype/copyparams —object.py:713,849,1115,1295,1843,2063:np.asarray(v, dtype=…)raisesTypeError;np.array(v, copy=False)warns. - Awkward
Recordmethods lose behavior —awkward.py:681-686: without global registration, calling a method on a vectorak.Recordreturns a behavior-less record; chaining fails. -
vector.Array(dict_of_columns)crashes —awkward_constructors.py:299: docstring promises allak.Arraysignatures but only list/ndarray are converted. - SymPy
_lib.signdelegates tonumpy.sign—sympy.py:95: symbolicscale()on rho/phi vectors raises. Alsomaximum/minimumare byte-identical (both returnval1if symbolic) andcopysignreturnsval1unconditionally — wrong when both args are symbolic. - Numba
vector.obj(x=, y=, t=)silently dropst—_numba_object.py:865-916: the 2D branch doesn't requiretemporal is None; pure Python raises for this combination. - Numba backend still implements removed mixed-dimension semantics —
_numba_object.py:1390-1546, 2120:add/subtract/dotauto-project to min dimension andcrossauto-demotes 4D, while pure Python now raisesTypeError. 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 aliases —
e,e2,m,m2,et,et2,mt,mt2work in Python,TypingErrorin JIT (_numba_object.py:2886-3058). - CI
passgate job is bypassable —.github/workflows/ci.yml:100: noif: always(), so a failed upstream job skips the gate and branch protection treats it as green. -
nox -s docsis broken —noxfile.py:93:session.install("-e.", doc_deps)passes the list un-unpacked →TypeError. - Free-threading check in noxfile inspects the wrong interpreter —
noxfile.py:51: checks the interpreter running nox, not the session venv, and leaksPYTHON_GIL=0into subsequent sessions viaos.environ.
Lower-severity / consistency
-
numpy.py:244-259:_getitemextracts longitudinal/temporal by hard-coded positional index — wrong for non-canonical field order. -
_compute/planar/unit.py: zero-vectorunit()gives(0,0)in XY butrho=1in RhoPhi;spatial/eta.py:42vs:64:nan_to_numguard present inxy_thetabut notrhophi_theta. - Numba: 4D constructor typers check
is_temporaltypetwice and never check longitudinal (_numba_object.py:392,410); twoTypingErrors constructed withoutraise(:2061,2085). -
object.py:752,1167,numpy.py:1249,sympy.py:825,1029: operator-precedence bug in_wrap_resultbranch conditions — the isinstance guards bind only to one arm of theor(latent;numpy.py:1266shows the intended parenthesization). -
_methods.py:3163:Vector.like()treats any non-2D/3D argument as 4D instead of raising for non-vectors. - Docstring sweep:
etadescribed withtheta's range (_methods.py:858);rotate_eulerlists the same six orders for proper Euler and Tait-Bryan (:997); "Momentum-synonyor" / brokenVectorProtocolLorent2cross-ref (:1507,1522); all twelveVectorObject4D.from_*say "VectorObject3D";to_Vector4D's temporal-coordinate error message says "longitudinal" (:3258,3347);to_pxpythetamasssays "energy" (:476);to_ptphietamasssays "theta" (:593); assorted awkward.py docstring copy-paste nits (1263, 264, 1522, 1133, 1213). -
sympy.pyand_pytree.pyare missing the BSD-3 header required on every module. -
pyproject.toml: numba extra>=0.62vstest-optional>=0.57;numpy>=1.19.3unsatisfiable underrequires-python>=3.10(first 3.10 wheels were 1.21.3); blanketignore::DeprecationWarning/ignore::UserWarningdefeatsfilterwarnings=["error"];slowmarker declared but unused;cast_python_valueduplicated in pylint disables. -
awkward_constructors.py:330,407:__builtins__["zip"]relies on a CPython implementation detail (dict vs module);import builtinsis the supported spelling. Also the behavior-mutation loop at:313-324has no observable effect (overwritten bywith_name(..., behavior=...)).
Performance
-
_handler_ofre-walks the winning handler's MRO per operand on every dispatched binary op (_methods.py:4455);_aztype/_ltype/_ttypedohasattr+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 overnumpy.voidrows (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 + v2through the NumPy ufunc protocol; callingself.add(other)directly in__add__would skip that overhead. -
boost_p4theta/eta signatures compute1/sin²θand1/tanθindependently;deltaphi.xy_xyuses twoarctan2+ modulo where onearctan2(x2·y1−y2·x1, x1·x2+y1·y2)suffices (feedsdeltaR2).
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
frozensetof all coordinate/momentum names shared by the five awkward_wrap_resultbranches (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 amake_conversionloop.- The ~60-line
to_t1/to_t2selection ladder is duplicated twice each in six lorentz modules (~700 lines); a lookup dict inlorentz/t.pywould replace it. - Dead code:
_numba.py:33unusednew_name;hasattr(operator, "matmul")guard;_array_repr's unusedis_momentumparam; unusedTypeVar Vin numpy.py;requirements-txt-fixerhook with no targets; commented-out ROOT CI job +environment.ymlthat exists only to serve it.
Modernizations
-
awkward.pyleans 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.Callable→collections.abc.Callable;isinstance(x, (A | B | C))one-element-tuple-wrapping-a-union hybrid atnumpy.py:222. - blacken-docs pins
black~=24.0inadditional_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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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