Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-07-15 08:13:02

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