Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-09-13 08:21:08

0001 // This file is part of the ACTS project.
0002 //
0003 // Copyright (C) 2016 CERN for the benefit of the ACTS project
0004 //
0005 // This Source Code Form is subject to the terms of the Mozilla Public
0006 // License, v. 2.0. If a copy of the MPL was not distributed with this
0007 // file, You can obtain one at https://mozilla.org/MPL/2.0/.
0008 
0009 #pragma once
0010 
0011 #include "Acts/Propagator/ConstrainedStep.hpp"
0012 #include "Acts/Propagator/PropagatorState.hpp"
0013 #include "Acts/Propagator/StandardAborters.hpp"
0014 #include "Acts/Surfaces/Surface.hpp"
0015 #include "ActsFatras/EventData/Particle.hpp"
0016 #include "ActsFatras/Kernel/SingleParticleSimulationResult.hpp"
0017 
0018 #include <algorithm>
0019 #include <cassert>
0020 #include <cmath>
0021 
0022 namespace ActsFatras::detail {
0023 
0024 /// Fatras simulation actor for the Acts propagator.
0025 ///
0026 /// This actor must be added to the action list of the propagator and is the
0027 /// equivalent to the `MaterialInteractor` for the reconstruction. This
0028 /// implements surface-based simulation of particle interactions with matter
0029 /// using a configurable interaction list as well as the decay simulation. The
0030 /// interactions are simulated for every surface with valid material.
0031 ///
0032 /// @tparam generator_t random number generator
0033 /// @tparam decay_t decay module
0034 /// @tparam interactions_t interaction list
0035 /// @tparam hit_surface_selector_t selector for hit surfaces
0036 template <typename generator_t, typename decay_t, typename interactions_t,
0037           typename hit_surface_selector_t>
0038 struct SimulationActor {
0039   using result_type = SingleParticleSimulationResult;
0040 
0041   /// Random number generator used for the simulation.
0042   generator_t *generator = nullptr;
0043   /// Decay module.
0044   decay_t decay;
0045   /// Interaction list containing the simulated interactions.
0046   interactions_t interactions;
0047   /// Selector for surfaces that should generate hits.
0048   hit_surface_selector_t selectHitSurface;
0049   /// Initial particle state.
0050   Particle initialParticle;
0051 
0052   /// Relative tolerance of the particles proper time limit
0053   double properTimeRelativeTolerance = 1e-3;
0054 
0055   /// Simulate the interaction with a single surface.
0056   ///
0057   /// @tparam propagator_state_t is propagator state
0058   /// @tparam stepper_t is the stepper instance
0059   ///
0060   /// @param state is the mutable propagator state object
0061   /// @param stepper is the propagation stepper object
0062   /// @param result is the mutable result/cache object
0063   /// @param logger a logger instance
0064   template <typename propagator_state_t, typename stepper_t,
0065             typename navigator_t>
0066   Acts::Result<void> act(propagator_state_t &state, stepper_t &stepper,
0067                          navigator_t &navigator, result_type &result,
0068                          const Acts::Logger &logger) const {
0069     assert(generator != nullptr && "The generator pointer must be valid");
0070 
0071     if (state.stage == Acts::PropagatorStage::prePropagation) {
0072       // first step is special: there is no previous state and we need to arm
0073       // the decay simulation for all future steps.
0074       result.particle =
0075           makeParticle(initialParticle, state, stepper, navigator);
0076       result.properTimeLimit =
0077           decay.generateProperTimeLimit(*generator, initialParticle);
0078       return Acts::Result<void>::success();
0079     }
0080 
0081     // actors are called once more after the propagation terminated
0082     if (!result.isAlive) {
0083       return Acts::Result<void>::success();
0084     }
0085 
0086     if (Acts::EndOfWorldReached{}.checkAbort(state, stepper, navigator,
0087                                              logger)) {
0088       result.isAlive = false;
0089       return Acts::Result<void>::success();
0090     }
0091 
0092     // update the particle state first. this also computes the proper time which
0093     // needs the particle state from the previous step for reference. that means
0094     // this must happen for every step (not just on surface) and before
0095     // everything, e.g. any interactions that could modify the state.
0096     result.particle = makeParticle(result.particle, state, stepper, navigator);
0097 
0098     // decay check. needs to happen at every step, not just on surfaces.
0099     if (std::isfinite(result.properTimeLimit) &&
0100         (result.properTimeLimit - result.particle.properTime() <
0101          result.properTimeLimit * properTimeRelativeTolerance)) {
0102       auto descendants = decay.run(generator, result.particle);
0103       for (const auto &descendant : descendants) {
0104         result.generatedParticles.emplace_back(descendant);
0105       }
0106       result.isAlive = false;
0107       return Acts::Result<void>::success();
0108     }
0109 
0110     // Regulate the step size
0111     if (std::isfinite(result.properTimeLimit)) {
0112       assert(result.particle.mass() > 0.0 && "Particle must have mass");
0113       //    beta² = p²/E²
0114       //    gamma = 1 / sqrt(1 - beta²) = sqrt(m² + p²) / m = E / m
0115       //     time = proper-time * gamma
0116       // ds = beta * dt = (p/E) dt (E/m) = (p/m) proper-time
0117       const auto properTimeDiff =
0118           result.properTimeLimit - result.particle.properTime();
0119       // Evaluate the step size for massive particle, assuming massless
0120       // particles to be stable
0121       const auto stepSize = properTimeDiff *
0122                             result.particle.absoluteMomentum() /
0123                             result.particle.mass();
0124       stepper.releaseStepSize(state.stepping,
0125                               Acts::ConstrainedStep::Type::User);
0126       stepper.updateStepSize(state.stepping, stepSize,
0127                              Acts::ConstrainedStep::Type::User);
0128     }
0129 
0130     // arm the point-like interaction limits in the first step
0131     if (std::isnan(result.x0Limit) || std::isnan(result.l0Limit)) {
0132       armPointLikeInteractions(initialParticle, result);
0133     }
0134 
0135     // If we are on target, everything should have been done
0136     if (state.stage == Acts::PropagatorStage::postPropagation) {
0137       return Acts::Result<void>::success();
0138     }
0139     // If we are not on a surface, there is nothing further for us to do
0140     if (!navigator.currentSurface(state.navigation)) {
0141       return Acts::Result<void>::success();
0142     }
0143     const Acts::Surface &surface = *navigator.currentSurface(state.navigation);
0144 
0145     // we need the particle state before and after the interaction for the hit
0146     // creation. create a copy since the particle will be modified in-place.
0147     const Particle before = result.particle;
0148 
0149     // interactions only make sense if there is material to interact with.
0150     if (surface.hasMaterial()) {
0151       // TODO is this the right thing to do when globalToLocal fails?
0152       //   it should in principle never happen, so probably it would be best
0153       //   to change to a model using transform() directly
0154       auto lpResult = surface.globalToLocal(state.geoContext, before.position(),
0155                                             before.direction());
0156       if (lpResult.ok()) {
0157         const Acts::Vector2 local = lpResult.value();
0158         Acts::MaterialSlab slab = surface.materialSlab(local);
0159         // again: interact only if there is valid material to interact with
0160         if (!slab.isVacuum()) {
0161           // adapt material for non-zero incidence
0162           auto normal = surface.normal(state.geoContext, before.position(),
0163                                        before.direction());
0164           // dot-product(unit normal, direction) = cos(incidence angle)
0165           // particle direction is normalized, not sure about surface normal
0166           auto cosIncidenceInv = normal.norm() / normal.dot(before.direction());
0167           // apply abs in case `normal` and `before` produce an angle > 90°
0168           slab.scaleThickness(std::abs(cosIncidenceInv));
0169           // run the interaction simulation
0170           interact(slab, result);  // MARK: fpeMask(FLTUND, 1, #2346)
0171         }
0172       }
0173     }
0174     Particle &after = result.particle;
0175 
0176     // store results of this interaction step, including potential hits
0177     if (selectHitSurface(surface)) {
0178       result.hits.emplace_back(
0179           surface.geometryId(), before.particleId(),
0180           // the interaction could potentially modify the particle position
0181           0.5 * (before.fourPosition() + after.fourPosition()),
0182           before.fourMomentum(), after.fourMomentum(), result.hits.size());
0183 
0184       after.setNumberOfHits(result.hits.size());
0185     }
0186 
0187     if (after.absoluteMomentum() == 0.0) {
0188       result.isAlive = false;
0189       return Acts::Result<void>::success();
0190     }
0191 
0192     // continue the propagation with the modified parameters
0193     stepper.update(state.stepping, after.position(), after.direction(),
0194                    after.qOverP(), after.time());
0195 
0196     return Acts::Result<void>::success();
0197   }
0198 
0199   template <typename propagator_state_t, typename stepper_t,
0200             typename navigator_t>
0201   bool checkAbort(propagator_state_t & /*state*/, const stepper_t & /*stepper*/,
0202                   const navigator_t & /*navigator*/, const result_type &result,
0203                   const Acts::Logger & /*logger*/) const {
0204     // must return true if the propagation should abort
0205     return !result.isAlive;
0206   }
0207 
0208   /// Construct the current particle state from the propagation state.
0209   template <typename propagator_state_t, typename stepper_t,
0210             typename navigator_t>
0211   Particle makeParticle(const Particle &previous, propagator_state_t &state,
0212                         stepper_t &stepper, navigator_t &navigator) const {
0213     // a particle can lose energy and thus its gamma factor is not a constant
0214     // of motion. since the stepper provides only the lab time, we need to
0215     // compute the change in proper time for each step separately. this assumes
0216     // that the gamma factor is constant over one stepper step.
0217     const auto deltaLabTime = stepper.time(state.stepping) - previous.time();
0218     // proper-time = time / gamma = (1/gamma) * time
0219     //       beta² = p²/E²
0220     //       gamma = 1 / sqrt(1 - beta²) = sqrt(m² + p²) / m
0221     //     1/gamma = m / sqrt(m² + p²) = m / E
0222     const auto gammaInv = previous.mass() / previous.energy();
0223     const auto properTime = previous.properTime() + gammaInv * deltaLabTime;
0224     const Acts::Surface *currentSurface = nullptr;
0225     if (navigator.currentSurface(state.navigation) != nullptr) {
0226       currentSurface = navigator.currentSurface(state.navigation);
0227     }
0228     // copy all properties and update kinematic state from stepper
0229     return Particle(previous)
0230         .setPosition4(stepper.position(state.stepping),
0231                       stepper.time(state.stepping))
0232         .setDirection(stepper.direction(state.stepping))
0233         .setAbsoluteMomentum(stepper.absoluteMomentum(state.stepping))
0234         .setProperTime(properTime)
0235         .setReferenceSurface(currentSurface);
0236   }
0237 
0238   /// Prepare limits and process selection for the next point-like interaction.
0239   void armPointLikeInteractions(const Particle &particle,
0240                                 result_type &result) const {
0241     auto selection = interactions.armPointLike(*generator, particle);
0242     result.x0Limit = selection.x0Limit;
0243     result.l0Limit = selection.l0Limit;
0244     result.x0Process = selection.x0Process;
0245     result.l0Process = selection.l0Process;
0246   }
0247 
0248   /// Run the interaction simulation for the given material.
0249   ///
0250   /// Simulate all continuous processes and at most one point-like process
0251   /// within the material.
0252   void interact(const Acts::MaterialSlab &slab, result_type &result) const {
0253     // run the continuous processes over a fraction of the material. returns
0254     // true on break condition (same as the underlying physics lists).
0255     auto runContinuousPartial = [&, this](float fraction) {
0256       Acts::MaterialSlab partialSlab = slab;
0257       partialSlab.scaleThickness(fraction);
0258       // material after passing this slab
0259       const auto x0 = result.particle.pathInX0() + partialSlab.thicknessInX0();
0260       const auto l0 = result.particle.pathInX0() + partialSlab.thicknessInL0();
0261       bool retval = false;
0262       if (interactions.runContinuous(*(this->generator), partialSlab,
0263                                      result.particle,
0264                                      result.generatedParticles)) {
0265         result.isAlive = false;
0266         retval = true;
0267       }
0268       // the SimulationActor is in charge of keeping track of the material.
0269       // since the accumulated material is stored in the particle it could (but
0270       // should not) be modified by a physics process. to avoid issues, the
0271       // material is updated only after process simulation has occurred. this
0272       // intentionally overwrites any material updates made by the process.
0273       result.particle.setMaterialPassed(x0, l0);
0274       return retval;
0275     };
0276 
0277     // material thickness measured in radiation/interaction lengths
0278     const auto slabX0 = slab.thicknessInX0();
0279     const auto slabL0 = slab.thicknessInL0();
0280     // remaining radiation/interaction length to next point-like interaction
0281     // NOTE for limit=inf this should result in dist=inf
0282     const auto x0Dist = result.x0Limit - result.particle.pathInX0();
0283     const auto l0Dist = result.l0Limit - result.particle.pathInL0();
0284 
0285     // something point-like could happen within this material and we need to
0286     // select which process would come first. x0/l0 measures the propagated path
0287     // along different scales. to be able to check which one would happen first
0288     // they need to be translated to a common scale.
0289 
0290     // relative fraction within material where the interaction occurs.
0291     //
0292     // fraction < 0:
0293     //   this is an error case where the point-like interaction should have
0294     //   occurred before reaching the material. not sure how this could happen,
0295     //   but in such a case the point-like interaction happens immediately.
0296     // 1 < fraction:
0297     //   the next point-like interaction does not occur within the current
0298     //   material. simulation is limited to the continuous processes.
0299     //
0300     // `clamp` ensures a valid range in all cases.
0301     const float fracX0 =
0302         std::clamp(static_cast<float>(x0Dist / slabX0), 0.0f, 1.0f);
0303     const float fracL0 =
0304         std::clamp(static_cast<float>(l0Dist / slabL0), 0.0f, 1.0f);
0305     // fraction of the material where the first point-like interaction occurs
0306     const float frac = std::min(fracX0, fracL0);
0307 
0308     // do not run if there is zero material before the point-like interaction
0309     if (0.0f < frac) {
0310       // simulate continuous processes before the point-like interaction
0311       if (runContinuousPartial(frac)) {
0312         return;
0313       }
0314     }
0315     // do not run if there is no point-like interaction
0316     if (frac < 1.0f) {
0317       // select which process to simulate
0318       const std::size_t process =
0319           (fracX0 < fracL0) ? result.x0Process : result.l0Process;
0320       // simulate the selected point-like process
0321       if (interactions.runPointLike(*generator, process, result.particle,
0322                                     result.generatedParticles)) {
0323         result.isAlive = false;
0324         return;
0325       }
0326       // simulate continuous processes after the point-like interaction
0327       if (runContinuousPartial(1.0 - frac)) {
0328         return;
0329       }
0330 
0331       // particle is still alive and point-like interactions can occur again.
0332       // in principle, the re-arming should occur directly after the point-like
0333       // process. this could lead to a situation where the next interaction
0334       // should already occur within the same material slab. thus, re-arming is
0335       // done after all processes are simulated to enforce the
0336       // one-interaction-per-slab rule.
0337       armPointLikeInteractions(result.particle, result);
0338     }
0339   }
0340 };
0341 
0342 }  // namespace ActsFatras::detail