![]() |
|
|||
File indexing completed on 2025-02-22 10:31:21
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/field/FieldPropagator.hh 0007 //---------------------------------------------------------------------------// 0008 #pragma once 0009 0010 #include "corecel/Macros.hh" 0011 #include "corecel/Types.hh" 0012 #include "corecel/math/Algorithms.hh" 0013 #include "corecel/math/ArrayOperators.hh" 0014 #include "corecel/math/NumericLimits.hh" 0015 #include "geocel/Types.hh" 0016 #include "celeritas/geo/GeoTrackView.hh" 0017 #include "celeritas/phys/ParticleTrackView.hh" 0018 0019 #include "Types.hh" 0020 0021 #include "detail/FieldUtils.hh" 0022 0023 namespace celeritas 0024 { 0025 //---------------------------------------------------------------------------// 0026 /*! 0027 * Propagate a charged particle in a field. 0028 * 0029 * For a given initial state (position, momentum), it propagates a charged 0030 * particle along a curved trajectory up to an interaction length proposed by 0031 * a chosen physics process for the step, possibly integrating sub-steps by 0032 * an adaptive step control with a required accuracy of tracking in a 0033 * field. It updates the final state (position, momentum, boundary) along with 0034 * the step actually taken. If the final position is outside the current 0035 * volume, it returns a geometry limited step and the state at the 0036 * intersection between the curve trajectory and the first volume boundary 0037 * using an iterative step control method within a tolerance error imposed on 0038 * the closest distance between two positions by the field stepper and the 0039 * linear projection to the volume boundary. 0040 * 0041 * \note This follows similar methods as in Geant4's G4PropagatorInField class. 0042 */ 0043 template<class DriverT, class GTV> 0044 class FieldPropagator 0045 { 0046 public: 0047 //!@{ 0048 //! \name Type aliases 0049 using result_type = Propagation; 0050 //!@} 0051 0052 public: 0053 // Construct with shared parameters and the field driver 0054 inline CELER_FUNCTION FieldPropagator(DriverT&& driver, 0055 ParticleTrackView const& particle, 0056 GTV&& geo); 0057 0058 // Move track to next volume boundary. 0059 inline CELER_FUNCTION result_type operator()(); 0060 0061 // Move track up to a user-provided distance, or to the next boundary 0062 inline CELER_FUNCTION result_type operator()(real_type dist); 0063 0064 //! Whether it's possible to have tracks that are looping 0065 static CELER_CONSTEXPR_FUNCTION bool tracks_can_loop() { return true; } 0066 0067 //! Limit on substeps 0068 inline CELER_FUNCTION short int max_substeps() const; 0069 0070 // Intersection tolerance 0071 inline CELER_FUNCTION real_type delta_intersection() const; 0072 0073 // Distance to bump or to consider a "zero" movement 0074 inline CELER_FUNCTION real_type bump_distance() const; 0075 0076 // Smallest allowable inner loop distance to take 0077 inline CELER_FUNCTION real_type minimum_substep() const; 0078 0079 private: 0080 //// DATA //// 0081 0082 DriverT driver_; 0083 GTV geo_; 0084 OdeState state_; 0085 }; 0086 0087 //---------------------------------------------------------------------------// 0088 // DEDUCTION GUIDES 0089 //---------------------------------------------------------------------------// 0090 template<class DriverT, class GTV> 0091 CELER_FUNCTION FieldPropagator(DriverT&&, 0092 ParticleTrackView const&, 0093 GTV&&) -> FieldPropagator<DriverT, GTV>; 0094 0095 //---------------------------------------------------------------------------// 0096 // INLINE DEFINITIONS 0097 //---------------------------------------------------------------------------// 0098 /*! 0099 * Construct with shared field parameters and the field driver. 0100 */ 0101 template<class DriverT, class GTV> 0102 CELER_FUNCTION FieldPropagator<DriverT, GTV>::FieldPropagator( 0103 DriverT&& driver, ParticleTrackView const& particle, GTV&& geo) 0104 : driver_(::celeritas::forward<DriverT>(driver)) 0105 , geo_(::celeritas::forward<GTV>(geo)) 0106 { 0107 using MomentumUnits = OdeState::MomentumUnits; 0108 0109 state_.pos = geo_.pos(); 0110 state_.mom = value_as<MomentumUnits>(particle.momentum()) * geo_.dir(); 0111 } 0112 0113 //---------------------------------------------------------------------------// 0114 /*! 0115 * Propagate a charged particle until it hits a boundary. 0116 */ 0117 template<class DriverT, class GTV> 0118 CELER_FUNCTION auto FieldPropagator<DriverT, 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 DriverT, class GTV> 0150 CELER_FUNCTION auto 0151 FieldPropagator<DriverT, 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 DriverResult substep = driver_.advance(remaining, state_); 0171 CELER_ASSERT(substep.step > 0 && substep.step <= 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 check boundary check from the start position toward 0177 // the substep end point. Travel to the end of the chord, plus a little 0178 // 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.step * 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.step; 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.step / 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.step); 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 DriverT, class GTV> 0341 CELER_FUNCTION real_type FieldPropagator<DriverT, GTV>::delta_intersection() const 0342 { 0343 return driver_.delta_intersection(); 0344 } 0345 0346 //---------------------------------------------------------------------------// 0347 /*! 0348 * Maximum number of substeps. 0349 */ 0350 template<class DriverT, class GTV> 0351 CELER_FUNCTION short int FieldPropagator<DriverT, GTV>::max_substeps() const 0352 { 0353 return driver_.max_substeps(); 0354 } 0355 0356 //---------------------------------------------------------------------------// 0357 /*! 0358 * Distance to bump or to consider a "zero" movement. 0359 */ 0360 template<class DriverT, class GTV> 0361 CELER_FUNCTION real_type FieldPropagator<DriverT, GTV>::minimum_substep() const 0362 { 0363 return driver_.minimum_step(); 0364 } 0365 0366 //---------------------------------------------------------------------------// 0367 /*! 0368 * Distance to bump or to consider a "zero" movement. 0369 */ 0370 template<class DriverT, class GTV> 0371 CELER_FUNCTION real_type FieldPropagator<DriverT, 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 |
![]() ![]() |