Source code for stk.molecular.molecules.constructed_molecule

"""
Constructed Molecule
====================

"""

from __future__ import annotations

import logging
import typing

import numpy as np
import rdkit.Chem.AllChem as rdkit

from ...utilities import OneOrMany
from ..atoms import Atom, AtomInfo
from ..bonds import Bond, BondInfo
from ..topology_graphs import TopologyGraph
from ..topology_graphs.topology_graph.construction_result import (
    ConstructionResult,
)
from .molecule import Molecule
from .utilities import get_bond_info_atom_ids, sort_bond_atoms_by_id

logger = logging.getLogger(__name__)


[docs]class ConstructedMolecule(Molecule): """ Represents constructed molecules. Examples: *Initialization* A :class:`.ConstructedMolecule` is initialized from a :class:`.TopologyGraph`, which is typically initialized from some :class:`.BuildingBlock` instances. .. testcode:: initialization import stk bb1 = stk.BuildingBlock( smiles='NCCCN', functional_groups=[stk.PrimaryAminoFactory()], ) bb2 = stk.BuildingBlock( smiles='O=CC(C=O)CC=O', functional_groups=[stk.AldehydeFactory()], ) tetrahedron = stk.cage.FourPlusSix((bb1, bb2)) cage = stk.ConstructedMolecule(tetrahedron) *Hierarchical Construction* A :class:`ConstructedMolecule` may be used to construct other :class:`ConstructedMolecule` instances, though you will probably have to convert it to a :class:`.BuildingBlock` first .. testcode:: hierarchical-construction import stk bb1 = stk.BuildingBlock( smiles='NCCCN', functional_groups=[stk.PrimaryAminoFactory()], ) bb2 = stk.BuildingBlock( smiles='O=CC(C=O)CC=O', functional_groups=[stk.AldehydeFactory()], ) tetrahedron = stk.cage.FourPlusSix((bb1, bb2)) cage = stk.ConstructedMolecule(tetrahedron) benzene = stk.host_guest.Guest( building_block=stk.BuildingBlock('c1ccccc1'), ) cage_complex = stk.host_guest.Complex( host=stk.BuildingBlock.init_from_molecule(cage), guests=benzene, ) cage_complex = stk.ConstructedMolecule(cage_complex) Obviously, the initialization of the :class:`.ConstructedMolecule` depends mostly on the specifics of the :class:`.TopologyGraph` used, and the documentation of those classes should be examined for more examples. """ _atom_infos: tuple[AtomInfo, ...] _bond_infos: tuple[BondInfo, ...] _num_building_blocks: dict[Molecule, int]
[docs] def __init__( self, topology_graph: TopologyGraph, ) -> None: """ Initialize a :class:`.ConstructedMolecule`. Parameters: topology_graph: The topology graph of the constructed molecule. """ self._init_from_construction_result( obj=self, construction_result=topology_graph.construct(), )
[docs] @classmethod def init( cls, atoms: tuple[Atom, ...], bonds: tuple[Bond, ...], position_matrix: np.ndarray, atom_infos: tuple[AtomInfo, ...], bond_infos: tuple[BondInfo, ...], num_building_blocks: dict[Molecule, int], ) -> ConstructedMolecule: """ Initialize a :class:`.ConstructedMolecule` from its components. Parameters: atoms: The atoms of the molecule. bond: The bonds of the molecule. position_matrix: A ``(n, 3)`` position matrix of the molecule. atom_infos: The atom infos of the molecule. bond_infos: The bond infos of the molecule. num_building_blocks: Maps each building block of the constructed molecule to the number of times it is present in it. Returns: The constructed molecule. """ molecule = cls.__new__(cls) Molecule.__init__(molecule, atoms, bonds, position_matrix) molecule._atom_infos = atom_infos molecule._bond_infos = bond_infos molecule._num_building_blocks = dict(num_building_blocks) return molecule
[docs] @classmethod def init_from_construction_result( cls, construction_result: ConstructionResult, ) -> ConstructedMolecule: """ Initialize a :class:`.ConstructedMolecule`. Parameters: construction_result: The result of a construction, from which the :class:`.ConstructedMolecule` should be initialized. Returns: The constructed molecule. """ return cls._init_from_construction_result( obj=cls.__new__(cls), construction_result=construction_result, )
@staticmethod def _init_from_construction_result( obj: ConstructedMolecule, construction_result: ConstructionResult, ) -> ConstructedMolecule: """ Initialize a :class:`.ConstructedMolecule`. This modifies `obj`. Parameters: obj: The constructed molecule to initialize. construction_result: The result of a construction, from which the :class:`.ConstructedMolecule` should be initialized. Returns: The `obj` instance. """ super(ConstructedMolecule, obj).__init__( atoms=construction_result.get_atoms(), bonds=construction_result.get_bonds(), position_matrix=construction_result.get_position_matrix(), ) obj._atom_infos = construction_result.get_atom_infos() obj._bond_infos = construction_result.get_bond_infos() obj._num_building_blocks = { building_block: construction_result.get_num_building_block( building_block=building_block, ) for building_block in construction_result.get_building_blocks() } return obj
[docs] def clone(self) -> ConstructedMolecule: clone = self._clone() clone._atom_infos = self._atom_infos clone._bond_infos = self._bond_infos clone._num_building_blocks = dict(self._num_building_blocks) return clone
[docs] def get_building_blocks(self) -> typing.Iterator[Molecule]: """ Yield the building blocks of the constructed molecule. Building blocks are yielded in an order based on their position in the constructed molecule. For two topologically equivalent constructed molecules, but with different building blocks, equivalently positioned building blocks will be yielded at the same time. Yields: A building block of the constructed molecule. """ yield from self._num_building_blocks
[docs] def get_num_building_block( self, building_block: Molecule, ) -> int: """ Get the number of times `building_block` is present. Parameters: building_block: The building block whose frequency in the constructed molecule is desired. Returns: The number of times `building_block` was used in the construction of the constructed molecule. """ return self._num_building_blocks[building_block]
[docs] def get_atom_infos( self, atom_ids: typing.Optional[OneOrMany[int]] = None, ) -> typing.Iterator[AtomInfo]: """ Yield data about atoms in the molecule. Parameters: atom_ids: The ids of atoms whose data is desired. If ``None``, data on all atoms will be yielded. Can be a single :class:`int`, if data on a single atom is desired. Yields: Data about an atom. """ if atom_ids is None: atom_ids = range(len(self._atoms)) elif isinstance(atom_ids, int): atom_ids = (atom_ids, ) for atom_id in atom_ids: yield self._atom_infos[atom_id]
[docs] def get_bond_infos(self) -> typing.Iterator[BondInfo]: """ Yield data about bonds in the molecule. Yields: Data about a bond. """ yield from self._bond_infos
[docs] def with_canonical_atom_ordering(self) -> ConstructedMolecule: return self.clone()._with_canonical_atom_ordering()
def _with_canonical_atom_ordering(self) -> ConstructedMolecule: # Make all building blocks canonically ordered too. building_blocks = { building_block: building_block.with_canonical_atom_ordering() for building_block in self._num_building_blocks } # Cache these mappings for later, to avoid unnecessary # re-computations of canonical ordering. canonical_map = { building_block: building_block.get_canonical_atom_ids() for building_block in self._num_building_blocks } self._num_building_blocks = { building_block: num for building_block, num in zip( building_blocks.values(), self._num_building_blocks.values(), ) } ordering = rdkit.CanonicalRankAtoms(self.to_rdkit_mol()) id_map = { new_id: atom.get_id() for new_id, atom in zip(ordering, self._atoms) } super()._with_canonical_atom_ordering() atom_map = { old_id: self._atoms[new_id] for old_id, new_id in enumerate(ordering) } old_atom_infos = self._atom_infos def get_atom_info(atom: Atom) -> AtomInfo: old_atom_info = old_atom_infos[id_map[atom.get_id()]] old_building_block = old_atom_info.get_building_block() if old_building_block is None: return AtomInfo( atom=atom, building_block_atom=None, building_block=None, building_block_id=None, ) old_building_block_atom = ( old_atom_info.get_building_block_atom() ) canonical_building_block_atom_id = canonical_map[ old_building_block ][old_building_block_atom.get_id()] canonical_building_block = building_blocks[ old_building_block ] canonical_building_block_atom, = ( canonical_building_block.get_atoms( atom_ids=canonical_building_block_atom_id, ) ) return AtomInfo( atom=atom, building_block_atom=canonical_building_block_atom, building_block=canonical_building_block, building_block_id=( old_atom_info.get_building_block_id() ), ) def get_bond_info(info: BondInfo) -> BondInfo: building_block = info.get_building_block() return BondInfo( bond=sort_bond_atoms_by_id( info.get_bond().with_atoms(atom_map) ), building_block=( building_block if building_block is None else building_blocks[building_block] ), building_block_id=info.get_building_block_id(), ) self._atom_infos = tuple(map(get_atom_info, self._atoms)) self._bond_infos = tuple(sorted( map(get_bond_info, self._bond_infos), key=get_bond_info_atom_ids, )) return self
[docs] def with_centroid( self, position: np.ndarray, atom_ids: typing.Optional[OneOrMany[int]] = None, ) -> ConstructedMolecule: return self.clone()._with_centroid(position, atom_ids)
[docs] def with_displacement( self, displacement: np.ndarray, ) -> ConstructedMolecule: return self.clone()._with_displacement(displacement)
[docs] def with_position_matrix( self, position_matrix: np.ndarray, ) -> ConstructedMolecule: return self.clone()._with_position_matrix(position_matrix)
[docs] def with_rotation_about_axis( self, angle: float, axis: np.ndarray, origin: np.ndarray, ) -> ConstructedMolecule: return self.clone()._with_rotation_about_axis( angle=angle, axis=axis, origin=origin, )
[docs] def with_rotation_between_vectors( self, start: np.ndarray, target: np.ndarray, origin: np.ndarray, ) -> ConstructedMolecule: return self.clone()._with_rotation_between_vectors( start=start, target=target, origin=origin, )
[docs] def with_rotation_to_minimize_angle( self, start: np.ndarray, target: np.ndarray, axis: np.ndarray, origin: np.ndarray, ) -> ConstructedMolecule: return self.clone()._with_rotation_to_minimize_angle( start=start, target=target, axis=axis, origin=origin, )
[docs] def with_structure_from_file( self, path: str, extension: typing.Optional[str] = None, ) -> ConstructedMolecule: return typing.cast( ConstructedMolecule, super().with_structure_from_file(path, extension), )
[docs] def write( self, path: str, atom_ids: typing.Optional[OneOrMany[int]] = None, ) -> ConstructedMolecule: return typing.cast( ConstructedMolecule, super().write(path, atom_ids), )