File size: 4,817 Bytes
79c9a60
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
from __future__ import annotations

from typing import Dict, Tuple
import pandas as pd
from rdkit import Chem
from rdkit import RDLogger

RDLogger.DisableLog("rdApp.*") # disable RDKit warnings

def canonicalize_smiles(smiles: str):
    """
    convert smiles to canonical smiles
    :param smiles: smiles string
    :return: returns canonical smiles string, or None if invalid
    """

    if not isinstance(smiles, str):
        print('not a valid smiles string')
        return None

    try:
        mol = Chem.MolFromSmiles(smiles, sanitize=True) # create rdkit mol object from smiles

        if mol is None:
            return None

        return Chem.MolToSmiles(mol, canonical=True)

    except Exception:
        print('exception occurred during canonicalization')
        return None

def canonicalize_smiles_df(df, smiles_column_name, canonical_column_name: str = "canonical_smiles", drop_bad_smiles: bool = True):
    """
    canonicalize a dataframe column of smiles strings
    :param df: pandas dataframe
    :param smiles_column_name: column name to look for smiles strings
    :param canonical_column_name: name of new column to store canonical smiles
    :param drop_bad_smiles: remove the rows with invalid smiles
    :return: original dataframe with new column of canonical smiles and invalid smiles removed (if specified)
    """

    df2 = df.copy()

    df2[canonical_column_name] = df2[smiles_column_name].apply(canonicalize_smiles)

    # find out how many smiles were sanitized by comparing the original and canonical columns
    df2['changed'] = df2[canonical_column_name] != df2[smiles_column_name]
    n_changed = df2['changed'].sum()
    print(len(df2) - n_changed, 'smiles were already canonical,', n_changed, 'smiles were changed during canonicalization')

    if drop_bad_smiles:

        n_before = len(df2)
        df2 = df2.dropna(subset=[canonical_column_name]).reset_index(drop=True)
        n_after = len(df2)

        print(f"dropped {n_before - n_after} invalid smiles")

    return df2

def count_stereocenters_from_smiles(smiles: str) -> Dict[str, int]:

    mol = Chem.MolFromSmiles(smiles)
    if mol is None:
        raise ValueError(f"Invalid SMILES: {smiles}")

    # Ensure RDKit has identified potential stereo bonds
    Chem.FindPotentialStereoBonds(mol)

    stereocenters = Chem.FindMolChiralCenters(mol, includeUnassigned=True)
    stereobonds = [
        bond
        for bond in mol.GetBonds()
        if bond.GetStereo() is not Chem.rdchem.BondStereo.STEREONONE
    ]

    atom_assigned = sum(1 for _, tag in stereocenters if tag != "?")
    atom_unassigned = sum(1 for _, tag in stereocenters if tag == "?")

    bond_assigned = sum(
        1
        for bond in stereobonds
        if bond.GetStereo() is not Chem.rdchem.BondStereo.STEREOANY
    )
    bond_unassigned = sum(
        1 for bond in stereobonds if bond.GetStereo() is Chem.rdchem.BondStereo.STEREOANY
    )

    return {
        "atom_assigned": atom_assigned,
        "atom_unassigned": atom_unassigned,
        "bond_assigned": bond_assigned,
        "bond_unassigned": bond_unassigned,
    }


def filter_unassigned_stereo_rows(df: pd.DataFrame, smiles_col: str = "original_smiles", *, drop_invalid_smiles: bool = True, keep_stereo_counts: bool = False) -> pd.DataFrame:

    counts = []
    bad_smiles_idx = []

    for idx, smi in df[smiles_col].items():
        if not isinstance(smi, str) or not smi.strip():
            bad_smiles_idx.append(idx)
            counts.append({"atom_assigned": 0, "atom_unassigned": 0, "bond_assigned": 0, "bond_unassigned": 0})
            continue

        try:
            c = count_stereocenters_from_smiles(smi)
            counts.append(c)
        except Exception:
            if drop_invalid_smiles:
                bad_smiles_idx.append(idx)
                counts.append({"atom_assigned": 0, "atom_unassigned": 0, "bond_assigned": 0, "bond_unassigned": 0})
            else:
                raise

    counts_df = pd.DataFrame(counts, index=df.index)

    if keep_stereo_counts:
        df2 = df.join(counts_df)
    else:
        df2 = df.copy()

    # mask rows to keep: no unassigned stereo AND not bad smiles (if dropping invalid)
    ok = (counts_df["atom_unassigned"] == 0) & (counts_df["bond_unassigned"] == 0)

    if drop_invalid_smiles and bad_smiles_idx:
        ok = ok & (~df2.index.isin(bad_smiles_idx))

    return df2.loc[ok].reset_index(drop=True)


if __name__ == '__main__':

    df = pd.read_csv('/Users/mimis_stuff/PycharmProjects/PythonProject/philicity_prediction/data/all_philicities.csv')

    df = canonicalize_smiles_df(df, 'radical_smiles', 'canonical_radical_smiles', True)

    df.to_csv('/Users/mimis_stuff/PycharmProjects/PythonProject/philicity_prediction/data/all_philicities_canonicalized.csv', index=False)