Project: s41467-024-55462-w | Function: filter2 (python)

system: You are an expert Python programmer implementing algorithms from scientific papers.
Complete the masked function based on the algorithm description.
Follow Python best practices and maintain consistent code style.
Return **only** the complete Python function code.

user: Complete this Python function:

**DESCRIPTION:** Computes a lower bound for the MCES distance based on neighborhood matching using bipartite minimum weight matching. This bound is used to quickly filter out dissimilar molecule pairs without computing the exact MCES.

**KEY TERMS:** {
  "filter2": "stronger lower bound",
  "get_cost": "node mapping cost",
  "minimum_weight_full_matching": "bipartite matching",
  "atom_types": "atom type partitioning"
}

**CONTEXT CODE:**      float
         Cost of mapping i to j

    """
    #Find all occuring atom types in neighborhood
    atom_types1=[]
    for k in G1.neighbors(i):
        if G1.nodes[k]["atom"] not in atom_types1:
            atom_types1.append(G1.nodes[k]["atom"])
    type_map1={}
    for k in atom_types1:
        type_map1[k]=list(filter(lambda x: k==G1.nodes[x]["atom"],G1.neighbors(i)))


    atom_types2=[]
    for k in G2.neighbors(j):
        if G2.nodes[k]["atom"] not in atom_types2:
            atom_types2.append(G2.nodes[k]["atom"])
    type_map2={}
    for k in atom_types2:
        type_map2[k]=list(filter(lambda x: k==G2.nodes[x]["atom"],G2.neighbors(j)))

    #calculate cost
    difference=0.
    #Every atom type is handled seperately
    for k in atom_types1:
        if k in atom_types2:
            n=min(len(type_map1[k]),len(type_map2[k]))
            #sort by incident edges by weight
            edgelist1=sorted(type_map1[k],key=lambda x:G1[i][x]["weight"],reverse=True)
            edgelist2=sorted(type_map2[k],key=lambda x:G2[j][x]["weight"],reverse=True)
            #map in order of sorted lists
            for l in range(n):
                difference+=(max(G1[i][edgelist1[l]]["weight"],G2[j][edgelist2[l]]["weight"])-min(G1[i][edgelist1[l]]["weight"],G2[j][edgelist2[l]]["weight"]))/2
            #cost for not mapped edges
            if len(edgelist1)>n:
                for l in range(n,len(edgelist1)):
                    difference+=G1[i][edgelist1[l]]["weight"]/2
            if len(edgelist2)>n:
                for l in range(n,len(edgelist2)):
                    difference+=G2[j][edgelist2[l]]["weight"]/2
        else:
            for l in type_map1[k]:
                difference+=G1[i][l]["weight"]/2
    for k in atom_types2:
        if k not in atom_types1:
            for l in type_map2[k]:
                difference+=G2[j][l]["weight"]/2
    return difference

def filter2(G1,G2):
    """
     Finds a lower bound for the distance based on neighborhood

     Parameters
     ----------
     G1 : networkx.classes.graph.Graph
         Graph representing the first molecule.
     G2 : networkx.classes.graph.Graph
         Graph representing the second molecule.

     Returns:
     -------
     float
         Lower bound for the distance between the molecules

    """
    raise NotImplementedError("Function implementation has been masked")

def apply_filter(G1,G2,threshold,always_stronger_bound=True):
    """
     Finds a lower bound for the distance

     Parameters
     ----------
     G1 : networkx.classes.graph.Graph
         Graph representing the first molecule.
     G2 : networkx.classes.graph.Graph
         Graph representing the second molecule.
     threshold : int
         Threshold for the comparison. We want to find a lower bound that is higher than the threshold
     always_stronger_bound : bool
         if true, always compute and use the second stronger bound



     Returns:
     -------
     float
         Lower bound for the distance between the molecules
     int
         Which lower bound was chosen: 2 - depending on threshold, 4 - second lower bound

    """
    if always_stronger_bound:
        d=filter2(G1,G2)
        return d, 4
    else:
        #calculate first lower bound
        d=filter1(G1,G2)
        #if below threshold calculate second lower bound
        if d<=threshold:
            d=filter2(G1,G2)
            if d<=threshold:
                return d, 2

        return d, 2


Complete the function implementation replacing "raise NotImplementedError".