LDMX Software
LDMXCascadeInterface.cxx
Go to the documentation of this file.
1
6
7#include <G4HadFinalState.hh>
8#include <G4HadSecondary.hh>
9#include <cmath>
10
11#include "G4InuclParticleNames.hh"
13
14namespace simcore {
15namespace bertini {
16
17namespace {
18
25constexpr int PDG_DIPROTON = 99111;
26constexpr int PDG_UNBOUNDPN = 99112;
27constexpr int PDG_DINEUTRON = 99122;
28
34int getPdgCode(int inuclType) {
35 using namespace G4InuclParticleNames;
36 switch (inuclType) {
37 case proton:
38 return 2212;
39 case neutron:
40 return 2112;
41 case pionPlus:
42 return 211;
43 case pionMinus:
44 return -211;
45 case pionZero:
46 return 111;
47 case photon:
48 return 22;
49 case kaonPlus:
50 return 321;
51 case kaonMinus:
52 return -321;
53 case kaonZero:
54 return 311;
55 case kaonZeroBar:
56 return -311;
57 case lambda:
58 return 3122;
59 case sigmaPlus:
60 return 3222;
61 case sigmaZero:
62 return 3212;
63 case sigmaMinus:
64 return 3112;
65 case xiZero:
66 return 3322;
67 case xiMinus:
68 return 3312;
69 case omegaMinus:
70 return 3334;
71 case electron:
72 return 11;
73 case positron:
74 return -11;
75 case muonMinus:
76 return 13;
77 case muonPlus:
78 return -13;
79 case deuteron:
80 return 1000010020;
81 case triton:
82 return 1000010030;
83 case He3:
84 return 1000020030;
85 case alpha:
86 return 1000020040;
87 // Quasi-deuteron states (virtual nucleon pairs for photon absorption)
88 case diproton:
89 return PDG_DIPROTON;
90 case unboundPN:
91 return PDG_UNBOUNDPN;
92 case dineutron:
93 return PDG_DINEUTRON;
94 default:
95 return 0;
96 }
97}
98
103int getCharge(int pdg) {
104 // Baryons
105 if (pdg == 2212) return +1; // proton
106 if (pdg == 2112) return 0; // neutron
107 if (pdg == 3122) return 0; // Lambda
108 if (pdg == 3222) return +1; // Sigma+
109 if (pdg == 3212) return 0; // Sigma0
110 if (pdg == 3112) return -1; // Sigma-
111 if (pdg == 3322) return 0; // Xi0
112 if (pdg == 3312) return -1; // Xi-
113 if (pdg == 3334) return -1; // Omega-
114
115 // Mesons
116 if (pdg == 211) return +1; // pi+
117 if (pdg == -211) return -1; // pi-
118 if (pdg == 111) return 0; // pi0
119 if (pdg == 321) return +1; // K+
120 if (pdg == -321) return -1; // K-
121 if (pdg == 311) return 0; // K0
122 if (pdg == -311) return 0; // K0bar
123 if (pdg == 310) return 0; // K0S
124 if (pdg == 130) return 0; // K0L
125
126 // Leptons/photons
127 if (pdg == 22) return 0; // photon
128 if (pdg == 11) return -1; // electron
129 if (pdg == -11) return +1; // positron
130 if (pdg == 13) return -1; // muon-
131 if (pdg == -13) return +1; // muon+
132
133 // Light nuclei
134 if (pdg == 1000010020) return +1; // deuteron
135 if (pdg == 1000010030) return +1; // triton
136 if (pdg == 1000020030) return +2; // He3
137 if (pdg == 1000020040) return +2; // alpha
138
139 // Quasi-deuterons
140 if (pdg == PDG_DIPROTON) return +2; // pp
141 if (pdg == PDG_UNBOUNDPN) return +1; // pn
142 if (pdg == PDG_DINEUTRON) return 0; // nn
143
144 return 0; // unknown
145}
146
151int getBaryonNumber(int pdg) {
152 // Baryons
153 if (pdg == 2212 || pdg == 2112) return +1; // p, n
154 if (pdg == 3122 || pdg == 3222 || pdg == 3212 || pdg == 3112)
155 return +1; // Lambda, Sigmas
156 if (pdg == 3322 || pdg == 3312 || pdg == 3334) return +1; // Xis, Omega
157
158 // Light nuclei
159 if (pdg == 1000010020) return +2; // deuteron
160 if (pdg == 1000010030) return +3; // triton
161 if (pdg == 1000020030) return +3; // He3
162 if (pdg == 1000020040) return +4; // alpha
163
164 // Quasi-deuterons
165 if (pdg == PDG_DIPROTON || pdg == PDG_UNBOUNDPN || pdg == PDG_DINEUTRON)
166 return +2;
167
168 return 0; // mesons, leptons, photons
169}
170
187int inferTargetPdg(int deltaCharge, int deltaBaryon) {
188 if (deltaBaryon == 1) {
189 // Single nucleon target
190 if (deltaCharge == 1) return 2212; // proton
191 if (deltaCharge == 0) return 2112; // neutron
192 } else if (deltaBaryon == 2) {
193 // Quasi-deuteron target
194 if (deltaCharge == 2) return PDG_DIPROTON; // pp
195 if (deltaCharge == 1) return PDG_UNBOUNDPN; // pn
196 if (deltaCharge == 0) return PDG_DINEUTRON; // nn
197 }
198 return 0; // unknown
199}
200
201} // anonymous namespace
202
203LDMXCascadeInterface::LDMXCascadeInterface(const G4String& name)
204 : G4CascadeInterface(name), incident_track_id_{-1} {}
205
206LDMXCascadeInterface::~LDMXCascadeInterface() = default;
207
208G4HadFinalState* LDMXCascadeInterface::ApplyYourself(
209 const G4HadProjectile& projectile, G4Nucleus& targetNucleus) {
210 double photon_energy = projectile.GetTotalEnergy();
211
212 ldmx_log(debug) << "LDMXCascadeInterface::ApplyYourself";
213 ldmx_log(debug) << " Track ID: " << incident_track_id_;
214 ldmx_log(debug) << " Photon energy: " << photon_energy << " MeV";
215 ldmx_log(debug) << " Energy threshold: " << energy_threshold_ << " MeV";
216
217 last_history_.clear();
218
219 bool should_record = (photon_energy >= energy_threshold_);
220
221 if (should_record) {
222 ldmx_log(debug) << " Recording history (above threshold)";
223 // Geant4 only creates the history object if G4CASCADE_SHOW_HISTORY is set,
224 // so we force-create it here
225 ensureCascadeHistoryExists();
226 } else {
227 ldmx_log(debug) << " Skipping history (below threshold)";
228 }
229
230 G4HadFinalState* result =
231 G4CascadeInterface::ApplyYourself(projectile, targetNucleus);
232
233 if (should_record) {
234 captureHistory();
235 last_history_.setIncidentEnergy(photon_energy);
236
237 ldmx_log(debug) << " Captured " << last_history_.getSteps().size()
238 << " steps";
239
240 // De-excitation (evaporation, gamma) happens after the cascade via
241 // G4ExcitationHandler. These products appear in the final state but
242 // are not in G4CascadeHistory, so we capture them separately.
243 captureDeexcitationProducts(result);
244
245 ldmx_log(debug) << " Total steps: " << last_history_.getSteps().size();
246
247 if (!last_history_.empty()) {
248 CascadeHistoryStore::getInstance().addHistory(incident_track_id_,
249 last_history_);
250 }
251 }
252
253 return result;
254}
255
256void LDMXCascadeInterface::ensureCascadeHistoryExists() {
257 // Force-create the cascade history object if it doesn't exist
258 // Geant4 normally only creates this if G4CASCADE_SHOW_HISTORY envvar is set
259
260 if (!collider) {
261 ldmx_log(debug) << " ensureCascadeHistoryExists: no collider yet";
262 return;
263 }
264
265 G4IntraNucleiCascader* cascader = collider->theIntraNucleiCascader;
266 if (!cascader) {
267 ldmx_log(debug) << " ensureCascadeHistoryExists: no cascader yet";
268 return;
269 }
270
271 if (!cascader->theCascadeHistory) {
272 ldmx_log(debug)
273 << " ensureCascadeHistoryExists: creating G4CascadeHistory";
274 cascader->theCascadeHistory = new G4CascadeHistory;
275 } else {
276 ldmx_log(debug) << " ensureCascadeHistoryExists: history already exists";
277 }
278}
279
280void LDMXCascadeInterface::captureHistory() {
281 // Navigate: this->collider->theIntraNucleiCascader->theCascadeHistory
282 if (!collider) return;
283
284 G4IntraNucleiCascader* cascader = collider->theIntraNucleiCascader;
285 if (!cascader) return;
286
287 G4CascadeHistory* g4_history = cascader->theCascadeHistory;
288 if (!g4_history) return;
289
290 last_history_.setIncidentTrackId(incident_track_id_);
291
292 if (cascader->tnuclei) {
293 last_history_.setTargetNucleus(cascader->tnuclei->getA(),
294 cascader->tnuclei->getZ());
295 }
296
297 const std::vector<G4CascadeHistory::HistoryEntry>& entries =
298 g4_history->theHistory;
299
300 if (entries.empty()) {
301 return;
302 }
303
304 // Build parent ID map from daughter lists
305 std::vector<int> parent_ids(entries.size(), -1);
306
307 for (size_t i = 0; i < entries.size(); ++i) {
308 const auto& entry = entries[i];
309 for (int d = 0; d < entry.n && d < 10; ++d) {
310 int daughter_id = entry.dId[d];
311 if (daughter_id >= 0 &&
312 static_cast<size_t>(daughter_id) < entries.size()) {
313 parent_ids[daughter_id] = static_cast<int>(i);
314 }
315 }
316 }
317
318 // First pass: convert G4 entries to LDMX steps
319 std::vector<ldmx::CascadeStep> steps;
320 steps.reserve(entries.size());
321
322 for (size_t i = 0; i < entries.size(); ++i) {
323 const auto& entry = entries[i];
324 int parent_id = parent_ids[i];
325
327
328 const G4CascadParticle& cpart = entry.cpart;
329 const G4InuclElementaryParticle& particle = cpart.getParticle();
330
331 step.setHistoryId(cpart.getHistoryId());
332 step.setParentId(parent_id);
333 step.setPdgId(getPdgCode(particle.type()));
334
335 // Geant4 Bertini uses GeV for momentum, fm for position
336 G4LorentzVector mom = cpart.getMomentum();
337 step.setMomentum(mom.px() * 1000.0, mom.py() * 1000.0, mom.pz() * 1000.0,
338 mom.e() * 1000.0);
339
340 const G4ThreeVector& pos = cpart.getPosition();
341 step.setPosition(pos.x(), pos.y(), pos.z());
342
343 step.setGeneration(cpart.getGeneration());
344 step.setZone(cpart.getCurrentZone());
345
346 std::vector<int> daughter_ids;
347 for (int d = 0; d < entry.n && d < 10; ++d) {
348 daughter_ids.push_back(entry.dId[d]);
349 }
350 step.setDaughterIds(daughter_ids);
351
352 bool interacted = (entry.n > 0);
353 step.setInteracted(interacted);
354
355 // Escaped if didn't interact (simplification - true escape needs more
356 // tracking)
357 bool escaped = !interacted && cpart.getGeneration() >= 0;
358 step.setEscaped(escaped);
359 step.setTargetPdgId(0); // inferred in second pass
360
361 ldmx::CascadeStage stage = ldmx::CascadeStage::UNKNOWN;
362 int generation = cpart.getGeneration();
363
364 if (generation == 0) {
365 stage = ldmx::CascadeStage::INCIDENT;
366 } else if (generation == 1) {
367 stage = ldmx::CascadeStage::PRIMARY;
368 } else if (!interacted && !escaped) {
369 stage = ldmx::CascadeStage::ABSORBED;
370 } else if (escaped && generation <= 2) {
371 stage = ldmx::CascadeStage::PREEQUILIBRIUM;
372 } else {
373 stage = ldmx::CascadeStage::CASCADE;
374 }
375 step.setStage(stage);
376
377 steps.push_back(std::move(step));
378 }
379
380 // Second pass: infer target nucleon from charge/baryon conservation
381 for (auto& step : steps) {
382 if (!step.didInteract()) continue;
383
384 int bullet_charge = getCharge(step.getPdgId());
385 int bullet_baryon = getBaryonNumber(step.getPdgId());
386 int daughter_charge = 0;
387 int daughter_baryon = 0;
388
389 for (int daughter_id : step.getDaughterIds()) {
390 for (const auto& s : steps) {
391 if (s.getHistoryId() == daughter_id) {
392 daughter_charge += getCharge(s.getPdgId());
393 daughter_baryon += getBaryonNumber(s.getPdgId());
394 break;
395 }
396 }
397 }
398
399 int delta_charge = daughter_charge - bullet_charge;
400 int delta_baryon = daughter_baryon - bullet_baryon;
401 step.setTargetPdgId(inferTargetPdg(delta_charge, delta_baryon));
402 }
403
404 last_history_.reserve(steps.size());
405 for (auto& step : steps) {
406 last_history_.addStep(std::move(step));
407 }
408
409 // Excitation energy approximation: E_incident - sum(KE of escaped)
410 double total_escaped_energy = 0.0;
411 int escaped_protons = 0;
412 int escaped_neutrons = 0;
413
414 for (const auto& step : last_history_.getSteps()) {
415 if (step.didEscape()) {
416 total_escaped_energy += step.getKineticEnergy();
417 int pdg = step.getPdgId();
418 if (pdg == 2212)
419 escaped_protons++;
420 else if (pdg == 2112)
421 escaped_neutrons++;
422 }
423 }
424
425 double excitation_energy =
426 last_history_.getIncidentEnergy() - total_escaped_energy;
427 if (excitation_energy < 0) excitation_energy = 0;
428 last_history_.setExcitationEnergy(excitation_energy);
429
430 int target_a = last_history_.getTargetA();
431 int target_z = last_history_.getTargetZ();
432 int residual_a = target_a - escaped_protons - escaped_neutrons;
433 int residual_z = target_z - escaped_protons;
434 if (residual_a < 0) residual_a = 0;
435 if (residual_z < 0) residual_z = 0;
436 last_history_.setResidualNucleus(residual_a, residual_z);
437}
438
439void LDMXCascadeInterface::captureDeexcitationProducts(
440 G4HadFinalState* finalState) {
441 if (!finalState) return;
442
443 int n_secondaries = finalState->GetNumberOfSecondaries();
444 if (n_secondaries == 0) return;
445
446 // Match final state particles against cascade escapees to identify
447 // de-excitation products. Energy matching has 1 MeV tolerance for recoil.
448 struct EscapedParticle {
449 int pdg_;
450 double energy_;
451 bool matched_;
452 };
453 std::vector<EscapedParticle> cascade_escaped;
454
455 for (const auto& step : last_history_.getSteps()) {
456 if (step.didEscape()) {
457 cascade_escaped.push_back({step.getPdgId(), step.getEnergy(), false});
458 }
459 }
460
461 int next_history_id = 0;
462 for (const auto& step : last_history_.getSteps()) {
463 if (step.getHistoryId() >= next_history_id) {
464 next_history_id = step.getHistoryId() + 1;
465 }
466 }
467
468 for (int i = 0; i < n_secondaries; ++i) {
469 G4HadSecondary* secondary = finalState->GetSecondary(i);
470 if (!secondary) continue;
471
472 const G4DynamicParticle* dyn_particle = secondary->GetParticle();
473 if (!dyn_particle) continue;
474
475 int pdg = dyn_particle->GetPDGcode();
476 double energy = dyn_particle->GetTotalEnergy(); // MeV
477
478 bool is_deexcitation = true;
479 const double energy_tolerance = 1.0;
480
481 for (auto& escaped : cascade_escaped) {
482 if (!escaped.matched_ && escaped.pdg_ == pdg &&
483 std::abs(escaped.energy_ - energy) < energy_tolerance) {
484 escaped.matched_ = true;
485 is_deexcitation = false;
486 break;
487 }
488 }
489
490 if (is_deexcitation) {
492 step.setHistoryId(next_history_id++);
493 step.setParentId(-2); // marker for de-excitation origin
494 step.setPdgId(pdg);
495
496 G4ThreeVector mom = dyn_particle->GetMomentum();
497 step.setMomentum(mom.x(), mom.y(), mom.z(), energy);
498 step.setPosition(0, 0, 0);
499 step.setGeneration(-1); // marker for de-excitation
500 step.setZone(0);
501 step.setInteracted(false);
502 step.setEscaped(true);
503 step.setStage(ldmx::CascadeStage::DEEXCITATION);
504
505 last_history_.addStep(std::move(step));
506 }
507 }
508}
509
510} // namespace bertini
511} // namespace simcore
Thread-local storage for cascade histories during simulation.
CascadeStage
Classification of cascade particle stages.
Definition CascadeStep.h:20
Bertini cascade interface with history capture.
Single particle state in the Bertini intranuclear cascade.
Definition CascadeStep.h:39
Dynamically loadable photonuclear models either from SimCore or external libraries implementing this ...