Belle II Software development
herwig.py
1
8
9"""
10Herwig multistep pipeline API for basf2.
11
12Provides add_herwig_continuum_generator() for e+e- -> qqbar continuum event generation
13using a 3-stage batch architecture (KKMC + Herwig + EvtGen).
14
15Example usage:
16 from herwig import add_herwig_continuum_generator
17 add_herwig_continuum_generator(path, finalstate='ccbar', nevents=5000)
18"""
19
20import basf2 as b2
21import hashlib
22import os
23import subprocess
24import tempfile
25from b2test_utils import run_in_subprocess
26
27
28def _run_herwig_batch(work_dir, template_path, lhe_path, seed, nevents,
29 tune_content, shower_mode, herwig_path='Herwig'):
30 """Run Herwig for all N events in a single Herwig subprocess invocation.
31
32 Reads herwig_fragmentation.in, substitutes template variables, and writes batch.in.
33 """
34 runname = 'HerwigFrag_batch'
35 hepmc_file = os.path.join(work_dir, 'all_events.hepmc')
36 cache_file = os.path.join(work_dir, 'all_events.lhe.cache')
37 in_file = os.path.join(work_dir, 'batch.in')
38
39 with open(template_path) as f:
40 content = f.read()
41
42 shower_config = {
43 'off': 'set LHEHandler:CascadeHandler NULL',
44 'minimal': 'set ShowerHandler:MaxTry 1',
45 'full': '# Full shower mode (default)',
46 }[shower_mode]
47
48 content = content.replace('{lhe_file}', lhe_path)
49 content = content.replace('{cache_file}', cache_file)
50 content = content.replace('{seed}', str(seed))
51 content = content.replace('{nevents}', str(nevents))
52 content = content.replace('{print_event}', '0')
53 content = content.replace('{output_file}', hepmc_file)
54 content = content.replace('{runname}', runname)
55 content = content.replace('{shower_config}', shower_config)
56 content = content.replace('{tune_settings}', tune_content)
57
58 with open(in_file, 'w') as f:
59 f.write(content)
60
61 log_file = os.path.join(work_dir, 'herwig_batch.log')
62 with open(log_file, 'w') as log:
63 subprocess.run([herwig_path, 'read', in_file],
64 cwd=work_dir, stdout=log, stderr=log)
65
66 # Non-zero exit may still produce valid HepMC after Herwig exhausts LHE input.
67 if not os.path.exists(hepmc_file) or os.path.getsize(hepmc_file) == 0:
68 raise RuntimeError(
69 f'Herwig batch run produced no HepMC output. See {log_file}')
70
71 return hepmc_file
72
73
74def _read_batch_seed(work_dir):
75 """Read the batch seed from manifest.txt written by HerwigLHEWriterModule."""
76 manifest_path = os.path.join(work_dir, 'manifest.txt')
77 with open(manifest_path) as f:
78 for line in f:
79 line = line.strip()
80 if line.startswith('batch_seed '):
81 return int(line.split()[1])
82 raise RuntimeError(f'batch_seed not found in {manifest_path}')
83
84
85def _stage1_worker(finalstate, work_dir, seed, nevents, stage1_exp, stage1_run,
86 eventType, shower_scale, beamparametersLabel):
87 """Run KKMC + HerwigLHEWriter in a forked subprocess for Stage 1.
88
89 Global tags and BeamParameters are inherited from the parent process unless
90 beamparametersLabel is explicitly set, in which case add_beamparameters() overrides the DB.
91 """
92 b2.set_random_seed(seed)
93 kkmc_input_files = {
94 'uubar': 'data/generators/kkmc/uubar_nohadronization.input.dat',
95 'ddbar': 'data/generators/kkmc/ddbar_nohadronization.input.dat',
96 'ssbar': 'data/generators/kkmc/ssbar_nohadronization.input.dat',
97 'ccbar': 'data/generators/kkmc/ccbar_nohadronization.input.dat',
98 }
99 path = b2.create_path()
100 if beamparametersLabel is not None:
101 from beamparameters import add_beamparameters
102 add_beamparameters(path, beamparametersLabel)
103 path.add_module('EventInfoSetter',
104 expList=stage1_exp, runList=stage1_run, evtNumList=[nevents])
105 path.add_module('KKGenInput',
106 tauinputFile=b2.find_file(kkmc_input_files[finalstate]),
107 KKdefaultFile=b2.find_file('data/generators/kkmc/KK2f_defaults.dat'),
108 taudecaytableFile='',
109 kkmcoutputfilename=os.path.join(work_dir, f'kkmc_{finalstate}.txt'),
110 eventType=eventType)
111 path.add_module('HerwigLHEWriter',
112 WorkDir=work_dir,
113 ShowerScale=shower_scale)
114 b2.process(path)
115
116
117def add_herwig_continuum_generator(path, finalstate, nevents=None, userdecfile='', *,
118 herwigParameterFile='',
119 work_dir=None,
120 seed=None,
121 expList=None,
122 runList=None,
123 beamparametersLabel=None,
124 keep_temp_files=None,
125 shower_scale=11.0,
126 skip_on_failure=True,
127 eventType=''):
128 """
129 Three-stage Herwig continuum generator (multistep pipeline).
130
131 Stage 1 (runs in a forked subprocess): KKMC generates N quark pairs,
132 HerwigLHEWriterModule writes all_events.lhe + per-event KKMC sidecars + manifest.txt.
133
134 Stage 2 (runs via subprocess): one 'Herwig read batch.in' call
135 processes all N LHE events in one Herwig invocation and writes all_events.hepmc.
136
137 Stage 3 (added to path, zero subprocess calls per event): HerwigHepMCFragmentationModule
138 reads all_events.hepmc and sidecar files, reconstructs MCParticles + truth tree,
139 and applies EvtGen. Supports multiprocessing with basf2 -p K.
140
141 Parameters:
142 path (basf2.Path): path for Stage 3 module
143 finalstate (str): uubar, ddbar, ssbar, ccbar
144 nevents (int or None): number of events for Stages 1+2.
145 Default None: auto-derived from the CLI -n/--events flag via
146 Belle2.Environment.Instance().getNumberEventsOverride().
147 Pass an explicit integer for programmatic use (no CLI -n flag).
148 B2FATAL if both are absent or zero.
149 userdecfile (str): EvtGen user decfile; '' means no user decfile
150 herwigParameterFile (str): Herwig tune .dat file; '' uses herwig_belle2.dat
151 work_dir (str): working directory path; None = auto-created via mkdtemp
152 seed (int or None): Seed for Stage 1 (KKMC) and Stage 2 (Herwig batch).
153 Default None: automatically derived from the current basf2 random seed via
154 SHA-256, so a single b2.set_random_seed() call
155 at the top of the steering script seeds all three stages.
156 Pass an explicit integer to override the auto-derived seed.
157 expList (list[int]): experiment number for the Stage 1 EventInfoSetter.
158 Default None -> [0]. The standard globaltag provides Y(4S) BeamParameters
159 for exp=0/run=0, so the default gives Y(4S) generation without any
160 additional setup. The expList must match the steering script's EventInfoSetter
161 to resolve the beam parameters correctly.
162 runList (list[int]): run number for the Stage 1 EventInfoSetter.
163 Default None -> [0]. Same rule as expList; the runList must match the steering script's EventInfoSetter
164 to resolve the beam parameters correctly.
165 beamparametersLabel (str or None): hardcode a beam-energy preset in Stage 1
166 (e.g. 'Y1S', 'Y4S', 'Y4S-off', 'Y5S') via add_beamparameters().
167 Caveat: add_beamparameters() is considered deprecated,
168 e.g., it places the IP at (0, 0, 0).
169 Default None: KKGenInput reads the BeamParameters from the DB.
170 keep_temp_files (bool or None): controls whether WorkDir and all pipeline
171 files (LHE, HepMC, sidecars, manifest, Herwig log) are deleted after
172 the run completes. Deletion is performed by HerwigHepMCFragmentationModule
173 in terminate(), after all events are processed and sidecars are no longer
174 needed.
175 Default None (auto-derive):
176 work_dir=None (auto mkdtemp) -> keep_temp_files resolves to False
177 (temp dir is created for this run only; deleted when done).
178 work_dir='<explicit path>' -> keep_temp_files resolves to True
179 (set keep_temp_files to False explicitly to override).
180 shower_scale (float): parton shower scale ceiling in GeV, written as
181 SCALUP in each LHE <event> block and as the nominal beam energy in
182 the LHE <init> block. Per-event SCALUP = min(shower_scale, sqrt(M2)),
183 where M2 is the Lorentz-invariant mass squared of the quark pair.
184 Note: shower_scale >= sqrt(s) is recommended to ensure SCALUP is never
185 capped below the hard-process scale; violating this truncates the parton
186 shower for events where the quark pair mass exceeds shower_scale.
187 However, shower_scale must not be set below sqrt(s)/2 as the LHE <init> block beam-energy
188 would fall below the actual per-event beam energy, causing Herwig to reject events
189 (empty HepMC file and a RuntimeError in Stage 2).
190 Default 11.0 GeV satisfies up to Y(5S).
191 skip_on_failure (bool): Default True, skip events where Herwig failed
192 eventType (str): event type label, KKMC metadata
193 """
194 if nevents is None:
195 from ROOT import Belle2 as _Belle2
196 nevents = _Belle2.Environment.Instance().getNumberEventsOverride()
197 if nevents <= 0:
198 b2.B2FATAL(
199 'add_herwig_continuum_generator: nevents must be > 0. '
200 'Use -n N on the CLI or pass nevents=N explicitly.'
201 )
202
203 # Derive Stage 1+2 seed from the current basf2 seed when not explicitly provided.
204 # basf2.get_random_seed() returns the seed string set by the
205 # user's b2.set_random_seed() call; SHA-256 maps it to an integer in Herwig's
206 # run. Different gbasf2 jobs have different basf2 seeds and
207 # therefore get different Herwig seeds automatically.
208 if seed is None:
209 seed = int(hashlib.sha256(b2.get_random_seed().encode()).hexdigest(), 16) % 2000000000 + 1
210
211 # Set up working directory; record whether the user specified it explicitly
212 # (needed to derive the keep_temp_files default before work_dir is overwritten)
213 user_specified_work_dir = work_dir is not None
214 if work_dir is None:
215 work_dir = tempfile.mkdtemp(prefix='herwig_pipeline_')
216 else:
217 os.makedirs(work_dir, exist_ok=True)
218
219 # Resolve keep_temp_files: None means "auto" (files kept if user chose the path,
220 # deleted if the directory was auto-created as a one-job temp dir).
221 if keep_temp_files is None:
222 keep_temp_files = user_specified_work_dir
223
224 b2.B2INFO(f'add_herwig_continuum_generator: work_dir = {work_dir}')
225
226 # Validate finalstate early so the error appears before the fork
227 _kkmc_input_files = {
228 'uubar': 'data/generators/kkmc/uubar_nohadronization.input.dat',
229 'ddbar': 'data/generators/kkmc/ddbar_nohadronization.input.dat',
230 'ssbar': 'data/generators/kkmc/ssbar_nohadronization.input.dat',
231 'ccbar': 'data/generators/kkmc/ccbar_nohadronization.input.dat',
232 }
233 if finalstate not in _kkmc_input_files:
234 b2.B2FATAL(f'add_herwig_continuum_generator: finalstate must be uubar/ddbar/ssbar/ccbar, got {finalstate}')
235
236 # Resolve tune file and decay file
237 herwig_config = b2.find_file('data/generators/modules/herwigfragmentation/herwig_belle2.dat')
238 if herwigParameterFile:
239 herwig_config = herwigParameterFile
240 decay_file = b2.find_file('decfiles/dec/DECAY_BELLE2.DEC')
241 decay_user = ''
242 if userdecfile:
243 b2.B2INFO(f'add_herwig_continuum_generator: using custom userdecfile {userdecfile}')
244 decay_user = userdecfile
245
246 # -----------------------------------------------------------------------
247 # Stage 1: run KKMC + HerwigLHEWriter in a forked subprocess.
248 # -----------------------------------------------------------------------
249 b2.B2INFO(f'[Stage 1] Running KKMC to generate {nevents} {finalstate} quark pairs ...')
250
251 stage1_exp = expList if expList is not None else [0]
252 stage1_run = runList if runList is not None else [0]
253
254 exitcode = run_in_subprocess(
255 target=_stage1_worker,
256 finalstate=finalstate,
257 work_dir=work_dir,
258 seed=seed,
259 nevents=nevents,
260 stage1_exp=stage1_exp,
261 stage1_run=stage1_run,
262 eventType=eventType,
263 shower_scale=shower_scale,
264 beamparametersLabel=beamparametersLabel,
265 )
266 if exitcode != 0:
267 raise RuntimeError(
268 f'[Stage 1] HerwigLHEWriter subprocess failed (exit {exitcode})')
269
270 manifest_path = os.path.join(work_dir, 'manifest.txt')
271 if not os.path.exists(manifest_path):
272 raise RuntimeError(f'[Stage 1] manifest.txt not created in {work_dir}')
273 b2.B2INFO('[Stage 1] Complete. manifest.txt written.')
274
275 # -----------------------------------------------------------------------
276 # Stage 2: single Herwig batch invocation (subprocess call for all N events)
277 # -----------------------------------------------------------------------
278 b2.B2INFO(f'[Stage 2] Running Herwig batch read for {nevents} events ...')
279
280 template = b2.find_file(
281 'data/generators/modules/herwigfragmentation/herwig_fragmentation.in')
282 batch_seed = _read_batch_seed(work_dir)
283 with open(herwig_config) as f:
284 tune_content = f.read()
285 lhe_path = os.path.join(work_dir, 'all_events.lhe')
286
287 _run_herwig_batch(work_dir, template, lhe_path,
288 seed=batch_seed, nevents=nevents,
289 tune_content=tune_content, shower_mode='full')
290
291 hepmc_path = os.path.join(work_dir, 'all_events.hepmc')
292 b2.B2INFO(f'[Stage 2] Complete. HepMC written to {hepmc_path}')
293
294 # -----------------------------------------------------------------------
295 # Stage 3: add HerwigHepMCFragmentationModule to the user's path
296 # -----------------------------------------------------------------------
297 fragmentation = path.add_module(
298 'HerwigHepMCFragmentation',
299 WorkDir=work_dir,
300 UseEvtGen=True,
301 DecFile=decay_file,
302 UserDecFile=decay_user,
303 CoherentMixing=True,
304 KeepTempFiles=keep_temp_files,
305 )
306 if skip_on_failure:
307 empty = b2.create_path()
308 fragmentation.if_value('<1', empty)