Files
rdkit/Code/GraphMol/MolInteractionFields/Wrap/rdMIF.cpp
Greg Landrum 644296fe13 Add Molecular Interaction Fields (#7993)
* Add RealValueVect.

* Add UniformRealValueGrid3D

* Add Molecular Interaction Fields (MIFs)

* line endings

* cherry-pick f1bc94a4c8

* format

* Adapt tests for python3.

* Adapt RealValueVector pickling for python3.

* Speed-up of MIF calculations.

* Bugfix in MIFDescriptors.cpp.

* all tests pass

* clean up some memory leaks

* update copyrights

* rename

* rename the library

* complete the rename

* lost file

* another forgotten file

* cleanup

* clang-tidy

* clang-tidy

* windows DLL builds work

* python wrapper and tests cleanup

* convert to catch2 testing

* switch RealValueVect to use std::vector

* remove obsolete friend

* - Replace explicit loops with stdlib implicit equivalents
- Replace explicit types with auto where possible
- Avoid unnecessary copy operations where possible
- Replace raw pointers with exception-safe unique_ptr
- Replace C-style #define with constexpr
- Replace C-style casts with C++ casts
- Replace C-style arrays with std::vector
- Avoid code duplication with templated operators
- Replace VdWaals class taking multiple atom type definitions and force-field name as string parameter with force-field-specific classes deriving from an abstract VdWaals class
- Replace x,y,z doubles with Point3D class where possible
- Removed unused (and untested) DistanceToClosestAtom class
- Renamed some variables and functions for better clarity
- Converted tabs to spaces
- Made the mol parameter in cube read/write functions optional for convenience
- Made the Python wrappers more pythonic (e.g., avoid C++-style passing objects as parameters which are modified in place)
- Implemented alternative Python class constructors using boost::python::make_constructor rather than with external non-class functions
- The Python wrappers taking a sequence of Point3D now take a sequence of sequences, such that the output of Conformer.GetPositions() can be passed
- Made the Python wrapper sequence parsing more robust
- Removed duplicated code from Python wrappers

* - avoid an unnecessary copy

* progress

* works

* more cleanup

* all tests pass

* changes in response to review

---------

Co-authored-by: dfhahn <dfhahn@users.noreply.github.com>
Co-authored-by: ptosco <paolo.tosco@novartis.com>
2024-12-19 09:54:52 +01:00

423 lines
21 KiB
C++

//
// Copyright (c) 2014-2024, Novartis Institutes for BioMedical Research and
// other RDKit contributors
//
// @@ All Rights Reserved @@
// This file is part of the RDKit.
// The contents are covered by the terms of the BSD license
// which is included in the file license.txt, found at the root
// of the RDKit source tree.
//
#include <RDBoost/Wrap.h>
#include <RDBoost/PySequenceHolder.h>
#include <boost/python.hpp>
#include <ForceField/MMFF/Nonbonded.h>
#include <GraphMol/MolInteractionFields/MIFDescriptors.h>
#include <Geometry/UniformRealValueGrid3D.h>
#include <Geometry/point.h>
#include <RDBoost/boost_numpy.h>
namespace python = boost::python;
using namespace RDMIF;
void wrap_mif();
BOOST_PYTHON_MODULE(rdMIF) {
python::scope().attr("__doc__") =
"Module containing functions for calculating molecular interaction fields (MIFs)\n\
NOTE: This functionality is experimental and the API and/or results may change in future releases.";
python::register_exception_translator<IndexErrorException>(
&translate_index_error);
python::register_exception_translator<ValueErrorException>(
&translate_value_error);
wrap_mif();
}
namespace RDMIF {
RDGeom::UniformRealValueGrid3D *constructGridHelper(const RDKit::ROMol &mol,
int confId, double margin,
double spacing) {
return constructGrid(mol, confId, margin, spacing).release();
}
std::pair<std::vector<double>, std::vector<RDGeom::Point3D>>
extractChargesAndPositions(const python::object &charges,
const python::object &positions) {
const auto pyPos = positions.ptr();
const auto pyCharges = charges.ptr();
if (!pyPos || !PySequence_Check(pyPos)) {
throw_value_error("positions argument must be a sequence");
}
if (!pyCharges || !PySequence_Check(pyCharges)) {
throw_value_error("charges argument must be a sequence");
}
auto nrows = PySequence_Size(pyPos);
if (nrows != PySequence_Size(pyCharges)) {
throw_value_error("positions and charges must have the same length");
}
std::vector<RDGeom::Point3D> pos(nrows);
std::vector<double> ch(nrows);
for (unsigned int i = 0; i < nrows; ++i) {
const auto pyXyz = PySequence_GetItem(pyPos, i);
if (!pyXyz || !PySequence_Check(pyXyz) || PySequence_Size(pyXyz) != 3) {
throw_value_error(
"all elements in positions argument must be x,y,z sequences");
}
pos[i].x = python::extract<double>(PySequence_GetItem(pyXyz, 0));
pos[i].y = python::extract<double>(PySequence_GetItem(pyXyz, 1));
pos[i].z = python::extract<double>(PySequence_GetItem(pyXyz, 2));
ch[i] = python::extract<double>(PySequence_GetItem(pyCharges, i));
}
return std::make_pair(std::move(ch), std::move(pos));
}
Coulomb *makeAltCoulomb(const python::object &charges,
const python::object &positions, double probecharge,
bool absVal, double alpha, double cutoff) {
const auto [ch, pos] = extractChargesAndPositions(charges, positions);
return new Coulomb(ch, pos, probecharge, absVal, alpha, cutoff);
}
CoulombDielectric *makeAltCoulombDielectric(const python::object &charges,
const python::object &positions,
double probecharge, bool absVal,
double alpha, double cutoff,
double epsilon, double xi) {
const auto [ch, pos] = extractChargesAndPositions(charges, positions);
return new CoulombDielectric(ch, pos, probecharge, absVal, alpha, cutoff,
epsilon, xi);
}
python::tuple readCubeFile(const std::string &filename) {
std::unique_ptr<RDGeom::UniformRealValueGrid3D> grd(
new RDGeom::UniformRealValueGrid3D());
auto res = readFromCubeFile(*grd, filename);
boost::python::manage_new_object::apply<
RDGeom::UniformRealValueGrid3D *>::type grdConverter;
boost::python::manage_new_object::apply<RDKit::ROMol *>::type molConverter;
return python::make_tuple(python::handle<>(grdConverter(grd.release())),
python::handle<>(molConverter(
static_cast<RDKit::ROMol *>(res.release()))));
}
struct mif_wrapper {
static void wrap() {
std::string docStringClass =
"Class for calculation of electrostatic interaction (Coulomb energy) between probe and molecule in\n\
vacuum (no dielectric).\n\n";
std::string docStringConst =
"Constructor for Coulomb class.\n\n\
ARGUMENTS:\n\
- mol: the molecule of interest\n\
- confId: the ID of the conformer to be used (defaults to -1)\n\
- probeCharge charge of probe [e] (defaults to 1.0 e)\n\
- absVal: if True, absolute values of interactions are calculated (defaults to False)\n\
- chargeKey property key for retrieving partial charges of atoms from molecule (defaults to '_GasteigerCharge')\n\
- softcoreParam softcore interaction parameter [A^2], if zero, a minimum cutoff distance is used (defaults to 0.0)\n\
- cutoff minimum cutoff distance [A] (defaults to 1.0)\n";
std::string docStringConstAlt =
"Alternative constructor for Coulomb class.\n\n\
ARGUMENTS:\n\
- charges: array of partial charges of a molecule's atoms\n\
- positions: array of positions of a molecule's atoms\n\
- probeCharge charge of probe [e] (defaults to 1.0 e)\n\
- absVal: if True, absolute values of interactions are calculated (defaults to False)\n\
- softcoreParam softcore interaction parameter [A^2], if zero, a minimum cutoff distance is used (defaults to 0.0)\n\
- cutoff minimum cutoff distance [A] (defaults to 1.0)\n";
std::string docString =
"Calculates the electrostatic interaction (Coulomb energy) between probe and molecule in\n\
vacuum (no dielectric).\n\n\
ARGUMENTS:\n\
- x, y, z: coordinates of probe position for energy calculation\n\
- threshold: maximal distance until which interactions are calculated\n\
RETURNS:\n\
- electrostatic potential in [kJ mol^-1]\n";
python::class_<Coulomb>(
"Coulomb", docStringClass.c_str(),
python::init<const RDKit::ROMol &, int, double, bool,
const std::string &, double, double>(
(python::arg("mol"), python::arg("confId") = -1,
python::arg("probeCharge") = 1.0, python::arg("absVal") = false,
python::arg("chargeKey") = "_GasteigerCharge",
python::arg("softcoreParam") = 0.0, python::arg("cutoff") = 1.0),
docStringConst.c_str()))
.def("__init__",
python::make_constructor(
makeAltCoulomb, python::default_call_policies(),
(python::arg("charges"), python::arg("positions"),
python::arg("probeCharge") = 1.0,
python::arg("absVal") = false,
python::arg("softcoreParam") = 0.0,
python::arg("cutoff") = 1.0)),
docStringConstAlt.c_str())
.def("__call__", &Coulomb::operator(),
(python::arg("x"), python::arg("y"), python::arg("z"),
python::arg("threshold")),
docString.c_str());
docStringClass =
"Class for calculation of electrostatic interaction (Coulomb energy) between probe and molecule in\n\
by taking a distance-dependent dielectric into account.\n\
Same energy term as used in GRID MIFs.\n\
References:\n\
- J. Med. Chem. 1985, 28, 849.\n\
- J. Comp. Chem. 1983, 4, 187.\n\n";
docStringConst =
"Constructor for CoulombDielectric class.\n\n\
ARGUMENTS:\n\
- mol: the molecule of interest\n\
- confId: the ID of the conformer to be used (defaults to -1)\n\
- probeCharge charge of probe [e] (defaults to 1.0 e)\n\
- absVal: if True, absolute values of interactions are calculated (defaults to False)\n\
- chargeKey property key for retrieving partial charges of atoms from molecule (defaults to '_GasteigerCharge')\n\
- softcoreParam softcore interaction parameter [A^2], if zero, a minimum cutoff distance is used (defaults to 0.0)\n\
- cutoff minimum cutoff distance [A] (defaults to 1.0)\n\
- epsilon relative permittivity of solvent (defaults to 80.0)\n\
- xi relative permittivity of solute (defaults to 4.0)\n";
docStringConstAlt =
"Alternative constructor for CoulombDielectric class.\n\n\
ARGUMENTS:\n\
- charges: array of partial charges of a molecule's atoms\n\
- positions: array of positions of a molecule's atoms\n\
- probeCharge charge of probe [e] (defaults to 1.0 e)\n\
- absVal: if True, absolute values of interactions are calculated (defaults to False)\n\
- softcoreParam softcore interaction parameter [A^2], if zero, a minimum cutoff distance is used (defaults to 0.0)\n\
- cutoff minimum cutoff distance [A] (defaults to 1.0)\n\
- epsilon relative permittivity of solvent (defaults to 80.0)\n\
- xi relative permittivity of solute (defaults to 4.0)\n";
docString =
"Calculates the electrostatic interaction (Coulomb energy) between probe and molecule in\n\
by taking a distance-dependent dielectric into account.\n\n\
ARGUMENTS:\n\
- x, y, z: coordinates of probe position for energy calculation\n\
- threshold: maximal distance until which interactions are calculated\n\
RETURNS:\n\
- electrostatic potential in [kJ mol^-1]\n";
python::class_<CoulombDielectric>(
"CoulombDielectric", docStringClass.c_str(),
python::init<const RDKit::ROMol &, int, double, bool,
const std::string &, double, double, double, double>(
(python::arg("mol"), python::arg("confId") = -1,
python::arg("probeCharge") = 1.0, python::arg("absVal") = false,
python::arg("chargeKey") = "_GasteigerCharge",
python::arg("softcoreParam") = 0.0, python::arg("cutoff") = 1.0,
python::arg("epsilon") = 80.0, python::arg("xi") = 4.0),
docStringConst.c_str()))
.def("__init__",
python::make_constructor(
makeAltCoulombDielectric, python::default_call_policies(),
(python::arg("charges"), python::arg("positions"),
python::arg("probeCharge") = 1.0,
python::arg("absVal") = false,
python::arg("softcoreParam") = 0.0,
python::arg("cutoff") = 1.0, python::arg("epsilon") = 80.0,
python::arg("xi") = 4.0)),
docStringConstAlt.c_str())
.def(python::init<const std::string &>(
python::args("self", "pklString")))
.def("__call__", &CoulombDielectric::operator(),
(python::arg("x"), python::arg("y"), python::arg("z"),
python::arg("threshold")),
docString.c_str());
docStringClass =
"Class for calculating van der Waals interactions between molecule and a probe at a gridpoint\
based on the MMFF forcefield.\n";
docStringConst =
"ARGUMENTS:\n\
- mol molecule object\n\
- confId conformation id which is used to get positions of atoms (default=-1)\n\
- probeAtomType MMFF94 atom type for the probe atom (default=6, sp3 oxygen)\n\
- cutoff minimum cutoff distance [A] (default:1.0)\n\
- scaling scaling of VdW parameters to take hydrogen bonds into account (default=False)\n";
docString =
"Calculates the van der Waals interaction between molecule and a probe at a gridpoint.\n\n\
ARGUMENTS:\n\
- x, y, z: coordinates of probe position for energy calculation\n\
- threshold: maximal distance until which interactions are calculated\n\
RETURNS:\n\
- van der Waals potential in [kJ mol^-1]\n";
python::class_<MMFFVdWaals, MMFFVdWaals *, boost::noncopyable>(
"MMFFVdWaals", docStringClass.c_str(),
python::init<const RDKit::ROMol &, int, unsigned int, bool, double>(
(python::arg("self"), python::arg("mol"),
python::arg("confId") = -1, python::arg("probeAtomType") = 6,
python::arg("scaling") = false, python::arg("cutoff") = 1.0),
docStringConst.c_str()))
.def("__call__", &MMFFVdWaals::operator(),
(python::arg("x"), python::arg("y"), python::arg("z"),
python::arg("threshold")),
docString.c_str());
docStringClass =
"Class for calculating van der Waals interactions between molecule and a probe at a gridpoint\
based on the UFF forcefield.\n";
docStringConst =
"ARGUMENTS:\n\
- mol molecule object\n\
- confId conformation id which is used to get positions of atoms (default=-1)\n\
- probeAtomType UFF atom type for the probe atom (default='O_3', sp3 oxygen)\n\
- cutoff minimum cutoff distance [A] (default:1.0)\n";
python::class_<UFFVdWaals, UFFVdWaals *, boost::noncopyable>(
"UFFVdWaals", docStringClass.c_str(),
python::init<const RDKit::ROMol &, int, const std::string &, double>(
(python::arg("self"), python::arg("mol"),
python::arg("confId") = -1, python::arg("probeAtomType") = "O_3",
python::arg("cutoff") = 1.0),
docStringConst.c_str()))
.def("__call__", &UFFVdWaals::operator(),
(python::arg("x"), python::arg("y"), python::arg("z"),
python::arg("threshold")),
docString.c_str());
docStringClass =
"Class for calculation of hydrogen bonding energy between a probe and a molecule.\n\n\
Similar to GRID hydrogen bonding descriptors.\n\
References:\n\
- J.Med.Chem. 1989, 32, 1083.\n\
- J.Med.Chem. 1993, 36, 140.\n\
- J.Med.Chem. 1993, 36, 148.\n";
docStringConst =
"Constructor for HBond class.\n\n\
ARGUMENTS:\n\
- mol: the molecule of interest\n\
- confId: the ID of the conformer to be used (defaults to -1)\n\
- probeAtomType: atom type for the probe atom (either 'OH', 'O', 'NH' or 'N') (defaults to 'OH')\n\
- fixed: for some groups, two different angle dependencies are defined:\n\
one which takes some flexibility of groups (rotation/swapping of lone pairs and hydrogen)\n\
into account and one for strictly fixed conformations\n\
if True, strictly fixed conformations (defaults to True)\n\
- cutoff minimum cutoff distance [A] (defaults to 1.0)\n";
docString =
"Calculates the hydrogen bonding energy between probe and molecule in\n\n\
ARGUMENTS:\n\
- x, y, z: coordinates of probe position for energy calculation\n\
- threshold: maximal distance until which interactions are calculated\n\
RETURNS:\n\
hydrogen bonding energy in [kJ mol^-1]\n";
python::class_<HBond>(
"HBond", docStringClass.c_str(),
python::init<RDKit::ROMol &, int, const std::string &, bool, double>(
(python::arg("mol"), python::arg("confId") = -1,
python::arg("probeAtomType") = "OH", python::arg("fixed") = true,
python::arg("cutoff") = 1.0),
docStringConst.c_str()))
.def("__call__", &HBond::operator(),
(python::arg("x"), python::arg("y"), python::arg("z"),
python::arg("threshold")),
docString.c_str());
docStringClass =
"Class for calculation of a hydrophilic potential of a molecule at a point.\n\n\
The interaction energy of hydrogen and oxygen of water is calculated at each point as a \n\
hydrogen bond interaction (either OH or O probe). The favored interaction is returned.\n";
docStringConst =
"Constructor for Hydrophilic class.\n\n\
ARGUMENTS:\n\
- mol: the molecule of interest\n\
- confId: the ID of the conformer to be used (defaults to -1)\n\
- fixed: for some groups, two different angle dependencies are defined:\n\
one which takes some flexibility of groups (rotation/swapping of lone pairs and hydrogen)\n\
into account and one for strictly fixed conformations\n\
if True, strictly fixed conformations (defaults to True)\n\
- cutoff minimum cutoff distance [A] (default:1.0)\n";
docString =
"Calculates the hydrophilic field energy at a point.\n\n\
ARGUMENTS:\n\
- x, y, z: coordinates of probe position for energy calculation\n\
- threshold: maximal distance until which interactions are calculated\n\
RETURNS:\n\
hydrophilic field energy in [kJ mol^-1]\n";
python::class_<Hydrophilic>(
"Hydrophilic", docStringClass.c_str(),
python::init<RDKit::ROMol &, int, bool, double>(
(python::arg("mol"), python::arg("confId") = -1,
python::arg("fixed") = true, python::arg("cutoff") = 1.0),
docStringConst.c_str()))
.def("__call__", &Hydrophilic::operator(),
(python::arg("x"), python::arg("y"), python::arg("z"),
python::arg("threshold")),
docString.c_str());
docString =
"Constructs a UniformRealValueGrid3D (3D grid with real values at gridpoints) fitting to a molecule.\n\n\
ARGUMENTS:\n\
- mol: molecule of interest\n\
- confId: the ID of the conformer to be used (defaults to -1)\n\
- margin: minimum distance of molecule to surface of grid [A] (defaults to 5.0 A)\n\
- spacing: grid spacing [A] (defaults to 0.5 A)\n";
python::def("ConstructGrid", constructGridHelper,
(python::arg("mol"), python::arg("confId") = -1,
python::arg("margin") = 5.0, python::arg("spacing") = 0.5),
docString.c_str(),
python::return_value_policy<python::manage_new_object>());
docString =
"Calculates descriptors (to be specified as parameter) of a molecule at every gridpoint of a grid.\n\n\
ARGUMENTS:\n\
- grid: UniformRealValueGrid3D which get the MIF values\n\
- descriptor: Descriptor class which is used to calculate values\n";
python::def("CalculateDescriptors", calculateDescriptors<Coulomb>,
(python::arg("grid"), python::arg("descriptor"),
python::arg("threshold") = -1.0),
docString.c_str());
python::def("CalculateDescriptors", calculateDescriptors<CoulombDielectric>,
(python::arg("grid"), python::arg("descriptor"),
python::arg("threshold") = -1.0),
docString.c_str());
python::def("CalculateDescriptors", calculateDescriptors<MMFFVdWaals>,
(python::arg("grid"), python::arg("descriptor"),
python::arg("threshold") = -1.0),
docString.c_str());
python::def("CalculateDescriptors", calculateDescriptors<UFFVdWaals>,
(python::arg("grid"), python::arg("descriptor"),
python::arg("threshold") = -1.0),
docString.c_str());
python::def("CalculateDescriptors", calculateDescriptors<HBond>,
(python::arg("grid"), python::arg("descriptor"),
python::arg("threshold") = -1.0),
docString.c_str());
python::def("CalculateDescriptors", calculateDescriptors<Hydrophilic>,
(python::arg("grid"), python::arg("descriptor"),
python::arg("threshold") = -1.0),
docString.c_str());
docString =
"Writes Grid to a file in Gaussian CUBE format.\n\n\
ARGUMENTS:\n\
- grid: UniformRealValueGrid3D to be stored\n\
- filename: filename of file to be written\n\
- mol: associated molecule (defaults to None)\n\
- confId: the ID of the conformer to be used (defaults to -1)\n";
python::def(
"WriteToCubeFile", writeToCubeFile,
(python::arg("grid"), python::arg("filename"),
python::arg("mol") = python::object(), python::arg("confId") = -1),
docString.c_str());
docString =
"Reads Grid from a file in Gaussian CUBE format.\n\n\
ARGUMENTS:\n\
- filename: filename of file to be read\n\
RETURNS:\n\
a tuple where the first element is the grid and\n\
the second element is the molecule object associated to the grid\n\
(only atoms and coordinates, no bonds;\n\
None if no molecule was associated to the grid)\n";
python::def("ReadFromCubeFile", readCubeFile, (python::arg("filename")),
docString.c_str());
}
};
} // namespace RDMIF
void wrap_mif() { mif_wrapper::wrap(); }