Scine::Readuct  5.0.0
This is the SCINE module ReaDuct.
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Pages
TsOptimizationTask.h
Go to the documentation of this file.
1 
7 #ifndef READUCT_TSOPTIMIZATIONTASK_H_
8 #define READUCT_TSOPTIMIZATIONTASK_H_
9 
10 /* Readuct */
11 #include "Tasks/Task.h"
12 /* Scine */
23 /* External */
24 #include <boost/exception/diagnostic_information.hpp>
25 #include <boost/filesystem.hpp>
26 /* std c++ */
27 #include <cstdio>
28 #include <fstream>
29 #include <iostream>
30 #include <vector>
31 
32 namespace Scine {
33 namespace Readuct {
34 
35 class TsOptimizationTask : public Task {
36  public:
43  TsOptimizationTask(std::vector<std::string> input, std::vector<std::string> output, std::shared_ptr<Core::Log> logger = nullptr)
44  : Task(std::move(input), std::move(output), std::move(logger)) {
45  }
46 
47  std::string name() const override {
48  return "TS Optimization";
49  }
50 
51  bool run(SystemsMap& systems, Utils::UniversalSettings::ValueCollection taskSettings, bool testRunOnly = false,
52  std::vector<std::function<void(const int&, const Utils::AtomCollection&, const Utils::Results&, const std::string&)>>
53  observers = {}) const final {
56 
57  bool silentCalculator = taskSettings.extract("silent_stdout_calculator", true);
58  std::shared_ptr<Core::Calculator> calc;
59  if (!testRunOnly) { // leave out in case of task chaining --> attention calc is NULL
60  // Note: _input is guaranteed not to be empty by Task constructor
61  calc = copyCalculator(systems, _input.front(), name());
62  Utils::CalculationRoutines::setLog(*calc, true, true, !silentCalculator);
63 
64  // Check system size
65  if (calc->getStructure()->size() == 1) {
66  throw std::runtime_error("Cannot calculate transition state for monoatomic systems.");
67  }
68  }
69 
70  // Generate optimizer
71  std::string optimizertype = taskSettings.extract("optimizer", std::string{"bofill"});
72  std::transform(optimizertype.begin(), optimizertype.end(), optimizertype.begin(), ::tolower);
73 
74  // Check for automatic mode selection
75  // sanity check
76  if (taskSettings.valueExists("automatic_mode_selection") &&
77  (taskSettings.valueExists(optimizertype + "_follow_mode") || taskSettings.valueExists("ev_follow_mode"))) {
78  throw std::logic_error("You specified an automatic selection of the mode and gave a specific mode yourself. "
79  "This is not possible. Only give one of those options.");
80  }
81  std::vector<int> relevantAtoms = taskSettings.extract("automatic_mode_selection", std::vector<int>{});
82  // another sanity check
83  if (!relevantAtoms.empty()) {
84  for (const auto& index : relevantAtoms) {
85  if (index < 0) {
86  throw std::logic_error("You gave an atom index smaller than 0 in automatic_mode_selection. "
87  "This does not make sense.");
88  }
89  }
90  }
91 
92  using XyzHandler = Utils::XyzStreamHandler;
93  auto cout = _logger->output;
94 
95  std::shared_ptr<Utils::GeometryOptimizerBase> optimizer;
96  if (optimizertype == "bofill") {
97  auto tmp = std::make_shared<Utils::GeometryOptimizer<Utils::Bofill>>(*calc);
98  optimizer = std::move(tmp);
99  }
100  else if (optimizertype == "ef" || optimizertype == "ev" || optimizertype == "evf" ||
101  optimizertype == "eigenvectorfollowing" || optimizertype == "eigenvector_following") {
102  auto tmp = std::make_shared<Utils::GeometryOptimizer<Utils::EigenvectorFollowing>>(*calc);
103  optimizer = std::move(tmp);
104  }
105  else if (optimizertype == "dimer") {
106  auto tmp = std::make_shared<Utils::GeometryOptimizer<Utils::Dimer>>(*calc);
107 
108  /* get if coordinates are transformed to know whether a provided guess vector has to be transformed */
109  auto coordinateSystem = tmp->coordinateSystem;
110  if (taskSettings.valueExists("geoopt_coordinate_system")) {
111  coordinateSystem = Utils::CoordinateSystemInterpreter::getCoordinateSystemFromString(
112  taskSettings.getString("geoopt_coordinate_system"));
113  }
114 
115  /* Check for possible guess vectors */
116  bool useEigenvectorForFirstStep = taskSettings.extract("dimer_calculate_hessian_once", false);
117  if (!useEigenvectorForFirstStep && !relevantAtoms.empty()) {
118  _logger->warning << "Specified Dimer optimizer with automatic mode selection, this means that a Hessian has to "
119  "be calculated.\n";
120  }
121  /* set to true if mode is specified or automated selection of mode is set in Settings */
122  if (taskSettings.valueExists("dimer_follow_mode") || !relevantAtoms.empty()) {
123  useEigenvectorForFirstStep = true;
124  }
125  int followedMode = taskSettings.extract("dimer_follow_mode", 0);
126  std::string guessVectorFileName = taskSettings.extract("dimer_guess_vector_file", std::string(""));
127  std::vector<std::string> discreteGuessesFileNames =
128  taskSettings.extract("dimer_discrete_guesses", std::vector<std::string>{"", ""});
129 
130  /* sanity checks for Dimer init options */
131  if (discreteGuessesFileNames.size() != 2) {
132  throw std::runtime_error("Problem with discrete guesses. You may only give list with two filepaths.");
133  }
134  // calc cannot be checked in test run
135  if (!testRunOnly && !useEigenvectorForFirstStep && calc->results().has<Utils::Property::Hessian>()) {
136  _logger->warning << "Did not specify to use eigenvector for initialization of Dimer, although your system "
137  "already includes a Hessian. The Hessian will therefore be ignored. "
138  "If you want to change that, activate the setting 'dimer_calculate_hessian_once': true"
139  << Core::Log::endl;
140  }
141  if (useEigenvectorForFirstStep) {
142  if (!guessVectorFileName.empty()) {
143  throw std::logic_error("You specified to use the eigenvector of the Hessian as Dimer initialization, "
144  "but also specified a file to read a guess vector for the initialization. "
145  "Only select one.");
146  }
147  if (!discreteGuessesFileNames[0].empty()) {
148  throw std::logic_error("You specified to use the eigenvector of the Hessian as Dimer initialization, "
149  "but also specified files to read discrete guesses for the initialization. "
150  "Only select one.");
151  }
152  }
153  else if (!guessVectorFileName.empty() && !discreteGuessesFileNames[0].empty()) {
154  throw std::logic_error("You specified a file to read a guess vector for the Dimer initialization. "
155  "but also specified files to read discrete guesses for the initialization. "
156  "Only select one.");
157  }
158 
159  if (testRunOnly) {
160  ; // skip all dimer if statements here, because they require a Hessian calculation or file look-up
161  }
162  /* Generate and pass initial eigenvector and tell optimizer to not perform the first rotation */
163  else if (useEigenvectorForFirstStep) {
164  tmp->optimizer.skipFirstRotation = true;
165  auto modes = getNormalModes(*calc);
166  auto system = calc->getStructure();
167  if (!relevantAtoms.empty()) { // clash with dimer_follow_mode option is already checked prior
168  followedMode = selectMode(*calc, relevantAtoms);
169  }
170  auto mode = modes.getMode(followedMode); // range check for followedMode is included here
171  /* Currently Hessian not possible in internal coordinates */
172  if (coordinateSystem == Utils::CoordinateSystem::Internal) {
173  /* Transform vector of normal mode into internal coordinates */
174  Utils::InternalCoordinates internalCoordinates(*system);
175  tmp->optimizer.guessVector = std::make_shared<Eigen::VectorXd>(internalCoordinates.coordinatesToInternal(mode));
176  }
177  else if (coordinateSystem == Utils::CoordinateSystem::CartesianWithoutRotTrans) {
178  /* Can give proper invH for all but internal */
179  /* Calculate inverse Hessian with one mode flipped for BFGS within Dimer optimizer */
180  Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> es;
181  auto hessian = calc->results().get<Utils::Property::Hessian>(); // guaranteed to have it from getNormalModes
182  es.compute(hessian);
183  int nEigenValues = es.eigenvalues().size();
184  Eigen::MatrixXd eigenValueMatrix = Eigen::MatrixXd::Identity(nEigenValues, nEigenValues);
185  eigenValueMatrix.diagonal() = es.eigenvalues();
186  eigenValueMatrix(followedMode, followedMode) = -es.eigenvalues()[followedMode];
187  Eigen::MatrixXd switchedHessian = es.eigenvectors() * eigenValueMatrix * es.eigenvectors().transpose();
188  /* Transform vector of normal mode into internal coordinates */
189  Utils::InternalCoordinates internalCoordinates(*system, true);
190  tmp->optimizer.guessVector = std::make_shared<Eigen::VectorXd>(internalCoordinates.coordinatesToInternal(mode));
191  /* Get rid of translation and rotation in Hessian */
192  Eigen::MatrixXd switchedTransformedHessian = internalCoordinates.hessianToInternal(switchedHessian);
193  tmp->optimizer.invH = switchedTransformedHessian.inverse();
194  }
195  else if (coordinateSystem == Utils::CoordinateSystem::Cartesian) {
196  /* no not give invH, because this leads to a lot of translations */
197  tmp->optimizer.guessVector =
198  std::make_shared<Eigen::VectorXd>(Eigen::Map<const Eigen::VectorXd>(mode.data(), mode.cols() * mode.rows()));
199  }
200  else {
201  throw std::logic_error("Unknown coordinate system " +
202  Utils::CoordinateSystemInterpreter::getStringFromCoordinateSystem(coordinateSystem));
203  }
204  }
205  /* Read vector from file to give as guess vector */
206  else if (!guessVectorFileName.empty()) {
207  /* push data into std::vector */
208  std::vector<double> container;
209  double num = 0.0;
210  std::ifstream file(guessVectorFileName, std::ios::in);
211  if (!file.is_open()) {
212  throw std::runtime_error("Problem when opening file " + guessVectorFileName);
213  }
214  while (file >> num) {
215  container.push_back(num);
216  }
217  file.close();
218  Eigen::Map<Eigen::VectorXd> guessVector(container.data(), container.size());
219  auto system = calc->getStructure();
220  /* Provided vector is Cartesian and internal shall be used -> transform to internal */
221  if (coordinateSystem != Utils::CoordinateSystem::Cartesian &&
222  static_cast<unsigned long>(guessVector.size()) == 3 * system->getElements().size()) {
223  std::shared_ptr<Utils::InternalCoordinates> transformation = nullptr;
224  if (coordinateSystem == Utils::CoordinateSystem::Internal) {
225  transformation = std::make_shared<Utils::InternalCoordinates>(*system);
226  }
227  else if (coordinateSystem == Utils::CoordinateSystem::CartesianWithoutRotTrans) {
228  transformation = std::make_shared<Utils::InternalCoordinates>(*system, true);
229  }
230  // vector to PositionCollection
231  Utils::PositionCollection guessPosition =
232  Eigen::Map<Utils::PositionCollection>(guessVector.data(), guessVector.size() / 3, 3);
233  try {
234  tmp->optimizer.guessVector =
235  std::make_shared<Eigen::VectorXd>(transformation->coordinatesToInternal(guessPosition));
236  }
237  catch (const Utils::InternalCoordinatesException& e) {
238  _logger->warning << "Could not transform given Cartesian coordinates to Internal coordinates.\n"
239  << "Omitting guess and initializing dimer with random vector. " << Core::Log::endl;
240  }
241  }
242  /* Provided vector is not Cartesian and Cartesian shall be used -> transform to Cartesian */
243  else if (coordinateSystem == Utils::CoordinateSystem::Cartesian &&
244  static_cast<unsigned long>(guessVector.size()) != 3 * system->getElements().size()) {
245  try {
246  std::shared_ptr<Utils::InternalCoordinates> transformation = nullptr;
247  if (static_cast<unsigned long>(guessVector.size()) == 3 * system->getElements().size() - 6 ||
248  static_cast<unsigned long>(guessVector.size()) == 3 * system->getElements().size() - 5) {
249  // cartesianWithoutRotTrans because 5 or 6 missing
250  transformation = std::make_shared<Utils::InternalCoordinates>(*system, true);
251  }
252  else {
253  transformation = std::make_shared<Utils::InternalCoordinates>(*system);
254  }
255  /* single guess vector cannot get backtransformed directly to Cartesian coords */
256  /* first get current position and transform to internal */
257  auto cartPos0 = system->getPositions();
258  auto ircVec0 = transformation->coordinatesToInternal(cartPos0);
259  /* add guessvector */
260  Eigen::VectorXd ircVec1 = ircVec0 + guessVector;
261  /* transform this new vector back to Cartesian and diff of 2 Cartesian PositionCollection is guessvector */
262  auto cartPos1 = transformation->coordinatesToCartesian(ircVec1);
263  Utils::PositionCollection posDiff = cartPos1 - cartPos0;
264  tmp->optimizer.guessVector =
265  std::make_shared<Eigen::VectorXd>(Eigen::Map<Eigen::VectorXd>(posDiff.data(), posDiff.size()));
266  }
267  catch (const Utils::InternalCoordinatesException& e) {
268  _logger->warning << "Could not transform given non-Cartesian coordinates to Cartesian.\n"
269  << "Omitting guess and initializing dimer with random vector. " << Core::Log::endl;
270  }
271  }
272  /* Provided vector should fit with wanted coordinates, optimizer later checks the correct length again */
273  else {
274  tmp->optimizer.guessVector = std::make_shared<Eigen::VectorXd>(guessVector);
275  }
276  }
277  /* Read 2 structures from files to form guess vector from discrete difference of the two */
278  else if (!discreteGuessesFileNames.at(0).empty() && !discreteGuessesFileNames.at(1).empty()) {
279  Utils::AtomCollection system;
280  /* read first */
281  std::string filepath1 = discreteGuessesFileNames.at(0);
282  std::ifstream file1(filepath1, std::ios::in);
283  if (!file1.is_open()) {
284  throw std::runtime_error("Problem when opening file " + filepath1);
285  }
286  system = XyzHandler::read(file1);
287  auto position1 = system.getPositions();
288  file1.close();
289 
290  /* read second */
291  std::string filepath2 = discreteGuessesFileNames.at(1);
292  std::ifstream file2(filepath2, std::ios::in);
293  if (!file2.is_open()) {
294  throw std::runtime_error("Problem when opening file " + filepath2);
295  }
296  system = XyzHandler::read(file2);
297  auto position2 = system.getPositions();
298  file2.close();
299 
300  Utils::PositionCollection positionDiff = position2 - position1;
301  Eigen::Map<Eigen::VectorXd> mode(positionDiff.data(), positionDiff.size());
302  std::shared_ptr<Utils::InternalCoordinates> transformation = nullptr;
303  if (coordinateSystem == Utils::CoordinateSystem::Internal) {
304  transformation = std::make_shared<Utils::InternalCoordinates>(system);
305  }
306  else if (coordinateSystem == Utils::CoordinateSystem::CartesianWithoutRotTrans) {
307  transformation = std::make_shared<Utils::InternalCoordinates>(system, true);
308  }
309  if (transformation) {
310  tmp->optimizer.guessVector = std::make_shared<Eigen::VectorXd>(transformation->coordinatesToInternal(positionDiff));
311  }
312  else {
313  tmp->optimizer.guessVector = std::make_shared<Eigen::VectorXd>(mode);
314  }
315  }
316  optimizer = std::move(tmp);
317  } // end of Dimer handling
318  else {
319  throw std::runtime_error(
320  "Unknown Optimizer requested for TS optimization, available are: Bofill, EVF and Dimer!");
321  }
322 
323  // Read and delete special settings
324  bool stopOnError = stopOnErrorExtraction(taskSettings);
325  // Apply settings
326  auto settings = optimizer->getSettings();
327  settings.merge(taskSettings);
328  // set automatically selected mode, if requested
329  // for Dimer this happened already, because it is GradientBased and does not know anything about modes / Hessians
330  // do not do in testRun because calc would be empty
331  if (!relevantAtoms.empty() && !testRunOnly) {
332  // necessary because multiple strings are accepted for eigenvectorfollowing optimizer
333  if (optimizertype != "dimer" && optimizertype != "bofill") {
334  settings.modifyValue("ev_follow_mode", selectMode(*calc, relevantAtoms));
335  }
336  else if (optimizertype == "bofill") {
337  settings.modifyValue("bofill_follow_mode", selectMode(*calc, relevantAtoms));
338  }
339  }
340  if (!settings.valid()) {
341  settings.throwIncorrectSettings();
342  }
343  optimizer->setSettings(settings);
344  if (!testRunOnly && !Utils::GeometryOptimization::settingsMakeSense(*optimizer)) {
345  throw std::logic_error("The given calculator settings are too inaccurate for the given convergence criteria of "
346  "this optimization Task");
347  }
348 
349  // If no errors encountered until here, the basic settings should be alright
350  if (testRunOnly) {
351  return true;
352  }
353 
354  // Add observer
355  // Trajectory stream
356  Utils::XyzStreamHandler writer;
357  const std::string& partialOutput = (!_output.empty() ? _output[0] : _input[0]);
358  boost::filesystem::path dir(partialOutput);
359  boost::filesystem::create_directory(dir);
360  boost::filesystem::path trjfile(partialOutput + ".tsopt.trj.xyz");
361  std::ofstream trajectory((dir / trjfile).string(), std::ofstream::out);
362  cout.printf("%7s %16s %16s %16s %16s\n", "Cycle", "Energy", "Energy Diff.", "Step RMS", "Max. Step");
363  double oldEnergy = 0.0;
364  Eigen::VectorXd oldParams;
365  auto func = [&](const int& cycle, const double& energy, const Eigen::VectorXd& params) {
366  if (oldParams.size() != params.size()) {
367  oldParams = params;
368  }
369  auto diff = (params - oldParams).eval();
370  cout.printf("%7d %+16.9f %+16.9f %+16.9f %+16.9f\n", cycle, energy, energy - oldEnergy,
371  sqrt(diff.squaredNorm() / diff.size()), diff.cwiseAbs().maxCoeff());
372  oldEnergy = energy;
373  oldParams = params;
374  auto structure = calc->getStructure();
375  XyzHandler::write(trajectory, *structure, std::to_string(energy));
376  };
377  optimizer->addObserver(func);
378 
379  // Add custom observers
380  auto customObservers = [&calc, &observers](const int& cycle, const double& /*energy*/, const Eigen::VectorXd& /*params*/) {
381  for (auto& observer : observers) {
382  auto atoms = calc->getStructure();
383  Utils::Results& results = calc->results();
384  observer(cycle, *atoms, results, "ts_optimization");
385  }
386  };
387  optimizer->addObserver(customObservers);
388 
389  // Run optimization
390  auto structure = calc->getStructure();
391  int cycles = 0;
392  try {
393  cycles = optimizer->optimize(*structure, *_logger);
394  }
395  catch (...) {
396  XyzHandler::write(trajectory, *calc->getStructure());
397  trajectory.close();
398  _logger->error << "TS Optimization failed with error!" << Core::Log::endl;
399  if (stopOnError) {
400  throw;
401  }
402  _logger->error << boost::current_exception_diagnostic_information() << Core::Log::endl;
403  return false;
404  }
405  trajectory.close();
406 
407  int maxiter = settings.getInt("convergence_max_iterations");
408  if (cycles < maxiter) {
409  cout << Core::Log::endl
410  << " Converged after " << cycles << " iterations." << Core::Log::endl
411  << Core::Log::endl;
412  }
413  else {
414  cout << Core::Log::endl
415  << " Stopped after " << maxiter << " iterations." << Core::Log::endl
416  << Core::Log::endl;
417  if (stopOnError) {
418  throw std::runtime_error("Problem: TS optimization did not converge.");
419  }
420  }
421 
422  // Print/Store results
423  systems[partialOutput] = calc;
424  boost::filesystem::path xyzfile(partialOutput + ".xyz");
425  std::ofstream xyz((dir / xyzfile).string(), std::ofstream::out);
426  XyzHandler::write(xyz, *(calc->getStructure()));
427  xyz.close();
428 
429  return cycles < maxiter;
430  }
431 
432  private:
433  int selectMode(Core::Calculator& calc, const std::vector<int>& relevantAtoms) const {
434  _logger->output << " Automatically selecting normal mode " << Core::Log::endl;
435  int nAtoms = calc.getStructure()->size();
436  for (const auto& index : relevantAtoms) {
437  if (index >= nAtoms) {
438  throw std::logic_error("You gave an atom index larger than the number of atoms in automatic_mode_selection. "
439  "This does not make sense.");
440  }
441  }
442  Utils::NormalModesContainer modes = getNormalModes(calc);
443  auto waveNumbers = modes.getWaveNumbers();
444  std::vector<double> contributions;
445  // cycle all imag. frequencies
446  for (int i = 0; i < static_cast<int>(waveNumbers.size()); ++i) {
447  if (waveNumbers[i] >= 0.0) {
448  if (i == 0) {
449  throw std::runtime_error("Structure has no imaginary normal mode.");
450  }
451  break;
452  }
453  auto mode = modes.getMode(i);
454  double contribution = 0.0;
455  // cycle all atoms
456  for (int j = 0; j < mode.size(); ++j) {
457  if (std::find(relevantAtoms.begin(), relevantAtoms.end(), j) != relevantAtoms.end()) {
458  contribution += mode.row(j).norm(); // add norm of vector of relevant atom contributing to mode
459  }
460  }
461  contributions.push_back(contribution);
462  }
463  int selection = std::distance(contributions.begin(), std::max_element(contributions.begin(), contributions.end()));
464  double tolerance = contributions[selection] * 0.1;
465  // Contributions are sorted by wave number
466  // -> pick the first one that fits into the tolerance.
467  for (unsigned int i = 0; i < contributions.size(); i++) {
468  if ((contributions[selection] - contributions[i]) < tolerance) {
469  selection = i;
470  break;
471  }
472  }
473  _logger->output << " Automatically selected mode " << std::to_string(selection) << " with wave number "
474  << std::to_string(waveNumbers[selection]) << " cm^-1" << Core::Log::endl;
475  return selection;
476  }
477 
478  static Utils::NormalModesContainer getNormalModes(Core::Calculator& calc) {
479  if (!calc.results().has<Utils::Property::Hessian>()) {
480  calc.setRequiredProperties(Utils::Property::Hessian);
481  calc.calculate("Hessian Calculation");
482  }
483  auto hessian = calc.results().get<Utils::Property::Hessian>();
484  auto system = calc.getStructure();
485  return Utils::NormalModeAnalysis::calculateNormalModes(hessian, system->getElements(), system->getPositions());
486  }
487 };
488 
489 } // namespace Readuct
490 } // namespace Scine
491 
492 #endif // READUCT_TSOPTIMIZATIONTASK_H_
void warningIfMultipleOutputsGiven() const
Warn if more than one output system was specified.
Definition: Task.h:104
static std::ostream & endl(std::ostream &os)
const std::vector< std::string > & input() const
Getter for the expected names of the input systems.
Definition: Task.h:82
std::string name() const override
Getter for the tasks name.
Definition: TsOptimizationTask.h:47
TsOptimizationTask(std::vector< std::string > input, std::vector< std::string > output, std::shared_ptr< Core::Log > logger=nullptr)
Construct a new TsOptimizationTask.
Definition: TsOptimizationTask.h:43
bool run(SystemsMap &systems, Utils::UniversalSettings::ValueCollection taskSettings, bool testRunOnly=false, std::vector< std::function< void(const int &, const Utils::AtomCollection &, const Utils::Results &, const std::string &)>> observers={}) const final
Executes the actual task represented by this class.
Definition: TsOptimizationTask.h:51
void warningIfMultipleInputsGiven() const
Warn if more than one input system was specified.
Definition: Task.h:95
const std::vector< std::string > & output() const
Getter for the names of the output systems generated by this task.
Definition: Task.h:89
The base class for all tasks in Readuct.
Definition: Task.h:28
Definition: TsOptimizationTask.h:35