# -*- 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']