Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 17 additions & 65 deletions src/jitr/utils/kinematics.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from dataclasses import astuple, dataclass

import numpy as np

from .constants import ALPHA, HBARC


Expand Down Expand Up @@ -126,42 +128,12 @@ def cm_to_lab_frame(
E: float,
Q: float,
):
r"""
Convert angles from center of mass to lab frame for a 2-body A + B -> C + D reaction,
using tan(theta_lab) = sin(theta_cm) / (rho + cos(theta_cm)) [Thompson & Nunes Eq 2.3.14]
where rho = sqrt(ma * mc / (mb * md) * E / (E + Q)) [Thompson & Nunes Eq. 2.3.16].

The lab frame is defined as the frame where particle B is initially at rest.

Parameters:
--------
angles_cm_deg : np.ndarray
Angles in degrees in the center of mass frame.
ma : float
Mass of particle A in MeV/c^2.
mb : float
Mass of particle B in MeV/c^2.
mc : float
Mass of particle C in MeV/c^2.
md : float
Mass of particle D in MeV/c^2.
E : float
Total energy in the center of mass frame in MeV.
Q : float
Q-value of the reaction in MeV.
Returns:
--------
np.ndarray
Angles in degrees in the lab frame.
"""
rho = np.sqrt(ma * mc / (mb * md) * E / (E + Q))
if rho >= 1:
raise ValueError("rho must be >= 1 for valid angle conversion.")
raise ValueError("rho must be < 1 for valid angle conversion.")
angles_cm_rad = np.deg2rad(angles_cm_deg)
angles_cm_rad[np.isclose(angles_cm_rad, np.pi)] = 0
y = np.sin(angles_cm_rad)
x = rho + np.cos(angles_cm_rad)

theta_lab_rad = np.arctan2(y, x)
return np.rad2deg(theta_lab_rad)

Expand All @@ -175,43 +147,23 @@ def lab_to_cm_frame(
E: float,
Q: float,
):
r"""
Convert angles from lab to center of mass frame for a 2-body A + B -> C + D reaction,
using tan(theta_lab) = sin(theta_cm) / (rho + cos(theta_cm))
[Thompson & Nunes Eq 2.3.14]cwhere rho = sqrt(ma * mc / (mb * md) * E / (E + Q))
[Thompson & Nunes Eq. 2.3.16].

The lab frame is defined as the frame where particle B is initially at rest.

Parameters:
--------
angles_lab_deg : np.ndarray
Angles in degrees in the center of mass frame.
ma : float
Mass of particle A in MeV/c^2.
mb : float
Mass of particle B in MeV/c^2.
mc : float
Mass of particle C in MeV/c^2.
md : float
Mass of particle D in MeV/c^2.
E : float
Total energy in the center of mass frame in MeV.
Q : float
Q-value of the reaction in MeV.
Returns:
--------
np.ndarray
Angles in degrees in the center of mass frame.
"""
rho = np.sqrt(ma * mc / (mb * md) * E / (E + Q))
if rho >= 1:
raise ValueError("rho must be >= 1 for valid angle conversion.")
raise ValueError("rho must be < 1 for valid angle conversion.")
t = np.tan(np.deg2rad(angles_lab_deg))

s = (rho * t + np.sqrt((1 - rho**2) * t**4 + t**2)) / (t**2 + 1)
peak_index = np.argmax(s) # angle cm is within [0,pi]
# Compute s with explicit clipping for numerical safety
under_sqrt = (1 - rho**2) * t**4 + t**2
# Prevent small negative numbers inside sqrt
# due to numerical noise
under_sqrt = np.clip(under_sqrt, 0, None)
s = (rho * t + np.sqrt(under_sqrt)) / (t**2 + 1)
s = np.clip(s, -1, 1) # For arcsin domain safety
theta_cm_rad = np.arcsin(s)
peak_index = np.argmax(s)
offset = 0 if s.size % 2 == 0 else 1
theta_cm_rad[peak_index + offset :] = np.pi - theta_cm_rad[peak_index + offset :]
# only set the mirror if in range
if peak_index + offset < s.size:
theta_cm_rad[peak_index + offset :] = (
np.pi - theta_cm_rad[peak_index + offset :]
)
return np.rad2deg(theta_cm_rad)
10 changes: 5 additions & 5 deletions test/test_kinematics.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import numpy as np
import pytest
from numpy.testing import assert_almost_equal

from jitr.utils.kinematics import cm_to_lab_frame, lab_to_cm_frame
from numpy.testing import assert_allclose


@pytest.fixture
Expand Down Expand Up @@ -41,13 +40,14 @@ def test_cm_to_lab(example_parameters):
params["E"],
params["Q"],
)
assert_almost_equal(params["angles_cm_deg"], angles_cm_deg_recovered, decimal=6)
assert_allclose(params["angles_cm_deg"], angles_cm_deg_recovered, atol=1e-6)


def test_lab_to_cm(example_parameters):
"""Test lab_to_cm_frame conversion."""
params = example_parameters
angles_lab_deg = np.linspace(0, 90, 5) # Example angles in lab frame
# Example angles in lab frame
angles_lab_deg = np.linspace(0, 90, 5)
angles_cm_deg = lab_to_cm_frame(
angles_lab_deg,
params["ma"],
Expand All @@ -68,4 +68,4 @@ def test_lab_to_cm(example_parameters):
params["E"],
params["Q"],
)
assert_almost_equal(angles_lab_deg, angles_lab_deg_recovered, decimal=6)
assert_allclose(angles_lab_deg, angles_lab_deg_recovered, atol=1e-6)