rdkit/rdkit — pull request #99 by ptosco (2013-09-16 11:39:44+00:00)
Title: MMFF & UFF update

- I have made MMFF atom typing more robust since I realized that
incorrect typing might arise when hydrogens were not added after
generating 3D coordinates from SMILES strings; now all 761 test molecules
are correctly typed no matter whether hydrogens are explicit or implicit

- MMFF test suite: I have cut down to the bare essential the
MMFF94/MMFF94s reference log files, but their size could be reduced only
by about 30%. It could have been reduced more converting multiple spaces
into a tab, but the MMFF94 file (the larger one) would still be around
11 MB, and human readability would be greatly impaired. Hence I decided
to keep the spaces and gzip the reference logs, which reduces their
combined size to ~ 3.5 MB, which I think is fine; the test program checks
if the gunzipped files already exist, otherwise it gunzips them upfront.
While cutting, I also sorted the molecules in the same order as in the
SDF/SMILES files, so that it runs about 10 times faster than before.
Now the test runs on MMFF94 only (MMFF94s only concerns different OOP
parameters, there are no algorithmic differences, so as long as one does
not alter the original parameters it can be safely skipped), computing
every 4th molecule, and it runs in 12 seconds on my laptop. Running
all molecules takes ~ 50 seconds, but I think it is rather overkill,
and I would keep it as it is.

- I have added a test suite for MMFF ForceFieldHelpers (like the one
already existing for UFF); I have also complemented the Python wrapper
test suite for ForceFieldHelpers with a few tests for MMFF.

- I have written Python wrappers for the MMFF-related functionality;
while doing that I realized that many of the wrapper code relocations
that I made in my previous pull request were not necessary/appropriate,
so I reverted them. The only difference from the UFF Python API is that,
just like for the C++ API, in addition to the PyForceField object there
is a PyMMFFMolProperties object which is created before constructing the
force field itself; the PyMMFFMolProperties is necessary to set (e.g.,
dielectric constant, dielectric model) or get (e.g., atom type, formal
and partial charge) some MMFF properties which are not present in UFF,
while preserving binary compatibility of the libraries. Probably you
remember that we discussed about setting atom type and charge properties
with SetProp besides populating the MMFFMolProperties object, in order
to allow easy access to Python users. However, I think that the solution
I adopted is preferrable since it is more consistent with the C++ API,
it enables faster access to properties and it allows tailoring the MMFF
environment (i.e., choosing MMFF94/MMFF94s, setting the verbosity level,
including/excluding terms from the MMFF equation, setting dielectric
constant/model) just as from C++.

The MMFF-related Python functions I implemented are:

* MMFFOptimizeMolecule(mol, mmffVariant = "MMFF94", maxIters = 200,
nonBondedThresh = 100.0, confId = -1, ignoreInterfragInteractions
= true)

uses MMFF to optimize a molecule's structure (just like
UFFOptimizeMolecule)

* SanitizeMMFFMol(mol)

sanitizes a molecule according to MMFF requirements

* SetupMMFFForceField(mol, mmffVariant = "MMFF94", mmffVerbosity = 0)

returns a PyMMFFMolProperties object for a molecule; the
PyMMFFMolProperties object is required by MMFFGetMoleculeForceField()
and can be used to get/set MMFF properties

* MMFFGetMoleculeForceField(mol, pyMMFFMolProperties,
nonBondedThresh = 100.0, confId = -1, ignoreInterfragInteractions
= true)

returns a MMFF force field for a molecule (just like
UFFGetMoleculeForceField)

* MMFFHasAllMoleculeParams(mol)

checks if MMFF parameters are available for all of a molecule's atoms
(just like UFFHasAllMoleculeParams)

There are also a few methods connected to the PyMMFFMolProperties class
which mirror those available from C++ for the MMFFMolProperties class:

* GetMMFFAtomType(idx)

Retrieves MMFF atom type for atom with index idx

* GetMMFFFormalCharge(idx)

Retrieves MMFF formal charge for atom with index idx

* GetMMFFPartialCharge(idx)

Retrieves MMFF partial charge for atom with index idx

* SetMMFFDielectricModel(dielModel = 1)

sets the DielModel MMFF property (1: constant; 2: distance-dependent;
defaults to constant)

* SetMMFFDielectricConstant(dielConst = 1.0)

Sets the DielConst MMFF property (defaults to 1.0)

* SetMMFFBondTerm(state = True)

Sets the bond term to be included in the MMFF equation (defaults
to True)

* SetMMFFAngleTerm(state = True)

Sets the angle term to be included in the MMFF equation (defaults
to True)

* SetMMFFStretchBendTerm(state = True)

Sets the stretch-bend term to be included in the MMFF equation (defaults
to True)

* SetMMFFOopTerm(state = True)

Sets the out-of-plane bend term to be included in the MMFF equation
(defaults to True)

* SetMMFFTorsionTerm(state = True)

Sets the torsional term to be included in the MMFF equation (defaults
to True)

* SetMMFFVdWTerm(state = True)

Sets the Van der Waals term to be included in the MMFF equation
(defaults to True)

* SetMMFFEleTerm(state = True)

Sets the electrostatic term to be included in the MMFF equation
(defaults to True)

* SetMMFFVariant(mmffVariant = "MMFF94")

Sets the MMFF variant to be used ("MMFF94" or "MMFF94s"; defaults to
"MMFF94")

* SetMMFFVerbosity(verbosity = 0)

Sets the MMFF verbosity (0: none; 1: low; 2: high; defaults to 0)

Hence, most users will do something like this to optimize a molecule
structure obtained from a SMILES string:

from rdkit import Chem
from rdkit.Chem import AllChem

m = Chem.MolFromSmiles("O=C(C)c1cccnc1", False)
AllChem.SanitizeMMFFMol(m)
m2 = Chem.AddHs(m)
AllChem.EmbedMolecule(m2)
# Opt
AllChem.MMFFOptimizeMolecule(m2)
print >>file('structure_min.sdf','w'), Chem.MolToMolBlock(m2)

Those willing to play a bit more with MMFF properties may do the
following:

from rdkit import Chem
from rdkit.Chem import AllChem

m = Chem.MolFromSmiles("O=C(C)c1cccnc1", False)
AllChem.SanitizeMMFFMol(m)
m2 = Chem.AddHs(m)
AllChem.EmbedMolecule(m2)
pyMP = AllChem.SetupMMFFForceField(m2)
pyMP.SetMMFFVariant("MMFF94s")
pyMP.SetMMFFDielectricModel(2)
pyFF = AllChem.MMFFGetMoleculeForceField(m2, pyMP)
pyFF.Minimize()
print >>file('structure_min.sdf','w'), Chem.MolToMolBlock(m2)
print 'Energy = {0:12.4f}'.format(pyFF.CalcEnergy())
i = 0
for i in range(0, m2.GetNumAtoms()):
print '{0:4d} {1:4d} {2:8.4f} {3:8.4f}'.format(i + 1,
int(pyMP.GetMMFFAtomType(i)),
float(pyMP.GetMMFFFormalCharge(i)),
float(pyMP.GetMMFFPartialCharge(i)))

- OOP backport to UFF. I added the inversion term to the UFF
implementation following the original UFF paper by Rappe'. I have already
modified the figures in a couple of test files to reflect the new energy
values.

- 2-bit neighbor matrix and graph-based angle enumeration in UFF now reflect
the MMFF implementation.

I sent a few more details in a separate e-mail.