ATLAS Offline Software
Loading...
Searching...
No Matches
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
3AthGeneration job.
4
5This module exists because the pepper_kokkos package as shipped in LCG
6needs three things wired up correctly before it will run:
7
81. 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
132. 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
193. 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
224. 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
285. "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
36import gzip
37import os
38import re
39import shutil
40import subprocess
41import tarfile
42
43from AthenaCommon.Logging import logging
44
45log = 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.
76DEFAULT_BATCH_SIZE = 1000
77
78_EVENT_TAG = re.compile(r"^\s*<event[\s>]")
79
80
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
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
118def _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
129def _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
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
146def 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
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
244def 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
_open_events(path, mode="rt")
count_lhe_events(path)
_split_ext(filename)
_resolve_pepper_executable()
_resolve_data_path(exe)
arrange_output(events_file, runArgs)
run_pepper(process, nevents, seed, ecm, output="pepper.lhef", batch_size=None, extra_args=None)