Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-01-18 09:12:13

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/Material/ISurfaceMaterial.hpp"
0012 #include "Acts/Propagator/ConstrainedStep.hpp"
0013 #include "Acts/Propagator/PropagatorState.hpp"
0014 #include "Acts/Propagator/StandardAborters.hpp"
0015 #include "Acts/Surfaces/Surface.hpp"
0016 #include "ActsFatras/EventData/Particle.hpp"
0017 #include "ActsFatras/Kernel/SimulationResult.hpp"
0018 
0019 #include <algorithm>
0020 #include <cassert>
0021 #include <cmath>
0022 
0023 namespace ActsFatras::detail {
0024 
0025 /// Fatras simulation actor for the Acts propagator.
0026 ///
0027 /// This actor must be added to the action list of the propagator and is the
0028 /// equivalent to the `MaterialInteractor` for the reconstruction. This
0029 /// implements surface-based simulation of particle interactions with matter
0030 /// using a configurable interaction list as well as the decay simulation. The
0031 /// interactions are simulated for every surface with valid material.
0032 ///
0033 /// @tparam generator_t random number generator
0034 /// @tparam decay_t decay module
0035 /// @tparam interactions_t interaction list
0036 /// @tparam hit_surface_selector_t selector for hit surfaces
0037 template <typename generator_t, typename decay_t, typename interactions_t,
0038           typename hit_surface_selector_t>
0039 struct SimulationActor {
0040   using result_type = SimulationResult;
0041 
0042   /// Random number generator used for the simulation.
0043   generator_t *generator = nullptr;
0044   /// Decay module.
0045   decay_t decay;
0046   /// Interaction list containing the simulated interactions.
0047   interactions_t interactions;
0048   /// Selector for surfaces that should generate hits.
0049   hit_surface_selector_t selectHitSurface;
0050   /// Initial particle state.
0051   Particle initialParticle;
0052 
0053   /// Relative tolerance of the particles proper time limit
0054   double properTimeRelativeTolerance = 1e-3;
0055 
0056   /// Simulate the interaction with a single surface.
0057   ///
0058   /// @tparam propagator_state_t is propagator state
0059   /// @tparam stepper_t is the stepper instance
0060   ///
0061   /// @param state is the mutable propagator state object
0062   /// @param stepper is the propagation stepper object
0063   /// @param result is the mutable result/cache object
0064   /// @param logger a logger instance
0065   template <typename propagator_state_t, typename stepper_t,
0066             typename navigator_t>
0067   void act(propagator_state_t &state, stepper_t &stepper,
0068            navigator_t &navigator, result_type &result,
0069            const Acts::Logger &logger) const {
0070     assert(generator != nullptr && "The generator pointer must be valid");
0071 
0072     if (state.stage == Acts::PropagatorStage::prePropagation) {
0073       // first step is special: there is no previous state and we need to arm
0074       // the decay simulation for all future steps.
0075       result.particle =
0076           makeParticle(initialParticle, state, stepper, navigator);
0077       result.properTimeLimit =
0078           decay.generateProperTimeLimit(*generator, initialParticle);
0079       return;
0080     }
0081 
0082     // actors are called once more after the propagation terminated
0083     if (!result.isAlive) {
0084       return;
0085     }
0086 
0087     if (Acts::EndOfWorldReached{}.checkAbort(state, stepper, navigator,
0088                                              logger)) {
0089       result.isAlive = false;
0090       return;
0091     }
0092 
0093     // update the particle state first. this also computes the proper time which
0094     // needs the particle state from the previous step for reference. that means
0095     // this must happen for every step (not just on surface) and before
0096     // everything, e.g. any interactions that could modify the state.
0097     result.particle = makeParticle(result.particle, state, stepper, navigator);
0098 
0099     // decay check. needs to happen at every step, not just on surfaces.
0100     if (std::isfinite(result.properTimeLimit) &&
0101         (result.properTimeLimit - result.particle.properTime() <
0102          result.properTimeLimit * properTimeRelativeTolerance)) {
0103       auto descendants = decay.run(generator, result.particle);
0104       for (const auto &descendant : descendants) {
0105         result.generatedParticles.emplace_back(descendant);
0106       }
0107       result.isAlive = false;
0108       return;
0109     }
0110 
0111     // Regulate the step size
0112     if (std::isfinite(result.properTimeLimit)) {
0113       assert(result.particle.mass() > 0.0 && "Particle must have mass");
0114       //    beta² = p²/E²
0115       //    gamma = 1 / sqrt(1 - beta²) = sqrt(m² + p²) / m = E / m
0116       //     time = proper-time * gamma
0117       // ds = beta * dt = (p/E) dt (E/m) = (p/m) proper-time
0118       const auto properTimeDiff =
0119           result.properTimeLimit - result.particle.properTime();
0120       // Evaluate the step size for massive particle, assuming massless
0121       // particles to be stable
0122       const auto stepSize = properTimeDiff *
0123                             result.particle.absoluteMomentum() /
0124                             result.particle.mass();
0125       stepper.releaseStepSize(state.stepping,
0126                               Acts::ConstrainedStep::Type::User);
0127       stepper.updateStepSize(state.stepping, stepSize,
0128                              Acts::ConstrainedStep::Type::User);
0129     }
0130 
0131     // arm the point-like interaction limits in the first step
0132     if (std::isnan(result.x0Limit) || std::isnan(result.l0Limit)) {
0133       armPointLikeInteractions(initialParticle, result);
0134     }
0135 
0136     // If we are on target, everything should have been done
0137     if (state.stage == Acts::PropagatorStage::postPropagation) {
0138       return;
0139     }
0140     // If we are not on a surface, there is nothing further for us to do
0141     if (!navigator.currentSurface(state.navigation)) {
0142       return;
0143     }
0144     const Acts::Surface &surface = *navigator.currentSurface(state.navigation);
0145 
0146     // we need the particle state before and after the interaction for the hit
0147     // creation. create a copy since the particle will be modified in-place.
0148     const Particle before = result.particle;
0149 
0150     // interactions only make sense if there is material to interact with.
0151     if (surface.surfaceMaterial()) {
0152       // TODO is this the right thing to do when globalToLocal fails?
0153       //   it should in principle never happen, so probably it would be best
0154       //   to change to a model using transform() directly
0155       auto lpResult = surface.globalToLocal(state.geoContext, before.position(),
0156                                             before.direction());
0157       if (lpResult.ok()) {
0158         Acts::Vector2 local = lpResult.value();
0159         Acts::MaterialSlab slab =
0160             surface.surfaceMaterial()->materialSlab(local);
0161         // again: interact only if there is valid material to interact with
0162         if (slab.isValid()) {
0163           // adapt material for non-zero incidence
0164           auto normal = surface.normal(state.geoContext, before.position(),
0165                                        before.direction());
0166           // dot-product(unit normal, direction) = cos(incidence angle)
0167           // particle direction is normalized, not sure about surface normal
0168           auto cosIncidenceInv = normal.norm() / normal.dot(before.direction());
0169           // apply abs in case `normal` and `before` produce an angle > 90°
0170           slab.scaleThickness(std::abs(cosIncidenceInv));
0171           // run the interaction simulation
0172           interact(slab, result);  // MARK: fpeMask(FLTUND, 1, #2346)
0173         }
0174       }
0175     }
0176     Particle &after = result.particle;
0177 
0178     // store results of this interaction step, including potential hits
0179     if (selectHitSurface(surface)) {
0180       result.hits.emplace_back(
0181           surface.geometryId(), before.particleId(),
0182           // the interaction could potentially modify the particle position
0183           0.5 * (before.fourPosition() + after.fourPosition()),
0184           before.fourMomentum(), after.fourMomentum(), result.hits.size());
0185 
0186       after.setNumberOfHits(result.hits.size());
0187     }
0188 
0189     if (after.absoluteMomentum() == 0.0) {
0190       result.isAlive = false;
0191       return;
0192     }
0193 
0194     // continue the propagation with the modified parameters
0195     stepper.update(state.stepping, after.position(), after.direction(),
0196                    after.qOverP(), after.time());
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