forked from acts-project/acts
-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathconftest.py
More file actions
434 lines (339 loc) · 13.1 KB
/
conftest.py
File metadata and controls
434 lines (339 loc) · 13.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
import multiprocessing
from pathlib import Path
import sys
import os
import tempfile
import shutil
from typing import Dict
import warnings
import pytest_check as check
from collections import namedtuple
import filelock
sys.path = [
str(Path(__file__).parent.parent.parent.parent / "Examples/Scripts/Python/"),
str(Path(__file__).parent),
] + sys.path
import helpers
import helpers.hash_root
import pytest
import acts
import acts.examples
from acts.examples.odd import getOpenDataDetector, getOpenDataDetectorDirectory
from acts.examples.simulation import addParticleGun, EtaConfig, ParticleConfig
try:
import ROOT
ROOT.gSystem.ResetSignals()
except ImportError:
pass
u = acts.UnitConstants
class RootHashAssertionError(AssertionError):
def __init__(
self, file: Path, key: str, exp_hash: str, act_hash: str, *args, **kwargs
):
super().__init__(f"{exp_hash} != {act_hash}", *args, **kwargs)
self.file = file
self.key = key
self.exp_hash = exp_hash
self.act_hash = act_hash
hash_assertion_failures = []
def _parse_hash_file(file: Path) -> Dict[str, str]:
res = {}
for line in file.open():
if line.strip() == "" or line.strip().startswith("#"):
continue
key, h = line.strip().split(":", 1)
res[key.strip()] = h.strip()
return res
@pytest.fixture(scope="session")
def root_file_exp_hashes():
path = Path(
os.environ.get("ROOT_HASH_FILE", Path(__file__).parent / "root_file_hashes.txt")
)
return _parse_hash_file(path)
@pytest.fixture(name="assert_root_hash")
def assert_root_hash(request, root_file_exp_hashes):
if not helpers.doHashChecks:
def fn(*args, **kwargs):
pass
return fn
def fn(key: str, file: Path):
"""
Assertion helper function to check the hashes of root files.
Do NOT use this function directly by importing, rather use it as a pytest fixture
Arguments you need to provide:
key: Explicit lookup key for the expected hash, should be unique per test function
file: Root file to check the expected hash against
"""
__tracebackhide__ = True
gkey = f"{request.node.name}__{key}"
act_hash = helpers.hash_root.hash_root_file(file)
if not gkey in root_file_exp_hashes:
warnings.warn(
f'Hash lookup key "{key}" not found for test "{request.node.name}"'
)
check.equal(act_hash, "[MISSING]")
exc = RootHashAssertionError(file, gkey, "[MISSING]", act_hash)
hash_assertion_failures.append(exc)
else:
refhash = root_file_exp_hashes[gkey]
check.equal(act_hash, refhash)
if act_hash != refhash:
exc = RootHashAssertionError(file, gkey, refhash, act_hash)
hash_assertion_failures.append(exc)
return fn
def pytest_terminal_summary(terminalreporter, exitstatus, config):
docs_url = "https://acts.readthedocs.io/en/latest/examples/python_bindings.html#root-file-hash-regression-checks"
if len(hash_assertion_failures) > 0:
terminalreporter.ensure_newline()
terminalreporter.section(
"RootHashAssertionErrors", sep="-", red=True, bold=True
)
terminalreporter.line(
"The ROOT files produced by tests have changed since the last recorded reference."
)
terminalreporter.line(
"This can be be expected if e.g. the underlying algorithm changed, or it can be a test failure symptom."
)
terminalreporter.line(
"Please manually check the output files listed below and make sure that their content is correct."
)
terminalreporter.line(
"If it is, you can update the test reference file Python/Examples/tests/root_file_hashes.txt with the new hashes below."
)
terminalreporter.line(f"See {docs_url} for more details")
terminalreporter.line("")
for e in hash_assertion_failures:
terminalreporter.line(f"{e.key}: {e.act_hash}")
if not helpers.doHashChecks:
terminalreporter.section("Root file hash checks", sep="-", blue=True, bold=True)
terminalreporter.line(
"NOTE: Root file hash checks were skipped, enable with ROOT_HASH_CHECKS=on"
)
terminalreporter.line(f"See {docs_url} for more details")
def kwargsConstructor(cls, *args, **kwargs):
return cls(*args, **kwargs)
def configKwConstructor(cls, *args, **kwargs):
assert hasattr(cls, "Config")
_kwargs = {}
if "level" in kwargs:
_kwargs["level"] = kwargs.pop("level")
config = cls.Config()
for k, v in kwargs.items():
setattr(config, k, v)
return cls(*args, config=config, **_kwargs)
def configPosConstructor(cls, *args, **kwargs):
assert hasattr(cls, "Config")
_kwargs = {}
if "level" in kwargs:
_kwargs["level"] = kwargs.pop("level")
config = cls.Config()
for k, v in kwargs.items():
setattr(config, k, v)
return cls(config, *args, **_kwargs)
@pytest.fixture(params=[configPosConstructor, configKwConstructor, kwargsConstructor])
def conf_const(request):
return request.param
@pytest.fixture
def rng():
return acts.examples.RandomNumbers(seed=42)
@pytest.fixture
def basic_prop_seq(rng):
def _basic_prop_seq_factory(geo, s=None):
if s is None:
s = acts.examples.Sequencer(events=10, numThreads=1)
addParticleGun(
s,
ParticleConfig(num=10, pdg=acts.PdgParticle.eMuon, randomizeCharge=True),
EtaConfig(-4.0, 4.0),
rnd=rng,
)
trkParamExtractor = acts.examples.ParticleTrackParamExtractor(
level=acts.logging.WARNING,
inputParticles="particles_generated",
outputTrackParameters="params_particles_generated",
)
s.addAlgorithm(trkParamExtractor)
nav = acts.Navigator(trackingGeometry=geo)
stepper = acts.StraightLineStepper()
prop = acts.examples.ConcretePropagator(acts.Propagator(stepper, nav))
alg = acts.examples.PropagationAlgorithm(
level=acts.logging.WARNING,
propagatorImpl=prop,
sterileLogger=False,
inputTrackParameters="params_particles_generated",
outputSummaryCollection="propagation_summary",
)
s.addAlgorithm(alg)
return s, alg
return _basic_prop_seq_factory
@pytest.fixture
def trk_geo():
detector = acts.examples.GenericDetector()
trackingGeometry = detector.trackingGeometry()
yield trackingGeometry
DetectorConfig = namedtuple(
"DetectorConfig",
[
"detector",
"trackingGeometry",
"decorators",
"geometrySelection",
"digiConfigFile",
"name",
],
)
def _get_generic_detector_config(srcdir: Path) -> DetectorConfig:
detector = acts.examples.GenericDetector()
trackingGeometry = detector.trackingGeometry()
decorators = detector.contextDecorators()
return DetectorConfig(
detector,
trackingGeometry,
decorators,
geometrySelection=(srcdir / "Examples/Configs/generic-seeding-config.json"),
digiConfigFile=(srcdir / "Examples/Configs/generic-digi-smearing-config.json"),
name="generic",
)
def _get_odd_detector_config(srcdir: Path) -> DetectorConfig:
if not helpers.dd4hepEnabled:
pytest.skip("DD4hep not set up")
odd_dir = getOpenDataDetectorDirectory()
matDeco = acts.IMaterialDecorator.fromFile(
odd_dir / "data/odd-material-maps.root", level=acts.logging.INFO
)
detector = getOpenDataDetector(matDeco)
trackingGeometry = detector.trackingGeometry()
decorators = detector.contextDecorators()
return DetectorConfig(
detector,
trackingGeometry,
decorators,
digiConfigFile=(srcdir / "Examples/Configs/odd-digi-smearing-config.json"),
geometrySelection=(srcdir / "Examples/Configs/odd-seeding-config.json"),
name="odd",
)
def _srcdir() -> Path:
return Path(__file__).resolve().parent.parent.parent.parent
@pytest.fixture
def generic_detector_config():
"""Detector config for the generic detector only."""
return _get_generic_detector_config(_srcdir())
@pytest.fixture
def odd_detector_config():
"""Detector config for the Open Data Detector only (requires DD4hep)."""
return _get_odd_detector_config(_srcdir())
@pytest.fixture(params=["generic", pytest.param("odd", marks=pytest.mark.odd)])
def detector_config(request):
"""Parametrized fixture that runs tests with both generic and ODD detectors."""
srcdir = _srcdir()
if request.param == "generic":
return _get_generic_detector_config(srcdir)
elif request.param == "odd":
return _get_odd_detector_config(srcdir)
else:
raise ValueError(f"Invalid detector {request.param}")
@pytest.fixture
def ptcl_gun(rng):
def _factory(s):
evGen = acts.examples.EventGenerator(
level=acts.logging.INFO,
generators=[
acts.examples.EventGenerator.Generator(
multiplicity=acts.examples.FixedMultiplicityGenerator(n=2),
vertex=acts.examples.GaussianVertexGenerator(
stddev=acts.Vector4(0, 0, 0, 0), mean=acts.Vector4(0, 0, 0, 0)
),
particles=acts.examples.ParametricParticleGenerator(
p=(1 * u.GeV, 10 * u.GeV),
eta=(-2, 2),
phi=(0, 360 * u.degree),
randomizeCharge=True,
numParticles=2,
),
)
],
outputEvent="particle_gun_event",
randomNumbers=rng,
)
s.addReader(evGen)
hepmc3Converter = acts.examples.hepmc3.HepMC3InputConverter(
level=acts.logging.INFO,
inputEvent=evGen.config.outputEvent,
outputParticles="particles_generated",
outputVertices="vertices_input",
)
s.addAlgorithm(hepmc3Converter)
return evGen, hepmc3Converter
return _factory
@pytest.fixture
def fatras(ptcl_gun, trk_geo, rng):
def _factory(s):
evGen, h3conv = ptcl_gun(s)
field = acts.ConstantBField(acts.Vector3(0, 0, 2 * acts.UnitConstants.T))
simAlg = acts.examples.FatrasSimulation(
level=acts.logging.INFO,
inputParticles=h3conv.config.outputParticles,
outputParticles="particles_simulated",
outputSimHits="simhits",
randomNumbers=rng,
trackingGeometry=trk_geo,
magneticField=field,
generateHitsOnSensitive=True,
emScattering=False,
emEnergyLossIonisation=False,
emEnergyLossRadiation=False,
emPhotonConversion=False,
)
s.addAlgorithm(simAlg)
# Digitization
from acts.examples import json
digiCfg = acts.examples.DigitizationAlgorithm.Config(
digitizationConfigs=acts.examples.json.readDigiConfigFromJson(
str(
Path(__file__).parent.parent.parent.parent
/ "Examples/Configs/generic-digi-smearing-config.json"
)
),
surfaceByIdentifier=trk_geo.geoIdSurfaceMap(),
randomNumbers=rng,
inputSimHits=simAlg.config.outputSimHits,
)
digiAlg = acts.examples.DigitizationAlgorithm(digiCfg, acts.logging.INFO)
s.addAlgorithm(digiAlg)
return evGen, simAlg, digiAlg
return _factory
def _do_material_recording(d: Path):
from material_recording import runMaterialRecording
s = acts.examples.Sequencer(events=2, numThreads=1)
with getOpenDataDetector() as detector:
runMaterialRecording(
detector=detector,
s=s,
tracksPerEvent=1000,
outputFileBase=d / "geant4_material_tracks",
)
s.run()
@pytest.fixture(scope="session")
def material_recording_session(tmp_path_factory):
tmp_dir = tmp_path_factory.getbasetemp().parent
d = Path(tmp_dir) / "material_recording"
if not helpers.geant4Enabled:
pytest.skip("Geantino recording requested, but Geant4 is not set up")
if not helpers.dd4hepEnabled:
pytest.skip("DD4hep recording requested, but DD4hep is not set up")
with filelock.FileLock(str(d) + ".lock"):
if not d.exists():
d.mkdir()
# explicitly ask for "spawn" as CI failures were observed with "fork"
spawn_context = multiprocessing.get_context("spawn")
p = spawn_context.Process(target=_do_material_recording, args=(d,))
p.start()
p.join()
if p.exitcode != 0:
raise RuntimeError("Failure to execute material recording")
return Path(d)
@pytest.fixture
def material_recording(material_recording_session: Path, tmp_path: Path):
target = tmp_path / material_recording_session.name
shutil.copytree(material_recording_session, target)
return target