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