Source code for stk._internal.topology_graphs.small.ncore

import typing
from collections import abc
from dataclasses import dataclass

import numpy as np

from stk._internal.building_block import BuildingBlock
from stk._internal.optimizers.null import NullOptimizer
from stk._internal.optimizers.optimizer import Optimizer
from stk._internal.reaction_factories.generic_reaction_factory import (
    GenericReactionFactory,
)
from stk._internal.reaction_factories.reaction_factory import (
    ReactionFactory,
)
from stk._internal.topology_graphs.edge import Edge
from stk._internal.topology_graphs.topology_graph.topology_graph import (
    TopologyGraph,
)
from stk._internal.topology_graphs.vertex import Vertex

from .vertices import CoreVertex, SubstituentVertex


[docs] class NCore(TopologyGraph): """ Represents a core reacting with N arms topology graph. The core building block, with N functional groups, defines the behaviour of this topology graph. The arm building blocks, which must have only one functional group, are oriented in a circle around the core based on N and the repeating units. The core is oriented into the XY plane. Examples: *Construction* The number of functional groups in the core building block define the topology graph .. testcode:: construction import stk core1 = stk.BuildingBlock( smiles="C(Br)1C(Br)C1Br", functional_groups=stk.BromoFactory(), ) arm = stk.BuildingBlock( smiles="BrC", functional_groups=stk.BromoFactory(), ) ncore = stk.ConstructedMolecule(stk.small.NCore( core_building_block=core1, arm_building_blocks=arm, repeating_unit="A", # 'AAA' would work too. )) .. moldoc:: import moldoc.molecule as molecule import stk arm = stk.BuildingBlock( smiles="BrC", functional_groups=stk.BromoFactory(), ) core1 = stk.BuildingBlock( smiles="C(Br)1C(Br)C1Br", functional_groups=stk.BromoFactory(), ) ncore = stk.ConstructedMolecule(stk.small.NCore( core_building_block=core1, arm_building_blocks=arm, repeating_unit="A", )) moldoc_display_molecule = molecule.Molecule( atoms=( molecule.Atom( atomic_number=atom.get_atomic_number(), position=position, ) for atom, position in zip( ncore.get_atoms(), ncore.get_position_matrix(), ) ), bonds=( molecule.Bond( atom1_id=bond.get_atom1().get_id(), atom2_id=bond.get_atom2().get_id(), order=bond.get_order(), ) for bond in ncore.get_bonds() ), ) .. testcode:: construction import stk core2 = stk.BuildingBlock( smiles="C(Br)1C(Br)C(Br)C(Br)C(Br)C(Br)C1Br", functional_groups=stk.BromoFactory(), ) arm = stk.BuildingBlock( smiles="BrC", functional_groups=stk.BromoFactory(), ) ncore = stk.ConstructedMolecule(stk.small.NCore( core_building_block=core2, arm_building_blocks=arm, repeating_unit="A", # 'AAAAAAA' would work too. )) .. moldoc:: import moldoc.molecule as molecule import stk arm = stk.BuildingBlock( smiles="BrC", functional_groups=stk.BromoFactory(), ) core2 = stk.BuildingBlock( smiles="C(Br)1C(Br)C(Br)C(Br)C(Br)C(Br)C1Br", functional_groups=stk.BromoFactory(), ) ncore = stk.ConstructedMolecule(stk.small.NCore( core_building_block=core2, arm_building_blocks=arm, repeating_unit="A", )) moldoc_display_molecule = molecule.Molecule( atoms=( molecule.Atom( atomic_number=atom.get_atomic_number(), position=position, ) for atom, position in zip( ncore.get_atoms(), ncore.get_position_matrix(), ) ), bonds=( molecule.Bond( atom1_id=bond.get_atom1().get_id(), atom2_id=bond.get_atom2().get_id(), order=bond.get_order(), ) for bond in ncore.get_bonds() ), ) *Suggested Optimization* For :class:`.NCore` topologies, it is recommend to use the :class:`.MCHammer` optimizer. .. testcode:: suggested-optimization import stk arm = stk.BuildingBlock( smiles="BrC", functional_groups=stk.BromoFactory(), ) core2 = stk.BuildingBlock( smiles="C(Br)1C(Br)C(Br)C(Br)C(Br)C(Br)C1Br", functional_groups=stk.BromoFactory(), ) ncore = stk.ConstructedMolecule( stk.small.NCore( core_building_block=core2, arm_building_blocks=arm, repeating_unit="A", optimizer=stk.MCHammer(), ) ) .. moldoc:: import moldoc.molecule as molecule import stk arm = stk.BuildingBlock( smiles="BrC", functional_groups=stk.BromoFactory(), ) core2 = stk.BuildingBlock( smiles="C(Br)1C(Br)C(Br)C(Br)C(Br)C(Br)C1Br", functional_groups=stk.BromoFactory(), ) ncore = stk.ConstructedMolecule( stk.small.NCore( core_building_block=core2, arm_building_blocks=arm, repeating_unit="A", optimizer=stk.MCHammer(), ) ) moldoc_display_molecule = molecule.Molecule( atoms=( molecule.Atom( atomic_number=atom.get_atomic_number(), position=position, ) for atom, position in zip( ncore.get_atoms(), ncore.get_position_matrix(), ) ), bonds=( molecule.Bond( atom1_id=bond.get_atom1().get_id(), atom2_id=bond.get_atom2().get_id(), order=bond.get_order(), ) for bond in ncore.get_bonds() ), ) *Construction with Distinct Arms* Any combination of arm building blocks can be provided. .. testcode:: construction-with-distinct-arms1 import stk core = stk.BuildingBlock( smiles="C(Br)1C(Br)C(Br)C(Br)C(Br)C(Br)C1Br", functional_groups=stk.BromoFactory(), ) arm1 = stk.BuildingBlock( smiles="BrC", functional_groups=stk.BromoFactory(), ) arm2 = stk.BuildingBlock( smiles="BrCN", functional_groups=stk.BromoFactory(), ) ncore = stk.ConstructedMolecule( stk.small.NCore( core_building_block=core, arm_building_blocks=[arm1, arm2], repeating_unit="ABABABA", ) ) .. moldoc:: import moldoc.molecule as molecule import stk core = stk.BuildingBlock( smiles="C(Br)1C(Br)C(Br)C(Br)C(Br)C1Br", functional_groups=stk.BromoFactory(), ) arm1 = stk.BuildingBlock( smiles="BrC", functional_groups=stk.BromoFactory(), ) arm2 = stk.BuildingBlock( smiles="BrCN", functional_groups=stk.BromoFactory(), ) ncore = stk.ConstructedMolecule( stk.small.NCore( core_building_block=core, arm_building_blocks=[arm1, arm2], repeating_unit="ABABAB", ) ) moldoc_display_molecule = molecule.Molecule( atoms=( molecule.Atom( atomic_number=atom.get_atomic_number(), position=position, ) for atom, position in zip( ncore.get_atoms(), ncore.get_position_matrix(), ) ), bonds=( molecule.Bond( atom1_id=bond.get_atom1().get_id(), atom2_id=bond.get_atom2().get_id(), order=bond.get_order(), ) for bond in ncore.get_bonds() ), ) Just be cautious of the repeating unit term, which must match the available functional groups in the core. Units that are a subset of available functional groups will be repeated. .. testcode:: construction-with-distinct-arms2 import stk core = stk.BuildingBlock( smiles="C(Br)1C(Br)C(Br)C(Br)C(Br)C1Br", functional_groups=stk.BromoFactory(), ) arm1 = stk.BuildingBlock( smiles="BrC", functional_groups=stk.BromoFactory(), ) arm2 = stk.BuildingBlock( smiles="BrCN", functional_groups=stk.BromoFactory(), ) ncore = stk.ConstructedMolecule( stk.small.NCore( core_building_block=core, arm_building_blocks=[arm1, arm2], repeating_unit="ABA", # Same as 'ABAABA'. ) ) .. moldoc:: import moldoc.molecule as molecule import stk core = stk.BuildingBlock( smiles="C(Br)1C(Br)C(Br)C(Br)C(Br)C1Br", functional_groups=stk.BromoFactory(), ) arm1 = stk.BuildingBlock( smiles="BrC", functional_groups=stk.BromoFactory(), ) arm2 = stk.BuildingBlock( smiles="BrCN", functional_groups=stk.BromoFactory(), ) ncore = stk.ConstructedMolecule( stk.small.NCore( core_building_block=core, arm_building_blocks=[arm1, arm2], repeating_unit="ABA", ) ) moldoc_display_molecule = molecule.Molecule( atoms=( molecule.Atom( atomic_number=atom.get_atomic_number(), position=position, ) for atom, position in zip( ncore.get_atoms(), ncore.get_position_matrix(), ) ), bonds=( molecule.Bond( atom1_id=bond.get_atom1().get_id(), atom2_id=bond.get_atom2().get_id(), order=bond.get_order(), ) for bond in ncore.get_bonds() ), ) """ def __init__( self, core_building_block: BuildingBlock, arm_building_blocks: BuildingBlock | abc.Iterable[BuildingBlock], repeating_unit: str | abc.Iterable[int], reaction_factory: ReactionFactory = GenericReactionFactory(), num_processes: int = 1, optimizer: Optimizer = NullOptimizer(), scale_multiplier: float = 1.0, ) -> None: """ Parameters: core_building_block: The central building block. arm_building_blocks (BuildingBlock | list[BuildingBlock]): The building blocks to react with the core. repeating_unit (str | list[int]): A string specifying the repeating unit of the arms. For example, ``'AB'`` or ``'ABB'``. The first building block passed to `arm_building_blocks` is ``'A'`` and so on. The repeating unit must fit evenly onto the `core_building_block`, defined by the number of functional groups the core has. The repeating unit can also be specified by the indices of `building_blocks`, for example ``'ABB'`` can be written as ``[0, 1, 1]``. reaction_factory: The factory to use for creating reactions between functional groups of building blocks. num_processes: The number of parallel processes to create during :meth:`construct`. optimizer: Used to optimize the structure of the constructed molecule. scale_multiplier: Scales the positions of the vertices. Raises: :class:`ValueError` If the length of `orientations` is not equal in length to `repeating_unit` or to the total number of vertices. """ if isinstance(arm_building_blocks, BuildingBlock): arm_building_blocks = (arm_building_blocks,) self._repr = ( f"NCore({core_building_block!r}, {arm_building_blocks!r}, " f"{repeating_unit!r})" ) if not isinstance(repeating_unit, str): repeating_unit = tuple(repeating_unit) self._num_arms = core_building_block.get_num_functional_groups() if self._num_arms % len(repeating_unit) != 0: raise ValueError( f"The repeating unit {repeating_unit} does not fit " "evenly onto the core building block with " f"{self._num_arms} functional groups." ) # Keep these for __repr__. self._repeating_unit = self._normalize_repeating_unit( repeating_unit=repeating_unit ) self._num_repeating_units = int(self._num_arms / len(repeating_unit)) vertices_and_edges = self._get_vertices_and_edges(self._num_arms) vertices = vertices_and_edges.vertices edges = vertices_and_edges.edges super().__init__( building_block_vertices=self._get_building_block_vertices( core_building_block=core_building_block, arm_building_blocks=tuple(arm_building_blocks), vertices=vertices, ), edges=edges, reaction_factory=reaction_factory, construction_stages=(), optimizer=optimizer, num_processes=num_processes, scale_multiplier=scale_multiplier, ) @staticmethod def _get_vertices_and_edges(num_arms: int) -> "_VerticesAndEdges": """ Get the vertices and edges of the topology graph. Parameters: num_arms: The number of arms to add to the core. Returns: The vertices and edges of the topology graph. """ if num_arms == 1: arm_positions = [np.array((1, 0, 0))] elif num_arms == 2: arm_positions = [np.array((1, 0, 0)), np.array((-1, 0, 0))] else: thetas = np.linspace(0, 2 * np.pi, num_arms + 1)[:-1] x_points = np.cos(thetas) y_points = np.sin(thetas) arm_positions = [ np.array((x, y, 0)) for x, y in zip(x_points, y_points) ] vertices: list[Vertex] = [CoreVertex(0, (0, 0, 0))] edges: list[Edge] = [] for i, pos in enumerate(arm_positions): vertices.append(SubstituentVertex(i + 1, pos)) edges.append(Edge(len(edges), vertices[0], vertices[-1])) return _VerticesAndEdges( vertices=tuple(vertices), edges=tuple(edges), )
[docs] def clone(self) -> typing.Self: clone = self._clone() clone._repr = self._repr clone._repeating_unit = self._repeating_unit clone._num_repeating_units = self._num_repeating_units return clone
@staticmethod def _normalize_repeating_unit( repeating_unit: str | tuple[int, ...], ) -> tuple[int, ...]: if isinstance(repeating_unit, tuple): return repeating_unit base = ord("A") return tuple(ord(letter) - base for letter in repeating_unit) def _get_building_block_vertices( self, core_building_block: BuildingBlock, arm_building_blocks: tuple[BuildingBlock, ...], vertices: tuple[Vertex, ...], ) -> dict[BuildingBlock, abc.Sequence[Vertex]]: building_block_vertices: dict[BuildingBlock, list[Vertex]] = {} building_block_vertices[core_building_block] = [vertices[0]] bb_order = self._repeating_unit * self._num_repeating_units for bb_index, vertex in zip(bb_order, vertices[1:]): bb = arm_building_blocks[bb_index] building_block_vertices[bb] = building_block_vertices.get(bb, []) building_block_vertices[bb].append(vertex) return { i: tuple(building_block_vertices[i]) for i in building_block_vertices } @staticmethod def _get_scale( building_block_vertices: dict[BuildingBlock, abc.Sequence[Vertex]], scale_multiplier: float, ) -> float: return scale_multiplier * max( bb.get_maximum_diameter() for bb in building_block_vertices )
[docs] def with_building_blocks( self, building_block_map: dict[BuildingBlock, BuildingBlock], ) -> typing.Self: return self.clone()._with_building_blocks(building_block_map)
def __repr__(self) -> str: return self._repr
@dataclass(frozen=True) class _VerticesAndEdges: vertices: tuple[Vertex, ...] edges: tuple[Edge, ...]