File indexing completed on 2026-01-09 09:26:48
0001 import os
0002 import multiprocessing
0003 from pathlib import Path
0004 import math
0005 import tempfile
0006 import shutil
0007 import pytest
0008
0009 from helpers import (
0010 edm4hepEnabled,
0011 podioEnabled,
0012 AssertCollectionExistsAlg,
0013 )
0014
0015 import acts
0016 from acts import UnitConstants as u
0017
0018 from acts.examples import (
0019 Sequencer,
0020 GenericDetector,
0021 )
0022
0023 from acts.examples.odd import getOpenDataDetector, getOpenDataDetectorDirectory
0024
0025
0026 def assert_podio(
0027 target_file: Path,
0028 category: str,
0029 collections: set[str] | None = None,
0030 nevents: int | None = None,
0031 ):
0032 assert target_file.exists(), f"File {target_file} does not exist"
0033 from podio.root_io import Reader
0034 import cppyy
0035
0036 reader = Reader(str(target_file))
0037 assert (
0038 category in reader.categories
0039 ), f"Category {category} not found in {target_file} ({reader.categories})"
0040
0041 if nevents is not None:
0042 assert (
0043 len(reader.get(category)) == nevents
0044 ), f"Expected {nevents} events in {target_file} ({category}) but got {len(reader.get(category))}"
0045
0046 if collections is not None:
0047 for frame in reader.get(category):
0048 assert (
0049 set(frame.getAvailableCollections()) == collections
0050 ), f"Expected collections {collections} in {target_file} ({category}) but got {frame.getAvailableCollections()}"
0051 break
0052
0053
0054 @pytest.mark.edm4hep
0055 @pytest.mark.skipif(not edm4hepEnabled, reason="EDM4hep is not set up")
0056 def test_edm4hep_measurement_writer(tmp_path, fatras):
0057 from acts.examples.edm4hep import EDM4hepMeasurementOutputConverter, PodioWriter
0058
0059 s = Sequencer(numThreads=1, events=10)
0060 _, simAlg, digiAlg = fatras(s)
0061
0062 out = tmp_path / "measurements_edm4hep.root"
0063
0064 converter = EDM4hepMeasurementOutputConverter(
0065 level=acts.logging.VERBOSE,
0066 inputMeasurements=digiAlg.config.outputMeasurements,
0067 inputClusters=digiAlg.config.outputClusters,
0068 outputTrackerHitsPlane="tracker_hits_plane",
0069 outputTrackerHitsRaw="tracker_hits_raw",
0070 )
0071 s.addAlgorithm(converter)
0072
0073 s.addWriter(
0074 PodioWriter(
0075 level=acts.logging.VERBOSE,
0076 outputPath=str(out),
0077 category="events",
0078 collections=converter.collections,
0079 )
0080 )
0081
0082 s.run()
0083
0084 assert os.path.isfile(out)
0085 assert os.stat(out).st_size > 10
0086
0087 assert_podio(
0088 out,
0089 "events",
0090 collections=set(["tracker_hits_plane", "tracker_hits_raw"]),
0091 nevents=10,
0092 )
0093
0094
0095 @pytest.mark.edm4hep
0096 @pytest.mark.skipif(not edm4hepEnabled, reason="EDM4hep is not set up")
0097 def test_edm4hep_simhit_writer(tmp_path, fatras, conf_const):
0098 from acts.examples.edm4hep import EDM4hepSimHitOutputConverter, PodioWriter
0099
0100 s = Sequencer(numThreads=1, events=10)
0101 _, simAlg, _ = fatras(s)
0102
0103 out = tmp_path / "simhits_edm4hep.root"
0104
0105 converter = EDM4hepSimHitOutputConverter(
0106 level=acts.logging.INFO,
0107 inputSimHits=simAlg.config.outputSimHits,
0108 outputSimTrackerHits="sim_tracker_hits",
0109 )
0110 s.addAlgorithm(converter)
0111
0112 s.addWriter(
0113 PodioWriter(
0114 level=acts.logging.INFO,
0115 outputPath=str(out),
0116 category="events",
0117 collections=converter.collections,
0118 )
0119 )
0120 s.run()
0121
0122 assert os.path.isfile(out)
0123 assert os.stat(out).st_size > 200
0124
0125
0126 @pytest.mark.edm4hep
0127 @pytest.mark.skipif(not edm4hepEnabled, reason="EDM4hep is not set up")
0128 def test_edm4hep_particle_writer(tmp_path, ptcl_gun):
0129 from acts.examples.edm4hep import EDM4hepParticleOutputConverter, PodioWriter
0130
0131 s = Sequencer(numThreads=1, events=10)
0132 _, h3conv = ptcl_gun(s)
0133
0134 out = tmp_path / "particles_edm4hep.root"
0135
0136 out.mkdir()
0137
0138 converter = EDM4hepParticleOutputConverter(
0139 acts.logging.INFO,
0140 inputParticles=h3conv.config.outputParticles,
0141 outputParticles="MCParticles",
0142 )
0143 s.addAlgorithm(converter)
0144
0145 s.addWriter(
0146 PodioWriter(
0147 level=acts.logging.INFO,
0148 outputPath=str(out),
0149 category="events",
0150 collections=converter.collections,
0151 )
0152 )
0153
0154 s.run()
0155
0156 assert os.path.isfile(out)
0157 assert os.stat(out).st_size > 200
0158
0159 assert_podio(out, "events", collections=set(["MCParticles"]), nevents=10)
0160
0161
0162 @pytest.mark.edm4hep
0163 @pytest.mark.skipif(not edm4hepEnabled, reason="EDM4hep is not set up")
0164 def test_edm4hep_particle_writer_separate_files(tmp_path, ptcl_gun):
0165 from acts.examples.edm4hep import EDM4hepParticleOutputConverter, PodioWriter
0166
0167 threads = 2
0168 events = 6
0169
0170 s = Sequencer(numThreads=threads, events=events)
0171 _, h3conv = ptcl_gun(s)
0172
0173 out = tmp_path / "particles_edm4hep.root"
0174
0175 converter = EDM4hepParticleOutputConverter(
0176 acts.logging.INFO,
0177 inputParticles=h3conv.config.outputParticles,
0178 outputParticles="MCParticles",
0179 )
0180 s.addAlgorithm(converter)
0181
0182 s.addWriter(
0183 PodioWriter(
0184 level=acts.logging.INFO,
0185 outputPath=str(out),
0186 category="events",
0187 collections=converter.collections,
0188 separateFilesPerThread=True,
0189 )
0190 )
0191
0192 s.run()
0193
0194 produced_files = sorted(tmp_path.glob("particles_edm4hep_thread*.root"))
0195 assert 1 < len(produced_files) <= threads
0196
0197 total_events = 0
0198
0199 from podio.root_io import Reader
0200
0201 for produced in produced_files:
0202 assert produced.exists()
0203 assert produced.stat().st_size > 200
0204 assert_podio(
0205 produced,
0206 "events",
0207 collections=set(["MCParticles"]),
0208 nevents=None,
0209 )
0210 reader = Reader(str(produced))
0211 total_events += len(reader.get("events"))
0212
0213 assert total_events == events
0214 assert not out.exists()
0215
0216
0217 @pytest.mark.edm4hep
0218 @pytest.mark.skipif(not edm4hepEnabled, reason="EDM4hep is not set up")
0219 def test_edm4hep_multitrajectory_writer(tmp_path):
0220 from acts.examples.edm4hep import EDM4hepMultiTrajectoryOutputConverter, PodioWriter
0221
0222 detector = GenericDetector()
0223 trackingGeometry = detector.trackingGeometry()
0224 field = acts.ConstantBField(acts.Vector3(0, 0, 2 * u.T))
0225
0226 from truth_tracking_kalman import runTruthTrackingKalman
0227
0228 s = Sequencer(numThreads=1, events=10)
0229 runTruthTrackingKalman(
0230 trackingGeometry,
0231 field,
0232 digiConfigFile=Path(
0233 str(
0234 Path(__file__).parent.parent.parent.parent
0235 / "Examples/Configs/generic-digi-smearing-config.json"
0236 )
0237 ),
0238 outputDir=tmp_path,
0239 s=s,
0240 )
0241
0242 s.addAlgorithm(
0243 acts.examples.TracksToTrajectories(
0244 level=acts.logging.INFO,
0245 inputTracks="tracks",
0246 outputTrajectories="trajectories",
0247 )
0248 )
0249
0250 out = tmp_path / "trajectories_edm4hep.root"
0251
0252 converter = EDM4hepMultiTrajectoryOutputConverter(
0253 level=acts.logging.VERBOSE,
0254 inputTrajectories="trajectories",
0255 inputMeasurementParticlesMap="measurement_particles_map",
0256 outputTracks="ActsTrajectories",
0257 )
0258 s.addAlgorithm(converter)
0259
0260 s.addWriter(
0261 PodioWriter(
0262 level=acts.logging.VERBOSE,
0263 outputPath=str(out),
0264 category="events",
0265 collections=converter.collections,
0266 )
0267 )
0268 s.run()
0269
0270 assert os.path.isfile(out)
0271 assert os.stat(out).st_size > 200
0272
0273
0274 @pytest.mark.edm4hep
0275 @pytest.mark.skipif(not edm4hepEnabled, reason="EDM4hep is not set up")
0276 def test_edm4hep_tracks_writer(tmp_path):
0277 from acts.examples.edm4hep import EDM4hepTrackOutputConverter, PodioWriter
0278
0279 detector = GenericDetector()
0280 trackingGeometry = detector.trackingGeometry()
0281 field = acts.ConstantBField(acts.Vector3(0, 0, 2 * u.T))
0282
0283 from truth_tracking_kalman import runTruthTrackingKalman
0284
0285 s = Sequencer(numThreads=1, events=10)
0286 runTruthTrackingKalman(
0287 trackingGeometry,
0288 field,
0289 digiConfigFile=Path(
0290 str(
0291 Path(__file__).parent.parent.parent.parent
0292 / "Examples/Configs/generic-digi-smearing-config.json"
0293 )
0294 ),
0295 outputDir=tmp_path,
0296 s=s,
0297 )
0298
0299 out = tmp_path / "tracks_edm4hep.root"
0300
0301 converter = EDM4hepTrackOutputConverter(
0302 level=acts.logging.VERBOSE,
0303 inputTracks="kf_tracks",
0304 outputTracks="ActsTracks",
0305 Bz=2 * u.T,
0306 )
0307 s.addAlgorithm(converter)
0308
0309 s.addWriter(
0310 PodioWriter(
0311 level=acts.logging.VERBOSE,
0312 outputPath=str(out),
0313 category="events",
0314 collections=converter.collections,
0315 )
0316 )
0317 s.run()
0318
0319 assert os.path.isfile(out), f"File {out} does not exist"
0320 assert os.stat(out).st_size > 200, f"File {out} is too small"
0321
0322 if not podioEnabled:
0323 import warnings
0324
0325 warnings.warn(
0326 "edm4hep output checks were skipped, because podio was not on the python path"
0327 )
0328 return
0329
0330 from podio.root_io import Reader
0331 import cppyy
0332
0333 reader = Reader(str(out))
0334
0335 actual = []
0336
0337 for frame in reader.get("events"):
0338 tracks = frame.get("ActsTracks")
0339 for track in tracks:
0340 actual.append(
0341 (track.getChi2(), track.getNdf(), len(track.getTrackStates()))
0342 )
0343
0344 locs = []
0345
0346 perigee = None
0347 for ts in track.getTrackStates():
0348 if ts.location == cppyy.gbl.edm4hep.TrackState.AtIP:
0349 perigee = ts
0350 continue
0351 locs.append(ts.location)
0352
0353 rp = ts.referencePoint
0354 r = math.sqrt(rp.x**2 + rp.y**2)
0355 assert r > 25
0356
0357 assert locs[0] == cppyy.gbl.edm4hep.TrackState.AtLastHit
0358 assert locs[-1] == cppyy.gbl.edm4hep.TrackState.AtFirstHit
0359
0360 assert perigee is not None
0361 rp = perigee.referencePoint
0362 assert rp.x == 0.0
0363 assert rp.y == 0.0
0364 assert rp.z == 0.0
0365 assert abs(perigee.D0) < 1e0
0366 assert abs(perigee.Z0) < 1e1
0367
0368
0369 def generate_input_test_edm4hep_simhit_reader(input, output, particle_type):
0370 from DDSim.DD4hepSimulation import DD4hepSimulation
0371
0372 ddsim = DD4hepSimulation()
0373
0374 ddsim.random.seed = 37
0375
0376 if isinstance(ddsim.compactFile, list):
0377 ddsim.compactFile = [input]
0378 else:
0379 ddsim.compactFile = input
0380 ddsim.enableGun = True
0381 ddsim.gun.direction = (1, 0, 0)
0382 ddsim.gun.particle = particle_type
0383 ddsim.gun.distribution = "eta"
0384 ddsim.gun.etaMin = -3.0
0385 ddsim.gun.etaMax = 3.0
0386 ddsim.numberOfEvents = 10
0387 ddsim.outputFile = output
0388 ddsim.outputConfig.forceEDM4HEP = True
0389 ddsim.run()
0390
0391
0392
0393 @pytest.fixture(scope="session", params=["mu-", "pi-"])
0394 def ddsim_input_session(request):
0395 particle_type = request.param
0396 with tempfile.TemporaryDirectory() as tmp_dir:
0397 odd_xml_file = str(
0398 getOpenDataDetectorDirectory() / "xml" / "OpenDataDetector.xml"
0399 )
0400
0401 output_file = str(Path(tmp_dir) / "output_edm4hep.root")
0402
0403 if not os.path.exists(output_file):
0404
0405 spawn_context = multiprocessing.get_context("spawn")
0406 p = spawn_context.Process(
0407 target=generate_input_test_edm4hep_simhit_reader,
0408 args=(odd_xml_file, output_file, particle_type),
0409 )
0410 p.start()
0411 p.join()
0412 if p.exitcode != 0:
0413 raise RuntimeError("ddsim process failed")
0414
0415 assert os.path.exists(output_file)
0416
0417 yield output_file
0418
0419
0420
0421 @pytest.fixture(scope="function")
0422 def ddsim_input(ddsim_input_session, tmp_path):
0423 tmp_file = str(tmp_path / "output_edm4hep.root")
0424 shutil.copy(ddsim_input_session, tmp_file)
0425 return tmp_file
0426
0427
0428 @pytest.mark.slow
0429 @pytest.mark.edm4hep
0430 @pytest.mark.skipif(not edm4hepEnabled, reason="EDM4hep is not set up")
0431 def test_edm4hep_simhit_particle_reader(tmp_path, ddsim_input):
0432 from acts.examples.edm4hep import EDM4hepSimInputConverter
0433 from acts.examples.edm4hep import PodioReader
0434
0435 s = Sequencer(numThreads=1)
0436
0437 with getOpenDataDetector() as detector:
0438 trackingGeometry = detector.trackingGeometry()
0439
0440 s.addReader(
0441 PodioReader(
0442 level=acts.logging.VERBOSE,
0443 inputPath=ddsim_input,
0444 outputFrame="events",
0445 category="events",
0446 )
0447 )
0448
0449 s.addAlgorithm(
0450 EDM4hepSimInputConverter(
0451 level=acts.logging.DEBUG,
0452 inputFrame="events",
0453 inputSimHits=[
0454 "PixelBarrelReadout",
0455 "PixelEndcapReadout",
0456 "ShortStripBarrelReadout",
0457 "ShortStripEndcapReadout",
0458 "LongStripBarrelReadout",
0459 "LongStripEndcapReadout",
0460 ],
0461 outputParticlesGenerator="particles_generated",
0462 outputParticlesSimulation="particles_simulated",
0463 outputSimHits="simhits",
0464 outputSimVertices="simvertices",
0465 dd4hepDetector=detector,
0466 trackingGeometry=trackingGeometry,
0467 )
0468 )
0469
0470 alg = AssertCollectionExistsAlg(
0471 ["simhits", "simvertices", "particles_generated", "particles_simulated"],
0472 "check_alg",
0473 acts.logging.WARNING,
0474 )
0475 s.addAlgorithm(alg)
0476
0477 alg = AssertCollectionExistsAlg(
0478 "particles_generated", "check_alg", acts.logging.WARNING
0479 )
0480 s.addAlgorithm(alg)
0481
0482 s.run()
0483
0484 assert alg.events_seen == 10
0485
0486
0487 @pytest.mark.edm4hep
0488 @pytest.mark.skipif(not edm4hepEnabled, reason="EDM4hep is not set up")
0489 def test_edm4hep_measurement_reader(tmp_path, fatras):
0490 from acts.examples.edm4hep import (
0491 EDM4hepMeasurementOutputConverter,
0492 EDM4hepMeasurementInputConverter,
0493 )
0494 from acts.examples.edm4hep import PodioWriter, PodioReader
0495
0496 s = Sequencer(numThreads=1, events=10)
0497 _, simAlg, digiAlg = fatras(s)
0498
0499 out = tmp_path / "measurements_edm4hep.root"
0500
0501 converter = EDM4hepMeasurementOutputConverter(
0502 level=acts.logging.INFO,
0503 inputMeasurements=digiAlg.config.outputMeasurements,
0504 inputClusters=digiAlg.config.outputClusters,
0505 )
0506 s.addAlgorithm(converter)
0507 s.addWriter(
0508 PodioWriter(
0509 level=acts.logging.INFO,
0510 outputPath=str(out),
0511 category="events",
0512 collections=converter.collections,
0513 )
0514 )
0515 s.run()
0516
0517
0518 s = Sequencer(numThreads=1)
0519
0520 s.addReader(
0521 PodioReader(
0522 level=acts.logging.WARNING,
0523 inputPath=str(out),
0524 outputFrame="events",
0525 category="events",
0526 )
0527 )
0528 s.addAlgorithm(
0529 EDM4hepMeasurementInputConverter(
0530 level=acts.logging.WARNING,
0531 inputFrame="events",
0532 outputMeasurements="measurements",
0533 outputMeasurementSimHitsMap="simhitsmap",
0534 )
0535 )
0536
0537 alg = AssertCollectionExistsAlg(
0538 ["measurements", "simhitsmap"], "check_alg", acts.logging.WARNING
0539 )
0540 s.addAlgorithm(alg)
0541
0542 s.run()
0543
0544 assert alg.events_seen == 10
0545
0546
0547 @pytest.mark.edm4hep
0548 @pytest.mark.skipif(not edm4hepEnabled, reason="EDM4hep is not set up")
0549 def test_edm4hep_tracks_reader(tmp_path):
0550 from acts.examples.edm4hep import (
0551 EDM4hepTrackOutputConverter,
0552 EDM4hepTrackInputConverter,
0553 )
0554 from acts.examples.edm4hep import PodioWriter, PodioReader
0555
0556 detector = acts.examples.GenericDetector()
0557 trackingGeometry = detector.trackingGeometry()
0558
0559 field = acts.ConstantBField(acts.Vector3(0, 0, 2 * u.T))
0560
0561 from truth_tracking_kalman import runTruthTrackingKalman
0562
0563 s = Sequencer(numThreads=1, events=10)
0564 runTruthTrackingKalman(
0565 trackingGeometry,
0566 field,
0567 digiConfigFile=Path(
0568 str(
0569 Path(__file__).parent.parent.parent.parent
0570 / "Examples/Configs/generic-digi-smearing-config.json"
0571 )
0572 ),
0573 outputDir=tmp_path,
0574 s=s,
0575 )
0576
0577 out = tmp_path / "tracks_edm4hep.root"
0578
0579 converter = EDM4hepTrackOutputConverter(
0580 level=acts.logging.VERBOSE,
0581 inputTracks="kf_tracks",
0582 Bz=2 * u.T,
0583 )
0584 s.addAlgorithm(converter)
0585
0586 s.addWriter(
0587 PodioWriter(
0588 level=acts.logging.VERBOSE,
0589 outputPath=str(out),
0590 category="events",
0591 collections=converter.collections,
0592 )
0593 )
0594 s.run()
0595
0596 s = Sequencer(numThreads=1)
0597
0598 s.addReader(
0599 PodioReader(
0600 level=acts.logging.VERBOSE,
0601 inputPath=str(out),
0602 outputFrame="events",
0603 category="events",
0604 )
0605 )
0606
0607 s.addAlgorithm(
0608 EDM4hepTrackInputConverter(
0609 level=acts.logging.VERBOSE,
0610 inputFrame="events",
0611 inputTracks="kf_tracks",
0612 outputTracks="kf_tracks",
0613 Bz=2 * u.T,
0614 )
0615 )
0616
0617 s.run()
0618
0619
0620 @pytest.mark.edm4hep
0621 @pytest.mark.skipif(not edm4hepEnabled, reason="EDM4hep is not set up")
0622 def test_edm4hep_reader(ddsim_input):
0623 from acts.examples.edm4hep import PodioReader
0624
0625 s = Sequencer(numThreads=1)
0626 s.addReader(
0627 PodioReader(
0628 level=acts.logging.VERBOSE,
0629 inputPath=ddsim_input,
0630 outputFrame="frame",
0631 )
0632 )
0633
0634 alg = AssertCollectionExistsAlg("frame", "check_alg", acts.logging.WARNING)
0635 s.addAlgorithm(alg)
0636
0637 s.run()
0638
0639
0640 @pytest.mark.edm4hep
0641 @pytest.mark.skipif(not edm4hepEnabled, reason="EDM4hep is not set up")
0642 def test_edm4hep_writer_copy(ddsim_input, tmp_path):
0643 from acts.examples.edm4hep import PodioWriter, PodioReader
0644
0645 target_file = tmp_path / "rewritten_edm4hep.root"
0646
0647 s = Sequencer(numThreads=1)
0648 s.addReader(
0649 PodioReader(
0650 level=acts.logging.VERBOSE,
0651 inputPath=ddsim_input,
0652 outputFrame="myframe",
0653 category="events",
0654 )
0655 )
0656 s.addWriter(
0657 PodioWriter(
0658 level=acts.logging.VERBOSE,
0659 inputFrame="myframe",
0660 outputPath=str(target_file),
0661 category="myevents",
0662 )
0663 )
0664
0665 s.run()
0666
0667 assert_podio(
0668 target_file,
0669 "myevents",
0670 nevents=10,
0671 collections=set(
0672 [
0673 "ShortStripEndcapReadout",
0674 "MCParticles",
0675 "LongStripBarrelReadout",
0676 "PixelEndcapReadout",
0677 "HCalEndcapCollectionContributions",
0678 "HCalEndcapCollection",
0679 "HCalBarrelCollectionContributions",
0680 "ECalBarrelCollectionContributions",
0681 "LongStripEndcapReadout",
0682 "EventHeader",
0683 "ECalEndcapCollectionContributions",
0684 "ShortStripBarrelReadout",
0685 "PixelBarrelReadout",
0686 "ECalEndcapCollection",
0687 "HCalBarrelCollection",
0688 "ECalBarrelCollection",
0689 ]
0690 ),
0691 )