Source code for linchemin.cgu.graph_transformations.data_model_converters

from abc import ABC, abstractmethod
from typing import Tuple, Type, Union

import linchemin.cgu.graph_transformations.exceptions as exceptions
import linchemin.cheminfo.functions as cif
from linchemin.cgu.convert import converter
from linchemin.cgu.graph_transformations.supporting_functions import build_iron_edge
from linchemin.cgu.iron import Iron, Node
from linchemin.cgu.syngraph import (
    BipartiteSynGraph,
    MonopartiteMolSynGraph,
    MonopartiteReacSynGraph,
)
from linchemin.cheminfo.models import ChemicalEquation, Molecule
from linchemin.utilities import console_logger

logger = console_logger(__name__)


[docs] class DataModelConverter(ABC): """ Abstract representation of converters handling graph data models. """
[docs] @abstractmethod def iron_to_syngraph( self, iron_graph: Iron ) -> Union[ MonopartiteReacSynGraph, MonopartiteMolSynGraph, BipartiteSynGraph, None ]: """To translate an Iron graph into a SynGraph object of the correct type""" pass
[docs] @abstractmethod def syngraph_to_iron( self, syngraph: Union[ MonopartiteReacSynGraph, MonopartiteMolSynGraph, BipartiteSynGraph, None ], ) -> Union[Iron, None]: """To translate a SynGraph object into an Iron graph""" pass
@abstractmethod def convert_syngraph( self, syngraph: Union[ MonopartiteReacSynGraph, MonopartiteMolSynGraph, BipartiteSynGraph ], ) -> Union[MonopartiteReacSynGraph, MonopartiteMolSynGraph, BipartiteSynGraph]: """To convert between SynGraph types""" pass def get_node_info( self, node: Union[ChemicalEquation, Molecule], iron: Iron ) -> Tuple[int, Node]: """To create or identify a specif node in an Iron instance""" # if the node is not yet in the Iron instance, it is created and added to Iron if node not in [n.properties["node_type"] for n in iron.nodes.values()]: id_n = iron.i_node_number() id_n1, node1 = self.build_iron_node(node, id_n) iron.add_node(str(id_n), node1) else: # if the node is already included in Iron, the relative information is retrieved id_n1, node1 = next( (i, n) for i, n in iron.nodes.items() if n.properties["node_type"] == node ) return id_n1, node1 @staticmethod def build_iron_node( node: Union[ChemicalEquation, Molecule], id_n: int ) -> Tuple[int, Node]: """To build an Iron node instance from a ChemicalEquation""" # the unmapped smiles is built so that the route is suitable to be correctly displayed in a png file if type(node) == ChemicalEquation: unmapped_smiles = cif.rdrxn_to_string( node.rdrxn, out_fmt="smiles", use_atom_mapping=False ) else: unmapped_smiles = node.smiles prop = { "node_unmapped_smiles": unmapped_smiles, "node_smiles": node.smiles, "node_type": node, } return id_n, Node(iid=str(id_n), properties=prop, labels=[])
[docs] class DataModelCatalog: """Class to store the available GraphFormatTranslators""" _registered_data_models = {} @classmethod def register_datamodel(cls, name: str, info: str): """ Decorator for registering a new data model translator. Parameters: ------------ name: str The name of the data model to be used as a key in the registry info: str A brief description of the translator Returns: --------- function: The decorator function. """ def decorator(converter_class: Type[DataModelConverter]): cls._registered_data_models[name.lower()] = { "class": converter_class, "info": info, } return converter_class return decorator @classmethod def get_data_model(cls, name: str) -> DataModelConverter: """ To get an instance of the specified DataModelConverter. Parameters: ------------ name: str The name of the DataModelConverter Returns: --------- DataModelConverter: An instance of the specified DataModelConverter Raises: ------- UnavailableDataModel: If the specified data model is not registered. """ converter = cls._registered_data_models.get(name.lower()) if converter is None: logger.error(f"Data model '{name}' not found") raise exceptions.UnavailableDataModel return converter["class"]() @classmethod def list_data_models(cls): """ To list the names of all available data models. Returns: --------- formats: dict The names and information of the available data models. """ return cls._registered_data_models
# Data model factory concrete implementations @DataModelCatalog.register_datamodel( "monopartite_reactions", "A graph with only reaction nodes" ) class MonopartiteReactionsGenerator(DataModelConverter): """DataModelConverter subclass to handle translations into and from MonopartiteReacSynGraph instances""" def iron_to_syngraph( self, iron_graph: Iron ) -> Union[MonopartiteReacSynGraph, None]: """Translates an Iron instance into a MonopartiteReacSynGraph instance""" try: if iron_graph is None: raise exceptions.EmptyRoute return MonopartiteReacSynGraph(iron_graph) except exceptions.EmptyRoute: logger.warning( "While converting from Iron to monopartite-reactions SynGraph object an empty route was found: " '"None" returned' ) return None def syngraph_to_iron(self, syngraph: MonopartiteReacSynGraph) -> Union[Iron, None]: """Translates a MonopartiteReacSynGraph instance into an Iron instance""" try: if syngraph is None: raise exceptions.EmptyRoute iron = Iron() id_e = 0 for parent, children in syngraph.graph.items(): id_n1, node1 = self.get_node_info(parent, iron) for c in children: id_n2, node2 = self.get_node_info(c, iron) e = build_iron_edge(str(id_n1), str(id_n2), str(id_e)) iron.add_edge(str(id_e), e) id_e += 1 if syngraph.name is None: iron.name = syngraph.uid else: iron.name = syngraph.name return iron except exceptions.EmptyRoute: logger.warning( 'While converting from a monopartite-reactions SynGraph to Iron an empty route was found: "None" ' "returned" ) return None def convert_syngraph( self, syngraph: Union[ MonopartiteReacSynGraph, MonopartiteMolSynGraph, BipartiteSynGraph ], ) -> MonopartiteReacSynGraph: return converter(syngraph, "monopartite_reactions") @DataModelCatalog.register_datamodel( "bipartite", "A graph with reaction and molecule nodes" ) class BipartiteGenerator(DataModelConverter): """DataModelConverter subclass to handle translations into and from BipartiteSynGraph instances""" def iron_to_syngraph(self, iron_graph: Iron) -> Union[BipartiteSynGraph, None]: """Translates an Iron instance into a BipartiteSynGraph instance""" try: if iron_graph is None: raise exceptions.EmptyRoute return BipartiteSynGraph(iron_graph) except exceptions.EmptyRoute: logger.warning( 'While converting from Iron to bipartite SynGraph object an empty route was found: "None" returned' ) return None def syngraph_to_iron(self, syngraph: BipartiteSynGraph) -> Union[Iron, None]: """Translates a BipartiteSynGraph instance into an Iron instance""" try: if syngraph is None: raise exceptions.EmptyRoute iron = Iron() id_e = 0 for parent, children in syngraph.graph.items(): id_n1, node1 = self.get_node_info(parent, iron) for c in children: id_n2, node2 = self.get_node_info(c, iron) e = build_iron_edge(str(id_n1), str(id_n2), str(id_e)) iron.add_edge(str(id_e), e) id_e += 1 if syngraph.name is None: iron.name = syngraph.uid else: iron.name = syngraph.name return iron except exceptions.EmptyRoute: logger.warning( 'While converting from a bipartite SynGraph to Iron an empty route was found: "None" returned' ) return None def convert_syngraph( self, syngraph: Union[ MonopartiteReacSynGraph, MonopartiteMolSynGraph, BipartiteSynGraph ], ) -> BipartiteSynGraph: return converter(syngraph, "bipartite") @DataModelCatalog.register_datamodel( "monopartite_molecules", "A graph with only molecule nodes" ) class MonopartiteMoleculesGenerator(DataModelConverter): """DataModelConverter subclass to handle translations into and from MonopartiteMolSynGraph instances""" def iron_to_syngraph(self, iron_graph: Iron) -> Union[MonopartiteMolSynGraph, None]: """Translates an Iron instance into a MonopartiteMolSynGraph instance""" try: if iron_graph is None: raise exceptions.EmptyRoute return MonopartiteMolSynGraph(iron_graph) except exceptions.EmptyRoute: logger.warning( "While converting from Iron to a monopartite-molecules SynGraph object an empty route was found: " '"None" returned' ) return None def syngraph_to_iron(self, syngraph: MonopartiteMolSynGraph) -> Union[Iron, None]: """Translates a MonopartiteReacSynGraph instance into an Iron instance""" try: if syngraph is None: raise exceptions.EmptyRoute iron = Iron() id_e = 0 for parent, children in syngraph.graph.items(): id_n1, node1 = self.get_node_info(parent, iron) for c in children: id_n2, node2 = self.get_node_info(c, iron) e = build_iron_edge(str(id_n1), str(id_n2), str(id_e)) iron.add_edge(str(id_e), e) id_e += 1 if syngraph.name is None: iron.name = syngraph.uid else: iron.name = syngraph.name return iron except exceptions.EmptyRoute: logger.warning( 'While converting from a monopartite-molecules SynGraph to Iron an empty route was found: "None" ' "returned" ) return None def convert_syngraph( self, syngraph: Union[ MonopartiteReacSynGraph, MonopartiteMolSynGraph, BipartiteSynGraph ], ) -> MonopartiteMolSynGraph: return converter(syngraph, "monopartite_molecules")