Back to home page

EIC code displayed by LXR

 
 

    


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