Source code for molfoundry.dgvertexmapper

from typing import Iterator, Optional

import molfoundry.dg as dg
from .core import TSObject, coerce_type
from .graph import VertexFilter
from .vertexmap import VertexMap

_class_ref = TSObject.get_class_ref("DGVertexMapper")


[docs] class DGVertexMapper(TSObject): """Enumerates the atom-atom maps of a hyperedge. A derivation records one map per match, but relabeling a map by an automorphism of the educts or of the products gives another valid map, so the full set is an orbit whose size is the product of two automorphism group orders — easily thousands for ordinary molecules. Maps are therefore produced one at a time as they are iterated, and never more than ``limit`` of them:: for m in DGVertexMapper(edge, limit=10): for entry in m: print(entry) No map is yielded twice, however it was reached. The enumeration is single-pass: a mapper that has run out stays exhausted, so build a new one to start over. When only some atoms matter, pass ``vertex_filter`` — a predicate on the :class:`~molfoundry.Vertex` atoms of the hyperedge's compounds. Only maps that differ on the accepted atoms are then enumerated, and every automorphism that fixes all of them is skipped, so the orbit shrinks to the accepted atoms' own symmetry. Tracing carbon through a hydrogen-rich network this way avoids the hydrogen permutations entirely — an ethanol whose 12-map orbit is all hydrogen relabelings collapses to a single map:: for m in DGVertexMapper(edge, limit=1000, vertex_filter=lambda v: v.atomId.symbol == 'C'): ... The maps produced are still complete (every atom is present); the atoms the filter rejects are simply frozen at one representative labeling rather than permuted. ``vertex_filter`` is evaluated at most once per atom. """ def __init__(self, edge: "dg.DG.HyperEdge", limit: int = 1, vertex_filter: Optional[VertexFilter] = None) -> None: keep = None if vertex_filter is not None: # The TS side addresses an atom by (graphIndex, nodeIndex) in the DG. # Resolve that to the actual Vertex and defer to the user's predicate. # graphIndex is the atom's species index in dg.vertices and nodeIndex # its canonical vertex id, matching what VertexMap reports. The species # graphs are cached so only the vertex lookup repeats. vertices = edge.dg.vertices graph_cache = {} def keep(graph_index: int, node_index: int) -> bool: # The engine hands these across as JavaScript numbers, which arrive # as Python floats; coerce before indexing. graph_index = int(graph_index) node_index = int(node_index) graph = graph_cache.get(graph_index) if graph is None: graph = vertices[graph_index].graph graph_cache[graph_index] = graph return bool(vertex_filter(graph.vertices[node_index])) super().__init__(_class_ref, edge._ref, limit, keep) @property def limit(self) -> int: return int(self._ref.getLimit()) @property def hyperEdge(self) -> "dg.DG.HyperEdge": return dg.DG.HyperEdge.wrap(self._ref.getHyperEdge()) def __iter__(self) -> Iterator[VertexMap]: while True: nxt = coerce_type(self._ref.next()) if nxt is None: return yield VertexMap.wrap(nxt) def __str__(self) -> str: return self._ref.toString() def __repr__(self) -> str: return self._ref.toString()