Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-01-06 10:05:28

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