198 """diff two ROOT files (containers and sizes)"""
204 gc.set_threshold (100000)
206 import PyUtils.RootUtils
as ru
207 root = ru.import_root()
209 RNTupleReader = root.RNTupleReader
210 except AttributeError:
211 RNTupleReader = root.Experimental.RNTupleReader
214 if 'AtlasProject' in environ
and environ[
'AtlasProject'] ==
'Athena':
215 root.xAOD.Init().ignore()
216 root.xAOD.ParticleContainer_v1
217 root.xAOD.DiTauJetContainer_v1
220 msg.setLevel(logging.VERBOSE)
222 msg.setLevel(logging.INFO)
224 from PyUtils.Helpers
import ShutUp
226 if args.entries ==
'':
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)
245 import PyUtils.Helpers
as H
247 fold = ru.RootFileDumper(args.old, args.tree_name)
248 fnew = ru.RootFileDumper(args.new, args.tree_name)
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)
258 raise NotImplementedError(f
"'obj_info' not implemented for object of {type(obj)=}")
260 def _tree_info(tree, args):
261 nentries = tree.GetEntriesFast()
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]
269 'leaves':
set(leaves),
272 def _reader_info(reader, args):
273 nentries = reader.GetNEntries()
275 RFieldVisitor = root.Detail.RFieldVisitor
276 except AttributeError:
277 RFieldVisitor = root.Experimental.Detail.RFieldVisitor
278 class NameVisitor(RFieldVisitor):
279 def __init__(self, names):
282 def VisitField(self, field):
283 if field.GetFieldName()[0] ==
'_':
285 self.names.append(field.GetQualifiedFieldName())
288 subFields = field.GetConstSubfields()
289 except AttributeError:
290 subFields = field.GetSubFields()
294 f.AcceptVisitor(self)
295 def VisitFieldZero(self, field):
298 subFields = field.GetConstSubfields()
299 except AttributeError:
300 subFields = field.GetSubFields()
304 f.AcceptVisitor(self)
306 fieldZero = reader.GetModel().GetConstFieldZero()
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]
316 'leaves':
set(leaves),
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)
325 raise NotImplementedError(f
"'ordered_indices' not implemented for object of {type(obj)=}")
327 def _reader_ordered_indices(reader, reverse_order=False):
331 nevts = reader.GetNEntries()
333 eiDict = {(): [
'EventInfoAuxDyn:eventNumber'],
334 (
'eventNumber',): [
'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']}
348 """Find the relevant attributes for reading the event number"""
350 kInvalidDescriptorId = root.kInvalidDescriptorId
351 except AttributeError:
352 kInvalidDescriptorId = root.Experimental.kInvalidDescriptorId
353 for path, names in eiDict.items():
355 if (fieldId := reader.GetDescriptor().FindFieldId(name)) != kInvalidDescriptorId:
356 typeName = reader.GetDescriptor().GetFieldDescriptor(fieldId).GetTypeName()
357 return (name, typeName), path
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=}")
367 view = reader.GetView[name[1]](name[0])
368 for idx in range(nevts):
370 msg.debug('Read {} events from the input so far'.format(idx))
372 event_number = reduce(getattr, attrs, value)
373 msg.debug('Idx : EvtNum {:10d} : {}'.format(idx, event_number))
374 dict_in[idx] = event_number
376 # Sort the dictionary by event numbers
377 dict_out = dict(sorted(dict_in.items(), key=operator.itemgetter(1), reverse=reverse_order))
379 # Write out the ordered index and event number pairs
380 return list(dict_out.items())
382 def _tree_ordered_indices(tree, reverse_order=False):
383 from collections import OrderedDict
387 nevts = tree.GetEntriesFast()
389 eiDict = {'':['EventInfoAuxDyn.eventNumber'],
390 'eventNumber':['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']}
404 """Find the relevant attributes for reading the event numbe
r"""
405 for ii, jj in eiDict.items():
407 if hasattr(tree, kk):
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}")
418 attrs = [attr1] + attr2.split()
420 tree.SetBranchStatus ('*', 0)
421 tree.SetBranchStatus (attr1, 1)
423 for idx in range(0, nevts):
425 msg.debug('Read {} events from the input so far'.format(idx))
427 event_number = reduce(getattr, attrs, tree)
428 msg.debug('Idx : EvtNum {:10d} : {}'.format(idx,event_number))
429 dict_in[idx] = event_number
431 tree.SetBranchStatus ('*', 1)
433 # Sort the dictionary by event numbers
434 dict_out = OrderedDict(sorted(dict_in.items(), key=operator.itemgetter(1), reverse = reverse_order))
436 # Write out the ordered index and event number pairs
437 return [(idx, ival) for idx, ival in dict_out.items()]
439 def diff_obj(fold, fnew, args):
442 'old' : obj_info(fold.obj, args),
443 'new' : obj_info(fnew.obj, args),
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)
458 itr_entries = args.entries
460 msg.info('comparing over [%s] entries...', itr_entries)
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:
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.
475 for pattern in skip_leaves:
477 if re.match(pattern, name_from_dump):
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)))
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)
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):
500 def filter_branches(leaves):
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)})
507 skipset = frozenset(args.ignore_leaves)
508 removed_leaves = infos['old']['leaves'] - infos['new']['leaves']
509 added_leaves = infos['new']['leaves'] - infos['old']['leaves']
511 if args.branches_of_interest:
512 removed_leaves = filter_branches(removed_leaves)
513 added_leaves = filter_branches(added_leaves)
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)}
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)
526 msg.warning('the following variables exist only in the old file !')
527 for l in removed_leaves_list:
528 msg.warning(' - [%s]', l)
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)
537 msg.warning('the following variables exist only in the new file !')
538 for l in added_leaves_list:
539 msg.warning(' - [%s]', l)
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)
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
557 if args.branches_of_interest:
558 branches_of_interest = args.branches_of_interest
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}')
567 for branch_of_interest in branches_of_interest:
569 r = re.compile(branch_of_interest)
570 BOI_matches.update(filter(r.match, branches))
574 if len(BOI_matches)<1:
575 msg.error('No matching branches found in both files for supplied branches of interest, quitting.')
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
582 msg.info('comparing [%s] leaves over entries...', len(infos['old']['leaves'] & infos['new']['leaves']))
585 if args.exact_branches:
586 n_bad += len(removed_leaves) + len(added_leaves)
588 summary = collections.defaultdict(int)
590 def get_event_range(entry):
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)
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():
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):
607 smax = entry if entry > 0 else None
608 # If we come across an unhandled case, bail out
610 msg.warning(f"Unknown entries argument {entry}, will compare all events...")
611 msg.debug(f"Event slice is parsed as [{smin},{smax}]")
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}")
632 itr_entries_old = itr_entries
633 itr_entries_new = itr_entries
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)
641 def leafname_fromdump(entry):
645 return '.'.join(s for s in entry[2] if not s.isdigit())
647 def elindices_fromdump(entry):
651 return [int(s) for s in entry[2] if s.isdigit()]
653 def reach_next(dump_iter, skip_leaves, skip_dict, leaves_prefix=None):
657 entry = next(dump_iter)
658 except StopIteration:
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.
671 #if entry[2][0].find('xAOD::TrackParticleAuxContainer_v5_HLT_xAOD__TrackParticleContainer_InDetTrigTrackingxAODCnv_Bphysics_FTFAux') >= 0:
672 # print ('aaa', entry)
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
680 entry[2][0] = entry[2][0].replace(leaves_prefix, '')
682 # Check whether we should skip this leaf, memoizing
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)
696 skip = skip_leaf(leafname, skip_leaves)
699 msg.debug('SKIP: {}'.format(leafname))
709 d_old = reach_next(old_dump_iter, skip_leaves, old_skip_dict, args.leaves_prefix)
711 d_new = reach_next(new_dump_iter, skip_leaves, new_skip_dict, args.leaves_prefix)
713 if not d_new and not d_old:
719 if (args.order_trees and d_old and d_new and d_old[2:] == d_new[2:]) or d_old == d_new:
724 tree_name, ientry, iname, iold = d_old
726 msg.debug("try to delete 'ientry', 'iname', 'iold'")
727 try: del ientry, iname, iold
728 except NameError: pass
730 tree_name, jentry, jname, inew = d_new
732 msg.debug("try to delete 'jentry', 'jname', 'inew'")
733 try: del jentry, jname, inew
734 except NameError: pass
737 # FIXME: that's a plain (temporary?) hack
738 if jname[-1] in args.known_hacks:
741 summary[leafname_fromdump(d_new)] += 1
745 # FIXME: that's a plain (temporary?) hack
746 if iname[-1] in args.known_hacks:
749 summary[leafname_fromdump(d_old)] += 1
755 if args.tolerances is not None:
756 rel_tol, abs_tol = leaf_tolerance(leafname_fromdump(d_old))
758 idiff = _vecdiff (iold, inew, args.nan_equal, rel_tol, abs_tol)
766 iname.insert(-1, str(idiff))
767 jname.insert(-1, str(idiff))
769 # for regression testing we should have NAN == NAN
771 if all([isinstance(x,Real) and isnan(x) for x in [iold,inew]]):
775 # FIXME: that's a plain (temporary?) hack
776 if iname[-1] in args.known_hacks or jname[-1] in args.known_hacks:
781 # Identifiers are event numbers if we're ordering the trees, otherwise tree indices
783 id_old = dict(idx_old)[ientry]
784 id_new = dict(idx_new)[jentry]
789 if not args.order_trees:
790 in_synch = d_old and d_new and d_old[:-1] == d_new[:-1]
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
794 if _is_detailed(args):
796 msg.info('::sync-old %s','.'.join(["%03i"%ientry]+d_old[2]))
798 msg.info('::sync-old ABSENT')
800 msg.info('::sync-new %s','.'.join(["%03i"%jentry]+d_new[2]))
802 msg.info('::sync-new ABSENT')
807 summary[leafname_fromdump(d_new)] += 1
810 summary[leafname_fromdump(d_old)] += 1
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:
825 elif branch_old < branch_new:
828 if leaf_old > leaf_new:
830 elif leaf_old < leaf_new:
832 elif indices_old and indices_new and len(indices_old) == len(indices_new):
833 if indices_old > indices_new:
835 elif indices_old < indices_new:
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')
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')
848 summary[leaf_new] += 1
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}")
855 summary[leaf_old] += 1
856 summary[leaf_new] += 1
859 if _is_exit_early(args):
860 msg.info('*** exit on first error ***')
864 if not args.order_trees:
865 n = '.'.join(["%03i"%ientry]+iname)
867 n = '.'.join(["%03i"%ientry]+iname+["%03i"%jentry]+jname)
869 if _is_detailed(args):
872 # difference calculated according to math.isclose
873 diff_value = 100.* (inew-iold) / max(abs(iold),abs(inew))
874 diff_value = '%.8f%%' % (diff_value,)
877 msg.info('%s %r -> %r => diff= [%s]', n, iold, inew, diff_value)
879 summary[leafname_fromdump(d_old)] += 1
881 if iname[0] in args.enforce_leaves or jname[0] in args.enforce_leaves:
882 msg.info("don't compare further")
884 pass # loop over events/branches
886 msg.info('Found [%s] identical leaves', n_good)
887 msg.info('Found [%s] different leaves', n_bad)
889 if not _is_summary(args):
890 keys = sorted(summary.keys())
893 msg.info(' [%s]: %i leaves differ', n, v)
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)
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)
908 raise NotImplementedError("Cannot compare object of type=%s to object of type=%s" % (type(fold.obj), type(fnew.obj)))
910 msg.error('files differ!')
912 msg.info('all good.')