Saturday, January 11, 2014

An interesting MCS use case

Inspired by this RDKit-discuss question from Liz Wylie: http://www.mail-archive.com/rdkit-discuss@lists.sourceforge.net/msg03676.html

In [1]:
from rdkit import Chem
from rdkit.Chem import MCS
from rdkit.Chem import Draw
from rdkit.Chem.Draw import IPythonConsole
from rdkit import rdBase
print rdBase.rdkitVersion
2014.03.1pre

Liz wanted to use custom atom types in the MCS code, yet still be able to get a readable SMILES for the MCS.

Here's a demonstration of the standard behavior of the MCS code for Liz's example.

In [2]:
smis=["COc1ccc(C(Nc2nc3c(ncn3COCC=O)c(=O)[nH]2)(c2ccccc2)c2ccccc2)cc1",
      "COc1ccc(C(Nc2nc3c(ncn3COC(CO)(CO)CO)c(=O)[nH]2)(c2ccccc2)c2ccccc2)cc1"]
ms = [Chem.MolFromSmiles(x) for x in smis]
Draw.MolsToGridImage(ms,subImgSize=(300,300))
Out[2]:

Start by defining a simple atom-type hash that combines atomic num and hybridization

In [3]:
def label(a): 
    return 100*int(a.GetHybridization())+a.GetAtomicNum()

The easiest way to use custom atom types is to set (bogus) isotope labels on the atoms in a copy of each molecule:

In [4]:
nms = [Chem.Mol(x) for x in ms]
for nm in nms:
    for at in nm.GetAtoms():
        at.SetIsotope(label(at))

And now run the MCS on the copy using the "istope" mode and print the results:

In [5]:
mcs=MCS.FindMCS(nms,atomCompare='isotopes')
print mcs.smarts
[406*]-[308*]-[306*]:1:[306*]:[306*]:[306*](:[306*]:[306*]:1)-[406*](-[306*]:1:[306*]:[306*]:[306*]:[306*]:[306*]:1)(-[306*]:1:[306*]:[306*]:[306*]:[306*]:[306*]:1)-[307*]-[306*]:1:[307*]:[306*]:2:[306*](:[306*](:[307*]:1)=[308*]):[307*]:[306*]:[307*]:2-[406*]-[408*]-[406*]

That's what we asked for, but it's not exactly readable.

We can get to a more readable form in a two step process

  1. Do a substructure match of the MCS onto a copied molecule
  2. Generate SMILES for the original molecule, using only the atoms that matched in the copy.

This works because we know that the atom indices in the copies and the original molecules are the same.

Start by getting the match

In [6]:
mcsp = Chem.MolFromSmarts(mcs.smarts)
match = nms[0].GetSubstructMatch(mcsp)

And now use Chem.MolFragmentToSmiles to generate the actual SMILES. This function generates the SMILES for a user-specified subset of a molecule.

In [7]:
smi=Chem.MolFragmentToSmiles(ms[0],atomsToUse=match,isomericSmiles=True,canonical=False)
print smi
COc1ccc(C(Nc2nc3c(ncn3COC)c(=O)[nH]2)(c2ccccc2)c2ccccc2)cc1

In [8]:
core = Chem.MolFromSmiles(smi)
core
Out[8]:

Note that in this particular case, the custom atom types don't make a difference in the MCS that is found.

Wednesday, December 18, 2013

Optimizing stereochemistry cleanup

Optimizing cleanup of stereochemistry

A fair amount of the time spent in constructing molecules using the RDKit goes to the assignment/cleanup of stereochemistry. I did an analysis for the 2012 UGM where I found that 27% of the time spent parsing 100K drug-like molecules from the ZINC set was in the assignStereochemistry() function. The function itself is responsible for assigning R/S labels to atoms and, more importantly, removing stereochemistry flags from atoms or double bonds that shouldn't have them (where the substituents are the same).

I think the assignStereochemistry() step is important (to be sure that molecules are actually correctly specified), but it certainly wouldn't be bad to make it faster. That's the point of this exercise.

As a readily availblbe test set for this, I'm going to take the ChEMBL molecules that appeared in documents published betwen 2010 and 2012 that I used in a previous blog post: http://rdkit.blogspot.ch/2013/12/finding-related-documents-in-chembl-2.html. The full set here contains 234681 molecules, since I'm only interested in molecules that have stereochemistry, I narrowed the set down like this:

egrep '@|/' chembl16_2010-2012.smi > chembl16_2010-2012.chiral.smi

That gives 81030 molecules that contain at least one specified chiral center or double bond with specified stereochemistry. For the purposes of this exercise, where I don't want to wait forever, I used the first 40K of those.

As with any optimization exercise, I did this one using the profiler and a relatively simple bit of code that reads in the 40K molecules and then generates canonical smiles for them. The SMILES generation isn't actually important here, but it does provide some useful information about relative timings.

Operation Percent Time(s)
Total 100.0 30.6
Sanitize 26.0 8.0
assignStereochemistry 24.5 7.4
MolToSmiles 39.9 12.2
rankAtoms 21.1 6.4

The rankAtoms step is part of MolToSmiles.

After an embarassing amount of time pursuing avenues that led to no real improvement, I made a couple of small changes (commits https://github.com/rdkit/rdkit/commit/d779c850c9696948f8e718ad790e0224ea7320b8#diff-ca0dbad92a874b2f69b549293387925e and https://github.com/rdkit/rdkit/commit/4d47482f0f9ac7d67be6a811232651da8e5dc635#diff-ca0dbad92a874b2f69b549293387925e) that ended up helping a fair amount:

Operation Percent Time(s)
Total 100.0 28.8
Sanitize 26.9 7.7
assignStereochemistry 20.3 5.8
MolToSmiles 42.9 12.3
rankAtoms 23.8 6.8

The rankAtoms step is part of MolToSmiles.

Stats about the dataset

  • Number of compounds considered: 40000
  • Number where stereochemistry was resolved in one pass: 38282
  • Number where dependent stereochemistry required two passes: 1712
  • Number where dependent stereochemistry required three passes: 6

Molecules where three passes were required

  • CN(C)C(=O)N[C@@H]1CC[C@@H](CN2[C@@H]3CC[C@H]2C[C@H](C3)Oc4cccc(c4)C(=O)N)CC1
  • CN(C)C(=O)N[C@@H]1CC[C@@H](CCN2[C@@H]3CC[C@H]2C[C@H](C3)Oc4cccc(c4)C(=O)N)CC1
  • CN(C)C(=O)N[C@@H]1CC[C@H](CN2[C@@H]3CC[C@H]2C[C@H](C3)Oc4cccc(c4)C(=O)N)CC1
  • CN(C)C(=O)N[C@@H]1CC[C@H](CCN2[C@@H]3CC[C@H]2C[C@H](C3)Oc4cccc(c4)C(=O)N)CC1
  • Cc1ccc(s1)C(=CCCN2C[C@@H]3[C@H](C2)[C@@H]3C(=O)O)c4ccc(C)s4
  • Cc1ccc(s1)C(=CCCN2C[C@@H]3[C@H](C2)[C@H]3C(=O)O)c4ccc(C)s4

Those all look reasonable.