Files
rdkit/Code/GraphMol/Descriptors/DCLV.cpp
2026-02-09 12:10:50 +01:00

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