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]