Loading...
Searching...
No Matches
XtsciOptimizer.cpp
Go to the documentation of this file.
1/*
2** This file is part of eOn.
3**
4** SPDX-License-Identifier: BSD-3-Clause
5**
6** Copyright (c) 2010--present, eOn Development Team
7** All rights reserved.
8**
9** Repo:
10** https://github.com/TheochemUI/eOn
11*/
12#include "eon/XtsciOptimizer.h"
13#include "eon/PairHessian.h"
14#include "eon/XtsciEindir.h"
15
16#include <algorithm>
17#include <stdexcept>
18#include <string>
19#include <vector>
20
21#include <xts.h>
22
23#if XTS_ABI_VERSION_MINOR < 10
24#error "xtsci-optimize ABI minor 10 or newer is required for set_periodic"
25#endif
26
27namespace {
28
29struct XtsObjectiveContext {
30 eonc::ObjectiveFunction *objective;
31 eonc::PotType pot;
32 std::string precon;
33 double precon_A;
34 double precon_mu;
35 double precon_rcut;
36 Eigen::VectorXd *cached_x;
37 eonc::xtsci_eindir::State *eindir;
38};
39
40Eigen::Map<const Eigen::VectorXd>
41map_input(const DLManagedTensorVersioned *tensor) {
42 const auto &dl = tensor->dl_tensor;
43 if (dl.ndim != 1 || dl.dtype.code != kDLFloat || dl.dtype.bits != 64 ||
44 dl.dtype.lanes != 1 || dl.device.device_type != kDLCPU ||
45 dl.shape == nullptr || dl.data == nullptr) {
46 throw std::runtime_error("xtsci objective requires a CPU f64 vector");
47 }
48 return {static_cast<const double *>(dl.data) +
49 dl.byte_offset / sizeof(double),
50 static_cast<Eigen::Index>(dl.shape[0])};
51}
52
53Eigen::Map<Eigen::VectorXd> map_output(DLManagedTensorVersioned *tensor) {
54 const auto &dl = tensor->dl_tensor;
55 if (dl.ndim != 1 || dl.dtype.code != kDLFloat || dl.dtype.bits != 64 ||
56 dl.dtype.lanes != 1 || dl.device.device_type != kDLCPU ||
57 dl.shape == nullptr || dl.data == nullptr) {
58 throw std::runtime_error("xtsci tensor requires a CPU f64 vector");
59 }
60 return {static_cast<double *>(dl.data) + dl.byte_offset / sizeof(double),
61 static_cast<Eigen::Index>(dl.shape[0])};
62}
63
64// Matter::setPositions always dirties the PES cache. Energy and
65// forces come from one potential->force() call, so eval then grad at
66// the same x must not setPositions twice.
67void set_positions_if_changed(eonc::ObjectiveFunction *obj,
68 const Eigen::VectorXd &x,
69 Eigen::VectorXd *cached) {
70 if (cached != nullptr && cached->size() == x.size() &&
71 (*cached - x).isZero(0.0)) {
72 return;
73 }
74 // First host iteration: relaxMatter already called isConverged()
75 // at this geometry. Do not dirty that cache.
76 const auto cur = obj->getPositions();
77 if (cur.size() == x.size() && (cur - x).isZero(0.0)) {
78 if (cached != nullptr) {
79 *cached = x;
80 }
81 return;
82 }
83 obj->setPositions(x);
84 if (cached != nullptr) {
85 *cached = x;
86 }
87}
88
89// One Matter::computePotential. Energy and forces share that call.
90xts_status_t evaluate_gradient(void *user, const DLManagedTensorVersioned *x,
91 double *value_out,
92 DLManagedTensorVersioned *gradient_out) {
93 try {
94 auto *context = static_cast<XtsObjectiveContext *>(user);
95 if (context->eindir != nullptr) {
96 return static_cast<xts_status_t>(eonc::xtsci_eindir::eval_grad(
97 context->eindir, x, value_out, gradient_out));
98 }
99 const auto positions = map_input(x);
100 set_positions_if_changed(context->objective, positions, context->cached_x);
101 *value_out = context->objective->getEnergy();
102 const auto gradient_value = context->objective->getGradient();
103 auto output = map_output(gradient_out);
104 if (output.size() != gradient_value.size()) {
105 return XTS_INVALID_PARAMETER;
106 }
107 output = gradient_value;
108 return XTS_SUCCESS;
109 } catch (...) {
110 return XTS_INTERNAL_ERROR;
111 }
112}
113
114xts_status_t hessian(void *user, const DLManagedTensorVersioned *x,
115 DLManagedTensorVersioned *hess_out) {
116 try {
117 auto *context = static_cast<XtsObjectiveContext *>(user);
118 const auto positions = map_input(x);
119 const Eigen::MatrixXd H = eonc::pairhess::build(
120 positions, context->precon, context->pot, context->precon_A,
121 context->precon_mu, context->precon_rcut, *context->objective);
122 auto output = map_output(hess_out);
123 const Eigen::Index n = H.rows();
124 if (output.size() != n * n) {
125 return XTS_INVALID_PARAMETER;
126 }
127 for (Eigen::Index i = 0; i < n; ++i) {
128 for (Eigen::Index j = 0; j < n; ++j) {
129 output[i * n + j] = H(i, j);
130 }
131 }
132 return XTS_SUCCESS;
133 } catch (...) {
134 return XTS_INTERNAL_ERROR;
135 }
136}
137
138xts_method_t method_from_name(const std::string &name) {
139 if (name == "lbfgs") {
140 return XTS_LBFGS;
141 }
142 if (name == "bfgs") {
143 return XTS_BFGS;
144 }
145 if (name == "sr1") {
146 return XTS_SR1;
147 }
148 if (name == "sr2") {
149 return XTS_SR2;
150 }
151 if (name == "newton") {
152 return XTS_NEWTON;
153 }
154 if (name == "rfo") {
155 return XTS_RFO;
156 }
157 if (name == "steepest") {
158 return XTS_STEEPEST;
159 }
160 if (name == "adam") {
161 return XTS_ADAM;
162 }
163 if (name == "pso") {
164 return XTS_PSO;
165 }
166 if (name == "polak_ribiere" || name == "nlcg" || name == "pr") {
167 return XTS_POLAK_RIBIERE;
168 }
169 if (name == "fletcher_reeves") {
170 return XTS_FLETCHER_REEVES;
171 }
172 if (name == "hestenes_stiefel") {
173 return XTS_HESTENES_STIEFEL;
174 }
175 if (name == "dai_yuan") {
176 return XTS_DAI_YUAN;
177 }
178 if (name == "conjugate_descent") {
179 return XTS_CONJUGATE_DESCENT;
180 }
181 if (name == "hager_zhang") {
182 return XTS_HAGER_ZHANG;
183 }
184 if (name == "liu_storey") {
185 return XTS_LIU_STOREY;
186 }
187 if (name == "fr_pr") {
188 return XTS_FR_PR;
189 }
190 if (name == "fire") {
191 return XTS_FIRE;
192 }
193 if (name == "bb" || name == "barzilai_borwein") {
194 return XTS_BB;
195 }
196 if (name == "dogleg") {
197 return XTS_DOGLEG;
198 }
199 if (name == "fire2") {
200 return XTS_FIRE2;
201 }
202 throw std::invalid_argument(
203 "unknown Xtsci.method '" + name +
204 "' (lbfgs, bfgs, sr1, sr2, newton, rfo, steepest, adam, pso, "
205 "polak_ribiere, fletcher_reeves, hestenes_stiefel, dai_yuan, "
206 "conjugate_descent, hager_zhang, liu_storey, fr_pr, fire, bb, "
207 "dogleg, fire2)");
208}
209
210bool is_host_precon(const std::string &name) {
211 return name == "pair" || name == "pair_abs" || name == "pair_full" ||
212 name == "exp" || name == "c1" || name == "lindh" ||
213 name == "lindh_full" || name == "fischer" || name == "schlegel" ||
214 name == "swart";
215}
216
217// [Xtsci] qn_step / precon are the natural knobs. [LBFGS] lbfgs_step
218// and lbfgs_precon stay on the native optimizer and fill in when the
219// Xtsci fields are still at their defaults.
220std::string
221resolved_qn_step(const eonc::Parameters::optimizer_options_t &opts) {
222 if (opts.xtsci.qn_step != "lbfgs") {
223 return opts.xtsci.qn_step;
224 }
225 if (opts.lbfgs.step == "newton" || opts.lbfgs.step == "rfo") {
226 return opts.lbfgs.step;
227 }
228 return opts.xtsci.qn_step;
229}
230
231std::string resolved_precon(const eonc::Parameters::optimizer_options_t &opts) {
232 if (opts.xtsci.precon != "none") {
233 return opts.xtsci.precon;
234 }
235 return opts.lbfgs.precon;
236}
237
238std::string resolved_accept(const eonc::Parameters::optimizer_options_t &opts) {
239 if (opts.xtsci.accept != "none") {
240 return opts.xtsci.accept;
241 }
242 if (opts.lbfgs.accept == "energy" || opts.lbfgs.accept == "nonmonotone") {
243 return opts.lbfgs.accept;
244 }
245 return opts.xtsci.accept;
246}
247
248} // namespace
249
250namespace eonc {
251
253 xts_solver_free(m_solver);
255 m_eindir = nullptr;
256}
257
258void XtsciOptimizer::ensureSolver(double a_maxMove) {
259 if (m_solver != nullptr) {
260 return;
261 }
262 const auto dim = static_cast<size_t>(m_objf->degreesOfFreedom());
263 if (dim == 0) {
264 return;
265 }
266 const auto stamp = xts_abi_stamp();
267 if (xts_abi_compatible(&stamp) == 0) {
268 throw std::runtime_error("incompatible xtsci-optimize ABI");
269 }
270 const auto &lbfgs = m_optConfig.opts.lbfgs;
271 const xts_method_t method = method_from_name(m_optConfig.opts.xtsci.method);
272 double istep = std::max(a_maxMove, 1.0e-12);
273 if (method == XTS_FIRE || method == XTS_FIRE2) {
274 istep = m_optConfig.opts.time_step;
275 if (istep <= 0.0) {
276 istep = m_optConfig.opts.time_step_input;
277 }
278 if (istep <= 0.0) {
279 istep = 0.1;
280 }
281 }
282 xts_control_t control{
283 static_cast<size_t>(std::max<long>(1, m_optConfig.opts.max_iterations)),
284 m_optConfig.opts.converged_force,
285 istep,
286 static_cast<size_t>(std::max<long>(0, lbfgs.memory)),
287 0.0,
288 };
289 m_solver = xts_solver_create(method, &control, dim);
290 if (m_solver == nullptr) {
291 throw std::runtime_error(xts_last_error());
292 }
293 const auto step = resolved_qn_step(m_optConfig.opts);
294 if (step == "newton") {
295 xts_solver_set_qn_step(m_solver, XTS_QN_NEWTON);
296 } else if (step == "rfo") {
297 xts_solver_set_qn_step(m_solver, XTS_QN_RFO);
298 } else {
299 xts_solver_set_qn_step(m_solver, XTS_QN_LBFGS);
300 }
301 const auto accept = resolved_accept(m_optConfig.opts);
302 if (accept == "energy") {
303 xts_solver_set_accept(m_solver, XTS_ACCEPT_ENERGY);
304 } else if (accept == "nonmonotone") {
305 xts_solver_set_accept(m_solver, XTS_ACCEPT_NONMONOTONE);
306 } else {
307 xts_solver_set_accept(m_solver, XTS_ACCEPT_NONE);
308 }
309 xts_solver_set_project_rigid(m_solver, lbfgs.project_rigid ? 1 : 0);
310 xts_solver_set_extra_updates(
311 m_solver, static_cast<size_t>(std::max<long>(0, lbfgs.extra_updates)));
312 if (lbfgs.curvature == "cautious") {
313 xts_solver_set_cautious(m_solver, lbfgs.cautious_eps, lbfgs.cautious_alpha);
314 } else {
315 xts_solver_set_cautious(m_solver, 0.0, lbfgs.cautious_alpha);
316 }
317 if (m_optConfig.opts.xtsci.highs) {
318 if (xts_solver_set_highs(m_solver, 1) != 0) {
319 throw std::runtime_error(
320 "Xtsci.highs needs xtsci-optimize built with --features highs");
321 }
322 }
323 const auto &mani = m_optConfig.opts.xtsci.manifold;
324 if (mani == "so3" && dim != 9) {
325 throw std::runtime_error(
326 "Xtsci.manifold=so3 needs length 9; a 3N cluster is "
327 "rigid_quotient (Sella R^{3N}/SE(3)), not a packed rotation");
328 }
329 if (mani == "se3" && dim != 12) {
330 throw std::runtime_error(
331 "Xtsci.manifold=se3 needs length 12; a 3N cluster is "
332 "rigid_quotient (Sella R^{3N}/SE(3)), not an SE(3) prefix");
333 }
334 if ((mani == "rigid_quotient" || mani == "mw_rigid" || mani == "sella" ||
335 mani == "eckart" || mani == "irc") &&
336 (dim < 6 || dim % 3 != 0)) {
337 throw std::runtime_error("Xtsci.manifold=" + mani +
338 " needs 3N Cartesians with N >= 2");
339 }
340 if (mani == "sphere") {
341 xts_solver_set_manifold(m_solver, XTS_MANIFOLD_SPHERE);
342 } else if (mani == "so3") {
343 xts_solver_set_manifold(m_solver, XTS_MANIFOLD_SO3);
344 } else if (mani == "stiefel") {
345 xts_solver_set_manifold(m_solver, XTS_MANIFOLD_STIEFEL);
346 } else if (mani == "se3") {
347 xts_solver_set_manifold(m_solver, XTS_MANIFOLD_SE3);
348 } else if (mani == "rigid_quotient" || mani == "sella") {
349 xts_solver_set_manifold(m_solver, XTS_MANIFOLD_RIGID_QUOTIENT);
350 } else if (mani == "mw_rigid" || mani == "eckart" || mani == "irc") {
351 xts_solver_set_manifold(m_solver, XTS_MANIFOLD_MW_RIGID);
352 const auto masses = m_objf->getMasses();
353 if (masses.size() > 0) {
354 xts_solver_set_masses(m_solver, masses.data(),
355 static_cast<size_t>(masses.size()));
356 }
357 } else {
358 xts_solver_set_manifold(m_solver, XTS_MANIFOLD_EUCLIDEAN);
359 }
360 if (mani == "rigid_quotient" || mani == "mw_rigid" || mani == "sella" ||
361 mani == "eckart" || mani == "irc") {
362 xts_solver_set_periodic(m_solver, m_objf->getPeriodic() ? 1 : 0);
363 }
364 if (m_eindir == nullptr) {
366 }
367}
368
369int XtsciOptimizer::step(double a_maxMove) {
370 if (m_objf->degreesOfFreedom() <= 0) {
371 return m_objf->isConverged() ? 1 : 0;
372 }
373 ensureSolver(a_maxMove);
374 if (m_solver == nullptr) {
375 return m_objf->isConverged() ? 1 : 0;
376 }
377 // Native LBFGS clips by the largest per-atom move, not ||d||_2.
378 xts_solver_set_maxmove(m_solver, 0.0);
379 xts_solver_set_atom_maxmove(m_solver, std::max(a_maxMove, 0.0));
380
381 if (m_x.size() == 0) {
382 m_x = m_objf->getPositions();
383 }
384 if (m_x.size() != m_objf->degreesOfFreedom()) {
385 throw std::runtime_error("xtsci objective position dimension mismatch");
386 }
387 auto *tensor =
388 xts_tensor_borrow_cpu_f64(m_x.data(), static_cast<size_t>(m_x.size()));
389 if (tensor == nullptr) {
390 throw std::runtime_error("could not allocate xtsci objective tensor");
391 }
392 const auto &lbfgs = m_optConfig.opts.lbfgs;
393 std::string precon = resolved_precon(m_optConfig.opts);
394 const xts_method_t method = method_from_name(m_optConfig.opts.xtsci.method);
395 if (method == XTS_DOGLEG && !is_host_precon(precon)) {
396 precon = "pair";
397 }
398 XtsObjectiveContext context{
399 m_objf.get(), m_optConfig.potential, precon, lbfgs.precon_A,
400 lbfgs.precon_mu, lbfgs.precon_rcut, &m_cached_x, m_eindir,
401 };
402 xts_report_t report{};
403 const bool want_hess = method == XTS_NEWTON || method == XTS_RFO ||
404 method == XTS_DOGLEG || is_host_precon(precon);
405 xts_status_t status;
406 if (want_hess) {
407 status = xts_solver_step_hess_fg(m_solver, evaluate_gradient, hessian,
408 &context, tensor, &report);
409 } else {
410 status = xts_solver_step_fg(m_solver, evaluate_gradient, &context, tensor,
411 &report);
412 }
413 xts_tensor_free(tensor);
414 if (status != XTS_SUCCESS) {
415 throw std::runtime_error(xts_last_error());
416 }
417 return m_objf->isConverged() ? 1 : 0;
418}
419
420int XtsciOptimizer::run(size_t a_maxIterations, double a_maxMove) {
421 if (m_objf->degreesOfFreedom() <= 0 || a_maxIterations == 0) {
422 return m_objf->isConverged() ? 1 : 0;
423 }
424 ensureSolver(a_maxMove);
425 const auto &lbfgs = m_optConfig.opts.lbfgs;
426 std::string precon = resolved_precon(m_optConfig.opts);
427 const xts_method_t method = method_from_name(m_optConfig.opts.xtsci.method);
428 if (method == XTS_DOGLEG && !is_host_precon(precon)) {
429 precon = "pair";
430 }
431 const bool want_hess = method == XTS_NEWTON || method == XTS_RFO ||
432 method == XTS_DOGLEG || is_host_precon(precon);
433 // A full minimize without a host Hessian goes through the eindir entry.
434 // step() stays one session iteration so the host loop keeps its memory.
435 if (m_eindir != nullptr && !want_hess) {
436 if (m_x.size() == 0) {
437 m_x = m_objf->getPositions();
438 }
439 double value = 0.0;
440 const int status = xtsci_eindir::minimize(
441 m_eindir, m_x.data(), static_cast<size_t>(m_x.size()), a_maxIterations,
442 m_optConfig.opts.converged_force, std::max(a_maxMove, 1.0e-12),
443 static_cast<size_t>(std::max<long>(0, lbfgs.memory)),
444 static_cast<int>(method), &value);
445 if (status != XTS_SUCCESS) {
446 throw std::runtime_error(xts_last_error());
447 }
448 m_objf->setPositions(m_x);
449 m_cached_x = m_x;
450 (void)value;
451 return m_objf->isConverged() ? 1 : 0;
452 }
453 for (size_t i = 0; i < a_maxIterations; ++i) {
454 if (step(a_maxMove) != 0) {
455 return 1;
456 }
457 }
458 return m_objf->isConverged() ? 1 : 0;
459}
460
461} // namespace eonc
virtual VectorXd getPositions()=0
virtual void setPositions(const VectorXd &x)=0
const OptimizerConfig m_optConfig
Definition Optimizer.h:69
std::shared_ptr< ObjectiveFunction > m_objf
Definition Optimizer.h:70
eonc::optimizer_options_t optimizer_options_t
Definition Parameters.h:70
int run(size_t a_maxIterations, double a_maxMove) override
void ensureSolver(double a_maxMove)
Eigen::VectorXd m_cached_x
xts_solver_t * m_solver
Eigen::VectorXd m_x
int step(double a_maxMove) override
xtsci_eindir::State * m_eindir
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.
int minimize(State *state, double *x, std::size_t n, std::size_t maxiter, double gtol, double istep, std::size_t memory, int method, double *value_out)
xts_minimize_eindir on a caller-owned buffer. The caller keeps State.
void release(State *state)
State * bind(ObjectiveFunction *objective, Eigen::VectorXd *cached)
Borrow an ObjectiveFunction as an eindir objective.
int eval_grad(State *state, const DLManagedTensorVersioned *x, double *value, DLManagedTensorVersioned *gradient)
One fused energy and gradient through the borrowed eindir handle.
RAII resource manager for the ARTn C library with global synchronization.
struct eonc::optimizer_options_t::xtsci_t xtsci
struct eonc::optimizer_options_t::lbfgs_t lbfgs