Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-01-30 10:17:13

0001 //----------------------------------*-C++-*----------------------------------//
0002 // Copyright 2021-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 orange/univ/SimpleUnitTracker.hh
0007 //---------------------------------------------------------------------------//
0008 #pragma once
0009 
0010 #include "corecel/Assert.hh"
0011 #include "corecel/math/Algorithms.hh"
0012 #include "orange/OrangeData.hh"
0013 #include "orange/detail/BIHTraverser.hh"
0014 #include "orange/surf/LocalSurfaceVisitor.hh"
0015 
0016 #include "detail/InfixEvaluator.hh"
0017 #include "detail/LogicEvaluator.hh"
0018 #include "detail/SenseCalculator.hh"
0019 #include "detail/SurfaceFunctors.hh"
0020 #include "detail/Types.hh"
0021 #include "detail/Utils.hh"
0022 
0023 namespace celeritas
0024 {
0025 //---------------------------------------------------------------------------//
0026 /*!
0027  * Track a particle in a universe of well-connected volumes.
0028  *
0029  * The simple unit tracker is based on a set of non-overlapping volumes
0030  * comprised of surfaces. It is a faster but less "user-friendly" version of
0031  * the masked unit tracker because it requires all volumes to be exactly
0032  * defined by their connected surfaces. It does *not* check for overlaps.
0033  */
0034 class SimpleUnitTracker
0035 {
0036   public:
0037     //!@{
0038     //! \name Type aliases
0039     using ParamsRef = NativeCRef<OrangeParamsData>;
0040     using Initialization = detail::Initialization;
0041     using Intersection = detail::Intersection;
0042     using LocalState = detail::LocalState;
0043     //!@}
0044 
0045   public:
0046     // Construct with parameters (unit definitions and this one's ID)
0047     inline CELER_FUNCTION
0048     SimpleUnitTracker(ParamsRef const& params, SimpleUnitId id);
0049 
0050     //// ACCESSORS ////
0051 
0052     //! Number of local volumes
0053     CELER_FUNCTION LocalVolumeId::size_type num_volumes() const
0054     {
0055         return unit_record_.volumes.size();
0056     }
0057 
0058     //! Number of local surfaces
0059     CELER_FUNCTION LocalSurfaceId::size_type num_surfaces() const
0060     {
0061         return unit_record_.surfaces.size();
0062     }
0063 
0064     //! SimpleUnitRecord for this tracker
0065     CELER_FUNCTION SimpleUnitRecord const& unit_record() const
0066     {
0067         return unit_record_;
0068     }
0069 
0070     // DaughterId of universe embedded in a given volume
0071     inline CELER_FUNCTION DaughterId daughter(LocalVolumeId vol) const;
0072 
0073     //// OPERATIONS ////
0074 
0075     // Find the local volume from a position
0076     inline CELER_FUNCTION Initialization
0077     initialize(LocalState const& state) const;
0078 
0079     // Find the new volume by crossing a surface
0080     inline CELER_FUNCTION Initialization
0081     cross_boundary(LocalState const& state) const;
0082 
0083     // Calculate the distance to an exiting face for the current volume
0084     inline CELER_FUNCTION Intersection intersect(LocalState const& state) const;
0085 
0086     // Calculate nearby distance to an exiting face for the current volume
0087     inline CELER_FUNCTION Intersection intersect(LocalState const& state,
0088                                                  real_type max_dist) const;
0089 
0090     // Calculate closest distance to a surface in any direction
0091     inline CELER_FUNCTION real_type safety(Real3 const& pos,
0092                                            LocalVolumeId vol) const;
0093 
0094     // Calculate the local surface normal
0095     inline CELER_FUNCTION Real3 normal(Real3 const& pos,
0096                                        LocalSurfaceId surf) const;
0097 
0098   private:
0099     //// DATA ////
0100 
0101     ParamsRef const& params_;
0102     SimpleUnitRecord const& unit_record_;
0103 
0104     //// METHODS ////
0105 
0106     // Get volumes that have the given surface as a "face" (connectivity)
0107     inline CELER_FUNCTION LdgSpan<LocalVolumeId const>
0108         get_neighbors(LocalSurfaceId) const;
0109 
0110     template<class F>
0111     inline CELER_FUNCTION LocalVolumeId find_volume_where(Real3 const& pos,
0112                                                           F&& predicate) const;
0113 
0114     template<class F>
0115     inline CELER_FUNCTION Intersection intersect_impl(LocalState const&,
0116                                                       F&&) const;
0117 
0118     inline CELER_FUNCTION Intersection simple_intersect(LocalState const&,
0119                                                         VolumeView const&,
0120                                                         size_type) const;
0121     inline CELER_FUNCTION Intersection complex_intersect(LocalState const&,
0122                                                          VolumeView const&,
0123                                                          size_type) const;
0124     inline CELER_FUNCTION Intersection background_intersect(LocalState const&,
0125                                                             size_type) const;
0126 
0127     // Create a Surfaces object from the params
0128     inline CELER_FUNCTION LocalSurfaceVisitor make_surface_visitor() const;
0129 
0130     // Create a Volumes object from the params
0131     inline CELER_FUNCTION VolumeView make_local_volume(LocalVolumeId vid) const;
0132 };
0133 
0134 //---------------------------------------------------------------------------//
0135 // INLINE DEFINITIONS
0136 //---------------------------------------------------------------------------//
0137 /*!
0138  * Construct with reference to persistent parameter data.
0139  *
0140  * \todo When adding multiple universes, this will calculate range of
0141  * LocalVolumeIds that belong to this unit. For now we assume all volumes and
0142  * surfaces belong to us.
0143  */
0144 CELER_FUNCTION
0145 SimpleUnitTracker::SimpleUnitTracker(ParamsRef const& params, SimpleUnitId suid)
0146     : params_(params), unit_record_(params.simple_units[suid])
0147 {
0148     CELER_EXPECT(params_);
0149 }
0150 
0151 //---------------------------------------------------------------------------//
0152 /*!
0153  * Find the local volume from a position.
0154  *
0155  * To avoid edge cases and inconsistent logical/physical states, it is
0156  * prohibited to initialize from an arbitrary point directly onto a surface.
0157  *
0158  * \todo This prohibition currently also extends to *internal* surfaces, even
0159  * if both sides of that surface are "in" the current cell. We may need to
0160  * relax that.
0161  */
0162 CELER_FUNCTION auto
0163 SimpleUnitTracker::initialize(LocalState const& state) const -> Initialization
0164 {
0165     CELER_EXPECT(params_);
0166     CELER_EXPECT(!state.surface && !state.volume);
0167 
0168     detail::SenseCalculator calc_senses(
0169         this->make_surface_visitor(), state.pos, state.temp_sense);
0170 
0171     // Use the BIH to locate a position that's inside, and save whether it's on
0172     // a surface in the found volume
0173     bool on_surface{false};
0174     auto is_inside
0175         = [this, &calc_senses, &on_surface](LocalVolumeId id) -> bool {
0176         VolumeView vol = this->make_local_volume(id);
0177         auto logic_state = calc_senses(vol);
0178         on_surface = static_cast<bool>(logic_state.face);
0179         return detail::LogicEvaluator(vol.logic())(logic_state.senses);
0180     };
0181     LocalVolumeId id = this->find_volume_where(state.pos, is_inside);
0182 
0183     if (on_surface)
0184     {
0185         // Prohibit initialization on a surface
0186         id = {};
0187     }
0188     else if (!id)
0189     {
0190         // Not found: replace with background volume (if any)
0191         id = unit_record_.background;
0192     }
0193 
0194     return Initialization{id, {}};
0195 }
0196 
0197 //---------------------------------------------------------------------------//
0198 /*!
0199  * Find the local volume on the opposite side of a surface.
0200  */
0201 CELER_FUNCTION auto
0202 SimpleUnitTracker::cross_boundary(LocalState const& state) const -> Initialization
0203 {
0204     CELER_EXPECT(state.surface && state.volume);
0205     detail::SenseCalculator calc_senses(
0206         this->make_surface_visitor(), state.pos, state.temp_sense);
0207 
0208     detail::OnLocalSurface on_surface;
0209     auto is_inside = [this, &state, &calc_senses, &on_surface](
0210                          LocalVolumeId const& id) -> bool {
0211         if (id == state.volume)
0212         {
0213             // Cannot cross surface into the same volume
0214             return false;
0215         }
0216 
0217         VolumeView vol = this->make_local_volume(id);
0218         auto logic_state
0219             = calc_senses(vol, detail::find_face(vol, state.surface));
0220 
0221         if (detail::LogicEvaluator(vol.logic())(logic_state.senses))
0222         {
0223             // Inside: find and save the local surface ID, and end the search
0224             on_surface = get_surface(vol, logic_state.face);
0225             return true;
0226         }
0227         return false;
0228     };
0229 
0230     auto neighbors = this->get_neighbors(state.surface.id());
0231 
0232     // If this surface has 2 neighbors or fewer (excluding the current cell),
0233     // use linear search to check neighbors. Otherwise, traverse the BIH tree.
0234     if (neighbors.size() < 3)
0235     {
0236         for (LocalVolumeId id : neighbors)
0237         {
0238             if (is_inside(id))
0239             {
0240                 return {id, on_surface};
0241             }
0242         }
0243     }
0244     else
0245     {
0246         if (LocalVolumeId id = this->find_volume_where(state.pos, is_inside))
0247         {
0248             return {id, on_surface};
0249         }
0250     }
0251 
0252     return {unit_record_.background, state.surface};
0253 }
0254 
0255 //---------------------------------------------------------------------------//
0256 /*!
0257  * Calculate distance-to-intercept for the next surface.
0258  */
0259 CELER_FUNCTION auto
0260 SimpleUnitTracker::intersect(LocalState const& state) const -> Intersection
0261 {
0262     Intersection result = this->intersect_impl(state, detail::IsFinite{});
0263     return result;
0264 }
0265 
0266 //---------------------------------------------------------------------------//
0267 /*!
0268  * Calculate distance-to-intercept for the next surface.
0269  */
0270 CELER_FUNCTION auto
0271 SimpleUnitTracker::intersect(LocalState const& state,
0272                              real_type max_dist) const -> Intersection
0273 {
0274     CELER_EXPECT(max_dist > 0);
0275     Intersection result
0276         = this->intersect_impl(state, detail::IsNotFurtherThan{max_dist});
0277     if (!result)
0278     {
0279         result.distance = max_dist;
0280     }
0281     return result;
0282 }
0283 
0284 //---------------------------------------------------------------------------//
0285 /*!
0286  * Calculate nearest distance to a surface in any direction.
0287  *
0288  * The safety calculation uses a very limited method for calculating the safety
0289  * distance: it's the nearest distance to any surface, for a certain subset of
0290  * surfaces.  Other surface types will return a safety distance of zero.
0291  * Complex surfaces might return the distance to internal surfaces that do not
0292  * represent the edge of a volume. Such distances are conservative but will
0293  * necessarily slow down the simulation.
0294  */
0295 CELER_FUNCTION real_type SimpleUnitTracker::safety(Real3 const& pos,
0296                                                    LocalVolumeId volid) const
0297 {
0298     CELER_EXPECT(volid);
0299 
0300     VolumeView vol = this->make_local_volume(volid);
0301     if (!vol.simple_safety())
0302     {
0303         // Has a tricky surface: we can't use the simple algorithm to calculate
0304         // the safety, so return a conservative estimate.
0305         return 0;
0306     }
0307 
0308     // Calculate minimim distance to all local faces
0309     real_type result = numeric_limits<real_type>::infinity();
0310     LocalSurfaceVisitor visit_surface(params_, unit_record_.surfaces);
0311     detail::CalcSafetyDistance calc_safety{pos};
0312     for (LocalSurfaceId surface : vol.faces())
0313     {
0314         result = celeritas::min(result, visit_surface(calc_safety, surface));
0315     }
0316 
0317     CELER_ENSURE(result >= 0);
0318     return result;
0319 }
0320 
0321 //---------------------------------------------------------------------------//
0322 /*!
0323  * Calculate the local surface normal.
0324  */
0325 CELER_FUNCTION auto
0326 SimpleUnitTracker::normal(Real3 const& pos, LocalSurfaceId surf) const -> Real3
0327 {
0328     CELER_EXPECT(surf);
0329 
0330     LocalSurfaceVisitor visit_surface(params_, unit_record_.surfaces);
0331     return visit_surface(detail::CalcNormal{pos}, surf);
0332 }
0333 
0334 //---------------------------------------------------------------------------//
0335 // PRIVATE INLINE DEFINITIONS
0336 //---------------------------------------------------------------------------//
0337 /*!
0338  * Get volumes that have the given surface as a "face" (connectivity).
0339  */
0340 CELER_FUNCTION auto SimpleUnitTracker::get_neighbors(LocalSurfaceId surf) const
0341     -> LdgSpan<LocalVolumeId const>
0342 {
0343     CELER_EXPECT(surf < this->num_surfaces());
0344 
0345     OpaqueId<ConnectivityRecord> conn_id
0346         = unit_record_.connectivity[surf.unchecked_get()];
0347     ConnectivityRecord const& conn = params_.connectivity_records[conn_id];
0348 
0349     CELER_ENSURE(!conn.neighbors.empty());
0350     return params_.local_volume_ids[conn.neighbors];
0351 }
0352 
0353 //---------------------------------------------------------------------------//
0354 /*!
0355  * Search the BIH to find where the predicate is true for the point.
0356  *
0357  * The predicate should have the signature \code bool(LocalVolumeId) \endcode.
0358  */
0359 template<class F>
0360 CELER_FUNCTION LocalVolumeId
0361 SimpleUnitTracker::find_volume_where(Real3 const& pos, F&& predicate) const
0362 {
0363     detail::BIHTraverser find_impl{unit_record_.bih_tree,
0364                                    params_.bih_tree_data};
0365     return find_impl(pos, predicate);
0366 }
0367 
0368 //---------------------------------------------------------------------------//
0369 /*!
0370  * Calculate distance-to-intercept for the next surface.
0371  *
0372  * The algorithm is:
0373  * - Use the current volume to find potential intersecting surfaces and maximum
0374  *   number of intersections.
0375  * - Loop over all surfaces and calculate the distance to intercept based on
0376  *   the given physical and logical state. Save to the thread-local buffer
0377  *   *only* intersections that are valid (either finite *or* less than the
0378  *   user-supplied maximum). The buffer contains the distances, the face
0379  *   indices, and an index used for sorting (if the volume has internal
0380  *   surfaes).
0381  * - If no intersecting surfaces are found, return immediately. (Rely on the
0382  *   caller to set the "maximum distance" if we're not searching to infinity.)
0383  * - If the volume has no special cases, find the closest surface by calling \c
0384  *   simple_intersect.
0385  * - If the volume has internal surfaces call \c complex_intersect.
0386  * - If the volume is the "background" then search externally for the next
0387  *   volume with \c background_intersect (equivalent of DistanceToIn for
0388  *   Geant4)
0389  */
0390 template<class F>
0391 CELER_FUNCTION auto
0392 SimpleUnitTracker::intersect_impl(LocalState const& state,
0393                                   F&& is_valid) const -> Intersection
0394 {
0395     CELER_EXPECT(state.volume && !state.temp_sense.empty());
0396 
0397     // Resize temporaries based on volume properties
0398     VolumeView vol = this->make_local_volume(state.volume);
0399     CELER_ASSERT(state.temp_next.size >= vol.max_intersections());
0400 
0401     // Find all valid (nearby or finite, depending on F) surface intersection
0402     // distances inside this volume. Fill the `isect` array if the tracking
0403     // algorithm requires sorting.
0404     detail::CalcIntersections calc_intersections{
0405         celeritas::forward<F>(is_valid),
0406         state.pos,
0407         state.dir,
0408         state.surface ? vol.find_face(state.surface.id()) : FaceId{},
0409         vol.simple_intersection(),
0410         state.temp_next};
0411     LocalSurfaceVisitor visit_surface(params_, unit_record_.surfaces);
0412     for (LocalSurfaceId surface : vol.faces())
0413     {
0414         visit_surface(calc_intersections, surface);
0415     }
0416     CELER_ASSERT(calc_intersections.face_idx() == vol.num_faces());
0417     size_type num_isect = calc_intersections.isect_idx();
0418     CELER_ASSERT(num_isect <= vol.max_intersections());
0419 
0420     if (num_isect == 0)
0421     {
0422         // No intersection (no surfaces in this volume, no finite distances, or
0423         // no "nearby" distances depending on F)
0424         return {};
0425     }
0426     else if (vol.simple_intersection())
0427     {
0428         // No internal surfaces nor implicit volume: the closest distance is
0429         // the next boundary
0430         return this->simple_intersect(state, vol, num_isect);
0431     }
0432     else
0433     {
0434         // Sort valid intersection distances in ascending order
0435         celeritas::sort(state.temp_next.isect,
0436                         state.temp_next.isect + num_isect,
0437                         [&state](size_type a, size_type b) {
0438                             return state.temp_next.distance[a]
0439                                    < state.temp_next.distance[b];
0440                         });
0441 
0442         if (vol.internal_surfaces())
0443         {
0444             // Internal surfaces: find closest surface that puts us outside
0445             return this->complex_intersect(state, vol, num_isect);
0446         }
0447         else if (vol.implicit_vol())
0448         {
0449             // Search all the volumes "externally"
0450             return this->background_intersect(state, num_isect);
0451         }
0452     }
0453 
0454     CELER_ASSERT_UNREACHABLE();  // Unexpected set of flags
0455 }
0456 
0457 //---------------------------------------------------------------------------//
0458 /*!
0459  * Calculate distance to the next boundary for nonreentrant volumes.
0460  */
0461 CELER_FUNCTION auto
0462 SimpleUnitTracker::simple_intersect(LocalState const& state,
0463                                     VolumeView const& vol,
0464                                     size_type num_isect) const -> Intersection
0465 {
0466     CELER_EXPECT(num_isect > 0);
0467 
0468     // Crossing any surface will leave the volume; perform a linear search for
0469     // the smallest (but positive) distance
0470     size_type distance_idx
0471         = celeritas::min_element(state.temp_next.distance,
0472                                  state.temp_next.distance + num_isect,
0473                                  Less<real_type>{})
0474           - state.temp_next.distance;
0475     CELER_ASSERT(distance_idx < num_isect);
0476 
0477     // Determine the crossing surface
0478     LocalSurfaceId surface;
0479     {
0480         FaceId face = state.temp_next.face[distance_idx];
0481         CELER_ASSERT(face);
0482         surface = vol.get_surface(face);
0483         CELER_ASSERT(surface);
0484     }
0485 
0486     Sense cur_sense;
0487     if (surface == state.surface.id())
0488     {
0489         // Crossing the same surface that we're currently on and inside (e.g.
0490         // on the inside surface of a sphere, and the next intersection is the
0491         // other side)
0492         cur_sense = state.surface.sense();
0493     }
0494     else
0495     {
0496         LocalSurfaceVisitor visit_surface(params_, unit_record_.surfaces);
0497         SignedSense ss = visit_surface(detail::CalcSense{state.pos}, surface);
0498         CELER_ASSERT(ss != SignedSense::on);
0499         cur_sense = to_sense(ss);
0500     }
0501 
0502     // Post-surface sense will be on the other side of the surface
0503     Intersection result;
0504     result.surface = {surface, cur_sense};
0505     result.distance = state.temp_next.distance[distance_idx];
0506     return result;
0507 }
0508 
0509 //---------------------------------------------------------------------------//
0510 /*!
0511  * Calculate boundary distance if internal surfaces are present.
0512  *
0513  * In "complex" volumes, crossing a surface can still leave the particle in an
0514  * "inside" state.
0515  *
0516  * We have to iteratively track through all surfaces, in order of minimum
0517  * distance, to determine whether crossing them in sequence will cause us to
0518  * exit the volume.
0519  *
0520  * \pre The `state.temp_next.isect` array must be sorted by the caller by
0521  * ascending distance.
0522  */
0523 CELER_FUNCTION auto
0524 SimpleUnitTracker::complex_intersect(LocalState const& state,
0525                                      VolumeView const& vol,
0526                                      size_type num_isect) const -> Intersection
0527 {
0528     CELER_ASSERT(num_isect > 0);
0529 
0530     // Calculate local senses, taking current face into account
0531     auto logic_state = detail::SenseCalculator(
0532         this->make_surface_visitor(), state.pos, state.temp_sense)(
0533         vol, detail::find_face(vol, state.surface));
0534 
0535     // Current senses should put us inside the volume
0536     detail::LogicEvaluator is_inside(vol.logic());
0537     CELER_ASSERT(is_inside(logic_state.senses));
0538 
0539     // Loop over distances and surface indices to cross by iterating over
0540     // temp_next.isect[:num_isect].
0541     // Evaluate the logic expression at each crossing to determine whether
0542     // we're actually leaving the volume.
0543     for (size_type isect_idx = 0; isect_idx != num_isect; ++isect_idx)
0544     {
0545         // Index into the distance/face arrays
0546         size_type const isect = state.temp_next.isect[isect_idx];
0547         // Face being crossed in this ordered intersection
0548         FaceId face = state.temp_next.face[isect];
0549         // Flip the sense of the face being crossed
0550         Sense new_sense = flip_sense(logic_state.senses[face.get()]);
0551         logic_state.senses[face.unchecked_get()] = new_sense;
0552         if (!is_inside(logic_state.senses))
0553         {
0554             // Flipping this sense puts us outside the current volume: in
0555             // other words, only after crossing all the internal surfaces along
0556             // this direction do we hit a surface that actually puts us
0557             // outside.
0558             Intersection result;
0559             result.surface = {vol.get_surface(face), flip_sense(new_sense)};
0560             result.distance = state.temp_next.distance[isect];
0561             CELER_ENSURE(result.distance > 0 && !std::isinf(result.distance));
0562             return result;
0563         }
0564     }
0565 
0566     // No intersection: perhaps leaving an exterior volume? Perhaps geometry
0567     // error.
0568     return {};
0569 }
0570 
0571 //---------------------------------------------------------------------------//
0572 /*!
0573  * Calculate distance from the background volume to enter any other volume.
0574  *
0575  * This is a slimmed-down version of the masked unit tracker's intersection
0576  * method. We loop over all surface intersections in ascending order, and test
0577  * all volumes that are connected to each surface. At the intersection point
0578  * being tested, we see whether each potential connected volume is "inside".
0579  * The first such volume gives our next surface.
0580  *
0581  * It's not cheap, as there are many embedded loops:
0582  * - Intersection points
0583  * - Volumes connected to the surface being intersected
0584  * - Surfaces connected to the target volume (sense evaluation) plus number of
0585  *   elements in the logic array ("is_inside" evaluation)
0586  *
0587  * \pre The `state.temp_next.isect` array must be sorted by the caller by
0588  * ascending distance.
0589  * \pre The "faces" for the background volume are *all* the surfaces in the
0590  * volume (alternatively we could introduce a mapping between Face and
0591  * LocalSurfaceId).
0592  */
0593 CELER_FUNCTION auto SimpleUnitTracker::background_intersect(
0594     LocalState const& state, size_type num_isect) const -> Intersection
0595 {
0596     // Calculate bump distance
0597     real_type const bump_dist
0598         = detail::BumpCalculator{params_.scalars.tol}(state.pos);
0599 
0600     // Loop over distances and surface indices to cross by iterating over
0601     // temp_next.isect[:num_isect].
0602     for (size_type isect_idx = 0; isect_idx != num_isect; ++isect_idx)
0603     {
0604         // Index into the distance/face arrays
0605         size_type const isect = state.temp_next.isect[isect_idx];
0606         // Inside the "background" volume, Face and Surface are the same
0607         LocalSurfaceId const surface{
0608             state.temp_next.face[isect].unchecked_get()};
0609 
0610         // Calculate position just past the surface in order to evaluate
0611         // senses, since we can't know the change in sense of the
0612         // target surface without marching through all interior surfaces.
0613         // Assume that bumping past the surface means not on any surface.
0614         Real3 pos{state.pos};
0615         axpy(state.temp_next.distance[isect] + bump_dist, state.dir, &pos);
0616 
0617         // Loop over volumes connected to this surface.
0618         //! \todo Accelerate by intersecting neighbors with BVH grid
0619         for (LocalVolumeId vid : this->get_neighbors(surface))
0620         {
0621             CELER_ASSERT(vid != state.volume);
0622             VolumeView vol = this->make_local_volume(vid);
0623             auto logic_state = detail::SenseCalculator{
0624                 this->make_surface_visitor(), pos, state.temp_sense}(vol);
0625 
0626             if (detail::LogicEvaluator{vol.logic()}(logic_state.senses))
0627             {
0628                 // We are in this new volume by crossing the tested surface.
0629                 // Get the sense corresponding to this "crossed" surface.
0630                 auto face = vol.find_face(surface);
0631                 CELER_ASSERT(face);
0632 
0633                 Intersection result;
0634                 result.distance = state.temp_next.distance[isect];
0635                 result.surface = detail::OnLocalSurface{
0636                     surface,
0637                     flip_sense(logic_state.senses[face.unchecked_get()])};
0638                 return result;
0639             }
0640         }
0641     }
0642 
0643     // No intersection in this unit
0644     return {};
0645 }
0646 
0647 //---------------------------------------------------------------------------//
0648 /*!
0649  * Create a surface visitor from the params for this unit.
0650  */
0651 CELER_FORCEINLINE_FUNCTION LocalSurfaceVisitor
0652 SimpleUnitTracker::make_surface_visitor() const
0653 {
0654     return LocalSurfaceVisitor{params_, unit_record_.surfaces};
0655 }
0656 
0657 //---------------------------------------------------------------------------//
0658 /*!
0659  * Create a Volume view object from the params for this unit.
0660  */
0661 CELER_FORCEINLINE_FUNCTION VolumeView
0662 SimpleUnitTracker::make_local_volume(LocalVolumeId vid) const
0663 {
0664     return VolumeView{params_, unit_record_, vid};
0665 }
0666 
0667 //---------------------------------------------------------------------------//
0668 /*!
0669  * DaughterId of universe embedded in a given volume.
0670  */
0671 CELER_FUNCTION DaughterId SimpleUnitTracker::daughter(LocalVolumeId vol) const
0672 {
0673     CELER_EXPECT(vol < unit_record_.volumes.size());
0674     return params_.volume_records[unit_record_.volumes[vol]].daughter_id;
0675 }
0676 
0677 //---------------------------------------------------------------------------//
0678 }  // namespace celeritas