ATLAS Offline Software
Loading...
Searching...
No Matches
chainDump.py
Go to the documentation of this file.
1#!/usr/bin/env python
2#
3# Copyright (C) 2002-2025 CERN for the benefit of the ATLAS collaboration
4#
5
6'''Script to dump trigger counts to a text file'''
7
8import sys
9import argparse
10import logging
11import json
12import yaml
13import ROOT
14from collections import defaultdict
15
16total_events_key = 'TotalEventsProcessed'
17column_width = 10 # width of the count columns for print out
18name_width = 50 # width of the item name column for print out
19
20# Store defaultdict as dict in yaml files
21from yaml.representer import Representer
22yaml.add_representer(defaultdict, Representer.represent_dict)
23
25 parser = argparse.ArgumentParser(usage='%(prog)s [options]',
26 description=__doc__)
27 parser.add_argument('-f', '--inputFile',
28 metavar='PATH',
29 default='expert-monitoring.root',
30 help='Name of input root file')
31 parser.add_argument('-r', '--referenceFile',
32 metavar='PATH',
33 help='Name of reference root file')
34 parser.add_argument('-v', '--verbose',
35 action='store_true',
36 help='Increase output verbosity')
37 parser.add_argument('-p', '--printOnly',
38 action='store_true',
39 default=False,
40 help='Print counts instead of saving to file')
41 parser.add_argument('-d', '--diffOnly',
42 action='store_true',
43 default=False,
44 help='Only store out of tolerance results (does not change JSON)')
45 parser.add_argument('--json',
46 metavar='PATH',
47 nargs='?',
48 const='chainDump.json',
49 help='Save outputs also to a json file with the given name or %(const)s if no name is given')
50 parser.add_argument('--yaml',
51 metavar='PATH',
52 nargs='?',
53 const='chainDump.yml',
54 help='Produce a small yaml file including condensed counts information for test file only '
55 '(no ref) with the given name or %(const)s if no name is given')
56 parser.add_argument('--yamlL1',
57 action='store_true',
58 help='Include the L1 count information to the yaml file')
59 parser.add_argument('--fracTolerance',
60 metavar='FRAC',
61 type=float,
62 default=0.001,
63 help='Tolerance as a fraction, default = %(default)s. '
64 'Flagged diffs must exceed all tolerances')
65 parser.add_argument('--intTolerance',
66 metavar='NUM',
67 type=int,
68 default=2,
69 help='Tolerance as a number of counts, default = %(default)s. '
70 'Flagged diffs must exceed all tolerances')
71 parser.add_argument('--countHists',
72 metavar='HISTS',
73 nargs='+',
74 default=[
75 'HLTFramework/TrigSignatureMoni/SignatureAcceptance',
76 'HLTFramework/TrigSignatureMoni/../TrigSignatureMoni/SignatureAcceptance',
77 'HLTFramework/../HLTFramework/TrigSignatureMoni/SignatureAcceptance',
78 'TrigSteer_HLT/ChainAcceptance',
79 'TrigSteer_HLT/NumberOfActiveTEs',
80 'HLTFramework/TrigSignatureMoni/DecisionCount',
81 'CTPSimulation/L1ItemsAV',
82 'L1/CTPSimulation/output/tavById'],
83 help='Histograms to use for counts dump. All existing '
84 'histograms from the list are used, default = %(default)s')
85 parser.add_argument('--totalHists',
86 metavar='HISTS',
87 nargs='+',
88 default=[
89 'TrigSteer_HLT/NInitialRoIsPerEvent',
90 'HLTFramework/HLTSeeding/RoIs_eEM/count'],
91 help='Histograms to use for total events. First existing '
92 'histogram from the list is used, default = %(default)s')
93 parser.add_argument('--histDict',
94 metavar='DICT',
95 nargs='+',
96 default=[
97 'HLTFramework/TrigSignatureMoni/SignatureAcceptance:HLTChain',
98 'HLTFramework/TrigSignatureMoni/../TrigSignatureMoni/SignatureAcceptance:HLTExpress',
99 'HLTFramework/../HLTFramework/TrigSignatureMoni/SignatureAcceptance:HLTStep',
100 'TrigSteer_HLT/ChainAcceptance:HLTChain',
101 'TrigSteer_HLT/NumberOfActiveTEs:HLTTE',
102 'HLTFramework/TrigSignatureMoni/DecisionCount:HLTDecision',
103 'CTPSimulation/L1ItemsAV:L1AV',
104 'L1/CTPSimulation/output/tavById:L1AV'],
105 help='Dictionary defining names of output text files for each '
106 'histogram, default = %(default)s')
107 parser.add_argument('--printHeader',
108 action='store_true',
109 default=False,
110 help='Add title of columns to the output txt (just for readability)')
111 return parser
112
113
114def open_root_file(file_path):
115 f = ROOT.TFile(file_path)
116 if f.IsOpen() and not f.IsZombie():
117 return f
118 else:
119 return None
120
121
122def load_histograms(root_file, hist_paths):
123 hist_dict = {}
124 for hist_path in hist_paths:
125 h = root_file.Get(hist_path)
126 if not isinstance(h, ROOT.TH1):
127 logging.debug('Cannot open histogram %s, skipping', hist_path)
128 continue
129 logging.debug('Loaded histogram %s', hist_path)
130 hist_dict[hist_path] = h
131 return hist_dict
132
133
134def get_counts(hist, rowLabel='Output'):
135 '''
136 Extract {xlabel, value} dictionary from a histogram. Values are stored as integers.
137 If histogram is 2D, the y-bin labelled rowLabel is used to extract the value.
138 '''
139
140 nbinsx = hist.GetNbinsX()
141 nbinsy = hist.GetNbinsY()
142 outputRow = None # Default to last row if 'Output' not found
143 for bin in range(1, nbinsy):
144 if hist.GetYaxis().GetBinLabel(bin) == rowLabel:
145 outputRow = bin
146 break
147
148 counts = {}
149 for b in range(1, nbinsx+1):
150 label = hist.GetXaxis().GetBinLabel(b)
151 if not label:
152 logging.debug('Bin %d in histogram %s has no label, skipping', b, hist.GetName())
153 continue
154
155 value = hist.GetBinContent(b) if hist.GetDimension() == 1 else hist.GetBinContent(b, outputRow or nbinsy)
156 counts[label] = int(value)
157
158 return counts
159
161 '''
162 Extract {xlabel_ylabel, value} dictionary from a histogram. Values are stored as integers.
163 '''
164 nbinsx = hist.GetNbinsX()
165 nbinsy = hist.GetNbinsY()
166 counts = {}
167 for x in range(1, nbinsx+1):
168 label = hist.GetXaxis().GetBinLabel(x)
169 if not label:
170 logging.debug('Bin %d in histogram %s has no label, skipping', x, hist.GetName())
171 continue
172
173 for y in range(3, nbinsy):
174 rowName = hist.GetYaxis().GetBinLabel(y)
175 # Get only steps and skip the base rows
176 if rowName in ['Input','AfterPS','Output','Express']:
177 continue
178 name = label + '_' + rowName
179 name = name.replace(' ', '')
180 value = hist.GetBinContent(x, y)
181 counts[name] = int(value)
182
183 return counts
184
185
186def make_counts_json_dict(in_counts, ref_counts):
187 counts = {}
188 all_keys = set(in_counts)
189 all_keys.update(ref_counts)
190 for k in sorted(all_keys,key=lambda chain : (chain[7:],chain[:6]) if chain.startswith('leg') else (chain,'')):
191 v = in_counts[k] if k in in_counts else 'n/a'
192 ref_v = ref_counts[k] if k in ref_counts else 'n/a'
193 counts[k] = {
194 'count': v,
195 'ref_count': ref_v,
196 'ref_diff': 'n/a' # Filled in compare_ref
197 }
198 return counts
199
200
201def parse_name_dict(name_dict_as_list):
202 name_dict = {}
203 for kv in name_dict_as_list:
204 kv_split = kv.split(':')
205 if len(kv_split) < 2:
206 continue
207 name_dict[kv_split[0]] = kv_split[1]
208 return name_dict
209
210
211def get_text_name(hist_name, name_dict):
212 if hist_name in name_dict:
213 return name_dict[hist_name]
214 else:
215 return hist_name.replace('/', '_')
216
217
218def count_diff(count_in, count_ref, total_in, total_ref, thr_frac, thr_num):
219 # normalise input counts to total events in reference
220 count_in_norm = (count_in / float(total_in)) * total_ref
221 frac = count_in_norm / float(count_ref) if count_ref != 0 else None
222
223 num_diff = abs(count_in_norm - count_ref) > thr_num
224 frac_diff = abs(frac - 1.0) > thr_frac if frac else True
225
226 return num_diff and frac_diff
227
228
229def compare_ref(json_dict, thr_frac, thr_num):
230 results = []
231 in_total = json_dict[total_events_key]['count']
232 ref_total = json_dict[total_events_key]['ref_count']
233 for text_name in sorted(json_dict):
234 if text_name == total_events_key:
235 continue
236 diff_val = [] # different counts in input and reference
237 missing_ref = [] # input count exists but reference is n/a
238 missing_val = [] # input count is n/a but reference exists
239 for item_name, item_counts in json_dict[text_name]['counts'].items():
240 v = item_counts['count']
241 ref_v = item_counts['ref_count']
242 if v == 'n/a':
243 missing_val.append([item_name, v, ref_v])
244 item_counts['ref_diff'] = True
245 elif ref_v == 'n/a':
246 missing_ref.append([item_name, v, ref_v])
247 item_counts['ref_diff'] = True
248 elif count_diff(v, ref_v, in_total, ref_total, thr_frac, thr_num):
249 diff_val.append([item_name, v, ref_v])
250 item_counts['ref_diff'] = True
251 else:
252 item_counts['ref_diff'] = False
253 good = True
254 if len(diff_val) > 0:
255 good = False
256 dump = '\n'.join(
257 [' {e[0]:{nw}s} {e[1]:>{w}d} {e[2]:>{w}d}'.format(
258 e=element, nw=name_width, w=column_width) for element in diff_val])
259 logging.info('%s has %d item(s) out of tolerance:\n%s',
260 text_name, len(diff_val), dump)
261 if (len(missing_ref)) > 0:
262 good = False
263 dump = '\n'.join([' {e[0]:s}'.format(e=element) for element in missing_ref])
264 logging.info('%s has %d item(s) missing in the reference:\n%s',
265 text_name, len(missing_ref), dump)
266 if (len(missing_val)) > 0:
267 good = False
268 dump = '\n'.join([' {e[0]:s}'.format(e=element) for element in missing_val])
269 logging.info('%s has %d item(s) missing with respect to the reference:\n%s',
270 text_name, len(missing_val), dump)
271 if good:
272 logging.info('%s is matching the reference', text_name)
273 results.append(0)
274 else:
275 results.append(1)
276 return max(results)
277
278
279def print_counts(json_dict):
280 for text_name in json_dict:
281 if text_name == total_events_key:
282 logging.info('%s: %d', text_name, json_dict[text_name]['count'])
283 continue
284 hist_name = json_dict[text_name]['hist_name']
285 counts = json_dict[text_name]['counts']
286 no_ref = True
287 for item_counts in counts.values():
288 if item_counts['ref_count'] != 'n/a':
289 no_ref = False
290 break
291 dump_lines = []
292 for item_name, item_counts in counts.items():
293 v = item_counts['count']
294 line = ' {name:{nw}s} {val:>{w}s}'.format(name=item_name, val=str(v), nw=name_width, w=column_width)
295 if not no_ref:
296 ref_v = item_counts['ref_count']
297 diff = item_counts['ref_diff']
298 line += ' {val:>{w}s}'.format(val=str(ref_v), w=column_width)
299 if diff:
300 line += ' <<<<<<<<<<'
301 dump_lines.append(line)
302 logging.info('Writing %s counts from histogram %s:\n%s', text_name, hist_name, '\n'.join(dump_lines))
303
304
306 if type(count) is int:
307 return '{val:>{w}d}'.format(val=count, w=column_width)
308 elif type(count) is not str:
309 logging.error('Unexpected count type %s', type(count))
310 count = 'ERROR'
311 if count == 'n/a':
312 count = '-'
313 return '{val:>{w}s}'.format(val=count, w=column_width)
314
315
316def write_txt_output(json_dict, diff_only=False, printHeader=False):
317 for text_name in sorted(json_dict):
318 if text_name == total_events_key:
319 logging.info('Writing total event count to file %s.txt', text_name)
320 with open('{:s}.txt'.format(text_name), 'w') as outfile:
321 outfile.write('{:d}\n'.format(json_dict[text_name]['count']))
322 continue
323 hist_name = json_dict[text_name]['hist_name']
324 logging.info('Writing counts from histogram %s to file %s.txt', hist_name, text_name)
325 counts = json_dict[text_name]['counts']
326 no_ref = True
327 for item_counts in counts.values():
328 if item_counts['ref_count'] != 'n/a':
329 no_ref = False
330 break
331 with open('{:s}.txt'.format(text_name), 'w') as outfile:
332 if printHeader:
333 line = '{name:{nw}s}'.format(name='chain', nw=name_width)
334 if not no_ref:
335 line += '{name:{cw}s}'.format(name='test', cw=column_width) + 'ref \n'
336 else:
337 line += 'test \n'
338 outfile.write(line)
339 for item_name, item_counts in counts.items():
340 v = item_counts['count']
341 line = '{name:{nw}s} '.format(name=item_name, nw=name_width) + format_txt_count(v)
342 if not no_ref:
343 ref_v = item_counts['ref_count']
344 diff = item_counts['ref_diff']
345 line += ' ' + format_txt_count(ref_v)
346 if diff:
347 line += ' <<<<<<<<<<'
348 elif diff_only:
349 line = None
350 if line:
351 outfile.write(line+'\n')
352
353
354def make_light_dict(full_dict, includeL1Counts):
355 # 3 nested dictionaries of int
356 light_dict = defaultdict(lambda: defaultdict(lambda: defaultdict(int)))
357
358 def extract_steps(in_name, out_name):
359 for name,c in full_dict[in_name]['counts'].items():
360 if c['count']==0:
361 continue
362 chain_name, chain_step = name.split('_Step')
363 light_dict[chain_name][out_name][int(chain_step)] = c['count']
364
365 # Change step dictionary to consecutive list of steps
366 for chain_name in light_dict:
367 steps = light_dict[chain_name][out_name]
368 light_dict[chain_name][out_name] = {i:steps[k] for i,k in enumerate(sorted(steps))}
369
370 extract_steps('HLTStep', 'stepCounts')
371 extract_steps('HLTDecision', 'stepFeatures')
372
373 # Add total chain count and skip total / groups / streams
374 for chain_name,c in full_dict['HLTChain']['counts'].items():
375 light_dict[chain_name]['eventCount'] = c['count']
376
377 if any(chain_name.startswith(s) for s in ['All', 'grp_', 'str_']):
378 del light_dict[chain_name]
379
380 if includeL1Counts and 'L1AV' in full_dict:
381 light_dict.update(
382 {name:{"eventCount": counts["count"]} for name,counts in full_dict["L1AV"]["counts"].items()}
383 )
384
385 return light_dict
386
387
388def main():
389 args = get_parser().parse_args()
390 logging.basicConfig(stream=sys.stdout,
391 format='%(levelname)-8s %(message)s',
392 level=logging.DEBUG if args.verbose else logging.INFO)
393
394 name_dict = parse_name_dict(args.histDict)
395
396
399
400 in_file = open_root_file(args.inputFile)
401 if not in_file:
402 logging.error('Failed to open input file %s', args.inputFile)
403 return 1
404 logging.debug('Opened input file %s', args.inputFile)
405
406 if args.referenceFile:
407 ref_file = open_root_file(args.referenceFile)
408 if not ref_file:
409 logging.error('Failed to open input file %s', args.referenceFile)
410 return 1
411 logging.debug('Opened input file %s', args.referenceFile)
412
413
416
417 in_hists = load_histograms(in_file, args.countHists)
418 if len(in_hists) == 0:
419 logging.error('No count histograms could be loaded.')
420 return 1
421 logging.info('Loaded count histograms: %s', sorted(in_hists))
422
423 in_total_hists = load_histograms(in_file, args.totalHists)
424 if len(in_total_hists) == 0:
425 logging.error('No total-events histogram could be loaded')
426 return 1
427 items = list(in_total_hists.items())
428 in_total = items[0][1].GetEntries()
429 logging.info('Loaded total-events histogram %s, number of events: %d',
430 items[0][0], in_total)
431
432 ref_hists = None
433 ref_total_hists = None
434 ref_total = None
435 if args.referenceFile:
436 ref_hists = load_histograms(ref_file, args.countHists)
437 logging.info('Loaded reference count histograms: %s', sorted(ref_hists))
438 missing_refs = [k for k in in_hists if k not in ref_hists]
439 if len(missing_refs) > 0:
440 logging.error('Count histogram(s) %s missing in the reference', missing_refs)
441 return 1
442 ref_total_hists = load_histograms(ref_file, args.totalHists)
443 if len(ref_total_hists) == 0:
444 logging.error('No total-events reference histogram could be loaded')
445 return 1
446 ref_total = list(ref_total_hists.values())[0].GetEntries()
447 logging.info('Loaded total-events reference histogram %s, number of events: %d',
448 list(ref_total_hists.keys())[0], ref_total)
449
450
453
454 json_dict = {
455 total_events_key: {
456 'hist_name': list(in_total_hists.keys())[0],
457 'count': int(in_total),
458 'ref_count': int(ref_total) if ref_total else 'n/a'
459 }
460 }
461
462 for hist_name, hist in in_hists.items():
463 text_name = get_text_name(hist_name, name_dict)
464 if text_name in json_dict:
465 logging.error(
466 'Name "%s" assigned to more than one histogram, ', text_name,
467 'results would be overwritten. Use --countHists and ',
468 '--histDict options to avoid duplicates. Exiting.')
469
470 rowLabel = 'Express' if 'Express' in text_name else 'Output'
471 counts = get_2D_counts(hist) if text_name in ['HLTStep', 'HLTDecision'] else get_counts(hist, rowLabel)
472 ref_counts = {}
473 if ref_hists:
474 ref_hist = ref_hists[hist_name]
475 ref_counts = get_2D_counts(ref_hist) if text_name in ['HLTStep', 'HLTDecision'] else get_counts(ref_hist, rowLabel)
476 d = make_counts_json_dict(counts, ref_counts)
477
478 json_dict[text_name] = {
479 'hist_name': hist_name,
480 'counts': d
481 }
482
483
486
487 retcode = 0
488 if args.referenceFile:
489 logging.info('Comparing counts to reference')
490 retcode = compare_ref(json_dict, args.fracTolerance, args.intTolerance)
491
492 if args.printOnly and not args.diffOnly:
493 logging.info('Printing counts instead of dumping to files because of --printOnly option')
494 print_counts(json_dict)
495
496 if not args.printOnly:
497 write_txt_output(json_dict, args.diffOnly, args.printHeader)
498
499 if args.json:
500 logging.info('Writing results to %s', args.json)
501 with open(args.json, 'w') as outfile:
502 json.dump(json_dict, outfile, sort_keys=False) # Key order defined above
503
504 if args.yaml:
505 logging.info('Writing results extract to %s', args.yaml)
506 light_dict = make_light_dict(json_dict, includeL1Counts = args.yamlL1)
507 with open(args.yaml, 'w') as outfile:
508 yaml.dump(light_dict, outfile, sort_keys=False) # Key order defined above
509
510 return retcode
511
512
513if '__main__' in __name__:
514 sys.exit(main())
TGraphErrors * GetEntries(TH2F *histo)
#define max(a, b)
Definition cfImp.cxx:41
STL class.
get_counts(hist, rowLabel='Output')
Definition chainDump.py:134
make_light_dict(full_dict, includeL1Counts)
Definition chainDump.py:354
print_counts(json_dict)
Definition chainDump.py:279
get_2D_counts(hist)
Definition chainDump.py:160
load_histograms(root_file, hist_paths)
Definition chainDump.py:122
compare_ref(json_dict, thr_frac, thr_num)
Definition chainDump.py:229
get_text_name(hist_name, name_dict)
Definition chainDump.py:211
open_root_file(file_path)
Definition chainDump.py:114
format_txt_count(count)
Definition chainDump.py:305
count_diff(count_in, count_ref, total_in, total_ref, thr_frac, thr_num)
Definition chainDump.py:218
parse_name_dict(name_dict_as_list)
Definition chainDump.py:201
write_txt_output(json_dict, diff_only=False, printHeader=False)
Definition chainDump.py:316
make_counts_json_dict(in_counts, ref_counts)
Definition chainDump.py:186