OPALX (Object Oriented Parallel Accelerator Library for Exascal) master (dc2a29eed580)
OPALX
Loading...
Searching...
No Matches
MuonDecay.cpp
Go to the documentation of this file.
2
4#include "Physics/Physics.h"
5
7 std::size_t localDestroyNum, std::size_t oldDaughterLocal,
8 const Kokkos::View<ippl::Vector<double, 3>*>& parentR,
9 const Kokkos::View<ippl::Vector<double, 3>*>& parentP,
10 const Kokkos::View<double*>& parentDt,
11 const Kokkos::View<ippl::Vector<float, 3>*>& parentPol) {
12 using pc_size_type = ippl::detail::size_type;
13
14 auto dR = daughterPC_m->R.getView();
15 auto dP = daughterPC_m->P.getView();
16 auto dDt = daughterPC_m->dt.getView();
17
18 const auto pool = randPool_m;
19 const double daughterMass = daughterMassGeV_m;
20 // V-A asymmetry sign: +1 for mu-, -1 for mu+. Set in Decay::apply from the
21 // parent container's reference charge.
22 const int chargeSign = parentChargeSign_m;
23 const double asymSign = (chargeSign < 0) ? +1.0 : -1.0;
24
25 /* Kinematics of mu -> e + nu_e + nu_mu (three-body decay).
26 *
27 * The two neutrinos are unobserved, so the electron energy in the muon
28 * rest frame is not fixed but follows the Michel spectrum:
29 * f(x) = 2 x^2 (3 - 2x), x = E_e / E_max, E_max = m_mu / 2.
30 *
31 * Procedure per daughter:
32 * 1. Sample x from the Michel spectrum via rejection sampling.
33 * 2. Compute daughter energy E = x * E_max and momentum |p| = sqrt(E^2 - m_e^2).
34 * 3. Sample the emission direction in the parent rest frame from the
35 * polarization-dependent angular distribution
36 * 1 + alpha(x) cos theta (theta measured from the parent's spin),
37 * then orient it relative to the parent's Pol vector. Reduces to
38 * isotropic when the parent is unpolarized.
39 * 4. Lorentz-boost the rest-frame 4-momentum to the lab frame using the
40 * parent's beta*gamma vector.
41 * 5. Store the result as beta*gamma = p_lab / m_daughter. */
42 const double eMax = parentMassGeV_m / 2.0;
43 const double xMin = daughterMassGeV_m / eMax;
44 const double fMax = 2.0; // Michel spectrum upper bound at x = 1
45
46 Kokkos::parallel_for(
47 "MuonDecay::createDaughters", static_cast<pc_size_type>(localDestroyNum),
48 KOKKOS_LAMBDA(const pc_size_type j) {
49 auto gen = pool.get_state();
50
51 /* Step 1: Sample reduced energy x from Michel spectrum (rejection). */
52 double x;
53 do {
54 x = xMin + (1.0 - xMin) * gen.drand(0.0, 1.0);
55 } while (gen.drand(0.0, fMax) >= 2.0 * x * x * (3.0 - 2.0 * x));
56
57 /* Step 2: Daughter energy and momentum magnitude in rest frame. */
58 const double eDaughter = x * eMax; // [GeV]
59 const double pMag = Kokkos::sqrt(
60 eDaughter * eDaughter - daughterMass * daughterMass); // [GeV/c]
61
62 /* Step 3: spin-dependent emission direction in the muon rest frame.
63 *
64 * Full polarized Michel rate (V-A theory, m_e -> 0, no radiative
65 * corrections) as a joint density in reduced energy x and the
66 * emission angle theta from the muon spin, with c = cos theta:
67 *
68 * dN/(dx dc) ~ x^2 [ (3 - 2x) + s |P| (2x - 1) c ], s = asymSign.
69 *
70 * Marginal in x (integrate c over [-1, 1]; the c-term cancels by
71 * symmetry) is the Michel spectrum already sampled in Step 1:
72 *
73 * p(x) ~ 2 x^2 (3 - 2x) = f(x).
74 *
75 * Conditional angle (joint / marginal) is linear in c:
76 *
77 * p(c | x) = 1/2 [ 1 + alpha(x) c ],
78 * alpha(x) = s |P| (2x - 1) / (3 - 2x), |alpha| <= |P| <= 1.
79 *
80 * Inverse-CDF sampling: F(c) = 1/2 (1 + c) + alpha/4 (c^2 - 1).
81 * Setting F(c) = u (u ~ U[0,1]) and rearranging gives
82 *
83 * (alpha/4) c^2 + (1/2) c + (1/2 - alpha/4 - u) = 0,
84 *
85 * solved below for the root in [-1, 1]. For alpha ~ 0 this reduces
86 * to the uniform c = 2u - 1 (separate branch, avoids dividing by
87 * the vanishing leading coefficient). The sampled (theta, phi) are
88 * then oriented with the polar axis along Pol-hat. */
89 double polX = 0.0, polY = 0.0, polZ = 0.0;
90 if (parentPol.extent(0) > 0) {
91 polX = static_cast<double>(parentPol(j)[0]);
92 polY = static_cast<double>(parentPol(j)[1]);
93 polZ = static_cast<double>(parentPol(j)[2]);
94 }
95 const double polMag = Kokkos::sqrt(polX * polX + polY * polY + polZ * polZ);
96
97 const double alpha = (polMag > 0.0)
98 ? asymSign * polMag * (2.0 * x - 1.0) / (3.0 - 2.0 * x)
99 : 0.0;
100
101 const double u = gen.drand(0.0, 1.0);
102 double cosTheta;
103 if (Kokkos::fabs(alpha) < 1.0e-12) {
104 cosTheta = 2.0 * u - 1.0;
105 } else {
106 // Solve (alpha/4) c^2 + (1/2) c + (1/2 - alpha/4 - u) = 0 for c in [-1, 1].
107 const double aQ = alpha / 4.0;
108 const double bQ = 0.5;
109 const double cQ = 0.5 - alpha / 4.0 - u;
110 const double disc = bQ * bQ - 4.0 * aQ * cQ;
111 const double sq = Kokkos::sqrt(disc > 0.0 ? disc : 0.0);
112 const double r1 = (-bQ + sq) / (2.0 * aQ);
113 const double r2 = (-bQ - sq) / (2.0 * aQ);
114 // Pick the root inside [-1, 1] (numerically clamp).
115 const double pick = (r1 >= -1.0 && r1 <= 1.0) ? r1 : r2;
116 cosTheta = pick < -1.0 ? -1.0 : (pick > 1.0 ? 1.0 : pick);
117 }
118 const double sinTheta = Kokkos::sqrt(
119 1.0 - cosTheta * cosTheta > 0.0 ? 1.0 - cosTheta * cosTheta : 0.0);
120 const double phi = 2.0 * Physics::pi * gen.drand(0.0, 1.0);
121
122 // Daughter momentum in the muon rest frame, expressed in the
123 // spin-aligned local frame: z along Pol-hat (polar angle theta),
124 // x/y an arbitrary transverse pair (azimuth phi). Rotated into
125 // lab-aligned rest-frame axes just below.
126 const double pLoc_x = pMag * sinTheta * Kokkos::cos(phi);
127 const double pLoc_y = pMag * sinTheta * Kokkos::sin(phi);
128 const double pLoc_z = pMag * cosTheta;
129
130 // Rotate from Pol-aligned local frame (z = Pol-hat) into the lab-axis
131 // rest frame. If |Pol| is 0 the choice is irrelevant.
132 double pxRF, pyRF, pzRF;
133 if (polMag > 1.0e-30) {
134 const double nz_x = polX / polMag;
135 const double nz_y = polY / polMag;
136 const double nz_z = polZ / polMag;
137 // Build an orthonormal basis {ex, ey, nz}. Choose ex perpendicular to nz.
138 double ex_x, ex_y, ex_z;
139 if (Kokkos::fabs(nz_x) <= Kokkos::fabs(nz_y)
140 && Kokkos::fabs(nz_x) <= Kokkos::fabs(nz_z)) {
141 const double n = Kokkos::sqrt(nz_y * nz_y + nz_z * nz_z);
142 ex_x = 0.0;
143 ex_y = -nz_z / n;
144 ex_z = nz_y / n;
145 } else if (Kokkos::fabs(nz_y) <= Kokkos::fabs(nz_z)) {
146 const double n = Kokkos::sqrt(nz_x * nz_x + nz_z * nz_z);
147 ex_x = nz_z / n;
148 ex_y = 0.0;
149 ex_z = -nz_x / n;
150 } else {
151 const double n = Kokkos::sqrt(nz_x * nz_x + nz_y * nz_y);
152 ex_x = -nz_y / n;
153 ex_y = nz_x / n;
154 ex_z = 0.0;
155 }
156 // ey = nz x ex
157 const double ey_x = nz_y * ex_z - nz_z * ex_y;
158 const double ey_y = nz_z * ex_x - nz_x * ex_z;
159 const double ey_z = nz_x * ex_y - nz_y * ex_x;
160 pxRF = pLoc_x * ex_x + pLoc_y * ey_x + pLoc_z * nz_x;
161 pyRF = pLoc_x * ex_y + pLoc_y * ey_y + pLoc_z * nz_y;
162 pzRF = pLoc_x * ex_z + pLoc_y * ey_z + pLoc_z * nz_z;
163 } else {
164 pxRF = pLoc_x;
165 pyRF = pLoc_y;
166 pzRF = pLoc_z;
167 }
168
169 /* Step 4: Lorentz boost to lab frame.
170 * Parent momentum is stored as beta*gamma (dimensionless).
171 * beta = P / gamma, with gamma = sqrt(1 + |P|^2).
172 * The boost formula for the spatial components is:
173 * p_lab = p_RF + [(gamma-1)(beta . p_RF)/beta^2 + gamma*E] * beta */
174 const auto& parentMom = parentP(j);
175 const double bg2 = parentMom[0] * parentMom[0] + parentMom[1] * parentMom[1]
176 + parentMom[2] * parentMom[2];
177 const double gamma = Kokkos::sqrt(1.0 + bg2);
178 const double betaX = parentMom[0] / gamma;
179 const double betaY = parentMom[1] / gamma;
180 const double betaZ = parentMom[2] / gamma;
181 const double beta2 = bg2 / (gamma * gamma);
182
183 double pxLab, pyLab, pzLab;
184 if (beta2 > 0.0) {
185 const double betaDotP = betaX * pxRF + betaY * pyRF + betaZ * pzRF;
186 const double factor = (gamma - 1.0) * betaDotP / beta2 + gamma * eDaughter;
187 pxLab = pxRF + factor * betaX;
188 pyLab = pyRF + factor * betaY;
189 pzLab = pzRF + factor * betaZ;
190 } else {
191 pxLab = pxRF;
192 pyLab = pyRF;
193 pzLab = pzRF;
194 }
195
196 /* Step 5: Write daughter attributes.
197 * Position inherits from parent. Momentum stored as beta*gamma = p / m. */
198 const pc_size_type idx = static_cast<pc_size_type>(oldDaughterLocal) + j;
199 dR(idx) = parentR(j);
200 dP(idx)[0] = pxLab / daughterMass;
201 dP(idx)[1] = pyLab / daughterMass;
202 dP(idx)[2] = pzLab / daughterMass;
203 dDt(idx) = parentDt(j);
204
205 pool.free_state(gen);
206 });
207 Kokkos::fence();
208}
Kokkos::Random_XorShift64_Pool randPool_m
Random pool for decay sampling.
Definition Decay.h:101
double parentMassGeV_m
Rest mass of the parent particle [GeV].
Definition Decay.h:114
std::shared_ptr< ParticleContainer< double, 3 > > daughterPC_m
Daughter container for decay products (nullptr = destroy-only mode).
Definition Decay.h:104
int parentChargeSign_m
Definition Decay.h:118
double daughterMassGeV_m
Rest mass of the daughter particle [GeV].
Definition Decay.h:111
void createDaughterParticles(std::size_t localDestroyNum, std::size_t oldDaughterLocal, const Kokkos::View< ippl::Vector< double, 3 > * > &parentR, const Kokkos::View< ippl::Vector< double, 3 > * > &parentP, const Kokkos::View< double * > &parentDt, const Kokkos::View< ippl::Vector< float, 3 > * > &parentPol) override
Create daughter particles from collected parent data.
Definition MuonDecay.cpp:6
constexpr double pi
The value of.
Definition Physics.h:36