File indexing completed on 2026-10-29 09:07:39
0001
0002
0003
0004
0005
0006
0007 #pragma once
0008
0009 #include "corecel/Macros.hh"
0010 #include "corecel/Types.hh"
0011 #include "corecel/data/StackAllocator.hh"
0012 #include "corecel/math/Algorithms.hh"
0013 #include "corecel/math/ArrayOperators.hh"
0014 #include "corecel/math/ArrayUtils.hh"
0015 #include "corecel/math/PolyEvaluator.hh"
0016 #include "corecel/random/distribution/BernoulliDistribution.hh"
0017 #include "corecel/random/distribution/GenerateCanonical.hh"
0018 #include "corecel/random/distribution/UniformRealDistribution.hh"
0019 #include "celeritas/Constants.hh"
0020 #include "celeritas/Quantities.hh"
0021 #include "celeritas/em/data/BetheHeitlerData.hh"
0022 #include "celeritas/em/distribution/TsaiUrbanDistribution.hh"
0023 #include "celeritas/em/xs/LPMCalculator.hh"
0024 #include "celeritas/mat/ElementView.hh"
0025 #include "celeritas/phys/Interaction.hh"
0026 #include "celeritas/phys/ParticleTrackView.hh"
0027 #include "celeritas/phys/Secondary.hh"
0028
0029 namespace celeritas
0030 {
0031
0032
0033
0034
0035
0036
0037
0038
0039
0040
0041
0042
0043
0044
0045
0046
0047
0048 class BetheHeitlerInteractor
0049 {
0050 public:
0051
0052
0053 using Mass = units::MevMass;
0054 using Energy = units::MevEnergy;
0055
0056
0057 public:
0058
0059 inline CELER_FUNCTION BetheHeitlerInteractor(
0060 BetheHeitlerData const& shared,
0061 ParticleTrackView const& particle,
0062 Real3 const& inc_direction,
0063 StackAllocator<Secondary>& allocate,
0064 MaterialView const& material,
0065 ElementView const& element);
0066
0067
0068 template<class Engine>
0069 inline CELER_FUNCTION Interaction operator()(Engine& rng);
0070
0071 private:
0072
0073
0074 using Real2 = Array<real_type, 2>;
0075
0076
0077
0078
0079 BetheHeitlerData const& shared_;
0080
0081 Energy const inc_energy_;
0082
0083 Real3 const& inc_direction_;
0084
0085 StackAllocator<Secondary>& allocate_;
0086
0087 bool const enable_lpm_;
0088
0089 LPMCalculator calc_lpm_functions_;
0090
0091 real_type epsilon0_;
0092
0093 real_type screen_delta_;
0094 real_type f_z_;
0095
0096
0097
0098
0099 static CELER_CONSTEXPR_FUNCTION Energy no_screening_threshold()
0100 {
0101 return units::MevEnergy{2};
0102 }
0103
0104
0105 static CELER_CONSTEXPR_FUNCTION Energy coulomb_corr_threshold()
0106 {
0107 return units::MevEnergy{50};
0108 }
0109
0110
0111 static CELER_CONSTEXPR_FUNCTION Energy lpm_threshold()
0112 {
0113 return units::MevEnergy{1e5};
0114 }
0115
0116
0117
0118
0119 inline CELER_FUNCTION real_type impact_parameter(real_type eps) const;
0120
0121
0122 static inline CELER_FUNCTION Real2 screening_phi(real_type delta);
0123
0124
0125 static inline CELER_FUNCTION Real2 screening_f(real_type delta);
0126 };
0127
0128
0129
0130
0131
0132
0133
0134
0135
0136 CELER_FUNCTION BetheHeitlerInteractor::BetheHeitlerInteractor(
0137 BetheHeitlerData const& shared,
0138 ParticleTrackView const& particle,
0139 Real3 const& inc_direction,
0140 StackAllocator<Secondary>& allocate,
0141 MaterialView const& material,
0142 ElementView const& element)
0143 : shared_(shared)
0144 , inc_energy_(particle.energy())
0145 , inc_direction_(inc_direction)
0146 , allocate_(allocate)
0147 , enable_lpm_(shared.enable_lpm && inc_energy_ > lpm_threshold())
0148 , calc_lpm_functions_(
0149 material, element, shared_.dielectric_suppression(), inc_energy_)
0150 {
0151 using namespace celeritas::literals;
0152
0153 CELER_EXPECT(particle.particle_id() == shared_.ids.gamma);
0154 CELER_EXPECT(value_as<Energy>(inc_energy_)
0155 >= 2 * value_as<Mass>(shared_.electron_mass));
0156
0157 epsilon0_ = value_as<Mass>(shared_.electron_mass)
0158 / value_as<Energy>(inc_energy_);
0159
0160 if (inc_energy_ < no_screening_threshold())
0161 {
0162
0163 screen_delta_ = 0;
0164 f_z_ = 0;
0165 }
0166 else
0167 {
0168 screen_delta_ = epsilon0_ * 136 / element.cbrt_z();
0169 f_z_ = 8.0_r / 3.0_r * element.log_z();
0170 if (inc_energy_ > coulomb_corr_threshold())
0171 {
0172
0173 f_z_ += 8 * element.coulomb_correction();
0174 }
0175 }
0176 }
0177
0178
0179
0180
0181
0182 template<class Engine>
0183 CELER_FUNCTION Interaction BetheHeitlerInteractor::operator()(Engine& rng)
0184 {
0185 using namespace celeritas::literals;
0186
0187
0188 Secondary* secondaries = allocate_(2);
0189 if (secondaries == nullptr)
0190 {
0191
0192 return Interaction::from_failure();
0193 }
0194
0195 constexpr real_type half = 0.5;
0196
0197
0198 real_type epsilon;
0199 if (screen_delta_ == 0)
0200 {
0201
0202 UniformRealDistribution<real_type> sample_eps(epsilon0_, half);
0203 epsilon = sample_eps(rng);
0204 }
0205 else
0206 {
0207
0208
0209
0210 real_type const delta_min = 4 * screen_delta_;
0211 real_type const delta_max = std::exp((42.038_r - f_z_) / 8.29_r)
0212 - 0.958_r;
0213 CELER_ASSERT(delta_min <= delta_max);
0214
0215
0216
0217
0218
0219
0220
0221 real_type const epsilon1
0222 = half * (1 - std::sqrt(1 - delta_min / delta_max));
0223 real_type const epsilon_min = celeritas::max(epsilon0_, epsilon1);
0224
0225
0226
0227
0228 Real2 const fmin = this->screening_f(delta_min) - f_z_;
0229 BernoulliDistribution choose_f1g1(
0230 ipow<2>(half - epsilon_min) * fmin[0], 1.5_r * fmin[1]);
0231
0232
0233
0234 real_type g;
0235 do
0236 {
0237 if (choose_f1g1(rng))
0238 {
0239
0240 epsilon = half
0241 - (half - epsilon_min)
0242 * std::cbrt(generate_canonical(rng));
0243 CELER_ASSERT(epsilon >= epsilon_min && epsilon <= half);
0244
0245
0246
0247 real_type delta = this->impact_parameter(epsilon);
0248 CELER_ASSERT(delta <= delta_max && delta >= delta_min);
0249
0250
0251 if (enable_lpm_)
0252 {
0253 auto screening = screening_phi(delta);
0254 auto lpm = calc_lpm_functions_(epsilon);
0255 g = lpm.xi
0256 * ((2 * lpm.phi + lpm.g) * screening[0]
0257 - lpm.g * screening[1] - lpm.phi * f_z_);
0258 }
0259 else
0260 {
0261 g = this->screening_f(delta)[0] - f_z_;
0262 }
0263 g /= fmin[0];
0264 CELER_ASSERT(g > 0);
0265 }
0266 else
0267 {
0268
0269 epsilon = UniformRealDistribution{epsilon_min, half}(rng);
0270 CELER_ASSERT(epsilon >= epsilon_min && epsilon <= half);
0271
0272
0273
0274 real_type delta = this->impact_parameter(epsilon);
0275 CELER_ASSERT(delta <= delta_max && delta >= delta_min);
0276
0277
0278 if (enable_lpm_)
0279 {
0280 auto screening = screening_phi(delta);
0281 auto lpm = calc_lpm_functions_(epsilon);
0282 g = half * lpm.xi
0283 * ((2 * lpm.phi + lpm.g) * screening[0]
0284 + lpm.g * screening[1] - (lpm.g + lpm.phi) * f_z_);
0285 }
0286 else
0287 {
0288 g = this->screening_f(delta)[1] - f_z_;
0289 }
0290 g /= fmin[1];
0291 CELER_ASSERT(g > 0);
0292 }
0293
0294 } while (g < generate_canonical(rng));
0295 }
0296
0297
0298 Interaction result = Interaction::from_absorption();
0299 result.secondaries = {secondaries, 2};
0300
0301
0302
0303 secondaries[0].particle_id = shared_.ids.electron;
0304 secondaries[1].particle_id = shared_.ids.positron;
0305
0306
0307 secondaries[0].energy = Energy{(1 - epsilon) * value_as<Energy>(inc_energy_)
0308 - value_as<Mass>(shared_.electron_mass)};
0309 secondaries[1].energy = Energy{epsilon * value_as<Energy>(inc_energy_)
0310 - value_as<Mass>(shared_.electron_mass)};
0311 if (BernoulliDistribution(half)(rng))
0312 {
0313 trivial_swap(secondaries[0].energy, secondaries[1].energy);
0314 }
0315
0316
0317 real_type const phi = UniformRealDistribution<real_type>(
0318 0, real_type(2 * constants::pi))(rng);
0319 auto sample_costheta = [&](Energy e) {
0320 return TsaiUrbanDistribution{e, shared_.electron_mass}(rng);
0321 };
0322
0323
0324 secondaries[0].direction
0325 = rotate(from_spherical(sample_costheta(secondaries[0].energy), phi),
0326 inc_direction_);
0327 secondaries[1].direction
0328 = rotate(from_spherical(sample_costheta(secondaries[1].energy),
0329 phi + constants::pi),
0330 inc_direction_);
0331
0332 return result;
0333 }
0334
0335
0336
0337
0338
0339
0340
0341
0342 CELER_FUNCTION real_type BetheHeitlerInteractor::impact_parameter(
0343 real_type eps) const
0344 {
0345 return screen_delta_ / (eps * (1 - eps));
0346 }
0347
0348
0349
0350
0351
0352
0353
0354
0355
0356 CELER_FUNCTION auto BetheHeitlerInteractor::screening_phi(real_type delta)
0357 -> Real2
0358 {
0359 using R = real_type;
0360
0361 Real2 result;
0362 if (delta > R(1.4))
0363 {
0364 result[0] = R(21.0190) - R(4.145) * std::log(delta + R(0.958));
0365 result[1] = result[0];
0366 }
0367 else
0368 {
0369 using PolyQuad = PolyEvaluator<real_type, 2>;
0370 result[0] = PolyQuad{20.806, -3.190, 0.5710}(delta);
0371 result[1] = PolyQuad{20.234, -2.126, 0.0903}(delta);
0372 }
0373 return result;
0374 }
0375
0376
0377
0378
0379
0380
0381
0382
0383
0384
0385
0386
0387
0388
0389
0390
0391 CELER_FUNCTION auto BetheHeitlerInteractor::screening_f(real_type delta)
0392 -> Real2
0393 {
0394 using R = real_type;
0395 auto temp = screening_phi(delta);
0396 return {3 * temp[0] - temp[1], R{1.5} * temp[0] + R{0.5} * temp[1]};
0397 }
0398
0399
0400 }