![]() |
|
|||
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
[ Source navigation ] | [ Diff markup ] | [ Identifier search ] | [ general search ] |
This page was automatically generated by the 2.3.7 LXR engine. The LXR team |
![]() ![]() |