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