Files
rdkit/Code/GraphMol/GaussianShape/SingleConformerAlignment.cpp
David Cosgrove 9f551aedbe Multi conf gaussian shape (#9265)
* First import of GaussianShape.

* Tidying.

* Custom features.

* Optimise.

* Optimise.

* Return 3 scores rather than 2 including combo score.

* Rename useFeatures to useColors.

* Python wrappers.

* Python tests.

* Take out big test.

* Add new start mode, as PubChem does it.

* Doh!

* Fix MolTransforms eigenvalue return.

* Two cycle optimisation, mostly working.

* Take out bestSoFar score from SCA.

* Take out DTYPE.

* Tidy out redundant variables.

* Optimisation in 2 parts.

* More fiddling in pursuit of speed.

* Update Python wrapper.

* Tweak.

* Atom subsets and different radii.

* Fix test.

* Revert pubchem_shape's test.cpp.

* Serialize ShapeInput.

* Trigger build

* Remove pointers to std::arrays in ShapeInput.

* ShapeInput virtual d'tor.

* Precondition - ShapeInput needs a molecule with at least 1 conformer.

* Rename ShapeInput::d_centroid to ShapeInput::d_canonTrans.

* Fix normalization bugs.

* Select start mode using moments of inertia rather than eigenvalues of canonical transformation.

* Include color features in moments of inertia.

* Smidge faster.

* Tversky similarity.

* Tidy tests.

* Tests working on Linux.

* Revert force of right handed axes in MolTransforms::computePrincipalAxesAndMomentsFromGyrationMatrix replacing with a comment in the code.

* Response to review.

* Sneaky allCarbon bug.

* add multithreaded test

* Response to review.

* Doh! Don't recalculate normalization after every transformation.

* Re-instate d_normalizationOK.

* Re-name functions for fetching canonical transformations.

* Separate alpha from coords.

* MultiConf works with single conf extraction.

* Extract all conformations.
Max and best similarities.

* Renames d_currConformer to d_activeShape.

* Update shapeToMol.

* Update shapeToMol.

* Changes from synthon shape searching.

* Fix normalization of multiple confs.

* Update Python wrappers.

* Fix shape merge.

* Improve bestSimilarity.

* Fix python wrapper.

* Pull in changes from SynthonShapeSearch:
make pruneShapes public.
function to negate Alpha values.

* clang-tidy suggestions.

* clang-tidy suggestions.

* Bug in quaternion gradients - we now have only 3 coordinates.

* Tidy tests.

* Mac result slightly different.

* Multi conformer molecule alignment.

* Optionally return raw overlap volumes in score functions.

* Python wrappers for raw overlap volumes.

* Update Python wrapper ShapeInputOptions.

* Tidy for PR.

* Extra include file.

* Extra library

* Tidy forward declarations.

* Don't prune if threshold < 0.0.

* Windows exporty thing.

* Check SMILES on merge of ShapeInputs.

* PRECONDITION of SMILES on merge of ShapeInputs.

* Response to review - rename some functions.

* change how overlapVols is passed
add a test for it

* API suggestions

* Response to review.

* Remove debugging writes.

* Fix Python wrappers.

---------

Co-authored-by: David Cosgrove <david@cozchemix.co.uk>
Co-authored-by: greg landrum <greg.landrum@gmail.com>
2026-06-03 06:09:09 +02:00

660 lines
26 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>
namespace RDKit {
namespace GaussianShape {
SingleConformerAlignment::SingleConformerAlignment(
const std::vector<double> &ref, const std::vector<double> &refAlphas,
const int *refTypes, const boost::dynamic_bitset<> *refCarbonRadii,
int nRefShape, int nRefColor, double refShapeVol, double refColorVol,
const std::vector<double> &fit, const std::vector<double> &fitAlphas,
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_refAlphas(refAlphas),
d_refTypes(refTypes),
d_refCarbonRadii(refCarbonRadii),
d_nRefShape(nRefShape),
d_nRefColor(nRefColor),
d_refShapeVol(refShapeVol),
d_refColorVol(refColorVol),
d_fit(fit),
d_fitAlphas(fitAlphas),
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_refAlphas.data(), d_nRefShape,
d_refCarbonRadii, fit, d_fitAlphas.data(),
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 * 3, d_refAlphas.data() + d_nRefShape, d_nRefColor,
d_refTypes + d_nRefShape, fit + d_nFitShape * 3,
d_fitAlphas.data() + d_nFitShape, 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 < 3 * numBPts;
i += 3, ++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 double *refAlphas,
const int numRefPts,
const boost::dynamic_bitset<> *refCarbonRadii,
const double *fit, const double *fitAlphas,
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 * 3; i += 3, i_idx++) {
const auto ai = refAlphas[i_idx];
if (ai < 0.0) {
continue;
}
for (int j = 0, j_idx = 0, k = 0; j < numFitPts * 3;
j += 3, 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 = fitAlphas[j_idx];
if (aj < 0.0) {
continue;
}
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 double *refAlphas,
const int numRefPts, const int *refTypes,
const double *fit, const double *fitAlphas,
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 * 3; i += 3, i_idx++) {
const auto ai = refAlphas[i_idx];
const auto aType = refTypes[i_idx];
for (int j = 0, j_idx = 0, k = 0; j < numFitPts * 3;
j += 3, 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 = fitAlphas[j_idx];
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_refAlphas.data(), d_nRefShape, d_refCarbonRadii,
d_fitTemp.data(), d_fitAlphas.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() + 3 * d_nRefShape, d_refAlphas.data() + d_nRefShape,
d_nRefColor, d_refTypes + d_nRefShape,
d_fitTemp.data() + 3 * d_nFitShape, d_fitAlphas.data() + 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