Skip to content

fix: align Lorentz/planar coordinate conventions across signatures#717

Draft
henryiii wants to merge 3 commits into
mainfrom
fix/lorentz-coordinate-consistency
Draft

fix: align Lorentz/planar coordinate conventions across signatures#717
henryiii wants to merge 3 commits into
mainfrom
fix/lorentz-coordinate-consistency

Conversation

@henryiii

Copy link
Copy Markdown
Member

🤖 AI text below 🤖

Summary

Several _compute functions returned different results for the same vector depending on which coordinate system it was stored in. This PR aligns them so all 12 Lorentz coordinate systems (and the planar/spatial helpers) agree, following the project convention of matching ROOT's TLorentzVector / Math::LorentzVector.

Each fix was reproduced before being changed; regression tests cover both the T- and Tau-coordinate representations.

Bugs fixed

  1. Et sign convention differed by signature. xy_z_t/xy_z_tau returned sqrt(Et2) (always ≥0) while the theta/eta/rhophi variants returned t·sinθ (sign-preserving in t). E.g. vector.obj(px=3, py=4, pz=0, E=-13).Et gave +13 but vector.obj(pt=5, phi=0, pz=0, E=-13).Et gave -13. Now xy_z_* use copysign(sqrt(Et2), t), matching ROOT's TLorentzVector::Et() (E<0 ? -sqrt(Et2) : sqrt(Et2)).

  2. Mt/Mt2 disagreed between T and Tau coordinates. The T-variants computed t²−z² unclamped (so Mt=nan for transverse-spacelike vectors), while the Tau-variants clamped with maximum(…, 0) (giving Mt=0). E.g. vector.obj(px=3, py=4, pz=10, E=2).Mt2 == -96, .Mt == nan, but the equivalent tau vector gave 0.0. Now both representations return the signed Mt2 = t²−z² (the Tau-variants reconstruct it as tau2 + rho², where tau2 is the signed copysign(tau², tau)), and Mt = copysign(sqrt(|Mt2|), Mt2), matching ROOT's Mt().

  3. Lorentz scale flipped tau's sign for negative factors. The Tau-variants returned tau * factor; since negative tau encodes spacelike, scaling a timelike vector by −2 produced a spacelike encoding, disagreeing with the T-coordinate version. Now tau * |factor|: scaling by λ multiplies t²−mag² by λ², so |tau| scales by |λ| and the causal character is invariant. (spatial/scale.py already takes |factor| for rho.)

  4. Planar unit of the zero vector was inconsistent. xy used nan_to_num(…, nan=0) (zero → (0,0)) but rhophi returned (1, phi) unconditionally. rhophi now mirrors spatial/unit.py: rho=0 → 0.

  5. spatial/eta.py inconsistent nan guard. xy_theta wrapped -log(tan(θ/2)) in nan_to_num(nan=0.0, …) but rhophi_theta returned the raw expression (giving nan for θ outside (0, π)). The same guard was added to rhophi_theta.

All changes stay as array-safe lib.* expressions, so the numba (register_jitable), awkward, and sympy backends keep working (copysign/maximum/nan_to_num have sympy shims). Two existing scale tau-coordinate test assertions and the sympy Mt _t assertions encoded the previous inconsistent behavior and were updated to the now-consistent values.

Convention decisions for maintainer review

These are physics-convention choices — please confirm they match the intended ROOT semantics:

  • Et is sign-preserving in energy (negative E → negative Et), per ROOT TLorentzVector::Et(). Applied uniformly to all 12 signatures.
  • Mt/Mt2 are signed (Mt2 = t²−z² unclamped; Mt = copysign(sqrt(|Mt2|), Mt2)), per ROOT Mt(), so transverse-spacelike vectors give a negative Mt rather than nan/0. The previous Tau-variant clamp to 0 is removed.
  • scale preserves causal character: |tau| scales by |factor|, sign unchanged. A side effect: for vectors stored in Tau coordinates, (v * negative).t now reports the (always-positive) reconstructed |t| with the correct timelike/spacelike magnitude instead of the previous sign-flipped value — the two updated assertions in tests/compute/test_scale.py reflect this.
  • Zero-vector unit maps to the zero vector in both xy and rhophi planar representations (matching the spatial backend).

Note: a Tau-coordinate vector does not store the sign of t, so (v*negative).t reconstructed from tau is always non-negative; this PR makes its magnitude consistent across coordinate systems but does not attempt to recover the lost sign (out of scope).

Tests

Added regression cases for: negative-energy Et across all signatures; transverse-spacelike Mt/Mt2 across both T- and Tau-coordinate vectors; negative scale factor on Tau-coordinates (timelike and spacelike); zero-vector planar unit in rhophi (object + numpy); and eta for θ outside (0, π) in both xy_theta and rhophi_theta.

uv run pytest tests/compute/ tests/backends/test_object.py tests/backends/test_numpy.py tests/backends/test_sympy.py tests/backends/test_awkward.py tests/backends/test_numba_object.py — all green.

Part of #711

🤖 Generated with Claude Code

@codecov

codecov Bot commented Jun 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.73%. Comparing base (0c94443) to head (505a2af).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #717      +/-   ##
==========================================
+ Coverage   87.72%   87.73%   +0.01%     
==========================================
  Files          96       96              
  Lines       11193    11206      +13     
==========================================
+ Hits         9819     9832      +13     
  Misses       1374     1374              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

henryiii added 2 commits June 17, 2026 16:09
Several compute functions produced different results for the same vector
depending on which coordinate system it was stored in. Align them with
ROOT's TLorentzVector conventions:

- Et: make xy_z_t/xy_z_tau sign-preserving in energy (copysign on t),
  matching the rhophi/theta/eta variants and ROOT's Et().
- Mt/Mt2: drop the clamp on the tau-variants and make Mt sign-preserving
  via copysign-sqrt, so transverse-spacelike vectors agree between the T
  and Tau representations (and match ROOT's Mt()).
- scale: scale tau by |factor| so a negative factor no longer flips a
  timelike vector into the spacelike (negative-tau) encoding.
- planar unit: rhophi now sends the zero vector to 0 (like the xy variant
  and spatial/unit.py) instead of returning (1, phi).
- spatial eta: add the same nan->0 guard to rhophi_theta that xy_theta
  already had for theta outside (0, pi).

Updates two scale tau-coordinate tests and the sympy Mt _t assertions
that encoded the previous (inconsistent) behavior, and adds regression
tests for each case.

Part of #711

Assisted-by: ClaudeCode:claude-opus-4.8
All 168 parametrizations of test_isSpacelike now match ROOT for the
PtEtaPhiM coordinate system after the coordinate-convention fix, so the
function-level xfail marker is removed to restore a hard PASS signal.

Assisted-by: ClaudeCode:claude-opus-4.8
@henryiii henryiii force-pushed the fix/lorentz-coordinate-consistency branch from 09245bb to 146f25f Compare June 17, 2026 20:27
@henryiii

Copy link
Copy Markdown
Member Author

🤖 AI text below 🤖

Ran the full ROOT comparison suite (tests/root/) against this branch (system ROOT + this branch's vector):

4740 passed, 13231 xfailed, 36635 xpassed in 303s

~73% of the previously-marked-failing ROOT comparisons now pass (36635 XPASS vs 13231 still XFAIL). The big wins are concentrated in the Lorentz systems (PtEtaPhiM/E, PxPyPzM/E), e.g. PtEtaPhiM::test_RotateZ went to 1560 XPASS / 120 XFAIL.

conftest.py marks xfail at the function level (strict=False), while the residual failures are individual parametrizations (the degenerate / nan-inf / wrap-around edge cases). So a function entry can only be dropped once every parametrization passes. After this fix exactly one entry flipped fully clean:

Entry XFAIL XPASS
test_PtEtaPhiMVector::test_isSpacelike 0 168

Removed it (now a hard 168 passed). Everything else still has a few stubborn parametrizations and stays marked.

Follow-up candidates (still 100% xfailing, untouched by this change): the test_*Perp entries under PtEtaPhiE/M, PxPyPzE/M, and XYZ (all XPASS=0).

Commit 146f25f dropped test_isSpacelike on the assumption that all 168
parametrizations matched ROOT, but that was only true on macOS. The
constructor (1, 2, 3, 0) is exactly massless, so tau = sqrt(t^2 - |p|^2)
of a true zero rounds platform-dependently: positive on macOS (timelike,
matches ROOT's isSpacelike() == False) but negative on the Linux CI
runner (spacelike == True), failing test_isSpacelike[to_xyzt-constructor8]
and [to_rhophizt-constructor8].

Re-add the non-strict xfail so the lightlike-boundary cases are tolerated
while the matching parametrizations still report XPASS.

Assisted-by: ClaudeCode:claude-opus-4.8
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant