![]() |
|
|||
File indexing completed on 2025-09-18 09:09:04
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/field/FieldPropagator.hh 0006 //---------------------------------------------------------------------------// 0007 #pragma once 0008 0009 #include "corecel/Macros.hh" 0010 #include "corecel/Types.hh" 0011 #include "corecel/math/Algorithms.hh" 0012 #include "corecel/math/ArrayOperators.hh" 0013 #include "corecel/math/NumericLimits.hh" 0014 #include "geocel/Types.hh" 0015 #include "celeritas/geo/GeoTrackView.hh" 0016 #include "celeritas/phys/ParticleTrackView.hh" 0017 0018 #include "Types.hh" 0019 0020 #include "detail/FieldUtils.hh" 0021 0022 namespace celeritas 0023 { 0024 //---------------------------------------------------------------------------// 0025 /*! 0026 * Propagate a charged particle in a field. 0027 * 0028 * For a given initial state (position, momentum), it propagates a charged 0029 * particle along a curved trajectory up to an interaction length proposed by 0030 * a chosen physics process for the step, possibly integrating sub-steps by 0031 * an adaptive step control with a required accuracy of tracking in a 0032 * field. It updates the final state (position, momentum, boundary) along with 0033 * the step actually taken. If the final position is outside the current 0034 * volume, it returns a geometry limited step and the state at the 0035 * intersection between the curve trajectory and the first volume boundary 0036 * using an iterative step control method within a tolerance error imposed on 0037 * the closest distance between two positions by the field stepper and the 0038 * linear projection to the volume boundary. 0039 * 0040 * \note This follows similar methods as in Geant4's G4PropagatorInField class. 0041 */ 0042 template<class SubstepperT, class GTV> 0043 class FieldPropagator 0044 { 0045 public: 0046 //!@{ 0047 //! \name Type aliases 0048 using result_type = Propagation; 0049 //!@} 0050 0051 public: 0052 // Construct with shared parameters and the field driver 0053 inline CELER_FUNCTION FieldPropagator(SubstepperT&& advance, 0054 ParticleTrackView const& particle, 0055 GTV&& geo); 0056 0057 // Move track to next volume boundary. 0058 inline CELER_FUNCTION result_type operator()(); 0059 0060 // Move track up to a user-provided distance, or to the next boundary 0061 inline CELER_FUNCTION result_type operator()(real_type dist); 0062 0063 //! Whether it's possible to have tracks that are looping 0064 static CELER_CONSTEXPR_FUNCTION bool tracks_can_loop() { return true; } 0065 0066 //! Limit on substeps 0067 inline CELER_FUNCTION short int max_substeps() const; 0068 0069 // Intersection tolerance 0070 inline CELER_FUNCTION real_type delta_intersection() const; 0071 0072 // Distance to bump or to consider a "zero" movement 0073 inline CELER_FUNCTION real_type bump_distance() const; 0074 0075 // Smallest allowable inner loop distance to take 0076 inline CELER_FUNCTION real_type minimum_substep() const; 0077 0078 private: 0079 //// DATA //// 0080 0081 SubstepperT advance_; 0082 GTV geo_; 0083 OdeState state_; 0084 }; 0085 0086 //---------------------------------------------------------------------------// 0087 // DEDUCTION GUIDES 0088 //---------------------------------------------------------------------------// 0089 template<class SubstepperT, class GTV> 0090 CELER_FUNCTION FieldPropagator(SubstepperT&&, 0091 ParticleTrackView const&, 0092 GTV&&) -> FieldPropagator<SubstepperT, GTV>; 0093 0094 //---------------------------------------------------------------------------// 0095 // INLINE DEFINITIONS 0096 //---------------------------------------------------------------------------// 0097 /*! 0098 * Construct with shared field parameters and the field driver. 0099 */ 0100 template<class SubstepperT, class GTV> 0101 CELER_FUNCTION FieldPropagator<SubstepperT, GTV>::FieldPropagator( 0102 SubstepperT&& advance, ParticleTrackView const& particle, GTV&& geo) 0103 : advance_(::celeritas::forward<SubstepperT>(advance)) 0104 , geo_(::celeritas::forward<GTV>(geo)) 0105 { 0106 using MomentumUnits = OdeState::MomentumUnits; 0107 0108 state_.pos = geo_.pos(); 0109 state_.mom = value_as<MomentumUnits>(particle.momentum()) * geo_.dir(); 0110 } 0111 0112 //---------------------------------------------------------------------------// 0113 /*! 0114 * Propagate a charged particle until it hits a boundary. 0115 */ 0116 template<class SubstepperT, class GTV> 0117 CELER_FUNCTION auto 0118 FieldPropagator<SubstepperT, GTV>::operator()() -> result_type 0119 { 0120 return (*this)(numeric_limits<real_type>::infinity()); 0121 } 0122 0123 //---------------------------------------------------------------------------// 0124 /*! 0125 * Propagate a charged particle in a field. 0126 * 0127 * It utilises a field driver (based on an adaptive step control to limit the 0128 * length traveled based on the magnetic field behavior and geometric 0129 * tolerances) to track a charged particle along a curved trajectory for a 0130 * given step length within a required accuracy or intersects 0131 * with a new volume (geometry limited step). 0132 * 0133 * The position of the internal OdeState `state_` should be consistent with the 0134 * geometry `geo_`'s position, but the geometry's direction will be a series 0135 * of "trial" directions that are the chords between the start and end points 0136 * of a curved substep through the field. At the end of the propagation step, 0137 * the geometry state's direction is updated based on the actual value of the 0138 * calculated momentum. 0139 * 0140 * Caveats: 0141 * - The physical (geometry track state) position may deviate from the exact 0142 * curved propagation position up to a driver-based tolerance at every 0143 * boundary crossing. The momentum will always be conserved, though. 0144 * - In some unusual cases (e.g. a very small caller-requested step, or an 0145 * unusual accumulation in the driver's substeps) the distance returned may 0146 * be slightly higher (again, up to a driver-based tolerance) than the 0147 * physical distance travelled. 0148 */ 0149 template<class SubstepperT, class GTV> 0150 CELER_FUNCTION auto 0151 FieldPropagator<SubstepperT, GTV>::operator()(real_type step) -> result_type 0152 { 0153 CELER_EXPECT(step > 0); 0154 result_type result; 0155 result.boundary = geo_.is_on_boundary(); 0156 result.distance = 0; 0157 0158 // Break the curved steps into substeps as determined by the driver *and* 0159 // by the proximity of geometry boundaries. Test for intersection with the 0160 // geometry boundary in each substep. This loop is guaranteed to converge 0161 // since the trial step always decreases *or* the actual position advances. 0162 real_type remaining = step; 0163 auto remaining_substeps = this->max_substeps(); 0164 do 0165 { 0166 CELER_ASSERT(soft_zero(distance(state_.pos, geo_.pos()))); 0167 CELER_ASSERT(result.boundary == geo_.is_on_boundary()); 0168 0169 // Advance up to (but probably less than) the remaining step length 0170 Substep substep = advance_(remaining, state_); 0171 CELER_ASSERT(substep.length > 0 && substep.length <= remaining); 0172 0173 // Check whether the chord for this sub-step intersects a boundary 0174 auto chord = detail::make_chord(state_.pos, substep.state.pos); 0175 0176 // Do a detailed boundary check from the start position toward 0177 // the substep end point. 0178 // Travel to the end of the chord, plus a little extra. 0179 if (chord.length >= this->minimum_substep()) 0180 { 0181 // Only update the direction if the chord length is nontrivial. 0182 // This is usually the case but might be skipped in two cases: 0183 // - if the initial step is very small compared to the 0184 // magnitude of the position (which can result in a zero length 0185 // for the chord and NaNs for the direction) 0186 // - in a high-curvature track where the remaining distance is just 0187 // barely above the remaining minimum step (in which case our 0188 // boundary test does lose some accuracy) 0189 geo_.set_dir(chord.dir); 0190 } 0191 auto linear_step 0192 = geo_.find_next_step(chord.length + this->delta_intersection()); 0193 0194 // Scale the effective substep length to travel by the fraction along 0195 // the chord to the boundary. This value can be slightly larger than 1 0196 // because we search up a little past the endpoint (thanks to the delta 0197 // intersection). 0198 real_type const update_length = substep.length * linear_step.distance 0199 / chord.length; 0200 0201 if (!linear_step.boundary) 0202 { 0203 // No boundary intersection along the chord: accept substep 0204 // movement inside the current volume and reset the remaining 0205 // distance so we can continue toward the next boundary or end of 0206 // caller-requested step. Reset the boundary flag to "false" only 0207 // in the unlikely case that we successfully shortened the substep 0208 // on a reentrant boundary crossing below. 0209 state_ = substep.state; 0210 result.boundary = false; 0211 result.distance += substep.length; 0212 remaining = step - result.distance; 0213 geo_.move_internal(state_.pos); 0214 --remaining_substeps; 0215 } 0216 else if (CELER_UNLIKELY(result.boundary 0217 && (linear_step.distance) 0218 < this->bump_distance())) 0219 { 0220 // Likely heading back into the old volume when starting on a 0221 // surface (this can happen when tracking through a volume at a 0222 // near tangent). Reduce substep size and try again. 0223 remaining = substep.length / 2; 0224 } 0225 else if (update_length <= this->minimum_substep() 0226 || detail::is_intercept_close(state_.pos, 0227 chord.dir, 0228 linear_step.distance, 0229 substep.state.pos, 0230 this->delta_intersection()) 0231 || chord.length == 0) 0232 { 0233 // We're close enough to the boundary that the next trial step 0234 // would be less than the driver's minimum step. 0235 // *OR* 0236 // The straight-line intersection point is a distance less than 0237 // `delta_intersection` from the substep's end position. 0238 // Commit the proposed state's momentum, use the 0239 // post-boundary-crossing track position for consistency, and 0240 // conservatively reduce the *reported* traveled distance to avoid 0241 // coincident boundary crossings. 0242 0243 // Only cross the boundary if the intersect point is less 0244 // than or exactly on the boundary, or if the crossing 0245 // doesn't put us past the end of the step 0246 result.boundary = (linear_step.distance <= chord.length 0247 || result.distance + update_length <= step 0248 || chord.length == 0); 0249 0250 if (!result.boundary) 0251 { 0252 // Don't move to the boundary, but instead move to the end of 0253 // the substep. This should result in basically the same effect 0254 // as "!linear_step.boundary" above. 0255 state_.pos = substep.state.pos; 0256 geo_.move_internal(substep.state.pos); 0257 } 0258 0259 // The update length can be slightly greater than the substep due 0260 // to the extra delta_intersection boost when searching. The 0261 // substep itself can be more than the requested step. 0262 result.distance += celeritas::min(update_length, substep.length); 0263 state_.mom = substep.state.mom; 0264 remaining = 0; 0265 } 0266 else 0267 { 0268 // The straight-line intercept is too far from substep's end state. 0269 // Decrease the allowed substep (curved path distance) by the 0270 // fraction along the chord, and retry the driver step. 0271 remaining = update_length; 0272 } 0273 } while (remaining > this->minimum_substep() && remaining_substeps > 0); 0274 0275 if (remaining_substeps == 0 && result.distance < step) 0276 { 0277 // Flag track as looping if the max number of substeps was reached 0278 // without hitting a boundary or moving the full step length 0279 result.looping = true; 0280 } 0281 else if (result.distance > 0) 0282 { 0283 if (result.boundary) 0284 { 0285 // We moved to a new boundary. Update the position to reflect the 0286 // geometry's state (and possibly "bump" the ODE state's position 0287 // because of the tolerance in the intercept checks above). 0288 geo_.move_to_boundary(); 0289 state_.pos = geo_.pos(); 0290 } 0291 else if (CELER_UNLIKELY(result.distance < step)) 0292 { 0293 // Even though the track traveled the full step length, the 0294 // distance might be slightly less than the step due to roundoff 0295 // error. Reset the distance so the track's action isn't 0296 // erroneously set as propagation-limited. 0297 CELER_ASSERT(soft_equal(result.distance, step)); 0298 result.distance = step; 0299 } 0300 } 0301 0302 // Even though the along-substep movement was through chord lengths, 0303 // conserve momentum through the field change by updating the final 0304 // *direction* based on the state's momentum. 0305 Real3 dir = make_unit_vector(state_.mom); 0306 geo_.set_dir(dir); 0307 0308 if (CELER_UNLIKELY(result.distance == 0)) 0309 { 0310 // We failed to move at all, which means we hit a boundary no matter 0311 // what step length we took, which means we're stuck. 0312 // Using the just-reapplied direction, hope that we're pointing deeper 0313 // into the current volume and bump the particle. 0314 result.distance = celeritas::min(this->bump_distance(), step); 0315 result.boundary = false; 0316 axpy(result.distance, dir, &state_.pos); 0317 geo_.move_internal(state_.pos); 0318 } 0319 else 0320 { 0321 CELER_ENSURE(result.boundary == geo_.is_on_boundary()); 0322 } 0323 0324 // Due to accumulation errors from multiple substeps or chord-finding 0325 // within the driver, the distance may be very slightly beyond the 0326 // requested step. 0327 CELER_ENSURE( 0328 result.distance > 0 0329 && (result.distance <= step || soft_equal(result.distance, step))); 0330 return result; 0331 } 0332 0333 //---------------------------------------------------------------------------// 0334 /*! 0335 * Distance close enough to a boundary to mark as being on the boundary. 0336 * 0337 * TODO: change delta intersection from property in FieldDriverOptions to 0338 * another FieldPropagatorOptions 0339 */ 0340 template<class SubstepperT, class GTV> 0341 CELER_FUNCTION real_type 0342 FieldPropagator<SubstepperT, GTV>::delta_intersection() const 0343 { 0344 return advance_.delta_intersection(); 0345 } 0346 0347 //---------------------------------------------------------------------------// 0348 /*! 0349 * Maximum number of substeps. 0350 */ 0351 template<class SubstepperT, class GTV> 0352 CELER_FUNCTION short int FieldPropagator<SubstepperT, GTV>::max_substeps() const 0353 { 0354 return advance_.max_substeps(); 0355 } 0356 0357 //---------------------------------------------------------------------------// 0358 /*! 0359 * Distance to bump or to consider a "zero" movement. 0360 */ 0361 template<class SubstepperT, class GTV> 0362 CELER_FUNCTION real_type FieldPropagator<SubstepperT, GTV>::minimum_substep() const 0363 { 0364 return advance_.minimum_step(); 0365 } 0366 0367 //---------------------------------------------------------------------------// 0368 /*! 0369 * Distance to bump or to consider a "zero" movement. 0370 */ 0371 template<class SubstepperT, class GTV> 0372 CELER_FUNCTION real_type FieldPropagator<SubstepperT, GTV>::bump_distance() const 0373 { 0374 return this->delta_intersection() * real_type(0.1); 0375 } 0376 0377 //---------------------------------------------------------------------------// 0378 } // 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 |
![]() ![]() |