Loading...
Searching...
No Matches
ProcessSearchJob.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*/
13#include "eon/PotCapabilities.h"
14#ifdef WITH_ARTN
16#endif
20#include "eon/EpiCenters.h"
21#include "eon/HelperFunctions.h"
23#include "eon/Optimizer.h"
24#include "eon/Prefactor.h"
25#include <exception>
26#include <filesystem>
27#include <thread>
28
29#include <format>
30#include <fstream>
31#include <memory>
32#include <stdexcept>
33#include <string>
34
35#include "eon/EonLogger.h"
36
37namespace eonc {
38
39std::vector<std::string> ProcessSearchJob::run() {
40 std::string reactantFilename = eonc::helpers::getRelevantFile("pos.con");
41 std::string displacementFilename("displacement.con");
42 std::string modeFilename("direction.dat");
43 size_t fctmp{0};
44 initial = std::make_shared<Matter>(pot, params);
45 if (params.saddle_search_options().method == "min_mode" ||
46 params.saddle_search_options().method == "basin_hopping" ||
47 params.saddle_search_options().method == "bgsd") {
48 displacement = std::make_shared<Matter>(pot, params);
49 } else if (params.saddle_search_options().method == "dynamics") {
50 displacement = nullptr;
51 }
52 saddle = std::make_shared<Matter>(pot, params);
53 // Give min2 its own potential for parallel endpoint minimization
54 // A clone keeps this job's potential; makePotential rebuilds from the
55 // configuration and is the fallback for backends that cannot clone.
56 std::shared_ptr<Potential> min2Pot = pot;
57 if (pot->needsPerImageInstance() && params.main_options().parallel) {
58 auto cloned = pot->clonePotential();
59 min2Pot = cloned ? cloned : eonc::helpers::makePotential(params);
60 }
61 min1 = std::make_shared<Matter>(pot, params);
62 min2 = std::make_shared<Matter>(min2Pot, params);
63
64 if (!eonc::io::io_ok(initial->con2matter(reactantFilename))) {
65 EONC_LOG_CRITICAL("Failed to load {}", reactantFilename);
66 throw std::runtime_error("failed to load " + reactantFilename);
67 }
68
69 if (params.process_search_options().minimize_first) {
70 QUILL_LOG_DEBUG(log, "Minimizing initial structure\n");
71 fctmp = initial->getPotentialCalls();
72 initial->relax();
73 fCallsMin += initial->getPotentialCalls() - fctmp;
74 QUILL_LOG_DEBUG(log, "Initial minimization took {} fcalls",
75 initial->getPotentialCalls() - fctmp);
76 }
77
80
81 AtomMatrix mode = AtomMatrix::Zero(initial->numberOfAtoms(), 3);
82 if (params.saddle_search_options().method == "min_mode" ||
83 params.saddle_search_options().method == "basin_hopping" ||
84 params.saddle_search_options().method == "bgsd") {
85 if (params.saddle_search_options().displace_type ==
87 // Load displacement.con, or synthesize from pos.con + direction.dat
88 // (#79).
90 *saddle, *initial, displacementFilename, modeFilename,
91 params.saddle_search_options().displace_magnitude)) {
92 EONC_LOG_CRITICAL("Failed to load {} (and no usable {})",
93 displacementFilename, modeFilename);
94 throw std::runtime_error("failed to load " + displacementFilename);
95 }
96 *min1 = *min2 = *initial;
98 &mode)) {
99 *min1 = *min2 = *initial;
100 } else {
101 *saddle = *min1 = *min2 = *initial;
102 }
103 if (displacement) {
105 }
106 } else {
107 // ARTn and dynamics start from the initial minimum
108 *saddle = *min1 = *min2 = *initial;
109 }
110 min2->setPotential(min2Pot);
111
112 const bool useARTnAsMinMode =
113 params.saddle_search_options().method == "min_mode" &&
114 params.saddle_search_options().minmode_method == "artn";
115
116 if (params.saddle_search_options().method == "min_mode") {
117 if (params.saddle_search_options().displace_type ==
119 std::filesystem::exists(modeFilename)) {
120 mode = eonc::helpers::loadMode(modeFilename, initial->numberOfAtoms());
121 }
122#ifdef WITH_ARTN
123 // ARTn as a min-mode drop-in: eOn displaces, seeds the mode, ARTn
124 // takes over from the displaced structure.
125 if (useARTnAsMinMode) {
127 std::make_unique<ARTnSaddleSearch>(saddle, pot, mode, params);
128 } else
129#endif
130 {
131 saddleSearch = std::make_unique<MinModeSaddleSearch>(
132 saddle, mode, initial->getPotentialEnergy(), params, pot);
133 }
134#ifdef WITH_ARTN
135 } else if (params.saddle_search_options().method == "artn") {
136 // ARTn handles its own push from the minimum, eigenmode estimation,
137 // and perpendicular relaxation internally.
138 AtomMatrix artnMode = AtomMatrix::Zero(initial->numberOfAtoms(), 3);
139 if (params.saddle_search_options().displace_type ==
141 std::filesystem::exists(modeFilename)) {
142 artnMode =
143 eonc::helpers::loadMode(modeFilename, initial->numberOfAtoms());
144 }
146 std::make_unique<ARTnSaddleSearch>(saddle, pot, artnMode, params);
147#endif
148 } else if (params.saddle_search_options().method == "basin_hopping") {
150 std::make_unique<BasinHoppingSaddleSearch>(min1, saddle, pot, params);
151 } else if (params.saddle_search_options().method == "dynamics") {
152 saddleSearch = std::make_unique<DynamicsSaddleSearch>(saddle, params);
153 } else if (params.saddle_search_options().method == "bgsd") {
154 saddleSearch = std::make_unique<BiasedGradientSquaredDescent>(
155 saddle, initial->getPotentialEnergy(), params);
156 }
157
158#ifndef WITH_ARTN
159 // Post-dispatch guard for both ARTn entry points so users without a
160 // WITH_ARTN build get a clean per-case error instead of a silent
161 // fall-through. Two distinct messages so downstream tooling and the
162 // integration tests can match on the specific entry point.
163 if (params.saddle_search_options().method == "artn") {
164 throw std::runtime_error(
165 "saddle_search.method=artn requires a build with ARTn support "
166 "(reconfigure with -Dwith_artn=true)");
167 }
168 if (useARTnAsMinMode) {
169 throw std::runtime_error(
170 "saddle_search.minmode_method=artn requires a build with ARTn "
171 "support (reconfigure with -Dwith_artn=true)");
172 }
173#endif
174
175 if (!saddleSearch) {
176 throw std::runtime_error("unknown saddle_search.method");
177 }
178
179 (void)runPrepared();
180 return returnFiles;
181}
182
183std::shared_ptr<Matter>
184ProcessSearchJob::runFromMatter(std::shared_ptr<Matter> seed) {
185 if (!seed) {
186 throw std::runtime_error("ProcessSearchJob::runFromMatter: null Matter");
187 }
188 initial = seed;
189 initial->setPotential(pot);
190 // A clone keeps this job's potential; makePotential rebuilds from the
191 // configuration and is the fallback for backends that cannot clone.
192 std::shared_ptr<Potential> min2Pot = pot;
193 if (pot->needsPerImageInstance() && params.main_options().parallel) {
194 auto cloned = pot->clonePotential();
195 min2Pot = cloned ? cloned : eonc::helpers::makePotential(params);
196 }
197 displacement = std::make_shared<Matter>(pot, params);
198 saddle = std::make_shared<Matter>(pot, params);
199 min1 = std::make_shared<Matter>(pot, params);
200 min2 = std::make_shared<Matter>(min2Pot, params);
201 AtomMatrix mode = AtomMatrix::Zero(initial->numberOfAtoms(), 3);
203 &mode)) {
204 *saddle = *initial;
205 }
207 *min1 = *min2 = *initial;
208 min2->setPotential(min2Pot);
209 if (params.saddle_search_options().method == "min_mode") {
210 saddleSearch = std::make_unique<MinModeSaddleSearch>(
211 saddle, mode, initial->getPotentialEnergy(), params, pot);
212 } else if (params.saddle_search_options().method == "basin_hopping") {
214 std::make_unique<BasinHoppingSaddleSearch>(min1, saddle, pot, params);
215 } else if (params.saddle_search_options().method == "dynamics") {
216 saddleSearch = std::make_unique<DynamicsSaddleSearch>(saddle, params);
217 } else if (params.saddle_search_options().method == "bgsd") {
218 saddleSearch = std::make_unique<BiasedGradientSquaredDescent>(
219 saddle, initial->getPotentialEnergy(), params);
220 } else {
221 throw std::runtime_error(
222 "ProcessSearchJob::runFromMatter: unsupported saddle_search.method");
223 }
224 return runPrepared();
225}
226
227std::shared_ptr<Matter> ProcessSearchJob::runPrepared() {
228 if (!saddleSearch) {
229 throw std::runtime_error("unknown saddle_search.method");
230 }
231
232 int status = doProcessSearch();
233
234 printEndState(status);
235 saveData(status);
236
237 return min2 ? min2 : saddle;
238}
239
241 Matter matterTemp(pot, params);
242 long status;
243 size_t fctmp{0};
244
245 fctmp = pot->forceCallCounter;
246 status = saddleSearch->run();
247 if (params.saddle_search_options().method == "min_mode" &&
248 params.saddle_search_options().minmode_method ==
250 fCallsSaddle += saddleSearch->getForceCalls();
251 } else if (params.saddle_search_options().method == "artn") {
252 fCallsSaddle += saddleSearch->getForceCalls();
253 } else {
254 fCallsSaddle += pot->forceCallCounter - fctmp;
255 }
256 EONC_LOG_DEBUG("Got {} calls in the saddle search, with previous {}",
257 fCallsSaddle, fctmp);
258
259 if (status != MinModeSaddleSearch::STATUS_GOOD) {
260 return status;
261 }
262
263 AtomMatrix posSaddle = saddle->getPositions();
264 AtomMatrix displacedPos;
265
266 // Matter's copy assignment copies the potential; keep each endpoint's
267 // own instance so the two minimizations can run at the same time.
268 const auto min1Pot = min1->getPotential();
269 const auto min2Pot = min2->getPotential();
270 *min1 = *saddle;
271 min1->setPotential(min1Pot);
272
273 displacedPos =
274 posSaddle - saddleSearch->getEigenvector() *
275 params.process_search_options().minimization_offset;
276 min1->setPositions(displacedPos);
277
278 *min2 = *saddle;
279 min2->setPotential(min2Pot);
280 displacedPos =
281 posSaddle + saddleSearch->getEigenvector() *
282 params.process_search_options().minimization_offset;
283 min2->setPositions(displacedPos);
284
285 // Minimize both endpoints concurrently when the shared potential instance is
286 // safe to call from multiple threads, or when each endpoint owns a separate
287 // potential instance.
288 QUILL_LOG_DEBUG(log, "Starting Minimization 1 & 2");
289 bool converged1{false}, converged2{false};
290 long fc1_before = min1->getPotentialCalls();
291 long fc2_before = min2->getPotentialCalls();
292
293 // Two threads may share an instance only when it is thread safe; a
294 // per-image potential needs the endpoints to hold distinct instances.
295 bool canParallel = eonc::potAllowsSharedInstance(*pot) ||
296 (pot->needsPerImageInstance() &&
297 min1->getPotential().get() != min2->getPotential().get());
298 if (params.main_options().parallel && canParallel) {
299 // An exception may not leave a thread function (std::terminate), and a
300 // joinable std::thread may not be destroyed: t1 hands its error back and
301 // the caller joins before rethrowing either side's.
302 std::exception_ptr t1Error;
303 std::thread t1([&] {
304 try {
305 converged1 = min1->relax(false, params.debug_options().write_movies,
306 false, "min1");
307 } catch (...) {
308 t1Error = std::current_exception();
309 }
310 });
311 try {
312 converged2 = min2->relax(false, params.debug_options().write_movies,
313 false, "min2");
314 } catch (...) {
315 t1.join();
316 throw;
317 }
318 t1.join();
319 if (t1Error)
320 std::rethrow_exception(t1Error);
321 } else {
322 converged1 =
323 min1->relax(false, params.debug_options().write_movies, false, "min1");
324 converged2 =
325 min2->relax(false, params.debug_options().write_movies, false, "min2");
326 }
327
328 if (min1->getPotential().get() == min2->getPotential().get()) {
329 fCallsMin += min1->getPotentialCalls() - fc1_before;
330 } else {
331 fCallsMin += (min1->getPotentialCalls() - fc1_before) +
332 (min2->getPotentialCalls() - fc2_before);
333 }
334 QUILL_LOG_DEBUG(log, "Min1: {} fcalls, Min2: {} fcalls",
335 min1->getPotentialCalls() - fc1_before,
336 min2->getPotentialCalls() - fc2_before);
337
338 if (!converged1 || !converged2) {
340 }
341
342 auto sameAs = [](const Matter &a, const Matter &b) {
343 Matter probe(a);
344 return probe.compare(b);
345 };
346
347 if (!sameAs(*initial, *min1) && sameAs(*initial, *min2)) {
348 matterTemp = *min1;
349 *min1 = *min2;
350 *min2 = matterTemp;
351 }
352
353 if (!sameAs(*initial, *min1)) {
354 // Report how far off the endpoint landed. Whether the minimisation
355 // stopped just outside the state-identity tolerance or relaxed into a
356 // different state entirely calls for opposite fixes, and the status
357 // alone does not distinguish them.
358 const double tol =
359 params.structure_comparison_options().distance_difference;
360 auto countMoved = [&](const Matter &m) {
361 long moved = 0;
362 for (long i = 0; i < initial->numberOfAtoms(); ++i) {
363 if (initial
364 ->pbc(initial->getPositions().row(i) - m.getPositions().row(i))
365 .norm() > tol) {
366 ++moved;
367 }
368 }
369 return moved;
370 };
371 QUILL_LOG_INFO(log,
372 "initial != min1: {} of {} atoms past the {} A tolerance "
373 "for min1 ({} for min2); largest separation {} A",
374 countMoved(*min1), initial->numberOfAtoms(), tol,
375 countMoved(*min2), initial->perAtomNorm(*min1));
377 }
378
379 if (sameAs(*initial, *min2)) {
380 QUILL_LOG_DEBUG(log, "both minima are the initial state");
382 }
383
384 if (!params.process_search_options().minimize_first) {
385 min1 = initial;
386 }
387
388 barriersValues[0] = saddle->getPotentialEnergy() - min1->getPotentialEnergy();
389 barriersValues[1] = saddle->getPotentialEnergy() - min2->getPotentialEnergy();
390
391 if ((params.saddle_search_options().max_energy < barriersValues[0]) ||
392 (params.saddle_search_options().max_energy < barriersValues[1])) {
394 }
395
396 if (barriersValues[0] < 0.0 || barriersValues[1] < 0.0) {
398 }
399
400 if (!params.prefactor_options().default_value) {
401 fctmp = min1->getPotentialCalls();
402 int prefStatus;
403 double pref1, pref2;
405 params, min1.get(), saddle.get(), min2.get(), pref1, pref2);
406 if (prefStatus == -1) {
407 EONC_LOG_ERROR("Prefactor: bad calculation");
409 }
410 fCallsPrefactors += min1->getPotentialCalls() - fctmp;
411
412 if ((pref1 > params.prefactor_options().max_value) ||
413 (pref1 < params.prefactor_options().min_value)) {
414 EONC_LOG_ERROR("Bad reactant-to-saddle prefactor: {}", pref1);
416 }
417 if ((pref2 > params.prefactor_options().max_value) ||
418 (pref2 < params.prefactor_options().min_value)) {
419 EONC_LOG_ERROR("Bad product-to-saddle prefactor: {}", pref2);
421 }
422 prefactorsValues[0] = pref1;
423 prefactorsValues[1] = pref2;
424
425 } else {
426 prefactorsValues[0] = params.prefactor_options().default_value;
427 prefactorsValues[1] = params.prefactor_options().default_value;
428 }
430}
431
433 std::string resultsFilename("results.dat");
434 returnFiles.push_back(resultsFilename);
435
436 std::ofstream out(resultsFilename, std::ios::binary);
437 if (out) {
438 out << std::format("{} termination_reason\n", status);
439 out << std::format("{} termination_reason_text\n",
440 saddleSearch->describeStatus(status));
441 out << std::format("{} random_seed\n", params.main_options().randomSeed);
442 out << std::format(
443 "{} potential_type\n",
444 magic_enum::enum_name<PotType>(params.potential_options().potential));
445 out << std::format("{} total_force_calls\n",
447 out << std::format("{} force_calls_minimization\n", fCallsMin);
448 out << std::format("{} force_calls_saddle\n", fCallsSaddle);
449 out << std::format("{:.12e} potential_energy_saddle\n",
450 saddle->getPotentialEnergy());
451 out << std::format("{:.12e} potential_energy_reactant\n",
452 min1->getPotentialEnergy());
453 out << std::format("{:.12e} potential_energy_product\n",
454 min2->getPotentialEnergy());
455 out << std::format("{:.12e} barrier_reactant_to_product\n",
456 barriersValues[0]);
457 out << std::format("{:.12e} barrier_product_to_reactant\n",
458 barriersValues[1]);
459 if (params.saddle_search_options().method == "min_mode") {
460 out << std::format("{:.12e} displacement_saddle_distance\n",
461 displacement->perAtomNorm(*saddle));
462 } else {
463 out << std::format("{:.12e} displacement_saddle_distance\n", 0.0);
464 }
465 if (params.saddle_search_options().method == "dynamics") {
466 auto ds = dynamic_cast<DynamicsSaddleSearch &>(*saddleSearch);
467 out << std::format("{:.12e} simulation_time\n",
468 ds.time * params.constants().timeUnit);
469 out << std::format("{:.12e} md_temperature\n",
470 params.saddle_search_options().dynamics.temperature);
471 }
472 out << std::format("{} force_calls_prefactors\n", fCallsPrefactors);
473 out << std::format("{:.12e} prefactor_reactant_to_product\n",
475 out << std::format("{:.12e} prefactor_product_to_reactant\n",
477 }
478
479 std::string reactantFilename("reactant.con");
480 returnFiles.push_back(reactantFilename);
481 if (!eonc::io::io_ok(min1->matter2con(reactantFilename))) {
482 QUILL_LOG_ERROR(log, "Failed to write {}", reactantFilename);
483 }
484
485 std::string modeFilename("mode.dat");
486 returnFiles.push_back(modeFilename);
487 eonc::helpers::saveMode(modeFilename, saddle, saddleSearch->getEigenvector());
488
489 std::string saddleFilename("saddle.con");
490 returnFiles.push_back(saddleFilename);
491 if (!eonc::io::io_ok(saddle->matter2con(saddleFilename))) {
492 QUILL_LOG_ERROR(log, "Failed to write {}", saddleFilename);
493 }
494
495 std::string productFilename("product.con");
496 returnFiles.push_back(productFilename);
497 if (!eonc::io::io_ok(min2->matter2con(productFilename))) {
498 QUILL_LOG_ERROR(log, "Failed to write {}", productFilename);
499 }
500}
501
503 auto msg = saddleSearch->describeStatus(status);
504 if (status == MinModeSaddleSearch::STATUS_GOOD) {
505 QUILL_LOG_DEBUG(log, "[Saddle Search] {}", msg);
506 } else {
507 QUILL_LOG_ERROR(log, "[Saddle Search] {}", msg);
508 }
509}
510
511} // namespace eonc
Eigen::Matrix< double, Eigen::Dynamic, 3, eOnStorageOrder > AtomMatrix
Definition Eigen.h:37
#define EONC_LOG_DEBUG(...)
Definition EonLogger.h:243
#define EONC_LOG_ERROR(...)
Definition EonLogger.h:261
#define EONC_LOG_CRITICAL(...)
Definition EonLogger.h:267
The optimizer class is used to serve as an abstract class for all optimizers, as well as to call an o...
Finds possible escape mecahnisms from a state.
std::shared_ptr< Potential > pot
Definition Job.h:63
Parameters params
Definition Job.h:58
static const char MINMODE_GPRDIMER[]
bool compare(const Matter &matter, bool indistinguishable=false)
Definition Matter.cpp:184
std::shared_ptr< Matter > displacement
Configuration used during the saddle point search.
size_t fCallsPrefactors
Force calls to find the prefactors.
eonc::log::Scoped log
size_t fCallsMin
Force calls to minimize.
std::shared_ptr< Matter > min1
First minimum from the saddle.
std::vector< std::string > returnFiles
Container for the results of the run.
void printEndState(int status)
Logs the run status and makes sure the run was successful.
std::shared_ptr< Matter > min2
Second minimum from the saddle.
std::shared_ptr< Matter > saddle
Configuration used during the saddle point search.
std::vector< std::string > run(void) override
Kicks off the Process Search.
std::shared_ptr< Matter > initial
Initial configuration.
std::unique_ptr< SaddleSearchMethod > saddleSearch
Pulled from parameters.
size_t fCallsSaddle
Force calls to find the saddle.
void saveData(int status)
Writes the results from the run to file.
std::shared_ptr< Matter > runFromMatter(std::shared_ptr< Matter > seed)
In-process entry: seed reactant Matter, no pos.con.
std::shared_ptr< Matter > runPrepared()
int doProcessSearch(void)
Runs the correct saddle search; also checks if the run was successful.
const char DISP_LOAD[]
Definition EpiCenters.h:20
int getPrefactors(const Parameters &parameters, Matter *min1, Matter *saddle, Matter *min2, double &pref1, double &pref2)
Definition Prefactor.cpp:24
bool applyClientDisplacement(Matter &target, const Matter &initial, const Parameters &params, AtomMatrix *modeOut)
std::string getRelevantFile(std::string filename)
bool loadOrSynthesizeDisplacement(Matter &target, const Matter &initial, const std::string &displacementPath, const std::string &modePath, double scale)
AtomMatrix loadMode(FILE *modeFile, int nAtoms)
void saveMode(FILE *modeFile, std::shared_ptr< Matter > matter, AtomMatrix mode)
Write a mode; constrained axes are emitted as 0.
std::shared_ptr< Potential > makePotential(const Parameters &params)
constexpr bool io_ok(IoStatus s) noexcept
Definition ConFileIO.h:38
RAII resource manager for the ARTn C library with global synchronization.
bool potAllowsSharedInstance(const P &p) noexcept