Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-11-04 09:07:11

0001 #!/usr/bin/env python3
0002 
0003 import os
0004 import argparse
0005 import pathlib
0006 
0007 import acts
0008 import acts.examples
0009 from acts.examples.simulation import (
0010     MomentumConfig,
0011     EtaConfig,
0012     PhiConfig,
0013     ParticleConfig,
0014     ParticleSelectorConfig,
0015     addParticleGun,
0016     addPythia8,
0017     addGenParticleSelection,
0018     addFatras,
0019     addGeant4,
0020     addSimParticleSelection,
0021     addDigitization,
0022     addDigiParticleSelection,
0023 )
0024 from acts.examples.reconstruction import (
0025     addSeeding,
0026     SeedingAlgorithm,
0027     GbtsConfigArg,
0028     CkfConfig,
0029     addCKFTracks,
0030     TrackSelectorConfig,
0031     addAmbiguityResolution,
0032     AmbiguityResolutionConfig,
0033     addAmbiguityResolutionML,
0034     AmbiguityResolutionMLConfig,
0035     addScoreBasedAmbiguityResolution,
0036     ScoreBasedAmbiguityResolutionConfig,
0037     addVertexFitting,
0038     VertexFinder,
0039     addSeedFilterML,
0040     SeedFilterMLDBScanConfig,
0041 )
0042 from acts.examples.odd import getOpenDataDetector, getOpenDataDetectorDirectory
0043 
0044 u = acts.UnitConstants
0045 
0046 
0047 parser = argparse.ArgumentParser(description="Full chain with the OpenDataDetector")
0048 parser.add_argument(
0049     "--output",
0050     "-o",
0051     help="Output directory",
0052     type=pathlib.Path,
0053     default=pathlib.Path.cwd() / "odd_output",
0054 )
0055 parser.add_argument("--events", "-n", help="Number of events", type=int, default=100)
0056 parser.add_argument("--skip", "-s", help="Number of events", type=int, default=0)
0057 parser.add_argument(
0058     "--jobs",
0059     "-j",
0060     help="Number of worker threads (-1 uses all cores)",
0061     type=int,
0062     default=None,
0063 )
0064 parser.add_argument("--edm4hep", help="Use edm4hep inputs", type=pathlib.Path)
0065 parser.add_argument(
0066     "--geant4", help="Use Geant4 instead of fatras", action="store_true"
0067 )
0068 parser.add_argument(
0069     "--ttbar",
0070     help="Use Pythia8 (ttbar, pile-up 200) instead of particle gun",
0071     action="store_true",
0072 )
0073 parser.add_argument(
0074     "--ttbar-pu",
0075     help="Number of pile-up events for ttbar",
0076     type=int,
0077     default=200,
0078 )
0079 parser.add_argument(
0080     "--gun-particles",
0081     help="Multiplicity (no. of particles) of the particle gun",
0082     type=int,
0083     default=4,
0084 )
0085 parser.add_argument(
0086     "--gun-particle-type",
0087     help="Particle type of the particle gun; options: muon(default), electron, pion, kaon, proton",
0088     type=str,
0089     default="muon",
0090 )
0091 parser.add_argument(
0092     "--gun-multiplicity",
0093     help="Multiplicity (no. of vertices) of the particle gun",
0094     type=int,
0095     default=200,
0096 )
0097 parser.add_argument(
0098     "--gun-eta-range",
0099     nargs=2,
0100     help="Eta range of the particle gun",
0101     type=float,
0102     default=[-3.0, 3.0],
0103 )
0104 parser.add_argument(
0105     "--gun-pt-range",
0106     nargs=2,
0107     help="Pt range of the particle gun (GeV)",
0108     type=float,
0109     default=[1.0 * u.GeV, 10.0 * u.GeV],
0110 )
0111 parser.add_argument(
0112     "--digi-config", help="Digitization configuration file", type=pathlib.Path
0113 )
0114 parser.add_argument(
0115     "--material-config", help="Material map configuration file", type=pathlib.Path
0116 )
0117 parser.add_argument(
0118     "--ambi-solver",
0119     help="Set which ambiguity solver to use, default is the classical one",
0120     type=str,
0121     choices=["greedy", "scoring", "ML"],
0122     default="greedy",
0123 )
0124 parser.add_argument(
0125     "--ambi-config",
0126     help="Set the configuration file for the Score Based ambiguity resolution",
0127     type=pathlib.Path,
0128     default=pathlib.Path.cwd() / "ambi_config.json",
0129 )
0130 
0131 parser.add_argument(
0132     "--MLSeedFilter",
0133     help="Use the Ml seed filter to select seed after the seeding step",
0134     action="store_true",
0135 )
0136 parser.add_argument(
0137     "--reco",
0138     help="Switch reco on/off",
0139     default=True,
0140     action=argparse.BooleanOptionalAction,
0141 )
0142 parser.add_argument(
0143     "--use-gbts",
0144     help="Use the Graph Based Track Seeding (GBTS) instead of triplet seeding",
0145     action="store_true",
0146 )
0147 parser.add_argument(
0148     "--gbts-layer-map",
0149     help="GBTS layer map configuration file",
0150     type=pathlib.Path,
0151 )
0152 parser.add_argument(
0153     "--gbts-connection-table",
0154     help="GBTS layer connection table configuration file",
0155     type=pathlib.Path,
0156 )
0157 parser.add_argument(
0158     "--output-root",
0159     help="Switch root output on/off",
0160     default=False,
0161     action=argparse.BooleanOptionalAction,
0162 )
0163 parser.add_argument(
0164     "--output-csv",
0165     help="Switch csv output on/off",
0166     default=False,
0167     action=argparse.BooleanOptionalAction,
0168 )
0169 parser.add_argument(
0170     "--output-obj",
0171     help="Switch obj output on/off",
0172     default=False,
0173     action=argparse.BooleanOptionalAction,
0174 )
0175 parser.add_argument(
0176     "--output-parquet",
0177     help="Switch parquet output on/off (requires ACTS_BUILD_EXAMPLES_PARQUET=ON)",
0178     default=False,
0179     action=argparse.BooleanOptionalAction,
0180 )
0181 
0182 args = parser.parse_args()
0183 
0184 outputDir = args.output
0185 ambi_ML = args.ambi_solver == "ML"
0186 ambi_scoring = args.ambi_solver == "scoring"
0187 ambi_config = args.ambi_config
0188 seedFilter_ML = args.MLSeedFilter
0189 geoDir = getOpenDataDetectorDirectory()
0190 actsDir = pathlib.Path(__file__).parent.parent.parent.parent
0191 # acts.examples.dump_args_calls()  # show python binding calls
0192 
0193 oddMaterialMap = (
0194     args.material_config
0195     if args.material_config
0196     else geoDir / "data/odd-material-maps.root"
0197 )
0198 
0199 oddDigiConfig = (
0200     args.digi_config
0201     if args.digi_config
0202     else actsDir / "Examples/Configs/odd-digi-smearing-config.json"
0203 )
0204 
0205 oddGbtsLayerMap = (
0206     args.gbts_layer_map
0207     if args.gbts_layer_map
0208     else actsDir / "Examples/Configs/odd-gbts-layer-map.json"
0209 )
0210 oddGbtsConnectionTable = (
0211     args.gbts_connection_table
0212     if args.gbts_connection_table
0213     else actsDir / "Examples/Configs/odd-gbts-connection-table.json"
0214 )
0215 
0216 oddSeedingSel = actsDir / "Examples/Configs/odd-seeding-config.json"
0217 oddMaterialDeco = acts.IMaterialDecorator.fromFile(oddMaterialMap)
0218 
0219 detector = getOpenDataDetector(odd_dir=geoDir, materialDecorator=oddMaterialDeco)
0220 trackingGeometry = detector.trackingGeometry()
0221 decorators = detector.contextDecorators()
0222 field = acts.ConstantBField(acts.Vector3(0.0, 0.0, 2.0 * u.T))
0223 rnd = acts.examples.RandomNumbers(seed=42)
0224 
0225 s = acts.examples.Sequencer(
0226     events=args.events,
0227     skip=args.skip,
0228     numThreads=args.jobs if args.jobs is not None else (1 if args.geant4 else -1),
0229     outputDir=str(outputDir),
0230 )
0231 
0232 if args.edm4hep:
0233     import acts.examples.edm4hep
0234     from acts.examples.edm4hep import PodioReader
0235 
0236     s.addReader(
0237         PodioReader(
0238             level=acts.logging.DEBUG,
0239             inputPath=str(args.edm4hep),
0240             outputFrame="events",
0241             category="events",
0242         )
0243     )
0244 
0245     edm4hepReader = acts.examples.edm4hep.EDM4hepSimInputConverter(
0246         inputFrame="events",
0247         inputSimHits=[
0248             "PixelBarrelReadout",
0249             "PixelEndcapReadout",
0250             "ShortStripBarrelReadout",
0251             "ShortStripEndcapReadout",
0252             "LongStripBarrelReadout",
0253             "LongStripEndcapReadout",
0254         ],
0255         outputParticlesGenerator="particles_generated",
0256         outputParticlesSimulation="particles_simulated",
0257         outputSimHits="simhits",
0258         outputSimVertices="vertices_truth",
0259         dd4hepDetector=detector,
0260         trackingGeometry=trackingGeometry,
0261         sortSimHitsInTime=False,
0262         particleRMax=1080 * u.mm,
0263         particleZ=(-3030 * u.mm, 3030 * u.mm),
0264         particlePtMin=150 * u.MeV,
0265         level=acts.logging.DEBUG,
0266     )
0267     s.addAlgorithm(edm4hepReader)
0268 
0269     s.addWhiteboardAlias("particles", edm4hepReader.config.outputParticlesSimulation)
0270 
0271     addSimParticleSelection(
0272         s,
0273         ParticleSelectorConfig(
0274             rho=(0.0, 24 * u.mm),
0275             absZ=(0.0, 1.0 * u.m),
0276             eta=(-3.0, 3.0),
0277             removeNeutral=True,
0278         ),
0279     )
0280 else:
0281     if not args.ttbar:
0282         particleType = {
0283             "muon": acts.PdgParticle.eMuon,
0284             "electron": acts.PdgParticle.eElectron,
0285             "pion": acts.PdgParticle.ePionPlus,
0286             "kaon": acts.PdgParticle.eKaonPlus,
0287             "proton": acts.PdgParticle.eProton,
0288         }[args.gun_particle_type]
0289         addParticleGun(
0290             s,
0291             MomentumConfig(
0292                 args.gun_pt_range[0] * u.GeV,
0293                 args.gun_pt_range[1] * u.GeV,
0294                 transverse=True,
0295             ),
0296             EtaConfig(args.gun_eta_range[0], args.gun_eta_range[1]),
0297             PhiConfig(0.0, 360.0 * u.degree),
0298             ParticleConfig(args.gun_particles, particleType, randomizeCharge=True),
0299             vtxGen=acts.examples.GaussianVertexGenerator(
0300                 mean=acts.Vector4(0, 0, 0, 0),
0301                 stddev=acts.Vector4(
0302                     0.0125 * u.mm, 0.0125 * u.mm, 55.5 * u.mm, 1.0 * u.ns
0303                 ),
0304             ),
0305             multiplicity=args.gun_multiplicity,
0306             rnd=rnd,
0307         )
0308     else:
0309         addPythia8(
0310             s,
0311             hardProcess=["Top:qqbar2ttbar=on"],
0312             npileup=args.ttbar_pu,
0313             vtxGen=acts.examples.GaussianVertexGenerator(
0314                 mean=acts.Vector4(0, 0, 0, 0),
0315                 stddev=acts.Vector4(
0316                     0.0125 * u.mm, 0.0125 * u.mm, 55.5 * u.mm, 5.0 * u.ns
0317                 ),
0318             ),
0319             rnd=rnd,
0320             outputDirRoot=outputDir if args.output_root else None,
0321             outputDirCsv=outputDir if args.output_csv else None,
0322         )
0323 
0324         addGenParticleSelection(
0325             s,
0326             ParticleSelectorConfig(
0327                 rho=(0.0, 24 * u.mm),
0328                 absZ=(0.0, 1.0 * u.m),
0329                 eta=(-3.0, 3.0),
0330                 pt=(150 * u.MeV, None),
0331             ),
0332         )
0333 
0334     if args.geant4:
0335         if s.config.numThreads != 1:
0336             raise ValueError("Geant 4 simulation does not support multi-threading")
0337 
0338         # Pythia can sometime simulate particles outside the world volume, a cut on the Z of the track help mitigate this effect
0339         # Older version of G4 might not work, this as has been tested on version `geant4-11-00-patch-03`
0340         # For more detail see issue #1578
0341         addGeant4(
0342             s,
0343             detector,
0344             trackingGeometry,
0345             field,
0346             outputDirRoot=outputDir if args.output_root else None,
0347             outputDirCsv=outputDir if args.output_csv else None,
0348             outputDirObj=outputDir if args.output_obj else None,
0349             rnd=rnd,
0350             killVolume=trackingGeometry.highestTrackingVolume,
0351             killAfterTime=25 * u.ns,
0352         )
0353     else:
0354         addFatras(
0355             s,
0356             trackingGeometry,
0357             field,
0358             enableInteractions=True,
0359             outputDirRoot=outputDir if args.output_root else None,
0360             outputDirCsv=outputDir if args.output_csv else None,
0361             outputDirObj=outputDir if args.output_obj else None,
0362             rnd=rnd,
0363         )
0364 
0365 addDigitization(
0366     s,
0367     trackingGeometry,
0368     field,
0369     digiConfigFile=oddDigiConfig,
0370     outputDirRoot=outputDir if args.output_root else None,
0371     outputDirCsv=outputDir if args.output_csv else None,
0372     rnd=rnd,
0373 )
0374 
0375 addDigiParticleSelection(
0376     s,
0377     ParticleSelectorConfig(
0378         pt=(1.0 * u.GeV, None),
0379         eta=(-3.0, 3.0),
0380         measurements=(9, None),
0381         removeNeutral=True,
0382     ),
0383 )
0384 
0385 if args.reco:
0386     addSeeding(
0387         s,
0388         trackingGeometry,
0389         field,
0390         **(
0391             dict(
0392                 seedingAlgorithm=SeedingAlgorithm.Gbts,
0393                 layerMappingConfigFile=oddGbtsLayerMap,
0394                 connectorInputConfigFile=oddGbtsConnectionTable,
0395                 # with four pixel barrel layers a seed needs only two linked
0396                 # doublets and no z0 histogram cut, which asks for a fourth hit
0397                 gbtsConfigArg=GbtsConfigArg(
0398                     seederConfig=acts.examples.GraphBasedSeedingConfig(
0399                         minSeedLevel=2,
0400                     ),
0401                     graphConfig=acts.examples.GbtsGraphBuilderConfig(
0402                         minPt=0.9 * u.GeV,
0403                         z0HistogramMaxBarrelOrder=-1,
0404                     ),
0405                     useAdaptiveCuts=False,
0406                 ),
0407                 gbtsSeedParameters=True,
0408                 # for seeds without a GBTS fit
0409                 paramEstimationSpacePoints=acts.examples.SeedSpacePointSelection.SpreadTriplet,
0410             )
0411             if args.use_gbts
0412             else {}
0413         ),
0414         initialSigmas=[
0415             1 * u.mm,
0416             1 * u.mm,
0417             1 * u.degree,
0418             1 * u.degree,
0419             0 * u.e / u.GeV,
0420             1 * u.ns,
0421         ],
0422         initialSigmaQoverPt=0.1 * u.e / u.GeV,
0423         initialSigmaPtRel=0.1,
0424         initialVarInflation=[1.0] * 6,
0425         particleHypothesis=acts.ParticleHypothesis.muon,
0426         geoSelectionConfigFile=oddSeedingSel,
0427         outputDirRoot=outputDir if args.output_root else None,
0428         outputDirCsv=outputDir if args.output_csv else None,
0429     )
0430 
0431     if seedFilter_ML:
0432         addSeedFilterML(
0433             s,
0434             SeedFilterMLDBScanConfig(
0435                 epsilonDBScan=0.03, minPointsDBScan=2, minSeedScore=0.1
0436             ),
0437             onnxModelFile=os.path.dirname(__file__)
0438             + "/MLAmbiguityResolution/seedDuplicateClassifier.onnx",
0439             outputDirRoot=outputDir if args.output_root else None,
0440             outputDirCsv=outputDir if args.output_csv else None,
0441         )
0442 
0443     addCKFTracks(
0444         s,
0445         trackingGeometry,
0446         field,
0447         TrackSelectorConfig(
0448             pt=(1.0 * u.GeV if args.ttbar else 0.0, None),
0449             absEta=(None, 3.0),
0450             loc0=(-4.0 * u.mm, 4.0 * u.mm),
0451             nMeasurementsMin=7,
0452             maxHoles=2,
0453             maxOutliers=2,
0454         ),
0455         CkfConfig(
0456             chi2CutOffMeasurement=15.0,
0457             chi2CutOffOutlier=25.0,
0458             numMeasurementsCutOff=2,
0459             seedDeduplication=True,
0460             stayOnSeed=True,
0461             pixelVolumes=[16, 17, 18],
0462             stripVolumes=[23, 24, 25],
0463             maxPixelHoles=1,
0464             maxStripHoles=2,
0465             constrainToVolumes=[
0466                 2,  # beam pipe
0467                 32,
0468                 4,  # beam pip gap
0469                 16,
0470                 17,
0471                 18,  # pixel
0472                 20,  # PST
0473                 23,
0474                 24,
0475                 25,  # short strip
0476                 26,
0477                 8,  # long strip gap
0478                 28,
0479                 29,
0480                 30,  # long strip
0481             ],
0482         ),
0483         outputDirRoot=outputDir if args.output_root else None,
0484         outputDirCsv=outputDir if args.output_csv else None,
0485         writeCovMat=True,
0486     )
0487 
0488     if ambi_ML:
0489         addAmbiguityResolutionML(
0490             s,
0491             AmbiguityResolutionMLConfig(
0492                 maximumSharedHits=3, maximumIterations=1000000, nMeasurementsMin=7
0493             ),
0494             outputDirRoot=outputDir if args.output_root else None,
0495             outputDirCsv=outputDir if args.output_csv else None,
0496             onnxModelFile=os.path.dirname(__file__)
0497             + "/MLAmbiguityResolution/duplicateClassifier.onnx",
0498         )
0499 
0500     elif ambi_scoring:
0501         addScoreBasedAmbiguityResolution(
0502             s,
0503             ScoreBasedAmbiguityResolutionConfig(
0504                 minScore=0,
0505                 minScoreSharedTracks=1,
0506                 maxShared=2,
0507                 minUnshared=3,
0508                 maxSharedTracksPerMeasurement=2,
0509                 useAmbiguityScoring=False,
0510             ),
0511             outputDirRoot=outputDir if args.output_root else None,
0512             outputDirCsv=outputDir if args.output_csv else None,
0513             ambiVolumeFile=ambi_config,
0514             writeCovMat=True,
0515         )
0516     else:
0517         addAmbiguityResolution(
0518             s,
0519             AmbiguityResolutionConfig(
0520                 maximumSharedHits=3, maximumIterations=1000000, nMeasurementsMin=7
0521             ),
0522             outputDirRoot=outputDir if args.output_root else None,
0523             outputDirCsv=outputDir if args.output_csv else None,
0524             writeCovMat=True,
0525         )
0526 
0527     addVertexFitting(
0528         s,
0529         field,
0530         vertexFinder=VertexFinder.AMVF,
0531         outputDirRoot=outputDir if args.output_root else None,
0532         outputDirCsv=outputDir if args.output_csv else None,
0533     )
0534 
0535 if args.output_parquet:
0536     try:
0537         from acts.arrow import (
0538             particleSchema,
0539             simHitSchema,
0540             trackSchema,
0541         )
0542         from acts.examples.arrow import (
0543             ArrowParticleOutputConverter,
0544             ArrowSimHitOutputConverter,
0545             ArrowTrackOutputConverter,
0546             makeVolumeIdDetectorResolver,
0547             ParquetWriter,
0548         )
0549     except ImportError as e:
0550         raise RuntimeError(
0551             "Parquet output requested but acts.examples.arrow is not available; "
0552             "rebuild with ACTS_BUILD_EXAMPLES_PARQUET=ON."
0553         ) from e
0554 
0555     # ODD volume -> detector enum mapping used in parquet simhit export.
0556     # 0..8 are subdetector-specific enums; 255 marks unknown/unmatched.
0557     _odd_detector_resolver = makeVolumeIdDetectorResolver(
0558         {
0559             7: 0,  # pixel_neg_endcap
0560             8: 1,  # pixel_barrel
0561             9: 2,  # pixel_pos_endcap
0562             12: 3,  # short_neg_endcap
0563             13: 4,  # short_barrel
0564             14: 5,  # short_pos_endcap
0565             16: 6,  # long_neg_endcap
0566             17: 7,  # long_barrel
0567             18: 8,  # long_pos_endcap
0568         },
0569         255,
0570     )
0571 
0572     # Each converter parks an arrow::Table on the whiteboard under a fresh
0573     # key, and one ParquetWriter picks them all up.
0574     arrParticleConv = ArrowParticleOutputConverter(
0575         level=acts.logging.INFO,
0576         inputParticles="particles_simulated",
0577         outputTable="particles_arrow",
0578     )
0579     s.addAlgorithm(arrParticleConv)
0580 
0581     arrSimHitConv = ArrowSimHitOutputConverter(
0582         level=acts.logging.INFO,
0583         inputSimHits="simhits",
0584         inputParticles="particles_simulated",
0585         inputClusters="clusters",
0586         inputSimHitMeasurementsMap="simhit_measurements_map",
0587         outputTable="simhits_arrow",
0588         detectorResolver=_odd_detector_resolver,
0589     )
0590     s.addAlgorithm(arrSimHitConv)
0591 
0592     if args.reco:
0593         arrTrackConv = ArrowTrackOutputConverter(
0594             level=acts.logging.INFO,
0595             inputTracks="tracks",
0596             inputTrackParticleMatching="track_particle_matching",
0597             inputParticles="particles_simulated",
0598             inputMeasurementSimHitsMap="measurement_simhits_map",
0599             outputTable="tracks_arrow",
0600         )
0601         s.addAlgorithm(arrTrackConv)
0602 
0603     s.addWriter(
0604         ParquetWriter(
0605             level=acts.logging.INFO,
0606             outputDir=str(outputDir),
0607             collections={
0608                 arrSimHitConv.config.outputTable: "simhits",
0609                 arrTrackConv.config.outputTable: "tracks",
0610                 arrParticleConv.config.outputTable: "particles",
0611             },
0612             expectedSchemas={
0613                 arrSimHitConv.config.outputTable: simHitSchema(),
0614                 arrTrackConv.config.outputTable: trackSchema(),
0615                 arrParticleConv.config.outputTable: particleSchema(),
0616             },
0617         )
0618     )
0619 
0620 s.run()