Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-02-22 10:31:17

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