r"""Relativistic four-vector kinematics.
Works throughout in **natural units with** :math:`c=1`: energy, momentum,
and mass all share the same energy-like unit (GeV for particle physics,
MeV for nuclear physics -- the caller's choice, so long as it is used
consistently), the same convention :mod:`physicskit.relativity` uses for
its own geometrized units (:math:`G=c=1`).
- :class:`FourVector` -- an energy-momentum four-vector :math:`p^\mu =
(E, p_x, p_y, p_z)`, with its invariant mass, speed, and Lorentz factor
as derived properties.
- :func:`boost` -- a Lorentz boost along a Cartesian axis.
- :func:`boost_generic` -- a Lorentz boost along an arbitrary 3-velocity.
- :func:`invariant_mass` -- the invariant mass of a system of particles.
- :func:`rapidity` -- the additive (under boosts) rapidity variable.
- :func:`boost_to_com` -- the velocity of a system's center-of-momentum frame.
"""
from __future__ import annotations
import numpy as np
__all__ = ["FourVector", "boost", "boost_generic", "invariant_mass", "rapidity", "boost_to_com"]
[docs]
class FourVector:
r"""An energy-momentum four-vector :math:`p^\mu=(E,p_x,p_y,p_z)`.
Parameters
----------
E : float
Energy.
px, py, pz : float
Momentum components.
Examples
--------
>>> p = FourVector(2.0, 0.0, 0.0, 1.0)
>>> round(p.mass, 6)
1.732051
"""
def __init__(self, E, px, py, pz):
self.E = float(E)
self.px = float(px)
self.py = float(py)
self.pz = float(pz)
@property
def p_vec(self):
"""``ndarray`` of shape (3,): the momentum 3-vector."""
return np.array([self.px, self.py, self.pz])
@property
def p_mag(self):
r"""Momentum magnitude, :math:`|\vec p|`."""
return float(np.linalg.norm(self.p_vec))
@property
def mass(self):
r"""Invariant mass, :math:`m=\sqrt{E^2-|\vec p|^2}`.
The radicand is clipped at 0 before the square root, so a
four-vector that is numerically spacelike only by floating-point
noise (e.g. the sum of several on-shell four-vectors) returns
``0.0`` rather than raising on a tiny negative argument.
"""
radicand = self.E**2 - self.p_mag**2
return float(np.sqrt(max(radicand, 0.0)))
@property
def beta(self):
r"""Speed, :math:`\beta=|\vec p|/E`."""
return self.p_mag / self.E
@property
def gamma(self):
r"""Lorentz factor, :math:`\gamma=E/m`.
Raises
------
ZeroDivisionError
If the four-vector is massless (no rest frame).
"""
m = self.mass
if m == 0.0:
raise ZeroDivisionError("A massless four-vector has no rest frame / undefined gamma.")
return self.E / m
def __add__(self, other):
return FourVector(self.E + other.E, self.px + other.px, self.py + other.py, self.pz + other.pz)
def __sub__(self, other):
return FourVector(self.E - other.E, self.px - other.px, self.py - other.py, self.pz - other.pz)
def __repr__(self):
return f"FourVector(E={self.E:.4g}, px={self.px:.4g}, py={self.py:.4g}, pz={self.pz:.4g})"
[docs]
def boost(four_vector, beta, axis="z"):
r"""Lorentz-boost a :class:`FourVector` along a Cartesian axis.
Parameters
----------
four_vector : FourVector
The four-vector to boost.
beta : float
Boost velocity, :math:`-1 < \beta < 1`, in units of :math:`c=1`.
axis : {"x", "y", "z"}, default="z"
Cartesian axis of the boost.
Returns
-------
FourVector
The boosted four-vector.
Examples
--------
>>> p = FourVector(1.0, 0.0, 0.0, 0.0) # a particle at rest, mass 1
>>> pb = boost(p, 0.6, axis="z")
>>> round(pb.E, 6), round(pb.pz, 6)
(1.25, 0.75)
"""
if not (-1.0 < beta < 1.0):
raise ValueError(f"beta={beta} must satisfy -1 < beta < 1.")
gamma = 1.0 / np.sqrt(1.0 - beta**2)
E, px, py, pz = four_vector.E, four_vector.px, four_vector.py, four_vector.pz
if axis == "x":
return FourVector(gamma * (E + beta * px), gamma * (px + beta * E), py, pz)
elif axis == "y":
return FourVector(gamma * (E + beta * py), px, gamma * (py + beta * E), pz)
elif axis == "z":
return FourVector(gamma * (E + beta * pz), px, py, gamma * (pz + beta * E))
raise ValueError(f"axis must be 'x', 'y', or 'z', got {axis!r}.")
[docs]
def boost_generic(four_vector, beta_vec):
r"""Lorentz-boost a :class:`FourVector` along an arbitrary 3-velocity.
Generalizes :func:`boost` to a boost direction that need not lie
along a Cartesian axis -- the transformation used, e.g., to take a
daughter four-momentum computed in a parent's rest frame back to the
lab frame when the parent's own flight direction is not one of the
axes (as happens at every vertex of a branching cascade; see
:mod:`physicskit.particle.collider`).
.. math::
E' = \gamma(E + \vec\beta\cdot\vec p), \qquad
\vec p' = \vec p + \left[\frac{\gamma-1}{\beta^2}(\vec\beta\cdot\vec p)
+ \gamma E\right]\vec\beta
which reduces to :func:`boost` when :math:`\vec\beta` is aligned with
a single axis.
Parameters
----------
four_vector : FourVector
The four-vector to boost.
beta_vec : array_like of shape (3,)
Boost velocity, :math:`|\vec\beta| < 1`, in units of :math:`c=1`.
Returns
-------
FourVector
The boosted four-vector.
Examples
--------
>>> p = FourVector(1.0, 0.0, 0.0, 0.0) # a particle at rest, mass 1
>>> pb = boost_generic(p, [0.0, 0.0, 0.6])
>>> round(pb.E, 6), round(pb.pz, 6)
(1.25, 0.75)
"""
beta_vec = np.asarray(beta_vec, dtype=float)
beta2 = float(beta_vec @ beta_vec)
if beta2 >= 1.0:
raise ValueError(f"|beta_vec|={np.sqrt(beta2)} must be < 1.")
if beta2 == 0.0:
return FourVector(four_vector.E, four_vector.px, four_vector.py, four_vector.pz)
gamma = 1.0 / np.sqrt(1.0 - beta2)
E, p_vec = four_vector.E, four_vector.p_vec
p_dot_beta = float(p_vec @ beta_vec)
p_new = p_vec + ((gamma - 1.0) * p_dot_beta / beta2 + gamma * E) * beta_vec
E_new = gamma * (E + p_dot_beta)
return FourVector(float(E_new), *[float(c) for c in p_new])
[docs]
def invariant_mass(four_vectors):
"""The invariant mass of a system of four-vectors.
Parameters
----------
four_vectors : iterable of FourVector
The particles making up the system.
Returns
-------
float
The mass of the sum of the four-vectors -- e.g. the reconstructed
mass of a resonance from its decay products.
Examples
--------
>>> p1 = FourVector(1.0, 0.0, 0.0, 0.6)
>>> p2 = FourVector(1.0, 0.0, 0.0, -0.6)
>>> round(invariant_mass([p1, p2]), 6)
2.0
"""
total = None
for p in four_vectors:
total = p if total is None else total + p
return total.mass
[docs]
def rapidity(four_vector, axis="z"):
r"""Rapidity along a Cartesian axis, :math:`y=\tfrac12\ln\!\big[(E+p_i)/(E-p_i)\big]`.
Rapidity is additive under boosts along the same axis: boosting a
particle by velocity :math:`\beta` shifts its rapidity by exactly
:math:`\operatorname{artanh}\beta`.
Parameters
----------
four_vector : FourVector
The particle.
axis : {"x", "y", "z"}, default="z"
Cartesian axis.
Returns
-------
float
Examples
--------
>>> p = FourVector(2.0, 0.0, 0.0, 1.0)
>>> round(rapidity(p), 6)
0.549306
"""
E = four_vector.E
p_i = {"x": four_vector.px, "y": four_vector.py, "z": four_vector.pz}[axis]
return float(0.5 * np.log((E + p_i) / (E - p_i)))
[docs]
def boost_to_com(four_vectors):
"""The 3-velocity of the center-of-momentum frame of a system.
Parameters
----------
four_vectors : iterable of FourVector
The particles making up the system.
Returns
-------
ndarray of shape (3,)
The velocity (units of :math:`c=1`) of the system's total
momentum relative to the current frame, :math:`\\vec\\beta_{\\rm
com} = \\vec p_{\\rm tot}/E_{\\rm tot}`.
Examples
--------
>>> p1 = FourVector(2.0, 0.0, 0.0, 1.0)
>>> p2 = FourVector(1.0, 0.0, 0.0, 0.0)
>>> beta_com = boost_to_com([p1, p2])
>>> [round(float(b), 6) for b in beta_com]
[0.0, 0.0, 0.333333]
"""
total = None
for p in four_vectors:
total = p if total is None else total + p
return total.p_vec / total.E