mirror of
https://github.com/rdkit/rdkit.git
synced 2026-06-03 21:44:30 +08:00
585 lines
16 KiB
C++
585 lines
16 KiB
C++
/*=================================================================*/
|
|
/* Copyright (C) 2024 Greg Landrum and other RDKit contributors */
|
|
/* Contributed by NextMove Software, Cambridge, UK. */
|
|
/* */
|
|
/* */
|
|
/* @@ All Rights Reserved @@ */
|
|
/* The contents are covered by the terms of the */
|
|
/* BSD license, which is included in the file */
|
|
/* license.txt. */
|
|
/*=================================================================*/
|
|
#define _USE_MATH_DEFINES
|
|
|
|
#include <limits>
|
|
#include <string>
|
|
#include <list>
|
|
#include <cmath>
|
|
|
|
#include <GraphMol/GraphMol.h>
|
|
#include <GraphMol/MolOps.h>
|
|
#include <GraphMol/MonomerInfo.h>
|
|
#include <Geometry/point.h>
|
|
#include <GraphMol/RDKitBase.h>
|
|
#include <GraphMol/FileParsers/FileParsers.h>
|
|
|
|
#include "DCLV.h"
|
|
#include "DCLV_dots.h"
|
|
|
|
using RDGeom::Point3D;
|
|
|
|
namespace RDKit {
|
|
namespace Descriptors {
|
|
|
|
constexpr int VOXORDER = 16;
|
|
int recordCache = -1;
|
|
|
|
static bool checkExcludedAtoms(const Atom *atm, bool includeLigand) {
|
|
// helper to check whether atom should be included
|
|
|
|
const auto *info = atm->getMonomerInfo();
|
|
if (info) {
|
|
const std::string &resName =
|
|
static_cast<const AtomPDBResidueInfo *>(info)->getResidueName();
|
|
|
|
switch (resName[0]) {
|
|
case 'D':
|
|
if (resName == "DOD" || resName == "D20") {
|
|
return true;
|
|
}
|
|
break;
|
|
case 'H':
|
|
if (resName == "HOH" || resName == "H20") {
|
|
return true;
|
|
}
|
|
break;
|
|
case 'S':
|
|
if (resName == "SOL" || resName == "SO4" || resName == "SUL") {
|
|
return true;
|
|
}
|
|
break;
|
|
case 'W':
|
|
if (resName == "WAT") {
|
|
return true;
|
|
}
|
|
break;
|
|
case 'T':
|
|
if (resName == "TIP") {
|
|
return true;
|
|
}
|
|
break;
|
|
case 'P':
|
|
if (resName == "P04") {
|
|
return true;
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (!includeLigand &&
|
|
static_cast<const AtomPDBResidueInfo *>(info)->getIsHeteroAtom()) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static bool includeAsPolar(const Atom *atm, const ROMol &mol, bool includeSandP,
|
|
bool includeHs) {
|
|
// using Peter Ertl definition, polar atoms = O, N, P, S and attached Hs
|
|
|
|
switch (atm->getAtomicNum()) {
|
|
// nitrogen and oxygen
|
|
case 7:
|
|
case 8: {
|
|
return true;
|
|
}
|
|
|
|
// sulphur and phosphorous
|
|
case 15:
|
|
case 16: {
|
|
return includeSandP;
|
|
}
|
|
|
|
// hydrogen
|
|
case 1: {
|
|
if (!includeHs) {
|
|
return false;
|
|
} else {
|
|
for (const auto nbr : mol.atomNeighbors(atm)) {
|
|
if (includeAsPolar(nbr, mol, includeSandP, includeHs)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
// everything else
|
|
default: {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
struct State {
|
|
// upper and lower grid bounds
|
|
double voxX = 0.0;
|
|
double voxY = 0.0;
|
|
double voxZ = 0.0;
|
|
double voxU = 0.0;
|
|
double voxV = 0.0;
|
|
double voxW = 0.0;
|
|
|
|
std::vector<unsigned int> grid[VOXORDER][VOXORDER][VOXORDER];
|
|
|
|
void createVoxelGrid(const Point3D &minXYZ, const Point3D &maxXYZ,
|
|
const std::vector<Point3D> &positions,
|
|
const std::vector<double> &radii_) {
|
|
voxX = VOXORDER / ((maxXYZ.x - minXYZ.x) + 0.1);
|
|
voxU = minXYZ.x;
|
|
voxY = VOXORDER / ((maxXYZ.y - minXYZ.y) + 0.1);
|
|
voxV = minXYZ.y;
|
|
voxZ = VOXORDER / ((maxXYZ.z - minXYZ.z) + 0.1);
|
|
voxW = minXYZ.z;
|
|
|
|
unsigned int pcount = (unsigned int)positions.size();
|
|
for (unsigned int i = 0; i < pcount; i++) {
|
|
if (radii_[i] != 0.0) {
|
|
// get grid positions and add to list
|
|
const Point3D &pos = positions[i];
|
|
const unsigned int x = (unsigned int)(voxX * (pos.x - voxU));
|
|
const unsigned int y = (unsigned int)(voxY * (pos.y - voxV));
|
|
const unsigned int z = (unsigned int)(voxZ * (pos.z - voxW));
|
|
grid[x][y][z].push_back(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
std::vector<std::vector<unsigned int>> findNeighbours(
|
|
const ROMol &mol, const std::vector<Point3D> &positions,
|
|
const std::vector<double> &radii_, double probeRad, double maxRadius) {
|
|
std::vector<std::vector<unsigned int>> nbrs;
|
|
std::vector<unsigned int> atomNeighbours;
|
|
|
|
for (const auto atom : mol.atoms()) {
|
|
atomNeighbours.clear();
|
|
const auto atm_idx = atom->getIdx();
|
|
if (radii_[atm_idx] == 0.0) {
|
|
continue;
|
|
}
|
|
const Point3D &pos = positions[atm_idx];
|
|
const double range = radii_[atm_idx] + probeRad + probeRad;
|
|
const double maxDist = range + maxRadius;
|
|
|
|
int lx = voxX * (pos.x - maxDist - voxU);
|
|
if (lx < 0) {
|
|
lx = 0;
|
|
}
|
|
|
|
int ly = voxY * (pos.y - maxDist - voxV);
|
|
if (ly < 0) {
|
|
ly = 0;
|
|
}
|
|
int lz = voxZ * (pos.z - maxDist - voxW);
|
|
if (lz < 0) {
|
|
lz = 0;
|
|
}
|
|
|
|
int ux = voxX * (pos.x + maxDist - voxU);
|
|
if (ux >= VOXORDER) {
|
|
ux = VOXORDER - 1;
|
|
}
|
|
int uy = voxY * (pos.y + maxDist - voxV);
|
|
if (uy >= VOXORDER) {
|
|
uy = VOXORDER - 1;
|
|
}
|
|
int uz = voxZ * (pos.z + maxDist - voxW);
|
|
if (uz >= VOXORDER) {
|
|
uz = VOXORDER - 1;
|
|
}
|
|
|
|
for (auto x = lx; x <= ux; x++) {
|
|
for (auto y = ly; y <= uy; y++) {
|
|
for (auto z = lz; z <= uz; z++) {
|
|
for (auto idx : grid[x][y][z]) {
|
|
if (idx != atm_idx) {
|
|
auto dist = range + radii_[idx];
|
|
if ((pos - positions[idx]).lengthSq() < (dist * dist)) {
|
|
atomNeighbours.push_back(idx);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
nbrs.push_back(atomNeighbours);
|
|
}
|
|
return nbrs;
|
|
}
|
|
};
|
|
|
|
// constructor definition
|
|
DoubleCubicLatticeVolume::DoubleCubicLatticeVolume(
|
|
const ROMol &mol, std::vector<double> radii, bool isProtein,
|
|
bool includeLigand, double probeRadius, int confId)
|
|
: mol(mol),
|
|
radii_(std::move(radii)),
|
|
isProtein(isProtein),
|
|
includeLigand(includeLigand),
|
|
probeRadius(probeRadius),
|
|
confId(confId) {
|
|
//! Class for calculation of the Shrake and Rupley surface area and volume
|
|
//! using the Double Cubic Lattice Method.
|
|
//!
|
|
//! Frank Eisenhaber, Philip Lijnzaad, Patrick Argos, Chris Sander and
|
|
//! Michael Scharf, "The Double Cubic Lattice Method: Efficient Approaches
|
|
//! to Numerical Integration of Surface Area and Volume and to Dot Surface
|
|
//! Contouring of Molecular Assemblies", Journal of Computational Chemistry,
|
|
//! Vol. 16, No. 3, pp. 273-284, 1995.
|
|
|
|
/*!
|
|
|
|
\param mol: input molecule or protein
|
|
\param radii: radii for atoms of input mol
|
|
\param isProtein: flag to calculate burried surface area of a protein ligand
|
|
complex [default=false, free ligand]
|
|
\param includeLigand: flag to trigger
|
|
inclusion of bound ligand in surface area and volume calculations where
|
|
molecule is a protein [default=true]
|
|
\param probeRadius: radius of the
|
|
sphere representing the probe solvent atom
|
|
\param confId: conformer ID to consider [default=-1]
|
|
\return class
|
|
object
|
|
*/
|
|
|
|
if (radii_.empty()) {
|
|
const auto *tbl = PeriodicTable::getTable();
|
|
radii_.reserve(mol.getNumAtoms());
|
|
for (const auto atom : mol.atoms()) {
|
|
radii_.push_back(tbl->getRvdw(atom->getAtomicNum()));
|
|
}
|
|
}
|
|
|
|
positions = mol.getConformer(confId).getPositions();
|
|
maxRadius = *std::max_element(radii_.begin(), radii_.end());
|
|
|
|
// total x,y,z centres
|
|
Point3D cXYZ;
|
|
|
|
// total min max coords
|
|
Point3D minXYZ;
|
|
Point3D maxXYZ;
|
|
|
|
minXYZ.x = minXYZ.y = minXYZ.z = std::numeric_limits<double>::infinity();
|
|
maxXYZ.x = maxXYZ.y = maxXYZ.z = -std::numeric_limits<double>::infinity();
|
|
|
|
State s = State();
|
|
|
|
bool init = false;
|
|
|
|
for (const auto atom : mol.atoms()) {
|
|
const unsigned int atomIdx = atom->getIdx();
|
|
numAtoms++;
|
|
|
|
if (isProtein) {
|
|
if (checkExcludedAtoms(atom, includeLigand)) {
|
|
radii_[atomIdx] = 0.0;
|
|
numAtoms--;
|
|
}
|
|
}
|
|
|
|
if (radii_[atomIdx] != 0.0) {
|
|
const Point3D position = positions[atomIdx];
|
|
// get sum over centres
|
|
cXYZ.x += position.x;
|
|
cXYZ.y += position.y;
|
|
cXYZ.z += position.z;
|
|
|
|
// loop over atoms to find min + max x, y + z
|
|
if (init) {
|
|
if (position.x > maxXYZ.x) {
|
|
maxXYZ.x = position.x;
|
|
} else if (position.x < minXYZ.x) {
|
|
minXYZ.x = position.x;
|
|
}
|
|
|
|
if (position.y > maxXYZ.y) {
|
|
maxXYZ.y = position.y;
|
|
} else if (position.y < minXYZ.y) {
|
|
minXYZ.y = position.y;
|
|
}
|
|
|
|
if (position.z > maxXYZ.z) {
|
|
maxXYZ.z = position.z;
|
|
} else if (position.z < minXYZ.z) {
|
|
minXYZ.z = position.z;
|
|
}
|
|
} else {
|
|
maxXYZ.x = minXYZ.x = position.x;
|
|
maxXYZ.y = minXYZ.y = position.y;
|
|
maxXYZ.z = minXYZ.z = position.z;
|
|
init = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// determineCentreOfGravity
|
|
centreOfGravity.x = cXYZ.x / numAtoms;
|
|
centreOfGravity.y = cXYZ.y / numAtoms;
|
|
centreOfGravity.z = cXYZ.z / numAtoms;
|
|
|
|
if (init) {
|
|
s.createVoxelGrid(minXYZ, maxXYZ, positions, radii_);
|
|
neighbours =
|
|
s.findNeighbours(mol, positions, radii_, probeRadius, maxRadius);
|
|
}
|
|
}
|
|
|
|
bool DoubleCubicLatticeVolume::testPoint(
|
|
const Point3D &vect, double solvrad,
|
|
const std::vector<unsigned int> &nbrs) {
|
|
if (recordCache != -1) {
|
|
const Point3D &pos = positions[recordCache];
|
|
const double dist = radii_[recordCache] + solvrad;
|
|
if ((pos - vect).lengthSq() < (dist * dist)) {
|
|
return false;
|
|
}
|
|
recordCache = -1;
|
|
}
|
|
|
|
for (unsigned int i : nbrs) {
|
|
const Point3D &pos = positions[i];
|
|
const double dist = radii_[i] + solvrad;
|
|
if ((pos - vect).lengthSq() < (dist * dist)) {
|
|
recordCache = i;
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
double DoubleCubicLatticeVolume::getAtomSurfaceArea(unsigned int atomIdx) {
|
|
// surface area for single atom
|
|
|
|
const auto rad = radii_[atomIdx];
|
|
if (rad == 0.0) {
|
|
return 0.0; // don't include if radius = 0, masked atom
|
|
}
|
|
|
|
const Point3D &pos = positions[atomIdx];
|
|
const std::vector<unsigned int> &nbr = neighbours[atomIdx];
|
|
|
|
double atomSurfaceArea = 0.0;
|
|
const double factor = rad + probeRadius;
|
|
|
|
recordCache = -1;
|
|
|
|
// standard dots has fixed NUMDOTS entries
|
|
// using precomputed dots in DCLV_dots.h
|
|
for (int i = 0; i < NUMDOTS; i++) {
|
|
const Point3D &dots =
|
|
Point3D(standardDots[i][0], standardDots[i][1], standardDots[i][2]);
|
|
const auto vect = pos + dots * factor;
|
|
surfacePoints[atomIdx].push_back(vect);
|
|
|
|
if (testPoint(vect, probeRadius, nbr)) {
|
|
atomSurfaceArea += dotArea;
|
|
}
|
|
}
|
|
atomSurfaceArea *= ((4.0 * M_PI) * (factor * factor)) / standardArea;
|
|
|
|
return atomSurfaceArea;
|
|
}
|
|
|
|
double DoubleCubicLatticeVolume::getSurfaceArea() {
|
|
// check if already calculated in compactness calc
|
|
if (surfaceArea != 0.0) {
|
|
return surfaceArea;
|
|
}
|
|
|
|
for (const auto atom : mol.atoms()) {
|
|
const auto atomIdx = atom->getIdx();
|
|
if (radii_[atomIdx] != 0.0) {
|
|
surfaceArea += getAtomSurfaceArea(atomIdx);
|
|
}
|
|
}
|
|
return surfaceArea;
|
|
}
|
|
|
|
double DoubleCubicLatticeVolume::getPartialSurfaceArea(
|
|
const boost::dynamic_bitset<> &incAtoms) {
|
|
// function to get the surface area of a set of atoms
|
|
// input is a set of indexes to include
|
|
|
|
double area = 0.0;
|
|
for (const auto atom : mol.atoms()) {
|
|
const auto atomIdx = atom->getIdx();
|
|
bool incAtom = incAtoms[atomIdx];
|
|
if (incAtom && radii_[atomIdx] != 0.0) {
|
|
area += getAtomSurfaceArea(atomIdx);
|
|
}
|
|
}
|
|
return area;
|
|
}
|
|
|
|
double DoubleCubicLatticeVolume::getPolarSurfaceArea(bool includeSandP,
|
|
bool includeHs) {
|
|
boost::dynamic_bitset<> polarAtoms(mol.getNumAtoms());
|
|
for (const auto atom : mol.atoms()) {
|
|
if (includeAsPolar(atom, mol, includeSandP, includeHs)) {
|
|
polarAtoms.set(atom->getIdx());
|
|
}
|
|
}
|
|
return getPartialSurfaceArea(polarAtoms);
|
|
}
|
|
|
|
std::map<unsigned int, std::vector<RDGeom::Point3D>> &
|
|
DoubleCubicLatticeVolume::getSurfacePoints() {
|
|
if (!surfacePoints.empty()) {
|
|
return surfacePoints;
|
|
}
|
|
|
|
for (const auto atom : mol.atoms()) {
|
|
const auto atomIdx = atom->getIdx();
|
|
if (radii_[atomIdx] != 0.0) {
|
|
const Point3D &pos = positions[atomIdx];
|
|
const double factor = radii_[atomIdx] + probeRadius;
|
|
|
|
// standard dots has fixed NUMDOTS entries
|
|
// using precomputed dots in DCLV_dots.h
|
|
for (int i = 0; i < NUMDOTS; i++) {
|
|
const Point3D &dots =
|
|
Point3D(standardDots[i][0], standardDots[i][1], standardDots[i][2]);
|
|
const auto vect = pos + dots * factor;
|
|
surfacePoints[atomIdx].push_back(vect);
|
|
}
|
|
}
|
|
}
|
|
return surfacePoints;
|
|
}
|
|
|
|
double DoubleCubicLatticeVolume::getAtomVolume(unsigned int atomIdx,
|
|
double solventRadius) {
|
|
// get volume for single atom
|
|
|
|
const double rad = radii_[atomIdx];
|
|
if (rad == 0.0) {
|
|
return 0.0; // don't include if radius = 0, masked atom
|
|
}
|
|
|
|
const auto &pos = positions[atomIdx];
|
|
const auto &nbr = neighbours[atomIdx];
|
|
|
|
const auto factor = rad + solventRadius;
|
|
|
|
recordCache = -1;
|
|
|
|
RDGeom::Point3D p;
|
|
unsigned int count = 0;
|
|
// using precomputed dots in DCLV_dots.h
|
|
for (const auto &dot : standardDots) {
|
|
auto vect = pos + dot * factor;
|
|
|
|
if (testPoint(vect, solventRadius, nbr)) {
|
|
p += dot;
|
|
count++;
|
|
}
|
|
}
|
|
|
|
/* Calculate Volume with Gauss-Ostrogradskii Theorem */
|
|
auto atomvolume = (pos - centreOfGravity).dotProduct(p);
|
|
atomvolume = factor * factor * (atomvolume + factor * count);
|
|
atomvolume *= ((4.0 / 3) * M_PI) / NUMDOTS; // 320 standard dots
|
|
return atomvolume;
|
|
}
|
|
|
|
double DoubleCubicLatticeVolume::getVolume() {
|
|
// function to return volume enclosed by probe sphere
|
|
|
|
// check if already calculated in compactness or packing density
|
|
if (totalVolume != 0.0) {
|
|
return totalVolume;
|
|
}
|
|
|
|
for (const auto atom : mol.atoms()) {
|
|
const unsigned int atomIdx = atom->getIdx();
|
|
if (radii_[atomIdx] != 0.0) {
|
|
totalVolume += getAtomVolume(atomIdx, probeRadius);
|
|
}
|
|
}
|
|
|
|
return totalVolume;
|
|
}
|
|
|
|
double DoubleCubicLatticeVolume::getVDWVolume() {
|
|
// check if already calculated in packing density
|
|
if (vdwVolume != 0.0) {
|
|
return vdwVolume;
|
|
}
|
|
|
|
// function to return VDW volume (probe radius == 0)
|
|
for (const auto atom : mol.atoms()) {
|
|
const unsigned int atomIdx = atom->getIdx();
|
|
if (radii_[atomIdx] != 0.0) {
|
|
vdwVolume += getAtomVolume(atomIdx, 0.0);
|
|
}
|
|
}
|
|
|
|
return vdwVolume;
|
|
}
|
|
|
|
double DoubleCubicLatticeVolume::getPartialVolume(
|
|
const boost::dynamic_bitset<> &incAtoms) {
|
|
// function to get the surface area of a set of atoms
|
|
// input is a set of indexes to include
|
|
|
|
double vol = 0.0;
|
|
for (const auto atom : mol.atoms()) {
|
|
const auto atomIdx = atom->getIdx();
|
|
bool incAtom = incAtoms[atomIdx];
|
|
if (incAtom && radii_[atomIdx] != 0.0) {
|
|
vol += getAtomVolume(atomIdx, probeRadius);
|
|
}
|
|
}
|
|
return vol;
|
|
}
|
|
|
|
double DoubleCubicLatticeVolume::getPolarVolume(bool includeSandP,
|
|
bool includeHs) {
|
|
boost::dynamic_bitset<> polarAtoms(mol.getNumAtoms());
|
|
for (const auto atom : mol.atoms()) {
|
|
if (includeAsPolar(atom, mol, includeSandP, includeHs)) {
|
|
polarAtoms.set(atom->getIdx());
|
|
}
|
|
}
|
|
return getPartialVolume(polarAtoms);
|
|
}
|
|
|
|
double DoubleCubicLatticeVolume::getCompactness() {
|
|
// check both surface area and volume already computed
|
|
if (surfaceArea == 0.0) {
|
|
getSurfaceArea();
|
|
}
|
|
|
|
if (totalVolume == 0.0) {
|
|
getVolume();
|
|
}
|
|
|
|
return surfaceArea / cbrt(36.0 * M_PI * totalVolume * totalVolume);
|
|
}
|
|
|
|
double DoubleCubicLatticeVolume::getPackingDensity() {
|
|
if (vdwVolume == 0.0) {
|
|
getVDWVolume();
|
|
}
|
|
|
|
if (totalVolume == 0.0) {
|
|
getVolume();
|
|
}
|
|
|
|
return vdwVolume / totalVolume;
|
|
}
|
|
|
|
} // namespace Descriptors
|
|
} // namespace RDKit
|