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