Source code for molfoundry.graph

from typing import Optional, Callable, List, overload

from ._engine import engine

from .atomid import AtomId
from .graphprinter import GraphPrinter
from .graphtype import GraphType
from .isotope import Isotope
from .charge import Charge
from .bondtype import BondType
from .aut import Aut
from .autgroup import AutGroup
from .core import TSObject
from .post import summaryRaw
from .exceptions import *
from .inchi import inchify_molfile, is_available

_class_ref_Graph = TSObject.get_class_ref("Graph")
_class_ref_Vertex = TSObject.get_class_ref("Vertex")
_class_ref_Edge = TSObject.get_class_ref("Edge")
_smiles_func: Callable[[str, Optional[str], str], "Graph"] = engine.eval("smiles")
_inchi_func: Callable[[str, Optional[str], str], "Graph"] = engine.eval("inchi")
_molfile_func: Callable[[str, Optional[str], str], "Graph"] = engine.eval("molfile")
_graphGMLString_func: Callable[[str, Optional[str], str], "Graph"] = engine.eval("graphGMLString")


# noinspection PyPep8Naming
[docs] class Graph(TSObject): # Injectable global state add function to prevent circular import _add_graph: Callable[["Graph"], None] def __init__(self) -> None: super().__init__(_class_ref_Graph)
[docs] @staticmethod def fromGMLString(gml: str, name: Optional[str] = None, graphType: GraphType = GraphType.Default, add=True) -> "Graph": g = Graph.wrap(_graphGMLString_func(gml, name, graphType.value)) if add: Graph._add_graph(g) return g
[docs] @staticmethod def fromSMILES(smiles: str, name: Optional[str] = None, graphType: GraphType = GraphType.Default, add=True) -> "Graph": g = Graph.wrap(_smiles_func(smiles, name, graphType.value)) if add: Graph._add_graph(g) return g
[docs] @staticmethod def fromInChI(inchi: str, name: Optional[str] = None, graphType: GraphType = GraphType.Default, add=True) -> "Graph": g = Graph.wrap(_inchi_func(inchi, name, graphType.value)) if add: Graph._add_graph(g) return g
[docs] @staticmethod def fromMolfile(molfile: str, name: Optional[str] = None, graphType: GraphType = GraphType.Default, add=True) -> "Graph": g = Graph.wrap(_molfile_func(molfile, name, graphType.value)) if add: Graph._add_graph(g) return g
[docs] @staticmethod def fromRMG(adjacencyList: str, name: Optional[str] = None, add=True) -> "Graph": g = Graph.wrap(TSObject.call_static(_class_ref_Graph, "fromRMG", adjacencyList, name)) if add: Graph._add_graph(g) return g
[docs] def getGMLString(self, withCoords: bool = False) -> str: return self._ref.getGMLString(withCoords)
[docs] def vLabelCount(self, label: str) -> int: return int(self._ref.vLabelCount(label))
[docs] def eLabelCount(self, label: str) -> int: return int(self._ref.eLabelCount(label))
[docs] def aut(self) -> AutGroup: return AutGroup.wrap(self._ref.aut())
[docs] def autInChI(self) -> AutGroup | None: if not is_available(): return None result = inchify_molfile(self.molfile, "/Automs", 0, 0) if result[9] != 0: return None saux = result[5] lines = saux.splitlines() # Parse generators from aux info and remap them to our vertex order inside_aut = False gens = [] mapping = {i + 1: int(x) - 1 for i, x in enumerate(lines[0].split("/N:")[1].split("/")[0].split(","))} for line in lines: if line.startswith("Automorphisms"): inside_aut = True elif inside_aut and line.startswith(" g"): gen = line.split("=")[1].strip() parts = [ [mapping[int(y)] for y in x.rstrip(" \t)").split(" ") if len(y) > 0] for x in gen.split("(")[1:] ] gen = " ".join([f"({' '.join(map(str, x))})" for x in parts]) gens.append(Aut.fromCycleNotation(self, gen)) return AutGroup.fromGens(self, gens)
@property def id(self) -> int: return int(self._ref.getId()) @property def type(self) -> GraphType: return GraphType.Electron if self._ref.getType() == "electron" else GraphType.Default @property def name(self) -> str: return self._ref.getName() @property def canonKey(self) -> str: return self._ref.getCanonKey() @property def smiles(self) -> str: return self._ref.getSMILES() @property def smilesWithIds(self) -> str: return self._ref.getSMILESWithIds() @property def smilesWithExternalIds(self) -> str: return self._ref.getSMILESWithExternalIds() @property def molfile(self) -> str: return self._ref.getMOLFile() @property def inchi(self) -> str | None: if not is_available(): return None result = inchify_molfile(self.molfile, "", 0, 0) return result[2] if result[9] == 0 else None @property def inchiKey(self) -> str | None: if not is_available(): return None result = inchify_molfile(self.molfile, "", 1, 0) return result[6] if result[9] == 0 else None @property def numVertices(self) -> int: return int(self._ref.getNumVertices()) @property def numEdges(self) -> int: return int(self._ref.getNumEdges()) @property def isMolecule(self) -> bool: return self._ref.isMolecule() @property def exactMass(self) -> float: if self.isMolecule: return self._ref.getExactMass() raise LogicError("Graph is not a molecule and exact mass cannot be calculated") @property def vertices(self) -> "VertexRange": return VertexRange(self) @property def edges(self) -> "EdgeRange": return EdgeRange([Edge.wrap(e) for e in self._ref.getEdges()]) # noinspection PyShadowingBuiltins
[docs] def getVertexFromExternalId(self, id: int) -> "Vertex": return Vertex.wrap(self._ref.getVertexFromExternalId(id))
@property def minExternalId(self) -> int: return int(self._ref.getMinExternalId()) @property def maxExternalId(self) -> int: return int(self._ref.getMaxExternalId())
[docs] def removeHydrogens(self) -> "Graph": return Graph.wrap(self._ref.removeHydrogens())
@overload def print(self) -> None: ... @overload def print(self, first: GraphPrinter) -> None: ... @overload def print(self, first: GraphPrinter, second: Optional[GraphPrinter]) -> None: ...
[docs] def print(self, first: Optional[GraphPrinter] = None, second: Optional[GraphPrinter] = None): summaryRaw(f"""<div class="card mb-4" style="max-width: 400px; margin: auto"> <div class="d-flex m-2"> <h5 class="card-title flex-grow-1 mb-0">{self.name}</h5> </div> {self._ref.getSVG(first._ref if first is not None else None)} </div>""")
[docs] def isomorphism(self, codomain: "Graph") -> int: return 1 if self.canonKey == codomain.canonKey else 0
[docs] def clone(self) -> "Graph": return Graph.wrap(self._ref.clone())
# noinspection PyPep8Naming
[docs] class Vertex(TSObject): def __init__(self) -> None: super().__init__(_class_ref_Vertex) def __bool__(self): return not self._ref.isNull() @property def graph(self) -> "Graph": return Graph.wrap(self._ref.getGraph()) @property def id(self) -> int: return int(self._ref.getId()) @property def degree(self) -> int: return int(self._ref.getDegree()) @property def stringLabel(self) -> str: return self._ref.getStringLabel() @property def atomId(self) -> AtomId: return AtomId.wrap(self._ref.getAtomId()) @property def isotope(self) -> Isotope: return Isotope(int(self._ref.getIsotope())) @property def charge(self) -> Charge: return Charge(int(self._ref.getCharge())) @property def incidentEdges(self) -> "EdgeRange": return EdgeRange([Edge.wrap(e) for e in self._ref.getIncidentEdges()])
[docs] def isNull(self) -> bool: return self._ref.isNull()
[docs] def isElectron(self) -> bool: return self._ref.isElectron()
@property def externalId(self) -> int: return int(self._ref.getExternalId()) # noinspection PyShadowingBuiltins
[docs] def setExternalId(self, id: int): return self._ref.setExternalId(id)
# noinspection PyShadowingBuiltins
[docs] def removeExternalId(self): return self._ref.removeExternalId()
# noinspection PyPep8Naming
[docs] class Edge(TSObject): def __init__(self) -> None: super().__init__(_class_ref_Edge) def __bool__(self): return not self._ref.isNull() @property def graph(self) -> "Graph": return Graph.wrap(self._ref.getGraph()) @property def source(self) -> Vertex: return Vertex.wrap(self._ref.getSource()) @property def target(self) -> Vertex: return Vertex.wrap(self._ref.getTarget()) @property def stringLabel(self) -> str: return self._ref.getStringLabel() @property def bondType(self) -> BondType: label = self._ref.getStringLabel() if label == '-': return BondType.Single if label == ':': return BondType.Aromatic if label == '=': return BondType.Double if label == '#': return BondType.Triple return BondType.Invalid
[docs] def isNull(self) -> bool: return self._ref.isNull()
class VertexRangeIterator: def __init__(self, graph: Graph) -> None: self._graph = graph self._index = 0 self._size = graph.numVertices def __iter__(self): return self def __next__(self): if self._index < self._size: self._index += 1 return Vertex.wrap(self._graph._ref.getVertexFromId(self._index - 1)) raise StopIteration
[docs] class VertexRange: def __init__(self, graph: Graph) -> None: self._graph = graph def __iter__(self): return VertexRangeIterator(self._graph) def __getitem__(self, i: int) -> Vertex: return Vertex.wrap(self._graph._ref.getVertexFromId(i))
class EdgeRangeIterator: def __init__(self, edges: List[Edge]) -> None: self._edges = edges self._index = 0 def __iter__(self): return self def __next__(self): if self._index < len(self._edges): self._index += 1 return self._edges[self._index - 1] raise StopIteration
[docs] class EdgeRange: def __init__(self, edges: List[Edge]) -> None: self._edges = edges def __iter__(self): return EdgeRangeIterator(self._edges)
VertexFilter = Callable[[Vertex], bool]