Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-09-17 08:53:36

0001 // Copyright 2021-2024 UT-Battelle, LLC, and other Celeritas developers.
0002 // See the top-level COPYRIGHT file for details.
0003 // SPDX-License-Identifier: (Apache-2.0 OR MIT)
0004 //---------------------------------------------------------------------------//
0005 //! \file celeritas/em/msc/detail/UrbanMscScatter.hh
0006 //---------------------------------------------------------------------------//
0007 #pragma once
0008 
0009 #include <cmath>
0010 
0011 #include "corecel/Macros.hh"
0012 #include "corecel/Types.hh"
0013 #include "corecel/math/Algorithms.hh"
0014 #include "corecel/math/ArrayOperators.hh"
0015 #include "corecel/math/ArrayUtils.hh"
0016 #include "corecel/math/PolyEvaluator.hh"
0017 #include "corecel/random/distribution/BernoulliDistribution.hh"
0018 #include "corecel/random/distribution/GenerateCanonical.hh"
0019 #include "corecel/random/distribution/UniformRealDistribution.hh"
0020 #include "celeritas/Quantities.hh"
0021 #include "celeritas/Types.hh"
0022 #include "celeritas/Units.hh"
0023 #include "celeritas/em/data/UrbanMscData.hh"
0024 #include "celeritas/em/distribution/UrbanLargeAngleDistribution.hh"
0025 #include "celeritas/mat/MaterialView.hh"
0026 #include "celeritas/phys/Interaction.hh"
0027 #include "celeritas/phys/ParticleTrackView.hh"
0028 #include "celeritas/phys/PhysicsTrackView.hh"
0029 
0030 #include "UrbanMscHelper.hh"
0031 #include "UrbanPositronCorrector.hh"
0032 
0033 namespace celeritas
0034 {
0035 namespace detail
0036 {
0037 //---------------------------------------------------------------------------//
0038 /*!
0039  * Sample angular change and lateral displacement with the Urban multiple
0040  * scattering model.
0041 
0042  * \note This code performs the same method as in
0043  * \c G4VMultipleScattering::AlongStepDoIt
0044  * and \c G4UrbanMscModel::SampleScattering of the Geant4 10.7 release.
0045  */
0046 class UrbanMscScatter
0047 {
0048   public:
0049     //!@{
0050     //! \name Type aliases
0051     using Energy = units::MevEnergy;
0052     using Mass = units::MevMass;
0053     using UrbanMscRef = NativeCRef<UrbanMscData>;
0054     //!@}
0055 
0056   public:
0057     // TODO: improve this
0058     static inline CELER_FUNCTION real_type
0059     calc_displacement(real_type true_path, real_type geom_path);
0060 
0061     // Construct with shared and state data
0062     inline CELER_FUNCTION UrbanMscScatter(UrbanMscRef const& shared,
0063                                           UrbanMscHelper const& helper,
0064                                           ParticleTrackView const& particle,
0065                                           PhysicsTrackView const& physics,
0066                                           MaterialView const& material,
0067                                           Real3 const& dir,
0068                                           real_type safety,
0069                                           MscStep const& input);
0070 
0071     // Sample the final true step length, position and direction by msc
0072     template<class Engine>
0073     inline CELER_FUNCTION MscInteraction operator()(Engine& rng);
0074 
0075   private:
0076     //// DATA ////
0077 
0078     // Shared constant data
0079     UrbanMscRef const& shared_;
0080     // Urban MSC material data
0081     UrbanMscMaterialData const& msc_;
0082     // Urban MSC helper class
0083     UrbanMscHelper const& helper_;
0084     // Material data
0085     MaterialView const& material_;
0086 
0087     real_type inc_energy_;
0088     Real3 const& inc_direction_;
0089     real_type safety_;
0090 
0091     // Results from UrbanMSCStepLimit
0092     bool is_displaced_;
0093     real_type geom_path_;
0094     real_type true_path_;
0095     real_type limit_min_;
0096 
0097     // Calculated values for sampling
0098     bool skip_sampling_;
0099     real_type end_energy_;
0100     real_type tau_{0};
0101     real_type theta0_{-1};
0102 
0103     //// COMMON PROPERTIES ////
0104 
0105     //! The minimum step length for geometry 0.05 nm
0106     static CELER_CONSTEXPR_FUNCTION real_type geom_min()
0107     {
0108         return 5e-9 * units::centimeter;
0109     }
0110 
0111     //// HELPER FUNCTIONS ////
0112 
0113     // Sample the angle, cos(theta), of the multiple scattering
0114     template<class Engine>
0115     inline CELER_FUNCTION real_type sample_cos_theta(Engine& rng) const;
0116 
0117     // Sample consine(theta) with a large angle scattering
0118     template<class Engine>
0119     inline CELER_FUNCTION real_type simple_scattering(Engine& rng) const;
0120 
0121     // Calculate the theta0 of the Highland formula
0122     inline CELER_FUNCTION real_type
0123     compute_theta0(ParticleTrackView const& particle) const;
0124 
0125     // Update direction and position after the multiple scattering
0126     template<class Engine>
0127     inline CELER_FUNCTION Real3 sample_displacement_dir(Engine& rng,
0128                                                         real_type phi) const;
0129 };
0130 
0131 //---------------------------------------------------------------------------//
0132 // INLINE DEFINITIONS
0133 //---------------------------------------------------------------------------//
0134 CELER_FUNCTION real_type UrbanMscScatter::calc_displacement(real_type geom_path,
0135                                                             real_type true_path)
0136 {
0137     CELER_EXPECT(true_path >= geom_path);
0138 
0139     // true^2 - geo^2
0140     real_type rmax2 = diffsq(true_path, geom_path);
0141 
0142     // 0.73 is (roughly) the expected value of a distribution of the mean
0143     // radius given rmax "based on single scattering results"
0144     // https://github.com/Geant4/geant4/blame/28a70706e0edf519b16e864ebf1d2f02a00ba596/source/processes/electromagnetic/standard/src/G4UrbanMscModel.cc#L1142
0145     constexpr real_type mean_radius_frac{0.73};
0146 
0147     return mean_radius_frac * std::sqrt(rmax2);
0148 }
0149 
0150 //---------------------------------------------------------------------------//
0151 /*!
0152  * Construct with shared and state data.
0153  *
0154  * This function also precalculates distribution-independent quantities, e.g.
0155  * converting the geometrical path length to the true path.
0156  */
0157 CELER_FUNCTION
0158 UrbanMscScatter::UrbanMscScatter(UrbanMscRef const& shared,
0159                                  UrbanMscHelper const& helper,
0160                                  ParticleTrackView const& particle,
0161                                  PhysicsTrackView const& physics,
0162                                  MaterialView const& material,
0163                                  Real3 const& dir,
0164                                  real_type safety,
0165                                  MscStep const& input)
0166     : shared_(shared)
0167     , msc_(shared.material_data[material.material_id()])
0168     , helper_(helper)
0169     , material_(material)
0170     , inc_energy_(value_as<Energy>(particle.energy()))
0171     , inc_direction_(dir)
0172     , safety_(safety)
0173     , is_displaced_(input.is_displaced)
0174     , geom_path_(input.geom_path)
0175     , true_path_(input.true_path)
0176     , limit_min_(physics.msc_range().limit_min)
0177 {
0178     CELER_EXPECT(safety_ >= 0);
0179     CELER_EXPECT(geom_path_ > 0);
0180     CELER_EXPECT(true_path_ >= geom_path_);
0181     CELER_EXPECT(limit_min_ >= UrbanMscParameters::min_step || !is_displaced_);
0182     CELER_EXPECT(!is_displaced_ || safety > 0);
0183 
0184     skip_sampling_ = [this, &helper, &physics] {
0185         if (true_path_ == physics.dedx_range())
0186         {
0187             // Range-limited step (particle stops)
0188             // TODO: probably redundant with low 'end energy'
0189             return true;
0190         }
0191         if (true_path_ < shared_.params.geom_limit)
0192         {
0193             // Very small step (NOTE: with the default values in UrbanMscData,
0194             // this is redundant witih the tau_small comparison below if MFP >=
0195             // 0.005 cm)
0196             return true;
0197         }
0198 
0199         // Lazy calculation of end energy
0200         end_energy_ = value_as<Energy>(helper.calc_end_energy(true_path_));
0201 
0202         if (Energy{end_energy_} < shared_.params.min_endpoint_energy)
0203         {
0204             // Ending energy is below the threshold to scatter
0205             return true;
0206         }
0207         if (true_path_ <= helper_.msc_mfp() * shared_.params.tau_small)
0208         {
0209             // Very small MFP travelled
0210             return true;
0211         }
0212         return false;
0213     }();
0214 
0215     // TODO: there are several different sampling strategies for angle change:
0216     // - very small step/very low energy endpoint: no scattering
0217     // - very small mfp: (probably impossible because of condition above):
0218     //   forward scatter
0219     // - very large mfp: exiting angle is isotropic
0220     // - large energy loss: "simple_scattering"
0221 
0222     if (!skip_sampling_)
0223     {
0224         // Calculate number of mean free paths traveled
0225         tau_ = true_path_ / [this, &helper] {
0226             // Calculate the average MFP assuming the cross section varies
0227             // linearly over the step
0228             real_type lambda = helper_.msc_mfp();
0229             real_type lambda_end = helper.calc_msc_mfp(Energy{end_energy_});
0230             if (std::fabs(lambda - lambda_end) < lambda * real_type(0.01))
0231             {
0232                 // Cross section is almost constant over the step: avoid
0233                 // numerical explosion
0234                 return helper_.msc_mfp();
0235             }
0236             return (lambda - lambda_end) / std::log(lambda / lambda_end);
0237         }();
0238 
0239         if (tau_ < shared_.params.tau_big)
0240         {
0241             // MSC "true path" step limit
0242             if (CELER_UNLIKELY(limit_min_ == 0))
0243             {
0244                 // Unlikely: MSC range cache wasn't initialized by
0245                 // UrbanMscStepLimit, because e.g. its first step was very
0246                 // small
0247                 CELER_ASSERT(!is_displaced_);
0248                 limit_min_ = UrbanMscParameters::min_step_fallback;
0249             }
0250             limit_min_ = min(limit_min_, physics.scalars().lambda_limit);
0251 
0252             // TODO: theta0_ calculation could be done externally, eliminating
0253             // many of the class member data
0254             theta0_ = this->compute_theta0(particle);
0255 
0256             if (theta0_ < real_type(1e-8))
0257             {
0258                 // Arbitrarily (?) small angle change (theta_0^2 < 1e-16):
0259                 // skip sampling angular distribution if width of direction
0260                 // distribution is too narrow
0261                 if (!is_displaced_)
0262                 {
0263                     // No angular sampling and no displacement => no change
0264                     skip_sampling_ = true;
0265                 }
0266                 else
0267                 {
0268                     theta0_ = 0;
0269                 }
0270             }
0271         }
0272     }
0273 }
0274 
0275 //---------------------------------------------------------------------------//
0276 /*!
0277  * Sample the angular distribution and the lateral displacement by multiple
0278  * scattering.
0279  */
0280 template<class Engine>
0281 CELER_FUNCTION auto UrbanMscScatter::operator()(Engine& rng) -> MscInteraction
0282 {
0283     if (skip_sampling_)
0284     {
0285         // Do not sample scattering at the last or at a small step
0286         return {inc_direction_, {0, 0, 0}, MscInteraction::Action::unchanged};
0287     }
0288 
0289     // Sample polar angle cosine
0290     real_type costheta = [this, &rng] {
0291         if (theta0_ <= 0)
0292         {
0293             // Very small outgoing angular distribution
0294             return real_type{1};
0295         }
0296         if (tau_ >= shared_.params.tau_big)
0297         {
0298             // Long mean free path: exiting direction is isotropic
0299             UniformRealDistribution<real_type> sample_isotropic(-1, 1);
0300             return sample_isotropic(rng);
0301         }
0302         if (2 * end_energy_ < inc_energy_ || theta0_ > constants::pi / 6)
0303         {
0304             // Large energy loss over the step or large angle distribution
0305             // width
0306             return this->simple_scattering(rng);
0307         }
0308         // No special cases match:
0309         return this->sample_cos_theta(rng);
0310     }();
0311     CELER_ASSERT(std::fabs(costheta) <= 1);
0312 
0313     // Sample azimuthal angle, used for displacement and exiting angle
0314     real_type phi = UniformRealDistribution<real_type>(
0315         0, real_type{2 * constants::pi})(rng);
0316 
0317     MscInteraction result;
0318     result.action = MscInteraction::Action::scattered;
0319     // This should only be needed to silence compiler warning, since the
0320     // displacement should be ignored since our action result is
0321     // 'scattered'
0322     result.displacement = {0, 0, 0};
0323 
0324     if (is_displaced_)
0325     {
0326         // Calculate displacement length
0327         real_type length = this->calc_displacement(geom_path_, true_path_);
0328         // Don't displace further than safety (minus a tolerance)
0329         length = min(length, (1 - shared_.params.safety_tol) * safety_);
0330 
0331         if (length >= shared_.params.geom_limit)
0332         {
0333             // Displacement distance is large enough to worry about
0334             result.displacement = this->sample_displacement_dir(rng, phi);
0335             result.displacement *= length;
0336             result.action = MscInteraction::Action::displaced;
0337         }
0338     }
0339 
0340     // Calculate direction and return
0341     result.direction = rotate(from_spherical(costheta, phi), inc_direction_);
0342     return result;
0343 }
0344 
0345 //---------------------------------------------------------------------------//
0346 /*!
0347  * Sample the scattering angle at the end of the true step length.
0348  *
0349  * The scattering angle \f$\theta\f$ and true step length, \f$t\f$ are
0350  * described in G4UrbanMscModel::SampleCosineTheta of the
0351  * Geant4 10.7 release. See also, CERN-OPEN-2006-077 by L. Urban.
0352  *
0353  * The mean value of \f$u = \cos\theta\f$ follows \f$\exp(-t/\lambda_{1})\f$
0354  * and the variance is written as \f$\frac{1+2e^{-\kappa r}}{3} - e^{-2r}\f$
0355  * where \f$r = t/\lambda_{1}\f$ and \f$\kappa = \lambda_{1}/\lambda_{2}\f$.
0356  * The \f$\cos\theta\f$ is sampled according to a model function of \f$u\f$,
0357  * \f[
0358  *   g(u) = q [ p g_1(u) + (1-p) g_2(u) ] - (1 - q) g_3(u)
0359  * \f]
0360  * where \f$p,q \in [0,1]\f$ and the functions \f$g_i\f$ have been chosen as
0361  * \f[
0362  *   g_1(u) = c_1 e^{-a(1-u)},
0363  *   g_2(u) = \frac{c_2}{(b-u)^d},
0364  *   g_3(u) = c_3
0365  * \f]
0366  * with normalization constants, \f$d\f$. For small angles, \f$g_1\f$ is
0367  * nearly Gaussian, \f$ \exp(-\frac{\theta^{2}}{2\theta_{0}^{2}}), \f$
0368  * if \f$\theta_0 \approx 1/1\f$, while \f$g_2\f$ has a Rutherford-like tail
0369  * for large \f$\theta\f$, if \f$b \approx 1\f$ and \f$d\f$ is not far from 2.
0370  */
0371 template<class Engine>
0372 CELER_FUNCTION real_type UrbanMscScatter::sample_cos_theta(Engine& rng) const
0373 {
0374     // Evaluate parameters for the tail distribution
0375     real_type xsi = [this] {
0376         using PolyQuad = PolyEvaluator<real_type, 2>;
0377 
0378         real_type maxtau
0379             = true_path_ < limit_min_ ? limit_min_ / helper_.msc_mfp() : tau_;
0380         // note: 0 < u <= sqrt(2) when shared_.params.tau_big == 8
0381         real_type u = fastpow(maxtau, 1 / real_type(6));
0382         // Number of radiation lengths traveled by the average MFP over this
0383         // step
0384         real_type radlen_mfp = true_path_
0385                                / (tau_ * material_.radiation_length());
0386         real_type result = PolyQuad(msc_.tail_coeff)(u)
0387                            + msc_.tail_corr * std::log(radlen_mfp);
0388         // The tail should not be too big
0389         return max(result, real_type(1.9));
0390     }();
0391 
0392     // Mean of cos\theta computed from the distribution g_1(cos\theta)
0393     // small theta => x = theta0^2
0394     // large xsi => xmean_1 = 1 - x
0395     // small tau => xmean = 1
0396     real_type x = ipow<2>(2 * std::sin(real_type(0.5) * theta0_));
0397 
0398     // Calculate intermediate values for the mean of cos(theta)
0399     // Since xsi is not near zero (thanks to max), no need to use expm1
0400     // The expression in outer parens is in [~0.666, 1]
0401     real_type xmean_1 = 1 - x * (1 - xsi / (std::exp(xsi) - 1));
0402 
0403     // Mean scattering cosine from GS legendre moments: see
0404     // fernandez-varea-crosssections-1993
0405     real_type xmean = std::exp(-tau_);
0406 
0407     // 0.0003 (max tau before assuming isotropic scattering) < xmean < 1
0408     if (xmean_1 <= real_type(0.999) * xmean)
0409     {
0410         return this->simple_scattering(rng);
0411     }
0412 
0413     // From continuity of derivatives
0414     real_type c = [xsi] {
0415         if (std::fabs(xsi - 3) < real_type(0.001))
0416         {
0417             return real_type(3.001);
0418         }
0419         else if (std::fabs(xsi - 2) < real_type(0.001))
0420         {
0421             return real_type(2.001);
0422         }
0423         return xsi;
0424     }();
0425     real_type b1 = 2 + (c - xsi) * x;
0426     real_type d = fastpow(c * x / b1, c - 1);
0427     real_type x0 = 1 - xsi * x;
0428 
0429     // Mean of cos\theta computed from the distribution g_2(cos\theta)
0430     real_type xmean_2 = (x0 + d - (c * x - b1 * d) / (c - 2)) / (1 - d);
0431 
0432     real_type prob = [xsi, c, d] {
0433         real_type f2x0 = (c - 1) / (c * (1 - d));
0434         // Note: ea_invm1 is always greater than ~0.9
0435         real_type ea_invm1 = std::exp(xsi) - 1;
0436         return 1 / (1 + 1 / (f2x0 * ea_invm1));
0437     }();
0438 
0439     // Eq. 8.14 in the PRM: note that can be greater than 1
0440     real_type qprob = xmean / (prob * xmean_1 + (1 - prob) * xmean_2);
0441 
0442     // Sampling of cos(theta)
0443     if (generate_canonical(rng) >= qprob)
0444     {
0445         // Sample \f$ \cos\theta \f$ from \f$ g_3(\cos\theta) \f$
0446         return UniformRealDistribution<real_type>(-1, 1)(rng);
0447     }
0448 
0449     // Note: prob is sometime a little greater than one
0450     if (generate_canonical(rng) < prob)
0451     {
0452         // Sample \f$ \cos\theta \f$ from \f$ g_1(\cos\theta) \f$
0453         UniformRealDistribution<real_type> sample_inner(std::exp(-xsi), 1);
0454         return 1 + std::log(sample_inner(rng)) * x;
0455     }
0456     else
0457     {
0458         // Sample \f$ \cos\theta \f$ from \f$ g_2(\cos\theta) \f$
0459         real_type var = (1 - d) * generate_canonical(rng);
0460         if (var < real_type(0.01) * d)
0461         {
0462             var /= (d * (c - 1));
0463             return -1
0464                    + var * (1 - real_type(0.5) * var * c) * (2 + (c - xsi) * x);
0465         }
0466         else
0467         {
0468             return x * (c - xsi - c * fastpow(var + d, -1 / (c - 1))) + 1;
0469         }
0470     }
0471 }
0472 //---------------------------------------------------------------------------//
0473 /*!
0474  * Sample the large angle scattering using 2 model functions.
0475  *
0476  * \param rng Random number generator
0477  */
0478 template<class Engine>
0479 CELER_FUNCTION real_type UrbanMscScatter::simple_scattering(Engine& rng) const
0480 {
0481     UrbanLargeAngleDistribution dist(tau_);
0482     return dist(rng);
0483 }
0484 
0485 //---------------------------------------------------------------------------//
0486 /*!
0487  * Calculate the width of an approximate Gaussian projected angle distribution
0488  * using a modified Highland-Lynch-Dahl formula.
0489  *
0490  * All particles take the width
0491  * of the central part from a parameterization similar to the original Highland
0492  * formula, Particle Physics Booklet, July 2002, eq. 26.10.
0493  * \f[
0494  *   \theta_0 = \frac{13.6\rm{MeV}}{\beta c p} z_{ch} \sqrt{\ln(t/X_o)} c
0495  * \f]
0496  * where \f$p, \beta\c, z_{ch}\f$, \f$t/X_0\f$ and \f$c\f$ are the momentum,
0497  * velocity, charge number of the incident particle, the true path length in
0498  * radiation length unit and the correction term, respectively. For details,
0499  * see the section 8.1.5 of the Geant4 10.7 Physics Reference Manual.
0500  */
0501 CELER_FUNCTION
0502 real_type
0503 UrbanMscScatter::compute_theta0(ParticleTrackView const& particle) const
0504 {
0505     real_type const mass = value_as<Mass>(shared_.electron_mass);
0506     real_type true_path = max(limit_min_, true_path_);
0507     real_type y = true_path / material_.radiation_length();
0508 
0509     // Correction for the positron
0510     if (particle.particle_id() == shared_.ids.positron)
0511     {
0512         detail::UrbanPositronCorrector calc_correction{material_.zeff()};
0513         y *= calc_correction(std::sqrt(inc_energy_ * end_energy_) / mass);
0514     }
0515     CELER_ASSERT(y > 0);
0516 
0517     real_type invbetacp
0518         = std::sqrt((inc_energy_ + mass) * (end_energy_ + mass)
0519                     / (inc_energy_ * (inc_energy_ + 2 * mass) * end_energy_
0520                        * (end_energy_ + 2 * mass)));
0521     constexpr units::MevEnergy c_highland{13.6};
0522     real_type theta0
0523         = c_highland.value()
0524           * std::abs(value_as<units::ElementaryCharge>(particle.charge()))
0525           * std::sqrt(y) * invbetacp;
0526 
0527     // Correction factor from e- scattering data
0528     theta0 *= PolyEvaluator<real_type, 1>(msc_.theta_coeff)(std::log(y));
0529 
0530     if (true_path_ < limit_min_)
0531     {
0532         // Apply correction if true path is very small
0533         theta0 *= std::sqrt(true_path_ / limit_min_);
0534     }
0535 
0536     // Very small path lengths can result in a negative e- scattering
0537     // correction: clamp to zero so that too-small paths result in no change
0538     // in angle
0539     return clamp_to_nonneg(theta0);
0540 }
0541 
0542 //---------------------------------------------------------------------------//
0543 /*!
0544  * Sample the displacement direction using G4UrbanMscModel::SampleDisplacement
0545  * (simple and fast sampling based on single scattering results) and update
0546  * direction and position of the particle.
0547  *
0548  * A simple distribution for the unit direction on the lateral (x-y) plane,
0549  * \f$ Phi = \phi \pm \psi \f$ where \f$ psi \sim \exp(-\beta*v) \f$ and
0550  * \f$\beta\f$ is determined from the requirement that the distribution should
0551  * give the same mean value that is obtained from the single scattering
0552  * simulation.
0553  *
0554  * \param rng Random number generator
0555  * \param phi the azimuthal angle of the multiple scattering.
0556  */
0557 template<class Engine>
0558 CELER_FUNCTION Real3
0559 UrbanMscScatter::sample_displacement_dir(Engine& rng, real_type phi) const
0560 {
0561     // Sample a unit direction of the displacement
0562     constexpr real_type cbeta = 2.160;
0563     // cbeta1 = 1 - std::exp(-cbeta * constants::pi);
0564     constexpr real_type cbeta1 = 0.9988703417569197;
0565 
0566     real_type psi = -std::log(1 - generate_canonical(rng) * cbeta1) / cbeta;
0567     phi += BernoulliDistribution(0.5)(rng) ? psi : -psi;
0568 
0569     Real3 displacement{std::cos(phi), std::sin(phi), 0};
0570 
0571     // Rotate along the incident particle direction
0572     displacement = rotate(displacement, inc_direction_);
0573     return displacement;
0574 }
0575 
0576 //---------------------------------------------------------------------------//
0577 }  // namespace detail
0578 }  // namespace celeritas