mirror of
https://github.com/rdkit/rdkit.git
synced 2026-06-03 21:44:30 +08:00
646 lines
25 KiB
C++
646 lines
25 KiB
C++
//
|
|
// Copyright (C) 2026 David Cosgrove 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.
|
|
//
|
|
// Original author: David Cosgrove (CozChemIx Limited)
|
|
//
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <numbers>
|
|
#include <numeric>
|
|
|
|
#include <Geometry/point.h>
|
|
#include <Geometry/Transform3D.h>
|
|
#include <GraphMol/GaussianShape/ShapeInput.h>
|
|
#include <GraphMol/GaussianShape/ShapeOverlayOptions.h>
|
|
#include <GraphMol/GaussianShape/SingleConformerAlignment.h>
|
|
|
|
constexpr int D = 4;
|
|
|
|
namespace RDKit {
|
|
namespace GaussianShape {
|
|
|
|
SingleConformerAlignment::SingleConformerAlignment(
|
|
const std::vector<double> &ref, const int *refTypes,
|
|
const boost::dynamic_bitset<> *refCarbonRadii, int nRefShape, int nRefColor,
|
|
double refShapeVol, double refColorVol, const std::vector<double> &fit,
|
|
const int *fitTypes, const boost::dynamic_bitset<> *fitCarbonRadii,
|
|
int nFitShape, int nFitColor, double fitShapeVol, double fitColorVol,
|
|
const std::array<double, 7> &initQuatTrans, OptimMode optimMode,
|
|
double simAlpha, double simBeta, double mixingParam, bool useCutoff,
|
|
double distCutoff, double shapeConvergenceCriterion, unsigned int maxIts)
|
|
: d_ref(ref),
|
|
d_refTypes(refTypes),
|
|
d_refCarbonRadii(refCarbonRadii),
|
|
d_nRefShape(nRefShape),
|
|
d_nRefColor(nRefColor),
|
|
d_refShapeVol(refShapeVol),
|
|
d_refColorVol(refColorVol),
|
|
d_fit(fit),
|
|
d_fitTypes(fitTypes),
|
|
d_fitCarbonRadii(fitCarbonRadii),
|
|
d_nFitShape(nFitShape),
|
|
d_nFitColor(nFitColor),
|
|
d_fitShapeVol(fitShapeVol),
|
|
d_fitColorVol(fitColorVol),
|
|
d_initQuatTrans(initQuatTrans),
|
|
d_optimMode(optimMode),
|
|
d_simAlpha(simAlpha),
|
|
d_simBeta(simBeta),
|
|
d_mixingParam(mixingParam),
|
|
d_useCutoff(useCutoff),
|
|
d_distCutoff2(distCutoff * distCutoff),
|
|
d_shapeConvergenceCriterion(shapeConvergenceCriterion),
|
|
d_maxIts(maxIts) {
|
|
// Move the reference by initialTrans, leaving fit at the origin where
|
|
// the rotations work properly. Apply the initial rotation to the fit.
|
|
translateShape(d_ref, RDGeom::Point3D{d_initQuatTrans[4], d_initQuatTrans[5],
|
|
d_initQuatTrans[6]});
|
|
RDGeom::Transform3D xform;
|
|
xform.SetRotationFromQuaternion(d_initQuatTrans.data());
|
|
applyTransformToShape(d_fit, xform);
|
|
d_refTemp.resize(d_ref.size());
|
|
d_fitTemp.resize(d_fit.size());
|
|
d_gradConverters.resize(12 * (d_nFitShape + d_nFitColor));
|
|
}
|
|
|
|
void SingleConformerAlignment::getFinalQuatTrans(
|
|
RDGeom::Transform3D &xform) const {
|
|
RDGeom::Transform3D tmp;
|
|
tmp.SetRotationFromQuaternion(d_quatTrans.data());
|
|
tmp.SetTranslation(
|
|
RDGeom::Point3D{d_quatTrans[4], d_quatTrans[5], d_quatTrans[6]});
|
|
RDGeom::Transform3D reverseInitialTrans;
|
|
reverseInitialTrans.SetTranslation(RDGeom::Point3D{
|
|
-d_initQuatTrans[4], -d_initQuatTrans[5], -d_initQuatTrans[6]});
|
|
RDGeom::Transform3D initialRot;
|
|
initialRot.SetRotationFromQuaternion(d_initQuatTrans.data());
|
|
xform = reverseInitialTrans * tmp * initialRot;
|
|
}
|
|
|
|
std::array<double, 5> SingleConformerAlignment::calcScores(
|
|
const double *ref, const double *fit, const bool includeColor) {
|
|
std::array<double, 5> scores{0.0, 0.0, 0.0, 0.0, 0.0};
|
|
scores[3] = calcVolAndGrads(ref, d_nRefShape, d_refCarbonRadii, fit,
|
|
d_nFitShape, d_fitCarbonRadii, d_gradConverters,
|
|
d_useCutoff, d_distCutoff2, nullptr, nullptr);
|
|
if (d_nRefColor && d_nFitColor &&
|
|
(d_optimMode == OptimMode::SHAPE_PLUS_COLOR || includeColor)) {
|
|
scores[4] = calcVolAndGrads(ref + d_nRefShape * D, d_nRefColor,
|
|
d_refTypes + d_nRefShape, fit + d_nFitShape * D,
|
|
d_nFitColor, d_fitTypes + d_nFitShape,
|
|
d_nFitShape, d_gradConverters, d_useCutoff,
|
|
d_distCutoff2, nullptr, nullptr);
|
|
}
|
|
scores = calcScores(scores[3], scores[4], includeColor);
|
|
return scores;
|
|
}
|
|
|
|
std::array<double, 5> SingleConformerAlignment::calcScores(
|
|
const bool includeColor) {
|
|
applyQuatTrans(d_quatTrans);
|
|
return calcScores(d_refTemp.data(), d_fitTemp.data(), includeColor);
|
|
}
|
|
|
|
std::array<double, 5> SingleConformerAlignment::calcScores(
|
|
const double shapeOvVol, const double colorOvVol,
|
|
const bool includeColor) const {
|
|
std::array<double, 5> scores{0.0, 0.0, 0.0, 0.0, 0.0};
|
|
scores[3] = shapeOvVol;
|
|
scores[4] = colorOvVol;
|
|
scores[1] =
|
|
shapeOvVol / (d_simAlpha * (d_refShapeVol - shapeOvVol) +
|
|
d_simBeta * (d_fitShapeVol - shapeOvVol) + shapeOvVol);
|
|
if (d_nRefColor && d_nFitColor && d_refColorVol > 0.0 &&
|
|
d_fitColorVol > 0.0 && includeColor) {
|
|
scores[2] =
|
|
colorOvVol / (d_simAlpha * (d_refColorVol - colorOvVol) +
|
|
d_simBeta * (d_fitColorVol - colorOvVol) + colorOvVol);
|
|
scores[0] = scores[1] * (1 - d_mixingParam) + scores[2] * d_mixingParam;
|
|
} else {
|
|
scores[0] = scores[1];
|
|
}
|
|
return scores;
|
|
}
|
|
|
|
namespace {
|
|
// Set of values to convert the cartesian gradients to quaternion gradients.
|
|
// This uses the chain rule: the dV/qQ = (dV/dr) * (dr/dQ) where V is the
|
|
// volume overlap and r is the Cartesian space. Assumes gradConverters
|
|
// is already the correct size.
|
|
void cartToQuatGrads(const double *quat, const double *mol, const int numBPts,
|
|
std::vector<double> &gradConverters,
|
|
const int gradConvOffset) {
|
|
// for ease of ref
|
|
const auto q = quat[0];
|
|
const auto r = quat[1];
|
|
const auto s = quat[2];
|
|
const auto u = quat[3];
|
|
const auto coef = 1.0 / (q * q + r * r + s * s + u * u);
|
|
for (int i = 0, j = gradConvOffset, k = 12 * gradConvOffset; i < 4 * numBPts;
|
|
i += 4, ++j, k += 12) {
|
|
const auto x = mol[i];
|
|
const auto y = mol[i + 1];
|
|
const auto z = mol[i + 2];
|
|
const auto dx_dq = coef * 2.0 * (q * x + u * y - s * z);
|
|
const auto dx_dr = coef * 2.0 * (r * x + s * y + u * z);
|
|
const auto dy_dr = coef * 2.0 * (s * x - r * y + q * z);
|
|
const auto dx_du = coef * 2.0 * (-u * x + q * y + r * z);
|
|
const auto dz_ds = dx_dq;
|
|
const auto dy_du = -dx_dq;
|
|
const auto dy_ds = dx_dr;
|
|
const auto dz_du = dx_dr;
|
|
const auto dx_ds = -dy_dr;
|
|
const auto dz_dq = dy_dr;
|
|
const auto dy_dq = dx_du;
|
|
const auto dz_dr = -dx_du;
|
|
gradConverters[k] = dx_dq;
|
|
gradConverters[k + 1] = dy_dq;
|
|
gradConverters[k + 2] = dz_dq;
|
|
gradConverters[k + 3] = dx_dr;
|
|
gradConverters[k + 4] = dy_dr;
|
|
gradConverters[k + 5] = dz_dr;
|
|
gradConverters[k + 6] = dx_ds;
|
|
gradConverters[k + 7] = dy_ds;
|
|
gradConverters[k + 8] = dz_ds;
|
|
gradConverters[k + 9] = dx_du;
|
|
gradConverters[k + 10] = dy_du;
|
|
gradConverters[k + 11] = dz_du;
|
|
}
|
|
}
|
|
} // namespace
|
|
|
|
// atoms/shape features
|
|
double calcVolAndGrads(const double *ref, const int numRefPts,
|
|
const boost::dynamic_bitset<> *refCarbonRadii,
|
|
const double *fit, const int numFitPts,
|
|
const boost::dynamic_bitset<> *fitCarbonRadii,
|
|
std::vector<double> &gradConverters,
|
|
const bool useCutoff, const double distCutoff2,
|
|
const double *quat, double *gradients) {
|
|
if (gradients) {
|
|
cartToQuatGrads(quat, fit, numFitPts, gradConverters, 0);
|
|
}
|
|
static constexpr double CARBON_A = KAPPA / (1.7 * 1.7);
|
|
static const double CARBON_BIT =
|
|
8.0 * pow(std::numbers::pi / (2 * CARBON_A), 1.5);
|
|
double vol = 0.0;
|
|
double vij;
|
|
// If either of the carbon radii flags aren't supplied, treat them
|
|
// both as being all carbon. There isn't enough information to do
|
|
// otherwise.
|
|
const bool allCarbon = !refCarbonRadii || !fitCarbonRadii;
|
|
for (int i = 0, i_idx = 0; i < numRefPts * 4; i += 4, i_idx++) {
|
|
const auto ai = ref[i + 3];
|
|
for (int j = 0, j_idx = 0, k = 0; j < numFitPts * 4;
|
|
j += 4, j_idx++, k += 12) {
|
|
const auto dx = ref[i] - fit[j];
|
|
const auto dy = ref[i + 1] - fit[j + 1];
|
|
const auto dz = ref[i + 2] - fit[j + 2];
|
|
const auto d2 = dx * dx + dy * dy + dz * dz;
|
|
if (useCutoff && d2 > distCutoff2) {
|
|
continue;
|
|
}
|
|
const auto aj = fit[j + 3];
|
|
const auto mult = -(ai * aj) / (ai + aj);
|
|
const auto kij = exp(mult * d2);
|
|
if (allCarbon || ((*refCarbonRadii)[i_idx] && (*fitCarbonRadii)[j_idx])) {
|
|
vij = kij * CARBON_BIT;
|
|
} else {
|
|
const auto pi_ai_aj = std::numbers::pi / (ai + aj);
|
|
vij = 8 * kij * pi_ai_aj * std::sqrt(pi_ai_aj);
|
|
}
|
|
vol += vij;
|
|
if (gradients) {
|
|
const auto r = 2.0 * vij * mult;
|
|
// Use the gradient converters to calculate the gradients in quaternion
|
|
// space.
|
|
// The zeroth gradient is never used, so don't waste time calculating
|
|
// it but leave the code here for completeness and possible future use.
|
|
// gradients[0] +=
|
|
// r * (dx * gradConverters[k] + dy *
|
|
// gradConverters[k + 1] +
|
|
// dz * gradConverters[k + 2]);
|
|
gradients[1] +=
|
|
r * (dx * gradConverters[k + 3] + dy * gradConverters[k + 4] +
|
|
dz * gradConverters[k + 5]);
|
|
gradients[2] +=
|
|
r * (dx * gradConverters[k + 6] + dy * gradConverters[k + 7] +
|
|
dz * gradConverters[k + 8]);
|
|
gradients[3] +=
|
|
r * (dx * gradConverters[k + 9] + dy * gradConverters[k + 10] +
|
|
dz * gradConverters[k + 11]);
|
|
gradients[4] += r * dx;
|
|
gradients[5] += r * dy;
|
|
gradients[6] += r * dz;
|
|
}
|
|
}
|
|
}
|
|
return vol;
|
|
}
|
|
|
|
// color features
|
|
double calcVolAndGrads(const double *ref, const int numRefPts,
|
|
const int *refTypes, const double *fit,
|
|
const int numFitPts, const int *fitTypes,
|
|
const int numFitShape,
|
|
std::vector<double> &gradConverters,
|
|
const bool useCutoff, const double distCutoff2,
|
|
const double *quat, double *gradients) {
|
|
double vol = 0.0;
|
|
if (gradients) {
|
|
cartToQuatGrads(quat, fit, numFitPts, gradConverters, numFitShape);
|
|
}
|
|
|
|
for (int i = 0, i_idx = 0; i < numRefPts * 4; i += 4, i_idx++) {
|
|
const auto ai = ref[i + 3];
|
|
const auto aType = refTypes[i_idx];
|
|
for (int j = 0, j_idx = 0, k = 0; j < numFitPts * 4;
|
|
j += 4, j_idx++, k += 12) {
|
|
const auto bType = fitTypes[j_idx];
|
|
if (aType != bType) {
|
|
continue;
|
|
}
|
|
const auto dx = ref[i] - fit[j];
|
|
const auto dy = ref[i + 1] - fit[j + 1];
|
|
const auto dz = ref[i + 2] - fit[j + 2];
|
|
const auto d2 = dx * dx + dy * dy + dz * dz;
|
|
if (useCutoff && d2 > distCutoff2) {
|
|
continue;
|
|
}
|
|
const auto aj = fit[j + 3];
|
|
const auto mult = -(ai * aj) / (ai + aj);
|
|
const auto kij = exp(mult * d2);
|
|
|
|
const auto pi_ai_aj = std::numbers::pi / (ai + aj);
|
|
const auto vij = 8 * kij * pi_ai_aj * std::sqrt(pi_ai_aj);
|
|
vol += vij;
|
|
if (gradients) {
|
|
const auto r = 2.0 * vij * mult;
|
|
// Use the converters to calculate the gradients in quaternion space.
|
|
// The zeroth gradient is never used, so don't waste time calculating
|
|
// it but leave the code here for completeness and possible future use.
|
|
// gradients[0] +=
|
|
// r * (dx * gradConverters[k + 0] + dy *
|
|
// gradConverters[k + 1] +
|
|
// dz * gradConverters[k + 2]);
|
|
gradients[1] +=
|
|
r * (dx * gradConverters[k + 3] + dy * gradConverters[k + 4] +
|
|
dz * gradConverters[k + 5]);
|
|
gradients[2] +=
|
|
r * (dx * gradConverters[k + 6] + dy * gradConverters[k + 7] +
|
|
dz * gradConverters[k + 8]);
|
|
gradients[3] +=
|
|
r * (dx * gradConverters[k + 9] + dy * gradConverters[k + 10] +
|
|
dz * gradConverters[k + 11]);
|
|
gradients[4] += r * dx;
|
|
gradients[5] += r * dy;
|
|
gradients[6] += r * dz;
|
|
}
|
|
}
|
|
}
|
|
return vol;
|
|
}
|
|
|
|
void SingleConformerAlignment::applyQuatTrans(
|
|
const std::array<double, 7> &quatTrans) {
|
|
// Leave fit at the origin, and move ref to meet it.
|
|
RDGeom::Point3D const translateA{-quatTrans[4], -quatTrans[5], -quatTrans[6]};
|
|
translateShape(d_ref.data(), d_refTemp.data(), d_nRefShape + d_nRefColor,
|
|
translateA);
|
|
// Rotate fit by quaternion
|
|
// double tq[4]{quatTrans[0], quatTrans[1], quatTrans[2], quatTrans[3]};
|
|
RDGeom::Transform3D transformB;
|
|
transformB.SetRotationFromQuaternion(quatTrans.data());
|
|
applyTransformToShape(d_fit.data(), d_fitTemp.data(),
|
|
d_nFitShape + d_nFitColor, transformB);
|
|
}
|
|
|
|
void SingleConformerAlignment::calcVolumeAndGradients(
|
|
const std::array<double, 7> &quatTrans, double &shapeOvlpVol,
|
|
double &colorOvlpVol, std::array<double, 7> &gradients) {
|
|
// Set the coords up.
|
|
applyQuatTrans(quatTrans);
|
|
// We assume that d_refTemp was once initialised to d_ref and the same with
|
|
// fit so that the radii are already there.
|
|
gradients[0] = gradients[1] = gradients[2] = gradients[3] = gradients[4] =
|
|
gradients[5] = gradients[6] = 0.0;
|
|
shapeOvlpVol = calcVolAndGrads(
|
|
d_refTemp.data(), d_nRefShape, d_refCarbonRadii, d_fitTemp.data(),
|
|
d_nFitShape, d_fitCarbonRadii, d_gradConverters, d_useCutoff,
|
|
d_distCutoff2, quatTrans.data(), gradients.data());
|
|
if (d_optimMode == OptimMode::SHAPE_PLUS_COLOR) {
|
|
std::array<double, 7> colorGrads{0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0};
|
|
colorOvlpVol = calcVolAndGrads(
|
|
d_refTemp.data() + 4 * d_nRefShape, d_nRefColor,
|
|
d_refTypes + d_nRefShape, d_fitTemp.data() + 4 * d_nFitShape,
|
|
d_nFitColor, d_fitTypes + d_nFitShape, d_nFitShape, d_gradConverters,
|
|
d_useCutoff, d_distCutoff2, quatTrans.data(), colorGrads.data());
|
|
// The color gradients are normally dwarfed by the shape gradients, so
|
|
// normalize them and then mix by the same rule as the final score.
|
|
const auto shapeSum = sqrt(std::accumulate(
|
|
gradients.begin() + 1, gradients.end(), 0.0,
|
|
[](const auto init, const auto g) -> double { return init + g * g; }));
|
|
const auto colorSum = sqrt(std::accumulate(
|
|
colorGrads.begin() + 1, colorGrads.end(), 0.0,
|
|
[](const auto init, const auto g) -> double { return init + g * g; }));
|
|
const auto ratio = shapeSum / colorSum;
|
|
std::transform(
|
|
gradients.begin() + 1, gradients.end(), colorGrads.begin(),
|
|
gradients.begin() + 1, [&](const auto g1, const auto g2) -> double {
|
|
return g1 * (1 - d_mixingParam) + g2 * ratio * d_mixingParam;
|
|
});
|
|
} else {
|
|
colorOvlpVol = 0.0;
|
|
}
|
|
}
|
|
|
|
bool SingleConformerAlignment::doOverlay(std::array<double, 20> &scores,
|
|
const unsigned int cycle) {
|
|
const unsigned int maxIters = cycle == 0 ? 10 : d_maxIts - 10;
|
|
const auto res = optimise(maxIters);
|
|
|
|
// Get the final coords for fit into d_fitTemp, and compute the scores
|
|
RDGeom::Transform3D xform;
|
|
xform.SetRotationFromQuaternion(d_quatTrans.data());
|
|
xform.SetTranslation(
|
|
RDGeom::Point3D{d_quatTrans[4], d_quatTrans[5], d_quatTrans[6]});
|
|
applyTransformToShape(d_fit.data(), d_fitTemp.data(),
|
|
d_nFitShape + d_nFitColor, xform);
|
|
|
|
const auto tscores = calcScores(d_ref.data(), d_fitTemp.data(), true);
|
|
scores[0] = tscores[0];
|
|
scores[1] = tscores[1];
|
|
scores[2] = tscores[2];
|
|
scores[3] = tscores[3];
|
|
scores[4] = tscores[4];
|
|
scores[5] = d_refShapeVol;
|
|
scores[6] = d_refColorVol;
|
|
scores[7] = d_fitShapeVol;
|
|
scores[8] = d_fitColorVol;
|
|
scores[9] = d_quatTrans[0];
|
|
scores[10] = d_quatTrans[1];
|
|
scores[11] = d_quatTrans[2];
|
|
scores[12] = d_quatTrans[3];
|
|
scores[13] = d_quatTrans[4];
|
|
scores[14] = d_quatTrans[5];
|
|
scores[15] = d_quatTrans[6];
|
|
scores[16] = 0.0;
|
|
scores[17] = 0.0;
|
|
scores[18] = 0.0;
|
|
scores[19] = 0.0;
|
|
return res;
|
|
}
|
|
|
|
namespace {
|
|
double oneStep(const double grad, const double stepSize, const double quatTrans,
|
|
const double oldGrad, const double oldQuatTrans) {
|
|
double step = 0.0;
|
|
if (std::signbit(grad) != std::signbit(oldGrad)) {
|
|
step = (quatTrans * fabs(oldGrad) + oldQuatTrans * fabs(grad)) /
|
|
(fabs(oldGrad) + fabs(grad) + fabs(grad)) -
|
|
quatTrans;
|
|
const double newStep = stepSize * grad;
|
|
if (fabs(step) > fabs(newStep)) {
|
|
// This is definitely what the PubChem code says! I read it as keeping
|
|
// the sign of step, but the value of newStep.
|
|
step *= fabs(newStep / step);
|
|
}
|
|
} else {
|
|
step = stepSize * grad;
|
|
}
|
|
return step;
|
|
}
|
|
|
|
void calcStep(const std::array<double, 7> &grad, const double qStepSize,
|
|
const double tStepSize, const std::array<double, 7> &oldGrad,
|
|
const std::array<double, 7> &quatTrans,
|
|
const std::array<double, 7> &oldQuatTrans,
|
|
const unsigned int iter, std::array<double, 7> &step) {
|
|
step[0] = 0.0;
|
|
if (iter == 0) {
|
|
// 1st iteration, use default step sizes
|
|
step[1] = qStepSize * grad[1];
|
|
step[2] = qStepSize * grad[2];
|
|
step[3] = qStepSize * grad[3];
|
|
step[4] = tStepSize * grad[4];
|
|
step[5] = tStepSize * grad[5];
|
|
step[6] = tStepSize * grad[6];
|
|
} else {
|
|
step[1] =
|
|
oneStep(grad[1], qStepSize, quatTrans[1], oldGrad[1], oldQuatTrans[1]);
|
|
step[2] =
|
|
oneStep(grad[2], qStepSize, quatTrans[2], oldGrad[2], oldQuatTrans[2]);
|
|
step[3] =
|
|
oneStep(grad[3], qStepSize, quatTrans[3], oldGrad[3], oldQuatTrans[3]);
|
|
step[4] =
|
|
oneStep(grad[4], tStepSize, quatTrans[4], oldGrad[4], oldQuatTrans[4]);
|
|
step[5] =
|
|
oneStep(grad[5], tStepSize, quatTrans[5], oldGrad[5], oldQuatTrans[5]);
|
|
step[6] =
|
|
oneStep(grad[6], tStepSize, quatTrans[6], oldGrad[6], oldQuatTrans[6]);
|
|
}
|
|
}
|
|
|
|
double constrainStep(const double maxStep, double *step, const bool checkSize) {
|
|
const double mStep = std::max({fabs(step[0]), fabs(step[1]), fabs(step[2])});
|
|
if (mStep > maxStep) {
|
|
const double scaleFactor = maxStep / mStep;
|
|
if (fabs(step[0]) > maxStep) {
|
|
step[0] *= scaleFactor;
|
|
}
|
|
if (fabs(step[1]) > maxStep) {
|
|
step[1] *= scaleFactor;
|
|
}
|
|
if (fabs(step[2]) > maxStep) {
|
|
step[2] *= scaleFactor;
|
|
}
|
|
}
|
|
if (checkSize) {
|
|
const double quatSquared =
|
|
step[0] * step[0] + step[1] * step[1] + step[2] * step[2];
|
|
if (quatSquared > 1.0) {
|
|
const double scaleFactor = 1.0 / (2.0 * quatSquared);
|
|
step[0] *= scaleFactor;
|
|
step[1] *= scaleFactor;
|
|
step[2] *= scaleFactor;
|
|
}
|
|
}
|
|
return mStep;
|
|
}
|
|
|
|
std::array<double, 7> combineQuatTrans(const std::array<double, 7> &q1,
|
|
const std::array<double, 7> &q2) {
|
|
std::array<double, 7> res;
|
|
// Multiply the quaternions, which are assumed to be normalised.
|
|
res[0] = q1[0] * q2[0] - q1[1] * q2[1] - q1[2] * q2[2] - q1[3] * q2[3];
|
|
res[1] = q1[0] * q2[1] + q1[1] * q2[0] + q1[2] * q2[3] - q1[3] * q2[2];
|
|
res[2] = q1[0] * q2[2] - q1[1] * q2[3] + q1[2] * q2[0] + q1[3] * q2[1];
|
|
res[3] = q1[0] * q2[3] + q1[1] * q2[2] - q1[2] * q2[1] + q1[3] * q2[0];
|
|
|
|
// Add the translations
|
|
res[4] = q1[4] + q2[4];
|
|
res[5] = q1[5] + q2[5];
|
|
res[6] = q1[6] + q2[6];
|
|
|
|
return res;
|
|
}
|
|
|
|
double oneReduceStep(const double grad, const double oldGrad,
|
|
const double quatTrans, const double oldQuatTrans,
|
|
const double stepSize, double step) {
|
|
if (std::signbit(grad) != std::signbit(oldGrad)) {
|
|
step = (quatTrans * fabs(oldGrad) + oldQuatTrans * fabs(grad)) /
|
|
(fabs(oldGrad) + fabs(grad)) -
|
|
quatTrans;
|
|
const double newStep = stepSize * grad;
|
|
if (fabs(step) > fabs(newStep)) {
|
|
step *= fabs(newStep / step);
|
|
}
|
|
} else if (fabs(grad) <= 1.0) {
|
|
step = stepSize * grad;
|
|
} else if (fabs(grad) > fabs(oldGrad)) {
|
|
// Going wrong way relative to other components?
|
|
step += stepSize * grad;
|
|
} else {
|
|
double delta = grad * (step / (oldGrad - grad));
|
|
if (fabs(delta) > fabs(step * 0.1) && fabs(delta) > 0.001) {
|
|
delta *= 0.0005 / fabs(delta);
|
|
}
|
|
step += delta;
|
|
}
|
|
return step;
|
|
}
|
|
|
|
void reduceStep(const std::array<double, 7> &grad,
|
|
const std::array<double, 7> &oldGrad,
|
|
const std::array<double, 7> &quatTrans,
|
|
const std::array<double, 7> &oldQuatTrans,
|
|
const unsigned int lineIter, std::array<double, 7> &step,
|
|
double &qStepSize, double &tStepSize) {
|
|
if (lineIter == 2) {
|
|
qStepSize *= 0.1;
|
|
tStepSize *= 0.1;
|
|
step[1] = qStepSize * oldGrad[1];
|
|
step[2] = qStepSize * oldGrad[2];
|
|
step[3] = qStepSize * oldGrad[3];
|
|
step[4] = tStepSize * oldGrad[4];
|
|
step[5] = tStepSize * oldGrad[5];
|
|
step[6] = tStepSize * oldGrad[6];
|
|
qStepSize *= 5.0;
|
|
tStepSize *= 5.0;
|
|
} else {
|
|
step[1] = oneReduceStep(grad[1], oldGrad[1], quatTrans[1], oldQuatTrans[1],
|
|
qStepSize, step[1]);
|
|
step[2] = oneReduceStep(grad[2], oldGrad[2], quatTrans[2], oldQuatTrans[2],
|
|
qStepSize, step[2]);
|
|
step[3] = oneReduceStep(grad[3], oldGrad[3], quatTrans[3], oldQuatTrans[3],
|
|
qStepSize, step[3]);
|
|
// The original PubChem code used qStepSize for all 6 of these updates, but
|
|
// that would appear to be a cut-and-paste error.
|
|
step[4] = oneReduceStep(grad[4], oldGrad[4], quatTrans[4], oldQuatTrans[4],
|
|
tStepSize, step[4]);
|
|
step[5] = oneReduceStep(grad[5], oldGrad[5], quatTrans[5], oldQuatTrans[5],
|
|
tStepSize, step[5]);
|
|
step[6] = oneReduceStep(grad[6], oldGrad[6], quatTrans[6], oldQuatTrans[6],
|
|
tStepSize, step[6]);
|
|
}
|
|
}
|
|
} // namespace
|
|
|
|
// The optimisation follows closely the procedure used in the PubChem
|
|
// code from
|
|
// https://github.com/ncbi/pubchem-align3d/blob/main/shape_neighbor.cpp
|
|
// Original Authors: Evan Bolton, Leonid Zaslavsky, Paul Thiessen
|
|
bool SingleConformerAlignment::optimise(unsigned int maxIters) {
|
|
const double maxQuaternionStep = 0.075; // Maximum step size for quaternion
|
|
const double maxTranslationStep = 0.500; // Maximum step size for translation
|
|
const double minQuaternionStep =
|
|
0.0002; // Convergence criteria for quaternion
|
|
const double minTranslationStep =
|
|
0.0020; // Convergence criteria for translation
|
|
|
|
std::array<double, 7> grad;
|
|
std::array<double, 7> oldGrad{0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0};
|
|
std::array<double, 7> oldQuatTrans;
|
|
std::array<double, 7> step;
|
|
double shapeOvlpVol, colorOvlpVol, comboScore = 0.0;
|
|
bool finished = false;
|
|
for (unsigned iter = 0; iter < maxIters; iter++) {
|
|
calcVolumeAndGradients(d_quatTrans, shapeOvlpVol, colorOvlpVol, grad);
|
|
|
|
// Note that the combo score will have a zero color score so will be half
|
|
// the shape score unless we're optimising with color gradients.
|
|
auto scores = calcScores(shapeOvlpVol, colorOvlpVol);
|
|
comboScore = scores[0];
|
|
calcStep(grad, d_qStepSize, d_tStepSize, oldGrad, d_quatTrans, oldQuatTrans,
|
|
iter, step);
|
|
|
|
// In case we have to backtrack
|
|
const double oldComboScore = comboScore;
|
|
oldQuatTrans = d_quatTrans;
|
|
oldGrad = grad;
|
|
|
|
// What the PubChem code calls "Line search (sort of) loop"
|
|
bool converged = false;
|
|
for (unsigned int lineIter = 0; !converged; lineIter++) {
|
|
// Check that the absolute max step size does not go beyond some
|
|
// reasonable size
|
|
const double mqStep =
|
|
constrainStep(maxQuaternionStep, step.data() + 1, true);
|
|
const double mtStep =
|
|
constrainStep(maxTranslationStep, step.data() + 4, false);
|
|
if (mqStep <= minQuaternionStep && mtStep <= minTranslationStep) {
|
|
converged = true;
|
|
comboScore = 0.0; // Make sure we return to the old one.
|
|
break;
|
|
}
|
|
// Calculate the 0th component of the quaternion. Obviously it
|
|
// relies on the other 3 components being small
|
|
const double quatSquared =
|
|
step[1] * step[1] + step[2] * step[2] + step[3] * step[3];
|
|
step[0] = sqrt(1.0 - quatSquared);
|
|
// Update the quaternion with the step, multiplying them.
|
|
auto newQuatTrans = combineQuatTrans(d_quatTrans, step);
|
|
calcVolumeAndGradients(newQuatTrans, shapeOvlpVol, colorOvlpVol, grad);
|
|
auto newScores = calcScores(shapeOvlpVol, colorOvlpVol);
|
|
comboScore = newScores[0];
|
|
// if we made a good step, keep the quaternion and we're done
|
|
if (comboScore > oldComboScore) {
|
|
d_quatTrans = newQuatTrans;
|
|
break;
|
|
}
|
|
if (lineIter > 2) {
|
|
converged = true;
|
|
d_quatTrans = newQuatTrans;
|
|
break;
|
|
}
|
|
// It got worse, so reduce the step.
|
|
reduceStep(grad, oldGrad, newQuatTrans, oldQuatTrans, lineIter, step,
|
|
d_qStepSize, d_tStepSize);
|
|
d_quatTrans = oldQuatTrans;
|
|
} // End of line search
|
|
// Did it converge?
|
|
if (converged ||
|
|
d_shapeConvergenceCriterion > (comboScore - oldComboScore)) {
|
|
if (oldComboScore > comboScore) {
|
|
// The previous step was better, so keep it.
|
|
comboScore = oldComboScore;
|
|
d_quatTrans = oldQuatTrans;
|
|
}
|
|
finished = true;
|
|
break;
|
|
}
|
|
}
|
|
return finished;
|
|
}
|
|
} // namespace GaussianShape
|
|
} // namespace RDKit
|