Loading...
Searching...
No Matches
BasinHoppingJob.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
13#include "eon/EonLogger.h"
14#include <cmath>
15#include <format>
16#include <fstream>
17#include <stdexcept>
18#include <string>
19
20#include "eon/BaseStructures.h"
21#include "eon/BasinHoppingJob.h"
22#include "eon/Dynamics.h"
23#include "eon/HelperFunctions.h"
24#include "eon/JobResult.h"
26#include "eon/Optimizer.h"
27#include "eon/PotRegistry.h"
28#include "eon/Potential.h"
29
30namespace eonc {
31
32std::vector<std::string> BasinHoppingJob::run() {
33 bool swapMove;
34 double swap_accept = 0.0;
35 jump_count = 0; // count of jump movies
36 swap_count = 0; // count of swap moves
37 disp_count = 0; // count of displacement moves
38 // Quench tail may not run when stop_energy breaks first.
39 int quench_displacements = 0;
40 int consecutive_rejected_trials = 0;
41 double totalAccept = 0.0;
42 std::unique_ptr<Matter> minTrial = std::make_unique<Matter>(pot, params);
43 std::unique_ptr<Matter> swapTrial = std::make_unique<Matter>(pot, params);
44
45 std::string conFilename =
46 eonc::helpers::getRelevantFile(params.main_options().conFilename);
47 if (!eonc::io::io_ok(current->con2matter(conFilename))) {
48 QUILL_LOG_CRITICAL(log, "Failed to load {}", conFilename);
49 throw std::runtime_error("failed to load " + conFilename);
50 }
51
52 // Sanity Check
53 std::vector<long> Elements;
54 Elements = getElements(current.get());
55 if (params.basin_hopping_options().swap_probability > 0 &&
56 Elements.size() == 1) {
58 QUILL_LOG_CRITICAL(log,
59 "error: [Basin Hopping] swap move probability must be "
60 "zero if there is only one element type\n");
61 throw std::invalid_argument(
62 "[Basin Hopping] swap_probability must be zero with one element type");
63 }
64
65 double randomProb =
66 params.basin_hopping_options().initial_random_structure_probability;
67 if (randomProb > 0.0) {
68 QUILL_LOG_DEBUG(log, "generating random structure with probability {:.4f}",
69 randomProb);
70 }
71 double u = eonc::rng::random();
72 if (u < params.basin_hopping_options().initial_random_structure_probability) {
73 AtomMatrix randomPositions = current->getPositionsFree();
74 for (int i = 0; i < current->numberOfFreeAtoms(); i++) {
75 for (int j = 0; j < 3; j++) {
76 randomPositions(i, j) = eonc::rng::random();
77 }
78 }
79 randomPositions *= current->getCell();
80 current->setPositionsFree(randomPositions);
81
83 current, params.basin_hopping_options().push_apart_distance);
84 }
85
86 *trial = *current;
87 *minTrial = *current;
88
89 current->relax(true);
90
91 double currentEnergy = current->getPotentialEnergy();
92 double minimumEnergy = currentEnergy;
93
94 auto minimumEnergyStructure = std::make_shared<Matter>(pot, params);
95 *minimumEnergyStructure = *current;
96 int nsteps = params.basin_hopping_options().steps +
97 params.basin_hopping_options().quenching_steps;
98
99 QUILL_LOG_DEBUG(
100 log, "[Basin Hopping] {:4s} {:12s} {:12s} {:12s} {:4s} {:5s} {:5s}",
101 "step", "current", "trial", "global min", "fc", "ar", "md");
102 QUILL_LOG_DEBUG(
103 log, "[Basin Hopping] {:4s} {:12s} {:12s} {:12s} {:4s} {:5s} {:5s}",
104 "----", "-------", "-----", "----------", "--", "--", "--");
105
106 int recentAccept = 0;
107 double curDisplacement = params.basin_hopping_options().displacement;
108
109 for (int step = 0; step < nsteps; step++) {
110
111 // Swap or displace
112 if (eonc::rng::randomDouble(1.0) <
113 params.basin_hopping_options().swap_probability &&
114 step < params.basin_hopping_options().steps) {
115 *swapTrial = *current;
116 randomSwap(swapTrial.get());
117 swapMove = true;
118 *minTrial = *swapTrial;
119 } else {
120 AtomMatrix displacement;
121 displacement = displaceRandom(curDisplacement);
122 if (step >= params.basin_hopping_options().steps) {
123 quench_displacements++;
124 }
125
126 trial->setPositions(current->getPositions() + displacement);
127 swapMove = false;
129 trial, params.basin_hopping_options().push_apart_distance);
130
131 *minTrial = *trial;
132 }
133
134 if (params.debug_options().write_movies) {
135 if (!eonc::io::io_ok(trial->matter2con("trials", true))) {
136 QUILL_LOG_WARNING(log, "Failed to append trials movie frame");
137 }
138 }
139
140 minTrial->relax(true);
141
142 double deltaE = minTrial->getPotentialEnergy() - currentEnergy;
143 double p = 0.0;
144 if (step >= params.basin_hopping_options().steps) {
145 if (deltaE <= 0.0) {
146 p = 1.0;
147 }
148 } else {
149 // Divide only for an uphill hop at positive temperature.
150 p = metropolisProbability(deltaE, params.constants().kB,
151 params.main_options().temperature);
152 }
153
154 bool accepted = false;
155 if (eonc::rng::randomDouble(1.0) < p) {
156 accepted = true;
157 if (params.basin_hopping_options().significant_structure) {
158 *current = *minTrial;
159 } else if (swapMove) {
160 *current = *swapTrial;
161 } else {
162 *current = *trial;
163 }
164 if (swapMove) {
165 swap_accept += 1;
166 }
167 if (step < params.basin_hopping_options().steps) {
168 totalAccept += 1;
169 recentAccept += 1;
170 }
171
172 currentEnergy = minTrial->getPotentialEnergy();
173
174 if (currentEnergy < minimumEnergy) {
175 minimumEnergy = currentEnergy;
176 *minimumEnergyStructure = *minTrial;
177 if (!eonc::io::io_ok(minimumEnergyStructure->matter2con("min.con"))) {
178 QUILL_LOG_WARNING(log, "Failed to write min.con");
179 }
180 }
181
182 if (params.basin_hopping_options().write_unique) {
183 bool newStructure = true;
184 for (unsigned int i = 0; i < uniqueEnergies.size(); i++) {
185 // if minTrial has a different energy or a different structure
186 // it is new, otherwise it is old
187 if (std::fabs(currentEnergy - uniqueEnergies[i]) <
188 params.structure_comparison_options().energy_difference) {
189 Matter probe = *current;
190 if (probe.compare(*uniqueStructures[i],
191 params.structure_comparison_options()
192 .indistinguishable_atoms)) {
193 newStructure = false;
194 }
195 }
196 }
197
198 if (newStructure) {
199 uniqueEnergies.push_back(currentEnergy);
200 auto currentCopy = std::make_shared<Matter>(pot, params);
201 *currentCopy = *current;
202 uniqueStructures.push_back(currentCopy);
203
204 char fname[128];
205 snprintf(fname, 128, "min_%.5i.con", step + 1);
206 if (!eonc::io::io_ok(current->matter2con(fname))) {
207 QUILL_LOG_WARNING(log, "Failed to write {}", fname);
208 }
209 returnFiles.push_back(fname);
210
211 snprintf(fname, 128, "energy_%.5i.dat", step + 1);
212 returnFiles.push_back(fname);
213 {
214 std::ofstream fh(fname);
215 if (fh)
216 fh << std::format("{:.10e}\n", currentEnergy);
217 }
218 }
219 }
220
221 consecutive_rejected_trials = 0; // STC: I think this should go here.
222 } else {
223 consecutive_rejected_trials++;
224 }
225
226 if (params.debug_options().write_movies) {
227 if (!eonc::io::io_ok(minTrial->matter2con("movie", true))) {
228 QUILL_LOG_WARNING(log, "Failed to append basin-hopping movie frame");
229 }
230 }
231
232 if (minimumEnergy < params.basin_hopping_options().stop_energy) {
233 break;
234 }
235
236 if (consecutive_rejected_trials ==
237 params.basin_hopping_options().jump_max &&
238 step < params.basin_hopping_options().steps) {
239 consecutive_rejected_trials = 0;
240 AtomMatrix jump;
241 for (int j = 0; j < params.basin_hopping_options().jump_steps; j++) {
242 jump_count++;
243 jump = displaceRandom(curDisplacement);
244 current->setPositions(current->getPositions() + jump);
245 // Only a minimized jump is a basin. The raw geometry must not become
246 // the Metropolis reference or the stored global minimum.
247 if (params.basin_hopping_options().significant_structure) {
249 current, params.basin_hopping_options().push_apart_distance);
250 current->relax(true);
251 currentEnergy = current->getPotentialEnergy();
252 if (currentEnergy < minimumEnergy) {
253 minimumEnergy = currentEnergy;
254 *minimumEnergyStructure = *current;
255 }
256 }
257 }
258 }
259
260 int nadjust = params.basin_hopping_options().adjust_period;
261 double adjustFraction = params.basin_hopping_options().adjust_fraction;
262 // A zero period is not an interval; the modulo would divide by zero.
263 if (nadjust != 0 && (step + 1) % nadjust == 0 &&
264 params.basin_hopping_options().adjust_displacement) {
265 double recentRatio =
266 static_cast<double>(recentAccept) / static_cast<double>(nadjust);
267 if (recentRatio > params.basin_hopping_options().target_ratio) {
268 curDisplacement *= 1.0 + adjustFraction;
269 } else {
270 curDisplacement *= 1.0 - adjustFraction;
271 }
272
273 recentAccept = 0;
274 }
275 }
276 /* Save Results */
277
278 std::string resultsFilename("results.dat");
279
280 if (params.debug_options().write_movies) {
281 std::string movieFilename("movie.con");
282 returnFiles.push_back(movieFilename);
283 }
284
285 {
287 RunStatus::GOOD, params.potential_options().potential,
288 PotRegistry::get().total_force_calls(), true, minimumEnergy);
289 env.job_type = "basin_hopping";
290 env.random_seed = params.main_options().randomSeed;
291 env.extras.emplace_back("minimum_energy", minimumEnergy);
292 const double nsteps_ratio = params.basin_hopping_options().steps;
293 env.extras.emplace_back("acceptance_ratio",
294 nsteps_ratio ? totalAccept / nsteps_ratio : 0.0);
295 if (params.basin_hopping_options().swap_probability > 0) {
296 env.extras.emplace_back(
297 "swap_acceptance_ratio",
298 swap_count ? swap_accept / static_cast<double>(swap_count) : 0.0);
299 }
300 env.extras.emplace_back(
301 "total_normal_displacement_steps",
302 static_cast<double>(disp_count - jump_count - quench_displacements));
303 env.extras.emplace_back("total_jump_steps",
304 static_cast<double>(jump_count));
305 env.extras.emplace_back("total_swap_steps",
306 static_cast<double>(swap_count));
307 env.writeResultsDat(resultsFilename);
308 returnFiles.push_back(resultsFilename);
309 }
310
311 std::string productFilename("min.con");
312 if (eonc::io::io_ok(minimumEnergyStructure->matter2con(productFilename))) {
313 returnFiles.push_back(productFilename);
314 } else {
315 QUILL_LOG_ERROR(log, "Failed to write {}", productFilename);
316 }
317
318 // minTrial and swapTrial automatically cleaned up by unique_ptr
319 return returnFiles;
320}
321
322double BasinHoppingJob::metropolisProbability(double de, double kB,
323 double temperature) {
324 if (!(de > 0.0)) {
325 return 1.0;
326 }
327 if (!(temperature > 0.0) || !(kB > 0.0)) {
328 return 0.0;
329 }
330 return std::exp(-de / (kB * temperature));
331}
332
334 disp_count++;
335 // Create a random displacement
336 AtomMatrix displacement;
337 displacement.resize(trial->numberOfAtoms(), 3);
338 displacement.setZero();
339 VectorXd distvec = calculateDistanceFromCenter(current.get());
340 // Coincident atoms share a zero radius. Dividing by it writes NaN and
341 // the hop cannot leave the center. Those atoms are the outer shell, so
342 // the step stays the unscaled displacement.
343 const double radius = distvec.size() > 0 ? distvec.maxCoeff() : 0.0;
344 int num = trial->numberOfAtoms();
345 int m = 0;
346 if (params.basin_hopping_options().single_atom_displace) {
347 m = eonc::rng::randomInt(0, trial->numberOfAtoms() - 1);
348 num = m + 1;
349 }
350
351 for (int i = m; i < num; i++) {
352 double dist = distvec(i);
353 double disp = 0.0; // displacement size, possibly scaled
354
355 if (!trial->getFixed(i)) {
356 const std::string &algorithm =
357 params.basin_hopping_options().displacement_algorithm;
358 if (algorithm == "standard") {
359 disp = curDisplacement;
360 }
361 // scale displacement linearly with the particle radius
362 else if (algorithm == "linear") {
363 disp =
364 radius == 0.0 ? curDisplacement : curDisplacement * (dist / radius);
365 }
366 // scale displacement quadratically with the particle radius
367 else if (algorithm == "quadratic") {
368 if (radius == 0.0) {
369 disp = curDisplacement;
370 } else {
371 const double scale = dist / radius;
372 disp = curDisplacement * scale * scale;
373 }
374 } else {
376 QUILL_LOG_CRITICAL(log, "Unknown displacement_algorithm\n");
377 throw std::invalid_argument(
378 std::format("[Basin Hopping] unknown displacement_algorithm: {}",
379 params.basin_hopping_options().displacement_algorithm));
380 }
381 for (int j = 0; j < 3; j++) {
382 if (params.basin_hopping_options().displacement_distribution ==
383 "uniform") {
384 displacement(i, j) = eonc::rng::randomDouble(2 * disp) - disp;
385 } else if (params.basin_hopping_options().displacement_distribution ==
386 "gaussian") {
387 displacement(i, j) = eonc::rng::gaussRandom(0.0, disp);
388 } else {
390 QUILL_LOG_CRITICAL(log, "Unknown displacement_distribution\n");
391 throw std::invalid_argument(std::format(
392 "[Basin Hopping] unknown displacement_distribution: {}",
393 params.basin_hopping_options().displacement_distribution));
394 }
395 }
396 }
397 }
398 return displacement;
399}
400
402 swap_count++;
403 std::vector<long> Elements;
404 Elements = getElements(matter);
405
406 long ela;
407 long elb;
408 long ia = eonc::rng::randomInt(0, Elements.size() - 1);
409 ela = Elements.at(ia);
410 Elements.erase(Elements.begin() + ia);
411
412 long ib = eonc::rng::randomInt(0, Elements.size() - 1);
413 elb = Elements.at(ib);
414
415 int changera = 0;
416 int changerb = 0;
417
418 changera = eonc::rng::randomInt(0, matter->numberOfAtoms() - 1);
419 int guard = 0;
420 while ((matter->getAtomicNr(changera) != ela || matter->getFixed(changera)) &&
421 guard < 10000) {
422 changera = eonc::rng::randomInt(0, matter->numberOfAtoms() - 1);
423 guard++;
424 }
425 changerb = eonc::rng::randomInt(0, matter->numberOfAtoms() - 1);
426 guard = 0;
427 while ((matter->getAtomicNr(changerb) != elb || matter->getFixed(changerb) ||
428 changerb == changera) &&
429 guard < 10000) {
430 changerb = eonc::rng::randomInt(0, matter->numberOfAtoms() - 1);
431 guard++;
432 }
433 if (matter->getAtomicNr(changera) != ela || matter->getFixed(changera) ||
434 matter->getAtomicNr(changerb) != elb || matter->getFixed(changerb)) {
435 return;
436 }
437
438 double posax = matter->getPosition(changera, 0);
439 double posay = matter->getPosition(changera, 1);
440 double posaz = matter->getPosition(changera, 2);
441
442 matter->setPosition(changera, 0, matter->getPosition(changerb, 0));
443 matter->setPosition(changera, 1, matter->getPosition(changerb, 1));
444 matter->setPosition(changera, 2, matter->getPosition(changerb, 2));
445
446 matter->setPosition(changerb, 0, posax);
447 matter->setPosition(changerb, 1, posay);
448 matter->setPosition(changerb, 2, posaz);
449}
450
451std::vector<long> BasinHoppingJob::getElements(Matter *matter) {
452 // Z is 0..118. A 118-slot table stores 118 one past the end.
453 constexpr int kElementSlots = 119;
454 std::array<int, kElementSlots> allElements{};
455 std::vector<long> elements;
456
457 for (long y = 0; y < matter->numberOfAtoms(); ++y) {
458 if (!matter->getFixed(y)) {
459 const long z = matter->getAtomicNr(y);
460 if (z >= 0 && z < kElementSlots) {
461 allElements[static_cast<size_t>(z)] = 1;
462 }
463 }
464 }
465
466 for (int i = 0; i < kElementSlots; ++i) {
467 if (allElements[static_cast<size_t>(i)] != 0) {
468 elements.push_back(i);
469 }
470 }
471
472 return elements;
473}
474
476 AtomMatrix pos = matter->getPositions();
477 Vector3d cen(0, 0, 0);
478 int num = matter->numberOfAtoms();
479
480 cen = pos.colwise().sum() / static_cast<double>(num);
481
482 VectorXd dist(num);
483
484 for (int n = 0; n < num; n++) {
485 pos.row(n) -= cen;
486 dist(n) = pos.row(n).norm();
487 }
488
489 return dist;
490}
491
492} // namespace eonc
Eigen::Matrix< double, Eigen::Dynamic, 3, eOnStorageOrder > AtomMatrix
Definition Eigen.h:37
The optimizer class is used to serve as an abstract class for all optimizers, as well as to call an o...
std::vector< std::string > returnFiles
std::vector< std::shared_ptr< Matter > > uniqueStructures
VectorXd calculateDistanceFromCenter(Matter *matter)
std::shared_ptr< Matter > current
std::vector< long > getElements(Matter *matter)
void randomSwap(Matter *matter)
AtomMatrix displaceRandom(double maxDisplacement)
std::vector< double > uniqueEnergies
eonc::log::Scoped log
static double metropolisProbability(double de, double kB, double temperature)
Metropolis weight for energy change de.
std::shared_ptr< Matter > trial
std::vector< std::string > run(void) override
Virtual run; used solely for dynamic dispatch.
std::shared_ptr< Potential > pot
Definition Job.h:63
Parameters params
Definition Job.h:58
void setPosition(long int atom, int axis, double position)
Definition Matter.cpp:290
const AtomMatrix & getPositions() const
Definition Matter.cpp:308
bool compare(const Matter &matter, bool indistinguishable=false)
Definition Matter.cpp:184
long int numberOfAtoms() const
Definition Matter.cpp:273
double getPosition(long int atom, int axis) const
Definition Matter.cpp:284
long getAtomicNr(long int atom) const
Definition Matter.cpp:495
int getFixed(long int atom) const
1 if every Cartesian axis of the atom is fixed, else 0.
Definition Matter.cpp:505
static PotRegistry & get() noexcept
Process-lifetime singleton.
void pushApart(std::shared_ptr< Matter > m1, double minDistance)
std::string getRelevantFile(std::string filename)
constexpr bool io_ok(IoStatus s) noexcept
Definition ConFileIO.h:38
quill::Logger * traceback() noexcept
Get or create the "_traceback" logger for traceback logging.
Definition EonLogger.h:88
double random(long newSeed=0)
long randomInt(int lower, int upper)
double randomDouble()
double gaussRandom(double avg, double std)
RAII resource manager for the ARTn C library with global synchronization.
static JobResultEnvelope fromMinimization(RunStatus status, PotType pot, std::uint64_t fcalls, bool hasE, double energy)
Definition JobResult.h:101