LDMX Software
APrimeConversionToFCPs.cxx
2
3#include "G4DarkBreM/G4APrime.h"
4#include "G4DarkBreM/G4FractionallyCharged.h"
5#include "G4EventManager.hh"
6#include "G4Exp.hh"
7#include "G4Log.hh"
8#include "G4Material.hh"
9#include "G4NistManager.hh"
10#include "G4ParticleTable.hh"
11#include "G4PhysicalConstants.hh"
12#include "G4SystemOfUnits.hh"
13#include "G4UnitsTable.hh"
14#include "Randomize.hh"
15#include "SimCore/G4User/UserEventInformation.h"
16
17namespace simcore {
18
19const std::string APrimeConversionToFCPs::PROCESS_NAME = "APrimeToFCPPair";
20
21static const G4double SQRTE = std::sqrt(std::exp(1.));
22static const G4double POW_SAT = -0.88;
23
25 G4ProcessType type)
26 : G4VDiscreteProcess(processName, type) {
27 // Get the fcp particle definitions
28 auto* fcp_minus = G4FractionallyCharged::FractionallyCharged();
29 mfcp_ = fcp_minus->GetPDGMass();
30
31 // Get fcp charge magnitude in units of e
32 G4double qfcp = std::abs(fcp_minus->GetPDGCharge() / CLHEP::eplus);
33
34 // Coupling-scaled reduced Compton wavelength
35 // rc_ = q^2 * elm_coupling / mfcp_
36 // This gives the correct q^4 scaling in sigfac = 4*alpha*Z^2*rc_^2
37 rc_ = qfcp * qfcp * CLHEP::elm_coupling / mfcp_;
38
39 limit_energy_ = 5. * mfcp_;
41 highest_energy_limit_ = 1e12 * CLHEP::GeV;
42 ldmx_log(debug) << "Energy limits: lowest="
43 << lowest_energy_limit_ / CLHEP::MeV
44 << " MeV, limit=" << limit_energy_ / CLHEP::MeV
45 << " MeV, highest=" << highest_energy_limit_ / CLHEP::GeV
46 << " GeV";
47
48 the_a_prime_ = G4APrime::APrime();
49 the_fcp_minus_ = fcp_minus;
50 // Look up the antiparticle (fcp+) from the particle table
51 the_fcp_plus_ = G4ParticleTable::GetParticleTable()->FindParticle("fcp+");
52
53 ldmx_log(debug) << "A' particle: "
54 << (the_a_prime_ ? the_a_prime_->GetParticleName() : "NULL")
55 << ", mass="
56 << (the_a_prime_ ? the_a_prime_->GetPDGMass() / CLHEP::MeV
57 : 0)
58 << " MeV";
59 ldmx_log(debug) << "FCP+ particle: "
60 << (the_fcp_plus_ ? the_fcp_plus_->GetParticleName()
61 : "NULL");
62 ldmx_log(debug) << "FCP- particle: "
63 << (the_fcp_minus_ ? the_fcp_minus_->GetParticleName()
64 : "NULL");
65}
66
68 const G4ParticleDefinition& particle) {
69 bool applicable = (&particle == the_a_prime_);
70 ldmx_log(debug) << "IsApplicable called for particle: "
71 << particle.GetParticleName() << " -> "
72 << (applicable ? "YES (this is A')" : "NO");
73 return applicable;
74}
75
76void APrimeConversionToFCPs::BuildPhysicsTable(const G4ParticleDefinition& p) {
77 ldmx_log(debug) << "=== BuildPhysicsTable for particle: "
78 << p.GetParticleName() << " ===";
79 auto table = G4Material::GetMaterialTable();
80 std::size_t nelm = 0;
81 for (auto const& mat : *table) {
82 std::size_t n = mat->GetNumberOfElements();
83 nelm = std::max(nelm, n);
84 ldmx_log(debug) << " Material: " << mat->GetName() << " with " << n
85 << " elements";
86 }
87 temp_.resize(nelm, 0);
88 ldmx_log(debug) << "Max elements in any material: " << nelm;
90}
91
92G4double APrimeConversionToFCPs::GetMeanFreePath(const G4Track& aTrack,
93 G4double, G4ForceCondition*) {
94 const G4DynamicParticle* a_dynamic_a_prime = aTrack.GetDynamicParticle();
95 G4double a_prime_energy = a_dynamic_a_prime->GetKineticEnergy();
96 const G4Material* a_material = aTrack.GetMaterial();
97 G4double mfp = computeMeanFreePath(a_prime_energy, a_material);
98 ldmx_log(trace) << "GetMeanFreePath: A' KE=" << a_prime_energy / CLHEP::MeV
99 << " MeV in " << a_material->GetName()
100 << " -> MFP=" << (mfp < DBL_MAX ? mfp / CLHEP::mm : -1)
101 << " mm";
102 return mfp;
103}
104
106 G4double APrimeEnergy, const G4Material* aMaterial) {
107 if (APrimeEnergy <= lowest_energy_limit_) {
108 return DBL_MAX;
109 }
110
111 const G4ElementVector* the_element_vector = aMaterial->GetElementVector();
112 const G4double* nb_of_atoms_per_volume =
113 aMaterial->GetVecNbOfAtomsPerVolume();
114
115 G4double sigma = 0.0;
116 G4double fact = 1.0;
117 G4double e = APrimeEnergy;
118
119 // Low energy approximation as in Bethe-Heitler model
120 if (e < limit_energy_) {
121 G4double y =
123 fact = y * y;
124 e = limit_energy_;
125 }
126
127 for (std::size_t i = 0; i < aMaterial->GetNumberOfElements(); ++i) {
128 sigma += nb_of_atoms_per_volume[i] * fact *
130 e, G4lrint((*the_element_vector)[i]->GetZ()));
131 }
132 return (sigma > 0.0) ? 1. / sigma : DBL_MAX;
133}
134
136 const G4DynamicParticle* aDynamicAPrime, const G4Element* anElement) {
137 return computeCrossSectionPerAtom(aDynamicAPrime->GetKineticEnergy(),
138 G4lrint(anElement->GetZ()));
139}
140
142 G4int Z) {
143 // Cross section parametrization adapted from H. Burkhardt
144 // (G4GammaConversionToMuons) with Mmuon -> mfcp_ and
145 // coupling scaled by q_fcp^4 through rc_
146 if (Egam <= lowest_energy_limit_) {
147 return 0.0;
148 }
149
150 G4NistManager* nist = G4NistManager::Instance();
151
152 G4double pow_thres, ecor, b, dn, zthird, winfty, w_med_appr, wsatur, sigfac;
153
154 if (Z == 1) { // special case of Hydrogen
155 b = 202.4;
156 dn = 1.49;
157 } else {
158 b = 183.;
159 dn = 1.54 * nist->GetA27(Z);
160 }
161 zthird = 1. / nist->GetZ13(Z); // Z^(-1/3)
162 winfty = b * zthird * mfcp_ / (dn * electron_mass_c2);
163 w_med_appr = 1. / (4. * dn * SQRTE * mfcp_);
164 wsatur = winfty / w_med_appr;
165 sigfac = 4. * fine_structure_const * Z * Z * rc_ * rc_;
166 pow_thres = 1.479 + 0.00799 * dn;
167 ecor = -18. + 4347. / (b * zthird);
168
169 G4double cor_fuc = 1. + .04 * G4Log(1. + ecor / Egam);
170 G4double eg = G4Exp(G4Log(1. - 4. * mfcp_ / Egam) * pow_thres) *
171 G4Exp(G4Log(G4Exp(G4Log(wsatur) * POW_SAT) +
172 G4Exp(G4Log(Egam) * POW_SAT)) /
173 POW_SAT);
174
175 G4double cross_section =
176 7. / 9. * sigfac * G4Log(1. + w_med_appr * cor_fuc * eg);
177 cross_section *= cross_sec_factor_;
178 return cross_section;
179}
180
182 if (fac < 0.0) return;
183 cross_sec_factor_ = fac;
184 ldmx_log(info) << "APrimeConversionToFCPs cross section factor set to "
186}
187
188G4VParticleChange* APrimeConversionToFCPs::PostStepDoIt(const G4Track& aTrack,
189 const G4Step& aStep) {
190 ldmx_log(debug) << "A' -> fcp+ fcp- conversion starting!";
191 aParticleChange.Initialize(aTrack);
192 const G4Material* a_material = aTrack.GetMaterial();
193
194 ldmx_log(debug) << "Conversion in material: " << a_material->GetName();
195 ldmx_log(debug) << "A' Track ID: " << aTrack.GetTrackID()
196 << ", Parent ID: " << aTrack.GetParentID();
197
198 // Current A' energy and direction
199 const G4DynamicParticle* a_dynamic_a_prime = aTrack.GetDynamicParticle();
200 G4double egam = a_dynamic_a_prime->GetKineticEnergy();
201
202 G4ThreeVector pos = aTrack.GetPosition();
203 ldmx_log(debug) << "A' kinetic energy: " << egam / CLHEP::MeV << " MeV";
204 ldmx_log(debug) << "A' position: (" << pos.x() / CLHEP::mm << ", "
205 << pos.y() / CLHEP::mm << ", " << pos.z() / CLHEP::mm
206 << ") mm";
207 ldmx_log(debug) << "A' momentum direction: "
208 << a_dynamic_a_prime->GetMomentumDirection();
209
210 if (egam <= lowest_energy_limit_) {
211 ldmx_log(warn) << "A' energy (" << egam / CLHEP::MeV
212 << " MeV) below threshold ("
213 << lowest_energy_limit_ / CLHEP::MeV
214 << " MeV), skipping conversion!";
215 return G4VDiscreteProcess::PostStepDoIt(aTrack, aStep);
216 }
217
218 // Kill the incident A'
219 ldmx_log(debug) << "Killing incident A' and creating fcp+/fcp- pair...";
220 aParticleChange.ProposeMomentumDirection(0., 0., 0.);
221 aParticleChange.ProposeEnergy(0.);
222 aParticleChange.ProposeTrackStatus(fStopAndKill);
223
224 G4ParticleMomentum a_prime_direction =
225 a_dynamic_a_prime->GetMomentumDirection();
226
227 // Select randomly one element constituting the material
228 const G4Element* an_element = selectRandomAtom(a_dynamic_a_prime, a_material);
229 G4int z = G4lrint(an_element->GetZ());
230
231 // Store the element Z where the A' conversion occurred for downstream DQM.
232 auto* event_info = static_cast<UserEventInformation*>(
233 G4EventManager::GetEventManager()->GetUserInformation());
234 if (event_info != nullptr) {
235 event_info->setAPrimeConversionMaterialZ(z);
236 }
237
238 G4NistManager* nist = G4NistManager::Instance();
239
240 G4double b, dn;
241 G4double a027 = nist->GetA27(z);
242
243 if (z == 1) {
244 b = 202.4;
245 dn = 1.49;
246 } else {
247 b = 183.;
248 dn = 1.54 * a027;
249 }
250 G4double zthird = 1. / nist->GetZ13(z);
251 G4double winfty = b * zthird * mfcp_ / (dn * electron_mass_c2);
252
253 G4double c1_num = 0.138 * a027;
254 G4double c1_num2 = c1_num * c1_num;
255 G4double c2_term2 = electron_mass_c2 / (183. * zthird * mfcp_);
256
257 G4double gamma_fcp_inv = mfcp_ / egam;
258
259 // Generate xPlus according to the differential cross section by rejection
260 G4double xmin =
261 (egam <= limit_energy_) ? 0.5 : 0.5 - std::sqrt(0.25 - gamma_fcp_inv);
262 G4double xmax = 1. - xmin;
263
264 G4double ds2 = (dn * SQRTE - 2.);
265 G4double s_bz = SQRTE * b * zthird / electron_mass_c2;
266 G4double log_wmax_inv = 1. / G4Log(winfty * (1. + 2. * ds2 * gamma_fcp_inv) /
267 (1. + 2. * s_bz * mfcp_ * gamma_fcp_inv));
268 G4double x_plus = 0.5;
269 G4double x_minus = 0.5;
270 G4double x_pm = 0.25;
271
272 G4int nn = 0;
273 const G4int nmax = 1000;
274
275 // Sampling for Egam > limit_energy_
276 if (xmin < 0.5) {
277 G4double result, w;
278 do {
279 x_plus = xmin + G4UniformRand() * (xmax - xmin);
280 x_minus = 1. - x_plus;
281 x_pm = x_plus * x_minus;
282 G4double del = mfcp_ * mfcp_ / (2. * egam * x_pm);
283 w = winfty * (1. + ds2 * del / mfcp_) / (1. + s_bz * del);
284 G4double xxp = 1. - 4. / 3. * x_pm;
285 result = (xxp > 0.) ? xxp * G4Log(w) * log_wmax_inv : 0.0;
286 if (result > 1.0) {
287 ldmx_log(warn) << "APrimeConversionToFCPs::PostStepDoIt WARNING:"
288 << " in dSigxPlusGen, result=" << result << " > 1";
289 }
290 ++nn;
291 if (nn >= nmax) {
292 break;
293 }
294 } while (G4UniformRand() > result);
295 }
296
297 // Generate angular variables via auxiliary variables t, psi, rho
298 G4double t;
299 G4double psi;
300 G4double rho;
301
302 G4double a3 = (gamma_fcp_inv / (2. * x_pm));
303 G4double a33 = a3 * a3;
304 G4double f1;
305 G4double b1 = 1. / (4. * c1_num2);
306 G4double b3 = b1 * b1 * b1;
307 G4double a21 = a33 + b1;
308
309 G4double f1_max =
310 -(1. - x_pm) * (2. * b1 + (a21 + a33) * G4Log(a33 / a21)) / (2 * b3);
311
312 G4double theta_plus, theta_minus, phi_half;
313 nn = 0;
314
315 // t, psi, rho generation (while angle < pi)
316 do {
317 // Generate t by rejection method
318 do {
319 ++nn;
320 t = G4UniformRand();
321 G4double a34 = a33 / (t * t);
322 G4double a22 = a34 + b1;
323 if (std::abs(b1) < 0.0001 * a34) {
324 f1 = (1. - 2. * x_pm + 4. * x_pm * t * (1. - t)) /
325 (12. * a34 * a34 * a34 * a34);
326 } else {
327 f1 = -(1. - 2. * x_pm + 4. * x_pm * t * (1. - t)) *
328 (2. * b1 + (a22 + a34) * G4Log(a34 / a22)) / (2 * b3);
329 }
330 if (f1 < 0.0 || f1 > f1_max) {
331 f1 = 0.0;
332 }
333 if (nn > nmax) {
334 break;
335 }
336 } while (G4UniformRand() * f1_max > f1);
337
338 // Generate psi by rejection method
339 G4double f2_max = 1. - 2. * x_pm * (1. - 4. * t * (1. - t));
340 G4double f2;
341 do {
342 ++nn;
343 psi = CLHEP::twopi * G4UniformRand();
344 f2 =
345 1. - 2. * x_pm + 4. * x_pm * t * (1. - t) * (1. + std::cos(2. * psi));
346 if (f2 < 0 || f2 > f2_max) {
347 f2 = 0.0;
348 }
349 if (nn >= nmax) {
350 break;
351 }
352 } while (G4UniformRand() * f2_max > f2);
353
354 // Generate rho by direct transformation
355 G4double c2_term1 = gamma_fcp_inv / (2. * x_pm * t);
356 G4double c22 = c2_term1 * c2_term1 + c2_term2 * c2_term2;
357 G4double c2 = 4. * c22 * c22 / std::sqrt(x_pm);
358 G4double rhomax = (1. / t - 1.) * 1.9 / a027;
359 G4double beta = G4Log((c2 + rhomax * rhomax * rhomax * rhomax) / c2);
360 rho = G4Exp(G4Log(c2 * (G4Exp(beta * G4UniformRand()) - 1.)) * 0.25);
361
362 // Get kinematical variables from t and psi
363 G4double u = std::sqrt(1. / t - 1.);
364 G4double xi_half = 0.5 * rho * std::cos(psi);
365 phi_half = 0.5 * rho / u * std::sin(psi);
366
367 theta_plus = gamma_fcp_inv * (u + xi_half) / x_plus;
368 theta_minus = gamma_fcp_inv * (u - xi_half) / x_minus;
369
370 // Protection against infinite loop
371 if (nn > nmax) {
372 if (std::abs(theta_plus) > CLHEP::pi) {
373 theta_plus = 0.0;
374 }
375 if (std::abs(theta_minus) > CLHEP::pi) {
376 theta_minus = 0.0;
377 }
378 }
379 } while (std::abs(theta_plus) > CLHEP::pi ||
380 std::abs(theta_minus) > CLHEP::pi);
381
382 // Construct the momentum vectors
383 // Azimuthal symmetry: take phi0 random between 0 and 2pi
384 G4double phi0 = CLHEP::twopi * G4UniformRand();
385 G4double e_plus = x_plus * egam;
386 G4double e_minus = x_minus * egam;
387
388 // fcp+ fcp- directions for A' in z-direction
389 G4ThreeVector fcp_plus_direction(
390 std::sin(theta_plus) * std::cos(phi0 + phi_half),
391 std::sin(theta_plus) * std::sin(phi0 + phi_half), std::cos(theta_plus));
392 G4ThreeVector fcp_minus_direction(
393 -std::sin(theta_minus) * std::cos(phi0 - phi_half),
394 -std::sin(theta_minus) * std::sin(phi0 - phi_half),
395 std::cos(theta_minus));
396
397 // Rotate to actual A' direction
398 fcp_plus_direction.rotateUz(a_prime_direction);
399 fcp_minus_direction.rotateUz(a_prime_direction);
400
401 // Create fcp+ secondary
402 auto a_particle1 =
403 new G4DynamicParticle(the_fcp_plus_, fcp_plus_direction, e_plus - mfcp_);
404 aParticleChange.AddSecondary(a_particle1);
405
406 ldmx_log(debug) << "Created FCP+ with:";
407 ldmx_log(debug) << " - Kinetic energy: " << (e_plus - mfcp_) / CLHEP::MeV
408 << " MeV";
409 ldmx_log(debug) << " - Total energy: " << e_plus / CLHEP::MeV << " MeV";
410 ldmx_log(debug) << " - Direction: " << fcp_plus_direction;
411
412 // Create fcp- secondary
413 auto a_particle2 = new G4DynamicParticle(the_fcp_minus_, fcp_minus_direction,
414 e_minus - mfcp_);
415 aParticleChange.AddSecondary(a_particle2);
416
417 ldmx_log(debug) << "Created FCP- with:";
418 ldmx_log(debug) << " - Kinetic energy: " << (e_minus - mfcp_) / CLHEP::MeV
419 << " MeV";
420 ldmx_log(debug) << " - Total energy: " << e_minus / CLHEP::MeV << " MeV";
421 ldmx_log(debug) << " - Direction: " << fcp_minus_direction;
422
423 ldmx_log(debug) << "=== A' -> fcp+ fcp- CONVERSION COMPLETE ===";
424
425 return G4VDiscreteProcess::PostStepDoIt(aTrack, aStep);
426} // end of PostStepDoIt
427
429 const G4DynamicParticle* aDynamicAPrime, const G4Material* aMaterial) {
430 const std::size_t number_of_elements = aMaterial->GetNumberOfElements();
431 const G4ElementVector* the_element_vector = aMaterial->GetElementVector();
432 const G4Element* elm = (*the_element_vector)[0];
433
434 if (number_of_elements > 1) {
435 G4double e = std::max(aDynamicAPrime->GetKineticEnergy(), limit_energy_);
436 const G4double* natom = aMaterial->GetVecNbOfAtomsPerVolume();
437
438 G4double sum = 0.;
439 for (std::size_t i = 0; i < number_of_elements; ++i) {
440 elm = (*the_element_vector)[i];
441 sum += natom[i] * computeCrossSectionPerAtom(e, G4lrint(elm->GetZ()));
442 temp_[i] = sum;
443 }
444 sum *= G4UniformRand();
445 for (std::size_t i = 0; i < number_of_elements; ++i) {
446 if (sum <= temp_[i]) {
447 elm = (*the_element_vector)[i];
448 break;
449 }
450 }
451 }
452 return elm;
453}
454
456 G4cout << G4endl << GetProcessName() << ": "
457 << "A' -> fcp+ fcp- Bethe-Heitler-like process" << G4endl;
458 G4cout << " good cross section parametrization from "
459 << G4BestUnit(lowest_energy_limit_, "Energy") << " to "
460 << highest_energy_limit_ / GeV << " GeV for all Z." << G4endl;
461 G4cout << " cross section factor: " << cross_sec_factor_ << G4endl;
462}
463
464} // namespace simcore
A' -> fcp+ fcp- conversion in a nuclear field.
G4double getCrossSectionPerAtom(const G4DynamicParticle *aDynamicAPrime, const G4Element *anElement)
Return the total cross section per atom.
const G4ParticleDefinition * the_fcp_minus_
Pointer to fcp- definition.
G4double cross_sec_factor_
Factor to artificially scale the cross section.
const G4Element * selectRandomAtom(const G4DynamicParticle *aDynamicAPrime, const G4Material *aMaterial)
Select a random atom from the material, weighted by cross section.
G4VParticleChange * PostStepDoIt(const G4Track &aTrack, const G4Step &aStep) override
Generate the final state: A' -> fcp+ fcp-.
G4bool IsApplicable(const G4ParticleDefinition &particle) override
Check if this process applies to the given particle.
APrimeConversionToFCPs(const G4String &processName="APrimeToFCPPair", G4ProcessType type=fElectromagnetic)
Constructor.
G4double computeCrossSectionPerAtom(G4double APrimeEnergy, G4int Z)
Compute the cross section per atom for the given A' energy and Z.
G4double rc_
Coupling-scaled reduced Compton wavelength: q^2 * elm_coupling / Mfcp.
static const std::string PROCESS_NAME
The name of this process.
void printInfoDefinition()
Print process information.
G4double lowest_energy_limit_
Absolute low energy limit of the model (2 * Mfcp)
void setCrossSecFactor(G4double fac)
Set a factor to artificially scale the cross section.
G4double GetMeanFreePath(const G4Track &aTrack, G4double previousStepSize, G4ForceCondition *condition) override
Return the mean free path of the A' in the current material.
G4double highest_energy_limit_
High energy limit of the model.
G4double computeMeanFreePath(G4double APrimeEnergy, const G4Material *aMaterial)
Compute the mean free path for the given energy and material.
const G4ParticleDefinition * the_fcp_plus_
Pointer to fcp+ definition.
void BuildPhysicsTable(const G4ParticleDefinition &p) override
Build physics table (called during initialization).
const G4ParticleDefinition * the_a_prime_
Pointer to the A' particle definition.
G4double limit_energy_
Energy limit for accurate cross section (5 * Mfcp)
std::vector< G4double > temp_
Temporary vector for element selection.
Encapsulates user defined information associated with a Geant4 event.
Dynamically loadable photonuclear models either from SimCore or external libraries implementing this ...