Back to home page

EIC code displayed by LXR

 
 

    


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