Analysis Software
Documentation for sPHENIX simulation software
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Groups Pages
conftest.py
Go to the documentation of this file. Or view the newest version in sPHENIX GitHub for file conftest.py
1 import multiprocessing
2 from pathlib import Path
3 import sys
4 import os
5 import tempfile
6 import shutil
7 from typing import Dict
8 import warnings
9 import pytest_check as check
10 from collections import namedtuple
11 
12 
13 sys.path += [
14  str(Path(__file__).parent.parent.parent.parent / "Examples/Scripts/Python/"),
15  str(Path(__file__).parent),
16 ]
17 
18 
19 import helpers
20 import helpers.hash_root
21 from common import getOpenDataDetectorDirectory
22 from acts.examples.odd import getOpenDataDetector
23 
24 import pytest
25 
26 import acts
27 import acts.examples
28 
29 try:
30  import ROOT
31 
32  ROOT.gSystem.ResetSignals()
33 except ImportError:
34  pass
35 
36 try:
37  if acts.logging.getFailureThreshold() != acts.logging.WARNING:
38  acts.logging.setFailureThreshold(acts.logging.WARNING)
39 except RuntimeError:
40  # Repackage with different error string
41  errtype = (
42  "negative"
43  if acts.logging.getFailureThreshold() < acts.logging.WARNING
44  else "positive"
45  )
46  warnings.warn(
47  "Runtime log failure threshold could not be set. "
48  "Compile-time value is probably set via CMake, i.e. "
49  f"`ACTS_LOG_FAILURE_THRESHOLD={acts.logging.getFailureThreshold().name}` is set, "
50  "or `ACTS_ENABLE_LOG_FAILURE_THRESHOLD=OFF`. "
51  f"The pytest test-suite can produce false-{errtype} results in this configuration"
52  )
53 
54 
55 u = acts.UnitConstants
56 
57 
58 class RootHashAssertionError(AssertionError):
59  def __init__(
60  self, file: Path, key: str, exp_hash: str, act_hash: str, *args, **kwargs
61  ):
62  super().__init__(f"{exp_hash} != {act_hash}", *args, **kwargs)
63  self.file = file
64  self.key = key
65  self.exp_hash = exp_hash
66  self.act_hash = act_hash
67 
68 
69 hash_assertion_failures = []
70 
71 
72 def _parse_hash_file(file: Path) -> Dict[str, str]:
73  res = {}
74  for line in file.open():
75  if line.strip() == "" or line.strip().startswith("#"):
76  continue
77  key, h = line.strip().split(":", 1)
78  res[key.strip()] = h.strip()
79  return res
80 
81 
82 @pytest.fixture(scope="session")
84  path = Path(
85  os.environ.get("ROOT_HASH_FILE", Path(__file__).parent / "root_file_hashes.txt")
86  )
87  return _parse_hash_file(path)
88 
89 
90 @pytest.fixture(name="assert_root_hash")
91 def assert_root_hash(request, root_file_exp_hashes, record_property):
92  if not helpers.doHashChecks:
93 
94  def fn(*args, **kwargs):
95  pass
96 
97  return fn
98 
99  def fn(key: str, file: Path):
100  """
101  Assertion helper function to check the hashes of root files.
102  Do NOT use this function directly by importing, rather use it as a pytest fixture
103 
104  Arguments you need to provide:
105  key: Explicit lookup key for the expected hash, should be unique per test function
106  file: Root file to check the expected hash against
107  """
108  __tracebackhide__ = True
109  gkey = f"{request.node.name}__{key}"
110  act_hash = helpers.hash_root.hash_root_file(file)
111  if not gkey in root_file_exp_hashes:
112  warnings.warn(
113  f'Hash lookup key "{key}" not found for test "{request.node.name}"'
114  )
115  check.equal(act_hash, "[MISSING]")
116  exc = RootHashAssertionError(file, gkey, "[MISSING]", act_hash)
117  hash_assertion_failures.append(exc)
118 
119  else:
120  refhash = root_file_exp_hashes[gkey]
121  check.equal(act_hash, refhash)
122  if act_hash != refhash:
123  exc = RootHashAssertionError(file, gkey, refhash, act_hash)
124  hash_assertion_failures.append(exc)
125 
126  return fn
127 
128 
129 def pytest_terminal_summary(terminalreporter, exitstatus, config):
130  docs_url = "https://acts.readthedocs.io/en/latest/examples/python_bindings.html#root-file-hash-regression-checks"
131  if len(hash_assertion_failures) > 0:
132  terminalreporter.ensure_newline()
133  terminalreporter.section(
134  "RootHashAssertionErrors", sep="-", red=True, bold=True
135  )
136  terminalreporter.line(
137  "The ROOT files produced by tests have changed since the last recorded reference."
138  )
139  terminalreporter.line(
140  "This can be be expected if e.g. the underlying algorithm changed, or it can be a test failure symptom."
141  )
142  terminalreporter.line(
143  "Please manually check the output files listed below and make sure that their content is correct."
144  )
145  terminalreporter.line(
146  "If it is, you can update the test reference file Examples/Python/tests/root_file_hashes.txt with the new hashes below."
147  )
148  terminalreporter.line(f"See {docs_url} for more details")
149  terminalreporter.line("")
150 
151  for e in hash_assertion_failures:
152  terminalreporter.line(f"{e.key}: {e.act_hash}")
153 
154  if not helpers.doHashChecks:
155  terminalreporter.section("Root file has checks", sep="-", blue=True, bold=True)
156  terminalreporter.line(
157  "NOTE: Root file hash checks were skipped, enable with ROOT_HASH_CHECKS=on"
158  )
159  terminalreporter.line(f"See {docs_url} for more details")
160 
161 
162 def kwargsConstructor(cls, *args, **kwargs):
163  return cls(*args, **kwargs)
164 
165 
166 def configKwConstructor(cls, *args, **kwargs):
167  assert hasattr(cls, "Config")
168  _kwargs = {}
169  if "level" in kwargs:
170  _kwargs["level"] = kwargs.pop("level")
171  config = cls.Config()
172  for k, v in kwargs.items():
173  setattr(config, k, v)
174  return cls(*args, config=config, **_kwargs)
175 
176 
177 def configPosConstructor(cls, *args, **kwargs):
178  assert hasattr(cls, "Config")
179  _kwargs = {}
180  if "level" in kwargs:
181  _kwargs["level"] = kwargs.pop("level")
182  config = cls.Config()
183  for k, v in kwargs.items():
184  setattr(config, k, v)
185 
186  return cls(config, *args, **_kwargs)
187 
188 
189 @pytest.fixture(params=[configPosConstructor, configKwConstructor, kwargsConstructor])
190 def conf_const(request):
191  return request.param
192 
193 
194 @pytest.fixture
195 def rng():
196  return acts.examples.RandomNumbers(seed=42)
197 
198 
199 @pytest.fixture
200 def basic_prop_seq(rng):
201  def _basic_prop_seq_factory(geo, s=None):
202  if s is None:
203  s = acts.examples.Sequencer(events=10, numThreads=1)
204 
205  nav = acts.Navigator(trackingGeometry=geo)
206  stepper = acts.StraightLineStepper()
207 
209  alg = acts.examples.PropagationAlgorithm(
210  propagatorImpl=prop,
211  level=acts.logging.INFO,
212  randomNumberSvc=rng,
213  ntests=10,
214  sterileLogger=False,
215  propagationStepCollection="propagation-steps",
216  )
217  s.addAlgorithm(alg)
218  return s, alg
219 
220  return _basic_prop_seq_factory
221 
222 
223 @pytest.fixture
224 def trk_geo(request):
225  detector, geo, contextDecorators = acts.examples.GenericDetector.create()
226  yield geo
227 
228 
229 DetectorConfig = namedtuple(
230  "DetectorConfig",
231  [
232  "detector",
233  "trackingGeometry",
234  "decorators",
235  "geometrySelection",
236  "digiConfigFile",
237  "name",
238  ],
239 )
240 
241 
242 @pytest.fixture(params=["generic", pytest.param("odd", marks=pytest.mark.odd)])
243 def detector_config(request):
244  srcdir = Path(__file__).resolve().parent.parent.parent.parent
245 
246  if request.param == "generic":
247  detector, trackingGeometry, decorators = acts.examples.GenericDetector.create()
248  return DetectorConfig(
249  detector,
250  trackingGeometry,
251  decorators,
252  geometrySelection=(
253  srcdir
254  / "Examples/Algorithms/TrackFinding/share/geoSelection-genericDetector.json"
255  ),
256  digiConfigFile=(
257  srcdir
258  / "Examples/Algorithms/Digitization/share/default-smearing-config-generic.json"
259  ),
260  name=request.param,
261  )
262  elif request.param == "odd":
263  if not helpers.dd4hepEnabled:
264  pytest.skip("DD4hep not set up")
265 
266  matDeco = acts.IMaterialDecorator.fromFile(
267  srcdir / "thirdparty/OpenDataDetector/data/odd-material-maps.root",
268  level=acts.logging.INFO,
269  )
270  detector, trackingGeometry, decorators = getOpenDataDetector(
272  )
273  return DetectorConfig(
274  detector,
275  trackingGeometry,
276  decorators,
277  digiConfigFile=(
278  srcdir
279  / "thirdparty/OpenDataDetector/config/odd-digi-smearing-config.json"
280  ),
281  geometrySelection=(
282  srcdir / "thirdparty/OpenDataDetector/config/odd-seeding-config.json"
283  ),
284  name=request.param,
285  )
286 
287  else:
288  raise ValueError(f"Invalid detector {detector}")
289 
290 
291 @pytest.fixture
292 def ptcl_gun(rng):
293  def _factory(s):
294  evGen = acts.examples.EventGenerator(
295  level=acts.logging.INFO,
296  generators=[
297  acts.examples.EventGenerator.Generator(
298  multiplicity=acts.examples.FixedMultiplicityGenerator(n=2),
299  vertex=acts.examples.GaussianVertexGenerator(
300  stddev=acts.Vector4(0, 0, 0, 0), mean=acts.Vector4(0, 0, 0, 0)
301  ),
302  particles=acts.examples.ParametricParticleGenerator(
303  p=(1 * u.GeV, 10 * u.GeV),
304  eta=(-2, 2),
305  phi=(0, 360 * u.degree),
306  randomizeCharge=True,
307  numParticles=2,
308  ),
309  )
310  ],
311  outputParticles="particles_input",
312  randomNumbers=rng,
313  )
314 
315  s.addReader(evGen)
316 
317  return evGen
318 
319  return _factory
320 
321 
322 @pytest.fixture
323 def fatras(ptcl_gun, trk_geo, rng):
324  def _factory(s):
325  evGen = ptcl_gun(s)
326 
327  field = acts.ConstantBField(acts.Vector3(0, 0, 2 * acts.UnitConstants.T))
328  simAlg = acts.examples.FatrasSimulation(
329  level=acts.logging.INFO,
330  inputParticles=evGen.config.outputParticles,
331  outputParticlesInitial="particles_initial",
332  outputParticlesFinal="particles_final",
333  outputSimHits="simhits",
334  randomNumbers=rng,
335  trackingGeometry=trk_geo,
336  magneticField=field,
337  generateHitsOnSensitive=True,
338  emScattering=False,
339  emEnergyLossIonisation=False,
340  emEnergyLossRadiation=False,
341  emPhotonConversion=False,
342  )
343 
344  s.addAlgorithm(simAlg)
345 
346  # Digitization
347  digiCfg = acts.examples.DigitizationConfig(
348  acts.examples.readDigiConfigFromJson(
349  str(
350  Path(__file__).parent.parent.parent.parent
351  / "Examples/Algorithms/Digitization/share/default-smearing-config-generic.json"
352  )
353  ),
354  trackingGeometry=trk_geo,
355  randomNumbers=rng,
356  inputSimHits=simAlg.config.outputSimHits,
357  )
358  digiAlg = acts.examples.DigitizationAlgorithm(digiCfg, acts.logging.INFO)
359 
360  s.addAlgorithm(digiAlg)
361 
362  return evGen, simAlg, digiAlg
363 
364  return _factory
365 
366 
368  from material_recording import runMaterialRecording
369 
370  detector, trackingGeometry, decorators = getOpenDataDetector(
372  )
373 
374  detectorConstructionFactory = (
375  acts.examples.geant4.dd4hep.DDG4DetectorConstructionFactory(detector)
376  )
377 
378  s = acts.examples.Sequencer(events=2, numThreads=1)
379 
380  runMaterialRecording(detectorConstructionFactory, str(d), tracksPerEvent=100, s=s)
381  s.run()
382 
383 
384 @pytest.fixture(scope="session")
386  if not helpers.geant4Enabled:
387  pytest.skip("Geantino recording requested, but Geant4 is not set up")
388 
389  if not helpers.dd4hepEnabled:
390  pytest.skip("DD4hep recording requested, but DD4hep is not set up")
391 
392  with tempfile.TemporaryDirectory() as d:
393  p = multiprocessing.Process(target=_do_material_recording, args=(d,))
394  p.start()
395  p.join()
396  if p.exitcode != 0:
397  raise RuntimeError("Failure to exeecute material recording")
398 
399  yield Path(d)
400 
401 
402 @pytest.fixture
403 def material_recording(material_recording_session: Path, tmp_path: Path):
404  target = tmp_path / material_recording_session.name
405  shutil.copytree(material_recording_session, target)
406  yield target