Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-10-16 08:03:39

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/Geometry/GeometryContext.hpp"
0012 #include "Acts/MagneticField/MagneticFieldContext.hpp"
0013 #include "Acts/Propagator/ActorList.hpp"
0014 #include "Acts/Utilities/Logger.hpp"
0015 #include "Acts/Utilities/Result.hpp"
0016 #include "ActsFatras/EventData/Particle.hpp"
0017 #include "ActsFatras/Kernel/SimulationResult.hpp"
0018 #include "ActsFatras/Kernel/detail/SimulationActor.hpp"
0019 #include "ActsFatras/Kernel/detail/SimulationError.hpp"
0020 
0021 #include <algorithm>
0022 #include <cassert>
0023 #include <iterator>
0024 #include <memory>
0025 #include <vector>
0026 
0027 namespace ActsFatras {
0028 
0029 /// Single particle simulation with fixed propagator, interactions, and decay.
0030 ///
0031 /// @tparam generator_t random number generator
0032 /// @tparam interactions_t interaction list
0033 /// @tparam hit_surface_selector_t selector for hit surfaces
0034 /// @tparam decay_t decay module
0035 template <typename propagator_t, typename interactions_t,
0036           typename hit_surface_selector_t, typename decay_t>
0037 struct SingleParticleSimulation {
0038   /// How and within which geometry to propagate the particle.
0039   propagator_t propagator;
0040   /// Absolute maximum step size
0041   double maxStepSize = std::numeric_limits<double>::max();
0042   /// Absolute maximum path length
0043   double pathLimit = std::numeric_limits<double>::max();
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   /// Logger for debug output.
0051   std::unique_ptr<const Acts::Logger> logger;
0052 
0053   /// Alternatively construct the simulator with an external logger.
0054   /// @param propagator_ Propagator to use for particle simulation
0055   /// @param _logger Logger instance for debug output
0056   SingleParticleSimulation(propagator_t &&propagator_,
0057                            std::unique_ptr<const Acts::Logger> _logger)
0058       : propagator(propagator_), logger(std::move(_logger)) {}
0059 
0060   /// Simulate a single particle without secondaries.
0061   ///
0062   /// @tparam generator_t is the type of the random number generator
0063   ///
0064   /// @param geoCtx is the geometry context to access surface geometries
0065   /// @param magCtx is the magnetic field context to access field values
0066   /// @param generator is the random number generator
0067   /// @param particle is the initial particle state
0068   /// @returns Simulated particle state, hits, and generated particles.
0069   template <typename generator_t>
0070   Acts::Result<SimulationResult> simulate(
0071       const Acts::GeometryContext &geoCtx,
0072       const Acts::MagneticFieldContext &magCtx, generator_t &generator,
0073       const Particle &particle) const {
0074     // propagator-related additional types
0075     using Actor = detail::SimulationActor<generator_t, decay_t, interactions_t,
0076                                           hit_surface_selector_t>;
0077     using Result = typename Actor::result_type;
0078     using ActorList = Acts::ActorList<Actor>;
0079     using PropagatorOptions =
0080         typename propagator_t::template Options<ActorList>;
0081 
0082     // Construct per-call options.
0083     PropagatorOptions options(geoCtx, magCtx);
0084     options.stepping.maxStepSize = maxStepSize;
0085     options.pathLimit = pathLimit;
0086     // setup the interactor as part of the propagator options
0087     auto &actor = options.actorList.template get<Actor>();
0088     actor.generator = &generator;
0089     actor.decay = decay;
0090     actor.interactions = interactions;
0091     actor.selectHitSurface = selectHitSurface;
0092     actor.initialParticle = particle;
0093 
0094     if (particle.hasReferenceSurface()) {
0095       auto result = propagator.propagate(
0096           particle.boundParameters(geoCtx).value(), options);
0097       if (!result.ok()) {
0098         return result.error();
0099       }
0100       auto &value = result.value().template get<Result>();
0101       return std::move(value);
0102     }
0103 
0104     auto result =
0105         propagator.propagate(particle.curvilinearParameters(), options);
0106     if (!result.ok()) {
0107       return result.error();
0108     }
0109     auto &value = result.value().template get<Result>();
0110     return std::move(value);
0111   }
0112 };
0113 
0114 /// A particle that failed to simulate.
0115 struct FailedParticle {
0116   /// Initial particle state of the failed particle.
0117   ///
0118   /// This must store the full particle state to be able to handle secondaries
0119   /// that are not in the input particle list. Otherwise they could not be
0120   /// referenced.
0121   Particle particle;
0122   /// The associated error code for this particular failure case.
0123   std::error_code error;
0124 };
0125 
0126 /// Multi-particle/event simulation.
0127 ///
0128 /// @tparam charged_selector_t Callable selector type for charged particles
0129 /// @tparam charged_simulator_t Single particle simulator for charged particles
0130 /// @tparam neutral_selector_t Callable selector type for neutral particles
0131 /// @tparam neutral_simulator_t Single particle simulator for neutral particles
0132 ///
0133 /// The selector types for charged and neutral particles **do not** need to
0134 /// check for the particle charge. This is done automatically by the simulator
0135 /// to ensure consistency.
0136 template <typename charged_selector_t, typename charged_simulator_t,
0137           typename neutral_selector_t, typename neutral_simulator_t>
0138 struct Simulation {
0139   /// Selector for charged particle simulation
0140   charged_selector_t selectCharged;
0141   /// Selector for neutral particle simulation
0142   neutral_selector_t selectNeutral;
0143   /// Simulator for charged particles
0144   charged_simulator_t charged;
0145   /// Simulator for neutral particles
0146   neutral_simulator_t neutral;
0147 
0148   /// Construct from the single charged/neutral particle simulators.
0149   /// @param charged_ Simulator for charged particles
0150   /// @param neutral_ Simulator for neutral particles
0151   Simulation(charged_simulator_t &&charged_, neutral_simulator_t &&neutral_)
0152       : charged(std::move(charged_)), neutral(std::move(neutral_)) {}
0153 
0154   /// Simulate multiple particles and generated secondaries.
0155   ///
0156   /// @param geoCtx is the geometry context to access surface geometries
0157   /// @param magCtx is the magnetic field context to access field values
0158   /// @param generator is the random number generator
0159   /// @param inputParticles contains all particles that should be simulated
0160   /// @param simulatedParticlesInitial contains initial particle states
0161   /// @param simulatedParticlesFinal contains final particle states
0162   /// @param hits contains all generated hits
0163   /// @retval Acts::Result::Error if there is a fundamental issue
0164   /// @retval Acts::Result::Success with all particles that failed to simulate
0165   ///
0166   /// @warning Particle-hit association is based on particle ids generated
0167   ///          during the simulation. This requires that all input particles
0168   ///          **must** have generation and sub-particle number set to zero.
0169   /// @note Parameter edge-cases can lead to errors in the underlying propagator
0170   ///       and thus to particles that fail to simulate. Here, full events are
0171   ///       simulated and the failure to simulate one particle should not be
0172   ///       considered a general failure of the simulator. Instead, a list of
0173   ///       particles that fail to simulate is provided to the user. It is the
0174   ///       users responsibility to handle them.
0175   /// @note Failed particles are removed from the regular output, i.e. they do
0176   ///       not appear in the simulated particles containers nor do they
0177   ///       generate hits.
0178   ///
0179   /// This takes all input particles and simulates those passing the selection
0180   /// using the appropriate simulator. All selected particle states including
0181   /// additional ones generated from interactions are stored in separate output
0182   /// containers; both the initial state at the production vertex and the final
0183   /// state after propagation are stored. Hits generated from selected input and
0184   /// generated particles are stored in the hit container.
0185   ///
0186   /// @tparam generator_t is the type of the random number generator
0187   /// @tparam input_particles_t is a Container for particles
0188   /// @tparam output_particles_t is a SequenceContainer for particles
0189   /// @tparam hits_t is a SequenceContainer for hits
0190   template <typename generator_t, typename input_particles_t,
0191             typename output_particles_t, typename hits_t>
0192   Acts::Result<std::vector<FailedParticle>> simulate(
0193       const Acts::GeometryContext &geoCtx,
0194       const Acts::MagneticFieldContext &magCtx, generator_t &generator,
0195       const input_particles_t &inputParticles,
0196       output_particles_t &simulatedParticlesInitial,
0197       output_particles_t &simulatedParticlesFinal, hits_t &hits) const {
0198     assert(
0199         (simulatedParticlesInitial.size() == simulatedParticlesFinal.size()) &&
0200         "Inconsistent initial sizes of the simulated particle containers");
0201 
0202     using SingleParticleSimulationResult = Acts::Result<SimulationResult>;
0203 
0204     std::vector<FailedParticle> failedParticles;
0205 
0206     for (const Particle &inputParticle : inputParticles) {
0207       // only consider simulatable particles
0208       if (!selectParticle(inputParticle)) {
0209         continue;
0210       }
0211       // required to allow correct particle id numbering for secondaries later
0212       if ((inputParticle.particleId().generation() != 0u) ||
0213           (inputParticle.particleId().subParticle() != 0u)) {
0214         return detail::SimulationError::eInvalidInputParticleId;
0215       }
0216 
0217       // Do a *depth-first* simulation of the particle and its secondaries,
0218       // i.e. we simulate all secondaries, tertiaries, ... before simulating
0219       // the next primary particle. Use the end of the output container as
0220       // a queue to store particles that should be simulated.
0221       //
0222       // WARNING the initial particle state output container will be modified
0223       //         during iteration. New secondaries are added to and failed
0224       //         particles might be removed. To avoid issues, access must always
0225       //         occur via indices.
0226       auto iinitial = simulatedParticlesInitial.size();
0227       simulatedParticlesInitial.push_back(inputParticle);
0228       for (; iinitial < simulatedParticlesInitial.size(); ++iinitial) {
0229         const auto &initialParticle = simulatedParticlesInitial[iinitial];
0230 
0231         // only simulatable particles are pushed to the container and here we
0232         // only need to switch between charged/neutral.
0233         SingleParticleSimulationResult result =
0234             SingleParticleSimulationResult::success({});
0235         if (initialParticle.charge() != 0.) {
0236           result = charged.simulate(geoCtx, magCtx, generator, initialParticle);
0237         } else {
0238           result = neutral.simulate(geoCtx, magCtx, generator, initialParticle);
0239         }
0240 
0241         if (!result.ok()) {
0242           // remove particle from output container since it was not simulated.
0243           simulatedParticlesInitial.erase(
0244               std::next(simulatedParticlesInitial.begin(), iinitial));
0245           // record the particle as failed
0246           failedParticles.push_back({initialParticle, result.error()});
0247           continue;
0248         }
0249 
0250         assert(result->particle.particleId() == initialParticle.particleId() &&
0251                "Particle id must not change during simulation");
0252 
0253         copyOutputs(result.value(), simulatedParticlesInitial,
0254                     simulatedParticlesFinal, hits);
0255         // since physics processes are independent, there can be particle id
0256         // collisions within the generated secondaries. they can be resolved by
0257         // renumbering within each sub-particle generation. this must happen
0258         // before the particle is simulated since the particle id is used to
0259         // associate generated hits back to the particle.
0260         renumberTailParticleIds(simulatedParticlesInitial, iinitial);
0261       }
0262     }
0263 
0264     assert(
0265         (simulatedParticlesInitial.size() == simulatedParticlesFinal.size()) &&
0266         "Inconsistent final sizes of the simulated particle containers");
0267 
0268     // the overall function call succeeded, i.e. no fatal errors occurred.
0269     // yet, there might have been some particle for which the propagation
0270     // failed. thus, the successful result contains a list of failed particles.
0271     // sounds a bit weird, but that is the way it is.
0272     return failedParticles;
0273   }
0274 
0275  private:
0276   /// Select if the particle should be simulated at all.
0277   bool selectParticle(const Particle &particle) const {
0278     if (particle.charge() != 0.) {
0279       return selectCharged(particle);
0280     } else {
0281       return selectNeutral(particle);
0282     }
0283   }
0284 
0285   /// Copy results to output containers.
0286   ///
0287   /// @tparam particles_t is a SequenceContainer for particles
0288   /// @tparam hits_t is a SequenceContainer for hits
0289   template <typename particles_t, typename hits_t>
0290   void copyOutputs(const SimulationResult &result,
0291                    particles_t &particlesInitial, particles_t &particlesFinal,
0292                    hits_t &hits) const {
0293     // initial particle state was already pushed to the container before
0294     // store final particle state at the end of the simulation
0295     particlesFinal.push_back(result.particle);
0296     std::copy(result.hits.begin(), result.hits.end(), std::back_inserter(hits));
0297 
0298     // move generated secondaries that should be simulated to the output
0299     std::copy_if(
0300         result.generatedParticles.begin(), result.generatedParticles.end(),
0301         std::back_inserter(particlesInitial),
0302         [this](const Particle &particle) { return selectParticle(particle); });
0303   }
0304 
0305   /// Renumber particle ids in the tail of the container.
0306   ///
0307   /// Ensures particle ids are unique by modifying the sub-particle number
0308   /// within each generation.
0309   ///
0310   /// @param particles particle container in which particles are renumbered
0311   /// @param lastValid index of the last particle with a valid particle id
0312   ///
0313   /// @tparam particles_t is a SequenceContainer for particles
0314   ///
0315   /// @note This function assumes that all particles in the tail have the same
0316   ///       vertex numbers (primary/secondary) and particle number and are
0317   ///       ordered according to their generation number.
0318   ///
0319   template <typename particles_t>
0320   static void renumberTailParticleIds(particles_t &particles,
0321                                       std::size_t lastValid) {
0322     // iterate over adjacent pairs; potentially modify the second element.
0323     // assume e.g. a primary particle 2 with generation=subparticle=0 that
0324     // generates two secondaries during simulation. we have the following
0325     // ids (decoded as vertex|particle|generation|subparticle)
0326     //
0327     //     v|2|0|0, v|2|1|0, v|2|1|0
0328     //
0329     // where v represents the vertex numbers. this will be renumbered to
0330     //
0331     //     v|2|0|0, v|2|1|0, v|2|1|1
0332     //
0333     // if each secondary then generates two tertiaries we could have e.g.
0334     //
0335     //     v|2|0|0, v|2|1|0, v|2|1|1, v|2|2|0, v|2|2|1, v|2|2|0, v|2|2|1
0336     //
0337     // which would then be renumbered to
0338     //
0339     //     v|2|0|0, v|2|1|0, v|2|1|1, v|2|2|0, v|2|2|1, v|2|2|2, v|2|2|3
0340     //
0341     for (auto j = lastValid; (j + 1u) < particles.size(); ++j) {
0342       const auto prevId = particles[j].particleId();
0343       auto currId = particles[j + 1u].particleId();
0344       // NOTE primary/secondary vertex and particle are assumed to be equal
0345       // only renumber within one generation
0346       if (prevId.generation() != currId.generation()) {
0347         continue;
0348       }
0349       // ensure the sub-particle is strictly monotonic within a generation
0350       if (prevId.subParticle() < currId.subParticle()) {
0351         continue;
0352       }
0353       // sub-particle numbering must be non-zero
0354       currId = currId.withSubParticle(prevId.subParticle() + 1u);
0355       particles[j + 1u] = particles[j + 1u].withParticleId(currId);
0356     }
0357   }
0358 };
0359 
0360 }  // namespace ActsFatras