28constexpr double kLbfgsEps = 1e-30;
30void maybeProjectRigid(Eigen::VectorXd &vec,
const Eigen::VectorXd &pos,
32 if (!enabled || pos.size() < 6 || pos.size() % 3 != 0) {
35 const long nat = pos.size() / 3;
36 const AtomMatrix coords = Eigen::Map<const AtomMatrix>(pos.data(), nat, 3);
40std::vector<int> twoLoopIndex(
int nPairs,
int extra) {
42 idx.reserve(
static_cast<size_t>(nPairs + std::max(extra, 0)));
43 for (
int i = 0; i < nPairs; ++i) {
46 for (
int k = 0; k < extra && nPairs > 0; ++k) {
47 idx.push_back(nPairs - 1);
55Eigen::Vector3d
LBFGS::micRij(
const Eigen::VectorXd &pos,
int i,
int j)
const {
56 Eigen::Vector3d dr = pos.segment<3>(3 * i) - pos.segment<3>(3 * j);
62 const std::string &p =
m_optConfig.opts.lbfgs.precon;
63 return p ==
"exp" || p ==
"c1" || p ==
"lindh" || p ==
"lindh_full" ||
64 p ==
"pair" || p ==
"pair_abs" || p ==
"pair_full" || p ==
"fischer" ||
65 p ==
"schlegel" || p ==
"swart";
71 cfg.precon_mu, cfg.precon_rcut, *
m_objf);
75 const Eigen::VectorXd &pos)
const {
77 if (
usesPrecon() && pos.size() >= 6 && pos.size() % 3 == 0) {
81 Eigen::PartialPivLU<Eigen::MatrixXd> lu(P);
82 const Eigen::VectorXd z = lu.solve(q);
86 QUILL_LOG_DEBUG(
m_log,
"[LBFGS] Packwood P failed to factor, using H0 I");
92 const Eigen::VectorXd &a_f) {
93 Eigen::VectorXd r =
m_objf->getPositions();
94 Eigen::VectorXd f = a_f;
97 m_optConfig.opts.lbfgs.inverse_curvature * f, a_maxMove);
99 maybeProjectRigid(f, r,
m_optConfig.opts.lbfgs.project_rigid);
100 const Eigen::VectorXd g = -f;
106 const int n =
static_cast<int>(H.rows());
107 Eigen::MatrixXd A = Eigen::MatrixXd::Zero(n + 1, n + 1);
108 A.topLeftCorner(n, n) = H;
109 A.col(n).head(n) = g;
110 A.row(n).head(n) = g.transpose();
111 Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> es(A);
112 if (es.info() != Eigen::Success) {
115 const Eigen::VectorXd v = es.eigenvectors().col(0);
116 if (std::abs(v(n)) < 1.0e-14) {
119 d = v.head(n) / v(n);
122 Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> es(H);
123 if (es.info() != Eigen::Success) {
126 Eigen::VectorXd ev = es.eigenvalues();
127 const double lmin = ev.minCoeff();
128 const double mu = (lmin < 1.0e-8) ? (1.0e-8 - lmin) : 0.0;
130 d = -es.eigenvectors() *
131 (es.eigenvectors().transpose() * g).cwiseQuotient(ev);
133 maybeProjectRigid(d, r,
m_optConfig.opts.lbfgs.project_rigid);
139 if (
step ==
"newton" ||
step ==
"rfo") {
142 double H0 =
m_optConfig.opts.lbfgs.inverse_curvature;
143 Eigen::VectorXd r =
m_objf->getPositions();
144 Eigen::VectorXd f = a_f;
145 maybeProjectRigid(f, r,
m_optConfig.opts.lbfgs.project_rigid);
149 const Eigen::VectorXd y =
m_fPrev - f;
150 const double sy = dr.dot(y);
151 const double yy = y.squaredNorm();
152 const double ss = dr.squaredNorm();
156 m_log,
"[LBFGS] Negative curvature: {:.4f} eV/A^2 take max move step",
164 if (
m_optConfig.opts.lbfgs.auto_scale && sy > 0.0 && yy > 0.0) {
165 const double g_syyy = sy / yy;
166 const double g_sssy = ss / sy;
170 }
else if (h0 ==
"adaptive") {
171 H0 = std::min(g_syyy, g_sssy);
175 if (
m_optConfig.opts.lbfgs.max_inverse_curvature > 0.0) {
176 H0 = std::min(H0,
m_optConfig.opts.lbfgs.max_inverse_curvature);
178 QUILL_LOG_DEBUG(
m_log,
"[LBFGS] H0={:.4e} ({} scale)", H0, h0);
182 const std::optional<double> known =
184 ?
m_objf->knownCurvature()
186 if (known && std::isfinite(*known) && *known > 0.0) {
188 if (
m_optConfig.opts.lbfgs.max_inverse_curvature > 0.0) {
189 H0 = std::min(H0,
m_optConfig.opts.lbfgs.max_inverse_curvature);
191 QUILL_LOG_DEBUG(
m_log,
"[LBFGS] H0={:.4e} (known curvature)", H0);
193 m_objf->supportsFiniteDifferenceCurvature()) {
195 eonc::safemath::safe_normalized(a_f));
196 Eigen::VectorXd dg =
m_objf->getGradient(
true) + a_f;
197 const double C = dg.dot(eonc::safemath::safe_normalized(a_f)) /
202 QUILL_LOG_WARNING(
m_log,
203 "[LBFGS] Negative curvature calculated via FD: {:.4e} "
204 "eV/A^2, take max move step",
209 QUILL_LOG_DEBUG(
m_log,
"[LBFGS] Curvature calculated via FD: {:.4e} eV/A^2",
214 twoLoopIndex(
static_cast<int>(
m_s.size()),
215 static_cast<int>(
m_optConfig.opts.lbfgs.extra_updates));
216 const int loopmax =
static_cast<int>(idx.size());
217 std::vector<double> a(
static_cast<size_t>(loopmax));
219 Eigen::VectorXd q = -f;
221 for (
int k = loopmax - 1; k >= 0; k--) {
222 const int i = idx[
static_cast<size_t>(k)];
223 a[
static_cast<size_t>(k)] =
224 m_rho[
static_cast<size_t>(i)] *
m_s[
static_cast<size_t>(i)].dot(q);
225 q -= a[
static_cast<size_t>(k)] *
m_y[
static_cast<size_t>(i)];
228 Eigen::VectorXd z =
applyH0(q, H0, r);
230 for (
int k = 0; k < loopmax; k++) {
231 const int i = idx[
static_cast<size_t>(k)];
233 m_rho[
static_cast<size_t>(i)] *
m_y[
static_cast<size_t>(i)].dot(z);
234 z +=
m_s[
static_cast<size_t>(i)] * (a[
static_cast<size_t>(k)] - b);
237 Eigen::VectorXd d = -z;
240 if (distance >= a_maxMove &&
m_optConfig.opts.lbfgs.distance_reset) {
241 QUILL_LOG_DEBUG(
m_log,
242 "[LBFGS] reset memory, proposed step too large: {:.4f}",
248 double vd = eonc::safemath::safe_normalized(d).dot(
249 eonc::safemath::safe_normalized(f));
255 if (angle > 90.0 &&
m_optConfig.opts.lbfgs.angle_reset) {
256 QUILL_LOG_DEBUG(
m_log,
257 "[LBFGS] reset memory, angle between LBFGS angle and "
258 "force too large: {:.4f}",
264 maybeProjectRigid(d, r,
m_optConfig.opts.lbfgs.project_rigid);
276 const Eigen::VectorXd &a_f1,
const Eigen::VectorXd &a_f0,
277 double a_e1,
double a_e0) {
278 Eigen::VectorXd s0 =
m_objf->difference(a_r1, a_r0);
281 Eigen::VectorXd y0 = a_f0 - a_f1;
282 double sy = s0.dot(y0);
284 const std::string &curv = cfg.curvature;
285 const double H0 = std::max(cfg.inverse_curvature, 1.0e-16);
286 const double ss = s0.squaredNorm();
288 if (cfg.secant ==
"zhangxu" && ss > kLbfgsEps) {
292 const double t = 6.0 * (a_e0 - a_e1) - 3.0 * (a_f0 + a_f1).dot(s0);
293 const double theta = t - sy;
294 y0 += (theta / ss) * s0;
296 QUILL_LOG_DEBUG(
m_log,
"[LBFGS] Zhang-Xu θ={:.4e} s·ŷ={:.4e}", theta, sy);
299 double sBs = ss / H0;
300 if (
usesPrecon() && a_r1.size() >= 6 && a_r1.size() % 3 == 0) {
302 sBs = s0.dot(P * s0);
305 const double gnorm = a_f1.norm();
306 const double sn = std::sqrt(ss);
307 const double yn = y0.norm();
309 if (curv ==
"cautious") {
311 const double thresh =
312 cfg.cautious_eps * ss *
313 std::pow(std::max(gnorm, 1.0e-30), cfg.cautious_alpha);
315 QUILL_LOG_DEBUG(
m_log,
"[LBFGS] Li-Fukushima skip, s·y={:.4e} < {:.4e}",
319 }
else if (std::abs(sy) < kLbfgsEps || (curv !=
"reset" && sy < 0.2 * sBs)) {
320 if (curv ==
"skip") {
321 QUILL_LOG_DEBUG(
m_log,
"[LBFGS] skip pair, s·y={:.4e}", sy);
324 if (curv ==
"damped" && sBs > sy) {
325 const double theta = std::clamp(0.8 * sBs / (sBs - sy), 0.0, 1.0);
326 const Eigen::VectorXd B0s =
327 (
usesPrecon() && a_r1.size() >= 6 && a_r1.size() % 3 == 0)
329 : Eigen::VectorXd(s0 / H0);
330 y0 = theta * y0 + (1.0 - theta) * B0s;
332 QUILL_LOG_DEBUG(
m_log,
"[LBFGS] Powell damp θ={:.3f} s·ŷ={:.4e}", theta,
334 }
else if (std::abs(sy) < kLbfgsEps) {
335 QUILL_LOG_WARNING(
m_log,
336 "[LBFGS] s0.y0 too small ({:.4e}), resetting memory",
343 if (!std::isfinite(sy)) {
344 QUILL_LOG_WARNING(
m_log,
"[LBFGS] non-finite s·y, resetting memory");
350 if (curv !=
"reset" && sy <= 1.0e-8 * sn * yn) {
351 QUILL_LOG_DEBUG(
m_log,
"[LBFGS] overlap skip, s·y={:.4e}", sy);
356 m_s.push_back(std::move(s0));
357 m_y.push_back(std::move(y0));
369 Eigen::VectorXd r =
m_objf->getPositions();
370 Eigen::VectorXd f = -
m_objf->getGradient();
371 const std::string &accept =
m_optConfig.opts.lbfgs.accept;
372 const bool energyAccept = accept ==
"energy" || accept ==
"nonmonotone";
373 const bool needE = energyAccept ||
m_optConfig.opts.lbfgs.secant ==
"zhangxu";
374 const double e0 = needE ?
m_objf->getEnergy() : 0.0;
382 Eigen::VectorXd dr =
getStep(a_maxMove, f);
384 constexpr double max_erise = 1.0e-8;
386 if (accept ==
"nonmonotone" && !
m_eHist.empty()) {
390 bool accepted =
false;
392 for (
int dec = 0; dec < 10; ++dec) {
393 m_objf->setPositions(r + alpha * dr);
394 e_acc =
m_objf->getEnergy();
395 if (e_acc - ref <= max_erise) {
406 e_acc =
m_objf->getEnergy();
413 m_objf->setPositions(r + dr);
422 return m_objf->isConverged() ? 1 : 0;
428 status =
step(a_maxMove);
432 return m_objf->isConverged() ? 1 : 0;
Eigen::Matrix< double, Eigen::Dynamic, 3, eOnStorageOrder > AtomMatrix
std::deque< double > m_rho
int update(const Eigen::VectorXd &a_r1, const Eigen::VectorXd &a_r0, const Eigen::VectorXd &a_f1, const Eigen::VectorXd &a_f0, double a_e1, double a_e0)
std::deque< Eigen::VectorXd > m_y
std::deque< double > m_eHist
std::deque< Eigen::VectorXd > m_s
int step(double a_maxMove) override
int run(size_t a_maxIterations, double a_maxMove) override
Eigen::VectorXd hessianStep(double a_maxMove, const Eigen::VectorXd &a_f)
eonc::log::FileScoped m_log
Eigen::MatrixXd buildPrecon(const Eigen::VectorXd &pos) const
Eigen::VectorXd applyH0(const Eigen::VectorXd &q, double H0, const Eigen::VectorXd &pos) const
Eigen::VectorXd getStep(double a_maxMove, const Eigen::VectorXd &a_f)
Eigen::Vector3d micRij(const Eigen::VectorXd &pos, int i, int j) const
const OptimizerConfig m_optConfig
std::shared_ptr< ObjectiveFunction > m_objf
void projectOutRotTrans(Eigen::VectorXd &step, const AtomMatrix &positions)
VectorXd maxAtomMotionAppliedV(const VectorXd v1, double maxMotion)
double maxAtomMotionV(const VectorXd v1)
Eigen::MatrixXd build(const Eigen::VectorXd &pos, const std::string &kind, PotType pot, double A, double mu, double rcut_in, ObjectiveFunction &objf)
Analytic pair or model Hessian.
constexpr double safe_div(double num, double denom, double fallback=0.0)
double safe_acos(double x)
constexpr double safe_recip(double x, double fallback=0.0)
RAII resource manager for the ARTn C library with global synchronization.