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)
|