2025, Oct 21 06:31

RDKit में ReplaceSubstructs से SMILES जोड़ते समय क्रम बचाने के तरीके

RDKit ReplaceSubstructs() से SMILES जोड़ते समय परमाणु क्रम उलट सकता है. जानें क्यों ऐसा होता है और रेगेक्स आधारित स्ट्रिंग-प्रतिस्थापन से मूल लेआउट/मैपिंग कैसे सुरक्षित रखें.

जब आप RDKit की ReplaceSubstructs() के साथ दो SMILES को जोड़ने की कोशिश करते हैं, तो अक्सर एक अनपेक्षित दुष्प्रभाव सामने आता है: प्रतिस्थापन “काम” तो करता है, लेकिन परमाणुओं का क्रम और मैपिंग उलट-पलट लगती है। अगर आपकी आगे की विज़ुअलाइज़ेशन सटीक टेक्स्ट लेआउट या परमाणु सूचकांकों पर निर्भर करती है, तो यह परेशानी बन जाती है। नीचे देखें कि वास्तव में होता क्या है और डिस्प्ले के लिए मूल समूह-लेआउट को जस का तस रखने का व्यावहारिक तरीका क्या है।

समस्या की रूपरेखा

उद्देश्य है ReplaceSubstructs() के जरिए एक SMILES में मौजूद किसी कार्बन परमाणु को दूसरे SMILES फ़्रैगमेंट से बदलना। व्यवहार में, लौटाई गई सूची में आप कोई भी विकल्प चुनें, RDKit अंततः परमाणुओं का क्रम बदल देता है और इंडेक्स बिखरे हुए नजर आते हैं। यह व्यवहार दिखाने वाला RDKit का एक न्यूनतम उदाहरण नीचे है।

from rdkit import Chem
def fuse_smiles(base_smi, insert_smi, choice_idx=1):
    base_mol = Chem.MolFromSmiles(base_smi)
    frag_mol = Chem.MolFromSmiles(insert_smi)
    if base_mol is None or frag_mol is None:
        raise ValueError("One or both SMILES strings are invalid.")
    tagged_base = add_atom_map_tags(base_mol)
    result_mol = Chem.ReplaceSubstructs(
        tagged_base,
        Chem.MolFromSmarts("[CH3]"),
        frag_mol
    )[choice_idx]
    result_smi = Chem.MolToSmiles(result_mol)
    print(result_smi)
def add_atom_map_tags(m):
    n_atoms = m.GetNumAtoms()
    for aidx in range(n_atoms):
        m.GetAtomWithIdx(aidx).SetProp(
            'molAtomMapNumber', str(m.GetAtomWithIdx(aidx).GetIdx())
        )
    return m
if __name__ == "__main__":
    base = "CC(C)(C)Cl"
    insert = "CN(C)C"
    fuse_smiles(base, insert)

विकल्प इंडेक्स बदलने पर आउटपुट इस तरह आता है:

CN(C)C[C:1]([CH3:2])([CH3:3])[Cl:4] for option 0

CN(C)C[C:1]([CH3:0])([CH3:3])[Cl:4] for option 1

CN(C)C[C:1]([CH3:0])([CH3:2])[Cl:4] for option 2

लेकिन इच्छित डिस्प्ले में टाइप किए गए मूल समूह और क्रम वैसे ही बने रहने चाहिए:

CN(C)C[C:1]([CH3:2])([CH3:3])[Cl:4] for option 0

[CH3:0][C:1](CN(C)C)([CH3:3])[Cl:4] for option 1

[CH3:0][C:1]([CH3:2])(CN(C)C)[Cl:4] for option 2

मुद्दा क्या है

RDKit अपेक्षित उप-संरचनाओं को बदल रहा है। असर यह दिखता है कि मान्य या कैनोनिकल रूप को दर्शाने के लिए RDKit अंदरूनी तौर पर परमाणुओं को बदल या पुन: क्रमित कर देता है। केमइन्फ़ोर्मेटिक्स के कामों के लिए यह ठीक है, लेकिन अगर विज़ुअल आउटपुट में लिखे गए क्रम और शाखा-लेआउट को बिल्कुल उसी तरह रखना जरूरी हो—जहाँ इंडेक्स और पोज़िशन मायने रखते हैं—तो यह बाधा बन जाता है।

व्यावहारिक उपाय: रेगेक्स के साथ स्ट्रिंग-स्तर पर प्रतिस्थापन

यदि प्राथमिकता डिस्प्ले है और आपको मूल SMILES टेक्स्ट-लेआउट सुरक्षित रखना है, तो SMILES को एक स्ट्रिंग की तरह लें और लक्षित टोकन का प्रतिस्थापन करें—मूल ब्रांच क्रम को बनाए रखने के लिए RDKit पर निर्भर न रहें। विचार यह है कि [CH3:number] की सभी आवृत्तियों को खोजें और हर जगह दूसरे फ़्रैगमेंट से बदल दें, ताकि प्रत्येक संभावित प्रतिस्थापन स्थिति के लिए अलग-अलग वैरिएंट बनें—बाकी टेक्स्ट बिना छेड़े।

import re
template_str = "[CH3:0][C:1]([CH3:2])([CH3:3])[Cl:4]"
hits = re.findall(r'\[CH3:\d+\]', template_str)
print('hits:', hits)
variants = []
for token in hits:
    replaced_str = template_str.replace(token, 'CN(C)C')
    variants.append(replaced_str)
    print('variant:', replaced_str)
choice_idx = 0
print(choice_idx, variants[choice_idx])

इससे निम्नलिखित प्राप्त होता है:

hits: ['[CH3:0]', '[CH3:2]', '[CH3:3]']

variant: CN(C)C[C:1]([CH3:2])([CH3:3])[Cl:4]

variant: [CH3:0][C:1](CN(C)C)([CH3:3])[Cl:4]

variant: [CH3:0][C:1]([CH3:2])(CN(C)C)[Cl:4]

0 CN(C)C[C:1]([CH3:2])([CH3:3])[Cl:4]

इसे अपने एप्लिकेशन में दोबारा उपयोग करने के लिए, इसे एक छोटे यूटिलिटी फ़ंक्शन में रखें:

import re
def substitute_tokens(src, token_pattern, payload):
    found = re.findall(token_pattern, src)
    out = []
    for token in found:
        out.append(src.replace(token, payload))
    return out
# --- usage ---
source = "[CH3:0][C:1]([CH3:2])([CH3:3])[Cl:4]"
token_re = r'\[CH3:\d+\]'
payload = 'CN(C)C'
alts = substitute_tokens(source, token_re, payload)
for choice_idx in range(3):
    print(choice_idx, alts[choice_idx])

आउटपुट:

0 CN(C)C[C:1]([CH3:2])([CH3:3])[Cl:4]

1 [CH3:0][C:1](CN(C)C)([CH3:3])[Cl:4]

2 [CH3:0][C:1]([CH3:2])(CN(C)C)[Cl:4]

यह क्यों मायने रखता है

ऐसे डेटा प्रोडक्ट्स में जहाँ विज़ुअलाइज़ेशन के लिए SMILES टेक्स्ट सीधे लिया जाता है, सटीक क्रम और शाखाएँ बनाए रखना उतना ही अहम हो सकता है जितना रासायनिक शुद्धता। अगर आपका रेंडरर या मैपिंग-लॉजिक लिखे हुए परमाणु-स्थान या लेबल पर निर्भर है, तो आप नहीं चाहेंगे कि टूलकिट पीछे से उनका क्रम बदल दे। स्ट्रिंग-स्तर वाला तरीका आपको मूल लेआउट सुरक्षित रखते हुए अनेक प्रतिस्थापन विकल्प देने की आज़ादी देता है।

मुख्य बातें

अगर आपको रासायनिक संदर्भ समझते हुए संपादन चाहिए, तो ReplaceSubstructs() काम कर देता है—पर यह परमाणुओं का क्रम बदल सकता है। अगर डिस्प्ले के लिए मूल SMILES लेआउट चाहिए और यह नियंत्रित करना है कि कौन-सा [CH3:number] बदला जाए, तो रेगेक्स के साथ स्ट्रिंग-आधारित तरीका अपनाएँ और मनचाहे वैरिएंट बनाएँ। साधारण प्रिंट डिबगिंग से देखते रहें कि आपका कोड क्या निकाल रहा है, और सुनिश्चित करें कि स्क्रिप्ट ठीक से इंडेंट और व्यवस्थित हो ताकि आप जो आउटपुट देखते हैं, उस पर भरोसा कर सकें।

यह लेख StackOverflow पर प्रश्न (लेखक: Anh Vu) और furas के उत्तर पर आधारित है।