diff --git a/rmgpy/molecule/fragment.py b/rmgpy/molecule/fragment.py index 8b6c92d51d..8a5eac8357 100644 --- a/rmgpy/molecule/fragment.py +++ b/rmgpy/molecule/fragment.py @@ -80,20 +80,23 @@ def __repr__(self): def symbol(self): return self.name - def is_specific_case_of(self, other): + def is_specific_case_of(self, other, check_labels=False): """ Return ``True`` if `self` is a specific case of `other`, or ``False`` otherwise. At this moment, this is the same as the :math:`equivalent()`. """ - return self.equivalent(other) + return self.equivalent(other, check_labels=check_labels) - def equivalent(self, other, strict=True): + def equivalent(self, other, strict=True, check_labels=False): """ Return ``True`` if `other` is indistinguishable from this CuttingLabel, or ``False`` otherwise. If `other` is an :class:`CuttingLabel` object, then all - attributes must match exactly. + attributes must match exactly. If ``check_labels`` is ``True``, the + `label` attributes must also match. """ if isinstance(other, CuttingLabel): + if check_labels and self.label != other.label: + return False return self.name == other.name else: return False @@ -247,7 +250,7 @@ def get_radical_count(self): return radicals def is_subgraph_isomorphic( - self, other, initial_map=None, generate_initial_map=False, save_order=False + self, other, initial_map=None, generate_initial_map=False, save_order=False, check_labels=False ): """ Fragment's subgraph isomorphism check is done by first creating @@ -303,15 +306,15 @@ def is_subgraph_isomorphic( for i, key in enumerate(keys): initial_map[key] = atmlist[i] if self.is_mapping_valid( - other, initial_map, equivalent=False + other, initial_map, equivalent=False, strict=True, check_labels=check_labels ) and Graph.is_subgraph_isomorphic( - self, other, initial_map, save_order=save_order + self, other, initial_map, save_order=save_order, check_labels=check_labels ): return True else: return False else: - if not self.is_mapping_valid(other, initial_map, equivalent=False): + if not self.is_mapping_valid(other, initial_map, equivalent=False, strict=True, check_labels=check_labels): return False # Do the isomorphism comparison @@ -322,7 +325,7 @@ def is_subgraph_isomorphic( repr_mol_vertex = mapping[fragment_vertex] new_initial_map[repr_mol_vertex] = initial_map[fragment_vertex] - result = Graph.is_subgraph_isomorphic(self.mol_repr, other, new_initial_map) + result = Graph.is_subgraph_isomorphic(self.mol_repr, other, new_initial_map, check_labels=check_labels) return result def calculate_cp0(self): diff --git a/rmgpy/molecule/graph.pxd b/rmgpy/molecule/graph.pxd index 26a4dac3e4..0f5f7f6744 100644 --- a/rmgpy/molecule/graph.pxd +++ b/rmgpy/molecule/graph.pxd @@ -40,9 +40,9 @@ cdef class Vertex(object): cpdef Vertex copy(self) - cpdef bint equivalent(self, Vertex other, bint strict=?) except -2 + cpdef bint equivalent(self, Vertex other, bint strict=?, bint check_labels=?) except -2 - cpdef bint is_specific_case_of(self, Vertex other) except -2 + cpdef bint is_specific_case_of(self, Vertex other, bint check_labels=?) except -2 cpdef reset_connectivity_values(self) @@ -110,13 +110,13 @@ cdef class Graph(object): cpdef restore_vertex_order(self) - cpdef bint is_isomorphic(self, Graph other, dict initial_map=?, bint generate_initial_map=?, bint save_order=?, bint strict=?) except -2 + cpdef bint is_isomorphic(self, Graph other, dict initial_map=?, bint generate_initial_map=?, bint save_order=?, bint strict=?, bint check_labels=?) except -2 - cpdef list find_isomorphism(self, Graph other, dict initial_map=?, bint save_order=?, bint strict=?) + cpdef list find_isomorphism(self, Graph other, dict initial_map=?, bint save_order=?, bint strict=?, bint check_labels=?) - cpdef bint is_subgraph_isomorphic(self, Graph other, dict initial_map=?, bint save_order=?) except -2 + cpdef bint is_subgraph_isomorphic(self, Graph other, dict initial_map=?, bint save_order=?, bint check_labels=?) except -2 - cpdef list find_subgraph_isomorphisms(self, Graph other, dict initial_map=?, bint save_order=?) + cpdef list find_subgraph_isomorphisms(self, Graph other, dict initial_map=?, bint save_order=?, bint check_labels=?) cpdef bint is_cyclic(self) except -2 @@ -140,6 +140,6 @@ cdef class Graph(object): cpdef list get_largest_ring(self, Vertex vertex) - cpdef bint is_mapping_valid(self, Graph other, dict mapping, bint equivalent=?, bint strict=?) except -2 + cpdef bint is_mapping_valid(self, Graph other, dict mapping, bint equivalent=?, bint strict=?, bint check_labels=?) except -2 cpdef list get_edges_in_cycle(self, list vertices, bint sort=?) diff --git a/rmgpy/molecule/graph.pyx b/rmgpy/molecule/graph.pyx index 9d958f18f3..14fa1946cc 100644 --- a/rmgpy/molecule/graph.pyx +++ b/rmgpy/molecule/graph.pyx @@ -95,19 +95,23 @@ cdef class Vertex(object): new = Vertex() return new - cpdef bint equivalent(self, Vertex other, bint strict=True) except -2: + cpdef bint equivalent(self, Vertex other, bint strict=True, bint check_labels=False) except -2: """ Return :data:`True` if two vertices `self` and `other` are semantically equivalent, or :data:`False` if not. You should reimplement this function in a derived class if your vertices have semantic information. + If `check_labels` is ``True``, subclasses with a `label` attribute + should also require that the labels match. """ return True - cpdef bint is_specific_case_of(self, Vertex other) except -2: + cpdef bint is_specific_case_of(self, Vertex other, bint check_labels=False) except -2: """ Return ``True`` if `self` is semantically more specific than `other`, or ``False`` if not. You should reimplement this function in a derived - class if your edges have semantic information. + class if your edges have semantic information. If `check_labels` is + ``True``, subclasses with a `label` attribute should also require that + the labels match. """ return True @@ -499,7 +503,7 @@ cdef class Graph(object): else: self.vertices = self.ordered_vertices - cpdef bint is_isomorphic(self, Graph other, dict initial_map=None, bint generate_initial_map=False, bint save_order=False, bint strict=True) except -2: + cpdef bint is_isomorphic(self, Graph other, dict initial_map=None, bint generate_initial_map=False, bint save_order=False, bint strict=True, bint check_labels=False) except -2: """ Returns :data:`True` if two graphs are isomorphic and :data:`False` otherwise. Uses the VF2 algorithm of Vento and Foggia. @@ -509,6 +513,7 @@ cdef class Graph(object): generate_initial_map (bool, optional): if ``True``, initialize map by pairing atoms with same labels save_order (bool, optional): if ``True``, reset atom order after performing atom isomorphism strict (bool, optional): if ``False``, perform isomorphism ignoring electrons + check_labels (bool, optional): if ``True``, atoms only match if their `label` attributes match """ if generate_initial_map: initial_map = dict() @@ -520,12 +525,12 @@ cdef class Graph(object): break else: return False - if not self.is_mapping_valid(other, initial_map, equivalent=True): + if not self.is_mapping_valid(other, initial_map, equivalent=True, strict=True, check_labels=check_labels): return False - return vf2.is_isomorphic(self, other, initial_map, save_order=save_order, strict=strict) + return vf2.is_isomorphic(self, other, initial_map, save_order=save_order, strict=strict, check_labels=check_labels) - cpdef list find_isomorphism(self, Graph other, dict initial_map=None, bint save_order=False, bint strict=True): + cpdef list find_isomorphism(self, Graph other, dict initial_map=None, bint save_order=False, bint strict=True, bint check_labels=False): """ Returns :data:`True` if `other` is subgraph isomorphic and :data:`False` otherwise, and the matching mapping. @@ -535,24 +540,25 @@ cdef class Graph(object): initial_map (dict, optional): initial atom mapping to use save_order (bool, optional): if ``True``, reset atom order after performing atom isomorphism strict (bool, optional): if ``False``, perform isomorphism ignoring electrons + check_labels (bool, optional): if ``True``, atoms only match if their `label` attributes match """ - return vf2.find_isomorphism(self, other, initial_map, save_order=save_order, strict=strict) + return vf2.find_isomorphism(self, other, initial_map, save_order=save_order, strict=strict, check_labels=check_labels) - cpdef bint is_subgraph_isomorphic(self, Graph other, dict initial_map=None, bint save_order=False) except -2: + cpdef bint is_subgraph_isomorphic(self, Graph other, dict initial_map=None, bint save_order=False, bint check_labels=False) except -2: """ Returns :data:`True` if `other` is subgraph isomorphic and :data:`False` otherwise. Uses the VF2 algorithm of Vento and Foggia. """ - return vf2.is_subgraph_isomorphic(self, other, initial_map, save_order=save_order) + return vf2.is_subgraph_isomorphic(self, other, initial_map, save_order=save_order, check_labels=check_labels) - cpdef list find_subgraph_isomorphisms(self, Graph other, dict initial_map=None, bint save_order=False): + cpdef list find_subgraph_isomorphisms(self, Graph other, dict initial_map=None, bint save_order=False, bint check_labels=False): """ Returns :data:`True` if `other` is subgraph isomorphic and :data:`False` otherwise. Also returns the lists all of valid mappings. Uses the VF2 algorithm of Vento and Foggia. """ - return vf2.find_subgraph_isomorphisms(self, other, initial_map, save_order=save_order) + return vf2.find_subgraph_isomorphisms(self, other, initial_map, save_order=save_order, check_labels=check_labels) cpdef bint is_cyclic(self) except -2: """ @@ -838,14 +844,16 @@ cdef class Graph(object): longest_cycle = cycle return longest_cycle - cpdef bint is_mapping_valid(self, Graph other, dict mapping, bint equivalent=True, bint strict=True) except -2: + cpdef bint is_mapping_valid(self, Graph other, dict mapping, bint equivalent=True, bint strict=True, bint check_labels=False) except -2: """ Check that a proposed `mapping` of vertices from `self` to `other` is valid by checking that the vertices and edges involved in the mapping are mutually equivalent. If equivalent is ``True`` it checks if atoms and edges are equivalent, if ``False`` it checks if they are specific cases of each other. If strict is ``True``, electrons - and bond orders are considered, and ignored if ``False``. + and bond orders are considered, and ignored if ``False``. If + check_labels is ``True``, atoms only match if their `label` + attributes also match. """ cdef Vertex vertex1, vertex2 cdef list vertices1, vertices2 @@ -855,10 +863,10 @@ cdef class Graph(object): # Check that the mapped pairs of vertices compare True for vertex1, vertex2 in mapping.items(): if equivalent: - if not vertex1.equivalent(vertex2, strict=strict): + if not vertex1.equivalent(vertex2, strict=strict, check_labels=check_labels): return False else: - if not vertex1.is_specific_case_of(vertex2): + if not vertex1.is_specific_case_of(vertex2, check_labels=check_labels): return False # Check that any edges connected mapped vertices are equivalent diff --git a/rmgpy/molecule/group.pxd b/rmgpy/molecule/group.pxd index 8bb1d43e0b..3a77071f91 100644 --- a/rmgpy/molecule/group.pxd +++ b/rmgpy/molecule/group.pxd @@ -74,9 +74,9 @@ cdef class GroupAtom(Vertex): cpdef apply_action(self, list action) - cpdef bint equivalent(self, Vertex other, bint strict=?) except -2 + cpdef bint equivalent(self, Vertex other, bint strict=?, bint check_labels=?) except -2 - cpdef bint is_specific_case_of(self, Vertex other) except -2 + cpdef bint is_specific_case_of(self, Vertex other, bint check_labels=?) except -2 cpdef bint is_surface_site(self) except -2 @@ -190,15 +190,15 @@ cdef class Group(Graph): cpdef update_charge(self) - cpdef bint is_isomorphic(self, Graph other, dict initial_map=?, bint generate_initial_map=?, bint save_order=?, bint strict=?) except -2 + cpdef bint is_isomorphic(self, Graph other, dict initial_map=?, bint generate_initial_map=?, bint save_order=?, bint strict=?, bint check_labels=?) except -2 - cpdef list find_isomorphism(self, Graph other, dict initial_map=?, bint save_order=?, bint strict=?) + cpdef list find_isomorphism(self, Graph other, dict initial_map=?, bint save_order=?, bint strict=?, bint check_labels=?) - cpdef bint is_subgraph_isomorphic(self, Graph other, dict initial_map=?, bint generate_initial_map=?, bint save_order=?) except -2 + cpdef bint is_subgraph_isomorphic(self, Graph other, dict initial_map=?, bint generate_initial_map=?, bint save_order=?, bint check_labels=?) except -2 - cpdef list find_subgraph_isomorphisms(self, Graph other, dict initial_map=?, bint save_order=?) + cpdef list find_subgraph_isomorphisms(self, Graph other, dict initial_map=?, bint save_order=?, bint check_labels=?) - cpdef bint is_identical(self, Graph other, bint save_order=?) + cpdef bint is_identical(self, Graph other, bint save_order=?, bint check_labels=?) cpdef bint is_surface_site(self) except -2 diff --git a/rmgpy/molecule/group.py b/rmgpy/molecule/group.py index 85ebf25540..e4028c1d19 100644 --- a/rmgpy/molecule/group.py +++ b/rmgpy/molecule/group.py @@ -491,12 +491,14 @@ def apply_action(self, action): else: raise ActionError('Unable to update GroupAtom: Invalid action {0}".'.format(action)) - def equivalent(self, other, strict=True): + def equivalent(self, other, strict=True, check_labels=False): """ Returns ``True`` if `other` is equivalent to `self` or ``False`` if not, where `other` can be either an :class:`Atom` or an :class:`GroupAtom` object. When comparing two :class:`GroupAtom` objects, this function - respects wildcards, e.g. ``R!H`` is equivalent to ``C``. + respects wildcards, e.g. ``R!H`` is equivalent to ``C``. If + ``check_labels`` is ``True``, the atoms must also have matching + `label` attributes. """ cython.declare(group=GroupAtom) @@ -506,9 +508,12 @@ def equivalent(self, other, strict=True): # Let the equivalent method of other handle it # We expect self to be an Atom object, but can't test for it here # because that would create an import cycle - return other.equivalent(self) + return other.equivalent(self, strict=True, check_labels=check_labels) group = other + if check_labels and self.label != group.label: + return False + cython.declare(atomType1=AtomType, atomtype2=AtomType, radical1=cython.short, radical2=cython.short, lp1=cython.short, lp2=cython.short, charge1=cython.short, charge2=cython.short) # Compare two atom groups for equivalence @@ -589,20 +594,25 @@ def equivalent(self, other, strict=True): # Otherwise the two atom groups are equivalent return True - def is_specific_case_of(self, other): + def is_specific_case_of(self, other, check_labels=False): """ Returns ``True`` if `self` is the same as `other` or is a more specific case of `other`. Returns ``False`` if some of `self` is not - included in `other` or they are mutually exclusive. + included in `other` or they are mutually exclusive. If + ``check_labels`` is ``True``, the atoms must also have matching + `label` attributes. """ cython.declare(group=GroupAtom) if not isinstance(other, GroupAtom): # Let the is_specific_case_of method of other handle it # We expect self to be an Atom object, but can't test for it here # because that would create an import cycle - return other.is_specific_case_of(self) + return other.is_specific_case_of(self, check_labels=check_labels) group = other + if check_labels and self.label != group.label: + return False + cython.declare(atomType1=AtomType, atomtype2=AtomType, radical1=cython.short, radical2=cython.short, lp1=cython.short, lp2=cython.short, charge1=cython.short, charge2=cython.short, site1=str, site2=str, morphology1=str, morphology2=str) @@ -2070,13 +2080,15 @@ def update_fingerprint(self): if len(atom.radical_electrons) >= 1: self.radicalCount += atom.radical_electrons[0] - def is_isomorphic(self, other, initial_map=None, generate_initial_map=False, save_order=False, strict=True): + def is_isomorphic(self, other, initial_map=None, generate_initial_map=False, save_order=False, strict=True, check_labels=False): """ Returns ``True`` if two graphs are isomorphic and ``False`` otherwise. The `initial_map` attribute can be used to specify a required mapping from `self` to `other` (i.e. the atoms of `self` are the keys, while the atoms of `other` are the values). The `other` parameter must - be a :class:`Group` object, or a :class:`TypeError` is raised. + be a :class:`Group` object, or a :class:`TypeError` is raised. If + ``check_labels`` is ``True``, atoms only match if their `label` + attributes also match. """ if not strict: raise NotImplementedError('There is currently no implementation of the strict argument for Group objects.') @@ -2086,9 +2098,9 @@ def is_isomorphic(self, other, initial_map=None, generate_initial_map=False, sav raise TypeError( 'Got a {0} object for parameter "other", when a Group object is required.'.format(other.__class__)) # Do the isomorphism comparison - return Graph.is_isomorphic(self, other, initial_map, generate_initial_map, save_order=save_order) + return Graph.is_isomorphic(self, other, initial_map, generate_initial_map, save_order=save_order, strict=True, check_labels=check_labels) - def find_isomorphism(self, other, initial_map=None, save_order=False, strict=True): + def find_isomorphism(self, other, initial_map=None, save_order=False, strict=True, check_labels=False): """ Returns ``True`` if `other` is isomorphic and ``False`` otherwise, and the matching mapping. The `initial_map` attribute can be @@ -2096,7 +2108,9 @@ def find_isomorphism(self, other, initial_map=None, save_order=False, strict=Tru atoms of `self` are the keys, while the atoms of `other` are the values). The returned mapping also uses the atoms of `self` for the keys and the atoms of `other` for the values. The `other` parameter must - be a :class:`Group` object, or a :class:`TypeError` is raised. + be a :class:`Group` object, or a :class:`TypeError` is raised. If + ``check_labels`` is ``True``, atoms only match if their `label` + attributes also match. """ if not strict: raise NotImplementedError('There is currently no implementation of the strict argument for Group objects.') @@ -2106,16 +2120,18 @@ def find_isomorphism(self, other, initial_map=None, save_order=False, strict=Tru raise TypeError( 'Got a {0} object for parameter "other", when a Group object is required.'.format(other.__class__)) # Do the isomorphism comparison - return Graph.find_isomorphism(self, other, initial_map, save_order=save_order) + return Graph.find_isomorphism(self, other, initial_map, save_order=save_order, strict=True, check_labels=check_labels) - def is_subgraph_isomorphic(self, other, initial_map=None, generate_initial_map=False, save_order=False): + def is_subgraph_isomorphic(self, other, initial_map=None, generate_initial_map=False, save_order=False, check_labels=False): """ Returns ``True`` if `other` is subgraph isomorphic and ``False`` otherwise. In other words, return ``True`` if self is more specific than other. The `initial_map` attribute can be used to specify a required mapping from `self` to `other` (i.e. the atoms of `self` are the keys, while the atoms of `other` are the values). The `other` parameter must - be a :class:`Group` object, or a :class:`TypeError` is raised. + be a :class:`Group` object, or a :class:`TypeError` is raised. If + ``check_labels`` is ``True``, atoms only match if their `label` + attributes also match. """ cython.declare(group=Group) cython.declare(mult1=cython.short, mult2=cython.short, m1=str, m2=str) @@ -2149,13 +2165,13 @@ def is_subgraph_isomorphic(self, other, initial_map=None, generate_initial_map=F continue for i, key in enumerate(keys): initial_map[key] = atmlist[i] - if (self.is_mapping_valid(other, initial_map, equivalent=False) and - Graph.is_subgraph_isomorphic(self, other, initial_map, save_order=save_order)): + if (self.is_mapping_valid(other, initial_map, equivalent=False, strict=True, check_labels=check_labels) and + Graph.is_subgraph_isomorphic(self, other, initial_map, save_order=save_order, check_labels=check_labels)): return True else: return False else: - if not self.is_mapping_valid(other, initial_map, equivalent=False): + if not self.is_mapping_valid(other, initial_map, equivalent=False, strict=True, check_labels=check_labels): return False if self.multiplicity: @@ -2186,9 +2202,9 @@ def is_subgraph_isomorphic(self, other, initial_map=None, generate_initial_map=F else: if group.facet: return False # Do the isomorphism comparison - return Graph.is_subgraph_isomorphic(self, other, initial_map, save_order=save_order) + return Graph.is_subgraph_isomorphic(self, other, initial_map, save_order=save_order, check_labels=check_labels) - def find_subgraph_isomorphisms(self, other, initial_map=None, save_order=False): + def find_subgraph_isomorphisms(self, other, initial_map=None, save_order=False, check_labels=False): """ Returns ``True`` if `other` is subgraph isomorphic and ``False`` otherwise. In other words, return ``True`` is self is more specific than other. @@ -2198,7 +2214,8 @@ def find_subgraph_isomorphisms(self, other, initial_map=None, save_order=False): atoms of `other` are the values). The returned mappings also use the atoms of `self` for the keys and the atoms of `other` for the values. The `other` parameter must be a :class:`Group` object, or a - :class:`TypeError` is raised. + :class:`TypeError` is raised. If ``check_labels`` is ``True``, atoms + only match if their `label` attributes also match. """ cython.declare(group=Group) cython.declare(mult1=cython.short, mult2=cython.short, m1=str, m2=str) @@ -2242,14 +2259,15 @@ def find_subgraph_isomorphisms(self, other, initial_map=None, save_order=False): return [] # Do the isomorphism comparison - return Graph.find_subgraph_isomorphisms(self, other, initial_map, save_order=save_order) + return Graph.find_subgraph_isomorphisms(self, other, initial_map, save_order=save_order, check_labels=check_labels) - def is_identical(self, other, save_order=False): + def is_identical(self, other, save_order=False, check_labels=False): """ Returns ``True`` if `other` is identical and ``False`` otherwise. The function `is_isomorphic` respects wildcards, while this function does not, make it more useful for checking groups to groups (as - opposed to molecules to groups) + opposed to molecules to groups). If ``check_labels`` is ``True``, + atoms only match if their `label` attributes also match. """ # It only makes sense to compare a Group to a Group for full # isomorphism, so raise an exception if this is not what was requested @@ -2260,9 +2278,9 @@ def is_identical(self, other, save_order=False): # is the only case where that is true. Therefore # if we do both directions of isSubgraphIsmorphic, we need # to get True twice for it to be identical - if not self.is_subgraph_isomorphic(other, None, save_order=save_order): + if not self.is_subgraph_isomorphic(other, None, save_order=save_order, check_labels=check_labels): return False - elif not other.is_subgraph_isomorphic(self, None, save_order=save_order): + elif not other.is_subgraph_isomorphic(self, None, save_order=save_order, check_labels=check_labels): return False else: return True diff --git a/rmgpy/molecule/molecule.pxd b/rmgpy/molecule/molecule.pxd index 296393740e..270f38748a 100644 --- a/rmgpy/molecule/molecule.pxd +++ b/rmgpy/molecule/molecule.pxd @@ -52,9 +52,9 @@ cdef class Atom(Vertex): cdef public int id cdef public dict props - cpdef bint equivalent(self, Vertex other, bint strict=?) except -2 + cpdef bint equivalent(self, Vertex other, bint strict=?, bint check_labels=?) except -2 - cpdef bint is_specific_case_of(self, Vertex other) except -2 + cpdef bint is_specific_case_of(self, Vertex other, bint check_labels=?) except -2 cpdef Vertex copy(self) @@ -253,13 +253,13 @@ cdef class Molecule(Graph): cpdef dict get_element_count(self) - cpdef bint is_isomorphic(self, Graph other, dict initial_map=?, bint generate_initial_map=?, bint save_order=?, bint strict=?) except -2 + cpdef bint is_isomorphic(self, Graph other, dict initial_map=?, bint generate_initial_map=?, bint save_order=?, bint strict=?, bint check_labels=?) except -2 - cpdef list find_isomorphism(self, Graph other, dict initial_map=?, bint save_order=?, bint strict=?) + cpdef list find_isomorphism(self, Graph other, dict initial_map=?, bint save_order=?, bint strict=?, bint check_labels=?) - cpdef bint is_subgraph_isomorphic(self, Graph other, dict initial_map=?, bint generate_initial_map=?, bint save_order=?) except -2 + cpdef bint is_subgraph_isomorphic(self, Graph other, dict initial_map=?, bint generate_initial_map=?, bint save_order=?, bint check_labels=?) except -2 - cpdef list find_subgraph_isomorphisms(self, Graph other, dict initial_map=?, bint save_order=?) + cpdef list find_subgraph_isomorphisms(self, Graph other, dict initial_map=?, bint save_order=?, bint check_labels=?) cpdef bint is_atom_in_cycle(self, Atom atom) except -2 @@ -362,7 +362,7 @@ cdef class Molecule(Graph): cpdef bint atom_ids_valid(self) - cpdef bint is_identical(self, Graph other, bint strict=?) except -2 + cpdef bint is_identical(self, Graph other, bint strict=?, bint check_labels=?) except -2 cpdef dict enumerate_bonds(self) diff --git a/rmgpy/molecule/molecule.py b/rmgpy/molecule/molecule.py index ca405319a6..d98aafed43 100644 --- a/rmgpy/molecule/molecule.py +++ b/rmgpy/molecule/molecule.py @@ -219,18 +219,21 @@ def sorting_key(self): """Returns a sorting key for comparing Atom objects. Read-only""" return self.element.number, -get_vertex_connectivity_value(self), self.radical_electrons, self.lone_pairs, self.charge - def equivalent(self, other, strict=True): + def equivalent(self, other, strict=True, check_labels=False): """ Return ``True`` if `other` is indistinguishable from this atom, or ``False`` otherwise. If `other` is an :class:`Atom` object, then all attributes except `label` and 'ID' must match exactly. If `other` is an :class:`GroupAtom` object, then the atom must match any of the combinations in the atom pattern. If ``strict`` is ``False``, then only - the element is compared and electrons are ignored. + the element is compared and electrons are ignored. If ``check_labels`` + is ``True``, the atoms must also have matching `label` attributes. """ cython.declare(atom=Atom, ap=gr.GroupAtom) if isinstance(other, Atom): atom = other + if check_labels and self.label != atom.label: + return False if strict: return (self.element is atom.element and self.radical_electrons == atom.radical_electrons @@ -248,6 +251,8 @@ def equivalent(self, other, strict=True): raise NotImplementedError('There is currently no implementation of ' 'the strict argument for Group objects.') ap = other + if check_labels and self.label != ap.label: + return False for a in ap.atomtype: if self.atomtype.equivalent(a): break else: @@ -282,19 +287,23 @@ def equivalent(self, other, strict=True): return False return True - def is_specific_case_of(self, other): + def is_specific_case_of(self, other, check_labels=False): """ Return ``True`` if `self` is a specific case of `other`, or ``False`` otherwise. If `other` is an :class:`Atom` object, then this is the same as the :meth:`equivalent()` method. If `other` is an :class:`GroupAtom` object, then the atom must match or be more - specific than any of the combinations in the atom pattern. + specific than any of the combinations in the atom pattern. If + ``check_labels`` is ``True``, the atoms must also have matching + `label` attributes. """ if isinstance(other, Atom): - return self.equivalent(other) + return self.equivalent(other, strict=True, check_labels=check_labels) elif isinstance(other, gr.GroupAtom): cython.declare(atom=gr.GroupAtom, a=AtomType, radical=cython.short, lp=cython.short, charge=cython.short) atom = other + if check_labels and self.label != atom.label: + return False if self.atomtype is None: return False for a in atom.atomtype: @@ -1665,7 +1674,7 @@ def get_element_count(self): return element_count - def is_isomorphic(self, other, initial_map=None, generate_initial_map=False, save_order=False, strict=True): + def is_isomorphic(self, other, initial_map=None, generate_initial_map=False, save_order=False, strict=True, check_labels=False): """ Returns :data:`True` if two graphs are isomorphic and :data:`False` otherwise. The `initialMap` attribute can be used to specify a required @@ -1679,6 +1688,7 @@ def is_isomorphic(self, other, initial_map=None, generate_initial_map=False, sav generate_initial_map (bool, optional): if ``True``, initialize map by pairing atoms with same labels save_order (bool, optional): if ``True``, reset atom order after performing atom isomorphism strict (bool, optional): if ``False``, perform isomorphism ignoring electrons + check_labels (bool, optional): if ``True``, atoms only match if their `label` attributes match """ # It only makes sense to compare a Molecule to a Molecule for full # isomorphism, so raise an exception if this is not what was requested @@ -1701,14 +1711,14 @@ def is_isomorphic(self, other, initial_map=None, generate_initial_map=False, sav return False # if given an initial map, ensure that it's valid. if initial_map: - if not self.is_mapping_valid(other, initial_map, equivalent=True): + if not self.is_mapping_valid(other, initial_map, equivalent=True, strict=True, check_labels=check_labels): return False # Do the full isomorphism comparison - result = Graph.is_isomorphic(self, other, initial_map, generate_initial_map, save_order=save_order, strict=strict) + result = Graph.is_isomorphic(self, other, initial_map, generate_initial_map, save_order=save_order, strict=strict, check_labels=check_labels) return result - def find_isomorphism(self, other, initial_map=None, save_order=False, strict=True): + def find_isomorphism(self, other, initial_map=None, save_order=False, strict=True, check_labels=False): """ Returns :data:`True` if `other` is isomorphic and :data:`False` otherwise, and the matching mapping. The `initialMap` attribute can be @@ -1722,6 +1732,7 @@ def find_isomorphism(self, other, initial_map=None, save_order=False, strict=Tru initial_map (dict, optional): initial atom mapping to use save_order (bool, optional): if ``True``, reset atom order after performing atom isomorphism strict (bool, optional): if ``False``, perform isomorphism ignoring electrons + check_labels (bool, optional): if ``True``, atoms only match if their `label` attributes match """ # It only makes sense to compare a Molecule to a Molecule for full # isomorphism, so raise an exception if this is not what was requested @@ -1743,10 +1754,10 @@ def find_isomorphism(self, other, initial_map=None, save_order=False, strict=Tru if self.facet != other.facet: return [] # Do the isomorphism comparison - result = Graph.find_isomorphism(self, other, initial_map, save_order=save_order, strict=strict) + result = Graph.find_isomorphism(self, other, initial_map, save_order=save_order, strict=strict, check_labels=check_labels) return result - def is_subgraph_isomorphic(self, other, initial_map=None, generate_initial_map=False, save_order=False): + def is_subgraph_isomorphic(self, other, initial_map=None, generate_initial_map=False, save_order=False, check_labels=False): """ Returns :data:`True` if `other` is subgraph isomorphic and :data:`False` otherwise. The `initial_map` attribute can be used to specify a required @@ -1805,20 +1816,20 @@ def is_subgraph_isomorphic(self, other, initial_map=None, generate_initial_map=F continue for i, key in enumerate(keys): initial_map[key] = atmlist[i] - if (self.is_mapping_valid(other, initial_map, equivalent=False) and - Graph.is_subgraph_isomorphic(self, other, initial_map, save_order=save_order)): + if (self.is_mapping_valid(other, initial_map, equivalent=False, strict=True, check_labels=check_labels) and + Graph.is_subgraph_isomorphic(self, other, initial_map, save_order=save_order, check_labels=check_labels)): return True else: return False else: - if not self.is_mapping_valid(other, initial_map, equivalent=False): + if not self.is_mapping_valid(other, initial_map, equivalent=False, strict=True, check_labels=check_labels): return False # Do the isomorphism comparison - result = Graph.is_subgraph_isomorphic(self, other, initial_map, save_order=save_order) + result = Graph.is_subgraph_isomorphic(self, other, initial_map, save_order=save_order, check_labels=check_labels) return result - def find_subgraph_isomorphisms(self, other, initial_map=None, save_order=False): + def find_subgraph_isomorphisms(self, other, initial_map=None, save_order=False, check_labels=False): """ Returns :data:`True` if `other` is subgraph isomorphic and :data:`False` otherwise. Also returns the lists all of valid mappings. The @@ -1860,7 +1871,7 @@ def find_subgraph_isomorphisms(self, other, initial_map=None, save_order=False): return [] # Do the isomorphism comparison - result = Graph.find_subgraph_isomorphisms(self, other, initial_map, save_order=save_order) + result = Graph.find_subgraph_isomorphisms(self, other, initial_map, save_order=save_order, check_labels=check_labels) return result def is_atom_in_cycle(self, atom): @@ -2993,7 +3004,7 @@ def atom_ids_valid(self): return True return False - def is_identical(self, other, strict=True): + def is_identical(self, other, strict=True, check_labels=False): """ Performs isomorphism checking, with the added constraint that atom IDs must match. @@ -3002,6 +3013,7 @@ def is_identical(self, other, strict=True): Returns :data:`True` if two graphs are identical and :data:`False` otherwise. If ``strict=False``, performs the check ignoring electrons and resonance structures. + If ``check_labels`` is ``True``, atoms only match if their `label` attributes also match. """ cython.declare(atom_ids=set, other_ids=set, atom_list=list, other_list=list, mapping=dict) from rmgpy.molecule.fragment import Fragment @@ -3025,7 +3037,7 @@ def is_identical(self, other, strict=True): for atom1, atom2 in zip(atom_list, other_list): mapping[atom1] = atom2 - return self.is_mapping_valid(other, mapping, equivalent=True, strict=strict) + return self.is_mapping_valid(other, mapping, equivalent=True, strict=strict, check_labels=check_labels) else: # The molecules don't have the same set of indices, so they are not identical return False diff --git a/rmgpy/molecule/vf2.pxd b/rmgpy/molecule/vf2.pxd index 3d9df4cbb6..df092e646e 100644 --- a/rmgpy/molecule/vf2.pxd +++ b/rmgpy/molecule/vf2.pxd @@ -37,19 +37,20 @@ cdef class VF2: cdef bint subgraph cdef bint find_all cdef bint strict - + cdef bint check_labels + cdef bint is_match cdef list mapping_list - - cpdef bint is_isomorphic(self, Graph graph1, Graph graph2, dict initial_mapping, bint save_order=?, bint strict=?) except -2 - - cpdef list find_isomorphism(self, Graph graph1, Graph graph2, dict initial_mapping, bint save_order=?, bint strict=?) - cpdef bint is_subgraph_isomorphic(self, Graph graph1, Graph graph2, dict initial_mapping, bint save_order=?) except -2 + cpdef bint is_isomorphic(self, Graph graph1, Graph graph2, dict initial_mapping, bint save_order=?, bint strict=?, bint check_labels=?) except -2 - cpdef list find_subgraph_isomorphisms(self, Graph graph1, Graph graph2, dict initial_mapping, bint save_order=?) - - cdef isomorphism(self, Graph graph1, Graph graph2, dict initial_mapping, bint subgraph, bint find_all, bint save_order=?, bint strict=?) + cpdef list find_isomorphism(self, Graph graph1, Graph graph2, dict initial_mapping, bint save_order=?, bint strict=?, bint check_labels=?) + + cpdef bint is_subgraph_isomorphic(self, Graph graph1, Graph graph2, dict initial_mapping, bint save_order=?, bint check_labels=?) except -2 + + cpdef list find_subgraph_isomorphisms(self, Graph graph1, Graph graph2, dict initial_mapping, bint save_order=?, bint check_labels=?) + + cdef isomorphism(self, Graph graph1, Graph graph2, dict initial_mapping, bint subgraph, bint find_all, bint save_order=?, bint strict=?, bint check_labels=?) cdef bint match(self, int call_depth) except -2 diff --git a/rmgpy/molecule/vf2.pyx b/rmgpy/molecule/vf2.pyx index c04c8cd21f..2b456fe099 100644 --- a/rmgpy/molecule/vf2.pyx +++ b/rmgpy/molecule/vf2.pyx @@ -63,53 +63,57 @@ cdef class VF2: self.graph2.sort_vertices() cpdef bint is_isomorphic(self, Graph graph1, Graph graph2, dict initial_mapping, bint save_order=False, - bint strict=True) except -2: + bint strict=True, bint check_labels=False) except -2: """ Return ``True`` if graph `graph1` is isomorphic to graph `graph2` with the optional initial mapping `initial_mapping`, or ``False`` otherwise. """ - self.isomorphism(graph1, graph2, initial_mapping, False, False, save_order=save_order, strict=strict) + self.isomorphism(graph1, graph2, initial_mapping, False, False, save_order=save_order, strict=strict, + check_labels=check_labels) return self.is_match cpdef list find_isomorphism(self, Graph graph1, Graph graph2, dict initial_mapping, bint save_order=False, - bint strict=True): + bint strict=True, bint check_labels=False): """ Return a list of dicts of all valid isomorphism mappings from graph - `graph1` to graph `graph2` with the optional initial mapping + `graph1` to graph `graph2` with the optional initial mapping `initial_mapping`. If no valid isomorphisms are found, an empty list is returned. """ - self.isomorphism(graph1, graph2, initial_mapping, False, True, save_order=save_order, strict=strict) + self.isomorphism(graph1, graph2, initial_mapping, False, True, save_order=save_order, strict=strict, + check_labels=check_labels) return self.mapping_list cpdef bint is_subgraph_isomorphic(self, Graph graph1, Graph graph2, dict initial_mapping, - bint save_order=False) except -2: + bint save_order=False, bint check_labels=False) except -2: """ Return ``True`` if graph `graph1` is subgraph isomorphic to subgraph `graph2` with the optional initial mapping `initial_mapping`, or ``False`` otherwise. """ - self.isomorphism(graph1, graph2, initial_mapping, True, False, save_order) + self.isomorphism(graph1, graph2, initial_mapping, True, False, save_order, strict=True, check_labels=check_labels) return self.is_match - cpdef list find_subgraph_isomorphisms(self, Graph graph1, Graph graph2, dict initial_mapping, bint save_order=False): + cpdef list find_subgraph_isomorphisms(self, Graph graph1, Graph graph2, dict initial_mapping, bint save_order=False, + bint check_labels=False): """ Return a list of dicts of all valid subgraph isomorphism mappings from - graph `graph1` to subgraph `graph2` with the optional initial mapping + graph `graph1` to subgraph `graph2` with the optional initial mapping `initial_mapping`. If no valid subgraph isomorphisms are found, an empty list is returned. """ - self.isomorphism(graph1, graph2, initial_mapping, True, True, save_order) + self.isomorphism(graph1, graph2, initial_mapping, True, True, save_order, strict=True, check_labels=check_labels) return self.mapping_list cdef isomorphism(self, Graph graph1, Graph graph2, dict initial_mapping, bint subgraph, bint find_all, - bint save_order=False, bint strict=True): + bint save_order=False, bint strict=True, bint check_labels=False): """ Evaluate the isomorphism relationship between graphs `graph1` and `graph2` with optional initial mapping `initial_mapping`. If `subgraph` is ``True``, `graph2` is treated as a possible subgraph of `graph1`. If `find_all` is ``True``, all isomorphisms are found; otherwise only - the first is found. + the first is found. If `check_labels` is ``True``, vertices only match + if their `label` attributes also match. """ cdef int call_depth, index1, index2 @@ -125,6 +129,7 @@ cdef class VF2: self.subgraph = subgraph self.find_all = find_all self.strict = strict + self.check_labels = check_labels # Clear previous result self.is_match = False @@ -287,9 +292,9 @@ cdef class VF2: # Semantic check #1: vertex1 and vertex2 must be equivalent if self.subgraph: - if not vertex1.is_specific_case_of(vertex2): return False + if not vertex1.is_specific_case_of(vertex2, check_labels=self.check_labels): return False else: - if not vertex1.equivalent(vertex2, strict=self.strict): return False + if not vertex1.equivalent(vertex2, strict=self.strict, check_labels=self.check_labels): return False # Semantic check #2: adjacent vertices to vertex1 and vertex2 that are # already mapped should be connected by equivalent edges diff --git a/test/rmgpy/molecule/fragmentTest.py b/test/rmgpy/molecule/fragmentTest.py index 2c9092051e..b693cdde86 100644 --- a/test/rmgpy/molecule/fragmentTest.py +++ b/test/rmgpy/molecule/fragmentTest.py @@ -94,6 +94,55 @@ def setup_class(self): def test_fragment_isomorphism(self): assert self.fragment1.is_isomorphic(self.fragment2) + def test_fragment_isomorphism_check_labels(self): + """ + Check that the check_labels option works for isomorphism between + Fragments containing CuttingLabel vertices. + """ + atom_C1 = Atom(element=get_element("C"), radical_electrons=0, charge=0, lone_pairs=0) + cutting_label_R1 = rmgpy.molecule.fragment.CuttingLabel("R", label="*1") + cutting_label_L1 = rmgpy.molecule.fragment.CuttingLabel("L", label="*2") + vertices = [atom_C1, cutting_label_R1, cutting_label_L1] + bonds = [Bond(atom_C1, cutting_label_R1), Bond(atom_C1, cutting_label_L1)] + fragment1 = rmgpy.molecule.fragment.Fragment() + for vertex in vertices: + fragment1.add_vertex(vertex) + for bond in bonds: + fragment1.add_edge(bond) + + # fragment2 has the same structure and the same labels in the same positions + atom_C2 = Atom(element=get_element("C"), radical_electrons=0, charge=0, lone_pairs=0) + cutting_label_R2 = rmgpy.molecule.fragment.CuttingLabel("R", label="*1") + cutting_label_L2 = rmgpy.molecule.fragment.CuttingLabel("L", label="*2") + vertices = [atom_C2, cutting_label_R2, cutting_label_L2] + bonds = [Bond(atom_C2, cutting_label_R2), Bond(atom_C2, cutting_label_L2)] + fragment2 = rmgpy.molecule.fragment.Fragment() + for vertex in vertices: + fragment2.add_vertex(vertex) + for bond in bonds: + fragment2.add_edge(bond) + + # fragment3 has the same structure, but the labels on the R and L + # cutting labels are swapped relative to fragment1 + atom_C3 = Atom(element=get_element("C"), radical_electrons=0, charge=0, lone_pairs=0) + cutting_label_R3 = rmgpy.molecule.fragment.CuttingLabel("R", label="*2") + cutting_label_L3 = rmgpy.molecule.fragment.CuttingLabel("L", label="*1") + vertices = [atom_C3, cutting_label_R3, cutting_label_L3] + bonds = [Bond(atom_C3, cutting_label_R3), Bond(atom_C3, cutting_label_L3)] + fragment3 = rmgpy.molecule.fragment.Fragment() + for vertex in vertices: + fragment3.add_vertex(vertex) + for bond in bonds: + fragment3.add_edge(bond) + + # Structurally, all three fragments are isomorphic regardless of labels + assert fragment1.is_isomorphic(fragment2) + assert fragment1.is_isomorphic(fragment3) + + # With check_labels=True, only the fragment with matching label placement should match + assert fragment1.is_isomorphic(fragment2, check_labels=True) + assert not fragment1.is_isomorphic(fragment3, check_labels=True) + def test_from_smiles_like_string1(self): # generate fragment from SMILES like string # the atom type is also calculated diff --git a/test/rmgpy/molecule/moleculeTest.py b/test/rmgpy/molecule/moleculeTest.py index 38447d6310..14021614df 100644 --- a/test/rmgpy/molecule/moleculeTest.py +++ b/test/rmgpy/molecule/moleculeTest.py @@ -1274,6 +1274,42 @@ def test_isomorphism(self): assert molecule1.is_isomorphic(molecule2, generate_initial_map=True) assert molecule2.is_isomorphic(molecule1, generate_initial_map=True) + def test_isomorphism_check_labels(self): + """ + Check that the check_labels option to the isomorphism functions + requires atom `label` attributes to match, in addition to the + usual structural comparison. + """ + # Ethanol (CH3-CH2-OH) has no nontrivial graph automorphism, since the + # two carbons and the oxygen are all structurally distinguishable. + # This lets us control exactly which atom gets the label. + adjlist_template = """ +1 {label1} C u0 p0 c0 {{2,S}} {{3,S}} {{4,S}} {{5,S}} +2 {label2} C u0 p0 c0 {{1,S}} {{6,S}} {{7,S}} {{8,S}} +3 H u0 p0 c0 {{1,S}} +4 H u0 p0 c0 {{1,S}} +5 H u0 p0 c0 {{1,S}} +6 H u0 p0 c0 {{2,S}} +7 H u0 p0 c0 {{2,S}} +8 O u0 p2 c0 {{2,S}} {{9,S}} +9 H u0 p0 c0 {{8,S}} +""" + # molecule1 and molecule3 label the CH3 carbon; molecule2 labels the CH2 carbon instead. + molecule1 = Molecule().from_adjacency_list(adjlist_template.format(label1="*1", label2="")) + molecule2 = Molecule().from_adjacency_list(adjlist_template.format(label1="", label2="*1")) + molecule3 = Molecule().from_adjacency_list(adjlist_template.format(label1="*1", label2="")) + + # Structurally, all three molecules are isomorphic regardless of where the label is. + assert molecule1.is_isomorphic(molecule2) + assert molecule1.is_isomorphic(molecule3) + + # With check_labels=True, only the mapping that also matches labels should succeed. + assert not molecule1.is_isomorphic(molecule2, check_labels=True) + assert molecule1.is_isomorphic(molecule3, check_labels=True) + + assert molecule1.find_isomorphism(molecule2, check_labels=True) == [] + assert len(molecule1.find_isomorphism(molecule3, check_labels=True)) > 0 + def test_subgraph_isomorphism(self): """ Check the graph isomorphism functions. @@ -1296,6 +1332,44 @@ def test_subgraph_isomorphism(self): assert key in molecule.atoms assert value in group.atoms + def test_subgraph_isomorphism_check_labels(self): + """ + Check that the check_labels option restricts subgraph isomorphism + matches to atoms whose `label` attributes also agree. + """ + molecule = Molecule().from_smiles("C=CC=C[CH]C") + # Capture a reference to one of the molecule's Cd atoms (the first + # atom of the first C=C) before running any isomorphism checks, + # since those checks may reorder molecule.atoms in place. + target_atom = molecule.atoms[0] + group = Group().from_adjacency_list( + """ + 1 *1 Cd u0 p0 c0 {2,D} + 2 Cd u0 p0 c0 {1,D} + """ + ) + + # Without a matching labeled atom in the molecule, the group's + # labeled atom can never find a matching counterpart. + assert molecule.is_subgraph_isomorphic(group, save_order=True) + assert not molecule.is_subgraph_isomorphic(group, check_labels=True, save_order=True) + assert molecule.find_subgraph_isomorphisms(group, check_labels=True, save_order=True) == [] + + # Label the captured Cd atom to match the group's labeled atom. + target_atom.label = "*1" + + assert molecule.is_subgraph_isomorphic(group, check_labels=True, save_order=True) + mappings_unlabeled = molecule.find_subgraph_isomorphisms(group, save_order=True) + mappings_labeled = molecule.find_subgraph_isomorphisms(group, check_labels=True, save_order=True) + assert len(mappings_unlabeled) == 4 + # Only the mapping that pairs the labeled molecule atom with the + # labeled group atom should survive. + assert len(mappings_labeled) == 1 + for mapping in mappings_labeled: + for molecule_atom, group_atom in mapping.items(): + if group_atom.label == "*1": + assert molecule_atom is target_atom + def test_subgraph_isomorphism_again(self): molecule = Molecule() molecule.from_adjacency_list(