OPALX (Object Oriented Parallel Accelerator Library for Exascal) master (dc2a29eed580)
OPALX
Loading...
Searching...
No Matches
TestMuonDecaySpectrum.cpp
Go to the documentation of this file.
1
46#include <gtest/gtest.h>
47#include <mpi.h>
48
49#include <array>
50#include <cmath>
51#include <cstddef>
52#include <iostream>
53#include <memory>
54#include <string>
55#include <vector>
56
57#include "Ippl.h"
59#include "Physics/MuonDecay.h"
61#include "Physics/Physics.h"
63#include "SpectrumTestSupport.h"
64#include "Utilities/Options.h"
65
66namespace {
67
69
72 double michelNormalization(double a) { return 1.0 - 2.0 * a * a * a + a * a * a * a; }
73
76 double angularAsymmetryNumerator(double a) {
77 const double a3 = a * a * a;
78 const double a4 = a3 * a;
79 return 1.0 / 3.0 - a4 + (2.0 / 3.0) * a3;
80 }
81
83 double integratedAsymmetry(double a) {
84 return angularAsymmetryNumerator(a) / michelNormalization(a);
85 }
86
87 class MuonDecaySpectrumTest : public ::testing::Test {
88 protected:
89 static void SetUpTestSuite() {
90 int argc = 0;
91 char** argv = nullptr;
92 ippl::initialize(argc, argv);
93 }
94
95 static void TearDownTestSuite() { ippl::finalize(); }
96
97 void SetUp() override {
98 oldSeed_m = Options::seed;
99 oldUseQMAttributes_m = Options::useQMAttributes;
101 }
102
103 void TearDown() override {
104 Options::seed = oldSeed_m;
105 Options::useQMAttributes = oldUseQMAttributes_m;
106 }
107
108 std::shared_ptr<PC_t> makeContainer(
109 ParticleType species = ParticleType::UNNAMED, bool spinEnabled = false) {
110 ippl::Vector<int, 3> nr = 8;
111 ippl::Vector<double, 3> rmin = -4.0;
112 ippl::Vector<double, 3> rmax = 4.0;
113 ippl::Vector<double, 3> origin = rmin;
114 ippl::Vector<double, 3> hr = (rmax - rmin) / ippl::Vector<double, 3>(nr);
115 std::array<bool, 3> decomp = {true, true, true};
116
117 ippl::NDIndex<3> domain;
118 for (unsigned i = 0; i < 3; ++i) {
119 domain[i] = ippl::Index(nr[i]);
120 }
121
122 Mesh_t<3> mesh(domain, hr, origin);
123 FieldLayout_t<3> fl(MPI_COMM_WORLD, domain, decomp, true);
124 auto pc = std::make_shared<PC_t>(mesh, fl, spinEnabled);
125 pc->Sp = static_cast<short>(species);
126 pc->setBunchStateHandler(std::make_shared<BunchStateHandler>());
127 return pc;
128 }
129
130 void createParticlesAtRest(std::shared_ptr<PC_t>& pc, std::size_t nPart) {
131 pc->createParticles(nPart);
132 auto R_host = pc->R.getHostMirror();
133 auto P_host = pc->P.getHostMirror();
134 for (std::size_t i = 0; i < nPart; ++i) {
135 R_host(i)[0] = 0.0;
136 R_host(i)[1] = 0.0;
137 R_host(i)[2] = 0.0;
138 P_host(i)[0] = 0.0;
139 P_host(i)[1] = 0.0;
140 P_host(i)[2] = 0.0;
141 }
142 Kokkos::deep_copy(pc->R.getView(), R_host);
143 Kokkos::deep_copy(pc->P.getView(), P_host);
144 Kokkos::fence();
145 }
146
148 void setUniformPolarization(std::shared_ptr<PC_t>& pc, const std::array<float, 3>& pol) {
149 using spin_t = typename PC_t::spin_vector_type;
150 auto Pol_host = pc->Pol.getHostMirror();
151 const std::size_t n = pc->getLocalNum();
152 for (std::size_t i = 0; i < n; ++i) {
153 Pol_host(i) = spin_t{pol[0], pol[1], pol[2]};
154 }
155 Kokkos::deep_copy(pc->Pol.getView(), Pol_host);
156 Kokkos::fence();
157 }
158
161 void runAngularCase(int parentSign, const std::string& csvPath) {
162 constexpr std::size_t nMuons = 100000;
163 constexpr std::size_t nBins = 50;
164 constexpr double polMag = 1.0; // fully polarized along +z
165
166 auto muons = makeContainer(ParticleType::UNNAMED, /*spinEnabled=*/true);
167 auto electrons = makeContainer(ParticleType::ELECTRON);
168 muons->setM(Physics::m_mu);
169 electrons->setM(Physics::m_e);
170 createParticlesAtRest(muons, nMuons);
171 setUniformPolarization(muons, {0.0f, 0.0f, static_cast<float>(polMag)});
172
173 Options::seed = 20260515;
174 // Timestep chosen so all muons decay within one step; we test the angular
175 // spectrum of the daughters, not the decay time.
176 MuonDecay decay(1.0e-12, 0, Physics::m_mu, parentSign);
177 decay.setDaughterContainer(electrons, Physics::m_e);
178
179 const std::size_t marked = decay.apply(*muons, 1.0, 0, 0);
180 const std::size_t destroyed = muons->deleteInvalidParticles();
181 ASSERT_EQ(marked, nMuons);
182 ASSERT_EQ(destroyed, nMuons);
183 ASSERT_EQ(electrons->getLocalNum(), nMuons);
184
185 auto eP_host = electrons->P.getHostMirror();
186 Kokkos::deep_copy(eP_host, electrons->P.getView());
187
188 // Muons are at rest, so the lab momentum direction equals the rest-frame
189 // direction; with spin along +z, cos(theta) = P_z / |P|.
190 std::vector<double> cosSamples;
191 cosSamples.reserve(nMuons);
192 for (std::size_t i = 0; i < nMuons; ++i) {
193 const double px = eP_host(i)[0];
194 const double py = eP_host(i)[1];
195 const double pz = eP_host(i)[2];
196 const double mag = std::sqrt(px * px + py * py + pz * pz);
197 if (mag > 0.0) {
198 cosSamples.push_back(pz / mag);
199 }
200 }
201
202 const double xMin = Physics::MuonDecay::minElectronX();
203 const double asymSign = (parentSign < 0) ? +1.0 : -1.0;
204 const double A = asymSign * polMag * integratedAsymmetry(xMin);
205
206 const auto hist = opalx::test::makeHistogram(cosSamples, -1.0, 1.0, nBins);
207 std::vector<double> analyticPdf(nBins, 0.0);
208 for (std::size_t i = 0; i < nBins; ++i) {
209 analyticPdf[i] = 0.5 * (1.0 + A * hist.center(i));
210 }
211
212 const double l1 = opalx::test::l1Distance(hist, analyticPdf);
213 const double meanCos = opalx::test::sampleMean(cosSamples);
214 const double area = opalx::test::histogramArea(hist);
215 const double meanRef = A / 3.0; // <cos> for p(c) = 1/2 (1 + A c) on [-1, 1]
216
217 std::cout << "[MuonAngularSpectrum sign=" << parentSign << "] L1=" << l1
218 << " mean=" << meanCos << " (analytic=" << meanRef << ") area=" << area
219 << '\n';
220
221 EXPECT_LT(l1, 0.10);
222 EXPECT_NEAR(meanCos, meanRef, 0.01);
223 EXPECT_NEAR(area, 1.0, 0.01);
224
226 csvPath, hist, analyticPdf, "cos(theta) w.r.t. muon spin");
227 std::cout << "[MuonAngularSpectrum] CSV written: " << csvPath << '\n';
228 }
229
230 private:
231 int oldSeed_m = -1;
232 bool oldUseQMAttributes_m = false;
233 };
234
235 TEST_F(MuonDecaySpectrumTest, MuMinusAngularSpectrumMatchesPolarization) {
236 runAngularCase(-1, "muon_angular_spectrum_mu_minus.csv");
237 }
238
239 TEST_F(MuonDecaySpectrumTest, MuPlusAngularSpectrumMatchesPolarization) {
240 runAngularCase(+1, "muon_angular_spectrum_mu_plus.csv");
241 }
242
243} // namespace
const int nr
ippl::FieldLayout< Dim > FieldLayout_t
Definition PBunchDefs.h:25
ippl::UniformCartesian< double, 3 > Mesh_t
Definition PBunchDefs.h:18
Lightweight histogram + CSV utilities shared by decay spectrum tests.
TEST_F(MonitorTest, GetType)
Muon decay: mu -> e + nu_e + nu_mu (three-body).
Container for all per-particle (and per-simulation) fields tracked during OPALX tracking.
bool useQMAttributes
Definition Options.cpp:109
int seed
The current random seed.
Definition Options.cpp:37
KOKKOS_INLINE_FUNCTION constexpr double minElectronX()
Minimum reduced energy .
constexpr double m_e
The electron rest mass in GeV.
Definition Physics.h:93
constexpr double m_mu
The muon rest mass in GeV.
Definition Physics.h:129
void writeSpectrumCsv(const std::string &path, const Histogram1D &h, const std::vector< double > &analyticPdf, const std::string &xLabel)
Histogram1D makeHistogram(const std::vector< double > &samples, double low, double high, std::size_t nBins)
double histogramArea(const Histogram1D &h)
Riemann sum of the histogram density (should be ~1 if all samples were in range).
double l1Distance(const Histogram1D &sampled, const std::vector< double > &analyticPdf)
double sampleMean(const std::vector< double > &samples)