Back to home page

sPhenix code displayed by LXR

 
 

    


File indexing completed on 2025-08-05 08:10:07

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