aegir
Phlex-based simulation framework for the SHiP experiment.
Loading...
Searching...
No Matches
pythia_common.hpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2026 CERN for the benefit of the SHiP Collaboration
2//
3// SPDX-License-Identifier: LGPL-3.0-or-later
4
5// pythia_common.hpp — helpers shared by the Pythia8-based generators
6//
7// Header-only and free of Phlex/data-model dependencies so the standalone
8// benchmark can share the same code. extract_particles() is templated on the
9// output particle type (any struct exposing the MCParticle fields), letting
10// the plugins emit SHiP::MCParticle while the benchmark keeps its local
11// stand-in.
12
13#pragma once
14
15#include <Pythia8/Pythia.h>
16
17#include <SHiP/Units.hpp>
18#include <cstdint>
19#include <stdexcept>
20#include <string>
21#include <string_view>
22#include <utility>
23#include <vector>
24
25#include "record_mode.hpp"
26
27namespace aegir {
28
29// Pythia8's native units: energies/momenta in GeV, positions in mm,
30// production times in mm/c.
31using PythiaTime = mp_units::quantity<ship::units::mm_per_c, double>;
32
33// Map a 32-bit base seed into Pythia's valid Random:seed range
34// [1, 900000000]. extra_streams reserves headroom for consecutive
35// per-instance seeds (PythiaParallel seeds helper i with Random:seed + i).
36inline int pythia_seed(std::uint32_t base, int extra_streams = 0) {
37 if (extra_streams < 0 || extra_streams >= 900000000) {
38 throw std::invalid_argument("pythia_seed: extra_streams " +
39 std::to_string(extra_streams) +
40 " must be in [0, 900000000)");
41 }
42 auto const range = static_cast<std::uint32_t>(900000000 - extra_streams);
43 return static_cast<int>(base % range) + 1;
44}
45
46// Configure a fixed-target beam: beam A on a stationary target B (frameType 2,
47// eB = 0). Templated so it works for both Pythia8::Pythia and PythiaParallel.
48template <typename Pythia>
49void configure_beams(Pythia& pythia, int idA, int idB,
50 ship::Energy beam_energy) {
51 pythia.readString("Beams:idA = " + std::to_string(idA));
52 pythia.readString("Beams:idB = " + std::to_string(idB));
53 pythia.readString("Beams:frameType = 2");
54 pythia.readString(
55 "Beams:eA = " +
56 std::to_string(beam_energy.numerical_value_in(ship::units::GeV)));
57 pythia.readString("Beams:eB = 0.");
58}
59
60// Make long-lived particles (tau0 above threshold) stable so a downstream
61// simulation (e.g. Geant4) handles their decay. Guards against null
62// particleData entries.
63template <typename Pythia>
64void stabilise_long_lived(Pythia& pythia, PythiaTime tau0_threshold) {
65 double const threshold =
66 tau0_threshold.numerical_value_in(ship::units::mm_per_c);
67 for (auto it = pythia.particleData.begin(); it != pythia.particleData.end();
68 ++it) {
69 auto const& entry = it->second; // ParticleDataEntryPtr (shared_ptr-like)
70 if (entry && entry->tau0() > threshold) {
71 entry->setMayDecay(false);
72 }
73 }
74}
75
76// Advance the generator to its next event, retrying transient failures.
77// Pythia8::next() can occasionally reject a trial event; persistent failure
78// is a hard error rather than a silently-empty event, so it cannot leak
79// empty entries into the output (same convention as Pythia8MTSource
80// exhaustion).
81template <typename Pythia>
82void next_event(Pythia& pythia, std::string_view source_name,
83 int max_attempts = 10) {
84 for (int attempt = 0; attempt < max_attempts; ++attempt) {
85 if (pythia.next()) {
86 return;
87 }
88 }
89 throw std::runtime_error(std::string(source_name) +
90 ": Pythia8 event generation failed " +
91 std::to_string(max_attempts) + " times in a row");
92}
93
94// Extract particles from a Pythia event record into a vector of MCParticle
95// (any type exposing pdgCode/vertex/momentum/energy/time/motherId/status).
96// vertex z is shifted by z_offset.
97//
98// In final_state mode only undecayed particles are emitted; in full mode the
99// whole record is, so beam particles, decayed mothers and intermediate
100// entries appear too. The emitted `status` is the HepMC status
101// (Pythia8's statusHepMC(); see hepmc_status.hpp for the code set), so a
102// consumer can tell the two apart — geant4_module tracks exactly status 1.
103// Pythia's entry 0, the "system" pseudo-particle, is never emitted.
104//
105// `mothers` is the complete mother list, taken from Pythia's motherList().
106// That accessor is derived from mother1, mother2 *and* the native status, so
107// it already resolves the six mother1/mother2 combinations — a carbon copy, a
108// single mother, an inclusive range of string-fragmentation mothers, two
109// genuinely distinct mothers — into one uniform list. It is empty for the
110// beam particles, whose history Pythia does not record.
111//
112// Indices are remapped from the Pythia record to the returned vector.
113// Mothers that were not themselves written out are dropped rather than
114// recorded as -1, because the data model reserves -1 for motherId alone and
115// requires every element of `mothers` to be a valid index. In final_state
116// mode most mothers are dropped, since mothers of final-state particles
117// generally are not final state themselves; in full mode the map degenerates
118// to "record index minus one" and the whole chain survives.
119//
120// motherId is then the first surviving mother, or -1 when none survived,
121// which keeps it equal to mothers.front() as the data model requires.
122template <typename MCParticle>
123std::vector<MCParticle> extract_particles(
124 Pythia8::Event const& event, ship::Length z_offset = ship::Length::zero(),
126 namespace su = ship::units;
127 std::vector<MCParticle> particles;
128 particles.reserve(event.size());
129
130 // Pythia-record index -> output index for written particles. Entry 0 is
131 // never written, so out_index[0] stays -1 and mother1() == 0 ("no mother"
132 // in Pythia) maps onto MCParticle's -1 without a special case.
133 std::vector<int> out_index(static_cast<std::size_t>(event.size()), -1);
134
135 for (int i = 1; i < event.size(); ++i) {
136 auto const& p = event[i];
137 if (mode == record_mode::final_state && !p.isFinal()) {
138 continue;
139 }
140
141 out_index[static_cast<std::size_t>(i)] = static_cast<int>(particles.size());
142
143 MCParticle mc;
144 mc.pdgCode = p.id();
145 // Each Pythia accessor acquires its unit on the read line; ship::raw is
146 // the single bitwise unwrap into the storage units. The ship::view
147 // setters are unavailable here because MCParticle is a template parameter
148 // (the benchmark instantiates a local struct without the data model).
149 mc.vertex = ship::raw(ship::Vec3<ship::Length>{
150 p.xProd() * su::mm, p.yProd() * su::mm, p.zProd() * su::mm + z_offset});
151 mc.momentum = ship::raw(ship::Vec3<ship::Momentum>{p.px() * su::GeV_per_c,
152 p.py() * su::GeV_per_c,
153 p.pz() * su::GeV_per_c});
154 mc.energy = ship::raw(p.e() * su::GeV);
155 // mm/c -> ns via the exact definition of c (no hand-typed constant).
156 mc.time = (p.tProd() * su::mm_per_c).numerical_value_in(su::ns);
157 mc.status = p.statusHepMC();
158 // Record indices for now; remapped below, once out_index is complete.
159 for (int const m : p.motherList()) {
160 mc.mothers.push_back(m);
161 }
162 particles.push_back(mc);
163 }
164
165 for (auto& mc : particles) {
166 std::vector<std::int32_t> mapped;
167 mapped.reserve(mc.mothers.size());
168 for (int const m : mc.mothers) {
169 if (m < 0 || std::cmp_greater_equal(m, out_index.size())) {
170 continue;
171 }
172 if (int const out = out_index[static_cast<std::size_t>(m)]; out >= 0) {
173 mapped.push_back(out);
174 }
175 }
176 mc.mothers = std::move(mapped);
177 mc.motherId = mc.mothers.empty() ? -1 : mc.mothers.front();
178 }
179 return particles;
180}
181
182} // namespace aegir
Definition hepmc_status.hpp:17
record_mode
Definition record_mode.hpp:20
@ final_state
Only particles the generator left undecayed.
void configure_beams(Pythia &pythia, int idA, int idB, ship::Energy beam_energy)
Definition pythia_common.hpp:49
mp_units::quantity< ship::units::mm_per_c, double > PythiaTime
Definition pythia_common.hpp:31
void next_event(Pythia &pythia, std::string_view source_name, int max_attempts=10)
Definition pythia_common.hpp:82
int pythia_seed(std::uint32_t base, int extra_streams=0)
Definition pythia_common.hpp:36
void stabilise_long_lived(Pythia &pythia, PythiaTime tau0_threshold)
Definition pythia_common.hpp:64
std::vector< MCParticle > extract_particles(Pythia8::Event const &event, ship::Length z_offset=ship::Length::zero(), record_mode mode=record_mode::final_state)
Definition pythia_common.hpp:123