FairShip
Loading...
Searching...
No Matches
makeGenieEvents.py
Go to the documentation of this file.
1#!/usr/bin/env python
2# SPDX-License-Identifier: LGPL-3.0-or-later
3# SPDX-FileCopyrightText: Copyright CERN for the benefit of the SHiP Collaboration
4"""Generate GENIE neutrino interaction events from flux histograms.
5
6This "launcher" wraps `genie_utils` (gmkspl/gevgen/gntpc helpers) and adds:
7- Argument parsing with sensible defaults and validation
8- Automatic neutrino/antineutrino scaling based on flux hist sums
9- Structured logging and robust error reporting
10
11Typical use
12-----------
13$ python $FAIRSHIP/macro/makeGenieEvents.py sim \
14 --seed 65539 \
15 --output ./work \
16 --filedir /eos/experiment/ship/data/Mbias/background-prod-2018 \
17 --target iron \
18 --nevents 1000 \
19 --particles 16 -16 \
20 --emin 0.5 --emax 350 \
21 --xsec-file gxspl-FNALsmall.xml \
22 --flux-file pythia8_Geant4_1.0_withCharm_nu.root \
23 --event-generator-list CC
24
25Notes
26-----
27- This tool *does not* modify your parent shell environment.
28- GXMLPATH is automatically set to $FAIRSHIP_ROOT/shipgen/genie_config by default.
29- Use `--gxmlpath` to override the default GXMLPATH if needed.
30
31"""
32
33from __future__ import annotations
34
35import argparse
36import logging
37import os
38from collections.abc import Mapping, Sequence
39from pathlib import Path
40
41import ROOT # type: ignore
42import shipRoot_conf
43from genie_interface import (
44 add_hists,
45 generate_genie_events,
46 get_1D_flux_name,
47 make_ntuples,
48 make_splines,
49)
50
51# ------------------------- Defaults & Mappings --------------------------------
52
53DEFAULT_XSEC_FILE = "gxspl-FNALsmall.xml"
54DEFAULT_FLUX_FILE = "pythia8_Geant4_1.0_withCharm_nu.root"
55
56DEFAULT_SPLINE_DIR = Path(
57 "/eos/experiment/ship/user/edursov/genie/genie_xsec/v3_02_00/NULL/G1802a00000-k250-e1000/data"
58)
59DEFAULT_FILE_DIR = Path("/eos/experiment/ship/data/Mbias/background-prod-2018")
60
61TARGET_CODE = {
62 "iron": "1000260560",
63 "lead": "1000822040[0.014],1000822060[0.241],1000822070[0.221],1000822080[0.524]",
64 "tungsten": "1000741840",
65}
66
67NUPDGLIST = [16, -16, 14, -14, 12, -12]
68
69# ------------------------------ Helpers ---------------------------------------
70
71
72def extract_nu_over_nubar(neutrino_flux: Path, particles: Sequence[int]) -> dict[int, float]:
73 """Compute ν/ν̄ = sum(nu)/sum(nubar) from 1D flux histograms."""
74 f = ROOT.TFile(str(neutrino_flux), "READ")
75 if not f or f.IsZombie():
76 raise FileNotFoundError(f"Cannot open flux file: {neutrino_flux}")
77 try:
78 ratios: dict[int, float] = {}
79 for pdg in particles:
80 fam = abs(int(pdg))
81 h_nu = f.Get(get_1D_flux_name(fam))
82 h_nubar = f.Get(get_1D_flux_name(-fam))
83 if not h_nu or not h_nubar:
84 raise FileNotFoundError(
85 f"Missing hists {get_1D_flux_name(fam)} / {get_1D_flux_name(-fam)} in {neutrino_flux}"
86 )
87 s_nu = float(h_nu.GetSumOfWeights())
88 s_nubar = float(h_nubar.GetSumOfWeights())
89 if s_nubar <= 0.0:
90 raise FileNotFoundError(f"ν̄ sum of weights is zero for family {fam} in {neutrino_flux}")
91 ratios[fam] = s_nu / s_nubar
92 log_output = f"ν/ν̄ ratio for |PDG|={fam}: {ratios[fam]:.4f} (nu={s_nu}, nubar={s_nubar})"
93 logging.info(log_output)
94 return ratios
95 finally:
96 f.Close()
97
98
99def _ensure_dir(path: Path) -> None:
100 path.mkdir(parents=True, exist_ok=True)
101
102
103def _build_env(gxmlpath: Path | None) -> Mapping[str, str | None] | None:
104 """Build per-call env overrides (sets GXMLPATH)."""
105 fairship_root_path = os.getenv("FAIRSHIP_ROOT") or os.getenv("FAIRSHIP")
106 if not fairship_root_path:
107 raise OSError(
108 "FAIRSHIP_ROOT (or FAIRSHIP) environment variable is not set. "
109 "Please run inside the FairShip alienv environment."
110 )
111 val = str(gxmlpath) if gxmlpath else fairship_root_path + "/shipgen/genie_config"
112 return {"GXMLPATH": val}
113
114
115def _target_code(name: str) -> str:
116 code = TARGET_CODE.get(name.lower())
117 if not code:
118 raise ValueError(f"Unknown target '{name}'. Choices: {', '.join(TARGET_CODE)}")
119 return code
120
121
122def _pdg_list(particles: Sequence[int]) -> Sequence[int]:
123 return [int(p) for p in particles]
124
125
126def make_splines_cli(target: str, work_dir: Path, nknots: int, emax: float) -> None:
127 """Build xsec splines with gmkspl and write `xsec_splines.xml` to work_dir."""
128 _ensure_dir(work_dir)
129 output = work_dir / "xsec_splines.xml"
130 make_splines(
131 nupdglist=NUPDGLIST,
132 targetcode=_target_code(target),
133 emax=emax,
134 nknots=nknots,
135 outputfile=output,
136 )
137 logging.info(f"Spline file written: {output}")
138
139
141 *,
142 run: int,
143 nevents: int,
144 particles: Sequence[int],
145 targetcode: str,
146 process: str | None,
147 emin: float,
148 emax: float,
149 neutrino_flux: Path,
150 splines: Path,
151 seed: int,
152 env_vars: Mapping[str, str | None] | None,
153 work_dir: Path,
154 nu_over_nubar: Mapping[int, float],
155) -> None:
156 """Generate events for PDGs, convert to GST, and attach 2D flux."""
157 _ensure_dir(work_dir)
158 pdg_db = ROOT.TDatabasePDG()
159
160 for pdg in particles:
161 fam = abs(pdg)
162 part = pdg_db.GetParticle(int(pdg))
163 pdg_name = part.GetName() if part else str(pdg)
164
165 # anti-neutrinos scaled by flux ratio
166 N = int(nevents) if pdg > 0 else int(nevents / nu_over_nubar[fam])
167
168 out_dir = work_dir / f"genie-{pdg_name}_{N}_events"
169 _ensure_dir(out_dir)
170
171 filename = f"run_{run}_{pdg_name}_{N}_events_{targetcode}_{emin}_{emax}_GeV_{process or 'ALL'}.ghep.root"
172 ghep_path = out_dir / filename
173 gst_path = out_dir / f"genie-{filename.replace('ghep.root', 'gst.root')}"
174
175 logging.info(f"Generating {N} events for PDG {pdg_name} (run={run}) -> {ghep_path}")
176
177 generate_genie_events(
178 nevents=N,
179 nupdg=int(pdg),
180 emin=emin,
181 emax=emax,
182 targetcode=targetcode,
183 inputflux=str(neutrino_flux),
184 spline=str(splines),
185 outputfile=str(ghep_path),
186 process=process,
187 seed=int(seed),
188 irun=int(run),
189 env_vars=env_vars,
190 )
191 make_ntuples(str(ghep_path), str(gst_path), env_vars=env_vars)
192 add_hists(str(neutrino_flux), str(gst_path), int(pdg))
193
194 logging.info(f"Done PDG {pdg_name}: {ghep_path} -> {gst_path}")
195
196
197# ----------------------------- CLI & main -------------------------------------
198
199
200def _build_parser() -> argparse.ArgumentParser:
201 p = argparse.ArgumentParser(description="Run GENIE neutrino simulation")
202 sub = p.add_subparsers(dest="cmd", required=True)
203
204 # sim command
205 ap = sub.add_parser("sim", help="Make GENIE simulation file(s)")
206 ap.add_argument("-s", "--seed", type=int, default=65539, help="RNG seed (default: GENIE 65539)")
207 ap.add_argument(
208 "-o",
209 "--output",
210 dest="work_dir",
211 type=Path,
212 default=Path("./work"),
213 help="Output directory",
214 )
215 ap.add_argument(
216 "-f",
217 "--filedir",
218 dest="filedir",
219 type=Path,
220 default=DEFAULT_FILE_DIR,
221 help="Flux dir",
222 )
223 ap.add_argument(
224 "-c",
225 "--crosssectiondir",
226 dest="splinedir",
227 type=Path,
228 default=DEFAULT_SPLINE_DIR,
229 help="Spline dir",
230 )
231 ap.add_argument(
232 "--xsec-file",
233 type=str,
234 default=DEFAULT_XSEC_FILE,
235 help=f"Spline XML (default: {DEFAULT_XSEC_FILE})",
236 )
237 ap.add_argument(
238 "--flux-file",
239 type=str,
240 default=DEFAULT_FLUX_FILE,
241 help=f"Flux ROOT (default: {DEFAULT_FLUX_FILE})",
242 )
243 ap.add_argument(
244 "-t",
245 "--target",
246 type=str,
247 default="iron",
248 choices=sorted(TARGET_CODE),
249 help="Target material",
250 )
251 ap.add_argument("-n", "--nevents", type=int, default=100, help="Events per neutrino species")
252 ap.add_argument("--emin", type=float, default=0.5, help="Min Eν [GeV]")
253 ap.add_argument("--emax", type=float, default=350.0, help="Max Eν [GeV]")
254 ap.add_argument(
255 "-e",
256 "--event-generator-list",
257 dest="evtype",
258 type=str,
259 default=None,
260 help="GENIE generator list (e.g. CC, CCDIS, CCQE, CharmCCDIS, RES, CCRES, ...)",
261 )
262 ap.add_argument("--gxmlpath", type=Path, default=None, help="Override GXMLPATH")
263 ap.add_argument(
264 "-p",
265 "--particles",
266 nargs="+",
267 type=int,
268 default=NUPDGLIST,
269 help="PDGs (e.g. 16 -16 14 -14 12 -12). Default: all flavors",
270 )
271 ap.add_argument("-r", "--run", type=int, default=1, help="GENIE run number")
272 ap.add_argument(
273 "-v",
274 "--verbose",
275 action="count",
276 default=0,
277 help="Increase verbosity (-v or -vv)",
278 )
279
280 # spline command
281 ap1 = sub.add_parser("spline", help="Make a new cross-section spline (xsec_splines.xml)")
282 ap1.add_argument("-t", "--target", type=str, default="iron", choices=sorted(TARGET_CODE))
283 ap1.add_argument("-o", "--output", dest="work_dir", type=Path, default=Path("./work"))
284 ap1.add_argument("-n", "--nknots", type=int, default=500)
285 ap1.add_argument("--emax", type=float, default=400.0)
286
287 return p
288
289
290def main(argv: Sequence[str] | None = None) -> int:
291 """Launch GENIE event generation or spline creation from CLI args."""
292 # CLI
293 parser = _build_parser()
294 args = parser.parse_args(argv)
295
296 # logging
297 level = logging.WARNING
298 if args.verbose == 1:
299 level = logging.INFO
300 elif args.verbose >= 2:
301 level = logging.DEBUG
302 logging.basicConfig(level=level, format="%(levelname)s %(name)s: %(message)s")
303
304 # ROOT env
306
307 if args.cmd == "spline":
309 target=args.target,
310 work_dir=args.work_dir,
311 nknots=args.nknots,
312 emax=args.emax,
313 )
314 return 0
315
316 # sim
317 env_vars = _build_env(gxmlpath=args.gxmlpath)
318 targetcode = _target_code(args.target)
319
320 splines = (args.splinedir / args.xsec_file).resolve()
321 flux = (args.filedir / args.flux_file).resolve()
322 if not splines.is_file():
323 parser.error(f"Spline file not found: {splines}")
324 if not flux.is_file():
325 parser.error(f"Flux ROOT file not found: {flux}")
326
327 particles = _pdg_list(args.particles)
328 nu_over_nubar = extract_nu_over_nubar(flux, particles)
329
330 logging.info(f"Seed: {args.seed} | Target: {args.target} ({targetcode}) | Process: {args.evtype or 'ALL'}")
332 run=int(args.run),
333 nevents=int(args.nevents),
334 particles=particles,
335 targetcode=targetcode,
336 process=args.evtype,
337 emin=float(args.emin),
338 emax=float(args.emax),
339 neutrino_flux=flux,
340 splines=splines,
341 seed=int(args.seed),
342 env_vars=env_vars,
343 work_dir=args.work_dir.resolve(),
344 nu_over_nubar=nu_over_nubar,
345 )
346 logging.info("Event generation completed successfully.")
347 return 0
348
349
350if __name__ == "__main__":
351 main()
str _target_code(str name)
Mapping[str, str|None]|None _build_env(Path|None gxmlpath)
None make_events(*int run, int nevents, Sequence[int] particles, str targetcode, str|None process, float emin, float emax, Path neutrino_flux, Path splines, int seed, Mapping[str, str|None]|None env_vars, Path work_dir, Mapping[int, float] nu_over_nubar)
None _ensure_dir(Path path)
int main(Sequence[str]|None argv=None)
Sequence[int] _pdg_list(Sequence[int] particles)
dict[int, float] extract_nu_over_nubar(Path neutrino_flux, Sequence[int] particles)
argparse.ArgumentParser _build_parser()
None make_splines_cli(str target, Path work_dir, int nknots, float emax)
None configure(darkphoton=None)