Back to home page

EIC code displayed by LXR

 
 

    


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

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