ATLAS Offline Software
Loading...
Searching...
No Matches
powheg_control.py
Go to the documentation of this file.
1# Copyright (C) 2002-2024 CERN for the benefit of the ATLAS collaboration
2
3import collections
4import os
5from . import processes
6from . import Logging
7from .decorators import timed
8from .algorithms import Scheduler
9from .utility import HeartbeatTimer
10
11
12logger = Logging.logging.getLogger("PowhegControl")
13
15 '''
16 Helper function to format QCD scale value
17 to conform to the ATLAS weight variation
18 naming scheme if possible.
19 Given a scale factor as a float, it returns
20 a string that is:
21 - '0.5'
22 - '1'
23 - '2'
24 - or the float rounded to two digits after the decimal point if it is not equal to one of the above
25 '''
26 try:
27 return {0.5 : '0.5', 1.0 : '1', 2.0 : '2'}[factor]
28 except KeyError:
29 return '{0:.2f}'.format(factor)
30
31
32
33class PowhegControl(object):
34 """! Provides PowhegConfig objects which are user-configurable in the jobOptions.
35
36 All subprocesses inherit from this class.
37
38 @author James Robinson <james.robinson@cern.ch>
39 """
40
41 def __init__(self, process_name, run_args=None, run_opts=None):
42 """! Constructor.
43
44 @param run_args Generate_tf run arguments
45 @param run_opts athena run options
46 """
47
48 try:
49 self.__run_directory = os.environ.get("PWD", os.getcwd())
50 except KeyError:
51 self.__run_directory = os.getenv("PWD", os.getcwd())
52
53
54 try:
55 pythonpath = os.environ.get("PYTHONPATH")
56 except KeyError:
57 pythonpath = os.getenv("PYTHONPATH")
58
59 if pythonpath:
60 os.environ["PYTHONPATH"] = pythonpath + ":" + self.__run_directory
61 else:
62 os.environ["PYTHONPATH"] = self.__run_directory
63
64
65 self.__output_LHE_file = "PowhegOTF._1.events"
66
67
68 self.__event_weight_groups = collections.OrderedDict()
69
70
72
73 # Load run arguments
74 process_kwargs = {"cores": int(os.environ.pop("ATHENA_CORE_NUMBER", 1))}
75 if run_args is None:
76 logger.warning("No run arguments found! Using defaults.")
77 else:
78 # Read values from run_args
79 if hasattr(run_args, "ecmEnergy"):
80 process_kwargs["beam_energy"] = 0.5 * run_args.ecmEnergy
81 if hasattr(run_args, "maxEvents") and run_args.maxEvents > 0:
82 if hasattr(run_args, "outputEVNTFile") or hasattr(run_args, "outputYODAFile"):
83 process_kwargs["nEvents"] = int(1.1 * run_args.maxEvents + 0.5)
84 else:# default nEvents value is maxEvents for lhe-only production
85 process_kwargs["nEvents"] = run_args.maxEvents
86 else:#default is 10k events if no --maxEvents was set
87 if hasattr(run_args, "outputEVNTFile") or hasattr(run_args, "outputYODAFile"):
88 process_kwargs["nEvents"] = 11000 # 10% safety factor if we shower the events
89 else:
90 process_kwargs["nEvents"] = 10000 # no safety factor for lhe-only production
91 if hasattr(run_args, "randomSeed"):
92 process_kwargs["random_seed"] = run_args.randomSeed
93 if hasattr(run_args, "outputTXTFile"):
94 for tarball_suffix in [x for x in [".tar.gz", ".tgz"] if x in run_args.outputTXTFile]:
95
96 self.__output_LHE_file = run_args.outputTXTFile.split(tarball_suffix)[0] + ".events"
97 self.scheduler.add("output tarball preparer", self.__output_LHE_file, run_args.outputTXTFile)
98 # Set inputGeneratorFile to match output events file - otherwise Generate_tf check will fail
99 run_args.inputGeneratorFile = self.__output_LHE_file
100
101 # Load correct process
102 self.process = getattr(processes.powheg, process_name)(os.environ["POWHEGPATH"].replace("POWHEG-BOX", ""), **process_kwargs)
103
104 # check if pre-made integration grids will be used or not
105 self.process.check_using_integration_files()
106
107 # Expose all keyword parameters as attributes of this config object
108 for parameter in self.process.parameters:
109 if parameter.is_visible:
110 setattr(self, parameter.name, parameter.value)
111
112 # Expose all external parameters as attributes of this config object
113 for external in self.process.externals.values():
114 for parameter in external.parameters:
115 if parameter.is_visible:
116 setattr(self, parameter.name, parameter.value)
117
118 # Add appropriate directory cleanup for this process
119 self.scheduler.add("directory cleaner", self.process)
120
121 # Schedule correct output file naming
122 self.scheduler.add("output file renamer", self.__output_LHE_file)
123
124 # Enable parallel mode if AthenaMP mode is enabled. Also force multistage mode for RES.
125 if self.process.cores == 1 and self.process.powheg_version != "RES":
126 self.scheduler.add("singlecore", self.process)
127 else:
128 if self.process.cores == 1:
129 logger.info("Configuring this POWHEG-BOX-RES process to run in multistage mode")
130 else:
131 # Try to modify the transform opts to suppress athenaMP mode
132 logger.info("This job is running with an athenaMP-like whole-node setup, requesting {} cores".format(self.process.cores))
133 if hasattr(run_opts, "nprocs"):
134 logger.info("Re-configuring to keep athena running serially while parallelising POWHEG-BOX generation.")
135 run_opts.nprocs = 0
136 else:
137 logger.warning("Running in multicore mode but no 'nprocs' option was provided!")
138 self.scheduler.add("multicore", self.process)
139 self.scheduler.add("merge output", self.process.cores, self.nEvents)
140 list(self.process.parameters_by_name("manyseeds"))[0].value = 1
141
142 # Freeze the interface so that no new attributes can be added
144
145 # Print executable being used
146 logger.info("Configured for event generation with: {}".format(self.process.executable))
147
148 def generate(self, create_run_card_only=False, save_integration_grids=True, use_external_run_card=False, remove_oldStyle_rwt_comments=False, is_bb4l_semilep=False):
149 """! Run normal event generation.
150
151 @param create_run_card_only Only generate the run card.
152 @param save_integration_grids Save the integration grids for future reuse.
153 @param use_external_run_card Use a user-provided Powheg run card (powheg.input).
154 @param remove_oldStyle_rwt_comments Removes old-style '#rwgt', '#pdf', '#new weight', '#matching', and ' #Random' comments in lhe files (kept by default despite using xml reweighting).
155 """
156 # we are now always using xml reweighting - set this to False if you still want the old style
157 self.process.use_XML_reweighting = True
158
159 self.process.remove_oldStyle_rwt_comments = remove_oldStyle_rwt_comments
160
161 # Schedule integration gridpack creator if requested
162 if save_integration_grids:
163 self.scheduler.add("integration gridpack creator", self.process)
164
165 # Run appropriate generation functions
166 if not use_external_run_card:
167 self._generate_run_card()
168 else:
169 logger.warning("Using native Powheg run card (must be located at './powheg.input' in order for Powheg to find it!) to configure event generation, instead of PowhegControl configuration interface")
170 if not create_run_card_only:
171 self._generate_events(is_bb4l_semilep)
172
174 """! Initialise runcard with appropriate options."""
175 # Check that event generation is correctly set up
176 if (hasattr(self, "bornsuppfact") and self.bornsuppfact > 0.0) and (hasattr(self, "bornktmin") and self.bornktmin <= 0.0):
177 logger.warning("These settings: bornsuppfact = {} and bornktmin = {} cannot be used to generate events!".format(self.bornsuppfact, self.bornktmin))
178 logger.warning("Only fixed-order distributions can be produced with these settings!")
179
180 # Scale-down number of events produced in each run if running in multicore mode
181 if self.process.cores > 1:
182 logger.info("Preparing to parallelise: running with {} jobs".format(self.process.cores))
183 self.process.prepare_to_parallelise(self.process.cores)
184
185 # Validate any parameters which need validation/processing
186 self.process.validate_parameters()
187
188 # Construct sorted list of configurable parameters for users - including those from external processes
189 parameters_unsorted = list(self.process.parameters)
190 for external in self.process.externals.values():
191 parameters_unsorted.extend(external.parameters)
192 parameters_sorted = [x[1] for x in sorted(dict((p.name.lower(), p) for p in parameters_unsorted).items(), key=lambda x: x[0])]
193
194 # Print sorted list of configurable parameters
195 logger.info("=========================================================================================================")
196 logger.info("| User configurable parameters for this process |")
197 logger.info("=========================================================================================================")
198 logger.info("| Option name | ATLAS default | Description |")
199 logger.info("=========================================================================================================")
200 for parameter in [p for p in parameters_sorted if p.is_visible]:
201 _default_value = "default" if (parameter.default_value is None or parameter.default_value == "") else str(parameter.default_value)
202 logger.info("| {:<25} | {:>19} | {}".format(parameter.name, _default_value, parameter.description))
203 logger.info("========================================================================================================")
204
205 # Print list of parameters that have been changed by the user
206 parameters_changed = [p for p in parameters_sorted if p.value is not p.default_value]
207 logger.info("In these jobOptions {} parameter(s) have been changed from their default value:".format(len(parameters_changed)))
208 for idx, parameter in enumerate(parameters_changed):
209 logger.info(" {:<3} {:<19} {:>15} => {}".format("{})".format(idx + 1), "{}:".format(parameter.name), str(parameter.default_value), parameter.value))
210
211 # Check for parameters which can result in non-equal event weights being used
212 event_weight_options = []
213
214 # Write out final runcard
215 run_card_path = "{}/powheg.input".format(self.__run_directory)
216 logger.info("Writing POWHEG-BOX runcard to {}".format(run_card_path))
217 with open(run_card_path, "w") as f_runcard:
218 for parameter in sorted(self.process.parameters, key=lambda p: p.keyword.lower()):
219 if parameter.name == "bornsuppfact" and parameter.value > 0:
220 event_weight_options.append(("Born-level suppression", "magnitude"))
221 if parameter.name == "withnegweights" and parameter.value > 0:
222 event_weight_options.append(("negative event weights", "sign"))
223 # PDF variations
224 if parameter.name == "PDF" and isinstance(parameter.value, collections.abc.Iterable):
225 if "PDF_variation" not in self.__event_weight_groups.keys(): # skip if this group already exists
226 if len(parameter.value) < 2:
227 logger.error("Use 'PowhegConfig.PDF = {0}' rather than 'PowhegConfig.PDF = [{0}]'".format(parameter.value[0] if len(parameter.value) > 0 else "<value>"))
228 raise TypeError("Use 'PowhegConfig.PDF = {0}' rather than 'PowhegConfig.PDF = [{0}]'".format(parameter.value[0] if len(parameter.value) > 0 else "<value>"))
229 self.define_event_weight_group("PDF_variation", ["PDF"], combination_method="hessian")
230 for PDF in map(int, parameter.value[1:]):
231 self.add_weight_to_group("PDF_variation", "MUR1_MUF1_PDF{:d}".format(PDF), [PDF])
232 # Scale variations
233 if parameter.name in ["mu_F", "mu_R"] and isinstance(parameter.value, collections.abc.Iterable):
234 pdfs = list(self.process.parameters_by_name("PDF"))[0].value
235 nominal_pdf = pdfs if isinstance(pdfs, int) or isinstance(pdfs, str) else pdfs[0]
236 if "scale_variation" not in self.__event_weight_groups.keys(): # skip if this group already exists
237 mu_Rs = list(self.process.parameters_by_name("mu_R"))[0].value
238 mu_Fs = list(self.process.parameters_by_name("mu_F"))[0].value
239 if len(parameter.value) < 2:
240 logger.error("Use 'PowhegConfig.{1} = {0}' rather than 'PowhegConfig.{1} = [{0}]'".format(parameter.value[0] if len(parameter.value) > 0 else "<value>", parameter.name))
241 raise TypeError("Use 'PowhegConfig.{1} = {0}' rather than 'PowhegConfig.{1} = [{0}]'".format(parameter.value[0] if len(parameter.value) > 0 else "<value>", parameter.name))
242 if not isinstance(mu_Rs, collections.abc.Iterable) or not isinstance(mu_Fs, collections.abc.Iterable) or len(mu_Rs) is not len(mu_Fs):
243 logger.error("Number of mu_R and mu_F variations must be the same.")
244 raise ValueError("Number of mu_R and mu_F variations must be the same.")
245 self.define_event_weight_group("scale_variation", ["mu_R", "mu_F"], combination_method="envelope")
246 for mu_R, mu_F in zip(map(float, mu_Rs[1:]), map(float, mu_Fs[1:])):
247 mu_R_text = _format_QCD_scale_text(mu_R)
248 mu_F_text = _format_QCD_scale_text(mu_F)
249 self.add_weight_to_group("scale_variation", "MUR{mur}_MUF{muf}_PDF{nominal_pdf}".format(mur=mu_R_text, muf=mu_F_text, nominal_pdf=nominal_pdf), [mu_R, mu_F])
250 f_runcard.write("{}\n".format(parameter))
251
252 # Schedule cross-section_calculator
253 if len(event_weight_options) > 0:
254 self.scheduler.add("cross section calculator")
255 logger.warning("POWHEG-BOX has been configured to run with {}".format(" and ".join([x[0] for x in event_weight_options])))
256 logger.warning("This means that event weights will vary in {}.".format(" and ".join([x[1] for x in event_weight_options])))
257 logger.warning("The cross-section passed to the parton shower will be inaccurate.")
258 logger.warning("Please use the cross-section printed in the log file before showering begins.")
259
260 # Schedule reweighting if more than the nominal weight is requested, or if for_reweighting is set to 1
261 doReweighting = False
262 if len(self.__event_weight_groups) >= 1:
263 doReweighting = True
264 elif len(list(self.process.parameters_by_keyword("for_reweighting"))) == 1:
265 if self.process.parameters_by_keyword("for_reweighting")[0].value == 1:
266 logger.warning ("No more than the nominal weight is requested, but for_reweighting is set to 1")
267 logger.warning ("Therefore, reweighting is enabled anyway, otherwise virtual corrections wouldn't be included")
268 doReweighting = True
269 if doReweighting:
270 # Change the order so that scale comes first and user-defined is last
271 __ordered_event_weight_groups_list = []
272 for __key in ["scale_variation", "PDF_variation"]:
273 if __key in self.__event_weight_groups.keys():
274 __ordered_event_weight_groups_list.append((__key, self.__event_weight_groups.pop(__key)))
275 for __item in self.__event_weight_groups.items():
276 __ordered_event_weight_groups_list.append(__item)
277 self.__event_weight_groups = collections.OrderedDict(__ordered_event_weight_groups_list)
278 for group_name, event_weight_group in self.__event_weight_groups.items():
279 _n_weights = len(event_weight_group) - 3 # there are always three entries: parameter_names, combination_method and keywords
280 # Sanitise weight groups, removing any with no entries
281 if _n_weights <= 0:
282 logger.warning("Ignoring weight group '{}' as it does not have any variations defined. Check your jobOptions!".format(group_name))
283 del self.__event_weight_groups[group_name] # this is allowed because items() makes a temporary copy of the dictionary
284 # Otherwise print weight group information for the user
285 else:
286 logger.info("Adding new weight group '{}' which contains {} weights defined by varying {} parameters".format(group_name, _n_weights, len(event_weight_group["parameter_names"])))
287 for parameter_name in event_weight_group["parameter_names"]:
288 logger.info("... {}".format(parameter_name))
289 if not self.process.has_parameter(parameter_name):
290 logger.warning("Parameter '{}' does not exist for this process!".format(parameter_name))
291 raise ValueError("Parameter '{}' does not exist for this process!".format(parameter_name))
292 # Add reweighting to scheduler
293 self.scheduler.add("reweighter", self.process, self.__event_weight_groups)
294
295 @timed("Powheg LHE event generation")
296 def _generate_events(self,is_bb4l_semilep=False):
297 """! Generate events according to the scheduler."""
298 # Setup heartbeat thread
299 heartbeat = HeartbeatTimer(600., "{}/eventLoopHeartBeat.txt".format(self.__run_directory))
300 heartbeat.setName("heartbeat thread")
301 heartbeat.daemon = True # Allow program to exit if this is the only live thread
302 heartbeat.start()
303
304 # Print executable being used
305 logger.info("Using executable: {}".format(self.process.executable))
306
307 # Additional arguments needed by some algorithms
308 extra_args = {"quark colour fixer": [self.process]}
309
310 # Schedule external processes (eg. MadSpin, PHOTOS, NNLO reweighting)
311 for algorithm, external in self.process.externals.items():
312 if external.needs_scheduling(self.process):
313 self.scheduler.add(algorithm, external, *extra_args.get(algorithm, []))
314
315 # Schedule additional algorithms (eg. quark colour fixer)
316 for algorithm in self.process.algorithms:
317 self.scheduler.add(algorithm, *extra_args.get(algorithm, []))
318
319 updated_xwgtup = False
320 if len(list(self.process.parameters_by_keyword("ubexcess_correct"))) == 1:
321 if list(self.process.parameters_by_keyword("ubexcess_correct"))[0].value == 1:
322 algorithm = "LHE ubexcess_correct weight updater"
323 self.scheduler.add(algorithm, *extra_args.get(algorithm, []))
324 algorithm = "LHE file nominal weight updater"
325 self.scheduler.add(algorithm, *extra_args.get(algorithm, []))
326 logger.info ("Since parameter ubexcess_correct was set to 1, event weights need to be modified by correction factor which is calculated during event generation.")
327 logger.info ("Will also run LHE file nominal weight updater so that XWGTUP value is updated with value of reweighted nominal weight.")
328 updated_xwgtup = True
329 if not updated_xwgtup:
330 if len(list(self.process.parameters_by_keyword("for_reweighting"))) == 1:
331 if list(self.process.parameters_by_keyword("for_reweighting"))[0].value == 1:
332 algorithm = "LHE file nominal weight updater"
333 self.scheduler.add(algorithm, *extra_args.get(algorithm, []))
334 logger.info ("Since parameter for_reweighting was set to 1, virtual corrections are added at the reweighting stage only.")
335 logger.info ("Will run LHE file nominal weight updater so that XWGTUP value is updated with value of reweighted nominal weight.")
336
337 # Output the schedule
338 self.scheduler.print_structure()
339
340 # Run pre-processing
341 if not is_bb4l_semilep:
342 self.scheduler.run_preprocessors()
343
344 # Run event generation
345 self.scheduler.run_generators()
346
347 # Run post-processing
348 self.scheduler.run_postprocessors()
349
350 # Kill heartbeat thread
351 heartbeat.cancel()
352
353 def define_event_weight_group(self, group_name, parameters_to_vary, combination_method="none"):
354 """! Add a new named group of event weights.
355
356 @exceptions ValueError Raise a ValueError if reweighting is not supported.
357
358 @param group_name Name of the group of weights.
359 @param parameters_to_vary Names of the parameters to vary.
360 @param combination_method Method for combining the weights.
361 """
362 if self.process.has_parameter("run_mode") and self.process.parameters_by_keyword("run_mode")[0].value != 1 and hasattr(self.process, "reweight_for_MiNNLO") and self.process.reweight_for_MiNNLO:
363 parameters_to_vary.append("run_mode")
364 if not self.process.is_reweightable:
365 logger.warning("Additional event weights cannot be added by this process! Remove reweighting lines from the jobOptions.")
366 raise ValueError("Additional event weights cannot be added by this process! Remove reweighting lines from the jobOptions.")
367 self.__event_weight_groups[group_name] = collections.OrderedDict()
368 self.__event_weight_groups[group_name]["parameter_names"] = parameters_to_vary
369 self.__event_weight_groups[group_name]["combination_method"] = combination_method
370 self.__event_weight_groups[group_name]["keywords"] = [[p.keyword for p in self.process.parameters_by_name(parameter)] for parameter in parameters_to_vary]
371
372 def add_weight_to_group(self, group_name, weight_name, parameter_values):
373 """! Add a new event weight to an existing group.
374
375 @param group_name Name of the group of weights that this weight belongs to.
376 @param weight_name Name of this event weight.
377 @param parameter_values Values of the parameters.
378 """
379 if group_name not in self.__event_weight_groups.keys():
380 raise ValueError("Weight group '{}' has not been defined.".format(group_name))
381 n_expected = len(self.__event_weight_groups[group_name]["parameter_names"])
382 if self.process.has_parameter("run_mode") and self.process.parameters_by_keyword("run_mode")[0].value != 1 and hasattr(self.process, "reweight_for_MiNNLO") and self.process.reweight_for_MiNNLO:
383 parameter_values.append(1)
384 if len(parameter_values) is not n_expected:
385 raise ValueError("Expected {} parameter values but only got {}".format(n_expected, len(parameter_values)))
386 self.__event_weight_groups[group_name][weight_name] = []
387 for parameter_name, value in zip(self.__event_weight_groups[group_name]["parameter_names"], parameter_values):
388 self.__event_weight_groups[group_name][weight_name].append((parameter_name, value))
389
390
391 def __setattr__(self, key, value):
392 """! Override default attribute setting to stop users setting non-existent attributes.
393
394 @exceptions AttributeError Raise an AttributeError if the interface is frozen
395
396 @param key Attribute name.
397 @param value Value to set the attribute to.
398 """
399 # If this is a known parameter then keep the values in sync
400 if hasattr(self, "process"):
401 # ... for parameters of the process
402 for parameter in self.process.parameters_by_name(key):
403 parameter.ensure_default()
404 parameter.value = value
405 # ... and for parameters of any external processes
406 for external in self.process.externals.values():
407 for parameter in external.parameters_by_name(key):
408 parameter.ensure_default()
409 parameter.value = value
410 # If the interface is frozen then raise an attribute error
411 if hasattr(self, "interface_frozen") and self.interface_frozen:
412 if not hasattr(self, key):
413 raise AttributeError("This POWHEG-BOX process has no option '{}'".format(key))
414 object.__setattr__(self, key, value)
415
416 def set_parameter_stage(self, parameterStageDict = {}):
417 logger.info("Setting parameters for the stages : {0}".format(parameterStageDict))
418
419 self.process.parameterStageDict = parameterStageDict
STL class.
Schedule algorithms in appropriate order.
Definition scheduler.py:14
Provides PowhegConfig objects which are user-configurable in the jobOptions.
__event_weight_groups
Dictionary of named groups of event weights.
define_event_weight_group(self, group_name, parameters_to_vary, combination_method="none")
Add a new named group of event weights.
set_parameter_stage(self, parameterStageDict={})
_generate_run_card(self)
Initialise runcard with appropriate options.
add_weight_to_group(self, group_name, weight_name, parameter_values)
Add a new event weight to an existing group.
__setattr__(self, key, value)
Override default attribute setting to stop users setting non-existent attributes.
__init__(self, process_name, run_args=None, run_opts=None)
Constructor.
str __output_LHE_file
Name of output LHE file used by Generate_tf for showering.
scheduler
Scheduler to determine algorithm ordering.
_generate_events(self, is_bb4l_semilep=False)
Generate events according to the scheduler.
Recurring heartbeat that emits a message to console and to file.
bool add(const std::string &hname, TKey *tobj)
Definition fastadd.cxx:55
std::string replace(std::string s, const std::string &s2, const std::string &s3)
Definition hcg.cxx:312