Source code for synkit.Graph.Wildcard.radwc
import re
from rdkit import Chem
from rdkit.Chem import SanitizeFlags
from typing import Tuple, Optional
[docs]
class RadWC:
"""
Static utility for appending wildcard dummy atoms ([*]) with atom-map
indices to all radical centers **in the product block** of a reaction SMILES.
- Reactant and agent blocks are not modified.
- Only atoms in the product with unpaired electrons are considered.
- Each product radical gets a new [*:N] with unique map number (auto or user-supplied).
Example
-------
>>> rxn = '[CH2:1][OH:2]>>[CH2:1][O:2]'
>>> RadWC.transform(rxn)
'[CH2:1][OH:2]>>[CH2:1][O:2]'
>>> rxn2 = '[CH2:1][OH:2]>>[CH:1].[OH:2]'
>>> RadWC.transform(rxn2)
'[CH2:1][OH:2]>>[CH:1]([*:3]).[OH:2]'
"""
[docs]
@staticmethod
def transform(rxn_smiles: str, start_map: Optional[int] = None) -> str:
"""
Add [*] wildcards (with atom-map index) to every radical in the
product block of the input reaction SMILES.
:param rxn_smiles: Reaction SMILES, 2 or 3 blocks (R>>P or R>A>P).
:type rxn_smiles: str
:param start_map: Optional; first atom-map index for wildcards.
:type start_map: int or None
:returns: Modified reaction SMILES with product wildcards.
:rtype: str
:raises ValueError: On parse error or invalid input.
Example
-------
>>> RadWC.transform('[CH2:1][OH:2]>>[CH:1].[OH:2]')
'[CH2:1][OH:2]>>[CH:1]([*:3]).[OH:2]'
"""
react_blk, agents_blk, prod_blk = RadWC._split_reaction(rxn_smiles)
# Determine atom-map to use for wildcards
existing = [int(n) for n in re.findall(r":(\d+)", rxn_smiles)]
next_map = (
start_map if start_map is not None else (max(existing, default=0) + 1)
)
prod_frags = prod_blk.split(".") if prod_blk else []
new_prod_frags = []
keep_ops = SanitizeFlags.SANITIZE_ALL & ~SanitizeFlags.SANITIZE_ADJUSTHS
for smi in prod_frags:
if not smi:
continue
mol = Chem.MolFromSmiles(smi, sanitize=False)
if mol is None:
raise ValueError(f"Cannot parse product SMILES fragment: {smi}")
Chem.SanitizeMol(mol, sanitizeOps=keep_ops)
rw = Chem.RWMol(mol)
atoms = list(rw.GetAtoms())
changed = False
for atom in atoms:
rad = atom.GetNumRadicalElectrons()
if rad > 0:
for _ in range(rad):
dummy = Chem.Atom(0)
dummy.SetAtomMapNum(next_map)
dummy.SetNoImplicit(True)
rw.AddAtom(dummy)
rw.AddBond(
atom.GetIdx(), rw.GetNumAtoms() - 1, Chem.BondType.SINGLE
)
next_map += 1
changed = True
if changed:
Chem.SanitizeMol(rw.GetMol(), sanitizeOps=keep_ops)
new_prod_frags.append(
Chem.MolToSmiles(rw.GetMol(), isomericSmiles=True, allHsExplicit=True)
)
prod_str = ".".join(new_prod_frags)
if agents_blk is None:
return f"{react_blk}>>{prod_str}"
return f"{react_blk}>{agents_blk}>{prod_str}"
@staticmethod
def _split_reaction(rxn: str) -> Tuple[str, Optional[str], str]:
"""
Split a reaction SMILES into (reactant, agent or None, product).
:param rxn: Reaction SMILES string.
:type rxn: str
:returns: (reactant, agent, product) tuple (agent may be None).
:rtype: Tuple[str, Optional[str], str]
:raises ValueError: If the SMILES does not contain 2 or 3 '>'s.
"""
parts = rxn.split(">")
if len(parts) == 2:
return parts[0], None, parts[1]
if len(parts) == 3:
return parts[0], parts[1], parts[2]
raise ValueError("Reaction SMILES must contain 2 or 3 '>' symbols")
[docs]
@staticmethod
def describe():
"""
Print a description and minimal example.
"""
print(RadWC.__doc__)
def __repr__(self):
return "<RadWC: static radical-wildcard utility for product block only>"