ATLAS Offline Software
Toggle main menu visibility
Loading...
Searching...
No Matches
Generators
Pepper_i
python
PepperConfig.py
Go to the documentation of this file.
1
# Copyright (C) 2002-2026 CERN for the benefit of the ATLAS collaboration
2
"""Driver that invokes the LCG-installed `pepper` binary from inside an
3
AthGeneration job.
4
5
This module exists because the pepper_kokkos package as shipped in LCG
6
needs three things wired up correctly before it will run:
7
8
1. The PEPPER_DATA_PATH env var must point at the process-data
9
directory (the binary's compiled-in path is the build server's
10
scratch dir and doesn't exist on cvmfs). The wrapper sets it
11
automatically.
12
13
2. The output filename's extension governs the format. The pepper
14
binary accepts `.lhef[.gz]`, `.hepmc3[.gz]`, `.hdf5`, and `.debug`.
15
For convenience `.events[.gz]` is accepted too: pepper is invoked
16
with the equivalent `.lhef[.gz]` form, then symlinked back to the
17
requested name.
18
19
3. The binary deadlocks in Kokkos initialisation on CPU-only nodes,
20
waiting for a CUDA device. Jobs MUST run on a GPU-equipped worker.
21
22
4. Pepper's `--batch-size`/`--n-batches` options count *trial* phase-space
23
points, of which only the fraction surviving unweighting is written
24
out (about 15% for ppjj at 13.6 TeV). The wrapper therefore steers
25
the run with `--n-events`, which is the minimum number of non-zero
26
(i.e. written) events, and checks the yield afterwards.
27
28
5. "Pepper" is an LHE generator for the Gen_tf.py skeletons
29
(GeneratorConfig.GenConfigHelpers.lhefGenerators). They therefore
30
expect runArgs.inputGeneratorFile to name an uncompressed
31
`<name>.events` file, which they link to `events.lhe` for the
32
shower and count for the job metadata. arrange_output() provides
33
that, in the same way as MadGraphControl.arrange_output().
34
"""
35
36
import
gzip
37
import
os
38
import
re
39
import
shutil
40
import
subprocess
41
import
tarfile
42
43
from
AthenaCommon.Logging
import
logging
44
45
log = logging.getLogger(
"PepperConfig"
)
46
47
48
# Map of user-facing output extensions to the pepper-CLI extension
49
# we actually invoke pepper with. pepper itself doesn't recognise
50
# .events[.gz], so we transparently swap to .lhef[.gz] and symlink
51
# afterwards.
52
_EXT_MAP = {
53
".events.gz"
:
".lhef.gz"
,
54
".events"
:
".lhef"
,
55
# Pass-through: pepper recognises these natively.
56
".lhef.gz"
:
".lhef.gz"
,
57
".lhef"
:
".lhef"
,
58
".hepmc3.gz"
:
".hepmc3.gz"
,
59
".hepmc3"
:
".hepmc3"
,
60
".hdf5"
:
".hdf5"
,
61
".debug"
:
".debug"
,
62
}
63
64
# Pepper-CLI extensions that are LHE files, i.e. whose events we can
65
# count and hand to the transform.
66
_LHE_EXTS = (
".lhef.gz"
,
".lhef"
)
67
68
# Name under which the events are handed to the transform when the job
69
# has no TXT output (cf. 'tmp_LHE_events' in MadGraphControl).
70
_TMP_LHE_NAME =
"pepper_LHE_events"
71
72
# Number of phase-space points evaluated per batch. This only tunes
73
# throughput (and the granularity of the overshoot, since pepper stops
74
# at the first batch boundary after reaching --n-events); it does not
75
# determine how many events are produced.
76
DEFAULT_BATCH_SIZE = 1000
77
78
_EVENT_TAG = re.compile(
r"^\s*<event[\s>]"
)
79
80
81
def
_resolve_pepper_executable
():
82
"""Return the absolute path to the `pepper` binary, or raise."""
83
exe = os.environ.get(
"PEPPER_EXECUTABLE"
)
or
shutil.which(
"pepper"
)
84
if
not
exe
or
not
os.path.isfile(exe):
85
raise
RuntimeError(
86
"Could not locate the `pepper` executable. "
87
"Either set PEPPER_EXECUTABLE, or ensure that an LCG view "
88
"containing pepper_kokkos is on PATH "
89
"(e.g. /cvmfs/sft.cern.ch/lcg/views/LCG_109a/x86_64-el9-gcc13-opt)."
90
)
91
return
exe
92
93
94
def
_resolve_data_path
(exe):
95
"""Return the directory containing the Pepper process CSVs.
96
97
Tries (in order): PEPPER_DATA_PATH env var; the share/pepper/data
98
directory next to the binary (1.8.x layout); the share/pepper_data
99
directory next to the binary (1.1.x layout).
100
"""
101
candidate = os.environ.get(
"PEPPER_DATA_PATH"
)
102
if
candidate
and
os.path.isfile(os.path.join(candidate,
"2j.csv"
)):
103
return
candidate
104
105
install_root = os.path.dirname(os.path.dirname(os.path.realpath(exe)))
106
for
sub
in
(
"share/pepper/data"
,
"share/pepper_data"
):
107
candidate = os.path.join(install_root, sub)
108
if
os.path.isfile(os.path.join(candidate,
"2j.csv"
)):
109
return
candidate
110
111
raise
RuntimeError(
112
"Could not locate Pepper's process-data directory. "
113
"Set PEPPER_DATA_PATH explicitly to the directory containing "
114
"2j.csv, z0j.csv, etc."
115
)
116
117
118
def
_split_ext
(filename):
119
"""Return (stem, longest matching extension) where the extension
120
is one of the keys in _EXT_MAP. Raises if no match."""
121
for
ext
in
sorted(_EXT_MAP.keys(), key=len, reverse=
True
):
122
if
filename.endswith(ext):
123
return
filename[:-len(ext)], ext
124
raise
ValueError(
125
"Pepper output filename %r must end in one of: %s"
126
% (filename,
", "
.join(sorted(_EXT_MAP.keys()))))
127
128
129
def
_open_events
(path, mode="rt"):
130
"""Open a (possibly gzipped) event file."""
131
if
path.endswith(
".gz"
):
132
return
gzip.open(path, mode)
133
return
open(path, mode)
134
135
136
def
count_lhe_events
(path):
137
"""Return the number of <event> blocks in a (possibly gzipped) LHE file."""
138
n = 0
139
with
_open_events
(path)
as
f:
140
for
line
in
f:
141
if
_EVENT_TAG.match(line):
142
n += 1
143
return
n
144
145
146
def
run_pepper
(process,
147
nevents,
148
seed,
149
ecm,
150
output="pepper.lhef",
151
batch_size=None,
152
extra_args=None):
153
"""Invoke `pepper`, produce an event file, and return its path.
154
155
The default output is an uncompressed LHE file. Use
156
arrange_output() afterwards to hand it to the Gen_tf.py skeleton.
157
158
Parameters
159
----------
160
process : Pepper process shortcut, e.g. "ppjj", "ppz1j", "pptt".
161
nevents : Minimum number of events to be written (passed to
162
pepper as --n-events). Pepper stops at the first batch
163
boundary after reaching it, so the file usually holds
164
slightly more.
165
seed : Random seed (typically runArgs.randomSeed).
166
ecm : Centre-of-mass energy in GeV (typically runArgs.ecmEnergy).
167
output : Output filename. Must end in one of .events[.gz],
168
.lhef[.gz], .hepmc3[.gz], .hdf5, or .debug.
169
batch_size : Phase-space points per batch. Throughput tuning only;
170
defaults to DEFAULT_BATCH_SIZE.
171
extra_args : List of additional CLI args to pass through.
172
173
Returns
174
-------
175
The absolute path of the event file in $PWD.
176
"""
177
nevents = int(nevents)
178
if
nevents < 1:
179
raise
ValueError(
"run_pepper: nevents must be >= 1, got %d"
% nevents)
180
181
exe =
_resolve_pepper_executable
()
182
data =
_resolve_data_path
(exe)
183
184
stem, user_ext =
_split_ext
(output)
185
pepper_ext = _EXT_MAP[user_ext]
186
pepper_output = stem + pepper_ext
187
188
if
batch_size
is
None
:
189
batch_size = DEFAULT_BATCH_SIZE
190
191
cmd = [exe,
192
"--process"
, str(process),
193
"--collision-energy"
, str(ecm),
194
"--seed"
, str(seed),
195
"--batch-size"
, str(batch_size),
196
"--n-events"
, str(nevents),
197
"--output"
, pepper_output]
198
if
extra_args:
199
cmd += list(extra_args)
200
201
env = os.environ.copy()
202
env[
"PEPPER_DATA_PATH"
] = data
203
# Sensible OpenMP defaults so Kokkos doesn't warn at startup.
204
env.setdefault(
"OMP_PROC_BIND"
,
"spread"
)
205
env.setdefault(
"OMP_PLACES"
,
"threads"
)
206
207
# Make sure the yield check below can only ever see this run's events.
208
if
os.path.lexists(pepper_output):
209
os.remove(pepper_output)
210
211
log.info(
"PEPPER_DATA_PATH = %s"
, data)
212
log.info(
"Pepper command: %s"
,
" "
.join(cmd))
213
214
rc = subprocess.call(cmd, env=env)
215
if
rc != 0:
216
raise
RuntimeError(
"pepper exited with rc=%d"
% rc)
217
if
not
os.path.isfile(pepper_output):
218
raise
RuntimeError(
"pepper succeeded but %s is missing"
% pepper_output)
219
220
if
pepper_ext
in
_LHE_EXTS:
221
nwritten =
count_lhe_events
(pepper_output)
222
log.info(
"Pepper wrote %d events to %s (%d requested)"
,
223
nwritten, pepper_output, nevents)
224
if
nwritten < nevents:
225
raise
RuntimeError(
226
"pepper wrote only %d events to %s, but %d were requested"
227
% (nwritten, pepper_output, nevents))
228
else
:
229
log.info(
"Output format %s: number of written events not checked"
,
230
pepper_ext)
231
232
# If user requested a name different from what pepper produces
233
# (typical: user asked for .events.gz; pepper made .lhef.gz),
234
# link the user-facing name to the actual file.
235
if
output != pepper_output:
236
if
os.path.lexists(output):
237
os.remove(output)
238
os.symlink(os.path.basename(pepper_output), output)
239
log.info(
"Linked %s -> %s"
, output, pepper_output)
240
241
return
os.path.abspath(output)
242
243
244
def
arrange_output
(events_file, runArgs):
245
"""Hand an LHE file from run_pepper() over to the Gen_tf.py skeleton.
246
247
Mirrors MadGraphControl.arrange_output():
248
249
* the events are made available uncompressed as `<name>.events`;
250
* if the job has a TXT output (--outputTXTFile=<name>.tar.gz), that
251
file is packed into the tarball;
252
* runArgs.inputGeneratorFile is set so that the skeleton finds
253
`<name>.events`, links it to `events.lhe` (which Pythia8_LHEF.py
254
reads) and counts its events for the job metadata.
255
256
Non-LHE outputs (.hepmc3, .hdf5, .debug) are left alone.
257
258
Parameters
259
----------
260
events_file : LHE file from run_pepper() (.lhef[.gz] or .events[.gz]).
261
runArgs : The transform's runArgs.
262
263
Returns
264
-------
265
The value given to runArgs.inputGeneratorFile, or None if nothing
266
was arranged.
267
"""
268
_, ext =
_split_ext
(events_file)
269
if
_EXT_MAP[ext]
not
in
_LHE_EXTS:
270
if
getattr(runArgs,
"outputTXTFile"
,
None
):
271
raise
RuntimeError(
272
"A TXT output was requested but %s is not an LHE file; "
273
"use an .lhef[.gz] or .events[.gz] output"
% events_file)
274
log.info(
"%s is not an LHE file: not handed to the transform"
,
275
events_file)
276
return
None
277
278
output_txt = getattr(runArgs,
"outputTXTFile"
,
None
)
279
output_ds = output_txt
if
output_txt
else
_TMP_LHE_NAME
280
lhe = output_ds.split(
".tar.gz"
)[0] +
".events"
281
282
# Uncompressed <name>.events: a link if pepper's file is already
283
# plain text, an unpacked copy otherwise.
284
if
os.path.realpath(lhe) != os.path.realpath(events_file):
285
if
os.path.lexists(lhe):
286
os.remove(lhe)
287
if
events_file.endswith(
".gz"
):
288
with
gzip.open(events_file,
"rb"
)
as
src, open(lhe,
"wb"
)
as
dst:
289
shutil.copyfileobj(src, dst)
290
else
:
291
os.symlink(os.path.realpath(events_file), lhe)
292
293
if
output_txt:
294
with
tarfile.open(output_txt,
"w:gz"
, dereference=
True
)
as
tar:
295
tar.add(lhe, arcname=os.path.basename(lhe))
296
log.info(
"Packed %s into %s"
, lhe, output_txt)
297
# As in MadGraphControl: the skeleton derives the name of the
298
# .events file from this.
299
input_file = output_txt.split(
".TXT"
)[0]
300
else
:
301
input_file = output_ds
302
303
log.info(
"Setting runArgs.inputGeneratorFile to %s (events in %s)"
,
304
input_file, lhe)
305
runArgs.inputGeneratorFile = input_file
306
return
input_file
PepperConfig._open_events
_open_events(path, mode="rt")
Definition
PepperConfig.py:129
PepperConfig.count_lhe_events
count_lhe_events(path)
Definition
PepperConfig.py:136
PepperConfig._split_ext
_split_ext(filename)
Definition
PepperConfig.py:118
PepperConfig._resolve_pepper_executable
_resolve_pepper_executable()
Definition
PepperConfig.py:81
PepperConfig._resolve_data_path
_resolve_data_path(exe)
Definition
PepperConfig.py:94
PepperConfig.arrange_output
arrange_output(events_file, runArgs)
Definition
PepperConfig.py:244
PepperConfig.run_pepper
run_pepper(process, nevents, seed, ecm, output="pepper.lhef", batch_size=None, extra_args=None)
Definition
PepperConfig.py:152
Generated on
for ATLAS Offline Software by
1.17.0