Scine::Readuct  6.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 */
24 /* External */
25 #include <boost/exception/diagnostic_information.hpp>
26 #include <boost/filesystem.hpp>
27 /* std c++ */
28 #include <cstdio>
29 #include <fstream>
30 #include <iostream>
31 #include <vector>
32 
33 namespace Scine {
34 namespace Readuct {
35 
36 class TsOptimizationTask : public Task {
37  public:
44  TsOptimizationTask(std::vector<std::string> input, std::vector<std::string> output, std::shared_ptr<Core::Log> logger = nullptr)
45  : Task(std::move(input), std::move(output), std::move(logger)) {
46  }
47 
48  std::string name() const override {
49  return "TS Optimization";
50  }
51 
52  bool run(SystemsMap& systems, Utils::UniversalSettings::ValueCollection taskSettings, bool testRunOnly = false,
53  std::vector<std::function<void(const int&, const Utils::AtomCollection&, const Utils::Results&, const std::string&)>>
54  observers = {}) const final {
57 
58  bool silentCalculator = taskSettings.extract("silent_stdout_calculator", true);
59  std::shared_ptr<Core::Calculator> calc;
60  bool isQmmm = false;
61  if (!testRunOnly) { // leave out in case of task chaining --> attention calc is NULL
62  // Note: _input is guaranteed not to be empty by Task constructor
63  calc = copyCalculator(systems, _input.front(), name());
64  Utils::CalculationRoutines::setLog(*calc, true, true, !silentCalculator);
65 
66  // Check system size
67  if (calc->getStructure()->size() == 1) {
68  throw std::runtime_error("Cannot calculate transition state for monoatomic systems.");
69  }
70  if (calc->name() == "QMMM") {
71  isQmmm = true;
72  }
73  }
74 
75  // Generate optimizer
76  std::string optimizertype = taskSettings.extract("optimizer", std::string{"bofill"});
77  std::string mmOptimizertype = taskSettings.extract("mm_optimizer", std::string{"bfgs"});
78  std::transform(optimizertype.begin(), optimizertype.end(), optimizertype.begin(), ::tolower);
79 
80  // Have to exclude settings check from test run due to newly introduced optimizer types
81  // that cannot be constructed in test runs, because they require the knowledge of the
82  // calculator type which we do not have in a test run
83  if (testRunOnly) {
84  return true;
85  }
86 
87  // Check for automatic mode selection
88  // sanity check
89  if (taskSettings.valueExists("automatic_mode_selection") &&
90  (taskSettings.valueExists(optimizertype + "_follow_mode") || taskSettings.valueExists("ev_follow_mode"))) {
91  throw std::logic_error("You specified an automatic selection of the mode and gave a specific mode yourself. "
92  "This is not possible. Only give one of those options.");
93  }
94  // Extract settings for mode selection
95  std::vector<int> relevantAtoms = taskSettings.extract("automatic_mode_selection", std::vector<int>{});
96  std::tuple<double, double> modeSelectionWeights = extractWeightsForModeSelection(taskSettings);
97  // another sanity check
98  if (!relevantAtoms.empty()) {
99  for (const auto& index : relevantAtoms) {
100  if (index < 0) {
101  throw std::logic_error("You gave an atom index smaller than 0 in automatic_mode_selection. "
102  "This does not make sense.");
103  }
104  }
105  }
106  bool writeSelectedMode = taskSettings.extract("write_selected_mode", false);
107 
108  using XyzHandler = Utils::XyzStreamHandler;
109  auto cout = _logger->output;
110 
111  auto optimizer = constructOptimizer(calc, optimizertype, mmOptimizertype, isQmmm, taskSettings, relevantAtoms,
112  modeSelectionWeights, testRunOnly); // rel atoms and weights for dimer
113  // Read and delete special settings
114  bool stopOnError = stopOnErrorExtraction(taskSettings);
115  // Apply settings
116  auto settings = optimizer->getSettings();
117  settings.merge(taskSettings);
118  // set automatically selected mode, if requested
119  // for Dimer this happened already, because it is GradientBased and does not know anything about modes / Hessians
120  // do not do in testRun because calc would be empty
121  if (!relevantAtoms.empty() && !testRunOnly) {
122  // necessary because multiple strings are accepted for eigenvectorfollowing optimizer
123  if (optimizertype != "dimer" && optimizertype != "bofill") {
124  settings.modifyValue("ev_follow_mode", selectMode(*calc, relevantAtoms, std::get<0>(modeSelectionWeights),
125  std::get<1>(modeSelectionWeights)));
126  }
127  else if (optimizertype == "bofill") {
128  settings.modifyValue("bofill_follow_mode", selectMode(*calc, relevantAtoms, std::get<0>(modeSelectionWeights),
129  std::get<1>(modeSelectionWeights)));
130  }
131  }
132  int autoSelectedMode = 0;
133  if (optimizertype != "dimer" && optimizertype != "bofill") {
134  autoSelectedMode = settings.getInt("ev_follow_mode");
135  }
136  else if (optimizertype == "bofill") {
137  autoSelectedMode = settings.getInt("bofill_follow_mode");
138  }
139  if (!settings.valid()) {
140  settings.throwIncorrectSettings();
141  }
142  optimizer->setSettings(settings);
143  if (!testRunOnly && !Utils::GeometryOptimization::settingsMakeSense(*optimizer)) {
144  throw std::logic_error("The given calculator settings are too inaccurate for the given convergence criteria of "
145  "this optimization Task");
146  }
147 
148  // Add observer
149  // Trajectory stream
150  Utils::XyzStreamHandler writer;
151  const std::string& partialOutput = (!_output.empty() ? _output[0] : _input[0]);
152  boost::filesystem::path dir(partialOutput);
153  boost::filesystem::create_directory(dir);
154  boost::filesystem::path trjfile(partialOutput + ".tsopt.trj.xyz");
155 
156  // Write selected mode, if requested and the dimer is not selected.
157  if (optimizertype == "dimer" && writeSelectedMode) {
158  cout << " Can't write a mode for the Dimer optimizer." << Core::Log::endl;
159  }
160  else if (optimizertype != "dimer" && writeSelectedMode) {
161  auto modes = getNormalModes(*calc);
162  auto trj = modes.getModeAsMolecularTrajectory(autoSelectedMode, *calc->getStructure(), 1.0);
163  std::string selectedModeFilename =
164  Utils::format("%s.selected.vibmode-%05d.trj.xyz", partialOutput.c_str(), autoSelectedMode + 1);
165  std::ofstream selectedModeXyz((dir / selectedModeFilename).string(), std::ofstream::out);
167  selectedModeXyz.close();
168  }
169 
170  std::ofstream trajectory((dir / trjfile).string(), std::ofstream::out);
171  cout.printf("%7s %16s %16s %16s %16s\n", "Cycle", "Energy", "Energy Diff.", "Step RMS", "Max. Step");
172  double oldEnergy = 0.0;
173  Eigen::VectorXd oldParams;
174  auto func = [&](const int& cycle, const double& energy, const Eigen::VectorXd& params) {
175  if (oldParams.size() != params.size()) {
176  oldParams = params;
177  }
178  auto diff = (params - oldParams).eval();
179  auto fullEnergy = energy;
180  cout.printf("%7d %+16.9f %+16.9f %+16.9f %+16.9f\n", cycle, fullEnergy, fullEnergy - oldEnergy,
181  sqrt(diff.squaredNorm() / diff.size()), diff.cwiseAbs().maxCoeff());
182  oldEnergy = fullEnergy;
183  oldParams = params;
184  auto structure = calc->getStructure();
185  XyzHandler::write(trajectory, *structure, std::to_string(fullEnergy));
186  };
187  optimizer->addObserver(func);
188 
189  // Add custom observers
190  auto customObservers = [&calc, &observers](const int& cycle, const double& /*energy*/, const Eigen::VectorXd& /*params*/) {
191  for (auto& observer : observers) {
192  auto atoms = calc->getStructure();
193  Utils::Results& results = calc->results();
194  observer(cycle, *atoms, results, "ts_optimization");
195  }
196  };
197  optimizer->addObserver(customObservers);
198 
199  // Run optimization
200  auto structure = calc->getStructure();
201  int cycles = 0;
202  try {
203  cycles = optimizer->optimize(*structure, *_logger);
204  }
205  catch (...) {
206  double lastEnergy = oldEnergy;
207  if (calc->results().has<Utils::Property::Energy>()) {
208  lastEnergy = calc->results().get<Utils::Property::Energy>();
209  }
210  XyzHandler::write(trajectory, *calc->getStructure(), std::to_string(lastEnergy));
211  trajectory.close();
212  _logger->error << "TS Optimization failed with error!" << Core::Log::endl;
213  if (stopOnError) {
214  throw;
215  }
216  _logger->error << boost::current_exception_diagnostic_information() << Core::Log::endl;
217  return false;
218  }
219  trajectory.close();
220 
221  int maxiter = settings.getInt("convergence_max_iterations");
222  if (cycles < maxiter) {
223  cout << Core::Log::endl
224  << " Converged after " << cycles << " iterations." << Core::Log::endl
225  << Core::Log::endl;
226  if (isQmmm) {
227  auto qmmmEnergy = calc->calculate("QMMM calculation").get<Utils::Property::Energy>();
228  cout << Core::Log::endl << " The final QM/MM energy is " << qmmmEnergy << Core::Log::endl << Core::Log::endl;
229  }
230  }
231  else {
232  cout << Core::Log::endl
233  << " Stopped after " << maxiter << " iterations." << Core::Log::endl
234  << Core::Log::endl;
235  if (stopOnError) {
236  throw std::runtime_error("Problem: TS optimization did not converge.");
237  }
238  }
239 
240  // Print/Store results
241  systems[partialOutput] = calc;
242  boost::filesystem::path xyzfile(partialOutput + ".xyz");
243  std::ofstream xyz((dir / xyzfile).string(), std::ofstream::out);
244  XyzHandler::write(xyz, *(calc->getStructure()));
245  xyz.close();
246 
247  return cycles < maxiter;
248  }
249 
250  private:
251  std::tuple<double, double> extractWeightsForModeSelection(Utils::UniversalSettings::ValueCollection& taskSettings) const {
252  double wavenumberWeight = taskSettings.extract("automatic_mode_selection_wavenumber_weight", 0.5);
253  if (wavenumberWeight < 0.0 || wavenumberWeight - 1.0 > 1e-12) {
254  _logger->output << " Weights for the automatic mode selection must be between 0.0 and 1.0." << Core::Log::endl
255  << " Resetting wavenumber weight to default (0.5)." << Core::Log::endl;
256  wavenumberWeight = 0.5;
257  }
258  double contributionWeight = taskSettings.extract("automatic_mode_selection_contribution_weight", 0.5);
259  if (contributionWeight < 0.0 || contributionWeight - 1.0 > 1e-12) {
260  _logger->output << " Weights for the automatic mode selection must be between 0.0 and 1.0." << Core::Log::endl
261  << " Resetting contribution weight to default (0.5)." << Core::Log::endl;
262  contributionWeight = 0.5;
263  }
264  // Check mode selection settings
265  if (std::fabs(wavenumberWeight + contributionWeight - 1.0) > 1e-12) {
266  _logger->output << " Weights for the automatic mode selection must add up to 1.0." << Core::Log::endl
267  << " Resetting weights to default (0.5)." << Core::Log::endl;
268  wavenumberWeight = 0.5;
269  contributionWeight = 0.5;
270  }
271  return std::make_tuple(wavenumberWeight, contributionWeight);
272  }
273 
274  int selectMode(Core::Calculator& calc, const std::vector<int>& relevantAtoms, double wavenumberWeight,
275  double contributionWeight) const {
276  _logger->output << " Automatically selecting normal mode " << Core::Log::endl;
277  int nAtoms = calc.getStructure()->size();
278  // Get square root of masses of all atoms
279  Eigen::MatrixXd sqRootMasses = Eigen::MatrixXd::Ones(nAtoms, 3);
280  for (int index = 0; index < nAtoms; index++) {
281  if (index >= nAtoms) {
282  throw std::logic_error("You gave an atom index larger than the number of atoms in automatic_mode_selection. "
283  "This does not make sense.");
284  }
285  sqRootMasses.row(index) *= std::sqrt(Utils::ElementInfo::mass(calc.getStructure()->getElement(index)));
286  }
287  Utils::NormalModesContainer modes = getNormalModes(calc);
288  auto waveNumbers = modes.getWaveNumbers();
289  std::vector<double> contributions;
290  std::vector<double> imWaveNumbers;
291  // cycle all imaginary frequencies
292  for (int i = 0; i < static_cast<int>(waveNumbers.size()); ++i) {
293  if (waveNumbers[i] >= 0.0) {
294  if (i == 0) {
295  throw std::runtime_error("Structure has no imaginary normal mode.");
296  }
297  break;
298  }
299  const auto& mode = modes.getMode(i);
300  auto massWeightedMode = mode.cwiseProduct(sqRootMasses).normalized();
301 
302  double contribution = 0.0;
303  // cycle all atoms
304  for (int j = 0; j < mode.size(); ++j) {
305  if (std::find(relevantAtoms.begin(), relevantAtoms.end(), j) != relevantAtoms.end()) {
306  // squared norm to obtain relative atom contribution of normalized mass weighted mode
307  contribution += massWeightedMode.row(j).squaredNorm();
308  }
309  }
310  contributions.push_back(contribution);
311  imWaveNumbers.push_back(waveNumbers[i]);
312  }
313  Eigen::VectorXd imWaveVec = Eigen::Map<Eigen::VectorXd>(imWaveNumbers.data(), static_cast<int>(imWaveNumbers.size()), 1);
314  Eigen::VectorXd contributionsVec =
315  Eigen::Map<Eigen::VectorXd>(contributions.data(), static_cast<int>(contributions.size()), 1);
316  // Normalize by lowest wavenumber
317  imWaveVec *= 1.0 / imWaveVec(0);
318  // Determine mode Score
319  Eigen::VectorXd modeScore = imWaveVec * wavenumberWeight + contributionsVec * contributionWeight;
320  _logger->output << " Mode score considering contribution (" << std::to_string(contributionWeight)
321  << ") and wavenumber (" << std::to_string(wavenumberWeight) << ")." << Core::Log::endl;
322 
323  // Get index of maximum
324  Eigen::Index maxRow = 0;
325  modeScore.maxCoeff(&maxRow);
326  int selection = static_cast<int>(maxRow);
327 
328  _logger->output << " Automatically selected mode " << std::to_string(selection) << " with wave number "
329  << std::to_string(waveNumbers[selection]) << " cm^-1" << Core::Log::endl;
330  return selection;
331  }
332 
333  static Utils::NormalModesContainer getNormalModes(Core::Calculator& calc) {
334  const auto previousResults = calc.results();
335  bool calculationRequired = false;
336  Utils::PropertyList requiredProperties = Utils::Property::Energy | Utils::Property::Gradients;
337  if (!calc.results().has<Utils::Property::Thermochemistry>() &&
338  calc.possibleProperties().containsSubSet(Utils::Property::Thermochemistry)) {
339  calculationRequired = true;
340  requiredProperties.addProperty(Utils::Property::Thermochemistry);
341  // many calculators cannot handle that only Thermochemistry is requested
342  // because they delete their results and then assume that Hessian was also calculated
343  if (calc.possibleProperties().containsSubSet(Utils::Property::PartialHessian)) {
344  requiredProperties.addProperty(Utils::Property::PartialHessian);
345  }
346  else if (calc.possibleProperties().containsSubSet(Utils::Property::Hessian)) {
347  requiredProperties.addProperty(Utils::Property::Hessian);
348  }
349  }
350  else if (!calc.results().has<Utils::Property::PartialHessian>() &&
351  calc.possibleProperties().containsSubSet(Utils::Property::PartialHessian)) {
352  requiredProperties.addProperty(Utils::Property::PartialHessian);
353  calculationRequired = true;
354  }
355  else if (!calc.results().has<Utils::Property::Hessian>() &&
356  calc.possibleProperties().containsSubSet(Utils::Property::Hessian)) {
357  requiredProperties.addProperty(Utils::Property::Hessian);
358  calculationRequired = true;
359  }
360  if (calculationRequired) {
361  calc.setRequiredProperties(requiredProperties);
362  auto results = calc.calculate("Hessian Calculation");
363  calc.results() = previousResults + results;
364  }
365  auto system = calc.getStructure();
366  if (calc.results().has<Utils::Property::PartialHessian>()) {
367  Utils::PartialHessian hessian = calc.results().get<Utils::Property::PartialHessian>();
368  // normalized (default in NormalModeAnalysis), but not mass weighted!
369  return Utils::NormalModeAnalysis::calculateNormalModes(hessian, system->getElements(), system->getPositions(),
370  true, false);
371  }
372  else if (calc.results().has<Utils::Property::Hessian>()) {
373  Utils::HessianMatrix hessian = calc.results().get<Utils::Property::Hessian>();
374  // normalized (default in NormalModeAnalysis), but not mass weighted!
375  return Utils::NormalModeAnalysis::calculateNormalModes(hessian, system->getElements(), system->getPositions(),
376  true, false);
377  }
378  else {
379  throw std::runtime_error("Calculator is missing a Hessian property");
380  }
381  }
382 
383  inline std::shared_ptr<Utils::GeometryOptimizerBase>
384  constructOptimizer(std::shared_ptr<Core::Calculator>& calc, std::string type, std::string mmType, bool isQmmm,
385  Utils::UniversalSettings::ValueCollection& taskSettings, const std::vector<int>& relevantAtoms,
386  const std::tuple<double, double>& modeSelectionWeights, bool testRunOnly) const {
387  // this method does not fail in test runs for QM/MM optimizers, because in test runs the isQmmm flag is always false
388  std::transform(type.begin(), type.end(), type.begin(), ::tolower);
389  const std::array<std::string, 5> evfSynonyms = {"ef", "ev", "evf", "eigenvectorfollowing", "eigenvector_following"};
390  if (!isQmmm) {
391  if (type == "bofill") {
392  return std::make_shared<Utils::GeometryOptimizer<Utils::Bofill>>(*calc);
393  }
394  if (std::find(evfSynonyms.begin(), evfSynonyms.end(), type) != evfSynonyms.end()) {
395  return std::make_shared<Utils::GeometryOptimizer<Utils::EigenvectorFollowing>>(*calc);
396  }
397  if (type == "dimer") {
398  auto optimizer = std::make_shared<Utils::GeometryOptimizer<Utils::Dimer>>(*calc);
399  handleDimerOptimizer(optimizer, taskSettings, calc, relevantAtoms, modeSelectionWeights, testRunOnly);
400  return optimizer;
401  }
402  throw std::runtime_error(
403  "Unknown Optimizer requested for TS optimization, available are: Bofill, EVF and Dimer!");
404  }
405  // construct QM/MM TransitionState Optimizer
406  std::transform(mmType.begin(), mmType.end(), mmType.begin(), ::tolower);
407  if (type == "bofill") {
408  if (mmType == "bfgs") {
409  return std::make_shared<Utils::QmmmTransitionStateOptimizer<Utils::Bofill, Utils::Bfgs>>(calc);
410  }
411  if (mmType == "lbfgs") {
412  return std::make_shared<Utils::QmmmTransitionStateOptimizer<Utils::Bofill, Utils::Lbfgs>>(calc);
413  }
414  if (mmType == "sd" || mmType == "steepestdescent") {
415  return std::make_shared<Utils::QmmmTransitionStateOptimizer<Utils::Bofill, Utils::SteepestDescent>>(calc);
416  }
417  throw std::runtime_error(
418  "Unknown MMOptimizer requested for a geometry optimization, available are: SD, BFGS and LBFGS!");
419  }
420  if (std::find(evfSynonyms.begin(), evfSynonyms.end(), type) != evfSynonyms.end()) {
421  if (mmType == "bfgs") {
422  return std::make_shared<Utils::QmmmTransitionStateOptimizer<Utils::EigenvectorFollowing, Utils::Bfgs>>(calc);
423  }
424  if (mmType == "lbfgs") {
425  return std::make_shared<Utils::QmmmTransitionStateOptimizer<Utils::EigenvectorFollowing, Utils::Lbfgs>>(calc);
426  }
427  if (mmType == "sd" || mmType == "steepestdescent") {
428  return std::make_shared<Utils::QmmmTransitionStateOptimizer<Utils::EigenvectorFollowing, Utils::SteepestDescent>>(calc);
429  }
430  throw std::runtime_error(
431  "Unknown MMOptimizer requested for a geometry optimization, available are: SD, BFGS and LBFGS!");
432  }
433  if (type == "dimer") {
434  if (mmType == "bfgs") {
435  auto optimizer = std::make_shared<Utils::QmmmTransitionStateOptimizer<Utils::Dimer, Utils::Bfgs>>(calc);
436  handleDimerOptimizer(optimizer->qmOptimizer, taskSettings, calc, relevantAtoms, modeSelectionWeights, testRunOnly);
437  return optimizer;
438  }
439  if (mmType == "lbfgs") {
440  auto optimizer = std::make_shared<Utils::QmmmTransitionStateOptimizer<Utils::Dimer, Utils::Lbfgs>>(calc);
441  handleDimerOptimizer(optimizer->qmOptimizer, taskSettings, calc, relevantAtoms, modeSelectionWeights, testRunOnly);
442  return optimizer;
443  }
444  if (mmType == "sd" || mmType == "steepestdescent") {
445  auto optimizer = std::make_shared<Utils::QmmmTransitionStateOptimizer<Utils::Dimer, Utils::SteepestDescent>>(calc);
446  handleDimerOptimizer(optimizer->qmOptimizer, taskSettings, calc, relevantAtoms, modeSelectionWeights, testRunOnly);
447  return optimizer;
448  }
449  throw std::runtime_error(
450  "Unknown MMOptimizer requested for a geometry optimization, available are: SD, BFGS and LBFGS!");
451  }
452  throw std::runtime_error("Unknown Optimizer requested for TS optimization, available are: Bofill, EVF and Dimer!");
453  };
454 
455  inline void handleDimerOptimizer(std::shared_ptr<Utils::GeometryOptimizer<Utils::Dimer>>& optimizer,
456  Utils::UniversalSettings::ValueCollection& taskSettings,
457  const std::shared_ptr<Core::Calculator>& calc, const std::vector<int>& relevantAtoms,
458  const std::tuple<double, double>& modeSelectionWeights, bool testRunOnly) const {
459  using XyzHandler = Utils::XyzStreamHandler;
460  /* get if coordinates are transformed to know whether a provided guess vector has to be transformed */
461  auto coordinateSystem = optimizer->coordinateSystem;
462  if (taskSettings.valueExists("geoopt_coordinate_system")) {
463  coordinateSystem = Utils::CoordinateSystemInterpreter::getCoordinateSystemFromString(
464  taskSettings.getString("geoopt_coordinate_system"));
465  }
466 
467  /* Check for possible guess vectors */
468  bool useEigenvectorForFirstStep = taskSettings.extract("dimer_calculate_hessian_once", false);
469  if (!useEigenvectorForFirstStep && !relevantAtoms.empty()) {
470  _logger->warning << "Specified Dimer optimizer with automatic mode selection, this means that a Hessian has to "
471  "be calculated.\n";
472  }
473  /* set to true if mode is specified or automated selection of mode is set in Settings */
474  if (taskSettings.valueExists("dimer_follow_mode") || !relevantAtoms.empty()) {
475  useEigenvectorForFirstStep = true;
476  }
477  int followedMode = taskSettings.extract("dimer_follow_mode", 0);
478  std::string guessVectorFileName = taskSettings.extract("dimer_guess_vector_file", std::string(""));
479  std::vector<std::string> discreteGuessesFileNames =
480  taskSettings.extract("dimer_discrete_guesses", std::vector<std::string>{"", ""});
481 
482  /* sanity checks for Dimer init options */
483  if (discreteGuessesFileNames.size() != 2) {
484  throw std::runtime_error("Problem with discrete guesses. You may only give list with two filepaths.");
485  }
486  // calc cannot be checked in test run
487  if (!testRunOnly && !useEigenvectorForFirstStep && calc->results().has<Utils::Property::Hessian>()) {
488  _logger->warning << "Did not specify to use eigenvector for initialization of Dimer, although your system "
489  "already includes a Hessian. The Hessian will therefore be ignored. "
490  "If you want to change that, activate the setting 'dimer_calculate_hessian_once': true"
491  << Core::Log::endl;
492  }
493  if (useEigenvectorForFirstStep) {
494  if (!guessVectorFileName.empty()) {
495  throw std::logic_error("You specified to use the eigenvector of the Hessian as Dimer initialization, "
496  "but also specified a file to read a guess vector for the initialization. "
497  "Only select one.");
498  }
499  if (!discreteGuessesFileNames[0].empty()) {
500  throw std::logic_error("You specified to use the eigenvector of the Hessian as Dimer initialization, "
501  "but also specified files to read discrete guesses for the initialization. "
502  "Only select one.");
503  }
504  }
505  else if (!guessVectorFileName.empty() && !discreteGuessesFileNames[0].empty()) {
506  throw std::logic_error("You specified a file to read a guess vector for the Dimer initialization. "
507  "but also specified files to read discrete guesses for the initialization. "
508  "Only select one.");
509  }
510 
511  if (testRunOnly) {
512  ; // skip all dimer if statements here, because they require a Hessian calculation or file look-up
513  }
514  /* Generate and pass initial eigenvector and tell optimizer to not perform the first rotation */
515  else if (useEigenvectorForFirstStep) {
516  optimizer->optimizer.skipFirstRotation = true;
517  auto modes = getNormalModes(*calc);
518 
519  auto castedCalc = std::dynamic_pointer_cast<Scine::Core::EmbeddingCalculator>(calc);
520  bool isQmmm = static_cast<bool>(castedCalc);
521  std::unique_ptr<Utils::AtomCollection> system;
522  if (isQmmm) {
523  auto underlying = castedCalc->getUnderlyingCalculators();
524  if (underlying.size() != 2 || !underlying[0]) {
525  throw std::runtime_error("Embedding calculator has non-initialized underlying calculators");
526  }
527  system = underlying[0]->getStructure();
528  }
529  else {
530  system = calc->getStructure();
531  }
532  if (!system) {
533  throw std::runtime_error("Calculator has non-initialized structure");
534  }
535  if (!relevantAtoms.empty()) { // clash with dimer_follow_mode option is already checked prior
536  followedMode = selectMode(*calc, relevantAtoms, std::get<0>(modeSelectionWeights), std::get<1>(modeSelectionWeights));
537  }
538  auto mode = modes.getMode(followedMode); // range check for followedMode is included here
539  /* Currently Hessian not possible in internal coordinates */
540  if (coordinateSystem == Utils::CoordinateSystem::Internal) {
541  /* Transform vector of normal mode into internal coordinates */
542  Utils::InternalCoordinates internalCoordinates(*system);
543  optimizer->optimizer.guessVector = std::make_shared<Eigen::VectorXd>(internalCoordinates.coordinatesToInternal(mode));
544  }
545  else if (coordinateSystem == Utils::CoordinateSystem::CartesianWithoutRotTrans) {
546  /* Can give proper invH for all but internal */
547  /* Calculate inverse Hessian with one mode flipped for BFGS within Dimer optimizer */
548  std::unique_ptr<Eigen::MatrixXd> hessian;
549  if (calc->results().has<Utils::Property::Hessian>()) {
550  hessian = std::make_unique<Eigen::MatrixXd>(calc->results().get<Utils::Property::Hessian>());
551  }
552  else if (calc->results().has<Utils::Property::PartialHessian>()) {
553  Eigen::MatrixXd partialHessian = calc->results().get<Utils::Property::PartialHessian>().getMatrix();
554  // pad with zeros for link atoms
555  auto castedCalc = std::dynamic_pointer_cast<Scine::Core::EmbeddingCalculator>(calc);
556  if (!castedCalc) {
557  throw std::runtime_error("Calculator only has a PartialHessian result, but is no embedding calculator.");
558  }
559  auto size = 3 * system->size();
560  auto partialSize = partialHessian.rows();
561  hessian = std::make_unique<Eigen::MatrixXd>(size, size);
562  // this works because link atoms are always at the end of the QM structure
563  hessian->block(0, 0, partialSize, partialSize) = partialHessian;
564  // set link entries to zero
565  hessian->block(partialSize, 0, size - partialSize, size).setZero();
566  }
567  else {
568  // should have it from getNormalModes, something went wrong
569  throw std::runtime_error("Calculate Hessian, but do not have it in Results, "
570  "something went wrong in the Dimer optimizer preparation");
571  }
572  Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> es;
573  es.compute(*hessian);
574  int nEigenValues = es.eigenvalues().size();
575  Eigen::MatrixXd eigenValueMatrix = Eigen::MatrixXd::Identity(nEigenValues, nEigenValues);
576  eigenValueMatrix.diagonal() = es.eigenvalues();
577  eigenValueMatrix(followedMode, followedMode) = -es.eigenvalues()[followedMode];
578  Eigen::MatrixXd switchedHessian = es.eigenvectors() * eigenValueMatrix * es.eigenvectors().transpose();
579  /* Transform vector of normal mode into internal coordinates */
580  Utils::InternalCoordinates internalCoordinates(*system, true);
581  optimizer->optimizer.guessVector = std::make_shared<Eigen::VectorXd>(internalCoordinates.coordinatesToInternal(mode));
582  /* Get rid of translation and rotation in Hessian */
583  Eigen::MatrixXd switchedTransformedHessian = internalCoordinates.hessianToInternal(switchedHessian);
584  optimizer->optimizer.invH = switchedTransformedHessian.inverse();
585  }
586  else if (coordinateSystem == Utils::CoordinateSystem::Cartesian) {
587  // do not give invH, because this leads to a lot of translations
588  optimizer->optimizer.guessVector =
589  std::make_shared<Eigen::VectorXd>(Eigen::Map<const Eigen::VectorXd>(mode.data(), mode.cols() * mode.rows()));
590  }
591  else {
592  throw std::logic_error("Unknown coordinate system " +
593  Utils::CoordinateSystemInterpreter::getStringFromCoordinateSystem(coordinateSystem));
594  }
595  }
596  /* Read vector from file to give as guess vector */
597  else if (!guessVectorFileName.empty()) {
598  /* push data into std::vector */
599  std::vector<double> container;
600  double num = 0.0;
601  std::ifstream file(guessVectorFileName, std::ios::in);
602  if (!file.is_open()) {
603  throw std::runtime_error("Problem when opening file " + guessVectorFileName);
604  }
605  while (file >> num) {
606  container.push_back(num);
607  }
608  file.close();
609  Eigen::Map<Eigen::VectorXd> guessVector(container.data(), container.size());
610  auto system = calc->getStructure();
611  /* Provided vector is Cartesian and internal shall be used -> transform to internal */
612  if (coordinateSystem != Utils::CoordinateSystem::Cartesian &&
613  static_cast<unsigned long>(guessVector.size()) == 3 * system->getElements().size()) {
614  std::shared_ptr<Utils::InternalCoordinates> transformation = nullptr;
615  if (coordinateSystem == Utils::CoordinateSystem::Internal) {
616  transformation = std::make_shared<Utils::InternalCoordinates>(*system);
617  }
618  else if (coordinateSystem == Utils::CoordinateSystem::CartesianWithoutRotTrans) {
619  transformation = std::make_shared<Utils::InternalCoordinates>(*system, true);
620  }
621  // vector to PositionCollection
622  Utils::PositionCollection guessPosition =
623  Eigen::Map<Utils::PositionCollection>(guessVector.data(), guessVector.size() / 3, 3);
624  try {
625  optimizer->optimizer.guessVector =
626  std::make_shared<Eigen::VectorXd>(transformation->coordinatesToInternal(guessPosition));
627  }
628  catch (const Utils::InternalCoordinatesException& e) {
629  _logger->warning << "Could not transform given Cartesian coordinates to Internal coordinates.\n"
630  << "Omitting guess and initializing dimer with random vector. " << Core::Log::endl;
631  }
632  }
633  /* Provided vector is not Cartesian and Cartesian shall be used -> transform to Cartesian */
634  else if (coordinateSystem == Utils::CoordinateSystem::Cartesian &&
635  static_cast<unsigned long>(guessVector.size()) != 3 * system->getElements().size()) {
636  try {
637  std::shared_ptr<Utils::InternalCoordinates> transformation = nullptr;
638  if (static_cast<unsigned long>(guessVector.size()) == 3 * system->getElements().size() - 6 ||
639  static_cast<unsigned long>(guessVector.size()) == 3 * system->getElements().size() - 5) {
640  // cartesianWithoutRotTrans because 5 or 6 missing
641  transformation = std::make_shared<Utils::InternalCoordinates>(*system, true);
642  }
643  else {
644  transformation = std::make_shared<Utils::InternalCoordinates>(*system);
645  }
646  /* single guess vector cannot get backtransformed directly to Cartesian coords */
647  /* first get current position and transform to internal */
648  auto cartPos0 = system->getPositions();
649  auto ircVec0 = transformation->coordinatesToInternal(cartPos0);
650  /* add guessvector */
651  Eigen::VectorXd ircVec1 = ircVec0 + guessVector;
652  /* transform this new vector back to Cartesian and diff of 2 Cartesian PositionCollection is guessvector */
653  auto cartPos1 = transformation->coordinatesToCartesian(ircVec1);
654  Utils::PositionCollection posDiff = cartPos1 - cartPos0;
655  optimizer->optimizer.guessVector =
656  std::make_shared<Eigen::VectorXd>(Eigen::Map<Eigen::VectorXd>(posDiff.data(), posDiff.size()));
657  }
658  catch (const Utils::InternalCoordinatesException& e) {
659  _logger->warning << "Could not transform given non-Cartesian coordinates to Cartesian.\n"
660  << "Omitting guess and initializing dimer with random vector. " << Core::Log::endl;
661  }
662  }
663  /* Provided vector should fit with wanted coordinates, optimizer later checks the correct length again */
664  else {
665  optimizer->optimizer.guessVector = std::make_shared<Eigen::VectorXd>(guessVector);
666  }
667  }
668  /* Read 2 structures from files to form guess vector from discrete difference of the two */
669  else if (!discreteGuessesFileNames.at(0).empty() && !discreteGuessesFileNames.at(1).empty()) {
670  Utils::AtomCollection system;
671  /* read first */
672  std::string filepath1 = discreteGuessesFileNames.at(0);
673  std::ifstream file1(filepath1, std::ios::in);
674  if (!file1.is_open()) {
675  throw std::runtime_error("Problem when opening file " + filepath1);
676  }
677  system = XyzHandler::read(file1);
678  auto position1 = system.getPositions();
679  file1.close();
680 
681  /* read second */
682  std::string filepath2 = discreteGuessesFileNames.at(1);
683  std::ifstream file2(filepath2, std::ios::in);
684  if (!file2.is_open()) {
685  throw std::runtime_error("Problem when opening file " + filepath2);
686  }
687  system = XyzHandler::read(file2);
688  auto position2 = system.getPositions();
689  file2.close();
690 
691  Utils::PositionCollection positionDiff = position2 - position1;
692  Eigen::Map<Eigen::VectorXd> mode(positionDiff.data(), positionDiff.size());
693  std::shared_ptr<Utils::InternalCoordinates> transformation = nullptr;
694  if (coordinateSystem == Utils::CoordinateSystem::Internal) {
695  transformation = std::make_shared<Utils::InternalCoordinates>(system);
696  }
697  else if (coordinateSystem == Utils::CoordinateSystem::CartesianWithoutRotTrans) {
698  transformation = std::make_shared<Utils::InternalCoordinates>(system, true);
699  }
700  if (transformation) {
701  optimizer->optimizer.guessVector =
702  std::make_shared<Eigen::VectorXd>(transformation->coordinatesToInternal(positionDiff));
703  }
704  else {
705  optimizer->optimizer.guessVector = std::make_shared<Eigen::VectorXd>(mode);
706  }
707  }
708  }
709 };
710 
711 } // namespace Readuct
712 } // namespace Scine
713 
714 #endif // READUCT_TSOPTIMIZATIONTASK_H_
void warningIfMultipleOutputsGiven() const
Warn if more than one output system was specified.
Definition: Task.h:107
static double mass(ElementType e)
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:85
std::string name() const override
Getter for the tasks name.
Definition: TsOptimizationTask.h:48
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:44
virtual std::vector< std::shared_ptr< Calculator > > getUnderlyingCalculators() const =0
static void write(format f, const std::string &fileName, const MolecularTrajectory &m, const BondOrderCollection &bondOrders)
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:52
void warningIfMultipleInputsGiven() const
Warn if more than one input system was specified.
Definition: Task.h:98
const std::vector< std::string > & output() const
Getter for the names of the output systems generated by this task.
Definition: Task.h:92
The base class for all tasks in Readuct.
Definition: Task.h:29
Definition: TsOptimizationTask.h:36