Source code for polymerist.rdutils.bonding.permutation

'''Tool for swapping bonds within and between RDKit Mols'''

__author__ = 'Timotej Bernat'
__email__ = 'timotej.bernat@colorado.edu'

import logging
LOGGER = logging.getLogger(__name__)

from typing import Optional
from rdkit.Chem.rdchem import RWMol

from collections import Counter
from IPython.display import display # for Jupyter display support

from . import portlib
from .formation import increase_bond_order
from .dissolution import decrease_bond_order
from ..chemlabel import atom_idxs_by_map_numbers

from ...genutils.sequences.seqops import int_complement
from ...genutils.decorators.functional import optional_in_place


def _is_valid_bond_derangement(bond_derangement : dict[int, tuple[int, int]]) -> bool:
    '''Determine whether an interatomic bond remapping describes a valid derangement'''
    # 1) check that each swap maps to a new element (i.e. no identity swaps)
    for begin_map_num, (curr_end_map_num, targ_end_map_num) in bond_derangement.items():
        if curr_end_map_num == targ_end_map_num:
            LOGGER.warning(f'Swap defined for initial index {begin_map_num} maps back to current partner ({curr_end_map_num} -> {targ_end_map_num})')
            return False
        
    # 2) check bijection (i.e. terminal atom remappings form a closed multiset)
    curr_end_counts, targ_end_counts = [Counter(i) for i in zip(*bond_derangement.values())] #  multisets are permissible for when multiple current/target bonds connect to the same atom 
    if curr_end_counts != targ_end_counts:
        LOGGER.warning('Bond derangement does not define a 1-1 correspondence between elements in the multiset')
        return False

    return True # only return if all above checks pass

[docs] @optional_in_place def swap_bonds( rwmol : RWMol, bond_derangement : dict[int, tuple[int, int]], show_steps : bool=False, bond_breakage_marker : str='--x->', bond_formation_marker : str='---->', ) -> Optional[RWMol]: ''' Takes a modifiable Mol and a bond derangement dict and performs the requested bond swaps Derangement dict should have th following form: keys : int = corresponds to the beginning atom of a bond values : tuple[int, int] = corresponds to the current end atom map number and target end atom map number (in that order) Modifiable Mol can contain multiple disconnected molecular components ''' # TODO : check for complete atom map num assignment if not _is_valid_bond_derangement(bond_derangement): raise ValueError('Invalid interatomic bond derangement provided') # determine non-interfering port flavors for new bonds (preserves parity between permutation sets) available_port_flavors = int_complement( # ensures newly-created temporary ports don't clash with any existing ones set(atom.GetIsotope() for atom in rwmol.GetAtoms()), bounded=False, ) flavor_pair = (next(available_port_flavors), next(available_port_flavors)) # grab first two available flavors portlib.Port.bondable_flavors.insert(flavor_pair) # temporarily register pair as bondable # break current bonds for begin_map_num, (curr_end_map_num, _) in bond_derangement.items(): decrease_bond_order( rwmol, *atom_idxs_by_map_numbers(rwmol, begin_map_num, curr_end_map_num), new_flavor_pair=flavor_pair, in_place=True # must be done in-place to allow optional_in_place decoration ) if show_steps: LOGGER.info(f'{begin_map_num} {bond_breakage_marker} {curr_end_map_num}') display(rwmol) # form new bonds - must be done AFTER breakage to ensure all necessary ports exist for begin_map_num, (_, targ_end_map_num) in bond_derangement.items(): increase_bond_order( rwmol, *atom_idxs_by_map_numbers(rwmol, begin_map_num, targ_end_map_num), flavor_pair=flavor_pair, in_place=True # must be done in-place to allow optional_in_place decoration ) if show_steps: LOGGER.info(f'{begin_map_num} {bond_formation_marker} {targ_end_map_num}') display(rwmol) # deregister bondable pair portlib.Port.bondable_flavors.pop(flavor_pair)