#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright 2019 Chris Sewell
#
# This file is part of aiida-gulp.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms and conditions
# of version 3 of the GNU Lesser General Public License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
"""
A parser to read output from a standard CRYSTAL17 run
"""
import traceback
import warnings
from ase.io import read as ase_read
from aiida.common import exceptions
from aiida.engine import ExitCode
from aiida.orm import Dict
from aiida.parsers.parser import Parser
from aiida.plugins import DataFactory
from aiida_gulp.parsers.raw.parse_output_std import parse_file
from aiida_gulp.parsers.raw.write_geometry import validate_1d_geometry
[docs]class GulpOptParser(Parser):
"""
Parser class for parsing output of a GULP single point energy calculation
"""
[docs] def parse(self, **kwargs):
"""
Parse outputs, store results in database.
"""
try:
output_folder = self.retrieved
except exceptions.NotExistent:
return self.exit_codes.ERROR_NO_RETRIEVED_FOLDER
mainout_file = self.node.get_option("output_main_file_name")
if mainout_file not in output_folder.list_object_names():
return self.exit_codes.ERROR_OUTPUT_FILE_MISSING
# parse the main output file and add nodes
self.logger.info("parsing main out file")
with output_folder.open(mainout_file) as handle:
try:
result_dict, exit_code = parse_file(
handle, parser_class=self.__class__.__name__
)
except Exception:
traceback.print_exc()
return self.exit_codes.ERROR_PARSING_STDOUT
if result_dict["parser_errors"]:
self.logger.warning(
"the parser raised the following errors:\n{}".format(
"\n\t".join(result_dict["parser_errors"])
)
)
if result_dict["errors"]:
self.logger.warning(
"the calculation raised the following errors:\n{}".format(
"\n\t".join(result_dict["errors"])
)
)
# look a stderr for fortran warnings, etc,
# e.g. IEEE_INVALID_FLAG IEEE_OVERFLOW_FLAG IEEE_UNDERFLOW_FLAG
stderr_file = self.node.get_option("output_stderr_file_name")
if stderr_file in output_folder.list_object_names():
with output_folder.open(stderr_file) as handle:
stderr_content = handle.read()
if stderr_content:
self.logger.warning("the calculation stderr file was not empty:")
self.logger.warning(stderr_content)
result_dict["warnings"].append(stderr_content.strip())
self.out("results", Dict(dict=result_dict))
out_structure, exit_code_structure = self.create_structure(
result_dict, output_folder
)
if out_structure is not None:
self.out("structure", out_structure)
if exit_code is not None:
return self.exit_codes[exit_code]
if exit_code_structure is not None:
return self.exit_codes[exit_code_structure]
return ExitCode()
[docs] def create_structure(self, results_dict, output_folder):
""" create the output structure """
opt_type = results_dict.get("opt_type", None)
if opt_type == "polymer":
if "final_coords" not in results_dict:
return None, "ERROR_STRUCTURE_PARSING"
final_coords = results_dict.pop("final_coords")
if "structure" not in self.node.inputs:
self.logger.error("the input structure node is not set")
return None, "ERROR_MISSING_INPUT_STRUCTURE"
if not set(final_coords.keys()).issuperset(["id", "x", "y", "z", "label"]):
self.logger.error(
'expected final_coords to contain ["id", "x", "y", "z", "label"]'
)
return None, "ERROR_STRUCTURE_PARSING"
if not final_coords["id"] == list(range(1, len(final_coords["id"]) + 1)):
self.logger.error("the final_coords ids were not ordered 1,2,3,...")
return None, "ERROR_STRUCTURE_PARSING"
if (
not final_coords["label"]
== self.node.inputs.structure.get_ase().get_chemical_symbols()
):
self.logger.error(
"the final_coords labels are != to the input structure symbols"
)
return None, "ERROR_STRUCTURE_PARSING"
try:
validate_1d_geometry(self.node.inputs.structure)
except Exception as err:
self.logger.error(str(err))
return None, "ERROR_STRUCTURE_PARSING"
out_structure = self.node.inputs.structure.clone()
positions = []
for x, y, z in zip(final_coords["x"], final_coords["y"], final_coords["z"]):
# x are fractional and y,z are cartesian
positions.append([x * out_structure.cell[0][0], y, z])
out_structure.reset_sites_positions(positions)
elif opt_type == "surface":
self.logger.error(
"creating output structures for surface optimisations has not yet been implemented"
)
return None, "ERROR_STRUCTURE_PARSING"
else:
cif_file = self.node.get_option("out_cif_file_name")
if cif_file not in output_folder.list_object_names():
self.logger.error("the output cif file is missing")
return None, "ERROR_CIF_FILE_MISSING"
# We do not use this method, since currently different kinds are set for each atom
# see aiidateam/aiida_core#2942
# NOTE cif files are read as binary, by default, since aiida-core v1.0.0b3
# with output_folder.open(cif_file, mode="rb") as handle:
# cif = DataFactory('cif')(file=handle)
# structure = cif.get_structure(converter="ase")
with warnings.catch_warnings():
# ase.io.read returns a warnings that can be ignored
# UserWarning: crystal system 'triclinic' is not interpreted for space group 1.
# This may result in wrong setting!
warnings.simplefilter("ignore", UserWarning)
with output_folder.open(cif_file, mode="r") as handle:
atoms = ase_read(handle, index=":", format="cif")[-1]
atoms.set_tags(0)
if self.node.get_option("use_input_kinds"):
if "structure" not in self.node.inputs:
self.logger.error("the input structure node is not set")
return None, "ERROR_MISSING_INPUT_STRUCTURE"
in_structure = self.node.inputs.structure
in_atoms = in_structure.get_ase()
if in_atoms.get_chemical_symbols() != atoms.get_chemical_symbols():
self.logger.error(
"the input and cif structures have different atomic configurations"
)
return None, "ERROR_CIF_INCONSISTENT"
out_structure = in_structure.clone()
out_structure.set_cell(atoms.cell)
out_structure.reset_sites_positions(atoms.positions)
else:
out_structure = DataFactory("structure")(ase=atoms)
return out_structure, None