RDKit
Open-source cheminformatics and machine learning.
Loading...
Searching...
No Matches
BFGSOpt.h
Go to the documentation of this file.
1//
2// Copyright (C) 2004-2008 Greg Landrum and Rational Discovery LLC
3//
4// @@ All Rights Reserved @@
5// This file is part of the RDKit.
6// The contents are covered by the terms of the BSD license
7// which is included in the file license.txt, found at the root
8// of the RDKit source tree.
9//
10#ifndef RD_BFGSOPT_H
11#define RD_BFGSOPT_H
12
13#include <RDGeneral/export.h>
14#include <cmath>
15#include <RDGeneral/Invariant.h>
17#include <cstring>
18#include <vector>
19#include <algorithm>
20#include "BFGSOpt_SVE.h"
21
22namespace BFGSOpt {
25const double FUNCTOL =
26 1e-4; //!< Default tolerance for function convergence in the minimizer
27const double MOVETOL =
28 1e-7; //!< Default tolerance for x changes in the minimizer
29const int MAXITS = 200; //!< Default maximum number of iterations
30const double EPS = 3e-8; //!< Default gradient tolerance in the minimizer
31const double TOLX =
32 4. * EPS; //!< Default direction vector tolerance in the minimizer
33const double MAXSTEP = 100.0; //!< Default maximum step size in the minimizer
34
35/*!
36 See Numerical Recipes in C, Section 9.7 for a description of the algorithm.
37
38 \param dim the dimensionality of the space.
39 \param oldPt the current position, as an array.
40 \param oldVal the current function value.
41 \param grad the value of the function gradient at oldPt
42 \param dir the minimization direction
43 \param newPt used to return the final position
44 \param newVal used to return the final function value
45 \param func the function to minimize
46 \param maxStep the maximum allowable step size
47 \param resCode used to return the results of the search.
48
49 Possible values for resCode are on return are:
50 - 0: success
51 - 1: the stepsize got too small. This probably indicates success.
52 - -1: the direction is bad (orthogonal to the gradient)
53*/
54template <typename EnergyFunctor>
55void linearSearch(unsigned int dim, double *oldPt, double oldVal, double *grad,
56 double *dir, double *newPt, double &newVal,
57 EnergyFunctor func, double maxStep, int &resCode) {
58 PRECONDITION(oldPt, "bad input array");
59 PRECONDITION(grad, "bad input array");
60 PRECONDITION(dir, "bad input array");
61 PRECONDITION(newPt, "bad input array");
62
63 const unsigned int MAX_ITER_LINEAR_SEARCH = 1000;
64 double sum = 0.0, slope = 0.0, test = 0.0, lambda = 0.0;
65 double lambda2 = 0.0, lambdaMin = 0.0, tmpLambda = 0.0, val2 = 0.0;
66
67 resCode = -1;
68
69 // get the length of the direction vector:
70 sum = 0.0;
71 for (unsigned int i = 0; i < dim; i++) {
72 sum += dir[i] * dir[i];
73 }
74 sum = sqrt(sum);
75
76 // Rescale if we're trying to move too far
77 if (sum > maxStep) {
78 for (unsigned int i = 0; i < dim; i++) {
79 dir[i] *= maxStep / sum;
80 }
81 }
82
83 // make sure our direction has at least some component along
84 // -grad
85 slope = 0.0;
86 for (unsigned int i = 0; i < dim; i++) {
87 slope += dir[i] * grad[i];
88 }
89 if (slope >= 0.0) {
90 return;
91 }
92
93 test = 0.0;
94 for (unsigned int i = 0; i < dim; i++) {
95 double temp = fabs(dir[i]) / std::max(fabs(oldPt[i]), 1.0);
96 if (temp > test) {
97 test = temp;
98 }
99 }
100
101 lambdaMin = MOVETOL / test;
102 lambda = 1.0;
103 unsigned int it = 0;
104 while (it < MAX_ITER_LINEAR_SEARCH) {
105 if (lambda < lambdaMin) {
106 // Step size is below the position-scaled threshold; treat as converged
107 resCode = 1;
108 break;
109 }
110 for (unsigned int i = 0; i < dim; i++) {
111 newPt[i] = oldPt[i] + lambda * dir[i];
112 }
113 newVal = func(newPt);
114 if (newVal - oldVal <= FUNCTOL * lambda * slope) {
115 // Armijo sufficient-decrease condition satisfied; accept the step
116 resCode = 0;
117 return;
118 }
119 // if we made it this far, we need to backtrack:
120 if (it == 0) {
121 // Quadratic model: only one prior function value available
122 tmpLambda = -slope / (2.0 * (newVal - oldVal - slope));
123 } else {
124 double rhs1 = newVal - oldVal - lambda * slope;
125 double rhs2 = val2 - oldVal - lambda2 * slope;
126 double a = (rhs1 / (lambda * lambda) - rhs2 / (lambda2 * lambda2)) /
127 (lambda - lambda2);
128 double b = (-lambda2 * rhs1 / (lambda * lambda) +
129 lambda * rhs2 / (lambda2 * lambda2)) /
130 (lambda - lambda2);
131 if (a == 0.0) {
132 tmpLambda = -slope / (2.0 * b);
133 } else {
134 double disc = b * b - 3 * a * slope;
135 if (disc < 0.0) {
136 tmpLambda = 0.5 * lambda;
137 } else if (b <= 0.0) {
138 tmpLambda = (-b + sqrt(disc)) / (3.0 * a);
139 } else {
140 tmpLambda = -slope / (b + sqrt(disc));
141 }
142 }
143 if (tmpLambda > 0.5 * lambda) {
144 tmpLambda = 0.5 * lambda;
145 }
146 }
147 lambda2 = lambda;
148 val2 = newVal;
149 lambda = std::max(tmpLambda, 0.1 * lambda);
150 ++it;
151 }
152 // nothing was done
153 for (unsigned int i = 0; i < dim; i++) {
154 newPt[i] = oldPt[i];
155 }
156}
157
158//! Do a BFGS minimization of a function.
159/*!
160 See Numerical Recipes in C, Section 10.7 for a description of the algorithm.
161
162 \param dim the dimensionality of the space.
163 \param pos the starting position, as an array.
164 \param gradTol tolerance for gradient convergence
165 \param numIters used to return the number of iterations required
166 \param funcVal used to return the final function value
167 \param func the function to minimize
168 \param gradFunc calculates the gradient of func
169 \param funcTol tolerance for changes in the function value for convergence.
170 \param maxIts maximum number of iterations allowed
171 \param snapshotFreq a snapshot of the minimization trajectory
172 will be stored after as many steps as indicated
173 through this parameter; defaults to 0 (no
174 snapshots stored)
175 \param snapshotVect pointer to a std::vector<Snapshot> object that will
176 receive the coordinates and energies every snapshotFreq steps; defaults to
177 NULL (no snapshots stored)
178
179 \return a flag indicating success (or type of failure). Possible values are:
180 - 0: success
181 - 1: too many iterations were required
182*/
183template <typename EnergyFunctor, typename GradientFunctor>
184int minimize(unsigned int dim, double *pos, double gradTol,
185 unsigned int &numIters, double &funcVal, EnergyFunctor func,
186 GradientFunctor gradFunc, unsigned int snapshotFreq,
187 RDKit::SnapshotVect *snapshotVect, double funcTol = TOLX,
188 unsigned int maxIts = MAXITS) {
189 RDUNUSED_PARAM(funcTol);
190 PRECONDITION(pos, "bad input array");
191 PRECONDITION(gradTol > 0, "bad tolerance");
192
193 std::vector<double> grad(dim);
194 std::vector<double> dGrad(dim);
195 std::vector<double> hessDGrad(dim);
196 std::vector<double> xi(dim);
197 std::vector<double> invHessian(dim * dim, 0);
198 std::unique_ptr<double[]> newPos(new double[dim]);
199 snapshotFreq = std::min(snapshotFreq, maxIts);
200
201 double fp = func(pos);
202 gradFunc(pos, grad.data());
203
204 double sum = 0.0;
205#ifdef RDK_SVE_AVAILABLE
206 if (cpuHasSVE()) {
207 // SVE path: initialise xi = -grad and compute ||pos||^2 in a single
208 // vectorised pass. The identity inverse Hessian is initialised separately
209 // (scalar, O(dim)) since it is a simple diagonal write and does not benefit
210 // from vectorisation over rows.
211 sveInitXiAndSum(dim, grad.data(), xi.data(), pos, &sum);
212 for (unsigned int i = 0; i < dim; i++) invHessian[i * dim + i] = 1.0;
213 } else
214#endif
215 {
216 // Scalar path: initialise the inverse Hessian to the identity matrix,
217 // set the initial search direction xi = -grad (steepest descent step),
218 // and accumulate ||pos||^2 to set an appropriate maximum step size.
219 for (unsigned int i = 0; i < dim; i++) {
220 unsigned int itab = i * dim;
221 invHessian[itab + i] = 1.0;
222 xi[i] = -grad[i];
223 sum += pos[i] * pos[i];
224 }
225 }
226 double maxStep = MAXSTEP * std::max(sqrt(sum), static_cast<double>(dim));
227
228 for (unsigned int iter = 1; iter <= maxIts; ++iter) {
229 numIters = iter;
230 int status = -1;
231
232 linearSearch(dim, pos, fp, grad.data(), xi.data(), newPos.get(), funcVal,
233 func, maxStep, status);
234 CHECK_INVARIANT(status >= 0, "bad direction in linearSearch");
235
236 // save the function value for the next search:
237 fp = funcVal;
238 // set the direction of this line and save the gradient:
239 double test = 0.0;
240 for (unsigned int i = 0; i < dim; i++) {
241 xi[i] = newPos[i] - pos[i];
242 pos[i] = newPos[i];
243 double temp = fabs(xi[i]) / std::max(fabs(pos[i]), 1.0);
244 if (temp > test) {
245 test = temp;
246 }
247 dGrad[i] = grad[i];
248 }
249 if (test < TOLX) {
250 if (snapshotVect && snapshotFreq) {
251 RDKit::Snapshot s(boost::shared_array<double>(newPos.release()), fp);
252 snapshotVect->push_back(s);
253 }
254 return 0;
255 }
256
257 // update the gradient:
258 double gradScale = gradFunc(pos, grad.data());
259
260 test = 0.0;
261 // Use |funcVal| so that negative energies (which arise routinely
262 // mid-minimization in force fields containing stabilizing
263 // electrostatic or dispersion terms) do not drive
264 // funcVal * gradScale below zero and clamp the denominator to 1.0,
265 // which would artificially tighten the gradient convergence test.
266 double term = std::max(fabs(funcVal) * gradScale, 1.0);
267 for (unsigned int i = 0; i < dim; i++) {
268 double temp = fabs(grad[i]) * std::max(fabs(pos[i]), 1.0);
269 test = std::max(test, temp);
270 dGrad[i] = grad[i] - dGrad[i];
271 }
272 test /= term;
273 if (test < gradTol) {
274 if (snapshotVect && snapshotFreq) {
275 RDKit::Snapshot s(boost::shared_array<double>(newPos.release()), fp);
276 snapshotVect->push_back(s);
277 }
278 return 0;
279 }
280
281 // BFGS inverse Hessian update.
282 double fac = 0, fae = 0, sumDGrad = 0, sumXi = 0;
283#ifdef RDK_SVE_AVAILABLE
284 if (cpuHasSVE()) {
285 // SVE path: matrix-vector multiply and all four dot products computed in
286 // one vectorised pass, saving two additional O(dim) traversals compared
287 // to separate scalar dot-product calls.
288 sveHessianVecMul(dim, invHessian.data(), dGrad.data(), hessDGrad.data(),
289 xi.data(), &fac, &fae, &sumDGrad, &sumXi);
290 } else
291#endif
292 {
293 // Scalar path: fused matrix-vector multiply and dot-product accumulation.
294 // Pointer arithmetic (++ivh, ++dgj) avoids repeated index computations
295 // and helps the compiler generate efficient load sequences.
296 for (unsigned int i = 0; i < dim; i++) {
297 double *ivh = &(invHessian[i * dim]);
298 double &hdgradi = hessDGrad[i];
299 double *dgj = dGrad.data();
300 hdgradi = 0.0;
301 for (unsigned int j = 0; j < dim; ++j, ++ivh, ++dgj) {
302 hdgradi += *ivh * *dgj;
303 }
304 fac += dGrad[i] * xi[i];
305 fae += dGrad[i] * hessDGrad[i];
306 sumDGrad += dGrad[i] * dGrad[i];
307 sumXi += xi[i] * xi[i];
308 }
309 }
310 if (fac > sqrt(EPS * sumDGrad * sumXi)) {
311 fac = 1.0 / fac;
312 double fad = 1.0 / fae;
313 for (unsigned int i = 0; i < dim; i++) {
314 dGrad[i] = fac * xi[i] - fad * hessDGrad[i];
315 }
316
317#ifdef RDK_SVE_AVAILABLE
318 if (cpuHasSVE()) {
319 // SVE path: symmetric rank-1 update with FMA, exploiting symmetry to
320 // halve memory writes and FLOPs versus a full-matrix update
321 sveHessianRank1Update(dim, invHessian.data(), xi.data(),
322 hessDGrad.data(), dGrad.data(), fac, fad, fae);
323 } else
324#endif
325 {
326 // Scalar path: upper-triangle-only update (j >= i) followed by
327 // explicit symmetrisation. This halves the number of Hessian writes
328 // at the cost of one additional pass over a row to mirror elements.
329 for (unsigned int i = 0; i < dim; i++) {
330 unsigned int itab = i * dim;
331 double pxi = fac * xi[i], hdgi = fad * hessDGrad[i],
332 dgi = fae * dGrad[i];
333 double *pxj = &(xi[i]), *hdgj = &(hessDGrad[i]), *dgj = &(dGrad[i]);
334 for (unsigned int j = i; j < dim; ++j, ++pxj, ++hdgj, ++dgj) {
335 invHessian[itab + j] += pxi * *pxj - hdgi * *hdgj + dgi * *dgj;
336 invHessian[j * dim + i] = invHessian[itab + j];
337 }
338 }
339 }
340 }
341
342#ifdef RDK_SVE_AVAILABLE
343 if (cpuHasSVE()) {
344 sveHessianVecMulNeg(dim, invHessian.data(), grad.data(), xi.data());
345 } else
346#endif
347 {
348 for (unsigned int i = 0; i < dim; i++) {
349 unsigned int itab = i * dim;
350 xi[i] = 0.0;
351 double &pxi = xi[i];
352 double *ivh = &(invHessian[itab]);
353 double *gj = grad.data();
354 for (unsigned int j = 0; j < dim; ++j, ++ivh, ++gj) {
355 pxi -= *ivh * *gj;
356 }
357 }
358 }
359 if (snapshotVect && snapshotFreq && !(iter % snapshotFreq)) {
360 RDKit::Snapshot s(boost::shared_array<double>(newPos.release()), fp);
361 snapshotVect->push_back(s);
362 newPos.reset(new double[dim]);
363 }
364 }
365 return 1;
366}
367
368//! Do a BFGS minimization of a function.
369/*!
370 \param dim the dimensionality of the space.
371 \param pos the starting position, as an array.
372 \param gradTol tolerance for gradient convergence
373 \param numIters used to return the number of iterations required
374 \param funcVal used to return the final function value
375 \param func the function to minimize
376 \param gradFunc calculates the gradient of func
377 \param funcTol tolerance for changes in the function value for convergence.
378 \param maxIts maximum number of iterations allowed
379
380 \return a flag indicating success (or type of failure). Possible values are:
381 - 0: success
382 - 1: too many iterations were required
383*/
384template <typename EnergyFunctor, typename GradientFunctor>
385int minimize(unsigned int dim, double *pos, double gradTol,
386 unsigned int &numIters, double &funcVal, EnergyFunctor func,
387 GradientFunctor gradFunc, double funcTol = TOLX,
388 unsigned int maxIts = MAXITS) {
389 return minimize(dim, pos, gradTol, numIters, funcVal, func, gradFunc, 0,
390 nullptr, funcTol, maxIts);
391}
392
393} // namespace BFGSOpt
394#endif // RD_BFGSOPT_H
#define CHECK_INVARIANT(expr, mess)
Definition Invariant.h:100
#define RDUNUSED_PARAM(x)
Definition Invariant.h:196
#define PRECONDITION(expr, mess)
Definition Invariant.h:108
#define RDKIT_OPTIMIZER_EXPORT
Definition export.h:401
const double EPS
Default gradient tolerance in the minimizer.
Definition BFGSOpt.h:30
const double TOLX
Default direction vector tolerance in the minimizer.
Definition BFGSOpt.h:31
void linearSearch(unsigned int dim, double *oldPt, double oldVal, double *grad, double *dir, double *newPt, double &newVal, EnergyFunctor func, double maxStep, int &resCode)
Definition BFGSOpt.h:55
static bool cpuHasSVE()
Definition BFGSOpt_SVE.h:16
const double FUNCTOL
Default tolerance for function convergence in the minimizer.
Definition BFGSOpt.h:25
int minimize(unsigned int dim, double *pos, double gradTol, unsigned int &numIters, double &funcVal, EnergyFunctor func, GradientFunctor gradFunc, unsigned int snapshotFreq, RDKit::SnapshotVect *snapshotVect, double funcTol=TOLX, unsigned int maxIts=MAXITS)
Do a BFGS minimization of a function.
Definition BFGSOpt.h:184
const double MAXSTEP
Default maximum step size in the minimizer.
Definition BFGSOpt.h:33
RDKIT_OPTIMIZER_EXPORT int REALLY_A_HEADER_ONLY_LIBRARY
const double MOVETOL
Default tolerance for x changes in the minimizer.
Definition BFGSOpt.h:27
const int MAXITS
Default maximum number of iterations.
Definition BFGSOpt.h:29
RDKIT_OPTIMIZER_EXPORT int HEAD_ONLY_LIBRARY
std::vector< Snapshot > SnapshotVect
Definition Snapshot.h:21