Skip to content

Commit

Permalink
Merge pull request #4 from nanotech-empa/release/1.1.0
Browse files Browse the repository at this point in the history
Release/1.1.0
  • Loading branch information
eimrek committed Feb 23, 2021
2 parents cbff3e7 + 3a5554d commit 48e2eec
Show file tree
Hide file tree
Showing 8 changed files with 216 additions and 67 deletions.
2 changes: 1 addition & 1 deletion aiida_gaussian/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@

from __future__ import absolute_import

__version__ = "1.0.0"
__version__ = "1.1.0"
18 changes: 14 additions & 4 deletions aiida_gaussian/calculations/cubegen.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class CubegenCalculation(CalcJob):
IFlag X0 Y0 Z0 # Output unit number and initial point.
N1 X1 Y1 Z1 # Number of points and step-size in the X-direction.
N2 X2 Y2 Z2 # Number of points and step-size in the Y-direction.
N3 X3 Y3 Z3 # Number of points and step-size in the Z-direction.
N3 X3 Y3 Z3 # Number of points and step-size in the Z-direction.
See more details at https://gaussian.com/cubegen/
"""
Expand Down Expand Up @@ -66,7 +66,7 @@ def define(cls, spec):
valid_type=Bool,
required=False,
default=lambda: Bool(False),
help='should the cube be retrieved?')
help='should the cubes be retrieved?')

spec.input(
"gauss_memdef",
Expand All @@ -86,13 +86,20 @@ def define(cls, spec):
non_db=True,
)

spec.inputs.dynamic = True
spec.outputs.dynamic = True

# Exit codes
spec.exit_code(
200,
300,
"ERROR_NO_RETRIEVED_FOLDER",
message="The retrieved folder data node could not be accessed.",
message="The retrieved folder could not be accessed.",
)

spec.exit_code(
301,
"ERROR_NO_RETRIEVED_TEMPORARY_FOLDER",
message="The retrieved temporary folder could not be accessed.",
)

# --------------------------------------------------------------------------
Expand All @@ -103,6 +110,7 @@ def prepare_for_submission(self, folder):
calcinfo.uuid = self.uuid
calcinfo.codes_info = []
calcinfo.retrieve_list = []
calcinfo.retrieve_temporary_list = []
calcinfo.prepend_text = "export GAUSS_MEMDEF=%dMB\n" % self.inputs.gauss_memdef

calcinfo.local_copy_list = []
Expand Down Expand Up @@ -149,6 +157,8 @@ def prepare_for_submission(self, folder):

if self.inputs.retrieve_cubes.value:
calcinfo.retrieve_list.append(cube_name)
else:
calcinfo.retrieve_temporary_list.append(cube_name)

# symlink or copy to parent calculation
calcinfo.remote_symlink_list = []
Expand Down
15 changes: 11 additions & 4 deletions aiida_gaussian/calculations/gaussian.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"""Gaussian input plugin."""
from __future__ import absolute_import

from aiida.orm import Dict, List, RemoteData
from aiida.orm import Dict, List, RemoteData, Float
from aiida.common import CalcInfo, CodeInfo

# from aiida.cmdline.utils import echo
Expand Down Expand Up @@ -97,6 +97,12 @@ def define(cls, spec):
required=False,
help="Final optimized structure, if available",
)
spec.output(
"energy_ev",
valid_type=Float,
required=False,
help="Final energy in electronvolts",
)

spec.default_output_node = "output_parameters"
spec.outputs.dynamic = True
Expand Down Expand Up @@ -212,14 +218,15 @@ def _render_input_string_from_params(cls, param_dict, pmg_structure):
pmg_structure,
title="input generated by the aiida-gaussian plugin",
charge=param_dict.get("charge"),
spin_multiplicity=param_dict.get("multiplicity"),
spin_multiplicity=param_dict.get(
"multiplicity", param_dict.get("spin_multiplicity")),
functional=param_dict.get("functional"),
basis_set=param_dict.get("basis_set"),
route_parameters=param_dict.get("route_parameters"),
input_parameters=param_dict.get("input_parameters"),
link0_parameters=param_dict.get("link0_parameters"),
dieze_tag=param_dict.get(
"dieze_tag", "#N"), # by default, use the normal print level
dieze_tag=param_dict.get("dieze_tag",
"#N"), # normal print level by default
)

return inp.to_string(cart_coords=True)
70 changes: 46 additions & 24 deletions aiida_gaussian/parsers/cubegen.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,49 +3,71 @@
from __future__ import absolute_import

import os
import tempfile
import numpy as np

from aiida.parsers import Parser
from aiida.common import OutputParsingError, NotExistent
from aiida.common import NotExistent
from aiida.engine import ExitCode
from aiida.orm import Dict, StructureData, ArrayData

import matplotlib.pyplot as plt
from aiida.orm import ArrayData

from aiida_gaussian.utils.cube import Cube


class CubegenBaseParser(Parser):
"""Cubegen parser that created 2d slices of the generated cube files"""
"""Cubegen parser that creates 2d slices of the generated cube files"""
def parse(self, **kwargs):
"""Receives in input a dictionary of retrieved nodes. Does all the logic here."""

retrieved_folder_paths = []

try:
out_folder = self.retrieved
retrieved_folder = self.retrieved
retrieved_folder_paths.append(
retrieved_folder._repository._get_base_folder().abspath)
except NotExistent:
return self.exit_codes.ERROR_NO_RETRIEVED_FOLDER

out_folder_path = out_folder._repository._get_base_folder().abspath
retrieve_temp_list_input = self.node.get_attribute(
'retrieve_temporary_list', None)
# If temporary files were specified, check that we have them
if retrieve_temp_list_input:
try:
retrieved_temp_folder_path = kwargs[
'retrieved_temporary_folder']
retrieved_folder_paths.append(retrieved_temp_folder_path)
except KeyError:
return self.exit(
self.exit_codes.ERROR_NO_RETRIEVED_TEMPORARY_FOLDER)

for retr_file in out_folder._repository.list_object_names():
if retr_file.endswith(".cube"):
cube_file_path = os.path.join(out_folder_path, retr_file)
if "parser_params" in self.node.inputs:
parser_params = dict(self.node.inputs.parser_params)
else:
parser_params = {}

cube = Cube()
cube.read_cube_file(cube_file_path)
self._parse_folders(retrieved_folder_paths, parser_params)

out_array = ArrayData()
return ExitCode(0)

for h in np.arange(0.0, 10.0, 1.0):
try:
cube_plane = cube.get_plane_above_topmost_atom(h)
out_array.set_array('z_h%d' % int(h), cube_plane)
except IndexError:
break
def _parse_folders(self, retrieved_folder_paths, parser_params):

out_node_label = "cube_" + retr_file.split('.')[0].replace(
'-', '').replace('+', '')
self.out(out_node_label, out_array)
for folder_path in retrieved_folder_paths:
for filename in os.listdir(folder_path):
filepath = os.path.join(folder_path, filename)

return ExitCode(0)
if filename.endswith(".cube"):

cube = Cube()
cube.read_cube_file(filepath)

out_array = ArrayData()

for h in np.arange(0.0, 10.0, 1.0):
try:
cube_plane = cube.get_plane_above_topmost_atom(h)
out_array.set_array('z_h%d' % int(h), cube_plane)
except IndexError:
break

out_node_label = "cube_" + filename.split('.')[0].replace(
'-', '').replace('+', '')
self.out(out_node_label, out_array)
70 changes: 53 additions & 17 deletions aiida_gaussian/parsers/gaussian.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from aiida.parsers import Parser
from aiida.common import OutputParsingError, NotExistent
from aiida.engine import ExitCode
from aiida.orm import Dict, StructureData
from aiida.orm import Dict, StructureData, Float

import pymatgen.io.gaussian as mgaus

Expand All @@ -36,36 +36,33 @@ def parse(self, **kwargs):
log_file_path = os.path.join(
out_folder._repository._get_base_folder().abspath, fname)

exit_code = self._parse_log(log_file_path)
exit_code = self._parse_log(log_file_path, self.node.inputs)

if exit_code is not None:
return exit_code

return ExitCode(0)

def _parse_log(self, log_file_path):
"""CCLIB parsing"""
def _parse_log(self, log_file_path, inputs):

data = cclib.io.ccread(log_file_path)
# parse with cclib
property_dict = self._parse_log_cclib(log_file_path)

if data is None:
return self.exit_codes.ERROR_OUTPUT_PARSING

property_dict = data.getattributes()

# replace the first delta-energy of nan with zero
# as nan is not allowed in AiiDA nodes
if 'scfvalues' in property_dict:
property_dict['scfvalues'] = [
np.nan_to_num(svs) for svs in property_dict['scfvalues']
]
# Extra stuff that cclib doesn't parse
property_dict.update(self._parse_log_spin_exp(log_file_path))

# set output nodes
self.out("output_parameters", Dict(dict=property_dict))

if 'scfenergies' in property_dict:
self.out("energy_ev", Float(property_dict['scfenergies'][-1]))

# in case of geometry optimization,
# return the last geometry as a separated node
if "atomcoords" in property_dict:
if len(property_dict["atomcoords"]) > 1:

if ('opt' in inputs.parameters['route_parameters']
or len(property_dict["atomcoords"]) > 1):

opt_coords = property_dict["atomcoords"][-1]

Expand Down Expand Up @@ -96,3 +93,42 @@ def _parse_log(self, log_file_path):
return self.exit_codes.ERROR_NO_NORMAL_TERMINATION

return None

def _parse_log_cclib(self, log_file_path):

data = cclib.io.ccread(log_file_path)

if data is None:
return self.exit_codes.ERROR_OUTPUT_PARSING

property_dict = data.getattributes()

# replace the first delta-energy of nan with zero
# as nan is not allowed in AiiDA nodes
if 'scfvalues' in property_dict:
property_dict['scfvalues'] = [
np.nan_to_num(svs) for svs in property_dict['scfvalues']
]

return property_dict

def _parse_log_spin_exp(self, log_file_path):
""" Parse spin expectation values """

num_pattern = "[-+]?(?:[0-9]*[.])?[0-9]+(?:[eE][-+]?\d+)?"

spin_pattern = "\n <Sx>= ({0}) <Sy>= ({0}) <Sz>= ({0}) <S\*\*2>= ({0}) S= ({0})".format(
num_pattern)
spin_list = []

with open(log_file_path, 'r') as f:
for spin_line in re.findall(spin_pattern, f.read()):
spin_list.append({
'Sx': float(spin_line[0]),
'Sy': float(spin_line[1]),
'Sz': float(spin_line[2]),
'S**2': float(spin_line[3]),
'S': float(spin_line[4]),
})

return {'spin_expectation_values': spin_list}
56 changes: 54 additions & 2 deletions aiida_gaussian/workchains/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
import os
import sys

from typing import Optional

from aiida.common import AttributeDict

from aiida.engine import WorkChain, ToContext
from aiida.engine import WorkChain, ToContext, process_handler, ProcessHandlerReport
from aiida.engine import BaseRestartWorkChain, while_
from aiida.orm import Int, Float, Str, Bool, Code, Dict, List
from aiida.orm import SinglefileData, StructureData, RemoteData
Expand Down Expand Up @@ -39,13 +41,63 @@ def define(cls, spec):

spec.expose_outputs(GaussianCalculation)

# TODO: Is there any way to expose dynamic outputs?
#spec.outputs.dynamic = True

spec.exit_code(
350,
'ERROR_UNRECOVERABLE_SCF_FAILURE',
message=
'The calculation failed with an unrecoverable SCF convergence error.'
)

def setup(self):
"""Call the `setup` of the `BaseRestartWorkChain` and then create the inputs dictionary in `self.ctx.inputs`.
This `self.ctx.inputs` dictionary will be used by the `BaseRestartWorkChain` to submit the calculations in the
internal loop.
"""

super(GaussianBaseWorkChain, self).setup()
self.ctx.inputs = AttributeDict(
self.exposed_inputs(GaussianCalculation, 'gaussian'))

@process_handler(
priority=400,
exit_codes=[GaussianCalculation.exit_codes.ERROR_SCF_FAILURE])
def handle_scf_failure(self, calculation):
"""
Try to restart with
1) scf=(qc)
and if it doesn't work then
2) scf=(yqc)
"""

params = dict(self.ctx.inputs.parameters)
route_params = params['route_parameters']

if 'scf' not in route_params:
route_params['scf'] = {}

if 'yqc' in route_params['scf']:
# QC and YQC failed:
self.report("SCF failed with QC and YQC, giving up...")
return ProcessHandlerReport(
False, self.exit_codes.ERROR_UNRECOVERABLE_SCF_FAILURE)

new_scf = {}
# keep the user-set convergence criterion; replace rest
if 'conver' in route_params['scf']:
new_scf['conver'] = route_params['scf']['conver']

if 'qc' in route_params['scf']:
self.report("SCF=(QC) failed, retrying with SCF=(YQC)")
new_scf['yqc'] = None
else:
self.report("SCF failed, retrying with SCF=(QC)")
new_scf['qc'] = None

# Update the params Dict
route_params['scf'] = new_scf
self.ctx.inputs.parameters = Dict(dict=params)

return ProcessHandlerReport(False)
Loading

0 comments on commit 48e2eec

Please sign in to comment.