Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-04-28 07:12:10

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 "ActsFatras/EventData/Particle.hpp"
0014 #include "ActsFatras/Kernel/SingleParticleSimulationResult.hpp"
0015 #include "ActsFatras/Kernel/detail/SimulationError.hpp"
0016 
0017 #include <algorithm>
0018 #include <cassert>
0019 #include <iterator>
0020 
0021 namespace ActsFatras {
0022 
0023 /// A particle that failed to simulate.
0024 struct FailedParticle {
0025   /// Initial particle state of the failed particle.
0026   ///
0027   /// This must store the full particle state to be able to handle secondaries
0028   /// that are not in the input particle list. Otherwise they could not be
0029   /// referenced.
0030   Particle particle;
0031   /// The associated error code for this particular failure case.
0032   std::error_code error;
0033 };
0034 
0035 /// Multi-particle/event simulation.
0036 ///
0037 /// @tparam charged_selector_t Callable selector type for charged particles
0038 /// @tparam charged_simulator_t Single particle simulator for charged particles
0039 /// @tparam neutral_selector_t Callable selector type for neutral particles
0040 /// @tparam neutral_simulator_t Single particle simulator for neutral particles
0041 ///
0042 /// The selector types for charged and neutral particles **do not** need to
0043 /// check for the particle charge. This is done automatically by the simulator
0044 /// to ensure consistency.
0045 template <typename charged_selector_t, typename charged_simulator_t,
0046           typename neutral_selector_t, typename neutral_simulator_t>
0047 struct MultiParticleSimulation {
0048   /// Selector for charged particle simulation
0049   charged_selector_t selectCharged;
0050   /// Selector for neutral particle simulation
0051   neutral_selector_t selectNeutral;
0052   /// Simulator for charged particles
0053   charged_simulator_t charged;
0054   /// Simulator for neutral particles
0055   neutral_simulator_t neutral;
0056 
0057   /// Construct from the single charged/neutral particle simulators.
0058   /// @param charged_ Simulator for charged particles
0059   /// @param neutral_ Simulator for neutral particles
0060   MultiParticleSimulation(charged_simulator_t &&charged_,
0061                           neutral_simulator_t &&neutral_)
0062       : charged(std::move(charged_)), neutral(std::move(neutral_)) {}
0063 
0064   /// Simulate multiple particles and generated secondaries.
0065   ///
0066   /// @param geoCtx is the geometry context to access surface geometries
0067   /// @param magCtx is the magnetic field context to access field values
0068   /// @param generator is the random number generator
0069   /// @param inputParticles contains all particles that should be simulated
0070   /// @param simulatedParticlesInitial contains initial particle states
0071   /// @param simulatedParticlesFinal contains final particle states
0072   /// @param hits contains all generated hits
0073   /// @retval Acts::Result::Error if there is a fundamental issue
0074   /// @retval Acts::Result::Success with all particles that failed to simulate
0075   ///
0076   /// @warning Particle-hit association is based on particle ids generated
0077   ///          during the simulation. This requires that all input particles
0078   ///          **must** have generation and sub-particle number set to zero.
0079   /// @note Parameter edge-cases can lead to errors in the underlying propagator
0080   ///       and thus to particles that fail to simulate. Here, full events are
0081   ///       simulated and the failure to simulate one particle should not be
0082   ///       considered a general failure of the simulator. Instead, a list of
0083   ///       particles that fail to simulate is provided to the user. It is the
0084   ///       users responsibility to handle them.
0085   /// @note Failed particles are removed from the regular output, i.e. they do
0086   ///       not appear in the simulated particles containers nor do they
0087   ///       generate hits.
0088   ///
0089   /// This takes all input particles and simulates those passing the selection
0090   /// using the appropriate simulator. All selected particle states including
0091   /// additional ones generated from interactions are stored in separate output
0092   /// containers; both the initial state at the production vertex and the final
0093   /// state after propagation are stored. Hits generated from selected input and
0094   /// generated particles are stored in the hit container.
0095   ///
0096   /// @tparam generator_t is the type of the random number generator
0097   /// @tparam input_particles_t is a Container for particles
0098   /// @tparam output_particles_t is a SequenceContainer for particles
0099   /// @tparam hits_t is a SequenceContainer for hits
0100   template <typename generator_t, typename input_particles_t,
0101             typename output_particles_t, typename hits_t>
0102   Acts::Result<std::vector<FailedParticle>> simulate(
0103       const Acts::GeometryContext &geoCtx,
0104       const Acts::MagneticFieldContext &magCtx, generator_t &generator,
0105       const input_particles_t &inputParticles,
0106       output_particles_t &simulatedParticlesInitial,
0107       output_particles_t &simulatedParticlesFinal, hits_t &hits) const {
0108     assert(
0109         (simulatedParticlesInitial.size() == simulatedParticlesFinal.size()) &&
0110         "Inconsistent initial sizes of the simulated particle containers");
0111 
0112     std::vector<FailedParticle> failedParticles;
0113 
0114     for (const Particle &inputParticle : inputParticles) {
0115       // only consider simulatable particles
0116       if (!selectParticle(inputParticle)) {
0117         continue;
0118       }
0119       // required to allow correct particle id numbering for secondaries later
0120       if ((inputParticle.particleId().generation() != 0u) ||
0121           (inputParticle.particleId().subParticle() != 0u)) {
0122         return detail::SimulationError::InvalidInputParticleId;
0123       }
0124 
0125       // Do a *depth-first* simulation of the particle and its secondaries,
0126       // i.e. we simulate all secondaries, tertiaries, ... before simulating
0127       // the next primary particle. Use the end of the output container as
0128       // a queue to store particles that should be simulated.
0129       //
0130       // WARNING the initial particle state output container will be modified
0131       //         during iteration. New secondaries are added to and failed
0132       //         particles might be removed. To avoid issues, access must always
0133       //         occur via indices.
0134       std::size_t iinitial = simulatedParticlesInitial.size();
0135       simulatedParticlesInitial.push_back(inputParticle);
0136       while (iinitial < simulatedParticlesInitial.size()) {
0137         const auto &initialParticle = simulatedParticlesInitial[iinitial];
0138 
0139         // only simulatable particles are pushed to the container and here we
0140         // only need to switch between charged/neutral.
0141         auto result = Acts::Result<SingleParticleSimulationResult>::success({});
0142         if (initialParticle.charge() != 0.) {
0143           result = charged.simulate(geoCtx, magCtx, generator, initialParticle);
0144         } else {
0145           result = neutral.simulate(geoCtx, magCtx, generator, initialParticle);
0146         }
0147 
0148         if (!result.ok()) {
0149           // remove particle from output container since it was not simulated.
0150           simulatedParticlesInitial.erase(
0151               std::next(simulatedParticlesInitial.begin(), iinitial));
0152           // record the particle as failed
0153           failedParticles.push_back({initialParticle, result.error()});
0154           continue;
0155         }
0156 
0157         assert(result->particle.particleId() == initialParticle.particleId() &&
0158                "Particle id must not change during simulation");
0159 
0160         copyOutputs(result.value(), simulatedParticlesInitial,
0161                     simulatedParticlesFinal, hits);
0162         // since physics processes are independent, there can be particle id
0163         // collisions within the generated secondaries. they can be resolved by
0164         // renumbering within each sub-particle generation. this must happen
0165         // before the particle is simulated since the particle id is used to
0166         // associate generated hits back to the particle.
0167         renumberTailParticleIds(simulatedParticlesInitial, iinitial);
0168 
0169         ++iinitial;
0170       }
0171     }
0172 
0173     assert(
0174         (simulatedParticlesInitial.size() == simulatedParticlesFinal.size()) &&
0175         "Inconsistent final sizes of the simulated particle containers");
0176 
0177     // the overall function call succeeded, i.e. no fatal errors occurred.
0178     // yet, there might have been some particle for which the propagation
0179     // failed. thus, the successful result contains a list of failed particles.
0180     // sounds a bit weird, but that is the way it is.
0181     return failedParticles;
0182   }
0183 
0184  private:
0185   /// Select if the particle should be simulated at all.
0186   bool selectParticle(const Particle &particle) const {
0187     if (particle.charge() != 0.) {
0188       return selectCharged(particle);
0189     } else {
0190       return selectNeutral(particle);
0191     }
0192   }
0193 
0194   /// Copy results to output containers.
0195   ///
0196   /// @tparam particles_t is a SequenceContainer for particles
0197   /// @tparam hits_t is a SequenceContainer for hits
0198   template <typename particles_t, typename hits_t>
0199   void copyOutputs(const SingleParticleSimulationResult &result,
0200                    particles_t &particlesInitial, particles_t &particlesFinal,
0201                    hits_t &hits) const {
0202     // initial particle state was already pushed to the container before
0203     // store final particle state at the end of the simulation
0204     particlesFinal.push_back(result.particle);
0205     std::copy(result.hits.begin(), result.hits.end(), std::back_inserter(hits));
0206 
0207     // move generated secondaries that should be simulated to the output
0208     std::copy_if(
0209         result.generatedParticles.begin(), result.generatedParticles.end(),
0210         std::back_inserter(particlesInitial),
0211         [this](const Particle &particle) { return selectParticle(particle); });
0212   }
0213 
0214   /// Renumber particle ids in the tail of the container.
0215   ///
0216   /// Ensures particle ids are unique by modifying the sub-particle number
0217   /// within each generation.
0218   ///
0219   /// @param particles particle container in which particles are renumbered
0220   /// @param lastValid index of the last particle with a valid particle id
0221   ///
0222   /// @tparam particles_t is a SequenceContainer for particles
0223   ///
0224   /// @note This function assumes that all particles in the tail have the same
0225   ///       vertex numbers (primary/secondary) and particle number and are
0226   ///       ordered according to their generation number.
0227   ///
0228   template <typename particles_t>
0229   static void renumberTailParticleIds(particles_t &particles,
0230                                       std::size_t lastValid) {
0231     // iterate over adjacent pairs; potentially modify the second element.
0232     // assume e.g. a primary particle 2 with generation=subparticle=0 that
0233     // generates two secondaries during simulation. we have the following
0234     // ids (decoded as vertex|particle|generation|subparticle)
0235     //
0236     //     v|2|0|0, v|2|1|0, v|2|1|0
0237     //
0238     // where v represents the vertex numbers. this will be renumbered to
0239     //
0240     //     v|2|0|0, v|2|1|0, v|2|1|1
0241     //
0242     // if each secondary then generates two tertiaries we could have e.g.
0243     //
0244     //     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
0245     //
0246     // which would then be renumbered to
0247     //
0248     //     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
0249     //
0250     for (auto j = lastValid; (j + 1u) < particles.size(); ++j) {
0251       const auto prevId = particles[j].particleId();
0252       auto currId = particles[j + 1u].particleId();
0253       // NOTE primary/secondary vertex and particle are assumed to be equal
0254       // only renumber within one generation
0255       if (prevId.generation() != currId.generation()) {
0256         continue;
0257       }
0258       // ensure the sub-particle is strictly monotonic within a generation
0259       if (prevId.subParticle() < currId.subParticle()) {
0260         continue;
0261       }
0262       // sub-particle numbering must be non-zero
0263       currId = currId.withSubParticle(prevId.subParticle() + 1u);
0264       particles[j + 1u] = particles[j + 1u].withParticleId(currId);
0265     }
0266   }
0267 };
0268 
0269 }  // namespace ActsFatras