Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-01-18 09:11:26

0001 // This file is part of the ACTS project.
0002 //
0003 // Copyright (C) 2016 CERN for the benefit of the ACTS project
0004 //
0005 // This Source Code Form is subject to the terms of the Mozilla Public
0006 // License, v. 2.0. If a copy of the MPL was not distributed with this
0007 // file, You can obtain one at https://mozilla.org/MPL/2.0/.
0008 
0009 #include "Acts/Material/Interactions.hpp"
0010 
0011 #include "Acts/Definitions/PdgParticle.hpp"
0012 #include "Acts/Material/Material.hpp"
0013 #include "Acts/Utilities/MathHelpers.hpp"
0014 
0015 #include <cassert>
0016 #include <cmath>
0017 
0018 using namespace Acts::UnitLiterals;
0019 
0020 namespace {
0021 
0022 // values from RPP2018 table 33.1
0023 // electron mass
0024 constexpr float Me = 0.5109989461_MeV;
0025 // Bethe formular prefactor. 1/mol unit is just a factor 1 here.
0026 constexpr float K = 0.307075_MeV * 1_cm * 1_cm;
0027 // Energy scale for plasma energy.
0028 constexpr float PlasmaEnergyScale = 28.816_eV;
0029 
0030 /// Additional derived relativistic quantities.
0031 struct RelativisticQuantities {
0032   float q2OverBeta2 = 0.0f;
0033   float beta2 = 0.0f;
0034   float betaGamma = 0.0f;
0035   float gamma = 0.0f;
0036 
0037   RelativisticQuantities(float mass, float qOverP, float absQ) {
0038     assert((0 < mass) && "Mass must be positive");
0039     assert((qOverP != 0) && "q/p must be non-zero");
0040     assert((absQ > 0) && "Absolute charge must be non-zero and positive");
0041     // beta²/q² = (p/E)²/q² = p²/(q²m² + q²p²) = 1/(q² + (m²(q/p)²)
0042     // q²/beta² = q² + m²(q/p)²
0043     q2OverBeta2 = absQ * absQ + (mass * qOverP) * (mass * qOverP);
0044     assert((q2OverBeta2 >= 0) && "Negative q2OverBeta2");
0045     // 1/p = q/(qp) = (q/p)/q
0046     const float mOverP = mass * std::abs(qOverP / absQ);
0047     const float pOverM = 1.0f / mOverP;
0048     // beta² = p²/E² = p²/(m² + p²) = 1/(1 + (m/p)²)
0049     beta2 = 1.0f / (1.0f + mOverP * mOverP);
0050     assert((beta2 >= 0) && "Negative beta2");
0051     // beta*gamma = (p/sqrt(m² + p²))*(sqrt(m² + p²)/m) = p/m
0052     betaGamma = pOverM;
0053     assert((betaGamma >= 0) && "Negative betaGamma");
0054     // gamma = sqrt(m² + p²)/m = sqrt(1 + (p/m)²)
0055     gamma = Acts::fastHypot(1.0f, pOverM);
0056   }
0057 };
0058 
0059 /// Compute q/p derivative of beta².
0060 inline float deriveBeta2(float qOverP, const RelativisticQuantities& rq) {
0061   return -2 / (qOverP * rq.gamma * rq.gamma);
0062 }
0063 
0064 /// Compute the 2 * mass * (beta * gamma)² mass term.
0065 inline float computeMassTerm(float mass, const RelativisticQuantities& rq) {
0066   return 2 * mass * rq.betaGamma * rq.betaGamma;
0067 }
0068 
0069 /// Compute mass term logarithmic derivative w/ respect to q/p.
0070 inline float logDeriveMassTerm(float qOverP) {
0071   // only need to compute d((beta*gamma)²)/(beta*gamma)²; rest cancels.
0072   return -2 / qOverP;
0073 }
0074 
0075 /// Compute the maximum energy transfer in a single collision.
0076 ///
0077 /// Uses RPP2018 eq. 33.4.
0078 inline float computeWMax(float mass, const RelativisticQuantities& rq) {
0079   const float mfrac = Me / mass;
0080   const float nominator = 2 * Me * rq.betaGamma * rq.betaGamma;
0081   const float denonimator = 1.0f + 2 * rq.gamma * mfrac + mfrac * mfrac;
0082   return nominator / denonimator;
0083 }
0084 
0085 /// Compute WMax logarithmic derivative w/ respect to q/p.
0086 inline float logDeriveWMax(float mass, float qOverP,
0087                            const RelativisticQuantities& rq) {
0088   // this is (q/p) * (beta/q).
0089   // both quantities have the same sign and the product must always be
0090   // positive. we can thus reuse the known (unsigned) quantity (q/beta)².
0091   const float a = std::abs(qOverP / std::sqrt(rq.q2OverBeta2));
0092   // (m² + me²) / me = me (1 + (m/me)²)
0093   const float b = Me * (1.0f + (mass / Me) * (mass / Me));
0094   return -2 * (a * b - 2 + rq.beta2) / (qOverP * (a * b + 2));
0095 }
0096 
0097 /// Compute epsilon energy pre-factor for RPP2018 eq. 33.11.
0098 ///
0099 /// Defined as
0100 ///
0101 ///     (K/2) * (Z/A)*rho * x * (q²/beta²)
0102 ///
0103 /// where (Z/A)*rho is the electron density in the material and x is the
0104 /// traversed length (thickness) of the material.
0105 inline float computeEpsilon(float molarElectronDensity, float thickness,
0106                             const RelativisticQuantities& rq) {
0107   return 0.5f * K * molarElectronDensity * thickness * rq.q2OverBeta2;
0108 }
0109 
0110 /// Compute epsilon logarithmic derivative w/ respect to q/p.
0111 inline float logDeriveEpsilon(float qOverP, const RelativisticQuantities& rq) {
0112   // only need to compute d(q²/beta²)/(q²/beta²); everything else cancels.
0113   return 2 / (qOverP * rq.gamma * rq.gamma);
0114 }
0115 
0116 /// Compute the density correction factor delta/2.
0117 inline float computeDeltaHalf(float meanExitationPotential,
0118                               float molarElectronDensity,
0119                               const RelativisticQuantities& rq) {
0120   /// Uses RPP2018 eq. 33.6 which is only valid for high energies.
0121   // only relevant for very high ernergies; use arbitrary cutoff
0122   if (rq.betaGamma < 10.0f) {
0123     return 0.0f;
0124   }
0125   // pre-factor according to RPP2019 table 33.1
0126   const float plasmaEnergy =
0127       PlasmaEnergyScale * std::sqrt(1000.f * molarElectronDensity);
0128   return std::log(rq.betaGamma) +
0129          std::log(plasmaEnergy / meanExitationPotential) - 0.5f;
0130 }
0131 
0132 /// Compute derivative w/ respect to q/p for the density correction.
0133 inline float deriveDeltaHalf(float qOverP, const RelativisticQuantities& rq) {
0134   // original equation is of the form
0135   //     log(beta*gamma) + log(eplasma/I) - 1/2
0136   // which the resulting derivative as
0137   //     d(beta*gamma)/(beta*gamma)
0138   return (rq.betaGamma < 10.0f) ? 0.0f : (-1.0f / qOverP);
0139 }
0140 
0141 /// Convert Landau full-width-half-maximum to an equivalent Gaussian sigma,
0142 ///
0143 /// Full-width-half-maximum for a Gaussian is given as
0144 ///
0145 ///     fwhm = 2 * sqrt(2 * log(2)) * sigma
0146 /// -> sigma = fwhm / (2 * sqrt(2 * log(2)))
0147 ///
0148 inline float convertLandauFwhmToGaussianSigma(float fwhm) {
0149   // return fwhm / (2 * std::sqrt(2 * std::log(2.0f)));
0150   return fwhm * 0.42466090014400953f;
0151 }
0152 
0153 namespace detail {
0154 
0155 inline float computeEnergyLossLandauFwhm(const Acts::MaterialSlab& slab,
0156                                          const RelativisticQuantities& rq) {
0157   // return early in case of vacuum or zero thickness
0158   if (!slab.isValid()) {
0159     return 0.0f;
0160   }
0161 
0162   const float Ne = slab.material().molarElectronDensity();
0163   const float thickness = slab.thickness();
0164   // the Landau-Vavilov fwhm is 4*eps (see RPP2018 fig. 33.7)
0165   return 4 * computeEpsilon(Ne, thickness, rq);
0166 }
0167 
0168 }  // namespace detail
0169 
0170 }  // namespace
0171 
0172 float Acts::computeEnergyLossBethe(const MaterialSlab& slab, float m,
0173                                    float qOverP, float absQ) {
0174   // return early in case of vacuum or zero thickness
0175   if (!slab.isValid()) {
0176     return 0.0f;
0177   }
0178 
0179   const RelativisticQuantities rq{m, qOverP, absQ};
0180   const float I = slab.material().meanExcitationEnergy();
0181   const float Ne = slab.material().molarElectronDensity();
0182   const float thickness = slab.thickness();
0183   const float eps = computeEpsilon(Ne, thickness, rq);
0184   const float dhalf = computeDeltaHalf(I, Ne, rq);
0185   const float u = computeMassTerm(Me, rq);
0186   const float wmax = computeWMax(m, rq);
0187   // uses RPP2018 eq. 33.5 scaled from mass stopping power to linear stopping
0188   // power and multiplied with the material thickness to get a total energy loss
0189   // instead of an energy loss per length.
0190   // the required modification only change the prefactor which becomes
0191   // identical to the prefactor epsilon for the most probable value.
0192   const float running =
0193       std::log(u / I) + std::log(wmax / I) - 2.0f * rq.beta2 - 2.0f * dhalf;
0194   return eps * running;
0195 }
0196 
0197 float Acts::deriveEnergyLossBetheQOverP(const MaterialSlab& slab, float m,
0198                                         float qOverP, float absQ) {
0199   // return early in case of vacuum or zero thickness
0200   if (!slab.isValid()) {
0201     return 0.0f;
0202   }
0203 
0204   const RelativisticQuantities rq{m, qOverP, absQ};
0205   const float I = slab.material().meanExcitationEnergy();
0206   const float Ne = slab.material().molarElectronDensity();
0207   const float thickness = slab.thickness();
0208   const float eps = computeEpsilon(Ne, thickness, rq);
0209   const float dhalf = computeDeltaHalf(I, Ne, rq);
0210   const float u = computeMassTerm(Me, rq);
0211   const float wmax = computeWMax(m, rq);
0212   // original equation is of the form
0213   //
0214   //     eps * (log(u/I) + log(wmax/I) - 2 * beta² - delta)
0215   //     = eps * (log(u) + log(wmax) - 2 * log(I) - 2 * beta² - delta)
0216   //
0217   // with the resulting derivative as
0218   //
0219   //     d(eps) * (log(u/I) + log(wmax/I) - 2 * beta² - delta)
0220   //     + eps * (d(u)/(u) + d(wmax)/(wmax) - 2 * d(beta²) - d(delta))
0221   //
0222   // where we can use d(eps) = eps * (d(eps)/eps) for further simplification.
0223   const float logDerEps = logDeriveEpsilon(qOverP, rq);
0224   const float derDHalf = deriveDeltaHalf(qOverP, rq);
0225   const float logDerU = logDeriveMassTerm(qOverP);
0226   const float logDerWmax = logDeriveWMax(m, qOverP, rq);
0227   const float derBeta2 = deriveBeta2(qOverP, rq);
0228   const float rel = logDerEps * (std::log(u / I) + std::log(wmax / I) -
0229                                  2.0f * rq.beta2 - 2.0f * dhalf) +
0230                     logDerU + logDerWmax - 2.0f * derBeta2 - 2.0f * derDHalf;
0231   return eps * rel;
0232 }
0233 
0234 float Acts::computeEnergyLossLandau(const MaterialSlab& slab, float m,
0235                                     float qOverP, float absQ) {
0236   // return early in case of vacuum or zero thickness
0237   if (!slab.isValid()) {
0238     return 0.0f;
0239   }
0240 
0241   const RelativisticQuantities rq{m, qOverP, absQ};
0242   const float I = slab.material().meanExcitationEnergy();
0243   const float Ne = slab.material().molarElectronDensity();
0244   const float thickness = slab.thickness();
0245   const float eps = computeEpsilon(Ne, thickness, rq);
0246   const float dhalf = computeDeltaHalf(I, Ne, rq);
0247   const float u = computeMassTerm(Me, rq);
0248   // uses RPP2018 eq. 33.12
0249   const float running =
0250       std::log(u / I) + std::log(eps / I) + 0.2f - rq.beta2 - 2 * dhalf;
0251   return eps * running;
0252 }
0253 
0254 float Acts::deriveEnergyLossLandauQOverP(const MaterialSlab& slab, float m,
0255                                          float qOverP, float absQ) {
0256   // return early in case of vacuum or zero thickness
0257   if (!slab.isValid()) {
0258     return 0.0f;
0259   }
0260 
0261   const RelativisticQuantities rq{m, qOverP, absQ};
0262   const float I = slab.material().meanExcitationEnergy();
0263   const float Ne = slab.material().molarElectronDensity();
0264   const float thickness = slab.thickness();
0265   const float eps = computeEpsilon(Ne, thickness, rq);
0266   const float dhalf = computeDeltaHalf(I, Ne, rq);
0267   const float t = computeMassTerm(Me, rq);
0268   // original equation is of the form
0269   //
0270   //     eps * (log(t/I) - log(eps/I) - 0.2 - beta² - delta)
0271   //     = eps * (log(t) - log(eps) - 2*log(I) - 0.2 - beta² - delta)
0272   //
0273   // with the resulting derivative as
0274   //
0275   //     d(eps) * (log(t/I) - log(eps/I) - 0.2 - beta² - delta)
0276   //     + eps * (d(t)/t + d(eps)/eps - d(beta²) - 2*d(delta/2))
0277   //
0278   // where we can use d(eps) = eps * (d(eps)/eps) for further simplification.
0279   const float logDerEps = logDeriveEpsilon(qOverP, rq);
0280   const float derDHalf = deriveDeltaHalf(qOverP, rq);
0281   const float logDerT = logDeriveMassTerm(qOverP);
0282   const float derBeta2 = deriveBeta2(qOverP, rq);
0283   const float rel = logDerEps * (std::log(t / I) + std::log(eps / I) - 0.2f -
0284                                  rq.beta2 - 2 * dhalf) +
0285                     logDerT + logDerEps - derBeta2 - 2 * derDHalf;
0286   return eps * rel;
0287 }
0288 
0289 float Acts::computeEnergyLossLandauSigma(const MaterialSlab& slab, float m,
0290                                          float qOverP, float absQ) {
0291   // return early in case of vacuum or zero thickness
0292   if (!slab.isValid()) {
0293     return 0.0f;
0294   }
0295 
0296   const RelativisticQuantities rq{m, qOverP, absQ};
0297   const float Ne = slab.material().molarElectronDensity();
0298   const float thickness = slab.thickness();
0299   // the Landau-Vavilov fwhm is 4*eps (see RPP2018 fig. 33.7)
0300   const float fwhm = 4 * computeEpsilon(Ne, thickness, rq);
0301   return convertLandauFwhmToGaussianSigma(fwhm);
0302 }
0303 
0304 float Acts::computeEnergyLossLandauFwhm(const MaterialSlab& slab, float m,
0305                                         float qOverP, float absQ) {
0306   const RelativisticQuantities rq{m, qOverP, absQ};
0307   return detail::computeEnergyLossLandauFwhm(slab, rq);
0308 }
0309 
0310 float Acts::computeEnergyLossLandauSigmaQOverP(const MaterialSlab& slab,
0311                                                float m, float qOverP,
0312                                                float absQ) {
0313   const RelativisticQuantities rq{m, qOverP, absQ};
0314   const float fwhm = detail::computeEnergyLossLandauFwhm(slab, rq);
0315   const float sigmaE = convertLandauFwhmToGaussianSigma(fwhm);
0316   //  var(q/p) = (d(q/p)/dE)² * var(E)
0317   // d(q/p)/dE = d/dE (q/sqrt(E²-m²))
0318   //           = q * -(1/2) * 1/p³ * 2E
0319   //           = -q/p² E/p = -(q/p)² * 1/(q*beta) = -(q/p)² * (q/beta) / q²
0320   //  var(q/p) = (q/p)^4 * (q/beta)² * (1/q)^4 * var(E)
0321   //           = (1/p)^4 * (q/beta)² * var(E)
0322   // do not need to care about the sign since it is only used squared
0323   const float pInv = qOverP / absQ;
0324   const float qOverBeta = std::sqrt(rq.q2OverBeta2);
0325   return qOverBeta * pInv * pInv * sigmaE;
0326 }
0327 
0328 namespace {
0329 
0330 /// Compute mean energy loss from bremsstrahlung per radiation length.
0331 inline float computeBremsstrahlungLossMean(float mass, float energy) {
0332   return energy * (Me / mass) * (Me / mass);
0333 }
0334 
0335 /// Derivative of the bremsstrahlung loss per rad length with respect to energy.
0336 inline float deriveBremsstrahlungLossMeanE(float mass) {
0337   return (Me / mass) * (Me / mass);
0338 }
0339 
0340 /// Expansion coefficients for the muon radiative loss as a function of energy
0341 ///
0342 /// Taken from ATL-SOFT-PUB-2008-003 eq. 7,8 where the expansion is expressed
0343 /// with terms E^n/X0 with fixed units [E] = MeV and [X0] = mm. The evaluated
0344 /// expansion has units MeV/mm. In this implementation, the X0 dependence is
0345 /// factored out and the coefficients must be scaled to the native units such
0346 /// that the evaluated expansion with terms E^n has dimension energy in
0347 /// native units.
0348 constexpr float MuonHighLowThreshold = 1_TeV;
0349 // [low0 / X0] = MeV / mm -> [low0] = MeV
0350 constexpr double MuonLow0 = -0.5345_MeV;
0351 // [low1 * E / X0] = MeV / mm -> [low1] = 1
0352 constexpr double MuonLow1 = 6.803e-5;
0353 // [low2 * E^2 / X0] = MeV / mm -> [low2] = 1/MeV
0354 constexpr double MuonLow2 = 2.278e-11 / 1_MeV;
0355 // [low3 * E^3 / X0] = MeV / mm -> [low3] = 1/MeV^2
0356 constexpr double MuonLow3 = -9.899e-18 / (1_MeV * 1_MeV);
0357 // units are the same as low0
0358 constexpr double MuonHigh0 = -2.986_MeV;
0359 // units are the same as low1
0360 constexpr double MuonHigh1 = 9.253e-5;
0361 
0362 /// Compute additional radiation energy loss for muons per radiation length.
0363 inline float computeMuonDirectPairPhotoNuclearLossMean(double energy) {
0364   if (energy < MuonHighLowThreshold) {
0365     return MuonLow0 +
0366            (MuonLow1 + (MuonLow2 + MuonLow3 * energy) * energy) * energy;
0367   } else {
0368     return MuonHigh0 + MuonHigh1 * energy;
0369   }
0370 }
0371 
0372 /// Derivative of the additional rad loss per rad length with respect to energy.
0373 inline float deriveMuonDirectPairPhotoNuclearLossMeanE(double energy) {
0374   if (energy < MuonHighLowThreshold) {
0375     return MuonLow1 + (2 * MuonLow2 + 3 * MuonLow3 * energy) * energy;
0376   } else {
0377     return MuonHigh1;
0378   }
0379 }
0380 
0381 }  // namespace
0382 
0383 float Acts::computeEnergyLossRadiative(const MaterialSlab& slab,
0384                                        PdgParticle absPdg, float m,
0385                                        float qOverP, float absQ) {
0386   assert((absPdg == Acts::makeAbsolutePdgParticle(absPdg)) &&
0387          "pdg is not absolute");
0388 
0389   // return early in case of vacuum or zero thickness
0390   if (!slab.isValid()) {
0391     return 0.0f;
0392   }
0393 
0394   // relative radiation length
0395   const float x = slab.thicknessInX0();
0396   // particle momentum and energy
0397   // do not need to care about the sign since it is only used squared
0398   const float momentum = absQ / qOverP;
0399   const float energy = fastHypot(m, momentum);
0400 
0401   float dEdx = computeBremsstrahlungLossMean(m, energy);
0402 
0403   // muon- or muon+
0404   // TODO magic number 8_GeV
0405   if ((absPdg == PdgParticle::eMuon) && (8_GeV < energy)) {
0406     dEdx += computeMuonDirectPairPhotoNuclearLossMean(energy);
0407   }
0408   // scale from energy loss per unit radiation length to total energy
0409   return dEdx * x;
0410 }
0411 
0412 float Acts::deriveEnergyLossRadiativeQOverP(const MaterialSlab& slab,
0413                                             PdgParticle absPdg, float m,
0414                                             float qOverP, float absQ) {
0415   assert((absPdg == Acts::makeAbsolutePdgParticle(absPdg)) &&
0416          "pdg is not absolute");
0417 
0418   // return early in case of vacuum or zero thickness
0419   if (!slab.isValid()) {
0420     return 0.0f;
0421   }
0422 
0423   // relative radiation length
0424   const float x = slab.thicknessInX0();
0425   // particle momentum and energy
0426   // do not need to care about the sign since it is only used squared
0427   const float momentum = absQ / qOverP;
0428   const float energy = fastHypot(m, momentum);
0429 
0430   // compute derivative w/ respect to energy.
0431   float derE = deriveBremsstrahlungLossMeanE(m);
0432 
0433   // muon- or muon+
0434   // TODO magic number 8_GeV
0435   if ((absPdg == PdgParticle::eMuon) && (8_GeV < energy)) {
0436     derE += deriveMuonDirectPairPhotoNuclearLossMeanE(energy);
0437   }
0438   // compute derivative w/ respect to q/p by using the chain rule
0439   //     df(E)/d(q/p) = df(E)/dE dE/d(q/p)
0440   // with
0441   //     E = sqrt(m² + p²) = sqrt(m² + q²/(q/p)²)
0442   // and the resulting derivative
0443   //     dE/d(q/p) = -q² / ((q/p)³ * E)
0444   const float derQOverP = -(absQ * absQ) / (qOverP * qOverP * qOverP * energy);
0445   return derE * derQOverP * x;
0446 }
0447 
0448 float Acts::computeEnergyLossMean(const MaterialSlab& slab, PdgParticle absPdg,
0449                                   float m, float qOverP, float absQ) {
0450   return computeEnergyLossBethe(slab, m, qOverP, absQ) +
0451          computeEnergyLossRadiative(slab, absPdg, m, qOverP, absQ);
0452 }
0453 
0454 float Acts::deriveEnergyLossMeanQOverP(const MaterialSlab& slab,
0455                                        PdgParticle absPdg, float m,
0456                                        float qOverP, float absQ) {
0457   return deriveEnergyLossBetheQOverP(slab, m, qOverP, absQ) +
0458          deriveEnergyLossRadiativeQOverP(slab, absPdg, m, qOverP, absQ);
0459 }
0460 
0461 float Acts::computeEnergyLossMode(const MaterialSlab& slab, PdgParticle absPdg,
0462                                   float m, float qOverP, float absQ) {
0463   // see ATL-SOFT-PUB-2008-003 section 3 for the relative fractions
0464   // TODO this is inconsistent with the text of the note
0465   return 0.9f * computeEnergyLossLandau(slab, m, qOverP, absQ) +
0466          0.15f * computeEnergyLossRadiative(slab, absPdg, m, qOverP, absQ);
0467 }
0468 
0469 float Acts::deriveEnergyLossModeQOverP(const MaterialSlab& slab,
0470                                        PdgParticle absPdg, float m,
0471                                        float qOverP, float absQ) {
0472   // see ATL-SOFT-PUB-2008-003 section 3 for the relative fractions
0473   // TODO this is inconsistent with the text of the note
0474   return 0.9f * deriveEnergyLossLandauQOverP(slab, m, qOverP, absQ) +
0475          0.15f * deriveEnergyLossRadiativeQOverP(slab, absPdg, m, qOverP, absQ);
0476 }
0477 
0478 namespace {
0479 
0480 /// Multiple scattering theta0 for minimum ionizing particles.
0481 inline float theta0Highland(float xOverX0, float momentumInv,
0482                             float q2OverBeta2) {
0483   // RPP2018 eq. 33.15 (treats beta and q² consistently)
0484   const float t = std::sqrt(xOverX0 * q2OverBeta2);
0485   // log((x/X0) * (q²/beta²)) = log((sqrt(x/X0) * (q/beta))²)
0486   //                          = 2 * log(sqrt(x/X0) * (q/beta))
0487   return 13.6_MeV * momentumInv * t * (1.0f + 0.038f * 2 * std::log(t));
0488 }
0489 
0490 /// Multiple scattering theta0 for electrons.
0491 inline float theta0RossiGreisen(float xOverX0, float momentumInv,
0492                                 float q2OverBeta2) {
0493   // TODO add source paper/ resource
0494   const float t = std::sqrt(xOverX0 * q2OverBeta2);
0495   return 17.5_MeV * momentumInv * t *
0496          (1.0f + 0.125f * std::log10(10.0f * xOverX0));
0497 }
0498 
0499 }  // namespace
0500 
0501 float Acts::computeMultipleScatteringTheta0(const MaterialSlab& slab,
0502                                             PdgParticle absPdg, float m,
0503                                             float qOverP, float absQ) {
0504   assert((absPdg == Acts::makeAbsolutePdgParticle(absPdg)) &&
0505          "pdg is not absolute");
0506 
0507   // return early in case of vacuum or zero thickness
0508   if (!slab.isValid()) {
0509     return 0.0f;
0510   }
0511 
0512   // relative radiation length
0513   const float xOverX0 = slab.thicknessInX0();
0514   // 1/p = q/(pq) = (q/p)/q
0515   const float momentumInv = std::abs(qOverP / absQ);
0516   // q²/beta²; a smart compiler should be able to remove the unused computations
0517   const float q2OverBeta2 = RelativisticQuantities(m, qOverP, absQ).q2OverBeta2;
0518 
0519   // electron or positron
0520   if (absPdg == PdgParticle::eElectron) {
0521     return theta0RossiGreisen(xOverX0, momentumInv, q2OverBeta2);
0522   } else {
0523     return theta0Highland(xOverX0, momentumInv, q2OverBeta2);
0524   }
0525 }