Source code for besmarts.core.graphs

"""
besmarts.core.graphs

Definitions and basic functions of the three graph data types of the BESMARTS
package. The *graph* class is the most basic, followed by the *subgraph*
subclass. The *structure* class extends a *subgraph* by defining using a
*structure_topology*, which describes the primary atoms in the *subgraph*. Much
of the mapping and operations in the BESMARTS package only work on structures
of the same topology, for example bonds, angles and dihedrals.
"""

from typing import Sequence, Dict, Tuple, List, Generator

import datetime
import itertools

from besmarts.core import arrays
from besmarts.core import chem
from besmarts.core import topology
from besmarts.core import geometry
from besmarts.core import configs
from besmarts.core import primitives

from besmarts.core.primitives import primitive_key

node_id = int
edge_id = Tuple[node_id, node_id]


[docs] class graph: """ A BESMARTS graph is a map of nodes and edges containing encoded primitives. """ __slots__ = ("nodes", "edges", "cache") def __init__( self, nodes: Dict[node_id, chem.bechem], edges: Dict[edge_id, chem.bechem], ): self.nodes: Dict[node_id, chem.bechem] = nodes self.edges: Dict[edge_id, chem.bechem] = edges self.cache: Dict = {} def __hash__(self) -> int: """ Return the hash Parameters ---------- Returns ------- int The hash """ h = self.cache.get("hash") # h = None if h is None: nh = {i: hash(n) for i, n in self.nodes.items()} adjlens = {i: j for i, j in graph_connections(self).items()} edges_hash = tuple( sorted( ( ( hash(n), tuple(sorted((nh[i], nh[j]))), tuple(sorted((len(adjlens[i]), len(adjlens[j])))), ) for (i, j), n in self.edges.items() ) ) ) gh = tuple(sorted(nh.values())) # print(gh, edges_hash) h = hash((gh, edges_hash)) self.cache["hash"] = h return h def __eq__(self, o) -> bool: """ Return whether two graphs are equal Parameters ---------- o: graph The other graph to compare to Returns ------- bool Whether the graphs are equal """ return hash(self) == hash(o) def __neq__(self, o) -> bool: """ Return whether two graphs are not equal Parameters ---------- o: graph The other graph to compare to Returns ------- bool Whether the graphs are not equal """ return hash(self) != hash(o)
[docs] def edge(x) -> edge_id: if x[1] < x[0]: x = x[::-1] return tuple(x)
[docs] class subgraph(graph): """A BESMARTS subgraph is a node-induced subgraph, where the subgraph is defined by the select nodes.""" __slots__ = ("nodes", "edges", "select", "cache") def __init__( self, nodes: Dict[node_id, chem.bechem], edges: Dict[edge_id, chem.bechem], select: Sequence[node_id], ): super().__init__(nodes, edges) self.select: Sequence[node_id] = select assert all(x in self.nodes for x in select), f"nodes: {list(self.nodes)}, select: {select}" self.cache: Dict = {} def __hash__(self): h = self.cache.get("hash") # h = None if h is None: beg = subgraph_to_graph(self) h = hash(beg) self.cache["hash"] = h return h
[docs] class structure(subgraph): """A BESMARTS structure is a subgraph that additionally describes a core structure in the subgraph. The core structure is defined by a topology, which identifies which of the selected nodes form the core structure. Examples of structures are bonds, angles, and torsions. These are always defined to have the same topology. The depth of a structure is defined by the distance from any of the atoms in the core structure, called the primary nodes in the topology.""" __slots__ = ("nodes", "edges", "select", "topology", "cache", "hashes") def __init__( self, nodes: Dict[node_id, chem.bechem], edges: Dict[edge_id, chem.bechem], select: Sequence[node_id], topology: topology.structure_topology, ): super().__init__(nodes, edges, select) self.topology: topology.structure_topology = topology if len(topology.primary) > len(select): breakpoint() print(topology.primary, print(select)) assert len(topology.primary) <= len(select) for e in topology.connect: _edge = edge((select[e[0]], select[e[1]])) if _edge not in edges: print(_edge, "not in edges", edges) breakpoint() assert _edge in edges self.cache: Dict = {} self.hashes: Dict = {} def __hash__(self): h = self.hashes.get(self.select) # h = None # print("CACHE:", id(self), self.hashes) # h = None if h is None: g = structure_remove_unselected(self) g = structure_remove_nodes(g, structure_unreachable_nodes(g)) g = structure_relabel_nodes(g, {k:i for i,k in enumerate(g.nodes)}) # beg = structure_to_graph(self) beg = subgraph(g.nodes, g.edges, g.select) # overall topology defines uniqueness depths = tuple(structure_node_depths(self)) # we can have a single graph with 2 labeled bonds.. make sure they are # unique core = 0 topo_nodes = tuple((self.select[n] for n in self.topology.primary)) core = structure_remove_nodes( self, tuple((n for n in self.nodes if n not in topo_nodes)) ) core = structure_to_graph(core) adjlens = {i: j for i, j in subgraph_connections(self).items() if i in self.select and j in self.select} nbr_hash = 0 if adjlens: nbr_hash = tuple((tuple([hash(self.nodes[j])] + sorted([hash(self.nodes[i]) for i in adjlens[j]])) for j in topo_nodes)) depth = structure_max_depth(g) atom_hash = 0 if len(self.topology.primary) > 1: atom_hashes = [] for i in self.topology.primary: if ( set([perm[i] for perm in self.topology.permutations]) == 1 ): atom_hashes.append(0) continue i = g.select[i] select = tuple([i] + [x for x in beg.select if i != x]) ag = structure( beg.nodes, beg.edges, select, topology.atom_topology() ) ag = structure_remove_unselected(ag) ag = structure_up_to_depth(ag, depth) remove = structure_unreachable_nodes(ag) ag = structure_remove_nodes(ag, remove) # print("SELECT", ag.select, "HASH", hash(ag), tuple(structure_node_depths(ag))) ah = tuple(structure_node_depths(ag)) atom_hashes.append(ah) atom_hash = max( hash(tuple(atom_hashes[i] for i in y)) for y in self.topology.permutations ) preh = (hash(beg), depths, hash(nbr_hash), hash(core), atom_hash) # atom_hash = 0 # preh = (hash(beg), atom_hash) # preh = (hash(beg), *depths, hash(core)) h = hash(preh) # print(h, preh) self.hashes[self.select] = h # print("CACHE MISS", self.select) # print("CACHE SET:", id(self), self.hashes) # else: # print("CACHE HIT", self.select) return h def __eq__(self, o): return hash(self) == hash(o) def __neq__(self, o): return hash(self) != hash(o)
[docs] def graph_nodes_copy(g: graph) -> Dict[node_id, chem.bechem]: nodes = {k: chem.bechem_copy(v) for k, v in g.nodes.items()} return nodes
[docs] def graph_edges_copy(g: graph) -> Dict[edge_id, chem.bechem]: edges = {k: chem.bechem_copy(v) for k, v in g.edges.items()} return edges
[docs] def graph_connection(g: graph, a: int): """ Return the nodes that are connected to a given node """ return list( set(x for edge in g.edges for x in edge if a in edge and a != x) )
[docs] def graph_connections(g: graph): """ Return a list of connected nodes for all nodes """ adj = g.cache.get("connections") adj = None if adj is None: adj = {x: [] for x in g.nodes} for a, b in g.edges: l = adj.get(a, list()) l.append(b) adj[a] = l l = adj.get(b, list()) l.append(a) adj[b] = l # g.cache["connections"] = adj return adj
[docs] def graph_hash(beg: graph): """ Return the hash of a graph """ return hash(beg)
[docs] def graph_copy(beg: graph) -> graph: """ Return a copy of a graph """ nodes = graph_nodes_copy(beg) edges = graph_edges_copy(beg) g = graph(nodes, edges) # g.cache = beg.cache.copy() return g
[docs] def graph_same(g: graph, h: graph) -> bool: if set(g.nodes).symmetric_difference(h.nodes): return False if set(g.edges).symmetric_difference(h.edges): return False for n in g.nodes: if n not in h.nodes: return False if g.nodes[n] != h.nodes[n]: return False for n in g.edges: if n not in h.edges: return False if g.edges[n] != h.edges[n]: return False return True
[docs] def graph_fill(beg: graph) -> None: """ Fill all selected primitives in a graph """ for node in beg.nodes.values(): chem.bechem_fill(node) for edge in beg.edges.values(): chem.bechem_fill(edge) beg.cache.clear()
[docs] def graph_invert(beg: graph) -> None: """ Invert selected primitives in a graph """ for i, node in beg.nodes.items(): beg.nodes[i] = ~node for i, edge in beg.edges.items(): beg.edges[i] = ~edge beg.cache.clear() return beg
[docs] def graph_invert_any(beg: graph) -> None: """ Invert primitives in a graph that have any bit set """ for i, node in beg.nodes.items(): for prim in node.select: bv = node.primitives[prim] if bv.any(): beg.nodes[i].primitives[prim] = ~bv for i, edge in beg.edges.items(): for prim in edge.select: bv = edge.primitives[prim] if bv.any(): beg.edges[i].primitives[prim] = ~bv beg.cache.clear() return beg
[docs] def graph_invert_all(beg: graph) -> None: """ Invert primitives in a graph that are full """ for i, node in beg.nodes.items(): for prim in node.select: bv = node.primitives[prim] if bv.all(): beg.nodes[i].primitives[prim] = ~bv for i, edge in beg.edges.items(): for prim in edge.select: bv = edge.primitives[prim] if bv.all(): beg.edges[i].primitives[prim] = ~bv beg.cache.clear() return beg
[docs] def subgraph_invert(beg: subgraph) -> None: """ Invert selected primitives in a graph """ beg = subgraph_copy(beg) beg = graph_invert(beg) return beg
[docs] def structure_invert(beg: structure) -> None: """ Invert selected primitives in a graph """ beg = structure_copy(beg) beg = graph_invert(beg) return beg
[docs] def structure_invert_any(beg: structure) -> None: """ Invert selected primitives in a graph """ beg = structure_copy(beg) beg = graph_invert_any(beg) return beg
[docs] def graph_clear(beg: graph) -> graph: """ Clear all selected primitives in a graph """ beg = graph_copy(beg) for node in beg.nodes.values(): chem.bechem_clear(node) for edge in beg.edges.values(): chem.bechem_clear(edge) beg.cache.clear() return beg
[docs] def graph_minimum_spanning_tree( g: graph, adj=None, keep: Sequence[edge_id] = None ) -> graph: """ Return a minimum spanning tree of a graph. This is an unweighted version, and not likely not even minimum in any sense. It primarily works to break rings. Parameters ---------- g : graph The input graph adj : Dict[int, List[int]] An adjacency dictionary keep: Sequence[edge_id] A list of edges that must be in the spanning tree Returns ------- graph The minimum spanning tree of the graph """ g = graph_copy(g) if len(g.nodes) < 3: return g if adj is None: adj = graph_connections(g) if keep is None: keep = [] new = set([list(g.nodes)[0]]) visited = set() else: new = set(i for e in keep for i in e) visited = set(i for e in keep for i in e) edges = list(keep) while new: i = new.pop() # print("base", i) # print("path", path) visited.add(i) for n in (x for x in adj[i] if x not in visited): if n not in visited: edges.append(tuple(sorted([i, n]))) new.add(n) visited.add(n) g.edges = {e: g.edges[e] for e in edges} return g
[docs] def graph_relabel_nodes(g: graph, M: Dict[node_id, node_id]) -> graph: """ Return the nodes selected by the subgraph. Parameters ---------- g: subgraph The input subgraph Returns ------- Sequence[node_id] The selected nodes """ assert len(set(M)) == len(set(M.values())) nodes = {} edges = {} M = {k: v for k, v in M.items() if v is not None} for tn, fn in M.items(): # tc = bes.nodes.pop(tn) nodes[fn] = chem.bechem_copy(g.nodes[tn]) for (i, j) in list(g.edges): tc = g.edges[(i, j)] a, b = (M.get(i, i), M.get(j, j)) e = edge((a, b)) edges[e] = chem.bechem_copy(tc) g_ = graph(nodes, edges) return g_
[docs] def graph_remove_hydrogen(g: graph) -> graph: """ Remove the non-primary nodes that define hydrogen fragments. Only nodes that have only hydrogen defined in the element primitive are removed. Parameters ---------- g : graph The input graph Returns ------- structure A new graph without hydrogen """ nodes = { k: v for k, v in graph_nodes_copy(g).items() if not v.primitives[primitive_key.ELEMENT][1] } edges = { k: v for k, v in graph_edges_copy(g).items() if k[0] in nodes and k[1] in nodes } copy = graph(nodes, edges) return copy
[docs] def graph_to_subgraph(g: graph, select: Sequence[node_id]) -> subgraph: """ Make a subgraph from a given graph Parameters ---------- g : graph The input graph selection : Sequence[node_id] The selection of the subgraph Returns ------- subgraph """ g = graph_copy(g) return subgraph(g.nodes, g.edges, select)
[docs] def graph_as_subgraph(g: graph, select: Sequence[node_id]) -> subgraph: """ Make a subgraph from a given graph Parameters ---------- g : graph The input graph selection : Sequence[node_id] The selection of the subgraph Returns ------- subgraph """ return subgraph(g.nodes, g.edges, select)
[docs] def graph_to_subgraphs( g: graph, selections: Sequence[Sequence[node_id]] ) -> Sequence[subgraph]: """ Make subgraphs from a given graph Parameters ---------- g : graph The input graph selections : Sequence[Sequence[node_id]] A list of selections for each subgraph Returns ------- Sequence[subgraph] A sequence of subgraph, one for each input selection """ return [subgraph(g.nodes, g.edges, select) for select in selections]
[docs] def graph_to_structure( g: graph, select: Sequence[node_id], topo: topology.structure_topology ) -> structure: """ Make a structure from a given graph Parameters ---------- g : graph The input graph select : Sequence[Sequence[node_id]] The selection for the structure topo: structure_topology The topology of the structure Returns ------- structure A sequence of structures, one for each input selection """ g = graph_copy(g) return structure(g.nodes, g.edges, select, topo)
[docs] def graph_as_structure( g: graph, select: Sequence[node_id], topo: topology.structure_topology ) -> structure: """ Make a structure from a given graph Parameters ---------- g : graph The input graph select : Sequence[Sequence[node_id]] The selection for the structure topo: structure_topology The topology of the structure Returns ------- structure A sequence of structures, one for each input selection """ return structure(g.nodes, g.edges, select, topo)
[docs] def graph_to_structures( g: graph, selections: Sequence[Sequence[node_id]], topo: topology.structure_topology, ) -> Sequence[structure]: """ Make a structure from a given graph Parameters ---------- g : graph The input graph selections : Sequence[Sequence[node_id]] A list of selections for each structure topo: structure_topology The topology of the structure Returns ------- Sequence[structure] A sequence of structures, one for each input selection """ if selections is None: selections = graph_to_structure_topology(g, topo) else: return [structure(g.nodes, g.edges, select, topo) for select in selections]
[docs] def graph_to_structure_topology(g, topo) -> Sequence[structure]: ic_tab = { topology.atom : graph_to_structure_atoms, topology.bond : graph_to_structure_bonds, topology.angle : graph_to_structure_angles, topology.torsion : graph_to_structure_torsions, topology.outofplane : graph_to_structure_outofplanes, topology.pair : graph_to_structure_pairs } return ic_tab[topo](g)
[docs] def graph_to_structure_atoms(beg: graph) -> Sequence[structure]: """ Make a structure representing each atom in a graph Parameters ---------- g : graph The input graph Returns ------- Sequence[structure] A sequence of atom structures """ topo = topology.atom_topology() return graph_to_structures(beg, graph_atoms(beg), topo)
[docs] def graph_to_structure_pairs(beg: graph) -> Sequence[structure]: """ Make a structure representing each bond in a graph Parameters ---------- g : graph The input graph Returns ------- Sequence[structure] A sequence of bond structures """ topo = topology.n_body_topology(2) return graph_to_structures(beg, graph_pairs(beg), topo)
[docs] def graph_to_structure_bonds(beg: graph) -> Sequence[structure]: """ Make a structure representing each bond in a graph Parameters ---------- g : graph The input graph Returns ------- Sequence[structure] A sequence of bond structures """ topo = topology.bond_topology() return graph_to_structures(beg, graph_bonds(beg), topo)
[docs] def graph_to_structure_angles(g: graph): """ Make a structure representing each angle in a graph Parameters ---------- g : graph The input graph Returns ------- Sequence[structure] A sequence of angle structures """ topo = topology.angle_topology() return graph_to_structures(g, graph_angles(g), topo)
[docs] def graph_to_structure_torsions(g: graph) -> Sequence[structure]: """ Make a structure representing each torsion dihedral in a graph Parameters ---------- g : graph The input graph Returns ------- Sequence[structure] A sequence of torsion dihedral structures """ topo = topology.torsion_topology() return graph_to_structures(g, graph_torsions(g), topo)
[docs] def graph_to_structure_outofplanes(g: graph) -> Sequence[structure]: """ Make a structure representing each out-of-plane dihedral in a graph Parameters ---------- g : graph The input graph Returns ------- Sequence[structure] A sequence of out-of-plane dihedral structures """ topo = topology.outofplane_topology() return graph_to_structures(g, graph_outofplanes(g), topo)
[docs] def graph_set_primitives_atom( g: graph, select: Sequence[primitive_key] ) -> None: """ Set the selected primitives for the atoms Parameters ---------- g : graph The input the graph select : Sequence[primitive_key] The primitives to select in all atoms Returns ------- None """ for bet in g.nodes.values(): bet.select = tuple() for p in select: bet.enable(p)
[docs] def graph_disable_primitives_atom( g: graph, select: Sequence[primitive_key] ) -> None: """ Disable the selected primitives for the atoms Parameters ---------- g : graph The input the graph select : Sequence[primitive_key] The primitives to select in all atoms Returns ------- None """ for bet in g.nodes.values(): for p in select: bet.disable(p)
[docs] def graph_disable_primitives_bond( g: graph, select: Sequence[primitive_key] ) -> None: """ Disable the selected primitives for the atoms Parameters ---------- g : graph The input the graph select : Sequence[primitive_key] The primitives to select in all atoms Returns ------- None """ for bet in g.edges.values(): for p in select: bet.disable(p)
[docs] def graph_set_primitives_bond(beg, select): """ Set the selected primitives for the bonds Parameters ---------- g : graph The input the graph select : Sequence[primitive_key] The primitives to select in all bonds Returns ------- None """ for bet in beg.edges.values(): bet.select = tuple() for p in select: bet.enable(p)
[docs] def graph_is_null(g: graph) -> bool: """ Return whether any node or edge is null, defined as whether any primitive is null Parameters ---------- g : graph The input the graph Returns ------- bool Whether the graph is null """ for atom in g.nodes.values(): if atom.is_null(): return True for bond in g.edges.values(): if bond.is_null(): return True return False
[docs] def graph_is_valid(g: graph) -> bool: """ Return whether all nodes are not null, defined as whether every primitive has at least one value set. This indicates whether the graph can represent a valid SMILES or SMARTS string. Parameters ---------- g : graph The input the graph Returns ------- bool Whether the graph is valid """ for atom in g.nodes.values(): if not atom.is_valid(): return False for bond in g.edges.values(): if not bond.is_valid(): return False return True
[docs] def graph_any(g: graph) -> bool: """ Return whether there is a value set in any primitive in the graph. Parameters ---------- g : graph The input the graph Returns ------- bool Whether any primitive has a value """ for atom in g.nodes: if g.nodes[atom].any(): return True for bond in g.edges: if g.edges[bond].any(): return True return False
[docs] def graph_all(g: graph) -> bool: """ Return whether all primitives are full Parameters ---------- g : graph The input the graph Returns ------- bool Whether all primitives are full """ for atom in g.nodes: if not g.nodes[atom].all(): return False for bond in g.edges: if not g.edges[bond].all(): return False return True
[docs] def graph_bits(g: graph, maxbits=True) -> int: """ Return the number of bits set across all primitives Parameters ---------- g : graph The input the graph maxbits : bool Whether to use the maximum bit limits when counting full primitives Returns ------- int The number of bits set in the graph """ bits = 0 for atom in g.nodes: bits += g.nodes[atom].bits(maxbits=maxbits) for bond in g.edges: bits += g.edges[bond].bits(maxbits=maxbits) return bits
[docs] def graph_bits_max(g: graph) -> int: """ Return the number of maximum bits set across all primitives Parameters ---------- g : graph The input the graph maxbits : bool Whether to use the maximum bit limits when counting full primitives Returns ------- int The number of bits set in the graph """ bits = 0 for atom in g.nodes: bits += g.nodes[atom].bits_max() for bond in g.edges: bits += g.edges[bond].bits_max() return bits
[docs] def graph_atoms(g: graph) -> Sequence[Tuple[node_id]]: """ Return the IDs of the atoms Parameters ---------- g : graph The input the graph Returns ------- Sequence[Tuple[node_id]] A sequence of 1-tuples containing the atom IDs """ return tuple(((i,) for i in g.nodes.keys()))
def graph_pairs(g: graph) -> Sequence[Tuple[int, int]]: """ Return the IDs of the bonds Parameters ---------- g : graph The input the graph Returns ------- Sequence[Tuple[int, int]] A sequence of 2-tuples containing the bond IDs """ pairs = [] for i in g.nodes: for j in g.nodes: if j > i: pairs.append((i,j)) return tuple(pairs)
[docs] def graph_bonds(g: graph) -> Sequence[Tuple[int, int]]: """ Return the IDs of the bonds Parameters ---------- g : graph The input the graph Returns ------- Sequence[Tuple[int, int]] A sequence of 2-tuples containing the bond IDs """ return tuple(g.edges.keys())
[docs] def graph_pairs(g: graph) -> Sequence[Tuple[int, int]]: """ Return the IDs of the pairs Parameters ---------- g : graph The input the graph Returns ------- Sequence[Tuple[int, int]] A sequence of 2-tuples containing the pair IDs """ pairs = [] for i in g.nodes: for j in g.nodes: if j <= i or (i,j) in g.edges: continue pairs.append((i,j)) return tuple(pairs)
[docs] def graph_angles(g: graph) -> Sequence[Tuple[int, int, int]]: """ Return the IDs of the angles Parameters ---------- g : graph The input the graph Returns ------- Sequence[Tuple[int, int, int]] A sequence of 3-tuples containing the angle IDs """ angles = [] for i, bond_i in enumerate(g.edges): for j, bond_j in enumerate(g.edges): if j <= i: continue combo = set((*bond_i, *bond_j)) if len(combo) == 3: for k, c in enumerate(combo): if c in bond_i and c in bond_j: combo.remove(c) adj = sorted(combo) angles.append((adj[0], c, adj[1])) break return tuple(sorted(list(set(angles)), key=lambda x: (x[1], x[0], x[2])))
[docs] def graph_torsions(g: graph) -> Sequence[Tuple[int, int, int, int]]: """ Return the IDs of the torsion dihedrals Parameters ---------- g : graph The input the graph Returns ------- Sequence[Tuple[int, int, int, int]] A sequence of 4-tuples containing the torsion dihedral IDs """ torsions = [] angles = graph_angles(g) for angle_i in angles: adj = graph_connection(g, angle_i[0]) adj = [x for x in adj if x not in angle_i] for n in set(adj): combo = ( n, *angle_i, ) if not geometry.is_torsion(combo, g.edges): continue torsions.append(geometry.torsion(combo)) adj = graph_connection(g, angle_i[2]) adj = [x for x in adj if x not in angle_i] for n in set(adj): combo = (*angle_i, n) if not geometry.is_torsion(combo, g.edges): continue torsions.append(geometry.torsion(combo)) return tuple(sorted(list(set(torsions)), key=lambda x: (x[1], x[2], x[0], x[3])))
[docs] def graph_outofplanes(g: graph) -> Sequence[Tuple[int, int, int, int]]: """ Return the IDs of the nonlinear out-of-plane dihedrals Parameters ---------- g : graph The input the graph Returns ------- Sequence[Tuple[int, int, int, int]] A sequence of 4-tuples containing the dihedral IDs """ dihedrals = [] angles = graph_angles(g) for angle_i in angles: adj = graph_connection(g, angle_i[1]) adj = [x for x in adj if x not in angle_i] for n in set(adj): combo = (*angle_i, n) if geometry.is_outofplane(combo, g.edges): dihedrals.append(geometry.outofplane(combo)) return tuple( sorted(list(set(dihedrals)), key=lambda x: (x[1], x[0], x[2], x[3])) )
[docs] def graph_topology(g, topo): """ Return the IDs of the primary atoms defined by the topology Parameters ---------- g : graph The input the graph Returns ------- Sequence[Tuple[int, int, int, int]] A sequence of 4-tuples containing the dihedral IDs """ if topo == topology.atom: return graph_atoms(g) if topo == topology.bond: return graph_bonds(g) if topo == topology.angle: return graph_angles(g) if topo == topology.torsion: return graph_torsions(g) if topo == topology.outofplane: return graph_outofplanes(g) if topo == topology.null: return [(0,)] return []
[docs] def graph_symbols(g: graph): s = {} for n in sorted(g.nodes): node = g.nodes[n] assert "element" in node.primitives e = node.primitives['element'].on()[0] s[n] = primitives.element_tr[str(e)] return s
[docs] def subgraph_connection(g: subgraph, a: int) -> Sequence[node_id]: """ Return the selected nodes that are connected to a given node """ return tuple( set(x for edge in g.edges for x in edge if a in edge and a != x) )
[docs] def subgraph_connections(g: subgraph) -> Sequence[node_id]: """ Return the selected nodes that are connected to a given node """ edges = {} for a in g.select: edges[a] = subgraph_connection(g, a) return edges
[docs] def subgraph_edges(sg: subgraph) -> Sequence[edge_id]: """ Return the edges of the node-induced subgraph Parameters ---------- subg : subgraph The input subgraph Returns ------- Sequence[edge_id] The sequence of edges of the subgraph """ edges = tuple( (i, j) for (i, j) in sg.edges if i in sg.select and j in sg.select ) return edges
[docs] def subgraph_to_graph(g: subgraph) -> graph: """ Make a graph from a subgraph that only includes the subgraph Parameters ---------- subg : subgraph The input subgraph Returns ------- graph A graph only containing the subgraph """ nodes = {i: c for i, c in g.nodes.items() if i in g.select} edges = {edge: g.edges[edge] for edge in subgraph_edges(g)} g_ = graph(nodes, edges) return g_
[docs] def subgraph_hash(g: subgraph) -> int: """ Return the hash of a subgraph. Parameters ---------- Returns ------- int The hash """ return hash(g)
[docs] def subgraph_invert_null(g: subgraph): """ Fill primitives that are null """ for node in [g.nodes[i] for i in g.select]: for chem in node.primitives.values(): if not chem.any(): chem[:] = True for (i, j) in g.edges: if i in g.select and j in g.select: edge = g.edges[(i, j)] for chem in edge.primitives.values(): if not chem.any(): chem[:] = True return g
[docs] def structure_to_graph(g: structure) -> graph: """ Make a graph from a structure's subgraph that only includes the subgraph Parameters ---------- g : structure The input structure Returns ------- graph A graph only containing the subgraph """ return subgraph_to_graph(g)
[docs] def structure_to_subgraph(g: structure) -> subgraph: """ Make a graph from a structure's subgraph that only includes the subgraph Parameters ---------- g : structure The input structure Returns ------- subgraph A graph only containing the subgraph """ return subgraph_copy(subgraph(g.nodes, g.edges, g.select))
[docs] def structure_node_depths(g: structure) -> Sequence[int]: """ Return the depths of all selected nodes in a structure, defined as the distance away from the primary nodes of the structure's topology. Parameters ---------- g : structure The input structure Returns ------- Dict[node_id, int] A mapping of nodes their depths """ lens = [len(g.topology.primary)] depths = {i: structure_node_depth(g, i) for i in g.select} max_depth = structure_max_depth(g) for i in range(1, max_depth + 1): lens.append(len([x for x in depths.values() if x == i])) return lens
[docs] def graph_remove_nodes(g: graph, nodes: Sequence[node_id]) -> graph: """ Remove a list of nodes from a structure Parameters ---------- g : structure The input structure nodes : Sequence[node_id] The nodes to remove Returns ------- structure A new structure with the nodes removed """ n = { k: chem.bechem_copy(v) for k, v in g.nodes.items() if k not in nodes } e = { k: chem.bechem_copy(v) for k, v in g.edges.items() if k[0] in n and k[1] in n } return graph(n, e)
[docs] def structure_remove_nodes(g: structure, nodes: Sequence[node_id]) -> structure: """ Remove a list of nodes from a structure Parameters ---------- g : structure The input structure nodes : Sequence[node_id] The nodes to remove Returns ------- structure A new structure with the nodes removed """ n = { k: chem.bechem_copy(v) for k, v in g.nodes.items() if k not in nodes } s = tuple((n_i for n_i in g.select if n_i in n and n_i not in nodes)) e = { k: chem.bechem_copy(v) for k, v in g.edges.items() if k[0] in s and k[1] in s } c = structure_remove_unselected( structure(n, e, s, g.topology) ) for select, h in g.hashes.items(): if all([x not in select for x in nodes]): c.hashes[select] = h return c
[docs] def structure_unreachable_nodes(g: structure) -> Sequence[node_id]: """ Return the nodes that are not reachable from the primary nodes of the topology Parameters ---------- g : structure The input structure Returns ------- Sequence[node_id] The unreachable nodes """ seen = set([g.select[i] for i in g.topology.primary]) change = True while change: change = False for i, j in g.edges: if i not in g.select or j not in g.select: continue if i in seen and j not in seen: change = True seen.add(j) elif j in seen and i not in seen: change = True seen.add(i) unconnected = [x for x in g.select if x not in seen] return unconnected
[docs] def structure_prune_null_nodes(g: structure) -> structure: """ Remove nodes that have any null primitives Parameters ---------- g : structure The input structure Returns ------- structure A new structure with the nodes removed, or None if the result is not a valid structure """ remove = [] for idx, node in g.nodes.items(): if not all((x.any() for x in node.primitives.values())): remove.append(idx) if [x in [list(g.select)[i] for i in g.topology.primary] for x in remove]: return None g_ = structure_remove_nodes(g, remove) remove = structure_unreachable_nodes(g_) g_ = structure_remove_nodes(g_, remove) return g_
[docs] def subgraph_to_structure( g: subgraph, topo: topology.structure_topology ) -> structure: """ Return a structure from a copy of the subgraph Parameters ---------- g: subgraph The input subgraph topo: topology.structure_topology The topology of the structure Returns ------- structure The structure """ g_ = graph_copy(g) g_ = structure(g_.nodes, g_.edges, tuple(g.select), topo) return g_
[docs] def subgraph_to_structure_bond(g: subgraph) -> structure: return subgraph_to_structure(g, topology.bond)
[docs] def subgraph_to_structure_angle(g: subgraph) -> structure: return subgraph_to_structure(g, topology.angle)
[docs] def subgraph_to_structure_torsion(g: subgraph) -> structure: return subgraph_to_structure(g, topology.torsion)
[docs] def subgraph_to_structure_outofplane(g: subgraph) -> structure: return subgraph_to_structure(g, topology.outofplane)
[docs] def subgraph_as_graph( g: subgraph ) -> structure: """ Return a structure from downcasting a subgraph. Parameters ---------- subg: subgraph The input subgraph topology: topology.structure_topology The topology of the structure Returns ------- structure The structure. The graph is not copied and references the input """ return graph(g.nodes, g.edges)
[docs] def subgraph_as_structure( g: subgraph, topo: topology.structure_topology ) -> structure: """ Return a structure from downcasting a subgraph. Parameters ---------- subg: subgraph The input subgraph topology: topology.structure_topology The topology of the structure Returns ------- structure The structure. The graph is not copied and references the input """ g = structure(g.nodes, g.edges, g.select, topo) return g
[docs] def subgraph_validate_structure( g: subgraph, topo: topology.structure_topology ) -> bool: """ Return a structure Parameters ---------- subg: subgraph The input subgraph topology: topology.structure_topology The topology of the structure Returns ------- bool Whether the subgraph can be represented as a structure with the given topology """ try: _ = structure(g.nodes, g.edges, tuple(g.select), topo) except AssertionError: breakpoint() _ = structure(g.nodes, g.edges, tuple(g.select), topo) return False return True
[docs] def subgraph_copy(g: subgraph) -> subgraph: """ Return a copy Parameters ---------- subg: subgraph The input subgraph Returns ------- subgraph A copy of the subgraph """ g_ = graph_copy(g) g_ = subgraph(g_.nodes, g_.edges, tuple(g.select)) return g_
[docs] def subgraph_nodes(g: subgraph) -> Sequence[node_id]: """ Return the nodes selected by the subgraph. Parameters ---------- g: subgraph The input subgraph Returns ------- Sequence[node_id] The selected nodes """ nodes = tuple(g.select) return nodes
[docs] def subgraph_relabel_nodes(g: subgraph, M: Dict[node_id, node_id]) -> subgraph: """ Return the nodes selected by the subgraph. Parameters ---------- g: subgraph The input subgraph Returns ------- Sequence[node_id] The selected nodes """ select = tuple((M.get(i, i) for i in g.select)) g_ = graph_relabel_nodes(g, M) return graph_as_subgraph(g_, select)
[docs] def subgraph_fill(g: subgraph) -> None: """ Fill the primitives in the subgraph. Parameters ---------- subg: subgraph The input subgraph Returns ------- None """ graph_fill(g)
[docs] def subgraph_bits(g: subgraph) -> int: """ Return the number of bits set across all selected primitives of the subgraph Parameters ---------- subg : subgraph The input the graph maxbits : bool Whether to use the maximum bit limits when counting full primitives Returns ------- int The number of bits set in the subgraph """ g_ = subgraph_to_graph(g) b = graph_bits(g_) return b
[docs] def subgraph_remove_unselected(g: structure) -> structure: """ Return a subgraph that has all unselected nodes removed from the graph. Parameters ---------- g : subgraph The input structure Returns ------- subgraph A new subgraph with no unselected nodes """ # g = structure_copy(g) nodes = {i: chem.bechem_copy(g.nodes[i]) for i in g.select} edges = { i: chem.bechem_copy(g.edges[i]) for i in subgraph_edges(g) } g_ = subgraph(nodes, edges, tuple(g.select)) return g_
[docs] def subgraph_any(g: subgraph) -> bool: """ Return whether there is a value set in any primitive in the subgraph. Parameters ---------- g : graph The input the subgraph Returns ------- bool Whether any primitive has a value """ for i in g.select: node = g.nodes[i] if any((node.primitives[p].any() for p in node.select)): return True for (i, j) in g.edges: if i in g.select and j in g.select: edge = g.edges[(i, j)] if any((edge.primitives[p].any() for p in edge.select)): return True return False
[docs] def structure_bits(g: structure) -> int: """ Return the number of bits set across all selected primitives of the subgraph Parameters ---------- g : structure The input the graph maxbits : bool Whether to use the maximum bit limits when counting full primitives Returns ------- int The number of bits set in the subgraph """ b = subgraph_bits(g) return b
[docs] def subgraph_print(g) -> None: for i, c in g.nodes.items(): for n, p in c.primitives.items(): print(f"{i:12d} {str(n):35s} {p}") for i, c in g.edges.items(): for n, p in c.primitives.items(): print(f"{str(i):12s} {str(n):35s} {p}") return
[docs] def structure_print(g) -> None: subgraph_print(g) return
[docs] def structure_remove_full_leaves(g: structure) -> structure: adj = structure_connections(g) primary = [g.select[i] for i in g.topology.primary] remove = [] for i, n in g.nodes.items(): if n.all() and len(adj[i]) == 1 and i not in primary: j = adj[i][0] e = edge((i,j)) if g.edges[e].all(): remove.append(i) if remove: g = structure_remove_nodes(g, remove) return structure_remove_full_leaves(g) return g
[docs] def structure_remove_empty_leaves(g: structure) -> structure: adj = structure_connections(g) remove = [] primary = [g.select[i] for i in g.topology.primary] for i, n in g.nodes.items(): if (not n.any()) and len(adj[i]) == 1 and (i not in primary): j = adj[i][0] e = edge((i,j)) if not g.edges[e].any(): remove.append(i) if remove: g = structure_remove_nodes(g, remove) return structure_remove_empty_leaves(g) return g
[docs] def structure_branch(template, m: dict, n, d, visited_groups=None, verbose=True) -> Generator: # if n < 1: # yield structure_copy(template) if visited_groups is None: visited_groups = set() nodes = set([x for x in structure_up_to_depth(template, d).nodes if x not in m.values()]) nodes = list((x for x in nodes if structure_node_depth(template, x) <= d)) if not nodes: yield structure_copy(template) if verbose: print(datetime.datetime.now(), f"Branching depth={d} nodes={len(nodes)} n={n} groups={len(visited_groups)}") for group in range(1, n + 1): nck = list(itertools.combinations(nodes, group)) for node_set in nck: if node_set in visited_groups: continue visited_groups.add(node_set) remove = [ x for x in template.select if x not in m.values() and x not in node_set ] g = structure_remove_nodes(template, remove) remove = structure_unreachable_nodes(g) g = structure_remove_nodes(g, remove) yield structure_copy(g) yield from structure_branch( template, m, n - len(node_set), d, visited_groups=visited_groups, verbose=verbose )
[docs] def structure_extend( config: configs.smarts_extender_config, atoms: Sequence[structure] ) -> bool: """ Extend the selection of each structure to the specified depth. This potentially modifies the selection in each structure. Parameters ---------- config : smarts_extender_config The configuration for extending the structures atoms : List[graphs.structure] The structures to extend. Returns ------- bool Whether any of the structure selections were modified. """ modified = True i = -1 include_hydrogen = config.include_hydrogen depth_min = config.depth_min depth_max = config.depth_max success = False while modified: i += 1 modified = False groups = [list(range(len(atoms)))] for group in groups: for atom_env in (atoms[j] for j in group): primaries = [ atom_env.select[n] for n in atom_env.topology.primary ] adj = graph_connections(atom_env) if not adj: continue neighbors = set( x for atom in atom_env.select for x in adj[atom] ) neighbors.difference_update(atom_env.select) if not include_hydrogen: neighbors = set( x for x in neighbors if not ( atom_env.nodes[x][primitive_key.ELEMENT].bits() == 1 and atom_env.nodes[x][primitive_key.ELEMENT][1] ) ) lengths = { nbr: min( ( graph_shortest_path_length( atom_env, origin, nbr, adj ) for origin in primaries ) ) for nbr in neighbors } # print("lengths\n", lengths) extension = [] for nbr, depth in lengths.items(): below = depth <= depth_min above = depth_max is not None and depth > depth_max if (below or not above) and nbr not in atom_env.select: if nbr not in extension: extension.append(nbr) if extension: atom_env.select = (*atom_env.select, *extension) atom_env.cache.clear() modified = True success = True return success
[docs] def structure_frontier_nodes(g, nodes, adj=None) -> Dict[int, Sequence[int]]: """ Return the nodes that are one level deeper than the given input nodes of the structure. Parameters ---------- g : structure The input structure nodes : Sequence[node_id] The input nodes to get the frontier nodes of adj : Dict[node_id, Sequence[node_id]] An adjacency map of the nodes Returns ------- Dict[node_id, Sequence[node_id]] A mapping of nodes and their corresponding frontier nodes. """ depth_cache = {} ftrs = {} for node in nodes: depth = structure_node_depth( g, node, depth_cache=depth_cache, adj=adj ) n_adj = tuple( ( n for n in subgraph_connection(g, node) if structure_node_depth(g, n, depth_cache=depth_cache) == depth + 1 ) ) ftrs[node] = n_adj return ftrs
[docs] def structure_relabel_nodes( g: structure, M: Dict[node_id, node_id] ) -> structure: """ Return a structure copy with relabled nodes Parameters ---------- g : structure The input structure M : Dict[node_id, node_id] A mapping of nodes and their new ID Returns ------- structure The relabeled structure """ g_ = subgraph_relabel_nodes(g, M) return subgraph_as_structure(g_, g.topology)
[docs] def structure_vertices_at_depth( g: structure, depth: int, depth_cache=None, adj=None ): """ Return the nodes at some some depth from the primary nodes Parameters ---------- g: structure The input structure depth : int The depth of the returned nodes depth_cache : Dict[node_id, int] A cache of precalculated depths adj : Dict[node_id, Sequence[node_id]] An adjacency map Returns ------- Sequence[node_id] The nodes at the given depth """ if depth_cache is not None: ret = depth_cache.get(depth) if ret is not None: return ret ret = set() for v in g.select: lens = [] for i in g.topology.primary: root_v = g.select[i] path_len = graph_shortest_path_length(g, root_v, v, adj) if path_len is not None: lens.append(path_len) if lens and min(lens) == depth: ret.add(v) if depth_cache is not None: depth_cache[depth] = ret return ret
[docs] def structure_connections(g: structure) -> Dict[node_id, List[node_id]]: connect = { n: [node_id(x) for y in g.edges for x in y if n in y and n != x] for n in g.select } return connect
[docs] def structure_fill(g: structure): graph_fill(g)
[docs] def structure_node_depth(g: structure, node: node_id, depth_cache=None, adj=None): """ get the vertices at some some depth from the primary set """ depth = 0 while depth <= structure_max_depth(g, adj=adj): nodes = structure_vertices_at_depth( g, depth, depth_cache=depth_cache, adj=adj ) if node in nodes: break else: depth += 1 return depth
[docs] def structure_max_depth(g: structure, adj=None) -> int: i = 0 seen = set() while len(seen) != len(g.select): result = structure_vertices_at_depth(g, i, adj=adj) if not result: i = max(i - 1, 0) break seen.update(result) i = i + 1 if i > 0: i -= 1 return i
[docs] def structure_edges(bes: structure) -> Sequence[Tuple[int, int]]: return subgraph_edges(bes)
[docs] def structure_nodes(bes: structure) -> Sequence[int]: return subgraph_nodes(bes)
[docs] def structure_topology_nodes(bes: structure) -> Sequence[int]: nodes = subgraph_nodes(bes) return tuple(nodes[i] for i in bes.topology.primary)
[docs] def structure_topology_edges(g: structure) -> Sequence[edge_id]: primary = structure_topology_nodes(bes) return tuple((edge((primary[i], primary[j])) for i, j in g.topology.connect))
[docs] def structure_up_to_depth(g: structure, i: int, adj=None): g = structure_copy(g) to_remove = [] for d in range(structure_max_depth(g), i, -1): nodes = structure_vertices_at_depth(g, d, adj=adj) if nodes: to_remove.extend(nodes) return structure_remove_nodes(g, to_remove)
[docs] def structure_copy(g: structure) -> structure: g_ = subgraph_copy(g) g_ = structure(g_.nodes, g_.edges, tuple(g_.select), g.topology) return g_
[docs] def structure_clear(g: structure) -> structure: g_ = graph_clear(g) g_ = structure(g_.nodes, g_.edges, tuple(g.select), g.topology) return g_
[docs] def structure_remove_unselected(g: structure) -> structure: """ Return a structure that has all unselected nodes removed from the graph. Parameters ---------- g : structure The input structure Returns ------- structure A new structure with no unselected nodes """ # g = structure_copy(g) nodes = {i: chem.bechem_copy(g.nodes[i]) for i in g.select} edges = { i: chem.bechem_copy(g.edges[i]) for i in subgraph_edges(g) } g_ = structure(nodes, edges, tuple(g.select), g.topology) return g_
[docs] def structure_remove_hydrogen(g: structure) -> structure: """ Remove the non-primary nodes that define hydrogen fragments. Only nodes that have only hydrogen defined in the element primitive are removed. Parameters ---------- g : structure The input structure Returns ------- structure A new structure without hydrogen """ primary = [g.select[i] for i in g.topology.primary] nodes = { i: chem.bechem_copy(g.nodes[i]) for i in g.select if not ( g.nodes[i].primitives[primitive_key.ELEMENT][1] and i not in primary ) } edges = { i: chem.bechem_copy(g.edges[i]) for i in subgraph_edges(g) } select = tuple(i for i in g.select if i in nodes) g_ = structure(nodes, edges, select, g.topology) return g_
[docs] def structure_build( g: graph, select: Sequence[int], topo: topology.structure_topology ) -> structure: """ Build a structure from a graph Parameters ---------- g : graph The input graph select : Sequence[int] The selection defining the subgraph topology : structure_topology The topology defining the structure Returns structure A structure defining some given topology """ g = structure(g.nodes, g.edges, select, topo) return bes
[docs] def structure_atom(g: graph, select: Sequence[int]) -> structure: """ Build a structure of an atom from a graph Parameters ---------- g : graph The input graph select : Sequence[int] The selection defining the subgraph Returns structure A structure defining an atom """ return structure_build(g, select, topology.atom_topology())
[docs] def structure_bond(g: graph, select: Sequence[int]) -> structure: """ Build a structure of a bond from a graph Parameters ---------- g : graph The input graph select : Sequence[int] The selection defining the subgraph Returns structure A structure defining a bond """ return structure_build(g, select, topology.bond_topology())
[docs] def structure_angle(g: graph, select: Sequence[int]) -> structure: """ Build a structure of an angle from a graph Parameters ---------- g : graph The input graph select : Sequence[int] The selection defining the subgraph Returns structure A structure defining an angle """ return structure_build(g, select, topology.angle_topology())
[docs] def structure_torsion(g: graph, select: Sequence[int]) -> structure: """ Build a structure of a torsion dihedral from a graph Parameters ---------- g : graph The input graph select : Sequence[int] The selection defining the subgraph Returns structure A structure defining a torsion dihedral """ return structure_build(g, select, topology.torsion_topology())
[docs] def structure_outofplane(g: graph, select: Sequence[int]) -> structure: """ Build a structure of an out-of-plane dihedral from a graph Parameters ---------- g : graph The input graph select : Sequence[int] The selection defining the subgraph Returns structure A structure defining an out-of-plane dihedral """ return structure_build(g, select, topology.outofplane_topology())
[docs] def structure_hash(g: structure) -> int: """ Return the hash of a structure Parameters ---------- Returns ------- int The hash """ return hash(g)
[docs] def graph_shortest_path(g: graph, a: node_id, b: node_id, adj=None) -> Sequence[node_id]: """ Return the shortest path between two nodes Parameters ---------- g : graph The input graph a : node_id The source node b : node_id The destination node adj : Dict[node_id, Sequence[node_id]] A node adjacency map Returns ------- Sequence[node_id] The shortest path of nodes between the source and destination. The path includes the source and destination. """ if a == b: return tuple([a]) paths = g.cache.get("shortest_paths") if paths is not None: # print("cached", paths[a][b]) return tuple(paths[a][b]) path = None paths = g.cache.get("shortest_path") if paths is not None: path = paths.get((a, b)) if path is not None: # print("cached2", path) return tuple(path) path = paths.get((b, a)) if path is not None: # print("cached3", path) return tuple(reversed(path)) path = {} # g.cache["shortest_path"] = {} if adj is None or not adj: adj = graph_connections(g) new = [a] visited = [a] path = {} debug = False if debug: print("from", a, "to", b) iteration = 1 if b in adj[a]: return (a,b) while new: if iteration == 1000: debug = True print("WARNING max iterations") iteration += 1 i = new.pop() if debug: print("base", i) print("path", path) visited.append(i) nbr = [] if i in adj: nbr = [x for x in adj[i] if (x not in visited) or x == b] for n in nbr: if debug: print("visit", n, "of list", nbr) if i in path: if n in path and len(path[n]) < len(path[i]): if debug: print("this path", path[n], "is shorter than", path[i], a, " to", b, ": replace with", list(path[n]) + [i]) path[i] = list(path[n]) + [i] else: path[n] = path[i] + [n] if debug: print("continuing path", i, "to", n, ":", path[n]) # if n == b : # continue elif n not in path: if n == b: # g.cache["shortest_path"][(a, b)] = tuple([a, n]) return tuple([a, n]) path[n] = [n] if debug: print("setting initial path to", n, "from", i, ":", path[n]) if n not in new: new.append(n) # if b not in path: # g.cache["shortest_path"][(a, b)] = None # return None # this must mean there is no path i.e. disconnected if b in path: path = tuple([a] + path[b]) else: path = tuple() # g.cache["shortest_path"][(a, b)] = path if debug: print("returned", path) return path
[docs] def graph_detect_rings(g: graph, adj=None): """ Decompose and return the shortest paths, connections, and ring edges of a graph Parameters ---------- g : graph The input graph adj : Dict[node_id, Sequence[node_id]] A node adjacency map Returns ------- tuple paths : Dict[node_id, Dict[node_id, Sequence[node_id]]] The shortest paths between all nodes that avoid ring edges connections : Dict[node_id, Sequence[node_id]] A node adjacency map that avoids ring edges rings : Dict[edge_id, chem.bechem] A mapping of edges and their primitives that form rings/cycles in the graph """ if adj is None: adj = graph_connections(g) MST = graph_minimum_spanning_tree(g, adj) adj = adj.copy() rings = {} for a, nbrs in list(adj.items()): for b in nbrs: e = tuple(sorted((a, b))) if e not in MST.edges: adj[a].remove(b) adj[b].remove(a) rings[e] = g.edges[e] paths = graph_shortest_paths(MST, adj=adj) return paths, adj, rings
[docs] def graph_shortest_paths(g: graph, adj=None): """ Return the shortest paths for each pair of nodes in a graph Parameters ---------- g : graph The input graph adj : Dict[int, Seqence[node_id]] A node adjacency map Returns ------- Dict[node_id, Dict[node_id, Sequence[node_id]]] A 2D mapping of source and destination nodes to a list of nodes defining the shortest path. The source and destination nodes are included in the path. For example, paths[a][b] is a list of nodes describing the shortest path between nodes a and b. """ paths = g.cache.get("shortest_paths") if paths is None: paths = {a: dict() for a in g.nodes} if adj is None: adj = graph_connections(g) for a in sorted(g.nodes): for b in sorted(g.nodes): if b < a: continue paths[a][b] = tuple(graph_shortest_path(g, a, b, adj=adj)) paths[b][a] = tuple(paths[a][b][::-1]) # g.cache["shortests_paths"] = paths if "shortest_path" in g.cache: g.cache.pop("shortest_path") return paths
[docs] def graph_shortest_path_length( g: graph, a: node_id, b: node_id, adj=None ) -> int: """ Return the length of the shortest path between two nodes Parameters ---------- g: graph The input graph src : node_id The source node dst : node_id The destination node adj : Dict[node_id, Sequence[node_id] A node adjacency map Returns ------- int The path length """ l = graph_shortest_path(g, a, b, adj) if l is None: return None else: return len(l)-1
[docs] def graph_to_intvec(g: graph, atom_primitives, bond_primitives) -> arrays.intvec: intvec = arrays.intvec() vec = intvec.v vec.fromlist([len(g.nodes), len(atom_primitives), len(g.edges), len(bond_primitives), 0]) for k, v in g.nodes.items(): vec.append(k) vec.fromlist([v.primitives[name].v for name in atom_primitives]) for k, v in g.edges.items(): vec.append(k[0]) vec.append(k[1]) vec.fromlist([v.primitives[name].v for name in bond_primitives]) return intvec
[docs] def subgraph_to_intvec(g: subgraph, atom_primitives, bond_primitives) -> arrays.intvec: intvec = arrays.intvec() vec = intvec.v vec.fromlist([len(g.nodes), len(atom_primitives), len(g.edges), len(bond_primitives), -1]) for k in g.select: vec.append(-k) vec.fromlist([g.nodes[k].primitives[name].v for name in atom_primitives]) for k, v in [(k, v) for k,v in g.nodes.items() if k not in g.select]: vec.append(k) vec.fromlist([v.primitives[name].v for name in atom_primitives]) for k, v in g.edges.items(): vec.append(k[0]) vec.append(k[1]) vec.fromlist([v.primitives[name].v for name in bond_primitives]) return intvec
[docs] def structure_to_intvec(g: structure, atom_primitives, bond_primitives) -> arrays.intvec: intvec = subgraph_to_intvec(g, atom_primitives, bond_primitives) intvec.v[4] = topology.index_of(g.topology) return intvec
[docs] def graph_complexity(g: graph, scale=1.0, offset=0.0): # C = len(g.nodes) + graphs.graph_bits_max(g) / graphs.graph_bits(g) / len(g.nodes) / 100 # return scale*C + offset C = graph_bits(g) / (len(g.nodes) + len(g.edges)) return C