Loading...
Searching...
No Matches
GPSurrogateJob.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/GPSurrogateJob.h"
13#include "eon/BaseStructures.h"
20
21#include "eon/EonLogger.h"
23#include <fstream>
24#include <sstream>
25#include <stdexcept>
26
27namespace eonc {
28
29std::vector<std::string> GPSurrogateJob::run() {
30 std::string reactantFilename = eonc::helpers::getRelevantFile("reactant.con");
31 std::string productFilename = eonc::helpers::getRelevantFile("product.con");
32 auto true_params = std::make_shared<Parameters>(params);
34 params.gp_surrogate_options().sub_job;
35 auto initial = std::make_shared<Matter>(pot, *true_params);
36 if (!eonc::io::io_ok(initial->con2matter(reactantFilename))) {
37 EONC_LOG_CRITICAL("Failed to load {}", reactantFilename);
38 throw std::runtime_error("failed to load " + reactantFilename);
39 }
40 auto final_state = std::make_shared<Matter>(pot, *true_params);
41 if (!eonc::io::io_ok(final_state->con2matter(productFilename))) {
42 EONC_LOG_CRITICAL("Failed to load {}", productFilename);
43 throw std::runtime_error("failed to load " + productFilename);
44 }
45 (void)runFromMatter(initial, final_state);
46 return returnFiles;
47}
48
49std::shared_ptr<NudgedElasticBand>
50GPSurrogateJob::runFromMatter(std::shared_ptr<Matter> initial,
51 std::shared_ptr<Matter> final_state) {
52 if (!initial || !final_state) {
53 throw std::runtime_error("GPSurrogateJob::runFromMatter: null Matter");
54 }
55 // Clone and setup "true" params
56 auto true_params = std::make_shared<Parameters>(params);
58 params.gp_surrogate_options().sub_job;
59 auto true_job = eonc::helpers::makeJob(
60 std::make_unique<Parameters>(*true_params), *runtime_);
61 auto pyparams = std::make_shared<Parameters>(params);
64
65 initial->setPotential(pot);
66 final_state->setPotential(pot);
68 *initial, *final_state, params.neb_options().image_count);
69 auto init_data = eonc::helpers::surrogate::getMidSlice(init_path);
70 auto features = eonc::helpers::surrogate::get_features(init_data);
71 EONC_LOG_TRACE("Potential is {}",
72 magic_enum::enum_name<PotType>(pot->getType()));
73 auto targets = eonc::helpers::surrogate::get_targets(init_data, pot);
74
75 // Setup a GPR Potential
77 params.gp_surrogate_options().potential, params);
78 surpot->train_optimize(features, targets);
79 auto neb = std::make_unique<NudgedElasticBand>(initial, final_state,
80 *pyparams, surpot);
81 auto status_neb{neb->compute()};
82 bool job_not_finished{true};
83 size_t n_gp{0};
84 while (job_not_finished) { // outer loop?
85 n_gp++;
86 if (n_gp > 750) {
87 EONC_LOG_CRITICAL("Whoops, power level of problem too high!!");
88 break;
89 }
90 EONC_LOG_TRACE("Must handle update to the GP, update number {}", n_gp);
91 auto [maxUnc, maxIndex] =
93 auto [feature, target] =
95 eonc::helpers::eigen::addVectorRow(features, feature);
97 surpot->train_optimize(features, targets);
100 params.optimizer_options().converged_force * 0.8;
101 for (auto &&obj : neb->path) {
102 obj->setPotential(surpot);
103 }
104 if (!(pyparams->gp_surrogate_options().linear_path_always)) {
105 EONC_LOG_TRACE("Using previous path");
106 std::vector<Matter> previous;
107 previous.reserve(neb->path.size());
108 for (const auto &image : neb->path) {
109 previous.push_back(*image);
110 }
111 neb = std::make_unique<NudgedElasticBand>(std::move(previous), *pyparams,
112 surpot);
113 } else {
114 EONC_LOG_TRACE("Using linear interpolation");
115 neb = std::make_unique<NudgedElasticBand>(initial, final_state, *pyparams,
116 surpot);
117 }
118 status_neb = neb->compute();
119
120 std::string nebFilename(std::format("neb_final_gpr_{:03d}.con", n_gp));
121 returnFiles.push_back(nebFilename);
123 neb->path, neb->tangent, neb->eigenmode_solvers, neb->numImages,
124 params.debug_options().estimate_neb_eigenvalues, nebFilename,
125 static_cast<size_t>(n_gp), neb->reactantEnergy))) {
126 throw std::runtime_error("Failed to write file: " + nebFilename);
127 }
128 if (status_neb == NudgedElasticBand::NEBStatus::GOOD &&
130 break;
131 } else {
132 continue;
133 }
134 }
135 neb->printImageData();
136 neb->findExtrema();
137 // Keep a shared view for the caller before saveData takes ownership of unique
138 std::shared_ptr<NudgedElasticBand> out(neb.release());
139 // saveData expects unique_ptr - rebuild unique from shared is unsafe.
140 // Write results without consuming: call saveData on a temporary unique wrap
141 // fails. Instead write via a clone path: only return the band; file artifacts
142 // optional.
143 return out;
144}
145
147 std::unique_ptr<NudgedElasticBand> neb) {
148 std::string resultsFilename = "results.dat";
149 returnFiles.push_back(resultsFilename);
150
151 std::ofstream fileResults(resultsFilename);
152 if (!fileResults) {
153 // Handle file open error
154 throw std::runtime_error("Failed to open file: " + resultsFilename);
155 }
156
157 fileResults << static_cast<int>(status) << " termination_reason\n";
158 fileResults << magic_enum::enum_name(status) << " termination_reason_text\n";
159 fileResults << magic_enum::enum_name<PotType>(
160 params.potential_options().potential)
161 << " potential_type\n";
162 fileResults << std::format("{:.6f} energy_reference\n", neb->reactantEnergy);
163 fileResults << neb->numImages << " number_of_images\n";
164
165 for (long i = 0; i <= neb->numImages + 1; i++) {
166 fileResults << std::format(
167 "{:.6f} image{}_energy\n",
168 neb->path[i]->getPotentialEnergy() - neb->reactantEnergy, i);
169 fileResults << std::format("{:.6f} image{}_force\n",
170 neb->path[i]->getForces().norm(), i);
171 fileResults << std::format("{:.6f} image{}_projected_force\n",
172 neb->projectedForce[i]->norm(), i);
173 }
174
175 fileResults << neb->numExtrema << " number_of_extrema\n";
176 for (long i = 0; i < neb->numExtrema; i++) {
177 fileResults << std::format("{:.6f} extremum{}_position\n",
178 neb->extremumPosition[i], i);
179 fileResults << std::format("{:.6f} extremum{}_energy\n",
180 neb->extremumEnergy[i], i);
181 }
182
183 fileResults.close();
184
185 std::string nebFilename = "neb.con";
186 returnFiles.push_back(nebFilename);
187
189 neb->path, neb->tangent, neb->eigenmode_solvers, neb->numImages,
190 params.debug_options().estimate_neb_eigenvalues, nebFilename,
191 std::nullopt, neb->reactantEnergy))) {
192 throw std::runtime_error("Failed to write file: " + nebFilename);
193 }
194
195 returnFiles.push_back("neb.dat");
196 neb->printImageData(true);
197}
198
199} // namespace eonc
200
202MatrixXd get_features(const std::vector<Matter> &matobjs) {
203 // Calculate dimensions
204 MatrixXd features(matobjs.size(), matobjs.front().numberOfFreeAtoms() * 3);
205 EONC_LOG_TRACE("rows: {}, cols:{}", matobjs.size(),
206 matobjs.front().numberOfFreeAtoms() * 3);
207 for (long idx{0}; idx < features.rows(); idx++) {
208 features.row(idx) = matobjs[idx].getPositionsFreeV();
209 }
210 std::ostringstream oss;
211 oss << features;
212 EONC_LOG_TRACE("Features\n:{}", oss.str());
213 return features;
214}
215MatrixXd get_features(const std::vector<std::shared_ptr<Matter>> &matobjs) {
216 // Calculate dimensions
217 MatrixXd features(matobjs.size(), matobjs.front()->numberOfFreeAtoms() * 3);
218 EONC_LOG_TRACE("rows: {}, cols:{}\n", matobjs.size(),
219 matobjs.front()->numberOfFreeAtoms() * 3);
220 for (long idx{0}; idx < features.rows(); idx++) {
221 features.row(idx) = matobjs[idx]->getPositionsFreeV();
222 }
223 std::ostringstream oss;
224 oss << features;
225 EONC_LOG_TRACE("Features\n:{}", oss.str());
226 return features;
227}
228MatrixXd get_targets(std::vector<Matter> &matobjs,
229 std::shared_ptr<Potential> true_pot) {
230 // Always with derivatives for now
231 // Energy + Derivatives for each row
232 const auto nrows = matobjs.size();
233 const auto ncols = (matobjs.front().numberOfFreeAtoms() * 3) + 1;
234 MatrixXd targets(nrows, ncols);
235 for (long idx{0}; idx < targets.rows(); idx++) {
236 matobjs[idx].setPotential(true_pot);
237 targets.row(idx)[0] = matobjs[idx].getPotentialEnergy();
238 targets.block(idx, 1, 1, ncols - 1) =
239 matobjs[idx].getForcesFreeV().array() * -1;
240 }
241 std::ostringstream oss;
242 oss << targets;
243 EONC_LOG_TRACE("Targets\n:{}", oss.str());
244 return targets;
245}
246MatrixXd get_targets(std::vector<std::shared_ptr<Matter>> &matobjs,
247 std::shared_ptr<Potential> true_pot) {
248 const auto nrows = matobjs.size();
249 const auto ncols = (matobjs.front()->numberOfFreeAtoms() * 3) + 1;
250 MatrixXd targets(nrows, ncols);
251 for (long idx{0}; idx < targets.rows(); idx++) {
252 matobjs[idx]->setPotential(true_pot);
253 targets.row(idx)[0] = matobjs[idx]->getPotentialEnergy();
254 targets.block(idx, 1, 1, ncols - 1) =
255 matobjs[idx]->getForcesFreeV().array() * -1;
256 }
257 std::ostringstream oss;
258 oss << targets;
259 EONC_LOG_TRACE("Targets\n:{}", oss.str());
260 return targets;
261}
262std::vector<Matter> getMidSlice(const std::vector<Matter> &matobjs) {
263 // Initial GP slice: endpoints plus one interior sample. CatLearn
264 // training is order-sensitive (front, back, interior). The interior
265 // index is two-thirds along the movable images, not n/2.
266 if (matobjs.size() < 3) {
267 throw std::invalid_argument("getMidSlice: need at least three images");
268 }
269 const std::size_t n = matobjs.size();
270 const std::size_t twoThirds =
271 static_cast<std::size_t>(((n - 2) * 2.0 / 3.0) + 1.0);
272 return {matobjs.front(), matobjs.back(), matobjs[twoThirds]};
273}
274Eigen::VectorXd make_target(Matter &m1, std::shared_ptr<Potential> true_pot) {
275 const auto ncols = (m1.numberOfFreeAtoms() * 3) + 1;
276 Eigen::VectorXd target(ncols);
277 m1.setPotential(true_pot);
278 target(0) = m1.getPotentialEnergy();
279 target.segment(1, ncols - 1) = m1.getForcesFreeV() * -1;
280 return target;
281}
282std::pair<double, Eigen::VectorXd::Index>
283getMaxUncertainty(const std::vector<std::shared_ptr<Matter>> &matobjs) {
284 if (matobjs.size() < 3) {
285 throw std::invalid_argument(
286 "getMaxUncertainty: need at least three images");
287 }
288 Eigen::VectorXd pathUncertainty{Eigen::VectorXd::Zero(matobjs.size() - 2)};
289 for (auto idx{0}; idx < pathUncertainty.size(); idx++) {
290 pathUncertainty[idx] = matobjs[idx + 1]->getEnergyVariance();
291 }
292 Eigen::VectorXd::Index maxIndex;
293 double maxUnc{pathUncertainty.maxCoeff()};
294 pathUncertainty.maxCoeff(&maxIndex);
295 return std::make_pair(maxUnc, maxIndex);
296}
297std::pair<Eigen::VectorXd, Eigen::VectorXd>
298getNewDataPoint(const std::vector<std::shared_ptr<Matter>> &matobjs,
299 std::shared_ptr<Potential> true_pot) {
300 auto [maxUnc, maxIndex] = getMaxUncertainty(matobjs);
301 Matter candidate{*matobjs[maxIndex + 1]};
302 return std::make_pair<Eigen::VectorXd, Eigen::VectorXd>(
303 candidate.getPositionsFreeV(), make_target(candidate, true_pot));
304}
305bool accuratePES(std::vector<std::shared_ptr<Matter>> &matobjs,
306 std::shared_ptr<Potential> true_pot) {
307 if (matobjs.empty()) {
308 throw std::invalid_argument("accuratePES: empty path");
309 }
310 Eigen::VectorXd predEnergies{Eigen::VectorXd::Zero(matobjs.size())};
311 Eigen::VectorXd trueEnergies{Eigen::VectorXd::Zero(matobjs.size())};
312 for (auto idx{0}; idx < predEnergies.size(); idx++) {
313 auto incoming = matobjs[idx]->getPotential();
314 predEnergies[idx] = matobjs[idx]->getPotentialEnergy();
315 matobjs[idx]->setPotential(true_pot);
316 trueEnergies[idx] = matobjs[idx]->getPotentialEnergy();
317 matobjs[idx]->setPotential(incoming);
318 }
319 Eigen::VectorXd difference = predEnergies - trueEnergies;
320 const auto maxAbs = difference.array().abs().maxCoeff();
321 std::ostringstream oss;
322 oss << "predicted\n"
323 << predEnergies << "\ntrue\n"
324 << trueEnergies << "\ndifference\n"
325 << difference << "\n maxAbs: " << maxAbs;
326 EONC_LOG_TRACE("{}", oss.str());
327 return maxAbs < 0.05;
328}
329} // namespace eonc::helpers::surrogate
330
332MatrixXd vertCat(const MatrixXd &m1, const MatrixXd &m2) {
333 assert(m1.cols() == m2.cols());
334 MatrixXd res(m1.rows() + m2.rows(), m2.cols());
335 res << m1, m2;
336 return res;
337}
338void addVectorRow(MatrixXd &data, const Eigen::VectorXd &newrow) {
339 assert(data.cols() == newrow.size());
340 data.conservativeResize(data.rows() + 1, data.cols());
341 data.row(data.rows() - 1) = newrow;
342}
343} // namespace eonc::helpers::eigen
Eigen::Matrix< double, Eigen::Dynamic, Eigen::Dynamic, eOnStorageOrder > MatrixXd
Definition Eigen.h:33
#define EONC_LOG_TRACE(...)
Convenience macro for one-shot logging without storing a logger.
Definition EonLogger.h:237
#define EONC_LOG_CRITICAL(...)
Definition EonLogger.h:267
std::shared_ptr< NudgedElasticBand > runFromMatter(std::shared_ptr< Matter > initial, std::shared_ptr< Matter > final_state)
Matter-first NEB surrogate path (endpoints as Matter).
std::vector< std::string > run() override
Virtual run; used solely for dynamic dispatch.
std::vector< std::string > returnFiles
void saveData(NudgedElasticBand::NEBStatus status, std::unique_ptr< NudgedElasticBand > neb)
std::shared_ptr< Potential > pot
Definition Job.h:63
Parameters params
Definition Job.h:58
Runtime * runtime_
Always valid: either owned_runtime_.get() or a caller-owned Runtime.
Definition Job.h:62
VectorXd getForcesFreeV() const
Definition Matter.cpp:442
void setPotential(std::shared_ptr< Potential > pot)
Definition Matter.cpp:754
VectorXd getPositionsFreeV() const
Definition Matter.cpp:345
double getPotentialEnergy() const
Definition Matter.cpp:554
long int numberOfFreeAtoms() const
Definition Matter.cpp:583
MatrixXd vertCat(const MatrixXd &m1, const MatrixXd &m2)
void addVectorRow(MatrixXd &data, const Eigen::VectorXd &newrow)
std::vector< Matter > linearPath(const Matter &initImg, const Matter &finalImg, const size_t nimgs)
MatrixXd get_targets(std::vector< Matter > &matobjs, std::shared_ptr< Potential > true_pot)
MatrixXd get_features(const std::vector< Matter > &matobjs)
bool accuratePES(std::vector< std::shared_ptr< Matter > > &matobjs, std::shared_ptr< Potential > true_pot)
std::pair< double, Eigen::VectorXd::Index > getMaxUncertainty(const std::vector< std::shared_ptr< Matter > > &matobjs)
std::vector< Matter > getMidSlice(const std::vector< Matter > &matobjs)
std::pair< Eigen::VectorXd, Eigen::VectorXd > getNewDataPoint(const std::vector< std::shared_ptr< Matter > > &matobjs, std::shared_ptr< Potential > true_pot)
Eigen::VectorXd make_target(Matter &m1, std::shared_ptr< Potential > true_pot)
std::string getRelevantFile(std::string filename)
std::unique_ptr< Job > makeJob(std::unique_ptr< Parameters > params, Runtime &runtime)
Borrow: caller keeps Runtime alive.
constexpr bool io_ok(IoStatus s) noexcept
Definition ConFileIO.h:38
eonc::io::IoStatus writePathCon(const std::vector< std::shared_ptr< Matter > > &path, const std::vector< std::shared_ptr< AtomMatrix > > &tangent, const std::vector< std::shared_ptr< EigenmodeStrategy > > &eigenmode_solvers, long numImages, bool estimateEigenvalues, std::string filename, std::optional< size_t > bandIndex, double referenceEnergy)
Write a NEB band as a multi-frame .con via readcon ConFrameBuilder::clone().
RAII resource manager for the ARTn C library with global synchronization.
std::shared_ptr< eonc::SurrogatePotential > makeSurrogatePotential(eonc::PotType a_ptype, const eonc::Parameters &a_params, Args &&...a_args)
Definition Create.hpp:22
static main_options_t & main_options(Parameters &p)
static neb_options_t & neb_options(Parameters &p)
static optimizer_options_t & optimizer_options(Parameters &p)
static potential_options_t & potential_options(Parameters &p)
struct eonc::neb_options_t::climbing_image_options_t climbing_image