Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-02-22 09:55:27

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