ATLAS Offline Software
Loading...
Searching...
No Matches
diff_root_files.py
Go to the documentation of this file.
1# Copyright (C) 2002-2026 CERN for the benefit of the ATLAS collaboration
2
3# @file PyUtils.scripts.diff_root_files
4# @purpose check that 2 ROOT files have same content (containers and sizes).
5# @author Sebastien Binet
6# @date February 2010
7
8__doc__ = "diff two ROOT files (containers and sizes)"
9__author__ = "Sebastien Binet"
10
11
12import PyUtils.acmdlib as acmdlib
13from PyUtils.Logging import logging
14import re
15from functools import cache, reduce
16from math import isnan, isclose
17from numbers import Real
18from os import environ
19
20
21g_ALLOWED_MODES = ('summary', 'semi-detailed', 'detailed')
22g_ALLOWED_ERROR_MODES = ('bailout', 'resilient')
23
24msg = logging.getLogger('diff-root')
25
26
27
28
29def _is_detailed(args):
30 return args.mode == 'detailed'
31
32def _is_summary(args):
33 return args.mode == 'summary'
34
36 return args.error_mode == 'bailout'
37
38
39_vectypes = {'std::vector<float>',
40 'std::vector<double>',
41 'std::vector<int>',
42 'std::vector<unsigned int>',
43 'std::vector<long>',
44 'std::vector<unsigned long>',
45 'std::vector<short>',
46 'std::vector<unsigned short>',
47 'std::vector<char>',
48 'std::vector<unsigned char>',
49 'std::vector<long long>',
50 'std::vector<unsigned long long>'}
51
52# Compare two values with optional tolerance. If nan_equal, then consider NaNs to be equal.
53def _cmp (r1, r2, nan_equal, rel_tol=None, abs_tol=None):
54 if (nan_equal and isinstance(r1, Real) and isnan(r1) and isinstance(r2, Real) and isnan(r2)):
55 return True
56 if rel_tol is None and abs_tol is None:
57 return r1 == r2
58 else:
59 return isclose(r1, r2, rel_tol=rel_tol or 0.0, abs_tol=abs_tol or 0.0)
60
61# Possibly compare two vectors. If nan_equal, then consider NaNs to be equal.
62# Returns None if we have two matching vectors.
63# If we have two vectors that differ at some element, return that index.
64# Otherwise return -1 (inputs not vectors, etc).
65def _vecdiff (v1, v2, nan_equal, rel_tol=None, abs_tol=None):
66 if getattr(type(type(v1)), '__cpp_name__', None) not in _vectypes:
67 return -1
68 if type(v1) is not type(v2): return -1
69 sz = v1.size()
70 if sz != v2.size(): return -1
71 for i in range (sz):
72 if not _cmp(v1[i], v2[i], nan_equal, rel_tol, abs_tol):
73 return i
74
75 return None
76
77
79 """Parser for --tolerance argument"""
80 import argparse
81 import yaml
82 # Read tolerances from file
83 if arg.startswith('@'):
84 from AthenaCommon.Utils.unixtools import find_datafile
85 filepath = find_datafile(arg[1:])
86 if filepath is None:
87 raise argparse.ArgumentTypeError(f"Failed to load '{arg[1:]}'")
88
89 msg.info("Reading tolerances from %s", filepath)
90 data = open(filepath, "r").read()
91 # Inline tolerances
92 else:
93 data = arg
94
95 try:
96 data = yaml.safe_load(data)
97 except Exception as e:
98 raise argparse.ArgumentTypeError(f"Invalid YAML/JSON string: {e}")
99
100 if not isinstance(data, dict):
101 raise argparse.ArgumentTypeError(
102 f"Tolerance config must evaluate to a dictionary, got {type(data)}")
103
104 # Parse the tolerances
105 tol = {}
106 for k, v in data.items():
107 if isinstance(v, Real):
108 tol[k] = (v, None) # only relative tolerance
109 elif isinstance(v, (list, tuple)) and len(v)==2:
110 tol[k] = tuple(v) # (rel, abs) tolerance
111 else:
112 raise argparse.ArgumentTypeError(
113 f"Invalid format for tolerance: {v}. Must be a number or [rel, abs] list.")
114
115 return tol
116
117
118@acmdlib.command(name='diff-root')
119@acmdlib.argument('old',
120 help='path to the reference ROOT file to analyze')
121@acmdlib.argument('new',
122 help='path to the ROOT file to compare to the reference')
123@acmdlib.argument('-t', '--tree-name',
124 default=None,
125 help='name of the TTree or RNTuple to compare')
126@acmdlib.argument('--branches-of-interest',
127 nargs='+',
128 default=set(),
129 help='set of regex matching names of branches to compare; assumes all if none specified.')
130@acmdlib.argument('--ignore-leaves',
131 nargs='+',
132 default=('Token', 'index_ref', r'(.*)TrigCostContainer(.*)'),
133 help='set of leaves names to ignore from comparison; can be a branch name or a partial leaf name (accepts regex)')
134@acmdlib.argument('--enforce-leaves',
135 nargs='+',
136 default=('BCID',),
137 help='set of leaves names we make sure to compare')
138@acmdlib.argument('--leaves-prefix',
139 default='',
140 help='Remove prefix value from all leaves')
141@acmdlib.argument('--known-hacks',
142 nargs='+',
143 default=('m_athenabarcode', 'm_token',),
144 help='set of leaves which are known to fail (but should be fixed at some point) [default: %(default)s]')
145@acmdlib.argument('--entries',
146 default='',
147 help='a list of entries (indices, not event numbers) or an expression (like range(3) or 0,2,1 or 0:3) leading to such a list, to compare.')
148@acmdlib.argument('-v', '--verbose',
149 action='store_true',
150 default=False,
151 help="""Enable verbose printout""")
152@acmdlib.argument('--order-trees',
153 action='store_true',
154 default=False,
155 help="""To order trees according to event numbers""")
156@acmdlib.argument('--exact-branches',
157 action='store_true',
158 default=False,
159 help="""Only allow exact list of branches present""")
160@acmdlib.argument('--mode',
161 choices=g_ALLOWED_MODES,
162 default='detailed',
163 help="""\
164Enable a particular mode.
165 'summary': only report the number of differences.
166 'semi-detailed': report the number of differences and the leaves that differ.
167 'detailed': display everything.
168default='%(default)s'.
169allowed: %(choices)s
170"""
171 )
172@acmdlib.argument('--error-mode',
173 choices=g_ALLOWED_ERROR_MODES,
174 default='bailout',
175 help="""\
176Enable a particular error mode.
177 'bailout': bail out on first error.
178 'resilient': keep running.
179default='%(default)s'.
180allowed: %(choices)s
181"""
182 )
183@acmdlib.argument('--nan-equal',
184 action='store_true',
185 default=False,
186 help="""Compare nan as equal to nan""")
187@acmdlib.argument('--tolerances',
188 metavar='DICT/FILE',
189 type=parse_tolerances,
190 help="""\
191YAML dictionary with leaf tolerances: {'regex': rel} or {'regex': [rel, abs]}
192with relative (<=1) and absolute tolerance (see math.isclose).
193First match wins. To read the values from a file (via DATAPATH) use '@myfile.yaml'.
194"""
195 )
196
197def main(args):
198 """diff two ROOT files (containers and sizes)"""
199
200 # We allocate many python objects at once.
201 # Running GC less often by jacking up the threshold speeds things up
202 # considerably.
203 import gc
204 gc.set_threshold (100000)
205
206 import PyUtils.RootUtils as ru
207 root = ru.import_root() # noqa: F841
208 try:
209 RNTupleReader = root.RNTupleReader
210 except AttributeError:
211 RNTupleReader = root.Experimental.RNTupleReader
212
213 # Force load some dictionaries to work around ATLASRECTS-6261/ROOT-10940/ATEAM-942
214 if 'AtlasProject' in environ and environ['AtlasProject'] == 'Athena':
215 root.xAOD.Init().ignore()
216 root.xAOD.ParticleContainer_v1
217 root.xAOD.DiTauJetContainer_v1
218
219 if args.verbose:
220 msg.setLevel(logging.VERBOSE)
221 else:
222 msg.setLevel(logging.INFO)
223
224 from PyUtils.Helpers import ShutUp # noqa: F401
225
226 if args.entries == '':
227 args.entries = -1
228
229 msg.info('comparing tree [%s] in files:', args.tree_name)
230 msg.info(' old: [%s]', args.old)
231 msg.info(' new: [%s]', args.new)
232 msg.info('branches of interest: %s', args.branches_of_interest)
233 msg.info('ignore leaves: %s', args.ignore_leaves)
234 msg.info('enforce leaves: %s', args.enforce_leaves)
235 msg.info('leaves prefix: %s', args.leaves_prefix)
236 msg.info('hacks: %s', args.known_hacks)
237 msg.info('entries: %s', args.entries)
238 msg.info('mode: %s', args.mode)
239 msg.info('error mode: %s', args.error_mode)
240 msg.info('order trees: %s', args.order_trees)
241 msg.info('exact branches: %s', args.exact_branches)
242 msg.info('nan equal: %s', args.nan_equal)
243 msg.info('tolerance (rel, abs): %s', args.tolerances)
244
245 import PyUtils.Helpers as H
246 with H.ShutUp() :
247 fold = ru.RootFileDumper(args.old, args.tree_name)
248 fnew = ru.RootFileDumper(args.new, args.tree_name)
249 pass
250
251 def obj_info(obj, args):
252 if isinstance(obj, root.TTree):
253 return _tree_info(obj, args)
254 elif isinstance(obj, RNTupleReader):
255 with H.ShutUp(filters=[r'.+RuntimeWarning: class "[\w:]+" has no virtual destructor']):
256 return _reader_info(obj, args)
257 else:
258 raise NotImplementedError(f"'obj_info' not implemented for object of {type(obj)=}")
259
260 def _tree_info(tree, args):
261 nentries = tree.GetEntriesFast()
262 # l.GetBranch().GetName() gives the full leaf path name
263 leaves = [l.GetBranch().GetName() for l in tree.GetListOfLeaves()
264 if l.GetBranch().GetName() not in args.ignore_leaves]
265 if args.leaves_prefix:
266 leaves = [l.replace(args.leaves_prefix, '') for l in leaves]
267 return {
268 'entries': nentries,
269 'leaves': set(leaves),
270 }
271
272 def _reader_info(reader, args):
273 nentries = reader.GetNEntries()
274 try:
275 RFieldVisitor = root.Detail.RFieldVisitor
276 except AttributeError:
277 RFieldVisitor = root.Experimental.Detail.RFieldVisitor
278 class NameVisitor(RFieldVisitor):
279 def __init__(self, names):
280 super().__init__()
281 self.names = names
282 def VisitField(self, field):
283 if field.GetFieldName()[0] == '_':
284 return
285 self.names.append(field.GetQualifiedFieldName())
286 try:
287 # ROOT Version: 6.35.01
288 subFields = field.GetConstSubfields()
289 except AttributeError:
290 subFields = field.GetSubFields()
291 for f in subFields:
292 # ROOT Version: 6.35.01
293 # for f in field.GetConstSubfields():
294 f.AcceptVisitor(self)
295 def VisitFieldZero(self, field):
296 try:
297 # ROOT Version: 6.35.01
298 subFields = field.GetConstSubfields()
299 except AttributeError:
300 subFields = field.GetSubFields()
301 for f in subFields:
302 # ROOT Version: 6.35.01
303 # for f in field.GetConstSubfields():
304 f.AcceptVisitor(self)
305
306 fieldZero = reader.GetModel().GetConstFieldZero()
307 names = list()
308 visitor = NameVisitor(names)
309 fieldZero.AcceptVisitor(visitor)
310 leaves = visitor.names
311 leaves = [l for l in leaves if l not in args.ignore_leaves]
312 if args.leaves_prefix:
313 leaves = [l.replace(args.leaves_prefix, '') for l in leaves]
314 return {
315 'entries': nentries,
316 'leaves': set(leaves),
317 }
318
319 def ordered_indices(obj, reverse_order=False):
320 if isinstance(obj, root.TTree):
321 return _tree_ordered_indices(obj, reverse_order)
322 elif isinstance(obj, RNTupleReader):
323 return _reader_ordered_indices(obj, reverse_order)
324 else:
325 raise NotImplementedError(f"'ordered_indices' not implemented for object of {type(obj)=}")
326
327 def _reader_ordered_indices(reader, reverse_order=False):
328 import operator
329
330 dict_in = {}
331 nevts = reader.GetNEntries()
332
333 eiDict = {(): ['EventInfoAuxDyn:eventNumber'],
334 ('eventNumber',): ['EventInfoAux:',
335 'Bkg_EventInfoAux:',
336 'xAOD::EventAuxInfo_v3_EventInfoAux:',
337 'xAOD::EventAuxInfo_v2_EventInfoAux:',
338 'xAOD::EventAuxInfo_v1_EventInfoAux:',
339 'xAOD::EventAuxInfo_v3_Bkg_EventInfoAux:',
340 'xAOD::EventAuxInfo_v2_Bkg_EventInfoAux:',
341 'xAOD::EventAuxInfo_v1_Bkg_EventInfoAux:'],
342 ('m_event_ID', 'm_event_number'): ['McEventInfo',
343 'ByteStreamEventInfo',
344 'EventInfo_p4_McEventInfo',
345 'EventInfo_p4_ByteStreamEventInfo']}
346
347 def find_attrs():
348 """Find the relevant attributes for reading the event number"""
349 try:
350 kInvalidDescriptorId = root.kInvalidDescriptorId
351 except AttributeError:
352 kInvalidDescriptorId = root.Experimental.kInvalidDescriptorId
353 for path, names in eiDict.items():
354 for name in names:
355 if (fieldId := reader.GetDescriptor().FindFieldId(name)) != kInvalidDescriptorId:
356 typeName = reader.GetDescriptor().GetFieldDescriptor(fieldId).GetTypeName()
357 return (name, typeName), path
358 else:
359 return None, None
360
361 name, attrs = find_attrs()
362 if name is None or attrs is None:
363 msg.error('Cannot read event info, will bail out.')
364 msg.error(f"Tried {name=} and attributes {attrs=}")
365 return []
366
367 view = reader.GetView[name[1]](name[0])
368 for idx in range(nevts):
369 if idx % 100 == 0:
370 msg.debug('Read {} events from the input so far'.format(idx))
371 value = view(idx)
372 event_number = reduce(getattr, attrs, value)
373 msg.debug('Idx : EvtNum {:10d} : {}'.format(idx, event_number))
374 dict_in[idx] = event_number
375
376 # Sort the dictionary by event numbers
377 dict_out = dict(sorted(dict_in.items(), key=operator.itemgetter(1), reverse=reverse_order))
378
379 # Write out the ordered index and event number pairs
380 return list(dict_out.items())
381
382 def _tree_ordered_indices(tree, reverse_order=False):
383 from collections import OrderedDict
384 import operator
385
386 dict_in = {}
387 nevts = tree.GetEntriesFast()
388
389 eiDict = {'':['EventInfoAuxDyn.eventNumber'],
390 'eventNumber':['EventInfoAux.',
391 'Bkg_EventInfoAux.',
392 'xAOD::EventAuxInfo_v3_EventInfoAux.',
393 'xAOD::EventAuxInfo_v2_EventInfoAux.',
394 'xAOD::EventAuxInfo_v1_EventInfoAux.',
395 'xAOD::EventAuxInfo_v3_Bkg_EventInfoAux.',
396 'xAOD::EventAuxInfo_v2_Bkg_EventInfoAux.',
397 'xAOD::EventAuxInfo_v1_Bkg_EventInfoAux.'],
398 'm_event_ID m_event_number':['McEventInfo',
399 'ByteStreamEventInfo',
400 'EventInfo_p4_McEventInfo',
401 'EventInfo_p4_ByteStreamEventInfo']}
402
403 def find_attrs():
404 """Find the relevant attributes for reading the event number"""
405 for ii, jj in eiDict.items():
406 for kk in jj:
407 if hasattr(tree, kk):
408 return kk, ii
409 else:
410 return None, None
411
412 tree.GetEntry(0)
413 attr1, attr2 = find_attrs()
414 if attr1 is None or attr2 is None:
415 msg.error('Cannot read event info, will bail out.')
416 msg.error(f"Tried attributes {attr1} and {attr2}")
417 return []
418 attrs = [attr1] + attr2.split()
419
420 tree.SetBranchStatus ('*', 0)
421 tree.SetBranchStatus (attr1, 1)
422
423 for idx in range(0, nevts):
424 if idx % 100 == 0:
425 msg.debug('Read {} events from the input so far'.format(idx))
426 tree.GetEntry(idx)
427 event_number = reduce(getattr, attrs, tree)
428 msg.debug('Idx : EvtNum {:10d} : {}'.format(idx,event_number))
429 dict_in[idx] = event_number
430
431 tree.SetBranchStatus ('*', 1)
432
433 # Sort the dictionary by event numbers
434 dict_out = OrderedDict(sorted(dict_in.items(), key=operator.itemgetter(1), reverse = reverse_order))
435
436 # Write out the ordered index and event number pairs
437 return [(idx, ival) for idx, ival in dict_out.items()]
438
439 def diff_obj(fold, fnew, args):
440
441 infos = {
442 'old' : obj_info(fold.obj, args),
443 'new' : obj_info(fnew.obj, args),
444 }
445
446 nentries = min(infos['old']['entries'],
447 infos['new']['entries'])
448 itr_entries = nentries
449 if args.entries in (-1,'','-1'):
450 #msg.info('comparing over [%s] entries...', nentries)
451 itr_entries = nentries
452 if infos['old']['entries'] != infos['new']['entries']:
453 msg.info('different numbers of entries:')
454 msg.info(' old: [%s]', infos['old']['entries'])
455 msg.info(' new: [%s]', infos['new']['entries'])
456 msg.info('=> comparing [%s] first entries...', nentries)
457 else:
458 itr_entries = args.entries
459 pass
460 msg.info('comparing over [%s] entries...', itr_entries)
461
462 @cache
463 def skip_leaf(name_from_dump, skip_leaves):
464 """ Here decide if the current leaf should be skipped.
465 Previously the matching was done based on the full or partial
466 leaf name. E.g. foo.bar.zzz would be skipped if any of the
467 following were provided:
468 * foo
469 * foo.bar
470 * foo.bar.zzz
471 * Any of the foo, bar, or zzz
472 Now, we make a regex matching such that the user doesn't
473 need to provide full branch names.
474 """
475 for pattern in skip_leaves:
476 try:
477 if re.match(pattern, name_from_dump):
478 return True
479 except re.error as e:
480 from traceback import format_exception
481 msg.error("Exception '%s', pattern %r, line %s, column %s\n%s",
482 e, e.pattern, e.lineno, e.colno, "".join(format_exception(e)))
483 continue
484 else:
485 return False
486
487 @cache
488 def skip_leaf_entry(entry2, skip_leaves):
489 leafname = '.'.join(s for s in entry2 if not s.isdigit())
490 return skip_leaf (leafname, skip_leaves)
491
492 @cache
493 def leaf_tolerance(name_from_dump):
494 """Return (rel, abs) tolerance for leaf"""
495 for pattern, tol in args.tolerances.items():
496 if re.match(pattern, name_from_dump):
497 return tol
498 return (None, None)
499
500 def filter_branches(leaves):
501 matches = set()
502 for regex in args.branches_of_interest:
503 test = re.compile(regex)
504 matches.update({l for l in leaves if test.match(l)})
505 return matches
506
507 skipset = frozenset(args.ignore_leaves)
508 removed_leaves = infos['old']['leaves'] - infos['new']['leaves']
509 added_leaves = infos['new']['leaves'] - infos['old']['leaves']
510
511 if args.branches_of_interest:
512 removed_leaves = filter_branches(removed_leaves)
513 added_leaves = filter_branches(added_leaves)
514 else:
515 removed_leaves = {l for l in removed_leaves if not skip_leaf(l, skipset)}
516 added_leaves = {l for l in added_leaves if not skip_leaf(l, skipset)}
517
518 if removed_leaves:
519 removed_leaves_list = list(removed_leaves)
520 removed_leaves_list.sort()
521 if args.exact_branches:
522 msg.error('the following variables exist only in the old file !')
523 for l in removed_leaves_list:
524 msg.error(' - [%s]', l)
525 else:
526 msg.warning('the following variables exist only in the old file !')
527 for l in removed_leaves_list:
528 msg.warning(' - [%s]', l)
529 if added_leaves:
530 added_leaves_list = list(added_leaves)
531 added_leaves_list.sort()
532 if args.exact_branches:
533 msg.error('the following variables exist only in the new file !')
534 for l in added_leaves_list:
535 msg.error(' - [%s]', l)
536 else:
537 msg.warning('the following variables exist only in the new file !')
538 for l in added_leaves_list:
539 msg.warning(' - [%s]', l)
540
541 # need to remove trailing dots as they confuse reach_next()?
542 skip_leaves = [ l.rstrip('.') for l in removed_leaves | added_leaves | set(args.ignore_leaves) ]
543 for l in skip_leaves:
544 msg.debug('skipping [%s]', l)
545 skip_leaves = frozenset (skip_leaves)
546
547 if isinstance(fold.obj, root.TTree):
548 oldBranches = set(b.GetName().rstrip('\0') for b in fold.tree.GetListOfBranches())
549 elif isinstance(fold.obj, RNTupleReader):
550 oldBranches = {f.GetFieldName() for f in fold.obj.GetDescriptor().GetTopLevelFields()}
551 if isinstance(fnew.obj, root.TTree):
552 newBranches = set(b.GetName().rstrip('\0') for b in fnew.tree.GetListOfBranches())
553 elif isinstance(fnew.obj, root.RNTupleReader):
554 newBranches = {f.GetFieldName() for f in fnew.obj.GetDescriptor().GetTopLevelFields()}
555 branches = oldBranches & newBranches
556
557 if args.branches_of_interest:
558 branches_of_interest = args.branches_of_interest
559
560 # check that all branches of interest exist in the new file
561 for regex in branches_of_interest:
562 test = re.compile(regex)
563 if not {l for l in infos['new']['leaves'] if test.match(l)}:
564 msg.error(f'no match in new file for branch of interest: {regex}')
565
566 BOI_matches = set()
567 for branch_of_interest in branches_of_interest:
568 try:
569 r = re.compile(branch_of_interest)
570 BOI_matches.update(filter(r.match, branches))
571 except TypeError:
572 continue
573
574 if len(BOI_matches)<1:
575 msg.error('No matching branches found in both files for supplied branches of interest, quitting.')
576 return 1
577 msg.info('only the following branches of interest will be compared: ')
578 for l in BOI_matches:
579 msg.info(' - [%s]', l)
580 branches = BOI_matches
581
582 msg.info('comparing [%s] leaves over entries...', len(infos['old']['leaves'] & infos['new']['leaves']))
583 n_good = 0
584 n_bad = 0
585 if args.exact_branches:
586 n_bad += len(removed_leaves) + len(added_leaves)
587 import collections
588 summary = collections.defaultdict(int)
589
590 def get_event_range(entry):
591 smin, smax = 0, None
592 # Parse user input
593 if isinstance(entry, str):
594 # We support three main cases in this format: 5:10 (5th to 10th),
595 # 5: (5th to the end), and :5 (from the start to 5th)
596 if ':' in entry:
597 vals = entry.split(':')
598 smin = int(vals[0]) if len(vals) > 0 and vals[0].isdigit() else 0
599 smax = int(vals[1]) if len(vals) > 1 and vals[1].isdigit() else None
600 # This is the case where the user inputs the total number of events
601 elif entry.isdigit():
602 smin = 0
603 smax = int(entry) if int(entry) > 0 else None
604 # Handle the case where the input is a number (i.e. default)
605 elif isinstance(entry, int):
606 smin = 0
607 smax = entry if entry > 0 else None
608 # If we come across an unhandled case, bail out
609 else:
610 msg.warning(f"Unknown entries argument {entry}, will compare all events...")
611 msg.debug(f"Event slice is parsed as [{smin},{smax}]")
612 return smin, smax
613
614 if args.order_trees:
615 smin, smax = get_event_range(itr_entries)
616 msg.debug("Indices/Event Numbers of old events ...")
617 idx_old = ordered_indices(fold.obj)[smin:smax]
618 msg.debug("Indices/Event Numbers of new events ...")
619 idx_new = ordered_indices(fnew.obj)[smin:smax]
620 itr_entries_old, event_numbers_old = list(map(list,zip(*idx_old)))
621 itr_entries_new, event_numbers_new = list(map(list,zip(*idx_new)))
622 msg.debug(f"List of old indices {itr_entries_old}")
623 msg.debug(f"List of new indices {itr_entries_new}")
624 msg.debug(f"List of old events {event_numbers_old}")
625 msg.debug(f"List of new events {event_numbers_new}")
626 if event_numbers_old != event_numbers_new:
627 msg.error('Events differ, quitting!')
628 msg.error(f"List of old events {event_numbers_old}")
629 msg.error(f"List of new events {event_numbers_new}")
630 return 1
631 else:
632 itr_entries_old = itr_entries
633 itr_entries_new = itr_entries
634
635 branches = sorted(branches)
636 old_dump_iter = fold.dump(args.tree_name, itr_entries_old, branches, True, False)
637 new_dump_iter = fnew.dump(args.tree_name, itr_entries_new, branches, True, False)
638 old_skip_dict = {}
639 new_skip_dict = {}
640
641 def leafname_fromdump(entry):
642 if entry is None:
643 return None
644 else:
645 return '.'.join(s for s in entry[2] if not s.isdigit())
646
647 def elindices_fromdump(entry):
648 if entry is None:
649 return None
650 else:
651 return [int(s) for s in entry[2] if s.isdigit()]
652
653 def reach_next(dump_iter, skip_leaves, skip_dict, leaves_prefix=None):
654 keep_reading = True
655 while keep_reading:
656 try:
657 entry = next(dump_iter)
658 except StopIteration:
659 return None
660
661 # entry is a tuple with these contents:
662 # entry[0]: Name of the tree being read
663 # entry[1]: Index of the current event in the tree
664 # entry[2]: Leaf being dumped, split at periods.
665 # So for a variable of an xAOD object, entry[2][0]
666 # is the aux container name (ending in 'Aux.') and
667 # entry[2][1] is the variable name.
668 # entry[3]: The value of the variable.
669 # For an xAOD variable, this will typically be a std::vector.
670
671 #if entry[2][0].find('xAOD::TrackParticleAuxContainer_v5_HLT_xAOD__TrackParticleContainer_InDetTrigTrackingxAODCnv_Bphysics_FTFAux') >= 0:
672 # print ('aaa', entry)
673
674 entry2_orig = entry[2][0]
675 if isinstance(fold.obj, root.TTree):
676 entry[2][0] = entry[2][0].rstrip('.\0') # clean branch name
677 elif isinstance(fold.obj, RNTupleReader):
678 entry[2][0] = entry[2][0].rstrip(':') # clean branch name
679 if leaves_prefix:
680 entry[2][0] = entry[2][0].replace(leaves_prefix, '')
681
682 # Check whether we should skip this leaf, memoizing
683 # the result.
684 # Earlier we tried to do this by looking at only the first
685 # element of entry[2]. On the first event we would record
686 # whether any leaves were skipped for this branch, so on
687 # subsequent events we can quickly test that no leaves
688 # were skipped. However, that turned out not to work
689 # because for xAOD variables that are vectors (and are
690 # thus stored as nested vectors), we don't see them at all
691 # if the container is empty.
692 # So instead just memoize the result of skip_leaf.
693 leafname = leafname_fromdump(entry)
694 skip = skip_dict.setdefault (entry2_orig, None)
695 if skip is None:
696 skip = skip_leaf(leafname, skip_leaves)
697 if not skip:
698 return entry
699 msg.debug('SKIP: {}'.format(leafname))
700 return None
701
702 read_old = True
703 read_new = True
704 d_old = None
705 d_new = None
706
707 while True:
708 if read_old:
709 d_old = reach_next(old_dump_iter, skip_leaves, old_skip_dict, args.leaves_prefix)
710 if read_new:
711 d_new = reach_next(new_dump_iter, skip_leaves, new_skip_dict, args.leaves_prefix)
712
713 if not d_new and not d_old:
714 break
715
716 read_old = True
717 read_new = True
718
719 if (args.order_trees and d_old and d_new and d_old[2:] == d_new[2:]) or d_old == d_new:
720 n_good += 1
721 continue
722
723 if d_old:
724 tree_name, ientry, iname, iold = d_old
725 else:
726 msg.debug("try to delete 'ientry', 'iname', 'iold'")
727 try: del ientry, iname, iold
728 except NameError: pass
729 if d_new:
730 tree_name, jentry, jname, inew = d_new
731 else:
732 msg.debug("try to delete 'jentry', 'jname', 'inew'")
733 try: del jentry, jname, inew
734 except NameError: pass
735
736 if not d_old:
737 # FIXME: that's a plain (temporary?) hack
738 if jname[-1] in args.known_hacks:
739 continue
740 fold.allgood = False
741 summary[leafname_fromdump(d_new)] += 1
742 n_bad += 1
743 continue
744 elif not d_new:
745 # FIXME: that's a plain (temporary?) hack
746 if iname[-1] in args.known_hacks:
747 continue
748 fnew.allgood = False
749 summary[leafname_fromdump(d_old)] += 1
750 n_bad += 1
751 continue
752
753 rel_tol = None
754 abs_tol = None
755 if args.tolerances is not None:
756 rel_tol, abs_tol = leaf_tolerance(leafname_fromdump(d_old))
757
758 idiff = _vecdiff (iold, inew, args.nan_equal, rel_tol, abs_tol)
759
760 if idiff is None:
761 n_good += 1
762 continue
763 elif idiff >= 0:
764 iold = iold[idiff]
765 inew = inew[idiff]
766 iname.insert(-1, str(idiff))
767 jname.insert(-1, str(idiff))
768
769 # for regression testing we should have NAN == NAN
770 if args.nan_equal:
771 if all([isinstance(x,Real) and isnan(x) for x in [iold,inew]]):
772 n_good += 1
773 continue
774
775 # FIXME: that's a plain (temporary?) hack
776 if iname[-1] in args.known_hacks or jname[-1] in args.known_hacks:
777 continue
778
779 n_bad += 1
780
781 # Identifiers are event numbers if we're ordering the trees, otherwise tree indices
782 if args.order_trees:
783 id_old = dict(idx_old)[ientry]
784 id_new = dict(idx_new)[jentry]
785 else:
786 id_old = ientry
787 id_new = jentry
788
789 if not args.order_trees:
790 in_synch = d_old and d_new and d_old[:-1] == d_new[:-1]
791 else:
792 in_synch = d_old and d_new and d_old[0] == d_new[0] and d_old[2] == d_new[2] and id_old == id_new
793 if not in_synch:
794 if _is_detailed(args):
795 if d_old:
796 msg.info('::sync-old %s','.'.join(["%03i"%ientry]+d_old[2]))
797 else:
798 msg.info('::sync-old ABSENT')
799 if d_new:
800 msg.info('::sync-new %s','.'.join(["%03i"%jentry]+d_new[2]))
801 else:
802 msg.info('::sync-new ABSENT')
803 pass
804 # remember for later
805 if not d_old:
806 fold.allgood = False
807 summary[leafname_fromdump(d_new)] += 1
808 elif not d_new:
809 fnew.allgood = False
810 summary[leafname_fromdump(d_old)] += 1
811 else:
812 branch_old = f"{id_old}.{d_old[2][0]}"
813 branch_new = f"{id_new}.{d_new[2][0]}"
814 leaf_old = leafname_fromdump(d_old)
815 leaf_new = leafname_fromdump(d_new)
816 indices_old = elindices_fromdump(d_old)
817 indices_new = elindices_fromdump(d_new)
818 # Branches/Leaves are alphabetically ordered
819 # If we're out-of-sync, we try to figure out
820 # if we should advance the old or the new branch
821 # For same branches, we look at the full leaf name
822 # If that fails we look at the indices
823 if branch_old > branch_new:
824 read_old = False
825 elif branch_old < branch_new:
826 read_new = False
827 else:
828 if leaf_old > leaf_new:
829 read_old = False
830 elif leaf_old < leaf_new:
831 read_new = False
832 elif indices_old and indices_new and len(indices_old) == len(indices_new):
833 if indices_old > indices_new:
834 read_old = False
835 elif indices_old < indices_new:
836 read_new = False
837 # Let's see if we can reconcile
838 # If not, just bail out to avoid false positivies
839 if read_old and not read_new:
840 if _is_detailed(args):
841 msg.info('::sync-old skipping entry')
842 fold.allgood = False
843 summary[leaf_old] += 1
844 elif read_new and not read_old:
845 if _is_detailed(args):
846 msg.info('::sync-new skipping entry')
847 fnew.allgood = False
848 summary[leaf_new] += 1
849 else:
850 msg.error('::sync attempt failed, bailing out...')
851 msg.error(f"::sync-old Leaf vs Index : {leaf_old} vs {indices_old}")
852 msg.error(f"::sync-new Leaf vs Index : {leaf_new} vs {indices_new}")
853 fold.allgood = False
854 fnew.allgood = False
855 summary[leaf_old] += 1
856 summary[leaf_new] += 1
857 break
858
859 if _is_exit_early(args):
860 msg.info('*** exit on first error ***')
861 break
862 continue
863
864 if not args.order_trees:
865 n = '.'.join(["%03i"%ientry]+iname)
866 else:
867 n = '.'.join(["%03i"%ientry]+iname+["%03i"%jentry]+jname)
868
869 if _is_detailed(args):
870 diff_value = 'N/A'
871 try:
872 # difference calculated according to math.isclose
873 diff_value = 100.* (inew-iold) / max(abs(iold),abs(inew))
874 diff_value = '%.8f%%' % (diff_value,)
875 except Exception:
876 pass
877 msg.info('%s %r -> %r => diff= [%s]', n, iold, inew, diff_value)
878
879 summary[leafname_fromdump(d_old)] += 1
880
881 if iname[0] in args.enforce_leaves or jname[0] in args.enforce_leaves:
882 msg.info("don't compare further")
883 break
884 pass # loop over events/branches
885
886 msg.info('Found [%s] identical leaves', n_good)
887 msg.info('Found [%s] different leaves', n_bad)
888
889 if not _is_summary(args):
890 keys = sorted(summary.keys())
891 for n in keys:
892 v = summary[n]
893 msg.info(' [%s]: %i leaves differ', n, v)
894 pass
895 pass
896
897 if (not fold.allgood) or (not fnew.allgood):
898 msg.error('NOTE: there were errors during the dump')
899 msg.info('fold.allgood: %s' , fold.allgood)
900 msg.info('fnew.allgood: %s' , fnew.allgood)
901 n_bad += 0.5
902 return n_bad
903
904 if (isinstance(fold.obj, root.TTree) and isinstance(fnew.obj, root.TTree) or
905 isinstance(fold.obj, RNTupleReader) and isinstance(fnew.obj, RNTupleReader)):
906 ndiff = diff_obj(fold, fnew, args)
907 else:
908 raise NotImplementedError("Cannot compare object of type=%s to object of type=%s" % (type(fold.obj), type(fnew.obj)))
909 if ndiff != 0:
910 msg.error('files differ!')
911 return 2
912 msg.info('all good.')
913 return 0
STL class.
_cmp(r1, r2, nan_equal, rel_tol=None, abs_tol=None)
_is_detailed(args)
classes ----------------------------------------------------------------—
_vecdiff(v1, v2, nan_equal, rel_tol=None, abs_tol=None)
IovVectorMap_t read(const Folder &theFolder, const SelectionCriterion &choice, const unsigned int limit=10)