ATLAS Offline Software
Loading...
Searching...
No Matches
GenericMonitoringTool.py
Go to the documentation of this file.
2# Copyright (C) 2002-2025 CERN for the benefit of the ATLAS collaboration
3#
4
5from AthenaConfiguration.ComponentFactory import CompFactory
6from AthenaCommon.Logging import logging
7import json
8
9log = logging.getLogger(__name__)
10
11
12class GenericMonitoringTool(CompFactory.GenericMonitoringTool):
13 '''GenericMonitoringTool configurable class'''
14
15 __slots__ = ('_configFlags', '_convention', '_defaultDuration')
16
17 def __init__(self, flags, name='GenericMonitoringTool', **kwargs):
18 self._configFlags = flags
19 self._convention = ''
20 self._defaultDuration = kwargs.pop('defaultDuration', None)
21 super().__init__(name, **kwargs)
22
23 @property
24 def convention(self):
25 return self._convention
26
27 @convention.setter
28 def convention(self, value):
29 self._convention = value
30
31 @property
32 def defaultDuration(self):
33 return self._defaultDuration
34
35 @defaultDuration.setter
36 def defaultDuration(self, value):
37 self._defaultDuration = value
38
39 def _coreDefine(self, deffunc, *args, **kwargs):
40 if 'convention' in kwargs:
41 # only if someone really knows what they're doing
42 pass
43 else:
44 duration = kwargs.pop('duration', self.defaultDuration)
45 if duration is not None:
46 kwargs['convention'] = self.convention + ':' + duration
47 # if an overall path for tool is specified, can leave path argument empty
48 if getattr(self, 'HistPath', '') != '':
49 kwargs.setdefault('path', '')
50 toadd = deffunc(self._configFlags, *args, **kwargs)
51 if toadd:
52 self.Histograms.append(toadd)
53
54 def defineHistogram(self, *args, **kwargs):
55 self._coreDefine(defineHistogram, *args, **kwargs)
56
57 def defineTree(self, *args, **kwargs):
58 self._coreDefine(defineTree, *args, **kwargs)
59
60
62 '''Array of configurables of GenericMonitoringTool objects'''
63 def __init__(self, flags, name, dimensions, **kwargs):
64 self.Tools = {}
65 self.Postfixes, self.Accessors = GenericMonitoringArray._postfixes(dimensions)
66 for postfix in self.Postfixes:
67 self.Tools[postfix] = GenericMonitoringTool(flags, name+postfix,**kwargs)
68
69 def __getitem__(self,index):
70 '''Forward operator[] on class to the list of tools'''
71 return self.toolList()[index]
72
73 def toolList(self):
74 return list(self.Tools.values())
75
76 def broadcast(self, member, value):
77 '''Allows one to set attributes of every tool simultaneously
78
79 Arguments:
80 member -- string which contains the name of the attribute to be set
81 value -- value of the attribute to be set
82 '''
83 for tool in self.toolList():
84 setattr(tool,member,value)
85
86 def defineHistogram(self, varname, title=None, path=None, pattern=None, **kwargs):
87 '''Propogate defineHistogram to each tool, adding a unique tag.
88
89 Arguments:
90 pattern -- if specified, list of n-tuples of indices for plots to create
91 '''
92 unAliased = varname.split(';')[0]
93 _, aliasBase = _alias(varname)
94 if aliasBase is None or aliasBase.strip() == '':
95 raise ValueError(f'Unable to define histogram using definition "{varname}" since we cannot determine its name')
96 if pattern is not None:
97 try:
98 iter(pattern)
99 except TypeError:
100 raise ValueError('Argument to GenericMonitoringArray.defineHistogram must be iterable')
101 if not isinstance(pattern, list):
102 pattern = list(pattern)
103 if len(pattern) == 0: # nothing to do
104 return
105 if isinstance(pattern[0], (str, int)):
106 # assume we have list of strings or ints; convert to list of 1-element tuples
107 pattern = [(_2,) for _2 in pattern]
108 for postfix, tool in self.Tools.items():
109 try:
110 accessors = tuple(self.Accessors[postfix])
111 if pattern is not None:
112 if accessors not in pattern:
113 continue
114 # two options for alias formatting,
115 # a) default convention: (var_0, var_1, etc.)
116 # b) custom formatting: 'alias{0}custom'.format(*(0, 1))
117 aliasBaseFormatted = aliasBase.format(*accessors)
118 if aliasBaseFormatted==aliasBase:
119 # if format call did not do anything, use default
120 aliased = unAliased+';'+aliasBase+postfix
121 else:
122 # if format call changed the alias, use custom
123 aliased = unAliased+';'+aliasBaseFormatted
124 if title is not None:
125 kwargs['title'] = title.format(*accessors)
126 if path is not None:
127 kwargs['path'] = path.format(*accessors)
128 except IndexError as e:
129 log.error('In title or path template of histogram {0}, too many positional '\
130 'arguments were requested. Title and path templates were "{1}" and "{2}", '\
131 'while only {3} fillers were given: {4}.'.format(aliasBase, title,\
132 path, len(accessors), accessors))
133 raise e
134
135 tool.defineHistogram(aliased, **kwargs)
136
137 @staticmethod
138 def _postfixes(dimensions, previous=''):
139 '''Generates a list of subscripts to add to the name of each tool
140
141 Arguments:
142 dimensions -- List containing the lengths of each side of the array off tools
143 previous -- Strings appended from the other dimensions of the array
144 '''
145 import collections
146 assert isinstance(dimensions,list) and len(dimensions)>0, \
147 'GenericMonitoringArray must have list of dimensions.'
148 try:
149 if dimensions==[1]:
150 return [''], {'': ['']}
151 except AttributeError:
152 #Evidently not [1]
153 pass
154 postList = []
155 accessorDict = collections.OrderedDict()
156 first = dimensions[0]
157 if isinstance(first,list):
158 iterable = first
159 elif isinstance(first,int):
160 iterable = range(first)
161 else:
162 #Assume GaudiConfig2.semantics._ListHelper
163 iterable = list(first)
164 for i in iterable:
165 if len(dimensions)==1:
166 postList.append(previous+'_'+str(i))
167 accessorDict[previous+'_'+str(i)]=[str(i)]
168 else:
169 postfixes, accessors = GenericMonitoringArray._postfixes(dimensions[1:],previous+'_'+str(i))
170 postList.extend(postfixes)
171 for acckey, accval in accessors.items():
172 accessorDict[acckey] = [str(i)] + accval
173 return postList, accessorDict
174
175
176
185def _invalidName(flags, name):
186 blocklist = '/\\'
187 if flags.Common.isOnline:
188 blocklist += '=,:.()'
189 return set(name).intersection(blocklist)
190
191
192
199def _alias(varname):
200 variableAliasSplit = varname.split(';')
201 varList = [v.strip() for v in variableAliasSplit[0].split(',')]
202 if len(variableAliasSplit)==1:
203 return varList, '_vs_'.join(reversed(varList))
204 elif len(variableAliasSplit)==2:
205 return varList, variableAliasSplit[1]
206 else:
207 message = 'Invalid variable or alias for {}. Histogram(s) not defined.'
208 log.warning(message.format(varname))
209 return None, None
210
211
212
218def _validateOptions(user, default):
219 for key, userVal in user.items():
220 # (1) Check that the requested key exists
221 assert key in default,\
222 f'Unknown option {key} provided. Choices are [{", ".join(default)}].'
223 # (2) Check that the provided type is correct
224 userType = type(userVal)
225 defaultVal = default[key]
226 defaultType = type(defaultVal)
227 if isinstance(userVal, bool) or isinstance(defaultVal, bool):
228 assert isinstance(userVal, bool) and isinstance(defaultVal, bool),\
229 f'{key} provided {userType}, expected bool.'
230 else:
231 assert isinstance(defaultVal, userType),\
232 f'{key} provided {userType}, expected {defaultType}'
233
234
235
238def _options(opt):
239 # Set the default dictionary of options
240 settings = {
241 'Sumw2': False, # store sum of squares of weights
242 'kLBNHistoryDepth': 0, # length of lumiblock history
243 'kAddBinsDynamically': False, # add new bins if fill is outside axis range
244 'kRebinAxes': False, # increase axis range without adding new bins
245 'kCanRebin': False, # allow all axes to be rebinned
246 'kVec': False, # add content to each bin from each element of a vector
247 'kVecUO': False, # same as above, but use 0th(last) element for underflow(overflow)
248 'kCumulative': False, # fill bin of monitored object's value, and every bin below it
249 'kLive': 0, # plot only the last N lumiblocks on y_vs_LB plots
250 'kAlwaysCreate': False # create the histogram, even if it is empty
251 }
252 if opt is None:
253 # If no options are provided, skip any further checks.
254 pass
255 elif isinstance(opt, dict):
256 # If the user provides a partial dictionary, update the default with user's.
257 _validateOptions(opt, settings) # check validity of user's options
258 settings.update(opt) # update the default dictionary
259 elif isinstance(opt, str) and len(opt)>0:
260 # If the user provides a comma- or space-separated string of options.
261 settings = _options (opt.replace(',',' ').split())
262
263 elif isinstance(opt,str):
264 # empty string case
265 pass
266 elif isinstance(opt, list):
267 # process each item in list
268 unknown = []
269 for o in opt:
270 kv = o.split('=', maxsplit=1)
271 key = kv[0]
272 if len(kv)==2:
273 value = False if kv[1]=='False' else int(kv[1]) # only bool and int supported
274 else:
275 value = True
276 if key in settings:
277 assert(type(settings[key])==type(value)) # ensure same type as in defaults
278 settings[key] = value
279 else:
280 unknown.append(key)
281
282 assert len(unknown)==0, f'Unknown option(s) provided: {", ".join(unknown)}.'
283 else:
284 raise ValueError("Unknown opt type")
285 return settings
286
287
288
306def defineHistogram(flags, varname, type='TH1F', path=None,
307 title=None, weight=None,
308 xbins=100, xmin=0, xmax=1, xlabels=None,
309 ybins=None, ymin=None, ymax=None, ylabels=None,
310 zmin=None, zmax=None, zlabels=None,
311 opt=None, convention=None, cutmask=None,
312 treedef=None, merge=None):
313
314 # All of these fields default to an empty string
315 stringSettingsKeys = ['xvar', 'yvar', 'zvar', 'type', 'path', 'title', 'weight',
316 'cutMask', 'convention', 'alias', 'treeDef', 'merge']
317 # All of these fileds default to 0
318 numberSettingsKeys = ['xbins', 'xmin', 'xmax', 'ybins', 'ymin', 'ymax', 'zbins',
319 'zmin', 'zmax']
320 # All of these fields default to an empty array
321 arraySettingsKeys = ['allvars', 'xlabels', 'xarray', 'ylabels', 'yarray', 'zlabels']
322 # Initialize a dictionary with all possible fields
323 settings = dict((key, '') for key in stringSettingsKeys)
324 settings.update(dict((key, 0.0) for key in numberSettingsKeys))
325 settings.update(dict((key, []) for key in arraySettingsKeys))
326
327 # Alias
328 varList, alias = _alias(varname)
329 if alias is None or alias.strip() == '':
330 log.warning(f'Unable to define histogram using definition "{varname}" since we cannot determine its name.')
331 return ''
332
333 invalid = _invalidName(flags, alias)
334 if invalid:
335 log.warning('%s is not a valid histogram name. Illegal characters: %s',
336 alias, ' '.join(invalid))
337 return ''
338
339 settings['alias'] = alias
340
341 # Variable names
342 nVars = len(varList)
343 if nVars>0:
344 settings['xvar'] = varList[0]
345 if nVars>1:
346 settings['yvar'] = varList[1]
347 if nVars>2:
348 settings['zvar'] = varList[2]
349 settings['allvars'] = varList
350
351 # Type
352 if flags.Common.isOnline and type in ['TTree']:
353 log.warning('Object %s of type %s is not supported for online running and '
354 'will not be added.', varname, type)
355 return ''
356
357 # Allowed histogram dimensions
358 if type.startswith('TH1'):
359 dims = {1}
360 elif type.startswith('TH2'):
361 dims = {2}
362 elif type=='TProfile':
363 dims = {2}
364 elif type=='TProfile2D':
365 dims = {3}
366 elif type=='TEfficiency':
367 dims = {2, 3}
368
369 assert type=='TTree' or nVars in dims,\
370 f'Number of monitored variables {varList} for "{path}/{alias}" '\
371 f'does not match histogram dimension for {type}'
372
373 settings['type'] = type
374
375 # Path
376 if path is None:
377 path = ''
378 settings['path'] = path
379
380 # Title
381 if title is None:
382 title = varname
383 settings['title'] = title
384
385 # Weight
386 if weight is not None:
387 settings['weight'] = weight
388
389 # Cutmask
390 if cutmask is not None:
391 settings['cutMask'] = cutmask
392
393 # Output path naming convention
394 if convention is not None:
395 settings['convention'] = convention
396
397 # Bin counts and ranges
398 # Possible types allowed for bin counts
399 binTypes = (int, list, tuple)
400
401 # X axis count and range
402 assert isinstance(xbins, binTypes),'xbins argument must be int, list, or tuple'
403 if isinstance(xbins, int): # equal x bin widths
404 settings['xbins'], settings['xarray'] = xbins, []
405 else: # x bin edges are set explicitly
406 settings['xbins'], settings['xarray'] = len(xbins)-1, xbins
407 settings['xmin'] = xmin
408 settings['xmax'] = xmax
409
410 # Y axis count and range
411 if ybins is not None:
412 assert isinstance(ybins, binTypes),'ybins argument must be int, list, or tuple'
413 if isinstance(ybins, int): # equal y bin widths
414 settings['ybins'], settings['yarray'] = ybins, []
415 else: # y bin edges are set explicitly
416 settings['ybins'], settings['yarray'] = len(ybins)-1, ybins
417 if ymin is not None:
418 settings['ymin'] = ymin
419 if ymax is not None:
420 settings['ymax'] = ymax
421
422 # Z axis count and range
423 if zmin is not None:
424 settings['zmin'] = zmin
425 if zmax is not None:
426 settings['zmax'] = zmax
427
428 # Then, parse the [xyz]label arguments
429 if xlabels is not None and len(xlabels)>0:
430 assert isinstance(xlabels, (list, tuple)),'xlabels must be list or tuple'
431 settings['xbins'] = len(xlabels)
432 settings['xlabels'] = xlabels
433 if ylabels is not None and len(ylabels)>0:
434 assert isinstance(ylabels, (list, tuple)),'ylabels must be list or tuple'
435 settings['ybins'] = len(ylabels)
436 settings['ylabels'] = ylabels
437 # if user did not specify ymin and ymax, set it here, as cannot have ymin=ymax=0
438 if ymin is None: settings["ymin"] = 0
439 if ymax is None: settings["ymax"] = settings["ymin"]+1
440 if zlabels is not None and len(zlabels)>0:
441 assert isinstance(zlabels, (list, tuple)),'zlabels must be list or tuple'
442 settings['zlabels'] = zlabels
443
444 # Tree branches
445 if treedef is not None:
446 assert type=='TTree','cannot define tree branches for a non-TTree object'
447 settings['treeDef'] = treedef
448
449 # Add all other options
450 settings.update(_options(opt))
451
452 # some things need merging
453 if ((settings['kAddBinsDynamically'] or settings['kRebinAxes'] or settings['kCanRebin'])
454 and (not flags.Common.isOnline and 'OFFLINE' in settings['convention'])):
455 if merge is None:
456 log.warning(f'Merge method for {alias} is not specified but needs to be "merge" due to histogram definition; overriding for your convenience')
457 merge = 'merge'
458
459 # merge method
460 if merge is not None:
461 assert type not in ['TEfficiency', 'TTree', 'TGraph'],'only default merge defined for non-histogram objects'
462 settings['merge'] = merge
463
464 # LB histograms always need to be published online (ADHI-4947)
465 if settings['kLBNHistoryDepth']>0 and flags.Common.isOnline:
466 settings['kAlwaysCreate'] = True
467 log.debug('Setting kAlwaysCreate for lumiblock histogram "%s"', varname)
468
469 # Check that kLBNHistoryDepth and kLive are both non-negative
470 assert settings['kLBNHistoryDepth']>=0, f'Histogram "{alias}" has invalid kLBNHistoryDepth.'
471 assert settings['kLive']>=0, f'Histogram "{alias}" has invalid kLive.'
472 # kLBNHistoryDepth and kLive options are mutually exclusive. User may not specify both.
473 assert settings['kLBNHistoryDepth']==0 or settings['kLive']==0,\
474 f'Cannot use both kLBNHistoryDepth and kLive for histogram {alias}.'
475 # kLive histograms are only available for Online monitoring.
476 assert settings['kLive']==0 or flags.Common.isOnline,\
477 f'Cannot use kLive with offline histogram {alias}.'
478
479 return json.dumps(settings)
480
481
482
498def defineTree(flags, varname, treedef, path=None, title=None,
499 opt='', convention=None,
500 cutmask=None):
501 return defineHistogram(flags, varname, type='TTree', path=path, title=title,
502 treedef=treedef, opt=opt, convention=convention,
503 cutmask=cutmask)
defineHistogram(self, varname, title=None, path=None, pattern=None, **kwargs)
__init__(self, flags, name, dimensions, **kwargs)
__init__(self, flags, name='GenericMonitoringTool', **kwargs)
STL class.
std::vector< std::string > intersection(std::vector< std::string > &v1, std::vector< std::string > &v2)
std::vector< std::string > split(const std::string &s, const std::string &t=":")
Definition hcg.cxx:177
_alias(varname)
Generate an alias for a set of variables.
defineHistogram(flags, varname, type='TH1F', path=None, title=None, weight=None, xbins=100, xmin=0, xmax=1, xlabels=None, ybins=None, ymin=None, ymax=None, ylabels=None, zmin=None, zmax=None, zlabels=None, opt=None, convention=None, cutmask=None, treedef=None, merge=None)
Generate histogram definition string for the GenericMonitoringTool.Histograms property.
defineTree(flags, varname, treedef, path=None, title=None, opt='', convention=None, cutmask=None)
Generate tree definition string for the GenericMonitoringTool.Histograms property.
_invalidName(flags, name)
Check if name is an allowed histogram/branch name.
_options(opt)
Generate dictionary entries for opt strings.
_validateOptions(user, default)
Validate user inputs for "opt" argument of defineHistogram.