Loading...
Searching...
No Matches
AMS.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
14#include "eon/Parameters.h"
15#include <algorithm>
16#include <cctype>
17#include <cstddef>
18#include <cstdint>
19#include <format>
20#include <fstream>
21#include <iterator>
22#include <ranges>
23#include <readcon-core.hpp>
24#include <stdexcept>
25#include <vector>
26
27namespace bp = boost::process;
28
30 : eonc::Potential(eonc::PotType::AMS, p) {
31 // Get the values from the configuration
32 // All the parameter values convert to lowercase in generate_run
33 this->engine = p.ams_options().engine;
35 this->model = p.ams_options().model;
36 this->xc = p.ams_options().xc;
38 this->basis = p.ams_options().basis;
39 this->engine_setup = generate_run(p);
40 // Environment
41 // TODO: Add more checks for how this can be set
42 if (p.ams_options().env.amshome.empty() &&
43 p.ams_options().env.scm_tmpdir.empty() &&
44 p.ams_options().env.scmlicense.empty() &&
45 p.ams_options().env.scm_pythondir.empty() &&
46 p.ams_options().env.amsbin.empty() &&
47 p.ams_options().env.amsresources.empty()) {
48 nativenv = boost::this_process::environment();
49 } else {
50 nativenv = boost::this_process::environment();
51 // Some of these can derive from the others
52 nativenv["AMSHOME"] = p.ams_options().env.amshome;
53 nativenv["SCM_TMPDIR"] = p.ams_options().env.scm_tmpdir;
54 nativenv["SCMLICENSE"] = p.ams_options().env.scmlicense;
55 nativenv["SCM_PYTHONDIR"] = p.ams_options().env.scm_pythondir;
56 nativenv["AMSBIN"] = p.ams_options().env.amsbin;
57 nativenv["AMSRESOURCES"] = p.ams_options().env.amsresources;
58 nativenv["PATH"] += p.ams_options().env.amsbin;
59 }
60 // Do not pass "" in the config files
61 amsevals = 0;
62 // TODO: Optimize and reuse existing files Currently each Matter will
63 // recreate the folders It should instead figure out if results exist and
64 // use them
65 this->first_run = true;
66 // Determine if the engine supports restarts
67 if (engine == "MOPAC") {
68 this->can_restart = false;
69 this->cjob = "amsResults";
70 this->pjob = "amsResults";
71 } else {
72 this->can_restart = true;
73 this->cjob = "firstRun";
74 this->pjob = "secondRun";
75 }
76 return;
77}
78
79void AMS::cleanMemory(void) { return; }
80
82
83namespace {
84
85// The driver script, written afresh before every AMS invocation.
86constexpr const char *kRunScript = "run_AMS.sh";
87
88std::string symbol_for_z(int n) {
89 if (n <= 0) {
90 throw std::runtime_error(
91 std::format("AMS knows no element symbol for atomic number {}", n));
92 }
93 return readcon::z_to_symbol(static_cast<uint64_t>(n));
94}
95} // namespace
96
98 boost::asio::io_context amsRun;
99 std::future<std::string> run_out_future, run_err_future;
100 std::string runout, runerr;
101 if (chmod(kRunScript, S_IRWXU) != 0) {
102 throw std::runtime_error(
103 std::format("Could not make {} executable", kRunScript));
104 }
105 nativenv["AMS_JOBNAME"] = cjob;
106 bp::child c(std::string(kRunScript),
107 nativenv, // set the input
108 bp::std_in.close(), // no input
109 bp::std_out > run_out_future, // STDOUT
110 bp::std_err > run_err_future, // STDERR
111 amsRun);
112 amsRun.run();
113 runout = run_out_future.get();
114 runerr = run_err_future.get();
115 if (runerr.find("ERROR") != std::string::npos) {
116 bp::spawn("cat myrestart.in");
117 bp::spawn("cat run_AMS.sh");
118 throw std::runtime_error("\n AMS error while running");
119 } else {
120 this->amsevals = amsevals + 1;
121 }
122}
123
124double AMS::extract_scalar_rkf(std::string key) {
125 // The logic here handles the extraction asynchronously, as we lack
126 // interest in processing this output line by line, and would prefer to
127 // have it all in one place
128 std::string execString;
129 std::vector<std::string> execDat;
130 boost::asio::io_context rkf;
131 std::future<std::string> rkf_out_future, rkf_err_future;
132 std::string rkfout, rkferr;
133 double xval, x;
134 std::vector<double> extracted;
135 execString = std::format("dmpkf {}.results/{}.rkf AMSResults%{}", this->cjob,
136 this->engine_lower, key);
137 // Extract
138 bp::child c(execString, nativenv, // execute with the environment
139 bp::std_in.close(), // no input
140 bp::std_out > rkf_out_future, // STDOUT
141 bp::std_err > rkf_err_future, // STDERR
142 rkf);
143
144 rkf.run(); // this blocks until the end of the command
145 // Populate the strings
146 rkfout = rkf_out_future.get();
147 rkferr = rkf_err_future.get();
148 if (rkferr.find("ERROR") != std::string::npos) {
149 throw std::runtime_error(std::format(
150 "\n AMS error while extracting {}, got:\n {}", key, rkferr));
151 }
152
153 execDat = absl::StrSplit(rkfout, '\n');
154 // Is energy or some other scalar property
155 // First 3 lines hold the headers
156 // [0] = "AMSResults "
157 // [1] = "Energy "
158 // [2] = " 1 1 2"
159 // [3] = " 0.135012547958282714E+000"
160 // [4] = ""
161 if (execDat.size() < 4) {
162 throw std::runtime_error(
163 std::format("\n AMS returned {} lines for {}, too few to hold a value",
164 execDat.size(), key));
165 }
166 if (absl::SimpleAtod(execDat[3], &x)) {
167 xval = x * this->energyConversion;
168 return xval;
169 } else {
170 throw std::runtime_error(
171 std::format("\n Expected {}, got {} instead", key, execDat[3]));
172 }
173 // Never reach here
174 throw std::runtime_error("Generic AMS dmpkf scalar error \n");
175}
176
177std::vector<double> AMS::extract_cartesian_rkf(std::string key) {
178 std::string execString;
179 std::vector<std::string> execDat, innerDat;
180 boost::asio::io_context rkf;
181 std::future<std::string> rkf_out_future, rkf_err_future;
182 std::string rkfout, rkferr;
183 double felem, x;
184 std::vector<double> extracted;
185 execString = std::format("dmpkf {}.results/{}.rkf AMSResults%{}", this->cjob,
186 this->engine_lower, key);
187
188 // Extract
189 bp::child c(execString, nativenv, // execute with the environment
190 bp::std_in.close(), // no input
191 bp::std_out > rkf_out_future, // STDOUT
192 bp::std_err > rkf_err_future, // STDERR
193 rkf);
194
195 rkf.run(); // this blocks until the end of the command
196 // Populate the strings
197 rkfout = rkf_out_future.get();
198 rkferr = rkf_err_future.get();
199
200 if (rkferr.find("ERROR") != std::string::npos) {
201 throw std::runtime_error(std::format(
202 "\n AMS error while extracting {}, got:\n {}", key, rkferr));
203 }
204
205 execDat = absl::StrSplit(rkfout, '\n');
206 // Assume gradients or some other x y z property
207 for (int i = 3; i < execDat.size(); i++) {
208 // There exist one per atom, the number of which equals N
209 // The first three lines hold the header files as before
210 std::vector<std::string> strrow = absl::StrSplit(execDat[i], ' ');
211 for (auto elem : strrow) {
212 // This loop uses an auto variable since some of the elements of strrow
213 // often evaluate to ' '
214 // TODO: Optimize, perhaps with a regex
215 // [0] = ""
216 // [1] = ""
217 // [2] = ""
218 // [3] = "0.798885933949825835E-004"
219 // [4] = ""
220 // [5] = "-0.755400038198822616E-004"
221 // [6] = ""
222 // [7] = ""
223 // [8] = "0.457005740252359742E-004"
224 if (!elem.empty() && absl::SimpleAtod(elem, &x)) {
225 felem = x * this->forceConversion;
226 extracted.emplace_back(felem);
227 }
228 }
229 }
230 return extracted;
231}
232
233void AMS::updateCoord(long N, const double *R) {
234 // The logic used here requires us to update the PREVIOUS job, since that
235 // functions as the one from which the calculation restarts
236 // Only the third line presents vague complexity
237 // https://www.scm.com/doc/Scripting/Commandline_Tools/KF_command_line_utilities.html
238 std::ofstream updCoord;
239 std::string execString, coordDump, newCoord;
240 std::vector<std::string> execDat;
241 boost::asio::io_context coordio;
242 std::future<std::string> err, rdump;
243 std::vector<double> gradients;
244 // Prep new run
245 // Get the previous run's coordinates
246 execString = std::format("dmpkf {}.results/ams.rkf Molecule%Coords", pjob);
247 // Store Coordinates
248 // TODO: Simplify this, we only need the first few lines
249 bp::child cprog(execString, nativenv, bp::std_in.close(), bp::std_out > rdump,
250 bp::std_err > err, coordio);
251 coordio.run();
252 execDat = absl::StrSplit(rdump.get(), '\n');
253 // Get the first three lines
254 int counter = 0;
255 for (auto j : execDat) {
256 if (counter >= 3) {
257 break;
258 } else {
259 absl::StrAppend(&newCoord, j, "\n");
260 }
261 counter++;
262 }
263 // Put the rest of the coordinates
264 counter = 0;
265 for (int a = 0; a < N * 3; a++) {
266 absl::StrAppend(&newCoord, (R[a] * lengthConversion), " ");
267 counter++;
268 if (counter % 3 == 0) {
269 absl::StrAppend(&newCoord, "\n");
270 }
271 }
272 coordDump = "#!/bin/sh\n udmpkf ";
273 absl::StrAppend(&coordDump, pjob, ".results/ams.rkf <<EOF\n", newCoord,
274 "EOF");
275 updCoord.open("updCoord.sh", std::ios::trunc);
276 if (!updCoord) {
277 throw std::runtime_error("Could not open updCoord.sh for writing");
278 }
279 updCoord << coordDump;
280 updCoord.close();
281 if (!updCoord) {
282 throw std::runtime_error("Could not write the coordinates to updCoord.sh");
283 }
284 // bp::spawn("chmod +x updCoord.sh");
285 if (chmod("updCoord.sh", S_IRWXU) != 0) {
286 throw std::runtime_error("Could not make updCoord.sh executable");
287 }
288 bp::child cuprog("updCoord.sh", nativenv, bp::std_err > bp::null);
289 cuprog.wait();
290 return;
291}
292
294 std::string tmp;
295 tmp = this->cjob;
296 this->cjob = this->pjob;
297 this->pjob = tmp;
298}
299
301 std::string restart_formatter;
302 if (can_restart) {
303 restart_formatter = R"(
304 EngineRestart {0}.results/{1}.rkf
305 LoadSystem
306 File {0}.results/ams.rkf
307 Section Molecule
308 End
309 )";
310 } else {
311 restart_formatter = R"(
312 LoadSystem
313 File {0}.results/ams.rkf
314 Section Molecule
315 End
316 )";
317 }
318 std::string restart_data = std::format(restart_formatter, pjob, engine_lower);
319 restartFrom.open("myrestart.in", std::ios::trunc);
320 if (!restartFrom) {
321 throw std::runtime_error("Could not open myrestart.in for writing");
322 }
323 restartFrom << restart_data;
324 restartFrom.close();
325 if (!restartFrom) {
326 throw std::runtime_error("Could not write the restart block to "
327 "myrestart.in");
328 }
329 return;
330}
331
332void AMS::copyForces(long N, const std::vector<double> &frc, double *F) {
333 // A short AMS run yields fewer than 3N values; copying blindly would hand
334 // the optimizer whatever lies past the end of the vector.
335 if (frc.size() < static_cast<std::size_t>(3 * N)) {
336 throw std::runtime_error(
337 std::format("\n AMS returned {} gradient components, expected {}",
338 frc.size(), 3 * N));
339 }
340 for (long i = 0; i < 3 * N; i++) {
341 F[i] = frc[i];
342 }
343}
344
345void AMS::force(long N, const double *R, const int *atomicNrs, double *F,
346 double *U, double *variance, const double *box) {
347 variance = nullptr;
348 if (not can_restart or first_run) {
349 // This holds true for all engines with no restart Also if an
350 // engine supports being restarted, the first run needs this
351 passToSystem(N, R, atomicNrs, box);
352 runAMS();
353 copyForces(N, extract_cartesian_rkf("Gradients"), F);
354 *U = extract_scalar_rkf("Energy");
355 // Update, will still not matter for those without restarts
356 if (can_restart) {
357 first_run = false;
358 // We need the "previous job" to pre-populate
359 // clang-format off
360 const auto copyOptions = std::filesystem::copy_options::overwrite_existing
361 | std::filesystem::copy_options::recursive
362 ;
363 // clang-format on
364 std::filesystem::copy(std::format("./{}.results/", cjob),
365 std::format("./{}.results/", pjob), copyOptions);
366 }
367 return;
368 } else {
369 smallSys(N, R, atomicNrs, box); // writes run_AMS.sh
370 updateCoord(N, R); // updates coordinates in previous job
371 write_restart(); // writes restart file using previous job
372 runAMS();
373 copyForces(N, extract_cartesian_rkf("Gradients"), F);
374 *U = extract_scalar_rkf("Energy");
375 switchjob(); // toggles the jobs
376 return;
377 }
378 // Never reach here
379 throw std::runtime_error("Generic AMS force error \n");
380}
381
382void AMS::passToSystem(long N, const double *R, const int *atomicNrs,
383 const double *box)
384// Creating the standard input file that the AMS driver reads
385{
386 std::ofstream out(kRunScript, std::ios::trunc);
387 if (!out) {
388 throw std::runtime_error(
389 std::format("Could not open {} for writing", kRunScript));
390 }
391
392 out << "#!/bin/sh\n";
393 out << std::format("export AMS_JOBNAME={}\n", cjob);
394 out << "$AMSBIN/ams --delete-old-results <<eor\n";
395 out << "Task SinglePoint\n";
396 out << "System\n";
397 out << " Atoms\n";
398 for (long i = 0; i < N; i++) {
399 out << std::format(" {}\t{:.19f}\t{:.19f}\t{:.19f}\n",
400 symbol_for_z(atomicNrs[i]), R[i * 3 + 0], R[i * 3 + 1],
401 R[i * 3 + 2]);
402 }
403 out << " End\n";
404 if (not model.empty() || not forcefield.empty()) {
405 out << " Lattice\n";
406 for (int i = 0; i < 3; i++) {
407 out << std::format(" {:.19f}\t{:.19f}\t{:.19f}\n", box[i * 3 + 0],
408 box[i * 3 + 1], box[i * 3 + 2]);
409 }
410 out << " End\n";
411 }
412 out << "End\n";
413 out << engine_setup;
414 out << "Properties\n";
415 out << " Gradients\n";
416 out << "End\n";
417 if (can_restart and not first_run) {
418 out << "@include myrestart.in\n";
419 }
420 out << "eor";
421
422 finishRunScript(out);
423 return;
424}
425
426void AMS::smallSys(long N, const double *R, const int *atomicNrs,
427 const double *box)
428// Creating the truncated input file that the AMS driver reads
429{
430 std::ofstream out(kRunScript, std::ios::trunc);
431 if (!out) {
432 throw std::runtime_error(
433 std::format("Could not open {} for writing", kRunScript));
434 }
435
436 out << "#!/bin/sh\n";
437 out << std::format("export AMS_JOBNAME={}\n", cjob);
438 out << "$AMSBIN/ams --delete-old-results <<eor\n";
439 out << "Task SinglePoint\n";
441 out << "Properties\n";
442 out << " Gradients\n";
443 out << "End\n";
444 out << "@include myrestart.in\n";
445 out << "eor";
446
447 finishRunScript(out);
448 return;
449}
450
451void AMS::finishRunScript(std::ofstream &out) {
452 out.close();
453 if (!out) {
454 throw std::runtime_error(
455 std::format("Could not write the AMS input to {}", kRunScript));
456 }
457 if (chmod(kRunScript, S_IRWXU) != 0) {
458 throw std::runtime_error(
459 std::format("Could not make {} executable", kRunScript));
460 }
461}
462
463std::string AMS::generate_run(const eonc::Parameters &p) {
464 std::string engine_block; // Shadows the class variable
465 // TODO: Use args everywhere, cleaner logic
466 // Ensure capitals and existence
467 if (engine.empty()) {
468 throw std::runtime_error("AMS Engine is required \n");
469 }
470 std::ranges::transform(engine, engine.begin(), [](unsigned char c) {
471 return static_cast<char>(std::toupper(c));
472 });
473 // engine functions uniquely, it serves as a filename, so we store
474 // engine_lower as a lowercase version too
476 std::ranges::transform(engine, engine_lower.begin(), [](unsigned char c) {
477 return static_cast<char>(std::tolower(c));
478 });
479 // Prepare the block
480 if (engine == "MOPAC") {
481 if (model.empty()) {
482 throw std::runtime_error("MOPAC needs a model\n");
483 }
484 std::ranges::transform(model, model.begin(), [](unsigned char c) {
485 return static_cast<char>(std::toupper(c));
486 });
487 std::string engine_formatter = R"(
488 Engine {}
489 Model {}
490 EndEngine
491)";
492 engine_block = std::format(engine_formatter, engine, model);
493 return engine_block;
494 } else if (engine == "ADF" || engine == "BAND") {
495 if (basis.empty()) {
496 throw std::runtime_error("ADF/BAND need a basis\n");
497 }
498 std::ranges::transform(basis, basis.begin(), [](unsigned char c) {
499 return static_cast<char>(std::toupper(c));
500 });
501 if (xc.empty()) {
502 throw std::runtime_error("ADF/BAND need a functional\n");
503 }
504 std::ranges::transform(xc, xc.begin(), [](unsigned char c) {
505 return static_cast<char>(std::toupper(c));
506 });
507 std::string engine_formatter = R"(
508 Engine {}
509 Basis
510 Type {}
511 End
512
513 XC
514 {}
515 End
516 EndEngine
517 )";
518 engine_block = std::format(engine_formatter, engine, basis, xc);
519 return engine_block;
520 } else if (engine == "DFTB") {
521 if (resources.empty()) {
522 throw std::runtime_error("DFTB need resources\n");
523 }
524 std::ranges::transform(resources, resources.begin(), [](unsigned char c) {
525 return static_cast<char>(std::toupper(c));
526 });
527 std::string engine_formatter = R"(
528 Engine {}
529 ResourcesDir {}
530 EndEngine
531 )";
532 engine_block = std::format(engine_formatter, engine, resources);
533 return engine_block;
534 } else if (engine == "reaxff") {
535 if (forcefield.empty()) {
536 throw std::runtime_error("REAXFF needs a forcefield\n");
537 }
538 std::ranges::transform(forcefield, forcefield.begin(), [](unsigned char c) {
539 return static_cast<char>(std::toupper(c));
540 });
541
542 std::string engine_formatter = R"(
543 Engine {}
544 ForceField {}
545 EndEngine
546 )";
547 engine_block = std::format(engine_formatter, engine, forcefield);
548 return engine_block;
549 } else if (engine == "FORCEFIELD") {
550 std::string engine_formatter = R"(
551 Engine {}
552 EndEngine
553 )";
554 engine_block = std::format(engine_formatter, engine);
555 return engine_block;
556 }
557
558 // Never reach here
559 throw std::runtime_error("Generic AMS engine error \n");
560}
std::string pjob
Definition AMS.h:65
void copyForces(long N, const std::vector< double > &frc, double *F)
Definition AMS.cpp:321
const double lengthConversion
Definition AMS.h:71
std::string forcefield
Definition AMS.h:57
std::string engine_setup
Definition AMS.h:58
void switchjob()
Definition AMS.cpp:293
const double energyConversion
Definition AMS.h:70
std::ofstream restartFrom
Definition AMS.h:66
void cleanMemory(void)
Definition AMS.cpp:79
std::string engine
Definition AMS.h:57
std::vector< double > extract_cartesian_rkf(std::string key)
Definition AMS.cpp:177
bool can_restart
Definition AMS.h:64
bool first_run
Definition AMS.h:64
std::string cjob
Definition AMS.h:65
void write_restart()
Definition AMS.cpp:300
boost::process::native_environment nativenv
Definition AMS.h:62
void smallSys(long N, const double *R, const int *atomicNrs, const double *box)
Flush, close and make the run script executable.
Definition AMS.cpp:415
std::string xc
Definition AMS.h:57
std::string engine_lower
Definition AMS.h:58
void runAMS()
Definition AMS.cpp:97
void passToSystem(long N, const double *R, const int *atomicNrs, const double *box)
< Creates a script to run AMS
Definition AMS.cpp:371
void updateCoord(long N, const double *R)
Definition AMS.cpp:233
std::string basis
Definition AMS.h:57
AMS(const eonc::Parameters &p)
Definition AMS.cpp:29
std::string generate_run(const eonc::Parameters &p)
Definition AMS.cpp:452
~AMS()
Definition AMS.cpp:81
int amsevals
Definition AMS.h:63
void finishRunScript(std::ofstream &out)
Copy 3N gradient components into the caller's force array.
Definition AMS.cpp:440
std::string model
Definition AMS.h:57
void force(long N, const double *R, const int *atomicNrs, double *F, double *U, double *variance, const double *box)
Definition AMS.cpp:334
std::string resources
Definition AMS.h:57
const double forceConversion
Definition AMS.h:67
double extract_scalar_rkf(std::string key)
Definition AMS.cpp:124
const ams_options_t & ams_options() const
Potential(PotType a_ptype)
Production default: construction-scope registry, else PotRegistry::get().
RAII resource manager for the ARTn C library with global synchronization.
struct eonc::ams_options_t::env_t env