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