EIC 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 
12 
13 #include <cassert>
14 #include <cmath>
15 
16 using namespace Acts::UnitLiterals;
17 
18 namespace {
19 
20 // values from RPP2018 table 33.1
21 // electron mass
22 constexpr float Me = 0.5109989461_MeV;
23 // Bethe formular prefactor. 1/mol unit is just a factor 1 here.
24 constexpr float K = 0.307075_MeV * 1_cm * 1_cm;
25 // Energy scale for plasma energy.
26 constexpr float PlasmaEnergyScale = 28.816_eV;
27 
29 struct RelativisticQuantities {
30  float q2OverBeta2 = 0.0f;
31  float beta2 = 0.0f;
32  float betaGamma = 0.0f;
33  float gamma = 0.0f;
34 
35  RelativisticQuantities(float mass, float qOverP, float q) {
36  // beta²/q² = (p/E)²/q² = p²/(q²m² + q²p²) = 1/(q² + (m²(q/p)²)
37  // q²/beta² = q² + m²(q/p)²
38  q2OverBeta2 = q * q + (mass * qOverP) * (mass * qOverP);
39  // 1/p = q/(qp) = (q/p)/q
40  const auto mOverP = mass * std::abs(qOverP / q);
41  const auto pOverM = 1.0f / mOverP;
42  // beta² = p²/E² = p²/(m² + p²) = 1/(1 + (m/p)²)
43  beta2 = 1.0f / (1.0f + mOverP * mOverP);
44  // beta*gamma = (p/sqrt(m² + p²))*(sqrt(m² + p²)/m) = p/m
45  betaGamma = pOverM;
46  // gamma = sqrt(m² + p²)/m = sqrt(1 + (p/m)²)
47  gamma = std::sqrt(1.0f + pOverM * pOverM);
48  }
49 };
50 
52 inline float deriveBeta2(float qOverP, const RelativisticQuantities& rq) {
53  return -2 / (qOverP * rq.gamma * rq.gamma);
54 }
55 
57 inline float computeMassTerm(float mass, const RelativisticQuantities& rq) {
58  return 2 * mass * rq.betaGamma * rq.betaGamma;
59 }
60 
62 inline float logDeriveMassTerm(float qOverP) {
63  // only need to compute d((beta*gamma)²)/(beta*gamma)²; rest cancels.
64  return -2 / qOverP;
65 }
66 
70 inline float computeWMax(float mass, const RelativisticQuantities& rq) {
71  const auto mfrac = Me / mass;
72  const auto nominator = 2 * Me * rq.betaGamma * rq.betaGamma;
73  const auto denonimator = 1.0f + 2 * rq.gamma * mfrac + mfrac * mfrac;
74  return nominator / denonimator;
75 }
76 
78 inline float logDeriveWMax(float mass, float qOverP,
79  const RelativisticQuantities& rq) {
80  // this is (q/p) * (beta/q).
81  // both quantities have the same sign and the product must always be
82  // positive. we can thus reuse the known (unsigned) quantity (q/beta)².
83  const auto a = std::abs(qOverP / std::sqrt(rq.q2OverBeta2));
84  // (m² + me²) / me = me (1 + (m/me)²)
85  const auto b = Me * (1.0f + (mass / Me) * (mass / Me));
86  return -2 * (a * b - 2 + rq.beta2) / (qOverP * (a * b + 2));
87 }
88 
97 inline float computeEpsilon(float molarElectronDensity, float thickness,
98  const RelativisticQuantities& rq) {
99  return 0.5f * K * molarElectronDensity * thickness * rq.q2OverBeta2;
100 }
101 
103 inline float logDeriveEpsilon(float qOverP, const RelativisticQuantities& rq) {
104  // only need to compute d(q²/beta²)/(q²/beta²); everything else cancels.
105  return 2 / (qOverP * rq.gamma * rq.gamma);
106 }
107 
113 inline float computeDeltaHalf(float meanExitationPotential,
114  float molarElectronDensity,
115  const RelativisticQuantities& rq) {
116  // only relevant for very high ernergies; use arbitrary cutoff
117  if (rq.betaGamma < 10.0f) {
118  return 0.0f;
119  }
120  // pre-factor according to RPP2019 table 33.1
121  const auto plasmaEnergy = PlasmaEnergyScale * std::sqrt(molarElectronDensity);
122  return std::log(rq.betaGamma) +
123  std::log(plasmaEnergy / meanExitationPotential) - 0.5f;
124 }
125 
127 inline float deriveDeltaHalf(float qOverP, const RelativisticQuantities& rq) {
128  // original equation is of the form
129  // log(beta*gamma) + log(eplasma/I) - 1/2
130  // which the resulting derivative as
131  // d(beta*gamma)/(beta*gamma)
132  return (rq.betaGamma < 10.0f) ? 0.0f : (-1.0f / qOverP);
133 }
134 
135 } // namespace
136 
137 #define ASSERT_INPUTS(mass, qOverP, q) \
138  assert((0 < mass) and "Mass must be positive"); \
139  assert((qOverP != 0) and "q/p must be non-zero"); \
140  assert((q != 0) and "Charge must be non-zero");
141 
142 float Acts::computeEnergyLossBethe(const MaterialSlab& slab, int /* unused */,
143  float m, float qOverP, float q) {
144  ASSERT_INPUTS(m, qOverP, q)
145 
146  // return early in case of vacuum or zero thickness
147  if (not slab) {
148  return 0.0f;
149  }
150 
151  const auto I = slab.material().meanExcitationEnergy();
152  const auto Ne = slab.material().molarElectronDensity();
153  const auto thickness = slab.thickness();
154  const auto rq = RelativisticQuantities(m, qOverP, q);
155  const auto eps = computeEpsilon(Ne, thickness, rq);
156  const auto dhalf = computeDeltaHalf(I, Ne, rq);
157  const auto u = computeMassTerm(Me, rq);
158  const auto wmax = computeWMax(m, rq);
159  // uses RPP2018 eq. 33.5 scaled from mass stopping power to linear stopping
160  // power and multiplied with the material thickness to get a total energy loss
161  // instead of an energy loss per length.
162  // the required modification only change the prefactor which becomes
163  // identical to the prefactor epsilon for the most probable value.
164  const auto running =
165  0.5f * std::log(u / I) + 0.5f * std::log(wmax / I) - rq.beta2 - dhalf;
166  return eps * running;
167 }
168 
170  int /* unused */, float m, float qOverP,
171  float q) {
172  ASSERT_INPUTS(m, qOverP, q)
173 
174  // return early in case of vacuum or zero thickness
175  if (not slab) {
176  return 0.0f;
177  }
178 
179  const auto I = slab.material().meanExcitationEnergy();
180  const auto Ne = slab.material().molarElectronDensity();
181  const auto thickness = slab.thickness();
182  const auto rq = RelativisticQuantities(m, qOverP, q);
183  const auto eps = computeEpsilon(Ne, thickness, rq);
184  const auto dhalf = computeDeltaHalf(I, Ne, rq);
185  const auto u = computeMassTerm(Me, rq);
186  const auto wmax = computeWMax(m, rq);
187  // original equation is of the form
188  //
189  // eps * (log(u/I)/2 + log(wmax/I)/2 - beta² - delta/2)
190  // = eps * (log(u)/2 + log(wmax/I)/2 - log(I) - beta² - delta/2)
191  //
192  // with the resulting derivative as
193  //
194  // d(eps) * (log(u/I)/2 + log(wmax/I)/2 - beta² - delta/2)
195  // + eps * (d(u)/(2*u) + d(wmax)/(2*wmax) - d(beta²) - d(delta/2))
196  //
197  // where we can use d(eps) = eps * (d(eps)/eps) for further simplification.
198  const auto logDerEps = logDeriveEpsilon(qOverP, rq);
199  const auto derDHalf = deriveDeltaHalf(qOverP, rq);
200  const auto logDerU = logDeriveMassTerm(qOverP);
201  const auto logDerWmax = logDeriveWMax(m, qOverP, rq);
202  const auto derBeta2 = deriveBeta2(qOverP, rq);
203  const auto rel = logDerEps * (0.5f * std::log(u / I) +
204  0.5f * std::log(wmax / I) - rq.beta2 - dhalf) +
205  0.5f * logDerU + 0.5f * logDerWmax - derBeta2 - derDHalf;
206  return eps * rel;
207 }
208 
209 float Acts::computeEnergyLossLandau(const MaterialSlab& slab, int /* unused */,
210  float m, float qOverP, float q) {
211  ASSERT_INPUTS(m, qOverP, q)
212 
213  // return early in case of vacuum or zero thickness
214  if (not slab) {
215  return 0.0f;
216  }
217 
218  const auto I = slab.material().meanExcitationEnergy();
219  const auto Ne = slab.material().molarElectronDensity();
220  const auto thickness = slab.thickness();
221  const auto rq = RelativisticQuantities(m, qOverP, q);
222  const auto eps = computeEpsilon(Ne, thickness, rq);
223  const auto dhalf = computeDeltaHalf(I, Ne, rq);
224  const auto t = computeMassTerm(m, rq);
225  // uses RPP2018 eq. 33.11
226  const auto running =
227  std::log(t / I) + std::log(eps / I) + 0.2f - rq.beta2 - 2 * dhalf;
228  return eps * running;
229 }
230 
232  int /* unused */, float m,
233  float qOverP, float q) {
234  ASSERT_INPUTS(m, qOverP, q)
235 
236  // return early in case of vacuum or zero thickness
237  if (not slab) {
238  return 0.0f;
239  }
240 
241  const auto I = slab.material().meanExcitationEnergy();
242  const auto Ne = slab.material().molarElectronDensity();
243  const auto thickness = slab.thickness();
244  const auto rq = RelativisticQuantities(m, qOverP, q);
245  const auto eps = computeEpsilon(Ne, thickness, rq);
246  const auto dhalf = computeDeltaHalf(I, Ne, rq);
247  const auto t = computeMassTerm(m, rq);
248  // original equation is of the form
249  //
250  // eps * (log(t/I) - log(eps/I) - 0.2 - beta² - delta)
251  // = eps * (log(t) - log(eps) - 2*log(I) - 0.2 - beta² - delta)
252  //
253  // with the resulting derivative as
254  //
255  // d(eps) * (log(t/I) - log(eps/I) - 0.2 - beta² - delta)
256  // + eps * (d(t)/t + d(eps)/eps - d(beta²) - 2*d(delta/2))
257  //
258  // where we can use d(eps) = eps * (d(eps)/eps) for further simplification.
259  const auto logDerEps = logDeriveEpsilon(qOverP, rq);
260  const auto derDHalf = deriveDeltaHalf(qOverP, rq);
261  const auto logDerT = logDeriveMassTerm(qOverP);
262  const auto derBeta2 = deriveBeta2(qOverP, rq);
263  const auto rel = logDerEps * (std::log(t / I) + std::log(eps / I) - 0.2f -
264  rq.beta2 - 2 * dhalf) +
265  logDerT + logDerEps - derBeta2 - 2 * derDHalf;
266  return eps * rel;
267 }
268 
269 namespace {
270 
278 inline float convertLandauFwhmToGaussianSigma(float fwhm) {
279  return fwhm / (2 * std::sqrt(2 * std::log(2.0f)));
280 }
281 
282 } // namespace
283 
285  int /* unused */, float m,
286  float qOverP, float q) {
287  ASSERT_INPUTS(m, qOverP, q)
288 
289  // return early in case of vacuum or zero thickness
290  if (not slab) {
291  return 0.0f;
292  }
293 
294  const auto Ne = slab.material().molarElectronDensity();
295  const auto thickness = slab.thickness();
296  const auto rq = RelativisticQuantities(m, qOverP, q);
297  // the Landau-Vavilov fwhm is 4*eps (see RPP2018 fig. 33.7)
298  const auto fwhm = 4 * computeEpsilon(Ne, thickness, rq);
299  return convertLandauFwhmToGaussianSigma(fwhm);
300 }
301 
303  int /* unused */, float m,
304  float qOverP, float q) {
305  ASSERT_INPUTS(m, qOverP, q)
306 
307  // return early in case of vacuum or zero thickness
308  if (not slab) {
309  return 0.0f;
310  }
311 
312  const auto Ne = slab.material().molarElectronDensity();
313  const auto thickness = slab.thickness();
314  const auto rq = RelativisticQuantities(m, qOverP, q);
315  // the Landau-Vavilov fwhm is 4*eps (see RPP2018 fig. 33.7)
316  const auto fwhm = 4 * computeEpsilon(Ne, thickness, rq);
317  const auto sigmaE = convertLandauFwhmToGaussianSigma(fwhm);
318  // var(q/p) = (d(q/p)/dE)² * var(E)
319  // d(q/p)/dE = d/dE (q/sqrt(E²-m²))
320  // = q * -(1/2) * 1/p³ * 2E
321  // = -q/p² E/p = -(q/p)² * 1/(q*beta) = -(q/p)² * (q/beta) / q²
322  // var(q/p) = (q/p)^4 * (q/beta)² * (1/q)^4 * var(E)
323  // = (1/p)^4 * (q/beta)² * var(E)
324  // do not need to care about the sign since it is only used squared
325  const auto pInv = qOverP / q;
326  return std::sqrt(rq.q2OverBeta2) * pInv * pInv * sigmaE;
327 }
328 
329 namespace {
330 
332 inline float computeBremsstrahlungLossMean(float mass, float energy) {
333  return energy * (Me / mass) * (Me / mass);
334 }
335 
337 inline float deriveBremsstrahlungLossMeanE(float mass) {
338  return (Me / mass) * (Me / mass);
339 }
340 
349 constexpr float MuonHighLowThreshold = 1_TeV;
350 // [low0 / X0] = MeV / mm -> [low0] = MeV
351 constexpr double MuonLow0 = -0.5345_MeV;
352 // [low1 * E / X0] = MeV / mm -> [low1] = 1
353 constexpr double MuonLow1 = 6.803e-5;
354 // [low2 * E^2 / X0] = MeV / mm -> [low2] = 1/MeV
355 constexpr double MuonLow2 = 2.278e-11 / 1_MeV;
356 // [low3 * E^3 / X0] = MeV / mm -> [low3] = 1/MeV^2
357 constexpr double MuonLow3 = -9.899e-18 / (1_MeV * 1_MeV);
358 // units are the same as low0
359 constexpr double MuonHigh0 = -2.986_MeV;
360 // units are the same as low1
361 constexpr double MuonHigh1 = 9.253e-5;
362 
364 inline float computeMuonDirectPairPhotoNuclearLossMean(double energy) {
365  if (energy < MuonHighLowThreshold) {
366  return MuonLow0 +
367  (MuonLow1 + (MuonLow2 + MuonLow3 * energy) * energy) * energy;
368  } else {
369  return MuonHigh0 + MuonHigh1 * energy;
370  }
371 }
372 
374 inline float deriveMuonDirectPairPhotoNuclearLossMeanE(double energy) {
375  if (energy < MuonHighLowThreshold) {
376  return MuonLow1 + (2 * MuonLow2 + 3 * MuonLow3 * energy) * energy;
377  } else {
378  return MuonHigh1;
379  }
380 }
381 
382 } // namespace
383 
385  float m, float qOverP, float q) {
386  ASSERT_INPUTS(m, qOverP, q)
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 auto 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 auto momentum = q / qOverP;
398  const auto energy = std::sqrt(m * m + momentum * momentum);
399 
400  auto dEdx = computeBremsstrahlungLossMean(m, energy);
401  if (((pdg == PdgParticle::eMuon) or (pdg == PdgParticle::eAntiMuon)) and
402  (8_GeV < energy)) {
403  dEdx += computeMuonDirectPairPhotoNuclearLossMean(energy);
404  }
405  // scale from energy loss per unit radiation length to total energy
406  return dEdx * x;
407 }
408 
410  float m, float qOverP, float q) {
411  ASSERT_INPUTS(m, qOverP, q)
412 
413  // return early in case of vacuum or zero thickness
414  if (not slab) {
415  return 0.0f;
416  }
417 
418  // relative radiation length
419  const auto x = slab.thicknessInX0();
420  // particle momentum and energy
421  // do not need to care about the sign since it is only used squared
422  const auto momentum = q / qOverP;
423  const auto energy = std::sqrt(m * m + momentum * momentum);
424 
425  // compute derivative w/ respect to energy.
426  auto derE = deriveBremsstrahlungLossMeanE(m);
427  if (((pdg == PdgParticle::eMuon) or (pdg == PdgParticle::eAntiMuon)) and
428  (8_GeV < energy)) {
429  derE += deriveMuonDirectPairPhotoNuclearLossMeanE(energy);
430  }
431  // compute derivative w/ respect to q/p by using the chain rule
432  // df(E)/d(q/p) = df(E)/dE dE/d(q/p)
433  // with
434  // E = sqrt(m² + p²) = sqrt(m² + q²/(q/p)²)
435  // and the resulting derivative
436  // dE/d(q/p) = -q² / ((q/p)³ * E)
437  const auto derQOverP = -(q * q) / (qOverP * qOverP * qOverP * energy);
438  return derE * derQOverP * x;
439 }
440 
441 float Acts::computeEnergyLossMean(const MaterialSlab& slab, int pdg, float m,
442  float qOverP, float q) {
443  return computeEnergyLossBethe(slab, pdg, m, qOverP, q) +
444  computeEnergyLossRadiative(slab, pdg, m, qOverP, q);
445 }
446 
448  float m, float qOverP, float q) {
449  return deriveEnergyLossBetheQOverP(slab, pdg, m, qOverP, q) +
450  deriveEnergyLossRadiativeQOverP(slab, pdg, m, qOverP, q);
451 }
452 
453 float Acts::computeEnergyLossMode(const MaterialSlab& slab, int pdg, float m,
454  float qOverP, float q) {
455  // see ATL-SOFT-PUB-2008-003 section 3 for the relative fractions
456  // TODO this is inconsistent with the text of the note
457  return 0.9f * computeEnergyLossLandau(slab, pdg, m, qOverP, q) +
458  0.15f * computeEnergyLossRadiative(slab, pdg, m, qOverP, q);
459 }
460 
462  float m, float qOverP, float q) {
463  // see ATL-SOFT-PUB-2008-003 section 3 for the relative fractions
464  // TODO this is inconsistent with the text of the note
465  return 0.9f * deriveEnergyLossLandauQOverP(slab, pdg, m, qOverP, q) +
466  0.15f * deriveEnergyLossRadiativeQOverP(slab, pdg, m, qOverP, q);
467 }
468 
469 namespace {
470 
472 inline float theta0Highland(float xOverX0, float momentumInv,
473  float q2OverBeta2) {
474  // RPP2018 eq. 33.15 (treats beta and q² consistenly)
475  const auto t = std::sqrt(xOverX0 * q2OverBeta2);
476  // log((x/X0) * (q²/beta²)) = log((sqrt(x/X0) * (q/beta))²)
477  // = 2 * log(sqrt(x/X0) * (q/beta))
478  return 13.6_MeV * momentumInv * t * (1.0f + 0.038f * 2 * std::log(t));
479 }
480 
482 inline float theta0RossiGreisen(float xOverX0, float momentumInv,
483  float q2OverBeta2) {
484  // TODO add source paper/ resource
485  const auto t = std::sqrt(xOverX0 * q2OverBeta2);
486  return 17.5_MeV * momentumInv * t *
487  (1.0f + 0.125f * std::log10(10.0f * xOverX0));
488 }
489 
490 } // namespace
491 
493  float m, float qOverP, float q) {
494  ASSERT_INPUTS(m, qOverP, q)
495 
496  // return early in case of vacuum or zero thickness
497  if (not slab) {
498  return 0.0f;
499  }
500 
501  // relative radiation length
502  const auto xOverX0 = slab.thicknessInX0();
503  // 1/p = q/(pq) = (q/p)/q
504  const auto momentumInv = std::abs(qOverP / q);
505  // q²/beta²; a smart compiler should be able to remove the unused computations
506  const auto q2OverBeta2 = RelativisticQuantities(m, qOverP, q).q2OverBeta2;
507 
508  if ((pdg == PdgParticle::eElectron) or (pdg == PdgParticle::ePositron)) {
509  return theta0RossiGreisen(xOverX0, momentumInv, q2OverBeta2);
510  } else {
511  return theta0Highland(xOverX0, momentumInv, q2OverBeta2);
512  }
513 }