Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-09-18 09:09:14

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/phys/PhysicsStepUtils.hh
0006 //! \brief Helper functions for physics step processing
0007 //---------------------------------------------------------------------------//
0008 #pragma once
0009 
0010 #include "corecel/Assert.hh"
0011 #include "corecel/Types.hh"
0012 #include "corecel/cont/Range.hh"
0013 #include "corecel/math/Algorithms.hh"
0014 #include "corecel/math/NumericLimits.hh"
0015 #include "corecel/random/distribution/GenerateCanonical.hh"
0016 #include "corecel/random/distribution/Selector.hh"
0017 #include "celeritas/Types.hh"
0018 #include "celeritas/grid/EnergyLossCalculator.hh"
0019 #include "celeritas/grid/InverseRangeCalculator.hh"
0020 #include "celeritas/grid/RangeCalculator.hh"
0021 #include "celeritas/grid/SplineCalculator.hh"
0022 #include "celeritas/grid/XsCalculator.hh"
0023 #include "celeritas/mat/MaterialTrackView.hh"
0024 
0025 #include "ParticleTrackView.hh"
0026 #include "PhysicsStepView.hh"
0027 #include "PhysicsTrackView.hh"
0028 
0029 namespace celeritas
0030 {
0031 namespace
0032 {
0033 //---------------------------------------------------------------------------//
0034 /*!
0035  * Sample the process for the discrete interaction.
0036  */
0037 template<class Engine>
0038 inline CELER_FUNCTION ParticleProcessId
0039 find_ppid(MaterialView const& material,
0040           ParticleTrackView const& particle,
0041           PhysicsTrackView const& physics,
0042           PhysicsStepView& pstep,
0043           Engine& rng)
0044 {
0045     if (physics.at_rest_process() && particle.is_stopped())
0046     {
0047         // If the particle is stopped and has an at-rest process, select it
0048         return physics.at_rest_process();
0049     }
0050 
0051     // Sample the process from the pre-calculated per-process cross section
0052     ParticleProcessId ppid = celeritas::make_selector(
0053         [&pstep](ParticleProcessId ppid) { return pstep.per_process_xs(ppid); },
0054         ParticleProcessId{physics.num_particle_processes()},
0055         pstep.macro_xs())(rng);
0056     CELER_ASSERT(ppid);
0057 
0058     // Determine if the discrete interaction occurs for particles with energy
0059     // loss processes
0060     if (physics.integral_xs_process(ppid))
0061     {
0062         // Recalculate the cross section at the post-step energy \f$ E_1 \f$
0063         real_type xs = physics.calc_xs(ppid, material, particle.energy());
0064 
0065         // The discrete interaction occurs with probability \f$ \sigma(E_1) /
0066         // \sigma_{\max} \f$. Note that it's possible for \f$ \sigma(E_1) \f$
0067         // to be larger than the estimate of the maximum cross section over the
0068         // step \f$ \sigma_{\max} \f$.
0069         if (generate_canonical(rng) * pstep.per_process_xs(ppid) > xs)
0070         {
0071             // No interaction occurs; reset the physics state and continue
0072             // tracking
0073             return {};
0074         }
0075     }
0076     return ppid;
0077 }
0078 
0079 //---------------------------------------------------------------------------//
0080 }  // namespace
0081 
0082 //---------------------------------------------------------------------------//
0083 /*!
0084  * Calculate physics step limits based on cross sections and range limiters.
0085  */
0086 inline CELER_FUNCTION StepLimit
0087 calc_physics_step_limit(MaterialTrackView const& material,
0088                         ParticleTrackView const& particle,
0089                         PhysicsTrackView& physics,
0090                         PhysicsStepView& pstep)
0091 {
0092     CELER_EXPECT(physics.has_interaction_mfp());
0093 
0094     /*! \todo For particles with decay, macro XS calculation will incorporate
0095      * decay probability, dividing decay constant by speed to become 1/len to
0096      * compete with interactions.
0097      *
0098      * \todo For neutral particles that haven't changed material since the last
0099      * step, we can reuse the previously calculated cross section.
0100      */
0101 
0102     // Loop over all processes that apply to this track (based on particle
0103     // type) and calculate cross section and particle range.
0104     real_type total_macro_xs = 0;
0105     for (auto ppid : range(ParticleProcessId{physics.num_particle_processes()}))
0106     {
0107         real_type process_xs = 0;
0108         if (auto const& process = physics.integral_xs_process(ppid))
0109         {
0110             // If the integral approach is used and this particle has an energy
0111             // loss process, estimate the maximum cross section over the step
0112             process_xs = physics.calc_max_xs(
0113                 process, ppid, material.material_record(), particle.energy());
0114         }
0115         else
0116         {
0117             // Calculate the macroscopic cross section for this process
0118             process_xs = physics.calc_xs(
0119                 ppid, material.material_record(), particle.energy());
0120         }
0121         // Accumulate process cross section into the total cross section and
0122         // save it for later
0123         total_macro_xs += process_xs;
0124         pstep.per_process_xs(ppid) = process_xs;
0125     }
0126     pstep.macro_xs(total_macro_xs);
0127     CELER_ASSERT(total_macro_xs > 0 || !particle.is_stopped());
0128 
0129     // Determine limits from discrete interactions
0130     StepLimit limit;
0131     limit.action = physics.scalars().discrete_action();
0132     if (particle.is_stopped())
0133     {
0134         limit.step = 0;
0135     }
0136     else
0137     {
0138         limit.step = physics.interaction_mfp() / total_macro_xs;
0139         if (auto grid_id = physics.range_grid())
0140         {
0141             auto calc_range = physics.make_calculator<RangeCalculator>(grid_id);
0142             real_type range = calc_range(particle.energy());
0143             // Save range for the current step and reuse it elsewhere
0144             physics.dedx_range(range);
0145 
0146             // Convert to the scaled range
0147             real_type eloss_step = physics.range_to_step(range);
0148             if (eloss_step <= limit.step)
0149             {
0150                 limit.step = eloss_step;
0151                 limit.action = physics.scalars().range_action();
0152             }
0153 
0154             // Limit charged particle step size
0155             real_type fixed_limit = physics.scalars().fixed_step_limiter;
0156             if (fixed_limit > 0 && fixed_limit < limit.step)
0157             {
0158                 limit.step = fixed_limit;
0159                 limit.action = physics.scalars().fixed_step_action;
0160             }
0161         }
0162         else if (physics.num_particle_processes() == 0)
0163         {
0164             // Clear post-step action so that unknown particles don't interact
0165             limit.action = {};
0166         }
0167     }
0168 
0169     return limit;
0170 }
0171 
0172 //---------------------------------------------------------------------------//
0173 /*!
0174  * Calculate mean energy loss over the given "true" step length.
0175  *
0176  * Stopping power is an integral over low-exiting-energy
0177  * secondaries. Above some threshold energy \em T_c we treat exiting
0178  * secondaries discretely; below it, we lump them into this continuous loss
0179  * term that varies based on the energy, the atomic number density, and the
0180  * element number:
0181  * \f[
0182  *   \frac{dE}{dx} = N_Z \int_0^{T_c} \frac{d \sigma_Z(E, T)}{dT} T dT
0183  * \f]
0184  * Here, the cross section is a function of the primary's energy \em E and the
0185  * exiting secondary energy \em T.
0186  *
0187  * The stopping range \em R due to these low-energy processes is an integral
0188  * over the inverse of the stopping power: basically the distance that will
0189  * take a particle (without discrete processes at play) from its current energy
0190  * to an energy of zero.
0191  * \f[
0192  *   R = \int_0 ^{E_0} - \frac{dx}{dE} dE .
0193  * \f]
0194  *
0195  * Both Celeritas and Geant4 approximate the range limit as the minimum range
0196  * over all processes, rather than the range as a result from integrating all
0197  * energy loss processes over the allowed energy range. This is usually not
0198  * a problem in practice because the range will get automatically decreased by
0199  * \c range_to_step , and the above range calculation neglects energy loss by
0200  * discrete processes.
0201  *
0202  * Both Geant4 and Celeritas integrate the energy loss terms across processes
0203  * to get a single energy loss vector per particle type. The range table is an
0204  * integral of the mean stopping power: the total distance for the particle's
0205  * energy to reach zero.
0206  *
0207  * \note The inverse range correction assumes range is always the integral of
0208  * the stopping power/energy loss.
0209  *
0210  * \todo The GEANT3 manual \cite{geant3-1993} makes the point that linear
0211  * interpolation of energy
0212  * loss rate results in a piecewise constant energy deposition curve, which is
0213  * why they use spline interpolation. Investigate higher-order reconstruction
0214  * of energy loss curve, e.g. through spline-based interpolation or log-log
0215  * interpolation.
0216  *
0217  * \note See section 7.2.4 Run Time Energy Loss Computation of the Geant4
0218  * physics manual \cite{g4prm}. See also the longer discussions in section 8
0219  * of PHYS010 of the Geant3 manual.
0220  *
0221  * Zero energy loss can occur in the following cases:
0222  * - The energy loss value at the given energy is zero (e.g. high energy
0223  * particles)
0224  * - The urban model is selected and samples zero collisions (possible in thin
0225  * materials and/or small steps)
0226  */
0227 inline CELER_FUNCTION ParticleTrackView::Energy
0228 calc_mean_energy_loss(ParticleTrackView const& particle,
0229                       PhysicsTrackView const& physics,
0230                       real_type step)
0231 {
0232     CELER_EXPECT(step > 0);
0233     using Energy = ParticleTrackView::Energy;
0234     static_assert(Energy::unit_type::value()
0235                       == EnergyLossCalculator::Energy::unit_type::value(),
0236                   "Incompatible energy types");
0237 
0238     Energy const pre_step_energy = particle.energy();
0239 
0240     // Calculate the sum of energy loss rate over all processes.
0241     Energy eloss;
0242     {
0243         auto grid_id = physics.energy_loss_grid();
0244         CELER_ASSERT(grid_id);
0245 
0246         auto calc_eloss_rate = physics.make_calculator<XsCalculator>(grid_id);
0247         eloss = Energy{step * calc_eloss_rate(pre_step_energy)};
0248     }
0249 
0250     if (eloss >= pre_step_energy * physics.scalars().linear_loss_limit)
0251     {
0252         // Enough energy is lost over this step that the dE/dx linear
0253         // approximation is probably wrong. Use the definition of the range as
0254         // the integral of 1/loss to back-calculate the actual energy loss
0255         // along the curve given the actual step.
0256         auto grid_id = physics.range_grid();
0257         CELER_ASSERT(grid_id);
0258 
0259         // Use the range limit stored from calc_physics_step_limit
0260         real_type range = physics.dedx_range();
0261         if (step == range)
0262         {
0263             // NOTE: eloss should be pre_step_energy at this point only if the
0264             // range was the step limiter (step == range), and if the
0265             // range-to-step conversion was 1.
0266             return pre_step_energy;
0267         }
0268         CELER_ASSERT(range > step);
0269 
0270         // Calculate energy along the range curve corresponding to the actual
0271         // step taken: this gives the exact energy loss over the step due to
0272         // this process. If the step is very near the range (a few ULP off, for
0273         // example), then the post-step energy will be calculated as zero
0274         // without going through the condition above.
0275         auto calc_energy = physics.make_calculator<InverseRangeCalculator>(
0276             physics.inverse_range_grid());
0277         eloss = pre_step_energy - calc_energy(range - step);
0278 
0279         // Spline interpolation does not ensure roundtrip consistency between
0280         // the range and the inverse. This can lead to negative values for the
0281         // energy loss
0282         eloss = clamp_to_nonneg(eloss);
0283     }
0284 
0285     CELER_ENSURE(eloss >= zero_quantity());
0286     return eloss;
0287 }
0288 
0289 //---------------------------------------------------------------------------//
0290 /*!
0291  * Choose the physics model for a track's pending interaction.
0292  *
0293  * - If the interaction MFP is zero, the particle is undergoing a discrete
0294  *   interaction. Otherwise, the result is a false ActionId.
0295  * - Sample from the previously calculated per-process cross section/decay to
0296  *   determine the interacting process ID.
0297  * - From the process ID and (post-slowing-down) particle energy, we obtain the
0298  *   applicable model ID.
0299  * - For energy loss (continuous-discrete) processes, the post-step energy will
0300  *   be different from the pre-step energy, so the assumption that the cross
0301  *   section is constant along the step is no longer valid. Use the "integral
0302  *   approach" to sample the discrete interaction from the correct probability
0303  *   distribution (section 7.4 of the Geant4 Physics Reference release 10.6).
0304  */
0305 template<class Engine>
0306 CELER_FUNCTION ActionId
0307 select_discrete_interaction(MaterialView const& material,
0308                             ParticleTrackView const& particle,
0309                             PhysicsTrackView const& physics,
0310                             PhysicsStepView& pstep,
0311                             Engine& rng)
0312 {
0313     // Nonzero MFP to interaction -- no interaction model
0314     CELER_EXPECT(physics.interaction_mfp() <= 0);
0315     CELER_EXPECT(pstep.macro_xs() > 0);
0316 
0317     // Sample the process from the pre-calculated cross sections
0318     auto ppid = find_ppid(material, particle, physics, pstep, rng);
0319     if (!ppid)
0320     {
0321         return physics.scalars().integral_rejection_action();
0322     }
0323 
0324     // Find the model that applies at the particle energy
0325     auto find_model = physics.make_model_finder(ppid);
0326     auto pmid = find_model(particle.energy());
0327 
0328     ElementComponentId elcomp_id{};
0329     if (material.num_elements() == 1)
0330     {
0331         elcomp_id = ElementComponentId{0};
0332     }
0333     else if (auto table_id = physics.value_table(pmid))
0334     {
0335         // Sample an element for discrete interactions that require it and for
0336         // materials with more than one element
0337         auto select_element
0338             = physics.make_element_selector(table_id, particle.energy());
0339         elcomp_id = select_element(rng);
0340     }
0341     pstep.element(elcomp_id);
0342 
0343     return physics.model_to_action(physics.model_id(pmid));
0344 }
0345 
0346 //---------------------------------------------------------------------------//
0347 }  // namespace celeritas