Source code for hippo.reaction

import mcol

from .compound import Compound
from .recipe import Recipe

import mrich


[docs] class Reaction: """ A :class:`.Reaction` is a simplified representation of a synthetic pathway to create a product :class:`.Compound`. Reactants (also :class:`.Compound` objects) as well as a reaction type are required. .. attention:: :class:`.Reaction` objects should not be created directly. Instead use :meth:`.HIPPO.register_reaction` or :meth:`.HIPPO.reactions` """ _table = "reaction" def __init__( self, db: "Database", id: int, type: str, product: int, product_yield: float, ) -> None: self._db = db self._id = id self._type = type self._product_id = product self._product = None self._product_yield = product_yield self._metadata = None ### PROPERTIES @property def id(self) -> int: """Returns the :class:`.Reaction` ID""" return self._id @property def type(self) -> str: """Returns the :class:`.Reaction` tyoe""" return self._type @property def product(self) -> "Compound": """Returns the reaction's product :class:`.Compound`""" if self._product is None: self._product = self.db.get_compound(id=self.product_id) return self._product @property def product_yield(self) -> float: """Returns the reaction's product yield (fraction)""" return self._product_yield @property def db(self) -> "Database": """Returns a pointer to the parent database""" return self._db @property def reactants(self) -> "CompoundSet": """Returns a :class:`.CompoundSet` of the reactants""" from .cset import CompoundSet return CompoundSet(self.db, indices=self.reactant_ids) @property def reaction_str(self) -> str: """Returns a string representing the reaction""" s = " + ".join([str(r) for r in self.reactants]) s = f"{s} -> {str(self.product)}" return s @property def reactant_ids(self) -> set[int]: """Returns a set of reactant ID's""" return set(v for v in self.get_reactant_ids()) @property def reactant_str_ids(self) -> str: """Return an SQL formatted tuple string of the reactant :class:`.Compound` IDs""" return str(tuple(self.reactant_ids)).replace(",)", ")") @property def product_id(self) -> int: """Returns the product :class:`.Compound` ID""" return self._product_id @property def product_smiles(self) -> str: """Product :class:`.Compound` SMILES string""" return self.product.smiles @property def reactant_smiles(self) -> list[str]: """List of reactant :class:`.Compound` SMILES strings""" return [r.smiles for r in self.reactants] @property def product_mol(self): """Product :class:`.Compound` ``rdkit.Chem.Mol`` object""" return self.product.mol @property def reactant_mols(self): """List of reactant :class:`.Compound` ``rdkit.Chem.Mol`` object""" return [r.mol for r in self.reactants] @property def price_estimate(self) -> float: """Estimate the price of this :class:`.Reaction`""" return self.db.get_reaction_price_estimate(reaction=self) @property def plain_repr(self) -> str: """Unformatted long string representation""" return f"{self}: {self.reaction_str} via {self.type}" @property def metadata(self) -> "MetaData": """Returns the compound's metadata dict""" if self._metadata is None: self._metadata = self.db.get_metadata(table="reaction", id=self.id) return self._metadata ### METHODS
[docs] def get_reactant_amount_pairs(self, compound_object: bool = True) -> list[tuple]: """Returns pairs of reactants and their amounts :param compound_object: return :class:`.Compound` object instead of ID, (Default value = True) :returns: list of tuples containing :class:`.Compound` ID/object and amount in mg """ compound_ids = self.db.select_where( query="reactant_compound, reactant_amount", table="reactant", key="reaction", value=self.id, multiple=True, ) if compound_ids: if compound_object: return [ # (self.db.get_compound(id=id), amount/self.product_yield) for id, amount in compound_ids (self.db.get_compound(id=id), amount) for id, amount in compound_ids ] else: return compound_ids else: return []
[docs] def get_reactant_ids(self) -> list[int]: """Returns list of reactants :class:`.Compound` IDs :returns: list of :class:`.Compound` IDs """ compound_ids = self.db.select_where( query="reactant_compound", table="reactant", key="reaction", value=self.id, multiple=True, ) if compound_ids: return [id for id, in compound_ids] else: return []
[docs] def get_recipes( self, amount: float = 1, # in mg debug: bool = False, pick_cheapest: bool = False, permitted_reactions: "None | ReactionSet" = None, supplier: str | None = None, ) -> "Recipe | list[Recipe]": """Get a :class:`.Recipe` describing how to make the product :param amount: Amount in ``mg``, defaults to ``1`` :param debug: Increase verbosity, (Default value = False) :param pick_cheapest: pick the cheapest :class:`.Recipe`, (Default value = False) :param permitted_reactions: Limit the reactions to consider to members of this set, (Default value = None) :param supplier: Limit to reactants from this supplier (Default value = None) :returns: :class:`.Recipe` object or list thereof """ from .recipe import Recipe return Recipe.from_reaction( self, amount=amount, debug=debug, pick_cheapest=pick_cheapest, permitted_reactions=permitted_reactions, supplier=supplier, )
[docs] def summary( self, draw: bool = True, ) -> None: """Print a summary of this reaction's information :param draw: draw the reaction compounds (Default value = True) """ print(f"id={self.id}") print(f"type={self.type}") print(f"product={self.product}") print(f"product_yield={self.product_yield}") reactants = self.get_reactant_amount_pairs() print(f"reactants={reactants}") print(f"price_estimate={self.price_estimate}") if draw: self.draw()
[docs] def draw(self) -> None: """Draw the molecules involved in this reaction""" from molparse.rdkit import draw_grid reactants = self.reactants product = self.product mols = [r.mol for r in reactants] mols.append(product.mol) labels = [f"+ {r}" if i > 0 else f"{r}" for i, r in enumerate(reactants)] labels.append(f"-> {product}") drawing = draw_grid(mols, labels=labels, highlightAtomLists=None) display(drawing)
[docs] def check_chemistry( self, debug: bool = False, ) -> bool: """Sanity check the chemistry of this reaction :param debug: increase verbosity (Default value = False) """ from .chem import check_chemistry return check_chemistry(self.type, self.reactants, self.product, debug=debug)
[docs] def check_reactant_availability( self, supplier: None | str = None, debug: bool = False, ) -> bool: """Check the availability of reactant compounds :param supplier: Limit to quotes from this supplier (Default value = None) :param debug: increase verbosity (Default value = False) """ if debug: mrich.var("reaction", self.id) mrich.var("reactants", self.reactant_ids) mrich.var("supplier", supplier) if supplier is None: triples = self.db.execute( f""" SELECT reactant_compound, SUM(quote_id), SUM(reaction_id) FROM reactant LEFT JOIN quote ON quote_compound = reactant_compound LEFT JOIN reaction ON reaction_product = reactant_compound WHERE reactant_reaction = {self.id} GROUP BY reactant_compound """ ).fetchall() else: triples = self.db.execute( f""" WITH filtered_quotes AS ( SELECT * FROM quote WHERE quote_supplier = "{supplier}" ) SELECT reactant_compound, SUM(quote_id), SUM(reaction_id) FROM reactant LEFT JOIN filtered_quotes ON quote_compound = reactant_compound LEFT JOIN reaction ON reaction_product = reactant_compound WHERE reactant_reaction = {self.id} GROUP BY reactant_compound """ ).fetchall() for reactant_compound, has_quote, has_reaction in triples: if debug: mrich.debug( f"{reactant_compound=}, {bool(has_quote)=}, {bool(has_reaction)=}" ) if has_quote: if debug: mrich.debug(f"reactant={reactant_compound} has quote") continue if has_reaction: if debug: mrich.debug(f"reactant={reactant_compound} has reaction") continue if debug: mrich.warning(f"No quote or reaction for reactant={reactant_compound}") return False return True
[docs] def get_dict( self, smiles: bool = True, mols: bool = True, ) -> dict[str]: """Returns a dictionary representing this :class:`.Reaction` :param smiles: include smiles string (Default value = True) :param mols: include ``rdkit.Chem.Mol`` (Default value = True) """ serialisable_fields = ["id", "type", "product_id", "reactant_ids"] data = {} for key in serialisable_fields: data[key] = getattr(self, key) if smiles: data["product_smiles"] = self.product_smiles data["reactant_smiles"] = self.reactant_smiles if mols: data["product_mol"] = self.product_mol data["reactant_mols"] = self.reactant_mols return data
def _delete(self) -> None: """Delete this reaction and any related reactants, routes, and components""" route_ids = self.db.select_where( query="component_route", table="component", key=f"component_ref = {self.id} AND component_type = 1", multiple=True, ) route_ids = [r for r, in route_ids] route_str_ids = str(tuple(route_ids)).replace(",)", ")") self.db.delete_where( table="component", key=f"component_route IN {route_str_ids}" ) self.db.delete_where(table="route", key=f"route_id IN {route_str_ids}") self.db.delete_where(table="reactant", key="reaction", value=self.id) self.db.delete_where(table="reaction", key="id", value=self.id) ### DUNDERS
[docs] def __str__(self) -> str: """Unformatted string representation""" return f"R{self.id}"
[docs] def __repr__(self) -> str: """ANSI Formatted string representation""" return f"{mcol.bold}{mcol.underline}{self.plain_repr}{mcol.unbold}{mcol.ununderline}"
def __rich__(self) -> str: """Rich Formatted string representation""" return f"[bold underline]{self.plain_repr}"
[docs] def __eq__( self, other: "int | Reaction", ) -> bool: """compare this reaction to a :class:`.Reaction` object or ID""" match other: case int(): return self.id == other case Reaction(): if self.type != other.type: return False if self.product != other.product: return False if self.reactant_ids != other.reactant_ids: return False return True case _: raise NotImplementedError
[docs] def __hash__(self) -> int: """Integer hash from ID""" return self.id