Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-01-30 09:14:56

0001 import multiprocessing
0002 from pathlib import Path
0003 import sys
0004 import os
0005 import tempfile
0006 import shutil
0007 from typing import Dict
0008 import warnings
0009 import pytest_check as check
0010 from collections import namedtuple
0011 
0012 
0013 sys.path += [
0014     str(Path(__file__).parent.parent.parent.parent / "Examples/Scripts/Python/"),
0015     str(Path(__file__).parent),
0016 ]
0017 
0018 
0019 import helpers
0020 import helpers.hash_root
0021 
0022 import pytest
0023 
0024 import acts
0025 import acts.examples
0026 from acts.examples.odd import getOpenDataDetector
0027 from acts.examples.simulation import addParticleGun, EtaConfig, ParticleConfig
0028 
0029 try:
0030     import ROOT
0031 
0032     ROOT.gSystem.ResetSignals()
0033 except ImportError:
0034     pass
0035 
0036 try:
0037     if acts.logging.getFailureThreshold() != acts.logging.WARNING:
0038         acts.logging.setFailureThreshold(acts.logging.WARNING)
0039 except RuntimeError:
0040     # Repackage with different error string
0041     errtype = (
0042         "negative"
0043         if acts.logging.getFailureThreshold() < acts.logging.WARNING
0044         else "positive"
0045     )
0046     warnings.warn(
0047         "Runtime log failure threshold could not be set. "
0048         "Compile-time value is probably set via CMake, i.e. "
0049         f"`ACTS_LOG_FAILURE_THRESHOLD={acts.logging.getFailureThreshold().name}` is set, "
0050         "or `ACTS_ENABLE_LOG_FAILURE_THRESHOLD=OFF`. "
0051         f"The pytest test-suite can produce false-{errtype} results in this configuration"
0052     )
0053 
0054 
0055 u = acts.UnitConstants
0056 
0057 
0058 class RootHashAssertionError(AssertionError):
0059     def __init__(
0060         self, file: Path, key: str, exp_hash: str, act_hash: str, *args, **kwargs
0061     ):
0062         super().__init__(f"{exp_hash} != {act_hash}", *args, **kwargs)
0063         self.file = file
0064         self.key = key
0065         self.exp_hash = exp_hash
0066         self.act_hash = act_hash
0067 
0068 
0069 hash_assertion_failures = []
0070 
0071 
0072 def _parse_hash_file(file: Path) -> Dict[str, str]:
0073     res = {}
0074     for line in file.open():
0075         if line.strip() == "" or line.strip().startswith("#"):
0076             continue
0077         key, h = line.strip().split(":", 1)
0078         res[key.strip()] = h.strip()
0079     return res
0080 
0081 
0082 @pytest.fixture(scope="session")
0083 def root_file_exp_hashes():
0084     path = Path(
0085         os.environ.get("ROOT_HASH_FILE", Path(__file__).parent / "root_file_hashes.txt")
0086     )
0087     return _parse_hash_file(path)
0088 
0089 
0090 @pytest.fixture(name="assert_root_hash")
0091 def assert_root_hash(request, root_file_exp_hashes):
0092     if not helpers.doHashChecks:
0093 
0094         def fn(*args, **kwargs):
0095             pass
0096 
0097         return fn
0098 
0099     def fn(key: str, file: Path):
0100         """
0101         Assertion helper function to check the hashes of root files.
0102         Do NOT use this function directly by importing, rather use it as a pytest fixture
0103 
0104         Arguments you need to provide:
0105         key: Explicit lookup key for the expected hash, should be unique per test function
0106         file: Root file to check the expected hash against
0107         """
0108         __tracebackhide__ = True
0109         gkey = f"{request.node.name}__{key}"
0110         act_hash = helpers.hash_root.hash_root_file(file)
0111         if not gkey in root_file_exp_hashes:
0112             warnings.warn(
0113                 f'Hash lookup key "{key}" not found for test "{request.node.name}"'
0114             )
0115             check.equal(act_hash, "[MISSING]")
0116             exc = RootHashAssertionError(file, gkey, "[MISSING]", act_hash)
0117             hash_assertion_failures.append(exc)
0118 
0119         else:
0120             refhash = root_file_exp_hashes[gkey]
0121             check.equal(act_hash, refhash)
0122             if act_hash != refhash:
0123                 exc = RootHashAssertionError(file, gkey, refhash, act_hash)
0124                 hash_assertion_failures.append(exc)
0125 
0126     return fn
0127 
0128 
0129 def pytest_terminal_summary(terminalreporter, exitstatus, config):
0130     docs_url = "https://acts.readthedocs.io/en/latest/examples/python_bindings.html#root-file-hash-regression-checks"
0131     if len(hash_assertion_failures) > 0:
0132         terminalreporter.ensure_newline()
0133         terminalreporter.section(
0134             "RootHashAssertionErrors", sep="-", red=True, bold=True
0135         )
0136         terminalreporter.line(
0137             "The ROOT files produced by tests have changed since the last recorded reference."
0138         )
0139         terminalreporter.line(
0140             "This can be be expected if e.g. the underlying algorithm changed, or it can be a test failure symptom."
0141         )
0142         terminalreporter.line(
0143             "Please manually check the output files listed below and make sure that their content is correct."
0144         )
0145         terminalreporter.line(
0146             "If it is, you can update the test reference file Examples/Python/tests/root_file_hashes.txt with the new hashes below."
0147         )
0148         terminalreporter.line(f"See {docs_url} for more details")
0149         terminalreporter.line("")
0150 
0151         for e in hash_assertion_failures:
0152             terminalreporter.line(f"{e.key}: {e.act_hash}")
0153 
0154     if not helpers.doHashChecks:
0155         terminalreporter.section("Root file has checks", sep="-", blue=True, bold=True)
0156         terminalreporter.line(
0157             "NOTE: Root file hash checks were skipped, enable with ROOT_HASH_CHECKS=on"
0158         )
0159         terminalreporter.line(f"See {docs_url} for more details")
0160 
0161 
0162 def kwargsConstructor(cls, *args, **kwargs):
0163     return cls(*args, **kwargs)
0164 
0165 
0166 def configKwConstructor(cls, *args, **kwargs):
0167     assert hasattr(cls, "Config")
0168     _kwargs = {}
0169     if "level" in kwargs:
0170         _kwargs["level"] = kwargs.pop("level")
0171     config = cls.Config()
0172     for k, v in kwargs.items():
0173         setattr(config, k, v)
0174     return cls(*args, config=config, **_kwargs)
0175 
0176 
0177 def configPosConstructor(cls, *args, **kwargs):
0178     assert hasattr(cls, "Config")
0179     _kwargs = {}
0180     if "level" in kwargs:
0181         _kwargs["level"] = kwargs.pop("level")
0182     config = cls.Config()
0183     for k, v in kwargs.items():
0184         setattr(config, k, v)
0185 
0186     return cls(config, *args, **_kwargs)
0187 
0188 
0189 @pytest.fixture(params=[configPosConstructor, configKwConstructor, kwargsConstructor])
0190 def conf_const(request):
0191     return request.param
0192 
0193 
0194 @pytest.fixture
0195 def rng():
0196     return acts.examples.RandomNumbers(seed=42)
0197 
0198 
0199 @pytest.fixture
0200 def basic_prop_seq(rng):
0201     def _basic_prop_seq_factory(geo, s=None):
0202         if s is None:
0203             s = acts.examples.Sequencer(events=10, numThreads=1)
0204 
0205         addParticleGun(
0206             s,
0207             ParticleConfig(num=10, pdg=acts.PdgParticle.eMuon, randomizeCharge=True),
0208             EtaConfig(-4.0, 4.0),
0209             rnd=rng,
0210         )
0211 
0212         trkParamExtractor = acts.examples.ParticleTrackParamExtractor(
0213             level=acts.logging.WARNING,
0214             inputParticles="particles_generated",
0215             outputTrackParameters="params_particles_generated",
0216         )
0217         s.addAlgorithm(trkParamExtractor)
0218 
0219         nav = acts.Navigator(trackingGeometry=geo)
0220         stepper = acts.StraightLineStepper()
0221 
0222         prop = acts.examples.ConcretePropagator(acts.Propagator(stepper, nav))
0223 
0224         alg = acts.examples.PropagationAlgorithm(
0225             level=acts.logging.WARNING,
0226             propagatorImpl=prop,
0227             sterileLogger=False,
0228             inputTrackParameters="params_particles_generated",
0229             outputSummaryCollection="propagation_summary",
0230         )
0231         s.addAlgorithm(alg)
0232 
0233         return s, alg
0234 
0235     return _basic_prop_seq_factory
0236 
0237 
0238 @pytest.fixture
0239 def trk_geo():
0240     detector = acts.examples.GenericDetector()
0241     trackingGeometry = detector.trackingGeometry()
0242     yield trackingGeometry
0243 
0244 
0245 DetectorConfig = namedtuple(
0246     "DetectorConfig",
0247     [
0248         "detector",
0249         "trackingGeometry",
0250         "decorators",
0251         "geometrySelection",
0252         "digiConfigFile",
0253         "name",
0254     ],
0255 )
0256 
0257 
0258 @pytest.fixture(params=["generic", pytest.param("odd", marks=pytest.mark.odd)])
0259 def detector_config(request):
0260     srcdir = Path(__file__).resolve().parent.parent.parent.parent
0261 
0262     if request.param == "generic":
0263         detector = acts.examples.GenericDetector()
0264         trackingGeometry = detector.trackingGeometry()
0265         decorators = detector.contextDecorators()
0266         return DetectorConfig(
0267             detector,
0268             trackingGeometry,
0269             decorators,
0270             geometrySelection=(
0271                 srcdir
0272                 / "Examples/Algorithms/TrackFinding/share/geoSelection-genericDetector.json"
0273             ),
0274             digiConfigFile=(
0275                 srcdir
0276                 / "Examples/Algorithms/Digitization/share/default-smearing-config-generic.json"
0277             ),
0278             name=request.param,
0279         )
0280     elif request.param == "odd":
0281         if not helpers.dd4hepEnabled:
0282             pytest.skip("DD4hep not set up")
0283 
0284         matDeco = acts.IMaterialDecorator.fromFile(
0285             srcdir / "thirdparty/OpenDataDetector/data/odd-material-maps.root",
0286             level=acts.logging.INFO,
0287         )
0288         detector = getOpenDataDetector(matDeco)
0289         trackingGeometry = detector.trackingGeometry()
0290         decorators = detector.contextDecorators()
0291         return DetectorConfig(
0292             detector,
0293             trackingGeometry,
0294             decorators,
0295             digiConfigFile=(
0296                 srcdir
0297                 / "thirdparty/OpenDataDetector/config/odd-digi-smearing-config.json"
0298             ),
0299             geometrySelection=(
0300                 srcdir / "thirdparty/OpenDataDetector/config/odd-seeding-config.json"
0301             ),
0302             name=request.param,
0303         )
0304     else:
0305         raise ValueError(f"Invalid detector {detector}")
0306 
0307 
0308 @pytest.fixture
0309 def ptcl_gun(rng):
0310     def _factory(s):
0311         evGen = acts.examples.EventGenerator(
0312             level=acts.logging.INFO,
0313             generators=[
0314                 acts.examples.EventGenerator.Generator(
0315                     multiplicity=acts.examples.FixedMultiplicityGenerator(n=2),
0316                     vertex=acts.examples.GaussianVertexGenerator(
0317                         stddev=acts.Vector4(0, 0, 0, 0), mean=acts.Vector4(0, 0, 0, 0)
0318                     ),
0319                     particles=acts.examples.ParametricParticleGenerator(
0320                         p=(1 * u.GeV, 10 * u.GeV),
0321                         eta=(-2, 2),
0322                         phi=(0, 360 * u.degree),
0323                         randomizeCharge=True,
0324                         numParticles=2,
0325                     ),
0326                 )
0327             ],
0328             outputParticles="particles_generated",
0329             outputVertices="vertices_input",
0330             randomNumbers=rng,
0331         )
0332 
0333         s.addReader(evGen)
0334 
0335         return evGen
0336 
0337     return _factory
0338 
0339 
0340 @pytest.fixture
0341 def fatras(ptcl_gun, trk_geo, rng):
0342     def _factory(s):
0343         evGen = ptcl_gun(s)
0344 
0345         field = acts.ConstantBField(acts.Vector3(0, 0, 2 * acts.UnitConstants.T))
0346         simAlg = acts.examples.FatrasSimulation(
0347             level=acts.logging.INFO,
0348             inputParticles=evGen.config.outputParticles,
0349             outputParticles="particles_simulated",
0350             outputSimHits="simhits",
0351             randomNumbers=rng,
0352             trackingGeometry=trk_geo,
0353             magneticField=field,
0354             generateHitsOnSensitive=True,
0355             emScattering=False,
0356             emEnergyLossIonisation=False,
0357             emEnergyLossRadiation=False,
0358             emPhotonConversion=False,
0359         )
0360 
0361         s.addAlgorithm(simAlg)
0362 
0363         # Digitization
0364         digiCfg = acts.examples.DigitizationAlgorithm.Config(
0365             digitizationConfigs=acts.examples.readDigiConfigFromJson(
0366                 str(
0367                     Path(__file__).parent.parent.parent.parent
0368                     / "Examples/Algorithms/Digitization/share/default-smearing-config-generic.json"
0369                 )
0370             ),
0371             surfaceByIdentifier=trk_geo.geoIdSurfaceMap(),
0372             randomNumbers=rng,
0373             inputSimHits=simAlg.config.outputSimHits,
0374         )
0375         digiAlg = acts.examples.DigitizationAlgorithm(digiCfg, acts.logging.INFO)
0376 
0377         s.addAlgorithm(digiAlg)
0378 
0379         return evGen, simAlg, digiAlg
0380 
0381     return _factory
0382 
0383 
0384 def _do_material_recording(d: Path):
0385     from material_recording import runMaterialRecording
0386 
0387     s = acts.examples.Sequencer(events=2, numThreads=1)
0388 
0389     with getOpenDataDetector() as detector:
0390         runMaterialRecording(detector, str(d), tracksPerEvent=100, s=s)
0391 
0392         s.run()
0393 
0394 
0395 @pytest.fixture(scope="session")
0396 def material_recording_session():
0397     if not helpers.geant4Enabled:
0398         pytest.skip("Geantino recording requested, but Geant4 is not set up")
0399 
0400     if not helpers.dd4hepEnabled:
0401         pytest.skip("DD4hep recording requested, but DD4hep is not set up")
0402 
0403     with tempfile.TemporaryDirectory() as d:
0404         # explicitly ask for "spawn" as CI failures were observed with "fork"
0405         spawn_context = multiprocessing.get_context("spawn")
0406         p = spawn_context.Process(target=_do_material_recording, args=(d,))
0407         p.start()
0408         p.join()
0409         if p.exitcode != 0:
0410             raise RuntimeError("Failure to exeecute material recording")
0411 
0412         yield Path(d)
0413 
0414 
0415 @pytest.fixture
0416 def material_recording(material_recording_session: Path, tmp_path: Path):
0417     target = tmp_path / material_recording_session.name
0418     shutil.copytree(material_recording_session, target)
0419     yield target