Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-10-31 08:58:40

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/interactor/LivermorePEInteractor.hh
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/ArrayUtils.hh"
0014 #include "corecel/math/PolyEvaluator.hh"
0015 #include "celeritas/Quantities.hh"
0016 #include "celeritas/em/data/LivermorePEData.hh"
0017 #include "celeritas/em/xs/LivermorePEMicroXsCalculator.hh"
0018 #include "celeritas/grid/NonuniformGridCalculator.hh"
0019 #include "celeritas/phys/CutoffView.hh"
0020 #include "celeritas/phys/Interaction.hh"
0021 #include "celeritas/phys/InteractionUtils.hh"
0022 #include "celeritas/phys/ParticleTrackView.hh"
0023 #include "celeritas/phys/Secondary.hh"
0024 
0025 #include "AtomicRelaxationHelper.hh"
0026 
0027 namespace celeritas
0028 {
0029 //---------------------------------------------------------------------------//
0030 /*!
0031  * Livermore model for the photoelectric effect.
0032  *
0033  * A parameterization of the photoelectric cross sections in two different
0034  * energy intervals, formulated as
0035  * \f[
0036  * \sigma(E)
0037      = a_1 / E + a_2 / E^2 + a_3 / E^3 + a_4 / E^4 + a_5 / E^5 + a_6 / E^6
0038      \: ,
0039  * \f]
0040  * is used. The coefficients for this model are calculated by fitting the
0041  * tabulated EPICS2014 subshell cross sections. The parameterized model applies
0042  * above approximately 5 keV; below  this limit (which depends on the atomic
0043  * number) the tabulated cross sections are used. The angle of the emitted
0044  * photoelectron is sampled from the Sauter-Gavrila distribution.
0045  *
0046  * \note This performs the same sampling routine as in Geant4's
0047  * G4LivermorePhotoElectricModel class, as documented in section 6.3.5 of the
0048  * Geant4 Physics Reference (release 10.6).
0049  */
0050 class LivermorePEInteractor
0051 {
0052   public:
0053     //!@{
0054     //! \name Type aliases
0055     using Energy = units::MevEnergy;
0056     //!@}
0057 
0058   public:
0059     // Construct with shared and state data
0060     inline CELER_FUNCTION
0061     LivermorePEInteractor(LivermorePERef const& shared,
0062                           AtomicRelaxationHelper const& relaxation,
0063                           ElementId el_id,
0064                           ParticleTrackView const& particle,
0065                           CutoffView const& cutoffs,
0066                           Real3 const& inc_direction,
0067                           StackAllocator<Secondary>& allocate);
0068 
0069     // Sample an interaction with the given RNG
0070     template<class Engine>
0071     inline CELER_FUNCTION Interaction operator()(Engine& rng);
0072 
0073   private:
0074     //// DATA ////
0075 
0076     // Shared constant physics properties
0077     LivermorePERef const& shared_;
0078     // Shared scratch space
0079     AtomicRelaxationHelper const& relaxation_;
0080     // Index in MaterialParams elements
0081     ElementId el_id_;
0082     // Production thresholds
0083     CutoffView const& cutoffs_;
0084     // Incident direction
0085     Real3 const& inc_direction_;
0086     // Incident gamma energy
0087     real_type const inc_energy_;
0088     // Allocate space for one or more secondary particles
0089     StackAllocator<Secondary>& allocate_;
0090     // Microscopic cross section calculator
0091     LivermorePEMicroXsCalculator calc_micro_xs_;
0092     // Reciprocal of the energy
0093     real_type inv_energy_;
0094 
0095     //// HELPER FUNCTIONS ////
0096 
0097     // Sample the atomic subshel
0098     template<class Engine>
0099     inline CELER_FUNCTION SubshellId sample_subshell(Engine& rng) const;
0100 
0101     // Sample the direction of the emitted photoelectron
0102     template<class Engine>
0103     inline CELER_FUNCTION Real3 sample_direction(Engine& rng) const;
0104 };
0105 
0106 //---------------------------------------------------------------------------//
0107 // INLINE DEFINITIONS
0108 //---------------------------------------------------------------------------//
0109 /*!
0110  * Construct with shared and state data.
0111  *
0112  * The incident particle must be above the energy threshold: this should be
0113  * handled in code *before* the interactor is constructed.
0114  */
0115 CELER_FUNCTION
0116 LivermorePEInteractor::LivermorePEInteractor(
0117     LivermorePERef const& shared,
0118     AtomicRelaxationHelper const& relaxation,
0119     ElementId el_id,
0120     ParticleTrackView const& particle,
0121     CutoffView const& cutoffs,
0122     Real3 const& inc_direction,
0123     StackAllocator<Secondary>& allocate)
0124     : shared_(shared)
0125     , relaxation_(relaxation)
0126     , el_id_(el_id)
0127     , cutoffs_(cutoffs)
0128     , inc_direction_(inc_direction)
0129     , inc_energy_(value_as<Energy>(particle.energy()))
0130     , allocate_(allocate)
0131     , calc_micro_xs_(shared, particle.energy())
0132 {
0133     CELER_EXPECT(particle.particle_id() == shared_.ids.gamma);
0134     CELER_EXPECT(inc_energy_ > 0);
0135 
0136     inv_energy_ = 1 / inc_energy_;
0137 }
0138 
0139 //---------------------------------------------------------------------------//
0140 /*!
0141  * Sample using the Livermore model for the photoelectric effect.
0142  */
0143 template<class Engine>
0144 CELER_FUNCTION Interaction LivermorePEInteractor::operator()(Engine& rng)
0145 {
0146     Span<Secondary> secondaries;
0147     size_type count = relaxation_ ? 1 + relaxation_.max_secondaries() : 1;
0148     if (Secondary* ptr = allocate_(count))
0149     {
0150         secondaries = {ptr, count};
0151     }
0152     else
0153     {
0154         // Failed to allocate space for secondaries
0155         return Interaction::from_failure();
0156     }
0157 
0158     // Sample atomic subshell
0159     SubshellId shell_id = this->sample_subshell(rng);
0160 
0161     // If the binding energy of the sampled shell is greater than the incident
0162     // photon energy, no secondaries are produced and the energy is deposited
0163     // locally.
0164     if (CELER_UNLIKELY(!shell_id))
0165     {
0166         Interaction result = Interaction::from_absorption();
0167         result.energy_deposition = Energy{inc_energy_};
0168         return result;
0169     }
0170 
0171     real_type binding_energy;
0172     {
0173         auto const& el = shared_.xs.elements[el_id_];
0174         auto const& shells = shared_.xs.shells[el.shells];
0175         binding_energy
0176             = value_as<Energy>(shells[shell_id.get()].binding_energy);
0177     }
0178 
0179     // Outgoing secondary is an electron
0180     CELER_ASSERT(!secondaries.empty());
0181     {
0182         Secondary& electron = secondaries.front();
0183         electron.particle_id = shared_.ids.electron;
0184 
0185         // Electron kinetic energy is the difference between the incident
0186         // photon energy and the binding energy of the shell
0187         electron.energy = Energy{inc_energy_ - binding_energy};
0188 
0189         // Direction of the emitted photoelectron is sampled from the
0190         // Sauter-Gavrila distribution
0191         electron.direction = this->sample_direction(rng);
0192     }
0193 
0194     // Construct interaction for change to primary (incident) particle
0195     Interaction result = Interaction::from_absorption();
0196     if (relaxation_)
0197     {
0198         // Sample secondaries from atomic relaxation, into all but the initial
0199         // secondary position
0200         AtomicRelaxation sample_relaxation = relaxation_.build_distribution(
0201             cutoffs_, shell_id, secondaries.subspan(1));
0202 
0203         auto outgoing = sample_relaxation(rng);
0204         secondaries = {secondaries.data(), 1 + outgoing.count};
0205 
0206         // The local energy deposition is the difference between the binding
0207         // energy of the vacancy subshell and the sum of the energies of any
0208         // secondaries created in atomic relaxation
0209         result.energy_deposition
0210             = Energy{binding_energy - value_as<Energy>(outgoing.energy)};
0211     }
0212     else
0213     {
0214         result.energy_deposition = Energy{binding_energy};
0215     }
0216     result.secondaries = secondaries;
0217 
0218     CELER_ENSURE(result.energy_deposition >= zero_quantity());
0219     return result;
0220 }
0221 
0222 //---------------------------------------------------------------------------//
0223 /*!
0224  * Sample the shell from which the photoelectron is emitted.
0225  */
0226 template<class Engine>
0227 CELER_FUNCTION SubshellId LivermorePEInteractor::sample_subshell(Engine& rng) const
0228 {
0229     LivermoreElement const& el = shared_.xs.elements[el_id_];
0230     auto const& shells = shared_.xs.shells[el.shells];
0231     size_type shell_id = 0;
0232 
0233     using Xs = RealQuantity<LivermoreSubshell::XsUnits>;
0234     real_type const cutoff = generate_canonical(rng)
0235                              * value_as<Xs>(calc_micro_xs_(el_id_));
0236     if (Energy{inc_energy_} < el.thresh_lo)
0237     {
0238         // Accumulate discrete PDF for tabulated shell cross sections
0239         //! \todo Rewrite using Selector with a remainder
0240         real_type xs = 0;
0241         real_type const inv_cube_energy = ipow<3>(inv_energy_);
0242         for (; shell_id < shells.size(); ++shell_id)
0243         {
0244             auto const& shell = shells[shell_id];
0245             if (Energy{inc_energy_} < shell.binding_energy)
0246             {
0247                 // No chance of interaction because binding energy is higher
0248                 // than incident
0249                 continue;
0250             }
0251 
0252             // Use the tabulated subshell cross sections
0253             NonuniformGridCalculator calc_xs(shell.xs, shared_.xs.reals);
0254             xs += inv_cube_energy * calc_xs(inc_energy_);
0255 
0256             if (xs > cutoff)
0257             {
0258                 break;
0259             }
0260         }
0261 
0262         if (CELER_UNLIKELY(shell_id == shells.size()))
0263         {
0264             // All shells are above incident energy (this can happen due to
0265             // a constant cross section below the lowest binding energy)
0266             return {};
0267         }
0268     }
0269     else
0270     {
0271         // Invert discrete CDF using a linear search. TODO: we could implement
0272         // an algorithm to encapsulate and later accelerate it.
0273 
0274         // Low/high index on params
0275         int const pidx = Energy{inc_energy_} < el.thresh_hi ? 0 : 1;
0276         size_type const shell_end = shells.size() - 1;
0277 
0278         for (; shell_id < shell_end; ++shell_id)
0279         {
0280             PolyEvaluator<real_type, 5> eval_poly(shells[shell_id].param[pidx]);
0281 
0282             // Calculate the *cumulative* subshell cross section (this plus all
0283             // below) from the fit parameters and energy as
0284             // \sigma(E) = a_1 / E + a_2 / E^2 + a_3 / E^3
0285             //             + a_4 / E^4 + a_5 / E^5 + a_6 / E^6.
0286             real_type xs = inv_energy_ * eval_poly(inv_energy_);
0287             if (xs > cutoff)
0288             {
0289                 break;
0290             }
0291         }
0292     }
0293 
0294     return SubshellId{shell_id};
0295 }
0296 
0297 //---------------------------------------------------------------------------//
0298 /*!
0299  * Sample a direction according to the Sauter-Gavrila distribution.
0300  *
0301  * \note The Sauter-Gavrila distribution for the K-shell is used to sample the
0302  * polar angle of a photoelectron. This performs the same sampling routine as
0303  * in Geant4's G4SauterGavrilaAngularDistribution class, as documented in
0304  * section 6.3.2 of the Geant4 Physics Reference (release 10.6) and section
0305  * 2.1.1.1 of the Penelope 2014 manual.
0306  */
0307 template<class Engine>
0308 CELER_FUNCTION Real3 LivermorePEInteractor::sample_direction(Engine& rng) const
0309 {
0310     constexpr units::MevEnergy min_energy{1.e-6};
0311     constexpr units::MevEnergy max_energy{100.};
0312 
0313     if (inc_energy_ > value_as<Energy>(max_energy))
0314     {
0315         // If the incident gamma energy is above 100 MeV, use the incident
0316         // gamma direction for the direction of the emitted photoelectron.
0317         return inc_direction_;
0318     }
0319     // If the incident energy is below 1 eV, set it to 1 eV.
0320     real_type energy_per_mecsq = max(value_as<Energy>(min_energy), inc_energy_)
0321                                  * shared_.inv_electron_mass;
0322 
0323     // Calculate Lorentz factors of the photoelectron
0324     real_type gamma = energy_per_mecsq + 1;
0325     real_type beta = std::sqrt(energy_per_mecsq * (gamma + 1)) / gamma;
0326     real_type a = (1 - beta) / beta;
0327 
0328     // Second term inside the brackets in Eq. 2.8 in the Penelope manual
0329     constexpr real_type half = 0.5;
0330     real_type b = half * beta * gamma * energy_per_mecsq * (gamma - 2);
0331 
0332     // Maximum of the rejection function g(1 - cos \theta) given in Eq. 2.8,
0333     // which is attained when 1 - cos \theta = 0
0334     real_type g_max = 2 * (1 / a + b);
0335 
0336     // Rejection loop: sample 1 - cos \theta
0337     real_type g;
0338     real_type nu;
0339     do
0340     {
0341         // Sample 1 - cos \theta from the distribution given in Eq. 2.9 using
0342         // the inverse function (Eq. 2.11)
0343         real_type u = generate_canonical(rng);
0344         nu = 2 * a * (2 * u + (a + 2) * std::sqrt(u))
0345              / (ipow<2>(a + 2) - 4 * u);
0346 
0347         // Calculate the rejection function (Eq 2.8) at the sampled value
0348         g = (2 - nu) * (1 / (a + nu) + b);
0349     } while (g < g_max * generate_canonical(rng));
0350 
0351     // Sample the azimuthal angle and calculate the direction of the
0352     // photoelectron
0353     return ExitingDirectionSampler{1 - nu, inc_direction_}(rng);
0354 }
0355 
0356 //---------------------------------------------------------------------------//
0357 }  // namespace celeritas