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