Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-07-11 07:50:13

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