Analysis Software
Documentation for sPHENIX simulation software
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Groups Pages
Interactions.cpp
Go to the documentation of this file. Or view the newest version in sPHENIX GitHub for file Interactions.cpp
1 // This file is part of the Acts project.
2 //
3 // Copyright (C) 2019-2020 CERN for the benefit of the Acts project
4 //
5 // This Source Code Form is subject to the terms of the Mozilla Public
6 // License, v. 2.0. If a copy of the MPL was not distributed with this
7 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 
10 
13 
14 #include <cassert>
15 #include <cmath>
16 
17 using namespace Acts::UnitLiterals;
18 
19 namespace {
20 
21 // values from RPP2018 table 33.1
22 // electron mass
23 constexpr float Me = 0.5109989461_MeV;
24 // Bethe formular prefactor. 1/mol unit is just a factor 1 here.
25 constexpr float K = 0.307075_MeV * 1_cm * 1_cm;
26 // Energy scale for plasma energy.
27 constexpr float PlasmaEnergyScale = 28.816_eV;
28 
30 struct RelativisticQuantities {
31  float q2OverBeta2 = 0.0f;
32  float beta2 = 0.0f;
33  float betaGamma = 0.0f;
34  float gamma = 0.0f;
35 
36  RelativisticQuantities(float mass, float qOverP, float absQ) {
37  assert((0 < mass) and "Mass must be positive");
38  assert((qOverP != 0) and "q/p must be non-zero");
39  assert((absQ > 0) and "Absolute charge must be non-zero and positive");
40  // beta²/q² = (p/E)²/q² = p²/(q²m² + q²p²) = 1/(q² + (m²(q/p)²)
41  // q²/beta² = q² + m²(q/p)²
42  q2OverBeta2 = absQ * absQ + (mass * qOverP) * (mass * qOverP);
43  assert((q2OverBeta2 >= 0) && "Negative q2OverBeta2");
44  // 1/p = q/(qp) = (q/p)/q
45  const float mOverP = mass * std::abs(qOverP / absQ);
46  const float pOverM = 1.0f / mOverP;
47  // beta² = p²/E² = p²/(m² + p²) = 1/(1 + (m/p)²)
48  beta2 = 1.0f / (1.0f + mOverP * mOverP);
49  assert((beta2 >= 0) && "Negative beta2");
50  // beta*gamma = (p/sqrt(m² + p²))*(sqrt(m² + p²)/m) = p/m
51  betaGamma = pOverM;
52  assert((betaGamma >= 0) && "Negative betaGamma");
53  // gamma = sqrt(m² + p²)/m = sqrt(1 + (p/m)²)
54  gamma = std::sqrt(1.0f + pOverM * pOverM);
55  }
56 };
57 
59 inline float deriveBeta2(float qOverP, const RelativisticQuantities& rq) {
60  return -2 / (qOverP * rq.gamma * rq.gamma);
61 }
62 
64 inline float computeMassTerm(float mass, const RelativisticQuantities& rq) {
65  return 2 * mass * rq.betaGamma * rq.betaGamma;
66 }
67 
69 inline float logDeriveMassTerm(float qOverP) {
70  // only need to compute d((beta*gamma)²)/(beta*gamma)²; rest cancels.
71  return -2 / qOverP;
72 }
73 
77 inline float computeWMax(float mass, const RelativisticQuantities& rq) {
78  const float mfrac = Me / mass;
79  const float nominator = 2 * Me * rq.betaGamma * rq.betaGamma;
80  const float denonimator = 1.0f + 2 * rq.gamma * mfrac + mfrac * mfrac;
81  return nominator / denonimator;
82 }
83 
85 inline float logDeriveWMax(float mass, float qOverP,
86  const RelativisticQuantities& rq) {
87  // this is (q/p) * (beta/q).
88  // both quantities have the same sign and the product must always be
89  // positive. we can thus reuse the known (unsigned) quantity (q/beta)².
90  const float a = std::abs(qOverP / std::sqrt(rq.q2OverBeta2));
91  // (m² + me²) / me = me (1 + (m/me)²)
92  const float b = Me * (1.0f + (mass / Me) * (mass / Me));
93  return -2 * (a * b - 2 + rq.beta2) / (qOverP * (a * b + 2));
94 }
95 
104 inline float computeEpsilon(float molarElectronDensity, float thickness,
105  const RelativisticQuantities& rq) {
106  return 0.5f * K * molarElectronDensity * thickness * rq.q2OverBeta2;
107 }
108 
110 inline float logDeriveEpsilon(float qOverP, const RelativisticQuantities& rq) {
111  // only need to compute d(q²/beta²)/(q²/beta²); everything else cancels.
112  return 2 / (qOverP * rq.gamma * rq.gamma);
113 }
114 
116 inline float computeDeltaHalf(float meanExitationPotential,
117  float molarElectronDensity,
118  const RelativisticQuantities& rq) {
120  // only relevant for very high ernergies; use arbitrary cutoff
121  if (rq.betaGamma < 10.0f) {
122  return 0.0f;
123  }
124  // pre-factor according to RPP2019 table 33.1
125  const float plasmaEnergy =
126  PlasmaEnergyScale * std::sqrt(1000.f * molarElectronDensity);
127  return std::log(rq.betaGamma) +
128  std::log(plasmaEnergy / meanExitationPotential) - 0.5f;
129 }
130 
132 inline float deriveDeltaHalf(float qOverP, const RelativisticQuantities& rq) {
133  // original equation is of the form
134  // log(beta*gamma) + log(eplasma/I) - 1/2
135  // which the resulting derivative as
136  // d(beta*gamma)/(beta*gamma)
137  return (rq.betaGamma < 10.0f) ? 0.0f : (-1.0f / qOverP);
138 }
139 
147 inline float convertLandauFwhmToGaussianSigma(float fwhm) {
148  // return fwhm / (2 * std::sqrt(2 * std::log(2.0f)));
149  return fwhm * 0.42466090014400953f;
150 }
151 
152 namespace detail {
153 
154 inline float computeEnergyLossLandauFwhm(const Acts::MaterialSlab& slab,
155  const RelativisticQuantities& rq) {
156  // return early in case of vacuum or zero thickness
157  if (not slab) {
158  return 0.0f;
159  }
160 
161  const float Ne = slab.material().molarElectronDensity();
162  const float thickness = slab.thickness();
163  // the Landau-Vavilov fwhm is 4*eps (see RPP2018 fig. 33.7)
164  return 4 * computeEpsilon(Ne, thickness, rq);
165 }
166 
167 } // namespace detail
168 
169 } // namespace
170 
171 float Acts::computeEnergyLossBethe(const MaterialSlab& slab, float m,
172  float qOverP, float absQ) {
173  // return early in case of vacuum or zero thickness
174  if (not slab) {
175  return 0.0f;
176  }
177 
178  const RelativisticQuantities rq{m, qOverP, absQ};
179  const float I = slab.material().meanExcitationEnergy();
180  const float Ne = slab.material().molarElectronDensity();
181  const float thickness = slab.thickness();
182  const float eps = computeEpsilon(Ne, thickness, rq);
183  const float dhalf = computeDeltaHalf(I, Ne, rq);
184  const float u = computeMassTerm(Me, rq);
185  const float wmax = computeWMax(m, rq);
186  // uses RPP2018 eq. 33.5 scaled from mass stopping power to linear stopping
187  // power and multiplied with the material thickness to get a total energy loss
188  // instead of an energy loss per length.
189  // the required modification only change the prefactor which becomes
190  // identical to the prefactor epsilon for the most probable value.
191  const float running =
192  std::log(u / I) + std::log(wmax / I) - 2.0f * rq.beta2 - 2.0f * dhalf;
193  return eps * running;
194 }
195 
197  float qOverP, float absQ) {
198  // return early in case of vacuum or zero thickness
199  if (not slab) {
200  return 0.0f;
201  }
202 
203  const RelativisticQuantities rq{m, qOverP, absQ};
204  const float I = slab.material().meanExcitationEnergy();
205  const float Ne = slab.material().molarElectronDensity();
206  const float thickness = slab.thickness();
207  const float eps = computeEpsilon(Ne, thickness, rq);
208  const float dhalf = computeDeltaHalf(I, Ne, rq);
209  const float u = computeMassTerm(Me, rq);
210  const float wmax = computeWMax(m, rq);
211  // original equation is of the form
212  //
213  // eps * (log(u/I) + log(wmax/I) - 2 * beta² - delta)
214  // = eps * (log(u) + log(wmax) - 2 * log(I) - 2 * beta² - delta)
215  //
216  // with the resulting derivative as
217  //
218  // d(eps) * (log(u/I) + log(wmax/I) - 2 * beta² - delta)
219  // + eps * (d(u)/(u) + d(wmax)/(wmax) - 2 * d(beta²) - d(delta))
220  //
221  // where we can use d(eps) = eps * (d(eps)/eps) for further simplification.
222  const float logDerEps = logDeriveEpsilon(qOverP, rq);
223  const float derDHalf = deriveDeltaHalf(qOverP, rq);
224  const float logDerU = logDeriveMassTerm(qOverP);
225  const float logDerWmax = logDeriveWMax(m, qOverP, rq);
226  const float derBeta2 = deriveBeta2(qOverP, rq);
227  const float rel = logDerEps * (std::log(u / I) + std::log(wmax / I) -
228  2.0f * rq.beta2 - 2.0f * dhalf) +
229  logDerU + logDerWmax - 2.0f * derBeta2 - 2.0f * derDHalf;
230  return eps * rel;
231 }
232 
234  float qOverP, float absQ) {
235  // return early in case of vacuum or zero thickness
236  if (not slab) {
237  return 0.0f;
238  }
239 
240  const RelativisticQuantities rq{m, qOverP, absQ};
241  const float I = slab.material().meanExcitationEnergy();
242  const float Ne = slab.material().molarElectronDensity();
243  const float thickness = slab.thickness();
244  const float eps = computeEpsilon(Ne, thickness, rq);
245  const float dhalf = computeDeltaHalf(I, Ne, rq);
246  const float u = computeMassTerm(Me, rq);
247  // uses RPP2018 eq. 33.12
248  const float running =
249  std::log(u / I) + std::log(eps / I) + 0.2f - rq.beta2 - 2 * dhalf;
250  return eps * running;
251 }
252 
254  float qOverP, float absQ) {
255  // return early in case of vacuum or zero thickness
256  if (not slab) {
257  return 0.0f;
258  }
259 
260  const RelativisticQuantities rq{m, qOverP, absQ};
261  const float I = slab.material().meanExcitationEnergy();
262  const float Ne = slab.material().molarElectronDensity();
263  const float thickness = slab.thickness();
264  const float eps = computeEpsilon(Ne, thickness, rq);
265  const float dhalf = computeDeltaHalf(I, Ne, rq);
266  const float t = computeMassTerm(Me, rq);
267  // original equation is of the form
268  //
269  // eps * (log(t/I) - log(eps/I) - 0.2 - beta² - delta)
270  // = eps * (log(t) - log(eps) - 2*log(I) - 0.2 - beta² - delta)
271  //
272  // with the resulting derivative as
273  //
274  // d(eps) * (log(t/I) - log(eps/I) - 0.2 - beta² - delta)
275  // + eps * (d(t)/t + d(eps)/eps - d(beta²) - 2*d(delta/2))
276  //
277  // where we can use d(eps) = eps * (d(eps)/eps) for further simplification.
278  const float logDerEps = logDeriveEpsilon(qOverP, rq);
279  const float derDHalf = deriveDeltaHalf(qOverP, rq);
280  const float logDerT = logDeriveMassTerm(qOverP);
281  const float derBeta2 = deriveBeta2(qOverP, rq);
282  const float rel = logDerEps * (std::log(t / I) + std::log(eps / I) - 0.2f -
283  rq.beta2 - 2 * dhalf) +
284  logDerT + logDerEps - derBeta2 - 2 * derDHalf;
285  return eps * rel;
286 }
287 
289  float qOverP, float absQ) {
290  // return early in case of vacuum or zero thickness
291  if (not slab) {
292  return 0.0f;
293  }
294 
295  const RelativisticQuantities rq{m, qOverP, absQ};
296  const float Ne = slab.material().molarElectronDensity();
297  const float thickness = slab.thickness();
298  // the Landau-Vavilov fwhm is 4*eps (see RPP2018 fig. 33.7)
299  const float fwhm = 4 * computeEpsilon(Ne, thickness, rq);
300  return convertLandauFwhmToGaussianSigma(fwhm);
301 }
302 
304  float qOverP, float absQ) {
305  const RelativisticQuantities rq{m, qOverP, absQ};
306  return detail::computeEnergyLossLandauFwhm(slab, rq);
307 }
308 
310  float m, float qOverP,
311  float absQ) {
312  const RelativisticQuantities rq{m, qOverP, absQ};
313  const float fwhm = detail::computeEnergyLossLandauFwhm(slab, rq);
314  const float sigmaE = convertLandauFwhmToGaussianSigma(fwhm);
315  // var(q/p) = (d(q/p)/dE)² * var(E)
316  // d(q/p)/dE = d/dE (q/sqrt(E²-m²))
317  // = q * -(1/2) * 1/p³ * 2E
318  // = -q/p² E/p = -(q/p)² * 1/(q*beta) = -(q/p)² * (q/beta) / q²
319  // var(q/p) = (q/p)^4 * (q/beta)² * (1/q)^4 * var(E)
320  // = (1/p)^4 * (q/beta)² * var(E)
321  // do not need to care about the sign since it is only used squared
322  const float pInv = qOverP / absQ;
323  const float qOverBeta = std::sqrt(rq.q2OverBeta2);
324  return qOverBeta * pInv * pInv * sigmaE;
325 }
326 
327 namespace {
328 
330 inline float computeBremsstrahlungLossMean(float mass, float energy) {
331  return energy * (Me / mass) * (Me / mass);
332 }
333 
335 inline float deriveBremsstrahlungLossMeanE(float mass) {
336  return (Me / mass) * (Me / mass);
337 }
338 
347 constexpr float MuonHighLowThreshold = 1_TeV;
348 // [low0 / X0] = MeV / mm -> [low0] = MeV
349 constexpr double MuonLow0 = -0.5345_MeV;
350 // [low1 * E / X0] = MeV / mm -> [low1] = 1
351 constexpr double MuonLow1 = 6.803e-5;
352 // [low2 * E^2 / X0] = MeV / mm -> [low2] = 1/MeV
353 constexpr double MuonLow2 = 2.278e-11 / 1_MeV;
354 // [low3 * E^3 / X0] = MeV / mm -> [low3] = 1/MeV^2
355 constexpr double MuonLow3 = -9.899e-18 / (1_MeV * 1_MeV);
356 // units are the same as low0
357 constexpr double MuonHigh0 = -2.986_MeV;
358 // units are the same as low1
359 constexpr double MuonHigh1 = 9.253e-5;
360 
362 inline float computeMuonDirectPairPhotoNuclearLossMean(double energy) {
363  if (energy < MuonHighLowThreshold) {
364  return MuonLow0 +
365  (MuonLow1 + (MuonLow2 + MuonLow3 * energy) * energy) * energy;
366  } else {
367  return MuonHigh0 + MuonHigh1 * energy;
368  }
369 }
370 
372 inline float deriveMuonDirectPairPhotoNuclearLossMeanE(double energy) {
373  if (energy < MuonHighLowThreshold) {
374  return MuonLow1 + (2 * MuonLow2 + 3 * MuonLow3 * energy) * energy;
375  } else {
376  return MuonHigh1;
377  }
378 }
379 
380 } // namespace
381 
383  PdgParticle absPdg, float m,
384  float qOverP, float absQ) {
385  assert((absPdg == Acts::makeAbsolutePdgParticle(absPdg)) &&
386  "pdg is not absolute");
387 
388  // return early in case of vacuum or zero thickness
389  if (not slab) {
390  return 0.0f;
391  }
392 
393  // relative radiation length
394  const float x = slab.thicknessInX0();
395  // particle momentum and energy
396  // do not need to care about the sign since it is only used squared
397  const float momentum = absQ / qOverP;
398  const float energy = std::hypot(m, momentum);
399 
400  float dEdx = computeBremsstrahlungLossMean(m, energy);
401 
402  // muon- or muon+
403  // TODO magic number 8_GeV
404  if ((absPdg == PdgParticle::eMuon) and (8_GeV < energy)) {
405  dEdx += computeMuonDirectPairPhotoNuclearLossMean(energy);
406  }
407  // scale from energy loss per unit radiation length to total energy
408  return dEdx * x;
409 }
410 
412  PdgParticle absPdg, float m,
413  float qOverP, float absQ) {
414  assert((absPdg == Acts::makeAbsolutePdgParticle(absPdg)) &&
415  "pdg is not absolute");
416 
417  // return early in case of vacuum or zero thickness
418  if (not slab) {
419  return 0.0f;
420  }
421 
422  // relative radiation length
423  const float x = slab.thicknessInX0();
424  // particle momentum and energy
425  // do not need to care about the sign since it is only used squared
426  const float momentum = absQ / qOverP;
427  const float energy = std::hypot(m, momentum);
428 
429  // compute derivative w/ respect to energy.
430  float derE = deriveBremsstrahlungLossMeanE(m);
431 
432  // muon- or muon+
433  // TODO magic number 8_GeV
434  if ((absPdg == PdgParticle::eMuon) and (8_GeV < energy)) {
435  derE += deriveMuonDirectPairPhotoNuclearLossMeanE(energy);
436  }
437  // compute derivative w/ respect to q/p by using the chain rule
438  // df(E)/d(q/p) = df(E)/dE dE/d(q/p)
439  // with
440  // E = sqrt(m² + p²) = sqrt(m² + q²/(q/p)²)
441  // and the resulting derivative
442  // dE/d(q/p) = -q² / ((q/p)³ * E)
443  const float derQOverP = -(absQ * absQ) / (qOverP * qOverP * qOverP * energy);
444  return derE * derQOverP * x;
445 }
446 
448  float m, float qOverP, float absQ) {
449  return computeEnergyLossBethe(slab, m, qOverP, absQ) +
450  computeEnergyLossRadiative(slab, absPdg, m, qOverP, absQ);
451 }
452 
454  PdgParticle absPdg, float m,
455  float qOverP, float absQ) {
456  return deriveEnergyLossBetheQOverP(slab, m, qOverP, absQ) +
457  deriveEnergyLossRadiativeQOverP(slab, absPdg, m, qOverP, absQ);
458 }
459 
461  float m, float qOverP, float absQ) {
462  // see ATL-SOFT-PUB-2008-003 section 3 for the relative fractions
463  // TODO this is inconsistent with the text of the note
464  return 0.9f * computeEnergyLossLandau(slab, m, qOverP, absQ) +
465  0.15f * computeEnergyLossRadiative(slab, absPdg, m, qOverP, absQ);
466 }
467 
469  PdgParticle absPdg, float m,
470  float qOverP, float absQ) {
471  // see ATL-SOFT-PUB-2008-003 section 3 for the relative fractions
472  // TODO this is inconsistent with the text of the note
473  return 0.9f * deriveEnergyLossLandauQOverP(slab, m, qOverP, absQ) +
474  0.15f * deriveEnergyLossRadiativeQOverP(slab, absPdg, m, qOverP, absQ);
475 }
476 
477 namespace {
478 
480 inline float theta0Highland(float xOverX0, float momentumInv,
481  float q2OverBeta2) {
482  // RPP2018 eq. 33.15 (treats beta and q² consistently)
483  const float t = std::sqrt(xOverX0 * q2OverBeta2);
484  // log((x/X0) * (q²/beta²)) = log((sqrt(x/X0) * (q/beta))²)
485  // = 2 * log(sqrt(x/X0) * (q/beta))
486  return 13.6_MeV * momentumInv * t * (1.0f + 0.038f * 2 * std::log(t));
487 }
488 
490 inline float theta0RossiGreisen(float xOverX0, float momentumInv,
491  float q2OverBeta2) {
492  // TODO add source paper/ resource
493  const float t = std::sqrt(xOverX0 * q2OverBeta2);
494  return 17.5_MeV * momentumInv * t *
495  (1.0f + 0.125f * std::log10(10.0f * xOverX0));
496 }
497 
498 } // namespace
499 
501  PdgParticle absPdg, float m,
502  float qOverP, float absQ) {
503  assert((absPdg == Acts::makeAbsolutePdgParticle(absPdg)) &&
504  "pdg is not absolute");
505 
506  // return early in case of vacuum or zero thickness
507  if (not slab) {
508  return 0.0f;
509  }
510 
511  // relative radiation length
512  const float xOverX0 = slab.thicknessInX0();
513  // 1/p = q/(pq) = (q/p)/q
514  const float momentumInv = std::abs(qOverP / absQ);
515  // q²/beta²; a smart compiler should be able to remove the unused computations
516  const float q2OverBeta2 = RelativisticQuantities(m, qOverP, absQ).q2OverBeta2;
517 
518  // electron or positron
519  if (absPdg == PdgParticle::eElectron) {
520  return theta0RossiGreisen(xOverX0, momentumInv, q2OverBeta2);
521  } else {
522  return theta0Highland(xOverX0, momentumInv, q2OverBeta2);
523  }
524 }