Source code for pycif.plugins.obsoperators.standard.transforms.utils.propagate_attributes

import copy
import itertools
from logging import warning, debug
import pandas as pd
import numpy as np

from . import init_default_transformations
from .initiate_default_attributes import initiate_default_attributes


print_all = False
# attribute2print = "input_dates"
attribute2print = "sparse_data"

transfor2print = [
    # 'transtest_2_after',
    # 'dump2inputs_std_00009',
    # 'time_interpolation_std_00021',
    # 'loadfromoutputs_std_00050',
    # 'run_model'
]


[docs] def propagate_attributes( self, all_transforms, mapper, transform_id, backup_comps=None, next_did_nothing=False, previous_did_nothing=False, only_backwards=False, only_forwards=False, parent_transform=None, parent_trids=None ): """Propagate attributes backward and forward when initializing a new transform.""" backup_comps = {} if backup_comps is None else backup_comps # If only_backwards and only_forwards, just skip if (only_backwards and only_forwards) or parent_trids == []: return # Initiate default values for some parameters if not explicitly specified in transforms # (e.g., is_lbc, is_top) initiate_default_attributes(all_transforms, mapper, transform_id) # Propagate information about forward or backward propagation priority to_propagate = [ "vdomain", "domain", "tracer", "sampled", "sparse_data", "is_lbc", "is_top", "input_dates", "input_files", "recombine_periods", "dirorig", "fileorig", "continuous_hdomain", "continuous_vdomain", "has_control_ancestor", "has_obsvect_successor", "all_successors_initialized" ] for attribute in to_propagate: propagate_attribute( self, all_transforms, mapper, [f"{attribute}_from_previous"], transform_id, only_backwards=True, next_did_nothing=next_did_nothing, parent_transform=parent_transform, parent_trids=parent_trids ) # Now propagate batches of attributes # For each direction, provide here the list of attributes # (some can go together) # For each attribute list, three arguments to be provided: # - whether or not to force propagation # - list of arguments # - how to deal with boolean: # -> if "or", propagates True if any # -> if None, will use the for_propagate argument to_propagate = { "only_forwards": [ (True, ["has_control_ancestor"], ["or"]) ], "only_backwards": [ (False, ["force_loadin"], ["or"]), (True, ["has_obsvect_successor"], ["or"]), (False, ["all_successors_initialized"], ["and"]) ], "both": [ (False, ["domain", "is_top", "is_lbc"], [None, None, None]), (False, ["continuous_hdomain"], [None]), (False, ["continuous_vdomain"], [None]), (False, ["tracer"], [None]), (False, ["sampled"], [None]), (False, ["sparse_data"], [None]), (False, ["input_dates"], [None]), (False, ["input_files"], [None]), (False, ["recombine_periods"], [None]), (False, ["dirorig", "fileorig"], [None]), ] } did_nothing_forwards = True did_nothing_backwards = True list_updated_output_trids = [] list_updated_input_trids = [] for direction, to_propagate_dir in to_propagate.items(): # Some attributes are to be propagated # in only one direction only_backwards_tmp = only_backwards only_forwards_tmp = only_forwards if direction == "only_forwards": only_forwards_tmp = True elif direction == "only_backwards": only_backwards_tmp = True for force_propagate, attributes, deal_boolean in to_propagate_dir: did_nothing_forwards_tmp, list_updated_output_trids_tmp, \ did_nothing_backwards_tmp, list_updated_input_trids_tmp = propagate_attribute( self, all_transforms, mapper, attributes, transform_id, backup_comps=backup_comps, only_backwards=only_backwards_tmp, only_forwards=only_forwards_tmp, next_did_nothing=next_did_nothing, previous_did_nothing=previous_did_nothing, parent_transform=parent_transform, parent_trids=parent_trids, force_propagate=force_propagate, deal_boolean=deal_boolean ) did_nothing_forwards = did_nothing_forwards and did_nothing_forwards_tmp did_nothing_backwards = did_nothing_backwards and did_nothing_backwards_tmp list_updated_output_trids.extend(list_updated_output_trids_tmp) list_updated_input_trids.extend(list_updated_input_trids_tmp) list_updated_input_trids = list(set(list_updated_input_trids)) list_updated_output_trids = list(set(list_updated_output_trids)) # Propagates backwards to precursors if not did_nothing_backwards or not next_did_nothing: precursors = mapper[transform_id]["precursors"] for trid in precursors: for tr in precursors[trid]: if tr == transform_id: continue propagate_attributes( self, all_transforms, mapper, tr, only_backwards=True, next_did_nothing=did_nothing_backwards, previous_did_nothing=True, parent_transform=transform_id, parent_trids=list_updated_input_trids if parent_transform is not None else None, ) # Propagates forwards to successors if not did_nothing_forwards or not previous_did_nothing: successors = mapper[transform_id]["successors"] for trid in successors: for tr in successors[trid]: if tr == transform_id: continue propagate_attributes( self, all_transforms, mapper, tr, only_forwards=True, previous_did_nothing=did_nothing_forwards, next_did_nothing=True, parent_transform=transform_id, parent_trids=list_updated_output_trids if parent_transform is not None else None, ) # If propagated arguments, try to initiate reprojections, reindex, etc. if not did_nothing_forwards or not did_nothing_backwards: init_default_transformations.init_default_transformations( self, all_transforms, backup_comps, mapper, transform_id, trid_to_check=list_updated_output_trids )
[docs] def propagate_attribute( self, all_transforms, mapper, attributes, transform_id, only_backwards=False, only_forwards=False, next_did_nothing=False, previous_did_nothing=False, backup_comps=None, parent_transform=None, parent_trids=None, force_propagate=False, deal_boolean=None ): backup_comps = {} if backup_comps is None else backup_comps did_nothing_backwards = True list_updated_input_trids = [] if not only_forwards: did_nothing_backwards, list_updated_input_trids = propagate_backwards( self, all_transforms, mapper, attributes, transform_id, next_did_nothing=next_did_nothing, backup_comps=backup_comps, parent_transform=parent_transform, parent_trids=parent_trids, force_propagate=force_propagate, deal_boolean=deal_boolean ) did_nothing = did_nothing_backwards did_nothing_forwards = True list_updated_output_trids = [] if not only_backwards: did_nothing_forwards, list_updated_output_trids = propagate_forwards( self, all_transforms, mapper, attributes, transform_id, previous_did_nothing=previous_did_nothing, backup_comps=backup_comps, parent_transform=parent_transform, parent_trids=parent_trids, force_propagate=force_propagate, deal_boolean=deal_boolean ) did_nothing = did_nothing_forwards return did_nothing_forwards, list_updated_output_trids, \ did_nothing_backwards, list_updated_input_trids
[docs] def propagate_backwards( self, all_transforms, mapper, attributes, transform_id, next_did_nothing=False, backup_comps=None, parent_transform=None, parent_trids=None, force_propagate=False, deal_boolean=None ): backup_comps = {} if backup_comps is None else backup_comps did_nothing = True transf_mapper = mapper[transform_id] # Print debug if need if transform_id in transfor2print: if attribute2print in attributes: debug( f"Backward propagation of {attribute2print} for {transform_id}: Before successors") for trid in mapper[transform_id]["outputs"]: if attribute2print in mapper[transform_id]["outputs"][trid]: debug(mapper[transform_id]["outputs"] [trid][attribute2print]) debug("\n\n\n") # Reformat deal_boolean if not provided if deal_boolean is None: deal_boolean = [None for attr in attributes] # If parent_transform, loop only on output trid with corresponding successors list_trid_outputs = copy.deepcopy(list(transf_mapper["outputs"].keys())) successors = transf_mapper["successors"] if parent_transform is not None: list_trid_outputs = [ trid for trid in transf_mapper["outputs"].keys() if parent_transform in successors[trid] ] # Propagate from successors did_nothing_successor = True list_updated_trid = [] for trid in list_trid_outputs: # Skip here if parent_transform is not in successors if parent_transform is not None: if parent_transform not in successors[trid]: continue # If attributes already in outputs, skip intersection = set( transf_mapper["outputs"][trid].keys()).intersection(attributes) if len(intersection) == len(attributes) \ and all([b is None for b in deal_boolean]): continue for tr in successors[trid]: # Propagate only from parent transform if tr != parent_transform and parent_transform is not None: continue # Propagate only parent trid if provided if parent_trids is not None: if trid not in parent_trids: continue # If not all attributes available in successor input, skip in_inputs = [ attr not in mapper[tr]["inputs"][trid] for attr in attributes ] if any(in_inputs): continue # If only one successor, just propagate ref_successor = transf_mapper["outputs"][trid].get( f"{attributes[0]}_from_successor", transform_id) if ref_successor == transform_id \ or ref_successor not in all_transforms.attributes: for attr in attributes: transf_mapper["outputs"][trid][attr] = mapper[tr]["inputs"][trid][attr] transf_mapper["outputs"][trid][f"{attr}_from_successor"] = tr did_nothing = False did_nothing_successor = False list_updated_trid.append(trid) continue # If multiple successors, check compatibility different_input_output = [ not compare_attribute( mapper[tr]["inputs"][trid][attr], transf_mapper["outputs"][trid][attr], deal_boolean=db ) for attr, db in zip(attributes, deal_boolean) ] # If not all values from successors are the same, # propagate conflict if any(different_input_output): warning( f"Could not propagate {attributes} backward from " f"{tr} to {transform_id}, due to conflict with {ref_successor} " f"being already used." ) did_nothing = False list_updated_trid.append(trid) for attr in attributes: transf_mapper["outputs"][trid][attr] = None transf_mapper["outputs"][trid][f"{attr}_conflict_propagation"] = True # From here, inputs and outputs are comparable # with special treatment if any boolean for db, attr in zip(deal_boolean, attributes): if db is None: continue boolean_value = compare_attribute( mapper[tr]["inputs"][trid][attr], transf_mapper["outputs"][trid][attr], deal_boolean=db, return_boolean_value=True ) transf_mapper["outputs"][trid][attr] = boolean_value transf_mapper["outputs"][trid][f"{attr}_from_successor"] = tr list_updated_trid.append(trid) # Print debug if need if transform_id in transfor2print: if attribute2print in attributes: debug( f"Backward propagation of {attribute2print} for {transform_id}: After successors") for trid in mapper[transform_id]["outputs"]: if attribute2print in mapper[transform_id]["outputs"][trid]: trid_dict = mapper[transform_id]["outputs"][trid] debug( f"\t- {trid}: {trid_dict[attribute2print]} / from_successor: {trid_dict.get(f'{attribute2print}_from_successor', 'no')}" ) debug("\n\n\n") # When parent_transform is not None, # Only propagate output attributes to relevant inputs list_trid_inputs = copy.deepcopy(list(transf_mapper["inputs"].keys())) if parent_transform is not None: list_trid_inputs = [ [ in_trid for in_trid in transf_mapper.get("outputs2inputs", {}).get(out_trid, [out_trid]) if in_trid in transf_mapper["inputs"] ] for out_trid in list_updated_trid ] list_trid_inputs = list(itertools.chain(*list_trid_inputs)) # Print debug if need if transform_id in transfor2print: if attribute2print in attributes: debug( f"Backward propagation of {attribute2print} for {transform_id}: Before outputs") for trid in mapper[transform_id]["inputs"]: if attribute2print in mapper[transform_id]["inputs"][trid]: debug(mapper[transform_id]["inputs"] [trid][attribute2print]) debug("\n\n\n") # Propagate outputs to inputs did_nothing_inputs = True list_updated_input_trids = [] for trid in set(list_trid_inputs): # If already in inputs, skip intersection = set( transf_mapper["inputs"][trid].keys()).intersection(attributes) if len(intersection) == len(attributes)\ and all([b is None for b in deal_boolean]): anyNone = [transf_mapper["inputs"][trid][attr] is None for attr in attributes] if sum(anyNone) == 0: continue # Skip if value in input is not the default value if not all([b is None for b in deal_boolean]) and len(intersection) == len(attributes): default_values = [ transf_mapper["inputs"][trid].get(f"{attr}_default", False) for attr in attributes ] if not all(default_values): continue # Check whether trid is linked to some outputs inputs2outputs = transf_mapper.get("inputs2outputs", {})[trid] output_attributes = [ [ transf_mapper["outputs"][trid_out][attr] if attr in transf_mapper["outputs"][trid_out] else None for attr in attributes ] for trid_out in inputs2outputs if trid_out in transf_mapper["outputs"] ] output_trid = [ [ trid_out if attr in transf_mapper["outputs"][trid_out] else None for attr in attributes ] for trid_out in inputs2outputs if trid_out in transf_mapper["outputs"] ] output_initialized = [ transf_mapper["outputs"][trid_out].get( "all_successors_initialized", False) or trid_out == trid for trid_out in inputs2outputs if trid_out in transf_mapper["outputs"] ] # Complement the list to merge if already in inputs # Only for boolean special_boolean_case = False if not all([b is None for b in deal_boolean]) \ and len(intersection) == len(attributes): special_boolean_case = True output_attributes += [[ transf_mapper["inputs"][trid][attr] for attr in attributes ]] output_trid.append([trid]) skip, to_propagate, ind_to_propagate, anyNone = skip_attributes( output_attributes, output_initialized, force_propagate=self.force_propagate_attributes or force_propagate, deal_boolean=deal_boolean ) if skip: # If outputs are not compatible, # check if transform has special way to deal with it getattr(all_transforms, transform_id).propagate_incompatible( transf_mapper, attributes, trid, anyNone, mode="backwards" ) continue # If ${attribute}_from_previous = True, # then forbid backwards propagation from_previous_out = [ transf_mapper["outputs"].get(output_trid[ind_to_propagate][k], {}).get( attr + "_from_previous", False) for k, attr in enumerate(attributes) ] from_previous_in = [ transf_mapper["inputs"][trid].get( attr + "_from_previous", False) for attr in attributes ] if any(from_previous_out) or any(from_previous_in): continue # In boolean case when merging with inputs as well, # skip if using input value if special_boolean_case: if len(output_attributes) == 1: continue # If several values, merge them for k, (db, attr) in enumerate(zip(deal_boolean, attributes)): if db is None: continue boolean_value = copy.deepcopy(output_attributes[0][k]) for out_attr in output_attributes: boolean_value = compare_attribute( boolean_value, out_attr[k], deal_boolean=db, return_boolean_value=True ) if boolean_value != transf_mapper["inputs"][trid][attr]: transf_mapper["inputs"][trid][attr] = boolean_value transf_mapper["inputs"][trid][f"{attr}_default"] = False list_updated_input_trids.append(trid) did_nothing = False did_nothing_inputs = False continue list_updated_input_trids.append(trid) did_nothing = False did_nothing_inputs = False for value, attribute in zip(to_propagate, attributes): transf_mapper["inputs"][trid][attribute] = value transf_mapper["inputs"][trid][f"{attribute}_default"] = False # Print debug if need if transform_id in transfor2print: if attribute2print in attributes: debug( f"Backward propagation of {attribute2print} for {transform_id}: After outputs") for trid in mapper[transform_id]["inputs"]: if attribute2print in mapper[transform_id]["inputs"][trid]: debug(mapper[transform_id]["inputs"] [trid][attribute2print]) debug("\n\n\n") list_updated_input_trids = list(set(list_updated_input_trids)) if parent_transform is not None: return did_nothing_inputs, list_updated_input_trids else: return did_nothing, list_updated_input_trids
[docs] def propagate_forwards( self, all_transforms, mapper, attributes, transform_id, previous_did_nothing=False, backup_comps=None, parent_transform=None, parent_trids=None, force_propagate=False, deal_boolean=None ): backup_comps = {} if backup_comps is None else backup_comps did_nothing = True transf_mapper = mapper[transform_id] transform = getattr(all_transforms, transform_id) # Print debug if need if transform_id in transfor2print: if attribute2print in attributes: debug( f"Forward propagation of {attribute2print} for {transform_id}: Before precursors") for trid in mapper[transform_id]["inputs"]: if attribute2print in mapper[transform_id]["inputs"][trid]: trid_dict = mapper[transform_id]["inputs"][trid] debug( f"\t- {trid}: {trid_dict[attribute2print]} / from_successor: {trid_dict.get(f'{attribute2print}_from_successor', 'no')}" ) debug("\n\n\n") # Loop on inputs # Limit the loop to precursors if provided precursors = transf_mapper["precursors"] list_inputs = copy.deepcopy(list(transf_mapper["inputs"].keys())) if parent_transform is not None: list_inputs = [ trid for trid in transf_mapper["inputs"] if parent_transform in precursors[trid] ] # Propagate from precursors list_updated_trid = [] for trid in list_inputs: # If attribute already in inputs and not None, skip intersection = set( transf_mapper["inputs"][trid].keys()).intersection(attributes) if len(intersection) == len(attributes): if not any( transf_mapper["inputs"][trid][attr] is None for attr in attributes ): continue did_nothing_precursor = True for tr in precursors[trid]: # Propagate only from parent transform if tr != parent_transform and parent_transform is not None: continue # If not all attributes available in precursor output, skip not_in_outputs = [ attr not in mapper[tr]["outputs"][trid] for attr in attributes ] if any(not_in_outputs): continue # If only one precursor, just propagate # Or if already from_precursor, just replace ref_precursor = transf_mapper["inputs"][trid].get( f"{attributes[0]}_from_precursor", transform_id) if ref_precursor == transform_id \ or ref_precursor == tr \ or ref_precursor not in all_transforms.attributes: for attr in attributes: if ( transf_mapper["inputs"][trid].get(attr, None) == mapper[tr]["outputs"][trid][attr] ): continue if transf_mapper["inputs"][trid].get(attr, None) is not None: continue transf_mapper["inputs"][trid][attr] = mapper[tr]["outputs"][trid][attr] transf_mapper["inputs"][trid][f"{attr}_from_precursor"] = tr did_nothing = False list_updated_trid.append(trid) continue same_input_output = [ compare_attribute( mapper[tr]["outputs"][trid][attr], transf_mapper["inputs"][trid][attr] ) for attr in attributes ] if sum(same_input_output) == len(attributes): continue warning( f"Could not propagate {attributes} forward from " f"{tr} to {transform_id}, due to conflict with {ref_precursor} " f"being already used." ) did_nothing = False list_updated_trid.append(trid) for attr in attributes: transf_mapper["inputs"][trid][attr] = None transf_mapper["inputs"][trid][ f"{attr}_conflict_propagation"] = True # Print debug if need if transform_id in transfor2print: if attribute2print in attributes: debug( f"Forward propagation of {attribute2print} for {transform_id}: After precursors") for trid in mapper[transform_id]["inputs"]: if attribute2print in mapper[transform_id]["inputs"][trid]: trid_dict = mapper[transform_id]["inputs"][trid] debug( f"\t- {trid}: {trid_dict[attribute2print]} / from_successor: {trid_dict.get(f'{attribute2print}_from_successor', 'no')}" ) debug("\n\n\n") # When parent_transform is not None, # Only propagate output attributes to relevant inputs list_trid_outputs = copy.deepcopy(list(transf_mapper["outputs"].keys())) if parent_transform is not None: list_trid_outputs = [ [ out_trid for out_trid in transf_mapper.get("inputs2outputs", {}).get(in_trid, [in_trid]) if out_trid in transf_mapper["outputs"] ] for in_trid in list_updated_trid ] list_trid_outputs = list(itertools.chain(*list_trid_outputs)) # Print debug if need if transform_id in transfor2print: if attribute2print in attributes: debug( f"Forward propagation of {attribute2print} for {transform_id}: Before inputs") for trid in mapper[transform_id]["outputs"]: if attribute2print in mapper[transform_id]["outputs"][trid]: trid_dict = mapper[transform_id]["outputs"][trid] debug( f"\t- {trid}: {trid_dict[attribute2print]} / from_successor: {trid_dict.get(f'{attribute2print}_from_successor', 'no')}" ) debug("\n\n\n") # Propagate inputs to outputs did_nothing_inputs = True list_updated_output_trids = [] for trid in set(list_trid_outputs): # If all already in outputs, skip intersection = set( transf_mapper["outputs"][trid].keys()).intersection(attributes) if len(intersection) == len(attributes): if not any( transf_mapper["outputs"][trid][attr] is None for attr in attributes ): continue # Check if output trid is linked to some inputs outputs2inputs = transf_mapper.get("outputs2inputs", {})[trid] input_attributes = [ [ transf_mapper["inputs"][trid_in].get(attr, None) for attr in attributes ] for trid_in in outputs2inputs if trid_in in transf_mapper["inputs"] ] skip, to_propagate, ind_to_propagate, any_none = skip_attributes( input_attributes, force_propagate=self.force_propagate_attributes or force_propagate ) if skip: continue did_nothing = False did_nothing_inputs = False list_updated_output_trids.append(trid) for value, attribute in zip(to_propagate, attributes): transf_mapper["outputs"][trid][attribute] = value # Print debug if need if transform_id in transfor2print: if attribute2print in attributes: debug( f"Forward propagation of {attribute2print} for {transform_id}: After inputs") for trid in mapper[transform_id]["outputs"]: if attribute2print in mapper[transform_id]["outputs"][trid]: trid_dict = mapper[transform_id]["outputs"][trid] debug( f"\t- {trid}: {trid_dict[attribute2print]} / from_successor: {trid_dict.get(f'{attribute2print}_from_successor', 'no')}" ) debug("\n\n\n") list_updated_output_trids = list(set(list_updated_output_trids)) if parent_transform is not None: return did_nothing_inputs, list_updated_output_trids else: return did_nothing, list_updated_output_trids
[docs] def skip_attributes( list_attributes, list_initialized=None, force_propagate=False, deal_boolean=None ): """Check compatibility of attributes and determines whether to skip Args: list_attributes (_type_): _description_ list_initialized (_type_): _description_ force_propagate (bool, optional): _description_. Defaults to False. Returns: bool, list, bool: - List of attributes are not compatible - List of attributes to propagate - True if all Nones """ # If empty, skip if len(list_attributes) == 0: return True, [], None, True # Check that all successors are fully initialized if list_initialized is not None: if not all(list_initialized): return True, [], None, True # Initiate deal_boolean with all Nones if not provided if deal_boolean is None: deal_boolean = [None for a in list_attributes[0]] # Check for the presence of None when no indication on initialized list_none = list( itertools.chain( *[ [attr is None for attr in lattrs] for lattrs in list_attributes ] ) ) any_none = any(list_none) all_none = all(list_none) if (any_none and not force_propagate and list_initialized is None) or all_none: return True, [], None, any_none # If length is one and not default, skip if None, otherwise do not skip if len(list_attributes) == 1: if all( lattrs is None for lattrs in list_attributes[0] ): return True, [], None, any_none else: return False, list_attributes[0], 0, any_none # Check that all the same all_same = all( all( compare_attribute( l, list_attributes[0][k], deal_boolean=deal_boolean[k] ) and l is not None for k, l in enumerate(lattrs) ) for lattrs in list_attributes ) if all_same: return False, list_attributes[0], 0, any_none # Stop here if not force_propagate if not force_propagate and list_initialized is None: return True, [], None, any_none # Check that all not None values are the same if force_propagate ind_ref = [ ind for ind, lattributes in enumerate(list_attributes) if not any(l is None for l in lattributes) and len(lattributes) > 0 ][0] all_same = all([ all([ compare_attribute( l, list_attributes[ind_ref][k], deal_boolean=deal_boolean[k] ) for k, l in enumerate(lattrs) if l is not None and list_attributes[ind_ref][k] is not None ]) for lattrs in list_attributes ]) if all_same: to_propagate = [ [l for l in lattributes if l is not None] for lattributes in list_attributes ] ind_to_propagate = [ ind for ind, lattributes in enumerate(to_propagate) if not any(l is None for l in lattributes) and len(lattributes) > 0 ] to_propagate = [ lattributes for lattributes in to_propagate if not any(l is None for l in lattributes) and len(lattributes) > 0 ] if len(to_propagate) > 0: to_propagate = to_propagate[0] ind_to_propagate = ind_to_propagate[0] return False, to_propagate, ind_to_propagate, any_none # Even in force_propagate, don't propagate if ambiguity return True, [], None, any_none
[docs] def compare_attribute( attr1, attr2, deal_boolean=None, return_boolean_value=False ): # Start with special treatment for boolean if deal_boolean is not None: if deal_boolean not in ["or", "and"]: return ValueError( f"{deal_boolean}is not recognized. " "Only 'or' is implemented so far" ) # If does not return the value, # Just returns True to indicate that the elements are comparable if not return_boolean_value: return True # Deal with Nones if any if attr1 is None: return attr2 elif attr2 is None: return attr1 # Now deal with the different cases if type(attr1) == type(attr2) == bool: # Either returns value, # or indicates that elements are comparable if return_boolean_value: if deal_boolean == "or": return attr1 or attr2 else: return attr1 and attr2 else: return True # Now deal with dictionaries # If only one of the two is a dictionay, expands the other if isinstance(attr1, dict) and type(attr2) == bool: attr2 = { k: attr2 for k in attr1 } if isinstance(attr2, dict) and type(attr1) == bool: attr1 = { k: attr1 for k in attr2 } return { k: attr1.get(k, False) or attr2.get(k, False) for k in set(list(attr1.keys()) + list(attr2.keys())) } # Now deal with regular cases if type(attr1) != type(attr2): return False elif isinstance(attr1, dict): if attr1.keys() != attr2.keys(): return False same_values = all( compare_attribute(attr1[key], attr2[key]) for key in attr1.keys() ) return same_values elif isinstance(attr1, (list, tuple, np.ndarray)): if len(attr1) != len(attr2): return False return np.all(np.asarray(attr1) == np.asarray(attr2)) elif isinstance(attr1, pd.DataFrame): return attr1.equals(attr2) # For other types, compare directly return attr1 == attr2