Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-04-03 07:48:19

0001 #!/usr/bin/env python3
0002 
0003 import sys, os, argparse, pathlib
0004 
0005 
0006 def parse_args():
0007     from acts.examples.reconstruction import SeedingAlgorithm
0008 
0009     parser = argparse.ArgumentParser(description="""
0010 Script to test the full chain ACTS simulation and reconstruction.
0011 
0012 This script is provided for interactive developer testing only.
0013 It is not intended (and not supported) for end user use, automated testing,
0014 and certainly should never be called in production. The Python API is the
0015 proper way to access the ActsExamples from scripts. The other Examples/Scripts
0016 are much better examples of how to do that. physmon in the CI is the proper
0017 way to do automated integration tests. This script is only for the case of
0018 interactive testing with one-off configuration specified by command-line options.
0019 """)
0020     parser.add_argument(
0021         "-G",
0022         "--generic-detector",
0023         action="store_true",
0024         help="Use generic detector geometry and config",
0025     )
0026     parser.add_argument(
0027         "--odd",
0028         default=True,
0029         action=argparse.BooleanOptionalAction,
0030         help="Use Open Data Detector geometry and config (default unless overridden by -G or -A). Requires ACTS_BUILD_ODD.",
0031     )
0032     parser.add_argument(
0033         "-A",
0034         "--itk",
0035         action="store_true",
0036         help="Use ATLAS ITk geometry and config. Requires acts-itk/ in current directory.",
0037     )
0038     parser.add_argument(
0039         "-g",
0040         "--geant4",
0041         action="store_true",
0042         help="Use Geant4 instead of Fatras for detector simulation",
0043     )
0044     parser.add_argument(
0045         "--edm4hep",
0046         type=pathlib.Path,
0047         help="Use edm4hep inputs",
0048     )
0049     parser.add_argument(
0050         "-b",
0051         "--bf-constant",
0052         action="store_true",
0053         help="Use constant 2T B-field also for ITk; and don't include material map",
0054     )
0055     parser.add_argument(
0056         "-j",
0057         "--threads",
0058         type=int,
0059         default=-1,
0060         help="Number of parallel threads, negative for automatic (default).",
0061     )
0062     parser.add_argument(
0063         "-o",
0064         "--output-dir",
0065         "--output",
0066         default=None,
0067         type=pathlib.Path,
0068         help="Directory to write outputs to",
0069     )
0070     parser.add_argument(
0071         "-O",
0072         "--output-detail",
0073         action="count",
0074         default=0,
0075         help="fewer output files. Use -OO for more output files. Use -OOO to disable all output.",
0076     )
0077     parser.add_argument(
0078         "-c",
0079         "--output-csv",
0080         action="count",
0081         default=0,
0082         help="Use CSV output instead of ROOT. Specify -cc to output all formats (ROOT, CSV, and obj).",
0083     )
0084     parser.add_argument(
0085         "--output-obj",
0086         action="store_true",
0087         help="Enable obj output",
0088     )
0089     parser.add_argument(
0090         "-n",
0091         "--events",
0092         type=int,
0093         default=100,
0094         help="The number of events to process (default=%(default)d).",
0095     )
0096     parser.add_argument(
0097         "-s",
0098         "--skip",
0099         type=int,
0100         default=0,
0101         help="Number of events to skip (default=%(default)d)",
0102     )
0103     # Many of the following option names were inherited from the old examples binaries and full_chain_odd.py.
0104     # To maintain compatibility, both option names are supported.
0105     parser.add_argument(
0106         "-N",
0107         "--gen-nparticles",
0108         "--gun-particles",
0109         type=int,
0110         default=4,
0111         help="Number of generated particles per vertex from the particle gun (default=%(default)d).",
0112     )
0113     parser.add_argument(
0114         "-M",
0115         "--gen-nvertices",
0116         "--gun-multiplicity",
0117         "--ttbar-pu",
0118         type=int,
0119         default=200,
0120         help="Number of vertices per event (multiplicity) from the particle gun; or number of pileup events (default=%(default)d)",
0121     )
0122     parser.add_argument(
0123         "-t",
0124         "--ttbar-pu200",
0125         "--ttbar",
0126         action="store_true",
0127         help="Generate ttbar + mu=200 pile-up using Pythia8",
0128     )
0129     parser.add_argument(
0130         "-p",
0131         "--gen-pt-range",
0132         "--gun-pt-range",
0133         default="1:10",
0134         help="transverse momentum (pT) range (min:max) of the particle gun in GeV (default=%(default)s)",
0135     )
0136     parser.add_argument(
0137         "--gen-eta-range",
0138         "--gun-eta-range",
0139         help="Eta range (min:max) of the particle gun (default -2:2 (Generic), -3:3 (ODD), -4:4 (ITk))",
0140     )
0141     parser.add_argument(
0142         "--gen-cos-theta",
0143         action="store_true",
0144         help="Sample eta as cos(theta) and not uniform",
0145     )
0146     parser.add_argument(
0147         "-r",
0148         "--random-seed",
0149         type=int,
0150         default=42,
0151         help="Random number seed (default=%(default)d)",
0152     )
0153     parser.add_argument(
0154         "-F",
0155         "--disable-fpemon",
0156         action="store_true",
0157         help="sets ACTS_SEQUENCER_DISABLE_FPEMON=1",
0158     )
0159     parser.add_argument(
0160         "-l",
0161         "--loglevel",
0162         type=int,
0163         default=2,
0164         help="The output log level. Please set the wished number (0 = VERBOSE, 1 = DEBUG, 2 = INFO (default), 3 = WARNING, 4 = ERROR, 5 = FATAL).",
0165     )
0166     parser.add_argument(
0167         "-d",
0168         "--dump-args-calls",
0169         action="store_true",
0170         help="Show pybind function call details",
0171     )
0172     parser.add_argument(
0173         "--digi-config",
0174         type=pathlib.Path,
0175         help="Digitization configuration file",
0176     )
0177     parser.add_argument(
0178         "--material-config",
0179         type=pathlib.Path,
0180         help="Material map configuration file",
0181     )
0182     parser.add_argument(
0183         "-S",
0184         "--seeding-algorithm",
0185         action=EnumAction,
0186         enum=SeedingAlgorithm,
0187         default=SeedingAlgorithm.GridTriplet,
0188         help="Select the seeding algorithm to use",
0189     )
0190     parser.add_argument(
0191         "--ckf",
0192         default=True,
0193         action=argparse.BooleanOptionalAction,
0194         help="Switch CKF on/off",
0195     )
0196     parser.add_argument(
0197         "--reco",
0198         default=True,
0199         action=argparse.BooleanOptionalAction,
0200         help="Switch reco on/off",
0201     )
0202     parser.add_argument(
0203         "--vertexing",
0204         default=True,
0205         action=argparse.BooleanOptionalAction,
0206         help="Switch vertexing on/off",
0207     )
0208     parser.add_argument(
0209         "--MLSeedFilter",
0210         action="store_true",
0211         help="Use the ML seed filter to select seed after the seeding step",
0212     )
0213     parser.add_argument(
0214         "--ambi-solver",
0215         type=str,
0216         choices=["greedy", "scoring", "ML", "none"],
0217         default="greedy",
0218         help="Set which ambiguity solver to use (default=%(default)s)",
0219     )
0220     parser.add_argument(
0221         "--ambi-config",
0222         type=pathlib.Path,
0223         default=pathlib.Path.cwd() / "ambi_config.json",
0224         help="Set the configuration file for the Score Based ambiguity resolution (default=%(default)s)",
0225     )
0226     return parser.parse_args()
0227 
0228 
0229 def full_chain(args):
0230     import acts, acts.root
0231 
0232     # keep these in memory after we return the sequence
0233     global detector, trackingGeometry, decorators, field, rnd
0234     global logger
0235 
0236     if args.disable_fpemon:
0237         os.environ["ACTS_SEQUENCER_DISABLE_FPEMON"] = "1"
0238 
0239     if args.dump_args_calls:
0240         acts.examples.dump_args_calls(locals())
0241 
0242     logger = acts.getDefaultLogger("full_chain_test", acts.logging.Level(args.loglevel))
0243 
0244     nDetArgs = [args.generic_detector, args.odd, args.itk].count(True)
0245     if nDetArgs == 0:
0246         args.generic_detector = True
0247     elif nDetArgs == 2:
0248         args.odd = False
0249     nDetArgs = [args.generic_detector, args.odd, args.itk].count(True)
0250     if nDetArgs != 1:
0251         logger.fatal("require exactly one of: --generic-detector --odd --itk")
0252         sys.exit(2)
0253     if args.generic_detector:
0254         detname = "gen"
0255     elif args.itk:
0256         detname = "itk"
0257     elif args.odd:
0258         detname = "odd"
0259 
0260     u = acts.UnitConstants
0261 
0262     if args.output_detail == 3:
0263         outputDirLess = None
0264     elif args.output_dir is None:
0265         outputDirLess = pathlib.Path.cwd() / f"{detname}_output"
0266     else:
0267         outputDirLess = args.output_dir
0268 
0269     outputDir = None if args.output_detail == 1 else outputDirLess
0270     outputDirMore = None if args.output_detail in (0, 1) else outputDirLess
0271 
0272     outputDirRoot = outputDir if args.output_csv != 1 else None
0273     outputDirLessRoot = outputDirLess if args.output_csv != 1 else None
0274     outputDirMoreRoot = outputDirMore if args.output_csv != 1 else None
0275     outputDirCsv = outputDir if args.output_csv != 0 else None
0276     outputDirLessCsv = outputDirLess if args.output_csv != 0 else None
0277     outputDirMoreCsv = outputDirMore if args.output_csv != 0 else None
0278     outputDirObj = (
0279         outputDirLess
0280         if args.output_obj
0281         else outputDir if args.output_csv == 2 else None
0282     )
0283 
0284     actsDir = pathlib.Path(__file__).resolve().parent.parent.parent.parent
0285 
0286     # fmt: off
0287     if args.generic_detector:
0288         etaRange = (-2.0, 2.0)
0289         ptMin = 0.5 * u.GeV
0290         rhoMax = 24.0 * u.mm
0291         if args.loglevel <= 2:
0292             logger.info(f"Load Generic Detector from {actsDir}")
0293         if args.digi_config is None:
0294             args.digi_config = actsDir / "Examples/Configs/generic-digi-smearing-config.json"
0295         seedingConfigFile = actsDir / "Examples/Configs/generic-seeding-config.json"
0296         args.bf_constant = True
0297         detector = acts.examples.GenericDetector()
0298         trackingGeometry = detector.trackingGeometry()
0299         decorators = detector.contextDecorators()
0300     elif args.odd:
0301         import acts.examples.odd
0302         etaRange = (-3.0, 3.0)
0303         ptMin = 1.0 * u.GeV
0304         rhoMax = 24.0 * u.mm
0305         geoDir = acts.examples.odd.getOpenDataDetectorDirectory()
0306         if args.loglevel <= 2:
0307             logger.info(f"Load Open Data Detector from {geoDir.resolve()}")
0308         if args.digi_config is None:
0309             args.digi_config = actsDir / "Examples/Configs/odd-digi-smearing-config.json"
0310         seedingConfigFile = actsDir / "Examples/Configs/odd-seeding-config.json"
0311         if args.material_config is None:
0312             args.material_config = geoDir / "data/odd-material-maps.root"
0313         args.bf_constant = True
0314         detector = acts.examples.odd.getOpenDataDetector(
0315             odd_dir=geoDir,
0316             materialDecorator=acts.IMaterialDecorator.fromFile(args.material_config),
0317         )
0318         trackingGeometry = detector.trackingGeometry()
0319         decorators = detector.contextDecorators()
0320     elif args.itk:
0321         import acts.examples.itk as itk
0322         etaRange = (-4.0, 4.0)
0323         ptMin = 1.0 * u.GeV
0324         rhoMax = 28.0 * u.mm
0325         geoDir = pathlib.Path("acts-itk")
0326         if args.loglevel <= 2:
0327             logger.info(f"Load ATLAS ITk from {geoDir.resolve()}")
0328         if args.digi_config is None:
0329             args.digi_config = geoDir / "itk-hgtd/itk-smearing-config.json"
0330         seedingConfigFile = geoDir / "itk-hgtd/geoSelection-ITk.json"
0331         # args.material_config defaulted in itk.buildITkGeometry: geoDir / "itk-hgtd/material-maps-ITk-HGTD.json"
0332         bFieldFile = geoDir / "bfield/ATLAS-BField-xyz.root"
0333         detector = itk.buildITkGeometry(
0334             geoDir,
0335             customMaterialFile=args.material_config,
0336             material=not args.bf_constant,
0337             logLevel=acts.logging.Level(args.loglevel),
0338         )
0339         trackingGeometry = detector.trackingGeometry()
0340         decorators = detector.contextDecorators()
0341     # fmt: on
0342 
0343     if args.bf_constant:
0344         field = acts.ConstantBField(acts.Vector3(0.0, 0.0, 2.0 * u.T))
0345     else:
0346         logger.info("Create magnetic field map from {}", bFieldFile)
0347         field = acts.root.MagneticFieldMapXyz(str(bFieldFile))
0348     rnd = acts.examples.RandomNumbers(seed=42)
0349 
0350     from acts.examples.simulation import (
0351         MomentumConfig,
0352         EtaConfig,
0353         PhiConfig,
0354         ParticleConfig,
0355         ParticleSelectorConfig,
0356         addDigitization,
0357         addGenParticleSelection,
0358         addSimParticleSelection,
0359         addDigiParticleSelection,
0360     )
0361 
0362     s = acts.examples.Sequencer(
0363         events=args.events,
0364         skip=args.skip,
0365         numThreads=args.threads if not (args.geant4 and args.threads == -1) else 1,
0366         logLevel=acts.logging.Level(args.loglevel),
0367         outputDir="" if outputDirLess is None else str(outputDirLess),
0368     )
0369 
0370     # is this needed?
0371     for d in decorators:
0372         s.addContextDecorator(d)
0373 
0374     if args.edm4hep:
0375         import acts.examples.edm4hep
0376 
0377         s.addReader(
0378             acts.examples.edm4hep.PodioReader(
0379                 level=acts.logging.DEBUG,
0380                 inputPath=str(args.edm4hep),
0381                 outputFrame="events",
0382                 category="events",
0383             )
0384         )
0385 
0386         edm4hepReader = acts.examples.edm4hep.EDM4hepSimInputConverter(
0387             inputFrame="events",
0388             inputSimHits=[
0389                 "PixelBarrelReadout",
0390                 "PixelEndcapReadout",
0391                 "ShortStripBarrelReadout",
0392                 "ShortStripEndcapReadout",
0393                 "LongStripBarrelReadout",
0394                 "LongStripEndcapReadout",
0395             ],
0396             outputParticlesGenerator="particles_generated",
0397             outputParticlesSimulation="particles_simulated",
0398             outputSimHits="simhits",
0399             outputSimVertices="vertices_truth",
0400             dd4hepDetector=detector,
0401             trackingGeometry=trackingGeometry,
0402             sortSimHitsInTime=False,
0403             particleRMax=1080 * u.mm,
0404             particleZ=(-3030 * u.mm, 3030 * u.mm),
0405             particlePtMin=150 * u.MeV,
0406             level=acts.logging.DEBUG,
0407         )
0408         s.addAlgorithm(edm4hepReader)
0409 
0410         s.addWhiteboardAlias(
0411             "particles", edm4hepReader.config.outputParticlesSimulation
0412         )
0413 
0414         addSimParticleSelection(
0415             s,
0416             ParticleSelectorConfig(
0417                 rho=(0.0 * u.mm, rhoMax),
0418                 absZ=(0.0 * u.mm, 1.0 * u.m),
0419                 eta=etaRange,
0420                 removeNeutral=True,
0421             ),
0422         )
0423 
0424     else:
0425 
0426         if not args.ttbar_pu200:
0427             from acts.examples.simulation import addParticleGun
0428 
0429             addParticleGun(
0430                 s,
0431                 MomentumConfig(
0432                     *strToRange(args.gen_pt_range, "--gen-pt-range", u.GeV),
0433                     transverse=True,
0434                 ),
0435                 EtaConfig(
0436                     *(
0437                         strToRange(args.gen_eta_range, "--gen-eta-range")
0438                         if args.gen_eta_range
0439                         else etaRange
0440                     ),
0441                     uniform=(
0442                         not args.gen_cos_theta
0443                         if args.gen_cos_theta or not args.odd
0444                         else None
0445                     ),
0446                 ),
0447                 PhiConfig(0.0, 360.0 * u.degree) if not args.itk else PhiConfig(),
0448                 ParticleConfig(
0449                     args.gen_nparticles, acts.PdgParticle.eMuon, randomizeCharge=True
0450                 ),
0451                 vtxGen=(
0452                     acts.examples.GaussianVertexGenerator(
0453                         mean=acts.Vector4(0, 0, 0, 0),
0454                         stddev=acts.Vector4(
0455                             0.0125 * u.mm, 0.0125 * u.mm, 55.5 * u.mm, 1.0 * u.ns
0456                         ),
0457                     )
0458                     if args.odd
0459                     else None
0460                 ),
0461                 multiplicity=args.gen_nvertices,
0462                 rnd=rnd,
0463                 outputDirRoot=outputDirMoreRoot,
0464                 outputDirCsv=outputDirMoreCsv,
0465             )
0466         else:
0467             from acts.examples.simulation import addPythia8
0468 
0469             addPythia8(
0470                 s,
0471                 hardProcess=["Top:qqbar2ttbar=on"],
0472                 npileup=args.gen_nvertices,
0473                 vtxGen=acts.examples.GaussianVertexGenerator(
0474                     stddev=acts.Vector4(
0475                         0.0125 * u.mm, 0.0125 * u.mm, 55.5 * u.mm, 5.0 * u.ns
0476                     ),
0477                     mean=acts.Vector4(0, 0, 0, 0),
0478                 ),
0479                 rnd=rnd,
0480                 outputDirRoot=outputDirRoot,
0481                 outputDirCsv=outputDirCsv,
0482             )
0483             addGenParticleSelection(
0484                 s,
0485                 ParticleSelectorConfig(
0486                     rho=(0.0 * u.mm, rhoMax),
0487                     absZ=(0.0 * u.mm, 1.0 * u.m),
0488                     eta=etaRange,
0489                     pt=(150 * u.MeV, None),
0490                 ),
0491             )
0492 
0493         if not args.geant4:
0494             from acts.examples.simulation import addFatras
0495 
0496             addFatras(
0497                 s,
0498                 trackingGeometry,
0499                 field,
0500                 rnd=rnd,
0501                 outputDirRoot=outputDirRoot,
0502                 outputDirCsv=outputDirCsv,
0503                 outputDirObj=outputDirObj,
0504             )
0505         else:
0506             if s.config.numThreads != 1:
0507                 logger.fatal(
0508                     f"Geant 4 simulation does not support multi-threading (threads={s.config.numThreads})"
0509                 )
0510                 sys.exit(2)
0511 
0512             from acts.examples.simulation import addGeant4
0513 
0514             # Pythia can sometime simulate particles outside the world volume, a cut on the Z of the track help mitigate this effect
0515             # Older version of G4 might not work, this as has been tested on version `geant4-11-00-patch-03`
0516             # For more detail see issue #1578
0517             addGeant4(
0518                 s,
0519                 detector,
0520                 trackingGeometry,
0521                 field,
0522                 rnd=rnd,
0523                 killVolume=trackingGeometry.highestTrackingVolume,
0524                 killAfterTime=25 * u.ns,
0525                 outputDirRoot=outputDirRoot,
0526                 outputDirCsv=outputDirCsv,
0527                 outputDirObj=outputDirObj,
0528             )
0529 
0530     addDigitization(
0531         s,
0532         trackingGeometry,
0533         field,
0534         digiConfigFile=args.digi_config,
0535         rnd=rnd,
0536         outputDirRoot=outputDirRoot,
0537         outputDirCsv=outputDirCsv,
0538     )
0539 
0540     addDigiParticleSelection(
0541         s,
0542         ParticleSelectorConfig(
0543             pt=(ptMin, None),
0544             eta=etaRange if not args.generic_detector else (None, None),
0545             hits=(9, None),
0546             removeNeutral=True,
0547         ),
0548     )
0549 
0550     if not args.reco:
0551         return s
0552 
0553     from acts.examples.reconstruction import (
0554         addSeeding,
0555         TrackSmearingSigmas,
0556         addCKFTracks,
0557         CkfConfig,
0558         SeedingAlgorithm,
0559         TrackSelectorConfig,
0560         addAmbiguityResolution,
0561         AmbiguityResolutionConfig,
0562         addVertexFitting,
0563         VertexFinder,
0564     )
0565 
0566     if args.itk and args.seeding_algorithm == SeedingAlgorithm.GridTriplet:
0567         seedingAlgConfig = itk.itkSeedingAlgConfig(
0568             itk.InputSpacePointsType.PixelSpacePoints
0569         )
0570     else:
0571         seedingAlgConfig = []
0572 
0573     addSeeding(
0574         s,
0575         trackingGeometry,
0576         field,
0577         *seedingAlgConfig,
0578         seedingAlgorithm=args.seeding_algorithm,
0579         **(
0580             dict(
0581                 trackSmearingSigmas=TrackSmearingSigmas(ptRel=0.01),
0582                 rnd=rnd,
0583             )
0584             if args.seeding_algorithm == SeedingAlgorithm.TruthSmeared
0585             else {}
0586         ),
0587         initialSigmas=[
0588             1 * u.mm,
0589             1 * u.mm,
0590             1 * u.degree,
0591             1 * u.degree,
0592             0 * u.e / u.GeV,
0593             1 * u.ns,
0594         ],
0595         initialSigmaQoverPt=0.1 * u.e / u.GeV,
0596         initialSigmaPtRel=0.1,
0597         initialVarInflation=[1.0] * 6,
0598         geoSelectionConfigFile=seedingConfigFile,
0599         outputDirRoot=outputDirLessRoot,
0600         outputDirCsv=outputDirLessCsv,
0601     )
0602 
0603     if args.MLSeedFilter:
0604         from acts.examples.reconstruction import (
0605             addSeedFilterML,
0606             SeedFilterMLDBScanConfig,
0607         )
0608 
0609         addSeedFilterML(
0610             s,
0611             SeedFilterMLDBScanConfig(
0612                 epsilonDBScan=0.03, minPointsDBScan=2, minSeedScore=0.1
0613             ),
0614             onnxModelFile=str(
0615                 actsDir
0616                 / "Examples/Scripts/Python/MLAmbiguityResolution/seedDuplicateClassifier.onnx"
0617             ),
0618             outputDirRoot=outputDirLessRoot,
0619             outputDirCsv=outputDirLessCsv,
0620         )
0621 
0622     if not args.ckf:
0623         return s
0624 
0625     if args.seeding_algorithm != SeedingAlgorithm.TruthSmeared:
0626         ckfConfig = CkfConfig(
0627             seedDeduplication=True,
0628             stayOnSeed=True,
0629         )
0630     else:
0631         ckfConfig = CkfConfig()
0632 
0633     if not args.itk:
0634         trackSelectorConfig = TrackSelectorConfig(
0635             pt=(ptMin if args.ttbar_pu200 else 0.0, None),
0636             absEta=(None, 3.0),
0637             loc0=(-4.0 * u.mm, 4.0 * u.mm),
0638             nMeasurementsMin=7,
0639             maxHoles=2,
0640             maxOutliers=2,
0641         )
0642         ckfConfig = ckfConfig._replace(
0643             chi2CutOffMeasurement=15.0,
0644             chi2CutOffOutlier=25.0,
0645             numMeasurementsCutOff=2,
0646         )
0647     else:
0648         # fmt: off
0649         trackSelectorConfig = (
0650             TrackSelectorConfig(absEta=(None, 2.0), pt=(0.9 * u.GeV, None), nMeasurementsMin=9, maxHoles=2, maxOutliers=2, maxSharedHits=2),
0651             TrackSelectorConfig(absEta=(None, 2.6), pt=(0.4 * u.GeV, None), nMeasurementsMin=8, maxHoles=2, maxOutliers=2, maxSharedHits=2),
0652             TrackSelectorConfig(absEta=(None, 4.0), pt=(0.4 * u.GeV, None), nMeasurementsMin=7, maxHoles=2, maxOutliers=2, maxSharedHits=2),
0653         )
0654         # fmt: on
0655 
0656     if args.odd:
0657         ckfConfig = ckfConfig._replace(
0658             pixelVolumes=[16, 17, 18],
0659             stripVolumes=[23, 24, 25],
0660             maxPixelHoles=1,
0661             maxStripHoles=2,
0662             constrainToVolumes=[
0663                 2,  # beam pipe
0664                 32,
0665                 4,  # beam pip gap
0666                 16,
0667                 17,
0668                 18,  # pixel
0669                 20,  # PST
0670                 23,
0671                 24,
0672                 25,  # short strip
0673                 26,
0674                 8,  # long strip gap
0675                 28,
0676                 29,
0677                 30,  # long strip
0678             ],
0679         )
0680     elif args.itk:
0681         ckfConfig = ckfConfig._replace(
0682             # ITk volumes from Noemi's plot
0683             pixelVolumes=[8, 9, 10, 13, 14, 15, 16, 18, 19, 20],
0684             stripVolumes=[22, 23, 24],
0685             maxPixelHoles=1,
0686             maxStripHoles=2,
0687         )
0688 
0689     if args.output_detail == 1:
0690         writeDetail = dict(writeTrackSummary=False)
0691     elif args.output_detail == 2:
0692         writeDetail = dict(writeTrackStates=True)
0693     else:
0694         writeDetail = {}
0695 
0696     if args.odd and args.output_detail != 1:
0697         writeCovMat = dict(writeCovMat=True)
0698     else:
0699         writeCovMat = {}
0700 
0701     addCKFTracks(
0702         s,
0703         trackingGeometry,
0704         field,
0705         trackSelectorConfig=trackSelectorConfig,
0706         ckfConfig=ckfConfig,
0707         **writeDetail,
0708         **writeCovMat,
0709         outputDirRoot=outputDirLessRoot,
0710         outputDirCsv=outputDirLessCsv,
0711     )
0712 
0713     if args.ambi_solver == "ML":
0714 
0715         from acts.examples.reconstruction import (
0716             addAmbiguityResolutionML,
0717             AmbiguityResolutionMLConfig,
0718         )
0719 
0720         addAmbiguityResolutionML(
0721             s,
0722             AmbiguityResolutionMLConfig(
0723                 maximumSharedHits=3, maximumIterations=1000000, nMeasurementsMin=7
0724             ),
0725             onnxModelFile=str(
0726                 actsDir
0727                 / "Examples/Scripts/Python/MLAmbiguityResolution/duplicateClassifier.onnx"
0728             ),
0729             outputDirRoot=outputDirLessRoot,
0730             outputDirCsv=outputDirLessCsv,
0731         )
0732 
0733     elif args.ambi_solver == "scoring":
0734 
0735         from acts.examples.reconstruction import (
0736             addScoreBasedAmbiguityResolution,
0737             ScoreBasedAmbiguityResolutionConfig,
0738         )
0739 
0740         addScoreBasedAmbiguityResolution(
0741             s,
0742             ScoreBasedAmbiguityResolutionConfig(
0743                 minScore=0,
0744                 minScoreSharedTracks=1,
0745                 maxShared=2,
0746                 minUnshared=3,
0747                 maxSharedTracksPerMeasurement=2,
0748                 useAmbiguityScoring=False,
0749             ),
0750             ambiVolumeFile=args.ambi_config,
0751             **writeCovMat,
0752             outputDirRoot=outputDirLessRoot,
0753             outputDirCsv=outputDirLessCsv,
0754         )
0755 
0756     elif args.ambi_solver == "greedy":
0757 
0758         addAmbiguityResolution(
0759             s,
0760             AmbiguityResolutionConfig(
0761                 maximumSharedHits=3,
0762                 maximumIterations=10000 if args.itk else 1000000,
0763                 nMeasurementsMin=6 if args.itk else 7,
0764             ),
0765             **writeDetail,
0766             **writeCovMat,
0767             outputDirRoot=outputDirLessRoot,
0768             outputDirCsv=outputDirLessCsv,
0769         )
0770 
0771     if args.vertexing:
0772         addVertexFitting(
0773             s,
0774             field,
0775             vertexFinder=VertexFinder.AMVF,
0776             outputDirRoot=outputDirLessRoot,
0777             outputDirCsv=outputDirLessCsv,
0778         )
0779 
0780     return s
0781 
0782 
0783 def strToRange(s: str, optName: str, unit: float = 1.0):
0784     global logger
0785     try:
0786         range = [float(e) * unit if e != "" else None for e in s.split(":")]
0787     except ValueError:
0788         range = []
0789     if len(range) == 1:
0790         range.append(range[0])  # 100 -> 100:100
0791     if len(range) != 2:
0792         logger.fatal(f"bad option value: {optName} {s}")
0793         sys.exit(2)
0794     return range
0795 
0796 
0797 # Graciously taken from https://stackoverflow.com/a/60750535/4280680 (via seeding.py)
0798 class EnumAction(argparse.Action):
0799     """
0800     Argparse action for handling Enums
0801     """
0802 
0803     def __init__(self, **kwargs):
0804         import enum
0805 
0806         # Pop off the type value
0807         enum_type = kwargs.pop("enum", None)
0808 
0809         # Ensure an Enum subclass is provided
0810         if enum_type is None:
0811             raise ValueError("type must be assigned an Enum when using EnumAction")
0812         if not issubclass(enum_type, enum.Enum):
0813             raise TypeError("type must be an Enum when using EnumAction")
0814 
0815         # Generate choices from the Enum
0816         kwargs.setdefault("choices", tuple(e.name for e in enum_type))
0817 
0818         super(EnumAction, self).__init__(**kwargs)
0819 
0820         self._enum = enum_type
0821 
0822     def __call__(self, parser, namespace, values, option_string=None):
0823         for e in self._enum:
0824             if e.name == values:
0825                 setattr(namespace, self.dest, e)
0826                 break
0827         else:
0828             raise ValueError("%s is not a validly enumerated algorithm." % values)
0829 
0830 
0831 # main program: parse arguments, setup sequence, and run the full chain
0832 full_chain(parse_args()).run()