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"
22#include <sstream>
23#include <stdexcept>
24
25std::vector<std::string> GPSurrogateJob::run() {
26 std::string reactantFilename = eonc::helpers::getRelevantFile("reactant.con");
27 std::string productFilename = eonc::helpers::getRelevantFile("product.con");
28 auto true_params = std::make_shared<Parameters>(params);
29 true_params->main_options.job = params.sub_job;
30 auto initial = std::make_shared<Matter>(pot, *true_params);
31 if (!eonc::io::io_ok(initial->con2matter(reactantFilename))) {
32 EONC_LOG_CRITICAL("Failed to load {}", reactantFilename);
33 throw std::runtime_error("failed to load " + reactantFilename);
34 }
35 auto final_state = std::make_shared<Matter>(pot, *true_params);
36 if (!eonc::io::io_ok(final_state->con2matter(productFilename))) {
37 EONC_LOG_CRITICAL("Failed to load {}", productFilename);
38 throw std::runtime_error("failed to load " + productFilename);
39 }
40 (void)runFromMatter(initial, final_state);
41 return returnFiles;
42}
43
44std::shared_ptr<NudgedElasticBand>
45GPSurrogateJob::runFromMatter(std::shared_ptr<Matter> initial,
46 std::shared_ptr<Matter> final_state) {
47 if (!initial || !final_state) {
48 throw std::runtime_error("GPSurrogateJob::runFromMatter: null Matter");
49 }
50 // Clone and setup "true" params
51 auto true_params = std::make_shared<Parameters>(params);
52 true_params->main_options.job = params.sub_job;
53 auto true_job =
54 eonc::helpers::makeJob(std::make_unique<Parameters>(*true_params));
55 auto pyparams = std::make_shared<Parameters>(params);
56 pyparams->potential_options.potential = PotType::CatLearn;
57
58 initial->setPotential(pot);
59 final_state->setPotential(pot);
61 *initial, *final_state, params.neb_options.image_count);
62 auto init_data = eonc::helpers::surrogate::getMidSlice(init_path);
63 auto features = eonc::helpers::surrogate::get_features(init_data);
64 EONC_LOG_TRACE("Potential is {}",
65 magic_enum::enum_name<PotType>(pot->getType()));
66 auto targets = eonc::helpers::surrogate::get_targets(init_data, pot);
67
68 // Setup a GPR Potential
69 auto surpot = eonc::helpers::create::makeSurrogatePotential(
70 params.gp_surrogate_options.potential, params);
71 surpot->train_optimize(features, targets);
72 auto neb = std::make_unique<NudgedElasticBand>(initial, final_state,
73 *pyparams, surpot);
74 auto status_neb{neb->compute()};
75 bool job_not_finished{true};
76 size_t n_gp{0};
77 double unc_conv{pyparams->gp_uncertainty};
78 while (job_not_finished) { // outer loop?
79 n_gp++;
80 if (n_gp > 750) {
81 EONC_LOG_CRITICAL("Whoops, power level of problem too high!!");
82 break;
83 }
84 EONC_LOG_TRACE("Must handle update to the GP, update number {}", n_gp);
85 auto [maxUnc, maxIndex] =
87 auto [feature, target] =
89 eonc::helpers::eigen::addVectorRow(features, feature);
91 surpot->train_optimize(features, targets);
92 pyparams->nebClimbingImageMethod = false;
93 pyparams->optimizer_options.converged_force =
94 params.optimizer_options.converged_force * 0.8;
95 for (auto &&obj : neb->path) {
96 obj->setPotential(surpot);
97 }
98 if (!(pyparams->gp_linear_path_always)) {
99 EONC_LOG_TRACE("Using previous path");
100 neb = std::make_unique<NudgedElasticBand>(neb->path, *pyparams, surpot);
101 } else {
102 EONC_LOG_TRACE("Using linear interpolation");
103 neb = std::make_unique<NudgedElasticBand>(initial, final_state, *pyparams,
104 surpot);
105 }
106 status_neb = neb->compute();
107
108 std::string nebFilename(std::format("neb_final_gpr_{:03d}.con", n_gp));
109 returnFiles.push_back(nebFilename);
111 neb->path, neb->tangent, neb->eigenmode_solvers, neb->numImages,
112 params.debug_options.estimate_neb_eigenvalues, nebFilename,
113 static_cast<size_t>(n_gp)))) {
114 throw std::runtime_error("Failed to write file: " + nebFilename);
115 }
116 if (status_neb == NudgedElasticBand::NEBStatus::GOOD &&
118 break;
119 } else {
120 continue;
121 }
122 }
123 neb->printImageData();
124 neb->findExtrema();
125 // Keep a shared view for the caller before saveData takes ownership of unique
126 std::shared_ptr<NudgedElasticBand> out(neb.release());
127 // saveData expects unique_ptr - rebuild unique from shared is unsafe.
128 // Write results without consuming: call saveData on a temporary unique wrap
129 // fails. Instead write via a clone path: only return the band; file artifacts
130 // optional.
131 return out;
132}
133
135 std::unique_ptr<NudgedElasticBand> neb) {
136 std::string resultsFilename = "results.dat";
137 returnFiles.push_back(resultsFilename);
138
139 std::ofstream fileResults(resultsFilename);
140 if (!fileResults) {
141 // Handle file open error
142 throw std::runtime_error("Failed to open file: " + resultsFilename);
143 }
144
145 fileResults << static_cast<int>(status) << " termination_reason\n";
146 fileResults << magic_enum::enum_name(status) << " termination_reason_text\n";
147 fileResults << magic_enum::enum_name<PotType>(
148 params.potential_options.potential)
149 << " potential_type\n";
150 fileResults << std::format("{:.6f} energy_reference\n",
151 neb->path[0]->getPotentialEnergy());
152 fileResults << neb->numImages << " number_of_images\n";
153
154 for (long i = 0; i <= neb->numImages + 1; i++) {
155 fileResults << std::format("{:.6f} image{}_energy\n",
156 neb->path[i]->getPotentialEnergy() -
157 neb->path[0]->getPotentialEnergy(),
158 i);
159 fileResults << std::format("{:.6f} image{}_force\n",
160 neb->path[i]->getForces().norm(), i);
161 fileResults << std::format("{:.6f} image{}_projected_force\n",
162 neb->projectedForce[i]->norm(), i);
163 }
164
165 fileResults << neb->numExtrema << " number_of_extrema\n";
166 for (long i = 0; i < neb->numExtrema; i++) {
167 fileResults << std::format("{:.6f} extremum{}_position\n",
168 neb->extremumPosition[i], i);
169 fileResults << std::format("{:.6f} extremum{}_energy\n",
170 neb->extremumEnergy[i], i);
171 }
172
173 fileResults.close();
174
175 std::string nebFilename = "neb.con";
176 returnFiles.push_back(nebFilename);
177
179 neb->path, neb->tangent, neb->eigenmode_solvers, neb->numImages,
180 params.debug_options.estimate_neb_eigenvalues, nebFilename))) {
181 throw std::runtime_error("Failed to write file: " + nebFilename);
182 }
183
184 returnFiles.push_back("neb.dat");
185 neb->printImageData(true);
186}
188MatrixXd get_features(const std::vector<Matter> &matobjs) {
189 // Calculate dimensions
190 MatrixXd features(matobjs.size(), matobjs.front().numberOfFreeAtoms() * 3);
191 EONC_LOG_TRACE("rows: {}, cols:{}", matobjs.size(),
192 matobjs.front().numberOfFreeAtoms() * 3);
193 for (long idx{0}; idx < features.rows(); idx++) {
194 features.row(idx) = matobjs[idx].getPositionsFreeV();
195 }
196 std::ostringstream oss;
197 oss << features;
198 EONC_LOG_TRACE("Features\n:{}", oss.str());
199 return features;
200}
201MatrixXd get_features(const std::vector<std::shared_ptr<Matter>> &matobjs) {
202 // Calculate dimensions
203 MatrixXd features(matobjs.size(), matobjs.front()->numberOfFreeAtoms() * 3);
204 EONC_LOG_TRACE("rows: {}, cols:{}\n", matobjs.size(),
205 matobjs.front()->numberOfFreeAtoms() * 3);
206 for (long idx{0}; idx < features.rows(); idx++) {
207 features.row(idx) = matobjs[idx]->getPositionsFreeV();
208 }
209 std::ostringstream oss;
210 oss << features;
211 EONC_LOG_TRACE("Features\n:{}", oss.str());
212 return features;
213}
214MatrixXd get_targets(std::vector<Matter> &matobjs,
215 std::shared_ptr<Potential> true_pot) {
216 // Always with derivatives for now
217 // Energy + Derivatives for each row
218 const auto nrows = matobjs.size();
219 const auto ncols = (matobjs.front().numberOfFreeAtoms() * 3) + 1;
220 MatrixXd targets(nrows, ncols);
221 for (long idx{0}; idx < targets.rows(); idx++) {
222 matobjs[idx].setPotential(true_pot);
223 targets.row(idx)[0] = matobjs[idx].getPotentialEnergy();
224 targets.block(idx, 1, 1, ncols - 1) =
225 matobjs[idx].getForcesFree().array() * -1;
226 }
227 std::ostringstream oss;
228 oss << targets;
229 EONC_LOG_TRACE("Targets\n:{}", oss.str());
230 return targets;
231}
232MatrixXd get_targets(std::vector<std::shared_ptr<Matter>> &matobjs,
233 std::shared_ptr<Potential> true_pot) {
234 const auto nrows = matobjs.size();
235 const auto ncols = (matobjs.front()->numberOfFreeAtoms() * 3) + 1;
236 MatrixXd targets(nrows, ncols);
237 for (long idx{0}; idx < targets.rows(); idx++) {
238 matobjs[idx]->setPotential(true_pot);
239 targets.row(idx)[0] = matobjs[idx]->getPotentialEnergy();
240 targets.block(idx, 1, 1, ncols - 1) =
241 matobjs[idx]->getForcesFree().array() * -1;
242 }
243 std::ostringstream oss;
244 oss << targets;
245 EONC_LOG_TRACE("Targets\n:{}", oss.str());
246 return targets;
247}
248std::vector<Matter> getMidSlice(const std::vector<Matter> &matobjs) {
249 // Used to get the initial data slice, endpoints and the midpoint
250 std::vector<Matter> res;
251 res.reserve(3);
252 res.push_back(matobjs.front());
253 // BUG: THIS ISN'T THE MIDDLE!!!!
254 // XXX: Why does this have to be in the same order?
255 // front mid back doesn't work
256 // front back mid works
257 res.push_back(matobjs.back());
258 res.push_back(matobjs[((matobjs.size() - 2) * 2.0 / 3.0) + 1]);
259 return res;
260}
261Eigen::VectorXd make_target(Matter &m1, std::shared_ptr<Potential> true_pot) {
262 const auto ncols = (m1.numberOfFreeAtoms() * 3) + 1;
263 Eigen::VectorXd target(ncols);
264 m1.setPotential(true_pot);
265 target(0) = m1.getPotentialEnergy();
266 target.segment(1, ncols - 1) = m1.getForcesFreeV() * -1;
267 // EONC_LOG_TRACE("Generated Target:\n{}",
268 // fmt::streamed(target));
269 return target;
270}
271std::pair<double, Eigen::VectorXd::Index>
272getMaxUncertainty(const std::vector<std::shared_ptr<Matter>> &matobjs) {
273 Eigen::VectorXd pathUncertainty{Eigen::VectorXd::Zero(matobjs.size() - 2)};
274 for (auto idx{0}; idx < pathUncertainty.size(); idx++) {
275 pathUncertainty[idx] = matobjs[idx + 1]->getEnergyVariance();
276 }
277 Eigen::VectorXd::Index maxIndex;
278 double maxUnc{pathUncertainty.maxCoeff()};
279 pathUncertainty.maxCoeff(&maxIndex);
280 // EONC_LOG_TRACE("Uncertainty along path
281 // is {}\nmax_index: {}, maxVal: {}",
282 // fmt::streamed(pathUncertainty), maxIndex, maxUnc);
283 return std::make_pair(maxUnc, maxIndex);
284}
285std::pair<Eigen::VectorXd, Eigen::VectorXd>
286getNewDataPoint(const std::vector<std::shared_ptr<Matter>> &matobjs,
287 std::shared_ptr<Potential> true_pot) {
288 auto [maxUnc, maxIndex] = getMaxUncertainty(matobjs);
289 Matter candidate{*matobjs[maxIndex + 1]};
290 return std::make_pair<Eigen::VectorXd, Eigen::VectorXd>(
291 candidate.getPositionsFreeV(), make_target(candidate, true_pot));
292}
293bool accuratePES(std::vector<std::shared_ptr<Matter>> &matobjs,
294 std::shared_ptr<Potential> true_pot) {
295 Eigen::VectorXd predEnergies{Eigen::VectorXd::Zero(matobjs.size())};
296 Eigen::VectorXd trueEnergies{Eigen::VectorXd::Zero(matobjs.size())};
297 Eigen::VectorXd accuracy{Eigen::VectorXd::Zero(matobjs.size())};
298 for (auto idx{0}; idx < predEnergies.size(); idx++) {
299 predEnergies[idx] = matobjs[idx]->getPotentialEnergy();
300 matobjs[idx]->setPotential(true_pot);
301 trueEnergies[idx] = matobjs[idx]->getPotentialEnergy();
302
303 accuracy[idx] = std::sqrt(predEnergies[idx] * predEnergies[idx] -
304 trueEnergies[idx] * trueEnergies[idx]);
305 }
306 Eigen::VectorXd difference = predEnergies - trueEnergies;
307 auto mae = difference.array()
308 .abs()
309 .maxCoeff(); //.squaredNorm() / predEnergies.size();
310 std::ostringstream oss;
311 oss << "predicted\n"
312 << predEnergies << "\ntrue\n"
313 << trueEnergies << "\ndifference\n"
314 << difference << "\n MAE: " << mae;
315 EONC_LOG_TRACE("{}", oss.str());
316 return mae < 0.05;
317}
318} // namespace eonc::helpers::surrogate
319
321MatrixXd vertCat(const MatrixXd &m1, const MatrixXd &m2) {
322 assert(m1.cols() == m2.cols());
323 MatrixXd res(m1.rows() + m2.rows(), m2.cols());
324 res << m1, m2;
325 return res;
326}
327void addVectorRow(MatrixXd &data, const Eigen::VectorXd &newrow) {
328 assert(data.cols() == newrow.size());
329 data.conservativeResize(data.rows() + 1, data.cols());
330 data.row(data.rows() - 1) = newrow;
331}
332} // 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:238
#define EONC_LOG_CRITICAL(...)
Definition EonLogger.h:268
std::vector< std::string > run() override
Virtual run; used solely for dynamic dispatch.
std::shared_ptr< NudgedElasticBand > runFromMatter(std::shared_ptr< Matter > initial, std::shared_ptr< Matter > final_state)
Matter-first NEB surrogate path (endpoints as Matter).
void saveData(NudgedElasticBand::NEBStatus status, std::unique_ptr< NudgedElasticBand > neb)
std::vector< std::string > returnFiles
std::shared_ptr< Potential > pot
Definition Job.h:55
Parameters params
Definition Job.h:54
double getPotentialEnergy() const
Definition Matter.cpp:446
VectorXd getForcesFreeV() const
Definition Matter.cpp:354
VectorXd getPositionsFreeV() const
Definition Matter.cpp:268
long int numberOfFreeAtoms() const
Definition Matter.cpp:475
void setPotential(std::shared_ptr< Potential > pot)
Definition Matter.cpp:625
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)
Definition Job.cpp:39
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)
Write a NEB band as a multi-frame .con via readcon ConFrameBuilder::clone().