Source code for puffin.jobs.scine_geometry_optimization

# -*- coding: utf-8 -*-
__copyright__ = """This file is part of SCINE Puffin.
This code is licensed under the 3-clause BSD license.
Copyright ETH Zurich, Laboratory for Physical Chemistry, Reiher Group.
See LICENSE.txt for details
"""

import os
from contextlib import redirect_stdout
from puffin.config import Configuration
from .job import ScineJob, calculation_context


[docs]class ScineGeometryOptimization(ScineJob): """ A job optimizing the geometry of a given structure, in search of a local minimum on the potential energy surface. Optimizing a given structure's geometry, generating a new minimum energy structure, if successful. **Order Name** ``scine_geometry_optimization`` **Optional Settings** Optional settings are read from the ``settings`` field, which is part of any ``Calculation`` stored in a SCINE Database. Possible settings for this job are: All settings recognized by ReaDuct's geometry optimization task. Common examples are: optimizer :: str The name of the optimizer to be used, e.g. 'bfgs', 'lbfgs', 'nr' or 'sd'. convergence_max_iterations :: int The maximum number of geometry optimization cycles. convergence_delta_value :: float The convergence criterion for the electronic energy difference between two steps. convergence_gradient_max_coefficient :: float The convergence criterion for the maximum absolute gradient. contribution. convergence_step_rms :: float The convergence criterion for root mean square of the geometric gradient. convergence_step_max_coefficient :: float The convergence criterion for the maximum absolute coefficient in the last step taken in the geometry optimization. convergence_gradient_rms :: float The convergence criterion for root mean square of the last step taken in the geometry optimization. For a complete list see the `ReaDuct manual <https://scine.ethz.ch/static/download/readuct_manual.pdf>`_ All settings that are recognized by the SCF program chosen. Common examples are: max_scf_iterations :: int The number of allowed SCF cycles until convergence. **Required Packages** - SCINE: Database (present by default) - SCINE: Readuct (present by default) - SCINE: Utils (present by default) - A program implementing the SCINE Calculator interface, e.g. Sparrow **Generated Data** If successful the following data will be generated and added to the database: Structures A new minimum energy structure. Properties The ``electronic_energy`` associated with the new structure. """ def __init__(self): super().__init__() def run(self, manager, calculation, config: Configuration) -> bool: import scine_database as db import scine_readuct as readuct import scine_utilities as utils # Gather all required collections structures = manager.get_collection('structures') calculations = manager.get_collection('calculations') properties = manager.get_collection('properties') # Link calculation if needed if not calculation.has_link(): calculation.link(calculations) # Get structure structure = db.Structure(calculation.get_structures()[0]) structure.link(structures) # Get model model = calculation.get_model() # Structure based arguments arguments = {} arguments['molecular_charge'] = structure.get_charge() arguments['spin_multiplicity'] = structure.get_multiplicity() # Model based arguments if model.method and (model.method != model.method_family): arguments['method'] = model.method if model.basis_set: arguments['basis_set'] = model.basis_set #arguments['spin_mode'] = model.spin_mode if not model.program == 'any': arguments['program'] = model.program # Task based arguments task_args = calculation.get_settings() with calculation_context(self._work_dir, 'output', 'errors'): # Get the settings that are available for the chosen calculator prog = model.program if (not model.program == "any") else "" available_calculator_settings = utils.core.get_available_settings(model.method_family, prog) # Transfer the settings that belong to the calculator to the other dictionary keys_to_remove = [] for key, value in task_args.items(): if key in available_calculator_settings: arguments[key] = value keys_to_remove.append(key) for key in keys_to_remove: del task_args[key] utils.IO.write('system.xyz', structure.get_atoms()) system = utils.core.load_system_into_calculator('system.xyz', model.method_family, **arguments) systems = {'system': system} systems, success = readuct.run_opt_task(systems, ['system'], **task_args) # After the calculation, verify that the connection to the database still exists self.verify_connection(manager) # Capture raw output stdout_path = os.path.join(self._work_dir, 'output') stderr_path = os.path.join(self._work_dir, 'errors') with open(stdout_path, 'r') as stdout, open(stderr_path, 'r') as stderr: calculation.set_raw_output(stdout.read()+'\n'+stderr.read()) if not os.path.exists(os.path.join(self._work_dir, 'success')): calculation.set_status(db.Status.FAILED) return False if not success: calculation.set_status(db.Status.FAILED) return False # Basic check for a complete calculation if not systems['system'].has_results(): calculation.set_status(db.Status.FAILED) return False # Update model results = systems['system'].get_results() if results.program_name: model.program = results.program_name.lower() model.version = config.programs()[model.program]['version'] calculation.set_model(model) # Check successfull termination if results.successful_calculation is None: return False if not results.successful_calculation: calculation.set_status(db.Status.FAILED) return False # Generate database results db_results = calculation.get_results() db_results.clear() # New structure new_structure = db.Structure() new_structure.link(structures) new_atoms = structure.get_atoms() new_atoms.positions = systems['system'].positions new_structure.create(new_atoms, arguments['molecular_charge'], arguments['spin_multiplicity'], model, db.Label.MINIMUM_OPTIMIZED) db_results.add_structure(new_structure.id()) calculation.set_results(db_results) # Store energy if results.energy is None: calculation.set_status(db.Status.FAILED) return False self.store_property(properties, 'electronic_energy', 'NumberProperty', results.energy, model, calculation, new_structure) calculation.set_executor(config['daemon']['uuid']) calculation.set_status(db.Status.COMPLETE) return True def required_programs(self): return ['database', 'utils', 'readuct']