File indexing completed on 2026-11-04 09:57:49
0001
0002
0003
0004
0005
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
0040
0041
0042
0043
0044
0045
0046 class UrbanMscScatter
0047 {
0048 public:
0049
0050
0051 using Energy = units::MevEnergy;
0052 using Mass = units::MevMass;
0053 using UrbanMscRef = NativeCRef<UrbanMscData>;
0054
0055
0056 public:
0057
0058 static inline CELER_FUNCTION real_type calc_displacement(
0059 real_type true_path, real_type geom_path);
0060
0061
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
0072 template<class Engine>
0073 inline CELER_FUNCTION MscInteraction operator()(Engine& rng);
0074
0075 private:
0076
0077
0078
0079 UrbanMscRef const& shared_;
0080
0081 UrbanMscMaterialData const& msc_;
0082
0083 UrbanMscHelper const& helper_;
0084
0085 MaterialView const& material_;
0086
0087 real_type inc_energy_;
0088 Real3 const& inc_direction_;
0089 real_type safety_;
0090
0091
0092 bool is_displaced_;
0093 real_type geom_path_;
0094 real_type true_path_;
0095 real_type limit_min_;
0096
0097
0098 bool skip_sampling_;
0099 real_type end_energy_;
0100 real_type tau_{0};
0101 real_type theta0_{-1};
0102
0103
0104
0105
0106 static CELER_CONSTEXPR_FUNCTION real_type geom_min()
0107 {
0108 return 5e-9 * units::centimeter;
0109 }
0110
0111
0112
0113
0114 template<class Engine>
0115 inline CELER_FUNCTION real_type sample_cos_theta(Engine& rng) const;
0116
0117
0118 template<class Engine>
0119 inline CELER_FUNCTION real_type simple_scattering(Engine& rng) const;
0120
0121
0122 inline CELER_FUNCTION real_type compute_theta0(
0123 ParticleTrackView const& particle) const;
0124
0125
0126 template<class Engine>
0127 inline CELER_FUNCTION Real3 sample_displacement_dir(Engine& rng,
0128 real_type phi) const;
0129 };
0130
0131
0132
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
0140 real_type rmax2 = diffsq(true_path, geom_path);
0141
0142
0143
0144
0145 constexpr real_type mean_radius_frac{0.73};
0146
0147 return mean_radius_frac * std::sqrt(rmax2);
0148 }
0149
0150
0151
0152
0153
0154
0155
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
0190
0191 return true;
0192 }
0193 if (true_path_ < shared_.params.geom_limit)
0194 {
0195
0196
0197
0198 return true;
0199 }
0200
0201
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
0207 return true;
0208 }
0209 if (true_path_ <= helper_.msc_mfp() * shared_.params.tau_small)
0210 {
0211
0212 return true;
0213 }
0214 return false;
0215 }();
0216
0217
0218
0219
0220
0221
0222
0223
0224 if (!skip_sampling_)
0225 {
0226
0227 tau_ = true_path_ / [this, &helper] {
0228
0229
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
0235
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
0244 if (CELER_UNLIKELY(limit_min_ == 0))
0245 {
0246
0247
0248
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
0255
0256 theta0_ = this->compute_theta0(particle);
0257
0258 if (theta0_ < 1e-8_r)
0259 {
0260
0261
0262
0263 if (!is_displaced_)
0264 {
0265
0266 skip_sampling_ = true;
0267 }
0268 else
0269 {
0270 theta0_ = 0;
0271 }
0272 }
0273 }
0274 }
0275 }
0276
0277
0278
0279
0280
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
0290 return {inc_direction_, {0, 0, 0}, MscInteraction::Action::unchanged};
0291 }
0292
0293
0294 real_type costheta = [this, &rng] {
0295 if (theta0_ <= 0)
0296 {
0297
0298 return 1_r;
0299 }
0300 if (tau_ >= shared_.params.tau_big)
0301 {
0302
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
0309
0310 return this->simple_scattering(rng);
0311 }
0312
0313 return this->sample_cos_theta(rng);
0314 }();
0315 CELER_ASSERT(std::fabs(costheta) <= 1);
0316
0317
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
0324
0325
0326 result.displacement = {0, 0, 0};
0327
0328 if (is_displaced_)
0329 {
0330
0331 real_type length = this->calc_displacement(geom_path_, true_path_);
0332
0333 length = min(length, (1 - shared_.params.safety_tol) * safety_);
0334
0335 if (length >= shared_.params.geom_limit)
0336 {
0337
0338 result.displacement = this->sample_displacement_dir(rng, phi);
0339 result.displacement *= length;
0340 result.action = MscInteraction::Action::displaced;
0341 }
0342 }
0343
0344
0345 result.direction = rotate(from_spherical(costheta, phi), inc_direction_);
0346 return result;
0347 }
0348
0349
0350
0351
0352
0353
0354
0355
0356
0357
0358
0359
0360
0361
0362
0363
0364
0365
0366
0367
0368
0369
0370
0371
0372
0373
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
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
0387 real_type u = fastpow(maxtau, 1.0_r / 6.0_r);
0388
0389
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
0395 return max(result, 1.9_r);
0396 }();
0397
0398
0399
0400
0401
0402 real_type x = ipow<2>(2 * std::sin(0.5_r * theta0_));
0403
0404
0405
0406
0407 real_type xmean_1 = 1 - x * (1 - xsi / (std::exp(xsi) - 1));
0408
0409
0410
0411 real_type xmean = std::exp(-tau_);
0412
0413
0414 if (xmean_1 <= 0.999_r * xmean)
0415 {
0416 return this->simple_scattering(rng);
0417 }
0418
0419
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
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
0441 real_type ea_invm1 = std::exp(xsi) - 1;
0442 return 1 / (1 + 1 / (f2x0 * ea_invm1));
0443 }();
0444
0445
0446 real_type qprob = xmean / (prob * xmean_1 + (1 - prob) * xmean_2);
0447
0448
0449 if (generate_canonical(rng) >= qprob)
0450 {
0451
0452 return UniformRealDistribution<real_type>(-1, 1)(rng);
0453 }
0454
0455
0456 if (generate_canonical(rng) < prob)
0457 {
0458
0459 UniformRealDistribution<real_type> sample_inner(std::exp(-xsi), 1);
0460 return 1 + std::log(sample_inner(rng)) * x;
0461 }
0462 else
0463 {
0464
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
0480
0481
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
0493
0494
0495
0496
0497
0498
0499
0500
0501
0502
0503
0504
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
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
0532 theta0 *= PolyEvaluator<real_type, 1>(msc_.theta_coeff)(std::log(y));
0533
0534 if (true_path_ < limit_min_)
0535 {
0536
0537 theta0 *= std::sqrt(true_path_ / limit_min_);
0538 }
0539
0540
0541
0542
0543 return clamp_to_nonneg(theta0);
0544 }
0545
0546
0547
0548
0549
0550
0551
0552
0553
0554
0555
0556
0557
0558
0559
0560
0561 template<class Engine>
0562 CELER_FUNCTION Real3 UrbanMscScatter::sample_displacement_dir(
0563 Engine& rng, real_type phi) const
0564 {
0565
0566 constexpr real_type cbeta = 2.160;
0567
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
0576 displacement = rotate(displacement, inc_direction_);
0577 return displacement;
0578 }
0579
0580
0581 }
0582 }