Source code for schrodinger.comparison.atom_mapper

"""
implementation of atom mapper used to perform comparisons and get atom mappings
"""

import abc
import collections
import itertools

import networkx as nx
from networkx.algorithms import isomorphism

from schrodinger.application.jaguar import file_logger
from schrodinger.comparison import chirality
from schrodinger.comparison.exceptions import ConformerError
from schrodinger.infra import mm
from schrodinger.structutils import build

MapScore = collections.namedtuple("MapScore", ["score", "atom_map"])
ACTIVE_ATOM = "-ACTIVE_ATOM"


def _register_file(handle):
    """
    Register file using file_logger.register_file
    but ignore RuntimeError.  This is temporary
    until we decide whether to/how to register these files
    outside of the Jaguar world.
    """
    try:
        file_logger.register_file(handle)
    except RuntimeError:
        pass


def _label_indexes(st, label):
    """
    store atom indexes in property label

    :type st: Structure
    :param st: structure to add labels to
    """
    for at in st.atom:
        at.property[label] = at.index


def _hash_of_list(lst):
    """
    Hash an iterable of strings, order independent.

    :type lst: list of strings
    :param lst: list of strings to hash as one item
    :rtype: str
    """

    return str(hash(tuple(sorted(lst))))


def _num_bonds(at, inclusions):
    """
    count number of bonds from at to atoms with indexes in list inclusions

    :type at: _StructureAtom
    :param at: atom to search around
    :type inclusions: list of ints
    :param inclusions: atom indexes to include in returned list
    :return: int, the number of bonded neighbors in the inclusions list
    """
    return sum(1 for x in at.bonded_atoms if x.index in inclusions)


def _fill_map(atom_map):
    """
    Fill in a partial map, the empty locations are
    indicated by Nones in the atom_map.
    These entries are filled in by unmapped atoms.

    :type atom_map: list of ints.  The length of the map is
                    assumed to be the number of atoms
    """
    # fill in the rest of the map with unmapped atoms
    natoms = len(atom_map)
    all_atoms = set(range(1, natoms + 1))
    remainder = iter(all_atoms.difference(set(atom_map)))

    for i in range(natoms):
        if atom_map[i] is None:
            atom_map[i] = next(remainder)


[docs]def discard_lewis_data(st): """ Get a copy of a structure with the Lewis data discarded. Necessary to get correct chirality labels calculated for connectivity-based Atom Mappers. :type st: Structure instance :param st: Structure to get remove Lewis data from :rtype: Structure instance :return: structure with Lewis data discarded """ st_copy = st.copy() for bond in st_copy.bond: bond.order = 1 for at in st_copy.atom: at.formal_charge = 0 # We mark the vacancies in the structure with unpaired # electrons because otherwise implicit hydrogens will # be used in stereo determination st_copy.retype() mm.mmhtreat_mark_unpaired_electrons_ct(st_copy) return st_copy
[docs]class BaseAtomMapper(metaclass=abc.ABCMeta): """ Base class for an atom mapper which reorders atoms in a Structure instance using graph isomorphism as implemented in networkx. This class can be used to easily construct atom mapping algorithms and/or to determine if two structures are conformers. The essential features of implementing a new algorithm are to define an atom type (graph invariant) which identifies nodes and a method to score particular atom mappings. For more control one can also define new methods for creating and comparing graphs. Once the abstract methods have been implemented in a subclass (call this MyAtomMapper for example) the mapper can be used to reorder the atoms of two structure instances such that there is an one-to-one correspondence between atom i in the first and second structure. That is, the atom map is applied to the structure:: mapper = MyAtomMapper(optimize_mapping=True) st1, st2 = mapper.reorder_atoms(st1, range(1, st1.atom_total + 1), st2, range(st2.atom_total + 1)) or two determine if the two structures are conformers:: are_conformers = mapper.are_conformers(st1, range(1, st1.atom_total + 1), st2, range(st2.atom_total + 1)) """ ATOM_TYPE = 's_m_custom_atom_type' MAPPER_INDEX = 'i_m_atom_mapper_index'
[docs] def __init__(self, optimize_mapping, debug=False): """ :type optimize_mapping: boolean :param optimize_mapping: if True search all possible bijections to deliver the one with the lowest score. if False, an arbitrary bijection (the first found) is returned. :type debug: boolean :param debug: print extra debugging information """ self.optimize_mapping = optimize_mapping self.debug = debug self._all_basenames = collections.Counter()
[docs] @abc.abstractmethod def initialize_atom_types(self, st, invert_stereocenters=False): """ Initialize the atom types This method typically calls set_atom_type to store the atom types :type st: Structure :param st: structure containing atoms to initial type of :type invert_stereocenters: boolean :param invert_stereocenters: whether or not R/S labels should be flipped """
[docs] def get_atom_type(self, at): """ This value is used as an atom property :type at: _StructureAtom :param at: atom we wish to obtain a type for :return: string which identifies atom type """ return at.property[self.ATOM_TYPE]
[docs] def set_atom_type(self, at, value): """ Set the value of the atom type :type at: _StructureAtom :param at: atom we wish to set type for :type value: string :param value: set the type of atom to this """ at.property[self.ATOM_TYPE] = value
[docs] @abc.abstractmethod def score_mapping(self, st1, st2, atset): """ Scores a particular atom reordering. :type st1: Structure :param st1: first structure :type st2: Structure :param st2: second structure :type atset: set of integers :param atset: the atoms which have been mapped. This may be a subset of the atoms in the two structures as we test partial as well as full maps. :return: any metric which measures the goodness of a particular relative atom ordering in st1 and st2. Can be any type that has the less than operator implemented. """
[docs] def score_is_equivalent(self, score1, score2): """ This defines if the two scores are actually equal. This helps to resolve machine dependent atom mappings in which two scores are very close to being equal but are not numerically equal. If this is an issue the this method can be implemented to return True if the two scores are within some numerical threshold and should be considered equal even if the less than operator considers them to not be equal. for example if the two scores are 1.2e-8 and 1.4e-8 you may want to consider this method returning True. :type score1: a score (return type of score_mapping) :param score1: the first score :type score2: a score (return type of score_mapping) :param score2: the first score """ return score1 == score2
[docs] def unique_job_name(self, base_name): """ Add an integer to the end of the base_name to get a unique name. :type base_name: str :param base_name: base job name """ i = self._all_basenames[base_name] self._all_basenames[base_name] += 1 return base_name + '_' + str(i)
[docs] def st_to_graph(self, st, atset): """ Convert Structure instance to a networkx Graph using _StructureAtom instances as nodes and adding an atom type property :type st: Structure :param st: the structure to convert :type atset: set of ints :param atset: a set of atoms to use to create the graph :return: networkx Graph """ graph = nx.Graph() for at in (st.atom[i] for i in atset): graph.add_node(at.index, atom_type=self.get_atom_type(at)) for bond in st.bond: if bond.atom1.index in atset and bond.atom2.index in atset: graph.add_edge(bond.atom1.index, bond.atom2.index) return graph
[docs] def comparator(self, d1, d2): """ Comparison function to be used to determine if two nodes on graph are equivalent. If this method is not used when constructing a GraphMatcher, node attributes will not be considered for the purpose of determining isomorphism. :type d1: dict :param d1: key=value dictionary from graph returned from st_to_graph which represents node 1 :type d1: dict :param d1: key=value dictionary from graph returned from st_to_graph which represents node 2 :return: boolean indicating equilvalence """ return d1 == d2
[docs] def isomeric_atom_sets(self, st1, atset1, st2, atset2): """ Check that the atom types in atset1 are the same as those in atset2. If not, the two structures cannot be conformers. :type st1: Structure :param st1: the first structure :type atset1: set of ints :param atset1: set of atom indexes defining the first substructure :type st2: Structure :param st2: the second structure :type atset2: set of ints :param atset2: set of atom indexes defining the second substructure :return: a boolean indicating if these atom lists are isomeric """ cnt1 = collections.Counter( self.get_atom_type(st1.atom[i]) for i in atset1) cnt2 = collections.Counter( self.get_atom_type(st2.atom[i]) for i in atset2) return cnt1 == cnt2
[docs] def are_consistently_ordered_conformers(self, st1, st2, atlist): """ Determine if two substructures are consistently ordered conformers. That is, they have the same atom numbering and bonding :type st1: Structure :param st1: the first structure :type st2: Structure :param st2: the second structure :type atlist: list of ints :param atlist: list of atom indexes defining the substructure :return: boolean indicating whether or not the two structures are ordered conformers """ st1_working = st1.copy() self.initialize_atom_types(st1_working) st1_graph = self.st_to_graph(st1_working, set(atlist)) st2_working = st2.copy() self.initialize_atom_types(st2_working) st2_graph = self.st_to_graph(st2_working, set(atlist)) are_same = st1_graph.nodes(data=True) == st2_graph.nodes( data=True) and st1_graph.edges() == st2_graph.edges() return are_same
[docs] def invert_chirality(self, ch_list): """ Invert the chirality (R/S) of an input list of chiralities. :type ch_list: list of strings :param ch_list: list of chirality labels for a structure """ inv = {'R': 'S', 'S': 'R'} for idx, ch in enumerate(ch_list): if ch in inv: ch_list[idx] = inv[ch]
[docs] def are_conformers(self, st1, atlist1, st2, atlist2): """ Determine if the two substructures, as defined by the atom lists, are conformers but do not explore isomorphisms. :type st1: Structure :param st1: the first structure :type atlist1: list of ints :param atlist1: list of atom indexes defining the first substructure :type st2: Structure :param st2: the second structure :type atlist2: list of ints :param atlist2: list of atom indexes defining the second substructure :return: boolean indicating whether or not the two structures are conformers """ # do not need to optimizing the mapping to determine if two structures are conformers tmp = self.optimize_mapping self.optimize_mapping = False are_conf = True try: _ = self.reorder_structures(st1, atlist1, st2, atlist2) except ConformerError: are_conf = False finally: self.optimize_mapping = tmp return are_conf
[docs] def are_enantiomers(self, st1, atlist1, st2, atlist2): """ Determine if the two substructures, as defined by the atom lists, are enantiomers but do not explore isomorphisms. :type st1: Structure :param st1: the first structure :type atlist1: list of ints :param atlist1: list of atom indexes defining the first substructure :type st2: Structure :param st2: the second structure :type atlist2: list of ints :param atlist2: list of atom indexes defining the second substructure :return: boolean indicating whether or not the two structures are conformers """ # do not need to optimizing the mapping to determine if two structures are conformers tmp = self.optimize_mapping self.optimize_mapping = False are_conf = True try: _ = self.reorder_structures(st1, atlist1, st2, atlist2, invert_stereocenters=True) except ConformerError: are_conf = False finally: self.optimize_mapping = tmp return are_conf
[docs] def reorder_structures(self, st1, atlist1, st2, atlist2, invert_stereocenters=False): """ Reorder the atoms in the two structures. :type st1: Structure :param st1: the first structure :type atlist1: list of ints :param atlist1: list of atom indexes defining the first substructure :type st2: Structure :param st2: the second structure :type atlist2: list of ints :param atlist2: list of atom indexes defining the second substructure :return: the two structures with structure 2 having had atoms reordered """ if len(atlist1) != len(atlist2): raise ConformerError( "Atom lists differ in length and cannot be conformers") st1_working = st1.copy() _label_indexes(st1_working, self.MAPPER_INDEX) self.initialize_atom_types(st1_working) st2_working = st2.copy() _label_indexes(st2_working, self.MAPPER_INDEX) self.initialize_atom_types(st2_working, invert_stereocenters=invert_stereocenters) st1, st2 = self._order_atoms(st1_working, set(atlist1), st2_working, set(atlist2)) return st1, st2
def _order_atoms(self, st1, atset1, st2, atset2): """ Order atoms by first recursively removing topologically degenerate terminal atoms :type st1: Structure :param st1: the first structure :type atset1: set of ints :param atset1: set of atom indexes defining the first substructure :type st2: Structure :param st2: the second structure :type atset2: set of ints :param atset2: set of atom indexes defining the second substructure :return: the two structures with structure 2 having had atoms reordered """ st1_renorm, k1, e1 = self._eliminate_degenerate_terminal_atoms( st1, atset1) st2_renorm, k2, e2 = self._eliminate_degenerate_terminal_atoms( st2, atset2) # check elimination for similarity if not self.isomeric_atom_sets(st1, e1, st2, e2): raise ConformerError("Elimination lists do not match") # if we've eliminated any terminal atoms recurse if e1 and e2: st1_reord, st2_reord = self._order_atoms(st1_renorm, k1, st2_renorm, k2) # order eliminated atoms st1_reord, st2_reord = self._append_terminal_atoms( st1_reord, e1, st2_reord, e2, k1) # order core else: st1_reord, st2_reord = self._order_core_atoms(st1, k1, st2, k2) if self.debug: st1_reord.append('AtomMapper_reorder.mae') st2_reord.append('AtomMapper_reorder.mae') _register_file('AtomMapper_reorder.mae') return st1_reord, st2_reord def _eliminate_degenerate_terminal_atoms(self, st, atset): """ Constructs two sets, one of topologically degenerate terminal (eliminated) and one of (kept) core atoms. Updates the atom types of the kept atoms that were attached to the eliminated ones atoms to reflect this change. The new atom types are Morgan-like fingerprints that retain information about the deleted atoms. :type st: Structure :param st: the structure we are working on :type atset: set of ints :param atset: set of atom indexes to consider :return: a new structure and two sets of atom indexes, the first is of the core atoms and the second is of the terminal atoms The union of these two sets is the input atset. """ kept = atset.copy() eliminated = set() st_working = st for i in atset: # restrict search to terminal atoms not yet eliminated (in atset) terminal_atoms = filter( lambda x: _num_bonds(x, atset) == 1 and x.index in atset \ and ACTIVE_ATOM not in self.get_atom_type(x), st_working.atom[i].bonded_atoms) # dict of atom_type: list of terminal atoms terminal_dict = collections.defaultdict(list) for atj in terminal_atoms: terminal_dict[self.get_atom_type(atj)].append(atj) topo_degeneracies = [] for indexes in terminal_dict.values(): if len(indexes) > 1: topo_degeneracies.extend(indexes) if topo_degeneracies: # Retype atom to include effect of atoms we remove. # Similar to Morgan fingerprinting. type_list = [ self.get_atom_type(atj) for atj in [st_working.atom[i]] + topo_degeneracies ] self.set_atom_type(st_working.atom[i], _hash_of_list(type_list)) for at in topo_degeneracies: kept.remove(at.index) eliminated.add(at.index) return st_working, kept, eliminated def _order_core_atoms(self, st1, atset1, st2, atset2): """ re-order the atoms atset2 in st2 to be consistent with atset1 in st1. Graphs made from the atom indexes listed in atset1 and atset2 must be isomorphic. :type st1: Structure :param st1: the first structure :type atset1: set of ints :param atset1: set of atom indexes defining the first substructure :type st2: Structure :param st2: the second structure :type atset2: list of ints :param atset2: list of atom indexes defining the second substructure :return: two structures """ if len(atset1) != len(atset2): raise ConformerError("length of atom lists in core region differ") g1 = self.st_to_graph(st1, atset1) g2 = self.st_to_graph(st2, atset2) matcher = isomorphism.GraphMatcher(g1, g2, self.comparator) best_score = None best_maps = [] for isomorph in matcher.isomorphisms_iter(): # scatter indexes to correct position in full map atom_map = [None for i in range(st2.atom_total)] for k, v in isomorph.items(): atom_map[k - 1] = v _fill_map(atom_map) st2_reord = build.reorder_atoms(st2, atom_map) if self.optimize_mapping: score = self.score_mapping(st1, st2_reord, atset1) map_score = MapScore(score, atom_map) self._update_map_list(best_maps, map_score) best_score = best_maps[-1].score else: # set best_score to something that is not None best_score = 0.0 best_st2 = st2_reord break ## now pick the best map. If there are ties, choose (entirely arbitrarily) ## the one that preserves the relative ordering of the atoms being reordered ## as best as possible if best_maps: best_score, best_mapping = min(best_maps, key=lambda bij: bij.atom_map) best_st2 = build.reorder_atoms(st2, best_mapping) if self.debug: if best_maps: fname = self.unique_job_name("best_maps_core") + ".mae" st1.write(fname) st2.append(fname) _register_file(fname) print("Best maps of core") for mapping in best_maps: print(str(mapping.score)) temp_st = build.reorder_atoms(st2, mapping.atom_map) temp_st.append(fname) if best_score is None: if self.debug: fname = "not_isomorphic.mae" st1.write(fname) st2.append(fname) _register_file(fname) raise ConformerError("Structure Graphs are not isomorphic") return st1, best_st2 def _append_terminal_atoms(self, st1, term1, st2, term2, core): """ Map the terminal atoms in the list term1 and term2. The atom indexes in the list core should be consistently ordered between st1 and st2. :type st1: Structure :param st1: the first structure :type term1: set of ints :param term1: set of terminal atom indexes in st1 :type st2: Structure :param st2: the second structure :type term2: set of ints :param term2: set of terminal atom indexes in st2 :type core: set of atoms in core structure which are already numbered consistently :return: two new structures """ # keeps track of which atoms have been mapped have_mapped = core.copy() for icore in core: types1 = self._group_neighbors(st1.atom[icore], term1) types2 = self._group_neighbors(st2.atom[icore], term2) # check that name of groups and lengths are the same group_cnts1 = {grp: len(types1[grp]) for grp in types1} group_cnts2 = {grp: len(types2[grp]) for grp in types2} if group_cnts1 != group_cnts2: raise ConformerError( "Groups are not the same when appending terminal atoms") # each group has a list of mappings group_mappings = collections.defaultdict(list) for group in types1: for p in itertools.permutations(types2[group], len(types2[group])): group_mappings[group].append(list(zip(types1[group], p))) # the set of atoms getting mapped mapped_atoms = set() for lst in types1.values(): mapped_atoms.update(lst) # cartesian product of all groups to get # all mappings of this atoms neighbors best_score = None best_maps = [] for mapping in itertools.product(*group_mappings.values()): # get a mapping of terminal atoms attached to this core full_list = [] for group_mapping in mapping: full_list.extend(group_mapping) # set initial map to None full_map = [None for i in range(st2.atom_total)] # map everything that has been mapped for j in have_mapped: full_map[j - 1] = j # map this grouping of atoms for group_mapping in mapping: for i, j in group_mapping: full_map[i - 1] = j # fill in Nones _fill_map(full_map) # RMS is computed over core atom and added terminals # whereas chirality need only be computed for the new core atom # this is because chirality is determined by core + terminal st2_reord = build.reorder_atoms(st2, full_map) if self.optimize_mapping: all_mapped = have_mapped.union(mapped_atoms) score = self.score_mapping(st1, st2_reord, all_mapped) map_score = MapScore(score, full_map) self._update_map_list(best_maps, map_score) best_score = best_maps[-1].score else: map_score = MapScore(0.0, full_map) self._update_map_list(best_maps, map_score) break ## now pick the best map. If there are ties, choose (entirely arbitrarily) ## the one that preserves the relative ordering of the atoms being reordered ## as best as possible if best_maps: best_score, best_mapping = min(best_maps, key=lambda bij: bij.atom_map) best_st2 = build.reorder_atoms(st2, best_mapping) if self.debug: if best_maps: fname = self.unique_job_name("best_maps_all") + ".mae" st1.write(fname) _register_file(fname) print("Best app maps") for mapping in best_maps: print(str(mapping.score)) temp_st = build.reorder_atoms(st2, mapping.atom_map) temp_st.append(fname) score, st2 = (best_score, best_st2) # we just mapped terminal atoms have_mapped.update(mapped_atoms) return st1, st2 def _group_neighbors(self, at, atset): """ Group the neighbors of atom at according to the atom type. The neighboring atoms must also appear in atset. Importantly, the indexes in atset refer to the atom property MAPPER_INDEX. :type at: _StructureAtom :param at: atom to inspect :type atset: set of ints :param atset: set of interesting atom indexes :return: defaultdict(list) relating atom type to a list of atom indexes with that type """ types = collections.defaultdict(list) for atj in at.bonded_atoms: if atj.property[self.MAPPER_INDEX] in atset: types[self.get_atom_type(atj)].append(atj.index) return types def _update_map_list(self, best_maps, new_entry): """ Determine if a new mapping should be retained in the map list and adjust the list accordingly :type best_maps: list of MapScore namedtuples :param best maps: existing best_maps list :type new_entry: MapScore :param new_entry: new map score to consider keeping or discarding, """ if not best_maps: # if we don't have any maps append the new one best_maps.append(new_entry) elif self.score_is_equivalent(new_entry.score, best_maps[-1].score): # add the new map and sort the best maps by the score best_maps.append(new_entry) best_maps.sort(key=lambda bij: bij.score) elif new_entry.score < best_maps[0].score: # the new map is definitely better, use that one best_maps.clear() best_maps.append(new_entry)
[docs]class ConnectivityAtomMapper(BaseAtomMapper): """ Used by comparison.py for comparison of conformers. Uses element types and optionally chirality to equate atoms and all bonds are considered equivalent. Usage example: mapper = ConnectivityAtomMapper(use_chirality=False) st1, st2 = mapper.are_conformers(st1, range(1, st1.atom_total + 1), st2, range(st2.atom_total + 1)) """
[docs] def __init__(self, use_chirality): """ :type use_chirality: boolean :param use_chirality: if True, in addition to element type use chirality (where defined) to equate atoms. """ super().__init__(optimize_mapping=False, debug=False) self.use_chirality = use_chirality
[docs] def initialize_atom_types(self, st, invert_stereocenters=False): """ Initialize the atom types :type st: Structure :param st: structure containing atoms :type invert_stereocenters: boolean :param invert_stereocenters: whether or not R/S labels should be flipped """ # uses element-chirality if self.use_chirality: #Discard Lewis data before determining chirality st_copy = discard_lewis_data(st) ch = chirality.get_chirality(st_copy) if invert_stereocenters: self.invert_chirality(ch) p_ch = chirality.get_pseudochirality(st_copy) for at in st.atom: # have to get rid of these atom number chiralities here if ch[at.index - 1] in ["ANR", "ANS"]: ch[at.index - 1] = None if ch[at.index - 1] is None: ch[at.index - 1] = p_ch[at.index - 1] self.set_atom_type(at, at.element + "-chiral=%s" % ch[at.index - 1]) else: for at in st.atom: self.set_atom_type(at, at.element)
[docs] def score_mapping(self, st1, st2, atset): """ Score a mapping over the atoms in atset. This class is not intended for atom mapping problems and so will raise a NotImplementedError :type st1: Structure :param st1: first structure :type st2: Structure :param st2: second structure :type atset: set of ints :param atset: set of atoms that have been mapped (indexes refer to both structures) :return: a NotImplementedError since this function is not intended for the use case of this class """ raise NotImplementedError
[docs] def score_is_equivalent(self, score1, score2): """ determines if score should be considered equivalent :type score1: float :param score1: the first score :type score2: float :param score2: the second score """ return abs(score1 - score2) < 1.0e-8
[docs]class LewisAtomMapper(BaseAtomMapper): """ Used by comparison.py for comparison of conformers. Uses element types, formal charge, number of neighbors and total number of bonds to distinguish atoms. Optionally uses chirality to equate atoms Usage example: mapper = LewisAtomMapper(use_chirality=False) st1, st2 = mapper.are_conformers(st1, range(1, st1.atom_total + 1), st2, range(st2.atom_total + 1)) """
[docs] def __init__(self, use_chirality): """ :type use_chirality: boolean :param use_chirality: if True, in addition to element type use chirality (where defined) to equate atoms. """ super().__init__(optimize_mapping=False, debug=False) self.use_chirality = use_chirality
[docs] def initialize_atom_types(self, st, invert_stereocenters=False): """ Initialize the atom types :type st: Structure :param st: structure containing atoms :type invert_stereocenters: boolean :param invert_stereocenters: whether or not R/S labels should be flipped """ def atom_label(at): order = sum(b.order for b in at.bond) label = "elmnt=%s-neighbors=%d-bondorder=%d-chg=%d" % ( at.element, at.bond_total, order, at.formal_charge) return label # uses element-chirality if self.use_chirality: ch = chirality.get_chirality(st) if invert_stereocenters: self.invert_chirality(ch) p_ch = chirality.get_pseudochirality(st) for at in st.atom: label = atom_label(at) # have to get rid of these atom number chiralities here if ch[at.index - 1] in ["ANR", "ANS"]: ch[at.index - 1] = None if ch[at.index - 1] is None: ch[at.index - 1] = p_ch[at.index - 1] self.set_atom_type(at, label + "-chiral=%s" % ch[at.index - 1]) else: for at in st.atom: label = atom_label(at) self.set_atom_type(at, label)
[docs] def score_mapping(self, st1, st2, atset): """ Score a mapping over the atoms in atset. This class is not intended for atom mapping problems and so will raise a NotImplementedError :type st1: Structure :param st1: first structure :type st2: Structure :param st2: second structure :type atset: set of ints :param atset: set of atoms that have been mapped (indexes refer to both structures) :return: a NotImplementedError since this function is not intended for the use case of this class """ raise NotImplementedError
[docs] def score_is_equivalent(self, score1, score2): """ determines if score should be considered equivalent :type score1: float :param score1: the first score :type score2: float :param score2: the second score """ return abs(score1 - score2) < 1.0e-8