Warning, file /snippets/Tracking/BackgroundStudy/extract_generator_hepmc.py was not indexed
or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).
0001
0002 """Recover generator particles from an EDM4hep npsim output file.
0003 Shujie Li with Codex, 2026.
0004
0005 The input file contains both the original generator record and particles
0006 created later by Geant4 interactions in detector material. This script keeps
0007 only MCParticles for which BITCreatedInSimulation (simulatorStatus bit 30) is
0008 not set, rebuilds their parent/daughter graph, and writes a HepMC3 ASCII file
0009 that can be used as a new npsim input.
0010
0011 Dependencies: numpy, awkward, uproot, pyhepmc. No local modules or detector
0012 geometry are needed. Input positions/time are mm/ns; momentum/mass are GeV.
0013
0014 Example:
0015 python extract_generator_hepmc.py \
0016 hepmc/edm4hep/sample.edm4hep.root \
0017 hepmc/sample.hepmc3
0018 """
0019
0020 import argparse
0021 import math
0022 from pathlib import Path
0023 import sys
0024
0025 import awkward as ak
0026 import numpy as np
0027 import pyhepmc
0028 import uproot
0029
0030
0031 CREATED_IN_SIMULATION_BIT = np.uint32(1 << 30)
0032 SPEED_OF_LIGHT_MM_PER_NS = 299.792458
0033
0034 BRANCHES = {
0035 "event_number": "EventHeader/EventHeader.eventNumber",
0036 "pdg": "MCParticles/MCParticles.PDG",
0037 "generator_status": "MCParticles/MCParticles.generatorStatus",
0038 "simulator_status": "MCParticles/MCParticles.simulatorStatus",
0039 "mass": "MCParticles/MCParticles.mass",
0040 "time": "MCParticles/MCParticles.time",
0041 "vertex_x": "MCParticles/MCParticles.vertex.x",
0042 "vertex_y": "MCParticles/MCParticles.vertex.y",
0043 "vertex_z": "MCParticles/MCParticles.vertex.z",
0044 "endpoint_x": "MCParticles/MCParticles.endpoint.x",
0045 "endpoint_y": "MCParticles/MCParticles.endpoint.y",
0046 "endpoint_z": "MCParticles/MCParticles.endpoint.z",
0047 "momentum_x": "MCParticles/MCParticles.momentum.x",
0048 "momentum_y": "MCParticles/MCParticles.momentum.y",
0049 "momentum_z": "MCParticles/MCParticles.momentum.z",
0050 "parents_begin": "MCParticles/MCParticles.parents_begin",
0051 "parents_end": "MCParticles/MCParticles.parents_end",
0052 "daughters_begin": "MCParticles/MCParticles.daughters_begin",
0053 "daughters_end": "MCParticles/MCParticles.daughters_end",
0054 "parent_index": "_MCParticles_parents/_MCParticles_parents.index",
0055 "daughter_index": "_MCParticles_daughters/_MCParticles_daughters.index",
0056 }
0057
0058
0059 def event_array(arrays, branch_name):
0060 """Return one event's jagged branch as a NumPy array."""
0061 return np.asarray(ak.to_numpy(arrays[branch_name][0]))
0062
0063
0064 def source_relations(begin, end, relation_indices, source_indices, source_set):
0065 """Return retained relation indices for each retained particle."""
0066 relations = {}
0067 number_of_particles = len(begin)
0068
0069 for particle_index in source_indices:
0070 relation_begin = int(begin[particle_index])
0071 relation_end = int(end[particle_index])
0072 if not 0 <= relation_begin <= relation_end <= len(relation_indices):
0073 raise ValueError(
0074 f"particle {particle_index} has an invalid relation range "
0075 f"[{relation_begin}, {relation_end})"
0076 )
0077
0078 retained_relations = []
0079 for related_index in relation_indices[relation_begin:relation_end]:
0080 related_index = int(related_index)
0081 if not 0 <= related_index < number_of_particles:
0082 raise ValueError(
0083 f"particle {particle_index} refers to invalid particle "
0084 f"index {related_index}"
0085 )
0086 if related_index in source_set:
0087 if related_index == particle_index:
0088 raise ValueError(f"particle {particle_index} refers to itself")
0089 retained_relations.append(related_index)
0090
0091 if len(set(retained_relations)) != len(retained_relations):
0092 raise ValueError(f"particle {particle_index} has duplicate relations")
0093
0094
0095 relations[int(particle_index)] = tuple(sorted(retained_relations))
0096
0097 return relations
0098
0099
0100 def build_hepmc_event(arrays, entry_number):
0101 """Convert one uproot event into a source-only HepMC3 GenEvent."""
0102 pdg = event_array(arrays, BRANCHES["pdg"])
0103 generator_status = event_array(arrays, BRANCHES["generator_status"])
0104 simulator_status = event_array(arrays, BRANCHES["simulator_status"])
0105 mass = event_array(arrays, BRANCHES["mass"])
0106 time = event_array(arrays, BRANCHES["time"])
0107 vertex_x = event_array(arrays, BRANCHES["vertex_x"])
0108 vertex_y = event_array(arrays, BRANCHES["vertex_y"])
0109 vertex_z = event_array(arrays, BRANCHES["vertex_z"])
0110 endpoint_x = event_array(arrays, BRANCHES["endpoint_x"])
0111 endpoint_y = event_array(arrays, BRANCHES["endpoint_y"])
0112 endpoint_z = event_array(arrays, BRANCHES["endpoint_z"])
0113 momentum_x = event_array(arrays, BRANCHES["momentum_x"])
0114 momentum_y = event_array(arrays, BRANCHES["momentum_y"])
0115 momentum_z = event_array(arrays, BRANCHES["momentum_z"])
0116
0117
0118
0119 source_mask = (
0120 simulator_status.astype(np.uint32) & CREATED_IN_SIMULATION_BIT
0121 ) == 0
0122 source_indices = np.flatnonzero(source_mask)
0123 source_set = set(source_indices.tolist())
0124
0125 parents = source_relations(
0126 event_array(arrays, BRANCHES["parents_begin"]),
0127 event_array(arrays, BRANCHES["parents_end"]),
0128 event_array(arrays, BRANCHES["parent_index"]),
0129 source_indices,
0130 source_set,
0131 )
0132 daughters = source_relations(
0133 event_array(arrays, BRANCHES["daughters_begin"]),
0134 event_array(arrays, BRANCHES["daughters_end"]),
0135 event_array(arrays, BRANCHES["daughter_index"]),
0136 source_indices,
0137 source_set,
0138 )
0139
0140
0141
0142 for child_index in source_indices:
0143 for parent_index in parents[int(child_index)]:
0144 if int(child_index) not in daughters[parent_index]:
0145 raise ValueError(
0146 f"parent/daughter mismatch between particles "
0147 f"{parent_index} and {child_index}"
0148 )
0149 for daughter_index in daughters[int(child_index)]:
0150 if int(child_index) not in parents[daughter_index]:
0151 raise ValueError(
0152 f"parent/daughter mismatch between particles "
0153 f"{child_index} and {daughter_index}"
0154 )
0155
0156 event_number_array = event_array(arrays, BRANCHES["event_number"])
0157 event_number = (
0158 int(event_number_array[0]) if len(event_number_array) else entry_number
0159 )
0160 event = pyhepmc.GenEvent(pyhepmc.Units.GEV, pyhepmc.Units.MM)
0161 event.event_number = event_number
0162
0163
0164
0165 particles = {}
0166 for particle_index in source_indices:
0167 particle_index = int(particle_index)
0168 px = float(momentum_x[particle_index])
0169 py = float(momentum_y[particle_index])
0170 pz = float(momentum_z[particle_index])
0171 particle_mass = float(mass[particle_index])
0172 energy = math.sqrt(
0173 max(0.0, px * px + py * py + pz * pz + particle_mass * particle_mass)
0174 )
0175
0176 particle = pyhepmc.GenParticle(
0177 pyhepmc.FourVector(px, py, pz, energy),
0178 int(pdg[particle_index]),
0179 int(generator_status[particle_index]),
0180 )
0181 particle.generated_mass = particle_mass
0182 event.add_particle(particle)
0183 particles[particle_index] = particle
0184
0185
0186
0187
0188
0189 children_by_parent_set = {}
0190 for child_index in source_indices:
0191 child_index = int(child_index)
0192 parent_set = parents[child_index]
0193 if parent_set:
0194 children_by_parent_set.setdefault(parent_set, []).append(child_index)
0195
0196 assigned_parents = set()
0197 for parent_set, child_indices in children_by_parent_set.items():
0198 if assigned_parents.intersection(parent_set):
0199 raise ValueError(
0200 f"parents {parent_set} would belong to multiple end vertices"
0201 )
0202 assigned_parents.update(parent_set)
0203 first_parent = parent_set[0]
0204 parent_endpoint = np.array(
0205 [
0206 float(endpoint_x[first_parent]),
0207 float(endpoint_y[first_parent]),
0208 float(endpoint_z[first_parent]),
0209 ]
0210 )
0211
0212
0213
0214
0215 vertex_distances = [
0216 np.linalg.norm(
0217 np.array(
0218 [
0219 float(vertex_x[child_index]),
0220 float(vertex_y[child_index]),
0221 float(vertex_z[child_index]),
0222 ]
0223 )
0224 - parent_endpoint
0225 )
0226 for child_index in child_indices
0227 ]
0228 reference_child = child_indices[int(np.argmin(vertex_distances))]
0229 position = (
0230 float(parent_endpoint[0]),
0231 float(parent_endpoint[1]),
0232 float(parent_endpoint[2]),
0233 float(time[reference_child]) * SPEED_OF_LIGHT_MM_PER_NS,
0234 )
0235
0236
0237 for parent_index in parent_set[1:]:
0238 other_parent_endpoint = (
0239 float(endpoint_x[parent_index]),
0240 float(endpoint_y[parent_index]),
0241 float(endpoint_z[parent_index]),
0242 )
0243 if not np.allclose(
0244 other_parent_endpoint, parent_endpoint, rtol=0.0, atol=1.0e-6
0245 ):
0246 raise ValueError(
0247 f"parents {parent_set} have different endpoint positions"
0248 )
0249
0250 vertex = pyhepmc.GenVertex(pyhepmc.FourVector(*position))
0251 event.add_vertex(vertex)
0252 for parent_index in parent_set:
0253 vertex.add_particle_in(particles[parent_index])
0254 for child_index in child_indices:
0255 vertex.add_particle_out(particles[child_index])
0256
0257
0258
0259
0260 for particle_index in source_indices:
0261 particle_index = int(particle_index)
0262 if parents[particle_index]:
0263 continue
0264 position = (
0265 float(vertex_x[particle_index]),
0266 float(vertex_y[particle_index]),
0267 float(vertex_z[particle_index]),
0268 float(time[particle_index]) * SPEED_OF_LIGHT_MM_PER_NS,
0269 )
0270 if not np.allclose(position, 0.0, rtol=0.0, atol=1.0e-9):
0271 raise ValueError(
0272 f"parentless particle {particle_index} has nonzero production "
0273 f"position {position}; explicit source vertices are not "
0274 "supported by this extractor"
0275 )
0276
0277 return event, len(pdg), len(source_indices)
0278
0279
0280 def parse_arguments():
0281 parser = argparse.ArgumentParser(description=__doc__)
0282 parser.add_argument("input_file", type=Path, help="npsim EDM4hep ROOT file")
0283 parser.add_argument("output_file", type=Path, help="output HepMC3 ASCII file")
0284 parser.add_argument(
0285 "--max-events",
0286 type=int,
0287 default=None,
0288 help="extract only the first N events (useful for a smoke test)",
0289 )
0290 return parser.parse_args()
0291
0292
0293 def main():
0294 args = parse_arguments()
0295 input_file = args.input_file.resolve()
0296 output_file = args.output_file.resolve()
0297
0298 if not input_file.is_file():
0299 raise FileNotFoundError(f"input file does not exist: {input_file}")
0300 if output_file.exists():
0301 raise FileExistsError(f"refusing to overwrite existing file: {output_file}")
0302 if not output_file.parent.is_dir():
0303 raise FileNotFoundError(
0304 f"output directory does not exist: {output_file.parent}"
0305 )
0306 if args.max_events is not None and args.max_events <= 0:
0307 raise ValueError("--max-events must be a positive integer")
0308
0309 partial_file = output_file.with_name(output_file.name + ".partial")
0310 if partial_file.exists():
0311 raise FileExistsError(f"refusing to overwrite partial file: {partial_file}")
0312
0313
0314
0315 with uproot.open(input_file, array_cache=None) as root_file:
0316 events = root_file["events"]
0317 number_of_events = events.num_entries
0318 if args.max_events is not None:
0319 number_of_events = min(number_of_events, args.max_events)
0320
0321 total_particles = 0
0322 total_source_particles = 0
0323
0324
0325 with partial_file.open("x"):
0326 pass
0327 try:
0328 with pyhepmc.open(partial_file, "w", format="hepmc3") as writer:
0329 for entry_number in range(number_of_events):
0330 arrays = events.arrays(
0331 list(BRANCHES.values()),
0332 entry_start=entry_number,
0333 entry_stop=entry_number + 1,
0334 library="ak",
0335 )
0336 event, particle_count, source_count = build_hepmc_event(
0337 arrays, entry_number
0338 )
0339 writer.write(event)
0340 total_particles += particle_count
0341 total_source_particles += source_count
0342
0343 print(
0344 f"\rExtracted event {entry_number + 1}/{number_of_events}",
0345 end="",
0346 flush=True,
0347 )
0348 if output_file.exists():
0349 raise FileExistsError(f"refusing to overwrite: {output_file}")
0350 partial_file.rename(output_file)
0351 except Exception:
0352 print(f"\nIncomplete output retained at {partial_file}", file=sys.stderr)
0353 raise
0354
0355 removed_particles = total_particles - total_source_particles
0356 print(
0357 f"\nWrote {number_of_events} events and {total_source_particles:,} "
0358 f"source particles to {output_file}"
0359 )
0360 print(
0361 f"Excluded {removed_particles:,} particles with "
0362 "BITCreatedInSimulation set"
0363 )
0364
0365
0366 if __name__ == "__main__":
0367 main()