Loading...
Searching...
No Matches
eonc::ARTnSaddleSearch Class Reference

Saddle search method using the Activation-Relaxation Technique nouveau. More...

#include <ARTnSaddleSearch.h>

Inheritance diagram for eonc::ARTnSaddleSearch:

Public Member Functions

 ARTnSaddleSearch (std::shared_ptr< Matter > matterPassed, std::shared_ptr< Potential > potPassed, AtomMatrix modeInitial, const Parameters &paramsPassed)
 ~ARTnSaddleSearch () override
int run () override
 Production path loads the process ARTn library when that library is linked.
int run (IARTnResource &res)
 Test seam: injected resource, no process-default libartn load.
double getEigenvalue () override
AtomMatrix getEigenvector () override
std::string_view describeStatus (int status) const override
int getStatus () const override
int getIterationCount () const override
int getForceCalls () const override
Public Member Functions inherited from eonc::SaddleSearchMethod
 SaddleSearchMethod (std::shared_ptr< Potential > potPassed, const Parameters &paramsPassed)
virtual ~SaddleSearchMethod ()

Static Public Attributes

static constexpr int STATUS_GOOD = 0
static constexpr int STATUS_BAD_MAX_ITERATIONS
static constexpr int STATUS_BAD_ARTN_ERROR = 22

Private Attributes

std::shared_ptr< Matter > matter
double eigenvalue {std::numeric_limits<double>::quiet_NaN()}
AtomMatrix eigenvector
AtomMatrix mode
int status {0}
int iteration {0}
int forcecalls {0}
eonc::log::Scoped log

Additional Inherited Members

Protected Attributes inherited from eonc::SaddleSearchMethod
std::shared_ptr< Potential > pot
const Parameters & params

Detailed Description

Saddle search method using the Activation-Relaxation Technique nouveau.

Wraps the pARTn Fortran library via its C API (artn.h).

Definition at line 28 of file ARTnSaddleSearch.h.

Constructor & Destructor Documentation

◆ ARTnSaddleSearch()

eonc::ARTnSaddleSearch::ARTnSaddleSearch ( std::shared_ptr< Matter > matterPassed,
std::shared_ptr< Potential > potPassed,
AtomMatrix modeInitial,
const Parameters & paramsPassed )

Definition at line 55 of file ARTnSaddleSearch.cpp.

59 : SaddleSearchMethod(potPassed, paramsPassed),
60 matter{matterPassed},
61 mode{modeInitial},
62 eigenvector{AtomMatrix::Zero(matterPassed->numberOfAtoms(), 3)} {
64 if (!log) {
65 throw std::runtime_error("ARTnSaddleSearch: Logger not initialized");
66 }
67}
eonc::log::Scoped log
std::shared_ptr< Matter > matter
SaddleSearchMethod(std::shared_ptr< Potential > potPassed, const Parameters &paramsPassed)
quill::Logger * get() noexcept
Get or create the default "combi" logger.
Definition EonLogger.h:44

◆ ~ARTnSaddleSearch()

eonc::ARTnSaddleSearch::~ARTnSaddleSearch ( )
override

Definition at line 69 of file ARTnSaddleSearch.cpp.

69 {
70#ifdef WITH_ARTN
71 // Clean up is done within the search loop, not in destructor
72#endif
73}

Member Function Documentation

◆ describeStatus()

std::string_view eonc::ARTnSaddleSearch::describeStatus ( int status) const
overridevirtual

Implements eonc::SaddleSearchMethod.

Definition at line 474 of file ARTnSaddleSearch.cpp.

474 {
475 switch (status) {
476 case STATUS_GOOD:
477 return "Success";
479 return "Too many iterations";
481 return "ARTn backend error";
482 default:
483 return "Unknown status";
484 }
485}
static constexpr int STATUS_GOOD
static constexpr int STATUS_BAD_ARTN_ERROR
static constexpr int STATUS_BAD_MAX_ITERATIONS

◆ getEigenvalue()

double eonc::ARTnSaddleSearch::getEigenvalue ( )
overridevirtual

Implements eonc::SaddleSearchMethod.

Definition at line 460 of file ARTnSaddleSearch.cpp.

460 {
461 if (log && std::isnan(eigenvalue)) {
462 QUILL_LOG_WARNING(log, "Requesting uninitialized/invalid eigenvalue");
463 }
464 return eigenvalue;
465}

◆ getEigenvector()

AtomMatrix eonc::ARTnSaddleSearch::getEigenvector ( )
overridevirtual

Implements eonc::SaddleSearchMethod.

Definition at line 467 of file ARTnSaddleSearch.cpp.

467 {
468 if (log && std::isnan(eigenvalue)) {
469 QUILL_LOG_WARNING(log, "Requesting uninitialized eigenvector");
470 }
471 return eigenvector;
472}

◆ getForceCalls()

int eonc::ARTnSaddleSearch::getForceCalls ( ) const
inlineoverridevirtual

Reimplemented from eonc::SaddleSearchMethod.

Definition at line 49 of file ARTnSaddleSearch.h.

◆ getIterationCount()

int eonc::ARTnSaddleSearch::getIterationCount ( ) const
inlineoverridevirtual

Reimplemented from eonc::SaddleSearchMethod.

Definition at line 48 of file ARTnSaddleSearch.h.

◆ getStatus()

int eonc::ARTnSaddleSearch::getStatus ( ) const
inlineoverridevirtual

Reimplemented from eonc::SaddleSearchMethod.

Definition at line 47 of file ARTnSaddleSearch.h.

47{ return status; }

◆ run() [1/2]

int eonc::ARTnSaddleSearch::run ( void )
overridevirtual

Production path loads the process ARTn library when that library is linked.

Implements eonc::SaddleSearchMethod.

Definition at line 75 of file ARTnSaddleSearch.cpp.

75 {
76#ifdef WITH_ARTN
77 return run(get_artn_resource());
78#else
79 QUILL_LOG_ERROR(log, "ARTn support not compiled");
81 return status;
82#endif
83}
int run() override
Production path loads the process ARTn library when that library is linked.
ARTnResource & get_artn_resource()
Global access to thread-safe ARTn resource.

◆ run() [2/2]

int eonc::ARTnSaddleSearch::run ( IARTnResource & res)

Test seam: injected resource, no process-default libartn load.

Definition at line 85 of file ARTnSaddleSearch.cpp.

85 {
86 const int nat = matter->numberOfAtoms();
87
88 if (mode.rows() != nat || mode.cols() != 3) {
89 mode = AtomMatrix::Zero(nat, 3);
90 }
91
92 // Pre-declare force-loop storage outside the lock so it outlives the
93 // setup critical section. These reads do not touch pARTn state.
94 AtomMatrix positions = matter->getPositions();
95 AtomMatrix forces = AtomMatrix::Zero(nat, 3);
96 AtomMatrix displacement = AtomMatrix::Zero(nat, 3);
97
98 // Eigen::Maps give Fortran a column-major [3, nat] view over the same
99 // memory as the row-major [nat, 3] AtomMatrix (zero-copy).
100 Eigen::Map<AtomMatrixF> pos_map(positions.data(), 3, nat);
101 Eigen::Map<AtomMatrixF> force_map(forces.data(), 3, nat);
102 Eigen::Map<AtomMatrixF> disp_map(displacement.data(), 3, nat);
103 Eigen::Map<AtomMatrixF> mode_map(mode.data(), 3, nat);
104
105 const double push_step = params.artn_options().push_step_size;
106 const double mode_norm = mode.norm();
107 AtomMatrixF mode_fort;
108 if (mode_norm > 1e-10) {
109 mode_fort = mode_map;
110 Eigen::Map<VectorXd> mode_vec_map(mode_fort.data(), mode_fort.size());
111 mode_vec_map *= (push_step / mode_norm);
112 }
113 int dim_mode[2] = {3, nat};
114
115 // Hold library_mutex from create() through destroy(). pARTn keeps one
116 // process-global Fortran search; releasing the mutex between artn_step
117 // calls lets another run artn_create or artn_step and corrupt it.
118 std::lock_guard<std::mutex> lock(res.library_mutex);
119 {
120 try {
121 res.require_loaded();
122 } catch (const std::exception &e) {
123 QUILL_LOG_ERROR(log, "ARTn library not available: {}", e.what());
125 return status;
126 }
127
128 res.get_create_fn()();
129
130 // Set engine units first (required before any other params)
131 const char *units = "lammps/metal";
132 int size0 = 0;
133 int result_units = res.get_set_param_fn()("engine_units", 0, &size0, units);
134 if (result_units != 0) {
135 QUILL_LOG_ERROR(log, "set_param(engine_units) failed with code {}",
136 result_units);
137 }
138
139 // Set parameters from eOn config
140 double push_step = params.artn_options().push_step_size;
141 int result_push =
142 res.get_set_param_fn()("push_step_size", 0, &size0, &push_step);
143 if (result_push != 0) {
144 QUILL_LOG_ERROR(log, "set_param(push_step_size) failed with code {}",
145 result_push);
146 }
147
148 double force_thr = params.artn_options().force_threshold;
149 int result_force =
150 res.get_set_param_fn()("forc_thr", 0, &size0, &force_thr);
151 if (result_force != 0) {
152 QUILL_LOG_ERROR(log, "set_param(forc_thr) failed with code {}",
153 result_force);
154 }
155
156 // filin names the artn.in input file pARTn reads at setup. artn_create
157 // resets its internal value to NAN_STR ("BBBB") meaning "undefined", so
158 // an empty eOn config leaves pARTn reading no file at all. If the user
159 // does set a path, surface a missing file before setup_artn runs so the
160 // failure names the file instead of hiding inside pARTn's ERR_FILE code.
161 const std::string &filin = params.artn_options().filin;
162 if (!filin.empty()) {
163 if (!std::filesystem::exists(filin)) {
164 QUILL_LOG_ERROR(log, "artn_options.filin '{}' does not exist", filin);
165 res.get_destroy_fn()();
167 return status;
168 }
169 int result_filin =
170 res.get_set_param_fn()("filin", 0, &size0, filin.c_str());
171 if (result_filin != 0) {
172 QUILL_LOG_ERROR(log, "set_param(filin) failed with code {}",
173 result_filin);
174 }
175 }
176
177 int verbosity = 3;
178 int result_verbose =
179 res.get_set_param_fn()("verbose", 0, &size0, &verbosity);
180 if (result_verbose != 0) {
181 QUILL_LOG_ERROR(log, "set_param(verbose) failed with code {}",
182 result_verbose);
183 }
184
185 // ninit controls initial push steps before Lanczos eigenmode estimation.
186 // 0 = skip push, go straight to Lanczos (appropriate when eOn provides
187 // the displacement direction via push_init); >0 = push that many steps.
188 // -1 sentinel means "leave pARTn's own default in place", so we only
189 // call set_param when the user asked for a specific value.
190 if (params.artn_options().ninit >= 0) {
191 int ninit = params.artn_options().ninit;
192 int result_ninit = res.get_set_param_fn()("ninit", 0, &size0, &ninit);
193 if (result_ninit != 0) {
194 QUILL_LOG_ERROR(log, "set_param(ninit) failed with code {}",
195 result_ninit);
196 }
197 }
198
199 // nperp_limitation: controls perp-relax steps per Lanczos cycle.
200 // pARTn defaults are tuned for exploration from minimum. For refinement
201 // near a saddle, -1 (unlimited) or 20-30 (for ML potentials) is better.
202 if (params.artn_options().nperp_limitation != "default") {
203 // Parse comma-separated integers into a vector. std::stoi throws on a
204 // blank or non-integer token; that must not escape run() after
205 // artn_create, or artn_destroy is skipped.
206 std::vector<int> nperp_vals;
207 if (!parse_nperp_limitation(params.artn_options().nperp_limitation,
208 nperp_vals)) {
209 QUILL_LOG_ERROR(log,
210 "artn_options.nperp_limitation '{}' is not a "
211 "comma-separated integer list",
212 params.artn_options().nperp_limitation);
213 res.get_destroy_fn()();
215 return status;
216 }
217 if (!nperp_vals.empty()) {
218 int nperp_size = static_cast<int>(nperp_vals.size());
219 int result_nperp = res.get_set_param_fn()(
220 "nperp_limitation", 1, &nperp_size, nperp_vals.data());
221 if (result_nperp != 0) {
222 QUILL_LOG_WARNING(log, "set_param(nperp_limitation) failed: {}",
223 result_nperp);
224 }
225 }
226 }
227
228 // lanczos_min_size: minimum Lanczos iterations before convergence check.
229 // Default 3 for exploration; 1 for refinement near a saddle.
230 if (params.artn_options().lanczos_min_size >= 0) {
231 int lms = params.artn_options().lanczos_min_size;
232 res.get_set_param_fn()("lanczos_min_size", 0, &size0, &lms);
233 }
234
235 // nsmooth: number of smooth interpolation steps. 0 disables.
236 if (params.artn_options().nsmooth >= 0) {
237 int ns = params.artn_options().nsmooth;
238 res.get_set_param_fn()("nsmooth", 0, &size0, &ns);
239 }
240
241 // nnewchance: retries permitted when Lanczos returns a positive lowest
242 // eigenvalue (convex region, no unstable mode). pARTn defaults to 0,
243 // i.e. immediate "EIGENVALUE LOST" failure -- too brittle for small
244 // clusters where the eigenvalue can flip positive transiently before
245 // the saddle direction stabilizes. eOn's default of 3 (Parameters.h)
246 // gives Lanczos a few random-restart chances before giving up.
247 if (params.artn_options().nnewchance >= 0) {
248 int nnc = params.artn_options().nnewchance;
249 res.get_set_param_fn()("nnewchance", 0, &size0, &nnc);
250 }
251
252 // Setup
253 bool cerr = false;
254 res.get_setup_fn()(nat, &cerr);
255 if (cerr) {
256 QUILL_LOG_ERROR(log, "ARTn setup failed (nat={})", nat);
257 res.get_destroy_fn()();
259 return status;
260 }
261
262 // Initial push vector (if eOn supplied a non-trivial mode). Must be set
263 // after setup_artn() and before the first artn_step(); keep inside the
264 // same critical section so no concurrent ARTn search can re-init between
265 // setup and push.
266 if (mode_norm > 1e-10) {
267 int result =
268 res.get_set_param_fn()("push_init", 2, dim_mode, mode_fort.data());
269 if (result != 0) {
270 QUILL_LOG_WARNING(log, "set_param(push_init) failed with code {}",
271 result);
272 }
273 }
274 } // setup only; library_mutex stays held until destroy
275
276 // Per-atom metadata for the Fortran step (no pARTn state).
277 std::vector<int> ityp(nat);
278 std::vector<int> if_pos(3 * nat, 1); // all atoms free by default
279 double box_f[9];
280 bool lconv = false;
281
282 for (int i = 0; i < nat; i++) {
283 if (matter->getFixed(i)) {
284 Eigen::Map<Eigen::Vector3i>(&if_pos[i * 3]).setZero();
285 }
286 ityp[i] = matter->getAtomicNr(i);
287 }
288
289 // pARTn reads lattice vectors as columns. Copy rows, do not transpose.
290 Matrix3d cell = matter->getCell();
291 lattice_rows_to_fortran_box(cell, box_f);
292
293 int maxIter = params.artn_options().max_iterations;
294
295 // while-loop (not for) so `iteration` reports the index of the converged
296 // step rather than one past it: on convergence during step k, we break
297 // before the increment and the post-loop value is k, matching log output.
298 iteration = 0;
299 while (iteration < maxIter && !lconv) {
300 // The potential call does not touch pARTn, but it stays inside the
301 // library lock. Releasing the mutex here would let another search
302 // artn_step on the same Fortran state before this one continues.
303 double energy = matter->getPotentialEnergy();
304 forces = matter->getForces(); // Returns RowMajor Nx3
305 this->forcecalls++;
306
307 // Perf note (artn-plugin >= 9dab2053): the inner Lanczos eigenvector
308 // reconstruction now uses intrinsic matmul on a reshaped Vmat slice,
309 // which allocates a temporary [3*nat, ilanc] array per Lanczos iteration.
310 // Negligible at our sizes (small molecules, ilanc < O(20)); revisit if
311 // we ever drive artn against large supercell DFT.
312 res.get_artn_step_fn()(nat, energy, force_map.data(), ityp.data(),
313 pos_map.data(), box_f, if_pos.data(),
314 disp_map.data(), &lconv);
315
316 if (!lconv) {
317 // Apply displacement and update Matter (PES-specific, typically
318 // thread-safe) Add displacement to positions
319 positions += displacement;
320 matter->setPositions(positions);
321 iteration++;
322 }
323 }
324
325 // 4. Data Retrieval (lock still held)
326 if (lconv) {
327 // pARTn exposes a dedicated C get_error() that returns both the error
328 // code and a c_malloc'd message pointer (see m_artn_error.f90 in
329 // artn-plugin). Prefer it when available; fall back to the has_error
330 // flag via get_data for older libartn builds that predate the wrapper.
331 int artn_err = 0;
332 std::string artn_err_msg;
333 if (auto *get_error_fn_ = res.get_get_error_fn()) {
334 void *cmsg = nullptr;
335 artn_err = get_error_fn_(&cmsg);
336 if (artn_err != 0 && cmsg != nullptr) {
337 artn_err_msg.assign(static_cast<const char *>(cmsg));
338 std::free(cmsg);
339 }
340 } else {
341 bool *has_error_ptr = nullptr;
342 int result_has_error = res.get_get_data_fn()(
343 "has_error", reinterpret_cast<void **>(&has_error_ptr));
344 if (result_has_error != 0) {
345 QUILL_LOG_WARNING(log, "get_data(has_error) failed with code {}",
346 result_has_error);
347 } else if (has_error_ptr) {
348 artn_err = *has_error_ptr ? 1 : 0;
349 std::free(has_error_ptr);
350 }
351 }
352 bool has_error = artn_err != 0;
353
354 bool has_sad = false;
355 bool *has_sad_ptr = nullptr;
356 int result_has_sad = res.get_get_data_fn()(
357 "has_sad", reinterpret_cast<void **>(&has_sad_ptr));
358 if (result_has_sad != 0) {
359 QUILL_LOG_WARNING(log, "get_data(has_sad) failed with code {}",
360 result_has_sad);
361 } else if (has_sad_ptr) {
362 has_sad = *has_sad_ptr;
363 std::free(has_sad_ptr);
364 }
365
366 if (has_error && !artn_err_msg.empty()) {
367 QUILL_LOG_WARNING(log, "pARTn reported error {}: {}", artn_err,
368 artn_err_msg);
369 }
370
371 // If a saddle was found, accept it even if force didn't fully converge
372 if (has_sad) {
373 QUILL_LOG_INFO(
374 log,
375 "ARTn found saddle after {} iterations (has_error={}, has_sad={})",
376 this->iteration, has_error, has_sad);
377
378 // Retrieve the saddle coordinates tracked internally by pARTn so the
379 // Matter object matches the reported eigenpair and subsequent endpoint
380 // minimizations start from the actual saddle.
381 double *tau_sad_ptr = nullptr;
382 int result_tau_sad = res.get_get_data_fn()(
383 "tau_sad", reinterpret_cast<void **>(&tau_sad_ptr));
384 // has_sad without coordinates leaves Matter on the pre-convergence
385 // geometry. Callers treat STATUS_GOOD as the saddle, so that is an error.
386 if (result_tau_sad != 0 || tau_sad_ptr == nullptr) {
387 QUILL_LOG_WARNING(
388 log, "Failed to retrieve tau_sad (result={}, ptr_valid={})",
389 result_tau_sad, tau_sad_ptr != nullptr);
390 std::free(tau_sad_ptr);
392 res.get_destroy_fn()();
393 return status;
394 }
396 std::vector<double>(tau_sad_ptr, tau_sad_ptr + 3 * nat), nat));
397 std::free(tau_sad_ptr);
398
399 // Retrieve eigenvalue
400 double *eigval_ptr = nullptr;
401 int result_eigval = res.get_get_data_fn()(
402 "eigval_sad", reinterpret_cast<void **>(&eigval_ptr));
403 if (result_eigval == 0 && eigval_ptr) {
404 eigenvalue = *eigval_ptr;
405 std::free(eigval_ptr);
406 } else {
407 QUILL_LOG_WARNING(
408 log, "Failed to retrieve eigenvalue (result={}, ptr_valid={})",
409 result_eigval, eigval_ptr != nullptr);
410 eigenvalue =
411 std::numeric_limits<double>::quiet_NaN(); // Use NaN to indicate
412 // missing value
413 }
414
415 // Retrieve eigenvector (3*nat flat array, column-major from Fortran).
416 // get_data allocates via c_malloc and writes the pointer to cval.
417 // The C header says void* but the Fortran intent(out) semantics
418 // require void** (see artn_c_wrappers.f90:324 and LAMMPS example).
419 double *evec_ptr = nullptr;
420 int result_evec = res.get_get_data_fn()(
421 "eigen_sad", reinterpret_cast<void **>(&evec_ptr));
422 if (result_evec == 0 && evec_ptr) {
423 // Use direct Eigen::Map to convert from Fortran layout
425 std::vector<double>(evec_ptr, evec_ptr + 3 * nat), nat);
426
427 // get_data allocates via c_malloc (artn_c_wrappers.f90), safe to free
428 std::free(evec_ptr);
429 } else {
430 QUILL_LOG_ERROR(
431 log, "Failed to retrieve eigenvector (result={}, ptr_valid={})",
432 result_evec, evec_ptr != nullptr);
433 // Set eigenvector to zero matrix if retrieval failed
434 eigenvector = AtomMatrix::Zero(nat, 3);
435 }
436
438 res.get_destroy_fn()();
439 return status;
440 }
441
442 // No saddle found - this is a real error
443 QUILL_LOG_WARNING(
444 log, "ARTn stopped after {} iterations (has_error={}, has_sad={})",
445 iteration, has_error, has_sad);
447 res.get_destroy_fn()();
448 return status;
449 }
450
451 QUILL_LOG_WARNING(log, "ARTn did not converge after {} iterations",
452 iteration);
454
455 // Clean up in all cases. The library lock is still held.
456 res.get_destroy_fn()();
457 return status;
458}
Eigen::Matrix< double, 3, 3, eOnStorageOrder > Matrix3d
Definition Eigen.h:35
Eigen::Matrix< double, 3, Eigen::Dynamic, Eigen::ColMajor > AtomMatrixF
Definition Eigen.h:44
Eigen::Matrix< double, Eigen::Dynamic, 3, eOnStorageOrder > AtomMatrix
Definition Eigen.h:37
AtomMatrix from_fortran_layout_vector(const std::vector< double > &flat_colmajor, int nat)
Reconstruct AtomMatrix from a flat column-major vector (e.g.
Definition Eigen.h:61
void lattice_rows_to_fortran_box(const Matrix3d &cell, double *box)
Pack an eOn cell for pARTn artn_step.
Definition Eigen.h:71

Member Data Documentation

◆ eigenvalue

double eonc::ARTnSaddleSearch::eigenvalue {std::numeric_limits<double>::quiet_NaN()}
private

Definition at line 53 of file ARTnSaddleSearch.h.

53{std::numeric_limits<double>::quiet_NaN()};

◆ eigenvector

AtomMatrix eonc::ARTnSaddleSearch::eigenvector
private

Definition at line 54 of file ARTnSaddleSearch.h.

◆ forcecalls

int eonc::ARTnSaddleSearch::forcecalls {0}
private

Definition at line 57 of file ARTnSaddleSearch.h.

57{0};

◆ iteration

int eonc::ARTnSaddleSearch::iteration {0}
private

Definition at line 56 of file ARTnSaddleSearch.h.

56{0};

◆ log

eonc::log::Scoped eonc::ARTnSaddleSearch::log
private

Definition at line 58 of file ARTnSaddleSearch.h.

◆ matter

std::shared_ptr<Matter> eonc::ARTnSaddleSearch::matter
private

Definition at line 52 of file ARTnSaddleSearch.h.

◆ mode

AtomMatrix eonc::ARTnSaddleSearch::mode
private

Definition at line 54 of file ARTnSaddleSearch.h.

◆ status

int eonc::ARTnSaddleSearch::status {0}
private

Definition at line 55 of file ARTnSaddleSearch.h.

55{0};

◆ STATUS_BAD_ARTN_ERROR

int eonc::ARTnSaddleSearch::STATUS_BAD_ARTN_ERROR = 22
staticconstexpr

Definition at line 33 of file ARTnSaddleSearch.h.

◆ STATUS_BAD_MAX_ITERATIONS

int eonc::ARTnSaddleSearch::STATUS_BAD_MAX_ITERATIONS
staticconstexpr

◆ STATUS_GOOD

int eonc::ARTnSaddleSearch::STATUS_GOOD = 0
staticconstexpr

Definition at line 30 of file ARTnSaddleSearch.h.


The documentation for this class was generated from the following files: