4"""Utilities to run GENIE tools with per-call environment overrides.
6This module wraps common GENIE command-line tools (``gmkspl``, ``gevgen``,
7``gntpc``) and a ROOT helper to copy flux histograms into simulation files.
10- Safe subprocess execution (no ``shell=True``)
11- Per-call environment overrides (set/unset variables like ``GXMLPATH``)
12- Helpful printing/logging of the exact command line
16* A Python process cannot modify its parent shell environment. Use the
17 ``env_vars=...`` argument to set/unset variables **for the child process**
18 only. If you need to persist environment variables, put them
in your shell
19 rc (e.g. ``~/.bashrc``)
or wrap execution
in ``source/env`` scripts.
21* The path to ``Messenger_laconic.xml``
is auto-detected
from ``$GENIE``
if
22 defined
and readable; otherwise the ``--message-thresholds`` option
is
27>>>
from genie_interface
import generate_genie_events
28>>> generate_genie_events(
29... nevents=1000, nupdg=14, emin=1, emax=100, targetcode=
"1000080160",
30... inputflux=
"/path/flux.root", spline=
"/path/xsec.xml", outputfile=
"out",
31... env_vars={
"GXMLPATH":
"/opt/genie/config",
"GENIE":
"/cvmfs/.../genie"}
36from __future__ import annotations
42from collections.abc import Mapping, Sequence
49 "generate_genie_events",
59PathLike = str | os.PathLike
61logger = logging.getLogger(__name__)
64def _merge_env(env_vars: Mapping[str, str |
None] |
None) -> dict[str, str]:
65 """Merge ``env_vars`` with the current environment.
70 Mapping of KEY -> VALUE to set for the child process. Use ``
None`` to
71 **unset** a variable (remove it)
for the child environment.
76 The environment dict to
pass into ``subprocess.run``.
79 env: dict[str, str] = dict(os.environ)
81 for k, v
in env_vars.items():
91 env_vars: Mapping[str, str |
None] |
None =
None,
94) -> subprocess.CompletedProcess:
95 """Run a command with optional environment overrides.
100 Command and arguments, each
as a separate list element. (No shell.)
102 Optional mapping of KEY -> VALUE to inject into the child environment.
103 Use ``
None`` value to **unset** a variable.
105 If
True (default),
raise ``CalledProcessError`` on non-zero exit.
109 subprocess.CompletedProcess
110 The completed process object.
114 subprocess.CalledProcessError
115 If the command exits
with non-zero status
and ``check=
True``.
119 cmd_str = shlex.join(args)
120 logger.info("Executing: %s", cmd_str)
122 return subprocess.run(args, env=env, check=check)
125def get_1D_flux_name(nupdg: int) -> str:
126 """Return the TH1D flux histogram name for a given neutrino PDG.
128 Naming convention (as used by input flux ROOT files):
129 - Neutrino (PDG > 0): ``
'10' + |pdg|``
130 - Anti-neutrino (PDG < 0): ``
'20' + |pdg|``
134 >>> get_1D_flux_name(12)
136 >>> get_1D_flux_name(-12)
140 x = ROOT.TMath.Abs(nupdg)
141 return (
"10" if nupdg > 0
else "20") + str(x)
145 """Return the TH2D p–pT flux histogram name for a given neutrino PDG.
147 Naming convention (as used by input flux ROOT files):
148 - Neutrino (PDG > 0): ``
'12' + |pdg|``
149 - Anti-neutrino (PDG < 0): ``
'22' + |pdg|``
159 x = ROOT.TMath.Abs(nupdg)
160 return (
"12" if nupdg > 0
else "22") + str(x)
164 nupdglist: Sequence[int],
168 outputfile: PathLike,
169 env_vars: Mapping[str, str |
None] |
None =
None,
170) -> subprocess.CompletedProcess:
171 """Generate GENIE cross-section splines via ``gmkspl``.
176 Iterable of neutrino PDG codes (e.g. ``[12, 14, 16, -12, -14, -16]``).
178 GENIE nuclear target code (e.g. ``'1000080160'``
for O-16).
180 Maximum neutrino energy (GeV).
182 Number of knots
for the spline generation.
184 Path to output XML spline file.
186 Per-call environment overrides (e.g. set ``GXMLPATH``
or ``GENIE``).
190 subprocess.CompletedProcess
193 inputnupdg = ",".join(str(p)
for p
in nupdglist)
207 return _run(args, env_vars=env_vars)
210def generate_genie_events(
218 outputfile: PathLike,
220 process: str |
None =
None,
221 seed: int |
None =
None,
222 irun: int |
None =
None,
223 env_vars: Mapping[str, str |
None] |
None =
None,
224) -> subprocess.CompletedProcess:
225 """Run GENIE ``gevgen`` to generate events.
230 Number of events to generate.
232 Neutrino PDG code (e.g. 12, 14, 16 or negatives
for anti-neutrinos).
234 Energy range
in GeV (inclusive, inclusive
as accepted by GENIE).
236 GENIE nuclear target code.
238 Path to ROOT file
with flux histograms.
240 Path to GENIE cross-section spline XML file.
242 Output prefix/file
for GENIE (
as accepted by ``-o``).
244 Optional GENIE generator list (e.g. ``
'CCDIS'``, ``
'NC'``).
246 Optional RNG seed
for reproducibility.
248 Optional integer run number (``--run``).
250 Per-call environment overrides (e.g. set/unset ``GXMLPATH``, ``GENIE``).
254 subprocess.CompletedProcess
258 If ``GENIE``
is defined
in the child environment, this function will
try
259 to
pass ``--message-thresholds $GENIE/config/Messenger_laconic.xml``
if
260 the file exists; otherwise that option
is omitted.
266 genie_root = env.get(
"GENIE")
268 candidate = os.path.join(genie_root,
"config",
"Messenger_laconic.xml")
269 if os.path.isfile(candidate):
284 args += [
"--message-thresholds", msg_xml]
288 f
"{inputflux},{get_1D_flux_name(nupdg)}",
295 if process
is not None:
296 args += [
"--event-generator-list", process]
298 args += [
"--seed", str(seed)]
300 args += [
"--run", str(irun)]
302 return _run(args, env_vars=env_vars)
307 outputfile: PathLike,
308 env_vars: Mapping[str, str |
None] |
None =
None,
309) -> subprocess.CompletedProcess:
310 """Convert a GENIE ``.ghep.root`` file to GST via ``gntpc``.
315 Input GHEP file (e.g. ``gntp.0.ghep.root``).
317 Output GST file path.
319 Per-call environment overrides.
323 subprocess.CompletedProcess
326 args = ["gntpc",
"-i", str(inputfile),
"-f",
"gst",
"-o", str(outputfile)]
327 return _run(args, env_vars=env_vars)
330def add_hists(inputflux: PathLike, simfile: PathLike, nupdg: int) ->
None:
331 """Copy the 2D p–pT flux histogram from ``inputflux`` into ``simfile``.
336 Path to ROOT file containing TH2D flux histograms.
338 Path to the simulation ROOT file to **update** (opened in 'UPDATE' mode).
340 Neutrino PDG code used to select the histogram (see ``get_2D_flux_name``).
345 If the requested histogram
is not found
in ``inputflux``.
347 If ROOT fails to open either file.
350 infile = ROOT.TFile(str(inputflux), "READ")
351 if not infile
or infile.IsZombie():
352 raise RuntimeError(f
"Failed to open input flux file: {inputflux}")
353 sfile = ROOT.TFile(str(simfile),
"UPDATE")
354 if not sfile
or sfile.IsZombie():
356 raise RuntimeError(f
"Failed to open simulation file for update: {simfile}")
360 obj = infile.Get(hname)
362 raise FileNotFoundError(f
"Histogram '{hname}' not found in {inputflux}")
375 """Parse ``KEY=VAL`` pairs for ``--env`` CLI flag."""
376 out: dict[str, str |
None] = {}
379 raise ValueError(f
"Expected KEY=VALUE, got '{p}'")
380 k, v = p.split(
"=", 1)
385def main(argv: Sequence[str] |
None =
None) -> int:
386 """Entry point for the command-line interface."""
389 parser = argparse.ArgumentParser(prog=
"genie-utils", description=
"GENIE helpers with per-call env overrides")
395 help=
"Set environment variable(s) for the child process (repeatable).",
402 help=
"Unset environment variable(s) for the child process (repeatable).",
404 parser.add_argument(
"-v",
"--verbose", action=
"count", default=0, help=
"Increase verbosity.")
405 sub = parser.add_subparsers(dest=
"cmd", required=
True)
407 p_spl = sub.add_parser(
"splines", help=
"Run gmkspl")
408 p_spl.add_argument(
"-p",
"--pdg", nargs=
"+", type=int, required=
True)
409 p_spl.add_argument(
"-t",
"--target", required=
True)
410 p_spl.add_argument(
"-e",
"--emax", type=float, required=
True)
411 p_spl.add_argument(
"-n",
"--nknots", type=int, required=
True)
412 p_spl.add_argument(
"-o",
"--output", required=
True)
414 p_gen = sub.add_parser(
"generate", help=
"Run gevgen")
415 p_gen.add_argument(
"-n",
"--nevents", type=int, required=
True)
416 p_gen.add_argument(
"-p",
"--pdg", type=int, required=
True)
417 p_gen.add_argument(
"--emin", type=float, required=
True)
418 p_gen.add_argument(
"--emax", type=float, required=
True)
419 p_gen.add_argument(
"-t",
"--target", required=
True)
420 p_gen.add_argument(
"-f",
"--flux", required=
True)
421 p_gen.add_argument(
"-s",
"--spline", required=
True)
422 p_gen.add_argument(
"-o",
"--output", required=
True)
423 p_gen.add_argument(
"--process", default=
None)
424 p_gen.add_argument(
"--seed", type=int, default=
None)
425 p_gen.add_argument(
"--run", type=int, dest=
"irun", default=
None)
427 p_nt = sub.add_parser(
"ntuples", help=
"Run gntpc (ghep -> gst)")
428 p_nt.add_argument(
"-i",
"--input", required=
True)
429 p_nt.add_argument(
"-o",
"--output", required=
True)
431 p_add = sub.add_parser(
"add-hists", help=
"Copy 2D flux hist to sim file")
432 p_add.add_argument(
"-f",
"--flux", required=
True)
433 p_add.add_argument(
"-s",
"--sim", required=
True)
434 p_add.add_argument(
"-p",
"--pdg", type=int, required=
True)
436 args = parser.parse_args(argv)
439 level = logging.WARNING
440 if args.verbose == 1:
442 elif args.verbose >= 2:
443 level = logging.DEBUG
444 logging.basicConfig(level=level, format=
"%(levelname)s %(name)s: %(message)s")
447 env_vars: dict[str, str |
None] = {}
450 for key
in args.unset:
453 if args.cmd ==
"splines":
456 targetcode=args.target,
459 outputfile=args.output,
460 env_vars=env_vars
or None,
462 elif args.cmd ==
"generate":
463 generate_genie_events(
464 nevents=args.nevents,
468 targetcode=args.target,
471 outputfile=args.output,
472 process=args.process,
475 env_vars=env_vars
or None,
477 elif args.cmd ==
"ntuples":
479 inputfile=args.input,
480 outputfile=args.output,
481 env_vars=env_vars
or None,
483 elif args.cmd ==
"add-hists":
484 add_hists(inputflux=args.flux, simfile=args.sim, nupdg=args.pdg)
486 parser.error(
"Unknown command")
491if __name__ ==
"__main__":
int main(Sequence[str]|None argv=None)
dict[str, str|None] _parse_env_kv(Sequence[str] pairs)
subprocess.CompletedProcess _run(Sequence[str] args, Mapping[str, str|None]|None env_vars=None, *bool check=True)
dict[str, str] _merge_env(Mapping[str, str|None]|None env_vars)
str get_2D_flux_name(int nupdg)