Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-11-04 09:57:49

0001 //------------------------------ -*- C++ -*- -------------------------------//
0002 // Copyright Celeritas contributors: see 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  * This code performs the same method as in \c
0043  * G4VMultipleScattering::AlongStepDoIt and \c
0044  * 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 calc_displacement(
0059         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 compute_theta0(
0123         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(
0135     real_type geom_path, 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 UrbanMscScatter::UrbanMscScatter(
0158     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     using namespace celeritas::literals;
0179 
0180     CELER_EXPECT(safety_ >= 0);
0181     CELER_EXPECT(geom_path_ > 0);
0182     CELER_EXPECT(true_path_ >= geom_path_);
0183     CELER_EXPECT(limit_min_ >= UrbanMscParameters::min_step || !is_displaced_);
0184     CELER_EXPECT(!is_displaced_ || safety > 0);
0185 
0186     skip_sampling_ = [this, &helper, &physics] {
0187         if (true_path_ == physics.dedx_range())
0188         {
0189             // Range-limited step (particle stops)
0190             // TODO: probably redundant with low 'end energy'
0191             return true;
0192         }
0193         if (true_path_ < shared_.params.geom_limit)
0194         {
0195             // Very small step (NOTE: with the default values in UrbanMscData,
0196             // this is redundant witih the tau_small comparison below if MFP >=
0197             // 0.005 cm)
0198             return true;
0199         }
0200 
0201         // Lazy calculation of end energy
0202         end_energy_ = value_as<Energy>(helper.calc_end_energy(true_path_));
0203 
0204         if (Energy{end_energy_} < shared_.params.min_endpoint_energy)
0205         {
0206             // Ending energy is below the threshold to scatter
0207             return true;
0208         }
0209         if (true_path_ <= helper_.msc_mfp() * shared_.params.tau_small)
0210         {
0211             // Very small MFP travelled
0212             return true;
0213         }
0214         return false;
0215     }();
0216 
0217     // TODO: there are several different sampling strategies for angle change:
0218     // - very small step/very low energy endpoint: no scattering
0219     // - very small mfp: (probably impossible because of condition above):
0220     //   forward scatter
0221     // - very large mfp: exiting angle is isotropic
0222     // - large energy loss: "simple_scattering"
0223 
0224     if (!skip_sampling_)
0225     {
0226         // Calculate number of mean free paths traveled
0227         tau_ = true_path_ / [this, &helper] {
0228             // Calculate the average MFP assuming the cross section varies
0229             // linearly over the step
0230             real_type lambda = helper_.msc_mfp();
0231             real_type lambda_end = helper.calc_msc_mfp(Energy{end_energy_});
0232             if (std::fabs(lambda - lambda_end) < lambda * 0.01_r)
0233             {
0234                 // Cross section is almost constant over the step: avoid
0235                 // numerical explosion
0236                 return helper_.msc_mfp();
0237             }
0238             return (lambda - lambda_end) / std::log(lambda / lambda_end);
0239         }();
0240 
0241         if (tau_ < shared_.params.tau_big)
0242         {
0243             // MSC "true path" step limit
0244             if (CELER_UNLIKELY(limit_min_ == 0))
0245             {
0246                 // Unlikely: MSC range cache wasn't initialized by
0247                 // UrbanMscStepLimit, because e.g. its first step was very
0248                 // small
0249                 CELER_ASSERT(!is_displaced_);
0250                 limit_min_ = UrbanMscParameters::min_step_fallback;
0251             }
0252             limit_min_ = min(limit_min_, physics.scalars().lambda_limit);
0253 
0254             // TODO: theta0_ calculation could be done externally, eliminating
0255             // many of the class member data
0256             theta0_ = this->compute_theta0(particle);
0257 
0258             if (theta0_ < 1e-8_r)
0259             {
0260                 // Arbitrarily (?) small angle change (theta_0^2 < 1e-16):
0261                 // skip sampling angular distribution if width of direction
0262                 // distribution is too narrow
0263                 if (!is_displaced_)
0264                 {
0265                     // No angular sampling and no displacement => no change
0266                     skip_sampling_ = true;
0267                 }
0268                 else
0269                 {
0270                     theta0_ = 0;
0271                 }
0272             }
0273         }
0274     }
0275 }
0276 
0277 //---------------------------------------------------------------------------//
0278 /*!
0279  * Sample the angular distribution and the lateral displacement by multiple
0280  * scattering.
0281  */
0282 template<class Engine>
0283 CELER_FUNCTION auto UrbanMscScatter::operator()(Engine& rng) -> MscInteraction
0284 {
0285     using namespace celeritas::literals;
0286 
0287     if (skip_sampling_)
0288     {
0289         // Do not sample scattering at the last or at a small step
0290         return {inc_direction_, {0, 0, 0}, MscInteraction::Action::unchanged};
0291     }
0292 
0293     // Sample polar angle cosine
0294     real_type costheta = [this, &rng] {
0295         if (theta0_ <= 0)
0296         {
0297             // Very small outgoing angular distribution
0298             return 1_r;
0299         }
0300         if (tau_ >= shared_.params.tau_big)
0301         {
0302             // Long mean free path: exiting direction is isotropic
0303             UniformRealDistribution<real_type> sample_isotropic(-1, 1);
0304             return sample_isotropic(rng);
0305         }
0306         if (2 * end_energy_ < inc_energy_ || theta0_ > constants::pi / 6)
0307         {
0308             // Large energy loss over the step or large angle distribution
0309             // width
0310             return this->simple_scattering(rng);
0311         }
0312         // No special cases match:
0313         return this->sample_cos_theta(rng);
0314     }();
0315     CELER_ASSERT(std::fabs(costheta) <= 1);
0316 
0317     // Sample azimuthal angle, used for displacement and exiting angle
0318     real_type phi
0319         = UniformRealDistribution<real_type>(0, 2_r * constants::pi)(rng);
0320 
0321     MscInteraction result;
0322     result.action = MscInteraction::Action::scattered;
0323     // This should only be needed to silence compiler warning, since the
0324     // displacement should be ignored since our action result is
0325     // 'scattered'
0326     result.displacement = {0, 0, 0};
0327 
0328     if (is_displaced_)
0329     {
0330         // Calculate displacement length
0331         real_type length = this->calc_displacement(geom_path_, true_path_);
0332         // Don't displace further than safety (minus a tolerance)
0333         length = min(length, (1 - shared_.params.safety_tol) * safety_);
0334 
0335         if (length >= shared_.params.geom_limit)
0336         {
0337             // Displacement distance is large enough to worry about
0338             result.displacement = this->sample_displacement_dir(rng, phi);
0339             result.displacement *= length;
0340             result.action = MscInteraction::Action::displaced;
0341         }
0342     }
0343 
0344     // Calculate direction and return
0345     result.direction = rotate(from_spherical(costheta, phi), inc_direction_);
0346     return result;
0347 }
0348 
0349 //---------------------------------------------------------------------------//
0350 /*!
0351  * Sample the scattering angle at the end of the true step length.
0352  *
0353  * The scattering angle \f$\theta\f$ and true step length, \f$t\f$ are
0354  * described in G4UrbanMscModel::SampleCosineTheta of the
0355  * Geant4 10.7 release. See also, CERN-OPEN-2006-077 by L. Urban.
0356  *
0357  * The mean value of \f$u = \cos\theta\f$ follows \f$\exp(-t/\lambda_{1})\f$
0358  * and the variance is written as \f$\frac{1+2e^{-\kappa r}}{3} - e^{-2r}\f$
0359  * where \f$r = t/\lambda_{1}\f$ and \f$\kappa = \lambda_{1}/\lambda_{2}\f$.
0360  * The \f$\cos\theta\f$ is sampled according to a model function of \f$u\f$,
0361  * \f[
0362  *   g(u) = q [ p g_1(u) + (1-p) g_2(u) ] - (1 - q) g_3(u)
0363  * \f]
0364  * where \f$p,q \in [0,1]\f$ and the functions \f$g_i\f$ have been chosen as
0365  * \f[
0366  *   g_1(u) = c_1 e^{-a(1-u)},
0367  *   g_2(u) = \frac{c_2}{(b-u)^d},
0368  *   g_3(u) = c_3
0369  * \f]
0370  * with normalization constants, \f$d\f$. For small angles, \f$g_1\f$ is
0371  * nearly Gaussian, \f$ \exp(-\frac{\theta^{2}}{2\theta_{0}^{2}}), \f$
0372  * if \f$\theta_0 \approx 1/1\f$, while \f$g_2\f$ has a Rutherford-like tail
0373  * for large \f$\theta\f$, if \f$b \approx 1\f$ and \f$d\f$ is not far from 2.
0374  */
0375 template<class Engine>
0376 CELER_FUNCTION real_type UrbanMscScatter::sample_cos_theta(Engine& rng) const
0377 {
0378     using namespace celeritas::literals;
0379 
0380     // Evaluate parameters for the tail distribution
0381     real_type xsi = [this] {
0382         using PolyQuad = PolyEvaluator<real_type, 2>;
0383 
0384         real_type maxtau
0385             = true_path_ < limit_min_ ? limit_min_ / helper_.msc_mfp() : tau_;
0386         // note: 0 < u <= sqrt(2) when shared_.params.tau_big == 8
0387         real_type u = fastpow(maxtau, 1.0_r / 6.0_r);
0388         // Number of radiation lengths traveled by the average MFP over this
0389         // step
0390         real_type radlen_mfp = true_path_
0391                                / (tau_ * material_.radiation_length());
0392         real_type result = PolyQuad(msc_.tail_coeff)(u)
0393                            + msc_.tail_corr * std::log(radlen_mfp);
0394         // The tail should not be too big
0395         return max(result, 1.9_r);
0396     }();
0397 
0398     // Mean of cos\theta computed from the distribution g_1(cos\theta)
0399     // small theta => x = theta0^2
0400     // large xsi => xmean_1 = 1 - x
0401     // small tau => xmean = 1
0402     real_type x = ipow<2>(2 * std::sin(0.5_r * theta0_));
0403 
0404     // Calculate intermediate values for the mean of cos(theta)
0405     // Since xsi is not near zero (thanks to max), no need to use expm1
0406     // The expression in outer parens is in [~0.666, 1]
0407     real_type xmean_1 = 1 - x * (1 - xsi / (std::exp(xsi) - 1));
0408 
0409     // Mean scattering cosine from GS legendre moments: see
0410     // fernandez-varea-crosssections-1993
0411     real_type xmean = std::exp(-tau_);
0412 
0413     // 0.0003 (max tau before assuming isotropic scattering) < xmean < 1
0414     if (xmean_1 <= 0.999_r * xmean)
0415     {
0416         return this->simple_scattering(rng);
0417     }
0418 
0419     // From continuity of derivatives
0420     real_type c = [xsi] {
0421         if (std::fabs(xsi - 3) < 0.001_r)
0422         {
0423             return 3.001_r;
0424         }
0425         else if (std::fabs(xsi - 2) < 0.001_r)
0426         {
0427             return 2.001_r;
0428         }
0429         return xsi;
0430     }();
0431     real_type b1 = 2 + (c - xsi) * x;
0432     real_type d = fastpow(c * x / b1, c - 1);
0433     real_type x0 = 1 - xsi * x;
0434 
0435     // Mean of cos\theta computed from the distribution g_2(cos\theta)
0436     real_type xmean_2 = (x0 + d - (c * x - b1 * d) / (c - 2)) / (1 - d);
0437 
0438     real_type prob = [xsi, c, d] {
0439         real_type f2x0 = (c - 1) / (c * (1 - d));
0440         // Note: ea_invm1 is always greater than ~0.9
0441         real_type ea_invm1 = std::exp(xsi) - 1;
0442         return 1 / (1 + 1 / (f2x0 * ea_invm1));
0443     }();
0444 
0445     // Eq. 8.14 in the PRM: note that can be greater than 1
0446     real_type qprob = xmean / (prob * xmean_1 + (1 - prob) * xmean_2);
0447 
0448     // Sampling of cos(theta)
0449     if (generate_canonical(rng) >= qprob)
0450     {
0451         // Sample \f$ \cos\theta \f$ from \f$ g_3(\cos\theta) \f$
0452         return UniformRealDistribution<real_type>(-1, 1)(rng);
0453     }
0454 
0455     // Note: prob is sometime a little greater than one
0456     if (generate_canonical(rng) < prob)
0457     {
0458         // Sample \f$ \cos\theta \f$ from \f$ g_1(\cos\theta) \f$
0459         UniformRealDistribution<real_type> sample_inner(std::exp(-xsi), 1);
0460         return 1 + std::log(sample_inner(rng)) * x;
0461     }
0462     else
0463     {
0464         // Sample \f$ \cos\theta \f$ from \f$ g_2(\cos\theta) \f$
0465         real_type var = (1 - d) * generate_canonical(rng);
0466         if (var < 0.01_r * d)
0467         {
0468             var /= (d * (c - 1));
0469             return -1 + var * (1 - 0.5_r * var * c) * (2 + (c - xsi) * x);
0470         }
0471         else
0472         {
0473             return x * (c - xsi - c * fastpow(var + d, -1 / (c - 1))) + 1;
0474         }
0475     }
0476 }
0477 //---------------------------------------------------------------------------//
0478 /*!
0479  * Sample the large angle scattering using 2 model functions.
0480  *
0481  * \param rng Random number generator
0482  */
0483 template<class Engine>
0484 CELER_FUNCTION real_type UrbanMscScatter::simple_scattering(Engine& rng) const
0485 {
0486     UrbanLargeAngleDistribution dist(tau_);
0487     return dist(rng);
0488 }
0489 
0490 //---------------------------------------------------------------------------//
0491 /*!
0492  * Calculate the width of an approximate Gaussian projected angle distribution
0493  * using a modified Highland-Lynch-Dahl formula.
0494  *
0495  * All particles take the width
0496  * of the central part from a parameterization similar to the original Highland
0497  * formula, Particle Physics Booklet, July 2002, eq. 26.10.
0498  * \f[
0499  *   \theta_0 = \frac{13.6\rm{MeV}}{\beta c p} z_{ch} \sqrt{\ln(t/X_o)} c
0500  * \f]
0501  * where \f$p, \beta\c, z_{ch}\f$, \f$t/X_0\f$ and \f$c\f$ are the momentum,
0502  * velocity, charge number of the incident particle, the true path length in
0503  * radiation length unit and the correction term, respectively. For details,
0504  * see the section 8.1.5 of the Geant4 10.7 Physics Reference Manual.
0505  */
0506 CELER_FUNCTION real_type UrbanMscScatter::compute_theta0(
0507     ParticleTrackView const& particle) const
0508 {
0509     real_type const mass = value_as<Mass>(shared_.electron_mass);
0510     real_type true_path = max(limit_min_, true_path_);
0511     real_type y = true_path / material_.radiation_length();
0512 
0513     // Correction for the positron
0514     if (particle.particle_id() == shared_.ids.positron)
0515     {
0516         detail::UrbanPositronCorrector calc_correction{material_.zeff()};
0517         y *= calc_correction(std::sqrt(inc_energy_ * end_energy_) / mass);
0518     }
0519     CELER_ASSERT(y > 0);
0520 
0521     real_type invbetacp
0522         = std::sqrt((inc_energy_ + mass) * (end_energy_ + mass)
0523                     / (inc_energy_ * (inc_energy_ + 2 * mass) * end_energy_
0524                        * (end_energy_ + 2 * mass)));
0525     constexpr units::MevEnergy c_highland{13.6};
0526     real_type theta0
0527         = c_highland.value()
0528           * std::fabs(value_as<units::ElementaryCharge>(particle.charge()))
0529           * std::sqrt(y) * invbetacp;
0530 
0531     // Correction factor from e- scattering data
0532     theta0 *= PolyEvaluator<real_type, 1>(msc_.theta_coeff)(std::log(y));
0533 
0534     if (true_path_ < limit_min_)
0535     {
0536         // Apply correction if true path is very small
0537         theta0 *= std::sqrt(true_path_ / limit_min_);
0538     }
0539 
0540     // Very small path lengths can result in a negative e- scattering
0541     // correction: clamp to zero so that too-small paths result in no change
0542     // in angle
0543     return clamp_to_nonneg(theta0);
0544 }
0545 
0546 //---------------------------------------------------------------------------//
0547 /*!
0548  * Sample the displacement direction using G4UrbanMscModel::SampleDisplacement
0549  * (simple and fast sampling based on single scattering results) and update
0550  * direction and position of the particle.
0551  *
0552  * A simple distribution for the unit direction on the lateral (x-y) plane,
0553  * \f$ Phi = \phi \pm \psi \f$ where \f$ psi \sim \exp(-\beta*v) \f$ and
0554  * \f$\beta\f$ is determined from the requirement that the distribution should
0555  * give the same mean value that is obtained from the single scattering
0556  * simulation.
0557  *
0558  * \param rng Random number generator
0559  * \param phi the azimuthal angle of the multiple scattering.
0560  */
0561 template<class Engine>
0562 CELER_FUNCTION Real3 UrbanMscScatter::sample_displacement_dir(
0563     Engine& rng, real_type phi) const
0564 {
0565     // Sample a unit direction of the displacement
0566     constexpr real_type cbeta = 2.160;
0567     // cbeta1 = 1 - std::exp(-cbeta * constants::pi);
0568     constexpr real_type cbeta1 = 0.9988703417569197;
0569 
0570     real_type psi = -std::log(1 - generate_canonical(rng) * cbeta1) / cbeta;
0571     phi += BernoulliDistribution(0.5)(rng) ? psi : -psi;
0572 
0573     Real3 displacement{std::cos(phi), std::sin(phi), 0};
0574 
0575     // Rotate along the incident particle direction
0576     displacement = rotate(displacement, inc_direction_);
0577     return displacement;
0578 }
0579 
0580 //---------------------------------------------------------------------------//
0581 }  // namespace detail
0582 }  // namespace celeritas