File indexing completed on 2026-09-13 08:27:26
0001
0002
0003
0004
0005
0006
0007
0008
0009
0010
0011
0012 #include "DDG4/Geant4EventAction.h"
0013 #include "DDG4/Geant4Kernel.h"
0014 #include "DD4hep/Printout.h"
0015
0016
0017 #include "G4Event.hh"
0018 #include "G4TrajectoryContainer.hh"
0019 #include "G4VTrajectory.hh"
0020 #include "G4VTrajectoryPoint.hh"
0021 #include "G4RichTrajectory.hh"
0022 #include "G4RichTrajectoryPoint.hh"
0023 #include "G4SystemOfUnits.hh"
0024 #include "G4AttValue.hh"
0025 #include "G4AttDef.hh"
0026
0027
0028 #include <algorithm>
0029 #include <cmath>
0030 #include <fstream>
0031 #include <limits>
0032 #include <sstream>
0033 #include <string>
0034 #include <vector>
0035
0036 #include <fmt/core.h>
0037 #include <fmt/format.h>
0038
0039 namespace dd4hep::sim {
0040
0041
0042
0043
0044
0045
0046
0047
0048
0049
0050 class FirebirdTrajectoryWriterEventAction : public Geant4EventAction {
0051
0052
0053
0054
0055 std::string m_outputFile{"trajectories.firebird.json"};
0056
0057
0058 std::string m_componentName{"Geant4Trajectories"};
0059
0060
0061 bool m_saveOptical{false};
0062 bool m_onlyPrimary{false};
0063 std::vector<int> m_saveParticles{};
0064 double m_minMomentum{150};
0065 double m_maxMomentum{1e12};
0066 double m_minTrackLength{0};
0067
0068
0069 bool m_vertexCut{false};
0070 double m_vertexZMin{-5000};
0071 double m_vertexZMax{ 5000};
0072
0073
0074 bool m_stepCut{false};
0075 double m_stepZMin{-5000};
0076 double m_stepZMax{ 5000};
0077 double m_stepRMax{ 5000};
0078
0079
0080 bool m_requireRichTrajectory{true};
0081
0082
0083 bool m_verboseTimeExtraction{false};
0084 bool m_verboseSteps{false};
0085
0086
0087
0088 long m_totalTrajectories{0};
0089 long m_filteredTrajectories{0};
0090 long m_savedTrajectories{0};
0091 long m_trajectoryWithoutTime{0};
0092 long m_stepsFiltered{0};
0093
0094
0095
0096 std::vector<std::string> m_entries;
0097
0098
0099
0100
0101 static bool isFinite(double v) { return !std::isinf(v) && !std::isnan(v); }
0102
0103
0104 static double sanitise(double v, double fallback = 0.0) {
0105 return isFinite(v) ? v : fallback;
0106 }
0107
0108
0109 static double rxy(const G4ThreeVector& p) {
0110 return std::sqrt(p.x() * p.x() + p.y() * p.y());
0111 }
0112
0113
0114
0115
0116
0117 static G4double parseTimeString(const std::string& raw) {
0118 std::istringstream iss(raw);
0119 G4double value{};
0120 std::string unit;
0121 iss >> value >> unit;
0122
0123 if (unit == "ps") return value * CLHEP::picosecond;
0124 else if (unit == "ns") return value * CLHEP::ns;
0125 else if (unit == "us" || unit == "µs") return value * CLHEP::microsecond;
0126 else if (unit == "ms") return value * CLHEP::ms;
0127 else if (unit == "s") return value * CLHEP::s;
0128 else return value * CLHEP::ns;
0129 }
0130
0131
0132
0133
0134
0135
0136
0137 G4double extractTimeFromPoint(G4VTrajectoryPoint* point,
0138 int pointIndex,
0139 G4VTrajectory* trajectory = nullptr)
0140 {
0141 auto* richPoint = dynamic_cast<G4RichTrajectoryPoint*>(point);
0142 if (!richPoint) {
0143 if (m_requireRichTrajectory) {
0144 if (m_verboseTimeExtraction) {
0145 warning("[firebird-writer] Point %d: not a G4RichTrajectoryPoint", pointIndex);
0146 }
0147 ++m_trajectoryWithoutTime;
0148 return -1.0;
0149 }
0150 return pointIndex * 0.1 * CLHEP::ns;
0151 }
0152
0153 auto* attValues = richPoint->CreateAttValues();
0154 if (!attValues) {
0155 if (m_verboseTimeExtraction) {
0156 warning("[firebird-writer] Point %d: CreateAttValues() returned null", pointIndex);
0157 }
0158 return -1.0;
0159 }
0160
0161 const std::string targetAttr = (pointIndex == 0) ? "PreT" : "PostT";
0162 G4double extractedTime = -1.0;
0163
0164 for (const auto& av : *attValues) {
0165 if (av.GetName() == targetAttr) {
0166 extractedTime = parseTimeString(av.GetValue());
0167
0168 if (m_verboseTimeExtraction) {
0169 info("[firebird-writer] Point %d: %s raw=\"%s\" → %.6f ns",
0170 pointIndex, targetAttr.c_str(),
0171 av.GetValue().c_str(), extractedTime / CLHEP::ns);
0172 }
0173 break;
0174 }
0175 }
0176 delete attValues;
0177
0178
0179 if (m_verboseSteps) {
0180 const auto pos = point->GetPosition();
0181 if (trajectory) {
0182 info("[firebird-steps] trk PDG=%d pt=%d pos=(%.3f, %.3f, %.3f) mm "
0183 "t=%.6f ns attr=%s",
0184 trajectory->GetPDGEncoding(), pointIndex,
0185 pos.x() / CLHEP::mm, pos.y() / CLHEP::mm, pos.z() / CLHEP::mm,
0186 (extractedTime >= 0 ? extractedTime / CLHEP::ns : -1.0),
0187 targetAttr.c_str());
0188 } else {
0189 info("[firebird-steps] pt=%d pos=(%.3f, %.3f, %.3f) mm t=%.6f ns attr=%s",
0190 pointIndex,
0191 pos.x() / CLHEP::mm, pos.y() / CLHEP::mm, pos.z() / CLHEP::mm,
0192 (extractedTime >= 0 ? extractedTime / CLHEP::ns : -1.0),
0193 targetAttr.c_str());
0194 }
0195 }
0196
0197
0198 if (extractedTime < 0) {
0199 if (m_requireRichTrajectory) {
0200 if (m_verboseTimeExtraction) {
0201 warning("[firebird-writer] Point %d: attribute %s not found", pointIndex, targetAttr.c_str());
0202 }
0203 ++m_trajectoryWithoutTime;
0204 return -1.0;
0205 }
0206 return pointIndex * 0.1 * CLHEP::ns;
0207 }
0208 return extractedTime;
0209 }
0210
0211
0212
0213 bool passesFilters(G4VTrajectory* trj) const {
0214 const int pdg = trj->GetPDGEncoding();
0215 const int parentID = trj->GetParentID();
0216 const double p_MeV = trj->GetInitialMomentum().mag() / CLHEP::MeV;
0217
0218
0219 if (m_saveOptical && trj->GetParticleName() == "opticalphoton") return true;
0220
0221 if (m_onlyPrimary && parentID != 0) return false;
0222
0223 if (p_MeV < m_minMomentum || p_MeV > m_maxMomentum) return false;
0224
0225
0226 if (!m_saveParticles.empty()) {
0227 if (std::find(m_saveParticles.begin(), m_saveParticles.end(), pdg)
0228 == m_saveParticles.end())
0229 return false;
0230 }
0231
0232
0233 if (m_minTrackLength > 0) {
0234 const int npts = trj->GetPointEntries();
0235 if (npts <= 1) return false;
0236
0237 double length = 0;
0238 auto prev = trj->GetPoint(0)->GetPosition();
0239 for (int i = 1; i < npts; ++i) {
0240 auto cur = trj->GetPoint(i)->GetPosition();
0241 length += (cur - prev).mag();
0242 prev = cur;
0243 }
0244 if (length / CLHEP::mm < m_minTrackLength) return false;
0245 }
0246
0247
0248 if (m_vertexCut && trj->GetPointEntries() > 0) {
0249 const double vz = trj->GetPoint(0)->GetPosition().z() / CLHEP::mm;
0250 if (vz < m_vertexZMin || vz > m_vertexZMax) return false;
0251 }
0252
0253 return true;
0254 }
0255
0256
0257
0258 bool passesRichTrajectoryCheck(G4VTrajectory* trj) {
0259 if (!m_requireRichTrajectory) return true;
0260
0261 if (!dynamic_cast<G4RichTrajectory*>(trj)) {
0262 if (m_verboseTimeExtraction)
0263 warning("[firebird-writer] Trajectory is not G4RichTrajectory — skipped");
0264 return false;
0265 }
0266 if (trj->GetPointEntries() > 0) {
0267 if (extractTimeFromPoint(trj->GetPoint(0), 0, trj) < 0) {
0268 if (m_verboseTimeExtraction)
0269 warning("[firebird-writer] First point has no time — trajectory skipped");
0270 return false;
0271 }
0272 }
0273 return true;
0274 }
0275
0276
0277
0278
0279 std::string buildParamsJson(G4VTrajectory* trj) {
0280 const auto mom = trj->GetInitialMomentum();
0281 const double p = std::max(mom.mag(), 1e-10);
0282
0283 const int pdg = trj->GetPDGEncoding();
0284 const double charge = trj->GetCharge();
0285 const double theta = mom.theta();
0286 const double phi = mom.phi();
0287 const double qOverP = charge / (p / CLHEP::GeV);
0288
0289 double vx = 0, vy = 0, vz = 0, time = 0;
0290 if (trj->GetPointEntries() > 0) {
0291 auto* pt0 = trj->GetPoint(0);
0292 const auto pos = pt0->GetPosition();
0293 vx = pos.x() / CLHEP::mm;
0294 vy = pos.y() / CLHEP::mm;
0295 vz = pos.z() / CLHEP::mm;
0296
0297 double t = extractTimeFromPoint(pt0, 0, trj);
0298 time = (t >= 0 ? t : 0.0) / CLHEP::ns;
0299 }
0300
0301
0302 return fmt::format("[{},\"{}\",{},{},{},{},{},{},{},{},{},{},{},{},{}]",
0303 pdg, trj->GetParticleName(), sanitise(charge),
0304 sanitise(mom.x() / CLHEP::MeV),
0305 sanitise(mom.y() / CLHEP::MeV),
0306 sanitise(mom.z() / CLHEP::MeV),
0307 sanitise(vx), sanitise(vy), sanitise(vz),
0308 sanitise(theta), sanitise(phi), sanitise(qOverP),
0309 0.0, 0.0, sanitise(time));
0310 }
0311
0312
0313 std::string buildPointsJson(G4VTrajectory* trj) {
0314 const int npts = trj->GetPointEntries();
0315 if (npts == 0) return "[]";
0316
0317 std::string out = "[";
0318 bool first = true;
0319
0320 for (int i = 0; i < npts; ++i) {
0321 auto* pt = trj->GetPoint(i);
0322 auto pos = pt->GetPosition();
0323
0324 if (m_stepCut) {
0325 const double z = pos.z() / CLHEP::mm;
0326 const double r = rxy(pos) / CLHEP::mm;
0327 if (z < m_stepZMin || z > m_stepZMax || r > m_stepRMax) {
0328 ++m_stepsFiltered;
0329 continue;
0330 }
0331 }
0332
0333 double t = extractTimeFromPoint(pt, i, trj);
0334 if (t < 0) t = i * 0.1 * CLHEP::ns;
0335 t /= CLHEP::ns;
0336
0337 if (!first) out += ',';
0338 first = false;
0339
0340 out += fmt::format("[{},{},{},{},{}]",
0341 sanitise(pos.x() / CLHEP::mm),
0342 sanitise(pos.y() / CLHEP::mm),
0343 sanitise(pos.z() / CLHEP::mm),
0344 sanitise(t), 0);
0345 }
0346 out += ']';
0347 return out;
0348 }
0349
0350
0351
0352 void writeJsonFile() const {
0353 if (m_entries.empty()) {
0354 warning("[firebird-writer] No events collected — output file not created.");
0355 return;
0356 }
0357
0358 std::ofstream out(m_outputFile);
0359 if (!out.is_open()) {
0360 error("[firebird-writer] Cannot open output file: %s", m_outputFile.c_str());
0361 return;
0362 }
0363
0364 out << fmt::format(
0365 R"({{"type":"firebird-dex-json","version":"0.04",)"
0366 R"("origin":{{"file":"{}","entries_count":{}}},)"
0367 R"("events":[)", m_outputFile, m_entries.size());
0368
0369 for (size_t i = 0; i < m_entries.size(); ++i) {
0370 if (i) out << ',';
0371 out << m_entries[i];
0372 }
0373 out << "]}";
0374 out.close();
0375
0376 info("[firebird-writer] Wrote %zu event(s) to %s",
0377 m_entries.size(), m_outputFile.c_str());
0378 }
0379
0380 void printStatistics() const {
0381 auto pct = [&](long n) {
0382 return m_totalTrajectories > 0
0383 ? n * 100.0 / m_totalTrajectories : 0.0;
0384 };
0385 info("[firebird-writer] ── statistics ──────────────────────");
0386 info("[firebird-writer] Total trajectories : %ld", m_totalTrajectories);
0387 info("[firebird-writer] Filtered (skipped) : %ld (%.1f%%)",
0388 m_filteredTrajectories, pct(m_filteredTrajectories));
0389 info("[firebird-writer] Saved : %ld (%.1f%%)",
0390 m_savedTrajectories, pct(m_savedTrajectories));
0391 if (m_stepCut)
0392 info("[firebird-writer] Step points filtered : %ld", m_stepsFiltered);
0393 if (m_requireRichTrajectory)
0394 info("[firebird-writer] Missing time info : %ld", m_trajectoryWithoutTime);
0395 }
0396
0397 void logConfiguration() const {
0398 info("[firebird-writer] ── configuration ───────────────────");
0399 info("[firebird-writer] OutputFile : %s", m_outputFile.c_str());
0400 info("[firebird-writer] ComponentName : %s", m_componentName.c_str());
0401 info("[firebird-writer] SaveOptical : %s", m_saveOptical ? "true" : "false");
0402 info("[firebird-writer] OnlyPrimary : %s", m_onlyPrimary ? "true" : "false");
0403 info("[firebird-writer] VertexCut : %s (Z: %.2f – %.2f mm)",
0404 m_vertexCut ? "true" : "false", m_vertexZMin, m_vertexZMax);
0405 info("[firebird-writer] StepCut : %s (Z: %.2f – %.2f mm, R < %.2f mm)",
0406 m_stepCut ? "true" : "false", m_stepZMin, m_stepZMax, m_stepRMax);
0407 info("[firebird-writer] Momentum : %.3f – %.3g MeV/c", m_minMomentum, m_maxMomentum);
0408 info("[firebird-writer] MinTrackLength : %.2f mm", m_minTrackLength);
0409 info("[firebird-writer] RequireRichTrajectory : %s", m_requireRichTrajectory ? "true" : "false");
0410 info("[firebird-writer] VerboseTimeExtraction : %s", m_verboseTimeExtraction ? "true" : "false");
0411 info("[firebird-writer] VerboseSteps : %s", m_verboseSteps ? "true" : "false");
0412
0413 if (m_saveParticles.empty()) {
0414 info("[firebird-writer] SaveParticles : [all]");
0415 } else {
0416 std::ostringstream ss;
0417 for (size_t i = 0; i < m_saveParticles.size(); ++i) {
0418 if (i) ss << ", ";
0419 ss << m_saveParticles[i];
0420 }
0421 info("[firebird-writer] SaveParticles : %s", ss.str().c_str());
0422 }
0423 }
0424
0425 public:
0426
0427
0428
0429 FirebirdTrajectoryWriterEventAction(Geant4Context* context,
0430 const std::string& name = "FirebirdTrajectoryWriterEventAction")
0431 : Geant4EventAction(context, name)
0432 {
0433 declareProperty("OutputFile", m_outputFile);
0434 declareProperty("ComponentName", m_componentName);
0435 declareProperty("SaveOptical", m_saveOptical);
0436 declareProperty("OnlyPrimary", m_onlyPrimary);
0437 declareProperty("VertexCut", m_vertexCut);
0438 declareProperty("VertexZMin", m_vertexZMin);
0439 declareProperty("VertexZMax", m_vertexZMax);
0440 declareProperty("StepCut", m_stepCut);
0441 declareProperty("StepZMin", m_stepZMin);
0442 declareProperty("StepZMax", m_stepZMax);
0443 declareProperty("StepRMax", m_stepRMax);
0444 declareProperty("MomentumMin", m_minMomentum);
0445 declareProperty("MomentumMax", m_maxMomentum);
0446 declareProperty("TrackLengthMin", m_minTrackLength);
0447 declareProperty("SaveParticles", m_saveParticles);
0448 declareProperty("RequireRichTrajectory", m_requireRichTrajectory);
0449 declareProperty("VerboseTimeExtraction", m_verboseTimeExtraction);
0450 declareProperty("VerboseSteps", m_verboseSteps);
0451
0452 logConfiguration();
0453 }
0454
0455 ~FirebirdTrajectoryWriterEventAction() override {
0456 writeJsonFile();
0457 printStatistics();
0458 }
0459
0460
0461
0462 void begin(const G4Event* ) override { }
0463
0464 void end(const G4Event* event) override {
0465 auto* container = event->GetTrajectoryContainer();
0466 if (!container || container->entries() == 0) {
0467 warning("[firebird-writer] Event %d: no trajectories", event->GetEventID());
0468 return;
0469 }
0470
0471 const int nTrajectories = container->entries();
0472 m_totalTrajectories += nTrajectories;
0473
0474 int nFiltered = 0, nSaved = 0;
0475
0476
0477 std::string ev = fmt::format(
0478 R"({{"id":{},"groups":[{{"name":"{}","type":"PointTrajectory",)"
0479 R"("origin":{{"type":["G4VTrajectory","G4VTrajectoryPoint"]}},)"
0480 R"("paramColumns":["pdg","type","charge","px","py","pz",)"
0481 R"("vx","vy","vz","theta","phi","q_over_p","loc_a","loc_b","time"],)"
0482 R"("pointColumns":["x","y","z","t","aux"],)"
0483 R"("trajectories":[)",
0484 event->GetEventID(), m_componentName);
0485
0486 bool first = true;
0487
0488 for (int i = 0; i < nTrajectories; ++i) {
0489 auto* trj = (*container)[i];
0490
0491 if (!passesFilters(trj) || !passesRichTrajectoryCheck(trj)) {
0492 ++nFiltered;
0493 continue;
0494 }
0495
0496 auto points = buildPointsJson(trj);
0497 if (points == "[]") { ++nFiltered; continue; }
0498
0499 if (!first) ev += ',';
0500 first = false;
0501
0502 ev += fmt::format(R"({{"points":{},"params":{}}})", points, buildParamsJson(trj));
0503 ++nSaved;
0504 }
0505
0506 ev += "]}]}";
0507
0508 if (nSaved > 0)
0509 m_entries.push_back(std::move(ev));
0510
0511 m_filteredTrajectories += nFiltered;
0512 m_savedTrajectories += nSaved;
0513
0514 info("[firebird-writer] Event %d: %d trajectories, %d filtered, %d saved",
0515 event->GetEventID(), nTrajectories, nFiltered, nSaved);
0516 }
0517 };
0518
0519 }
0520
0521
0522 #include "DDG4/Factories.h"
0523 DECLARE_GEANT4ACTION(FirebirdTrajectoryWriterEventAction)