Skip to content

Commit

Permalink
Add default test for ProjwfcCalculation and move aiida.out to ``r…
Browse files Browse the repository at this point in the history
…etrieve_temporary_list``
  • Loading branch information
chrisjsewell committed Oct 20, 2019
1 parent d6fb727 commit e395edd
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 15 deletions.
11 changes: 8 additions & 3 deletions aiida_quantumespresso/calculations/namelists.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ class NamelistsCalculation(CalcJob):
_default_namelists = ['INPUTPP']
_blocked_keywords = [] # a list of tuples with key and value fixed

_retrieve_temporary_list = []
_retrieve_temporary_list = ()
_retrieve_output_as_temp = False

_DEFAULT_INPUT_FILE = 'aiida.in'
_DEFAULT_OUTPUT_FILE = 'aiida.out'
Expand Down Expand Up @@ -173,12 +174,16 @@ def prepare_for_submission(self, folder):

# Retrieve by default the output file and the xml file
calcinfo.retrieve_list = []
calcinfo.retrieve_list.append(self.inputs.metadata.options.output_filename)
if not self._retrieve_output_as_temp:
calcinfo.retrieve_list.append(self.inputs.metadata.options.output_filename)
settings_retrieve_list = settings.pop('ADDITIONAL_RETRIEVE_LIST', [])
calcinfo.retrieve_list += settings_retrieve_list
calcinfo.retrieve_list += self._internal_retrieve_list

calcinfo.retrieve_temporary_list = self._retrieve_temporary_list
calcinfo.retrieve_temporary_list = []
if self._retrieve_output_as_temp:
calcinfo.retrieve_temporary_list.append(self.inputs.metadata.options.output_filename)
calcinfo.retrieve_temporary_list += list(self._retrieve_temporary_list)

# We might still have parser options in the settings dictionary: pop them.
_pop_parser_options(self, settings)
Expand Down
4 changes: 4 additions & 0 deletions aiida_quantumespresso/calculations/projwfc.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class ProjwfcCalculation(NamelistsCalculation):
]
_default_parser = 'quantumespresso.projwfc'
_internal_retrieve_list = [NamelistsCalculation._PREFIX + '.pdos*']
_retrieve_output_as_temp = True

@classmethod
def define(cls, spec):
Expand All @@ -46,6 +47,9 @@ def define(cls, spec):
spec.exit_code(
100, 'ERROR_NO_RETRIEVED_FOLDER', message='The retrieved folder data node could not be accessed.'
)
spec.exit_code(
101, 'ERROR_NO_RETRIEVED_TEMPORARY_FOLDER', message='The retrieved temporary folder could not be accessed.'
)
spec.exit_code(
110, 'ERROR_READING_OUTPUT_FILE', message='The output file could not be read from the retrieved folder.'
)
Expand Down
22 changes: 18 additions & 4 deletions aiida_quantumespresso/parsers/projwfc.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division
import re
import os
import fnmatch
import re
import traceback

import numpy as np
Expand Down Expand Up @@ -256,14 +257,21 @@ def parse(self, **kwargs):
except NotExistent:
return self.exit_codes.ERROR_NO_RETRIEVED_FOLDER

# Check that the retrieved temporary folder is there
try:
temp_folder = kwargs['retrieved_temporary_folder']
except KeyError:
return self.exit_codes.ERROR_NO_RETRIEVED_TEMPORARY_FOLDER

# Read standard out
try:
filename_stdout = self.node.get_option('output_filename') # or get_attribute(), but this is clearer
with out_folder.open(filename_stdout, 'r') as fil:
filename_stdout = self.node.get_option('output_filename')
with open(os.path.join(temp_folder, filename_stdout), 'r') as fil:
out_file_lines = fil.readlines()
except OSError:
return self.exit_codes.ERROR_READING_OUTPUT_FILE

# check that the computation completed successfully
job_done = False
for line in reversed(out_file_lines):
if 'JOB DONE' in line:
Expand Down Expand Up @@ -349,8 +357,14 @@ def retrieve_parent_node(self, linkname, outgoing=True):
return node

def _parse_lodwin_charges(self, out_info_dict):
"""Parse the Lodwin charge data from the output file."""
data, spill_parameter = parse_lowdin_charges(out_info_dict['out_file_lines'], out_info_dict['lowdin_lines'])
structure = self.retrieve_parent_node('structure', outgoing=False)
# we store the uuid of the structure that these charges relate to, which will be
# an input if the PwCalculation was an scf/nscf or output if relax/vc-relax
try:
structure = self.retrieve_parent_node('output_structure', outgoing=True)
except QEOutputParsingError:
structure = self.retrieve_parent_node('structure', outgoing=False)
try:
site_data = [data[i + 1] for i in range(len(structure.sites))]
except KeyError:
Expand Down
53 changes: 53 additions & 0 deletions tests/calculations/test_projwfc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
# pylint: disable=unused-argument
"""Tests for the `ProjwfcCalculation` class."""
from __future__ import absolute_import

from aiida import orm
from aiida.common import datastructures

from aiida_quantumespresso.utils.resources import get_default_options


def test_projwfc_default(
fixture_database, fixture_computer_localhost, fixture_sandbox_folder, generate_calc_job, generate_code_localhost,
file_regression
):
"""Test a default `ProjwfcCalculation`."""
entry_point_name = 'quantumespresso.projwfc'

parameters = {'PROJWFC': {'emin': -1, 'emax': 1, 'DeltaE': 0.01, 'ngauss': 0, 'degauss': 0.01}}
parent_folder = orm.RemoteData(computer=fixture_computer_localhost, remote_path='path/on/remote')
parent_folder.store()

inputs = {
'code': generate_code_localhost(entry_point_name, fixture_computer_localhost),
'parameters': orm.Dict(dict=parameters),
'parent_folder': parent_folder,
'metadata': {
'options': get_default_options()
}
}

calc_info = generate_calc_job(fixture_sandbox_folder, entry_point_name, inputs)
code_info = calc_info.codes_info[0]

# Check the attributes of the returned `CodeInfo`
assert isinstance(code_info, datastructures.CodeInfo)
assert code_info.stdin_name == 'aiida.in'
assert code_info.stdout_name == 'aiida.out'
assert code_info.cmdline_params == []
# Check the attributes of the returned `CalcInfo`
assert isinstance(calc_info, datastructures.CalcInfo)
assert sorted(calc_info.local_copy_list) == sorted([])
assert sorted(calc_info.remote_copy_list
) == sorted([(parent_folder.computer.uuid, 'path/on/remote/./out/', './out/')])
assert sorted(calc_info.retrieve_list) == sorted(['aiida.pdos*'])
assert sorted(calc_info.retrieve_temporary_list) == sorted(['aiida.out'])

with fixture_sandbox_folder.open('aiida.in') as handle:
input_written = handle.read()

# Checks on the files written to the sandbox folder as raw input
assert sorted(fixture_sandbox_folder.get_content_list()) == sorted(['aiida.in'])
file_regression.check(input_written, encoding='utf-8', extension='.in')
15 changes: 15 additions & 0 deletions tests/calculations/test_projwfc/test_projwfc_default.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
&PROJWFC
degauss = 1.0000000000d-02
deltae = 1.0000000000d-02
emax = 1
emin = -1
kresolveddos = .false.
lbinary_data = .false.
lsym = .true.
lwrite_overlaps = .false.
ngauss = 0
outdir = './out/'
plotboxes = .false.
prefix = 'aiida'
tdosinboxes = .false.
/
59 changes: 55 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ def fixture_computer_localhost(fixture_work_directory):
hostname='localhost',
transport_type='local',
scheduler_type='direct',
workdir=fixture_work_directory).store()
workdir=fixture_work_directory
).store()
computer.set_default_mpiprocs_per_machine(1)
yield computer

Expand Down Expand Up @@ -126,7 +127,7 @@ def _generate_calc_job_node(entry_point_name, computer, test_name=None, inputs=N
node.set_option('max_wallclock_seconds', 1800)

if attributes:
node.set_attributes(attributes)
node.set_attribute_many(attributes)

if inputs:
for link_label, input_node in flatten_inputs(inputs):
Expand All @@ -137,7 +138,9 @@ def _generate_calc_job_node(entry_point_name, computer, test_name=None, inputs=N

if test_name is not None:
basepath = os.path.dirname(os.path.abspath(__file__))
filepath = os.path.join(basepath, 'parsers', 'fixtures', entry_point_name[len('quantumespresso.'):], test_name)
filepath = os.path.join(
basepath, 'parsers', 'fixtures', entry_point_name[len('quantumespresso.'):], test_name
)

retrieved = orm.FolderData()
retrieved.put_object_from_tree(filepath)
Expand Down Expand Up @@ -236,9 +239,57 @@ def _generate_parser(entry_point_name):
return _generate_parser


@pytest.fixture
def parse_from_node():
"""Fixture to parse calcjob outputs directly from a `CalcJobNode`."""

# TODO this can be removed once aiidateam/aiida-core#3061 is implemented # pylint: disable=fixme
def _parse_from_node(cls, node, store_provenance=True, retrieved_temp=None):
"""Parse the outputs directly from the `CalcJobNode`.
:param cls: a `Parser` instance
:param node: a `CalcJobNode` instance
:param store_provenance: bool, if True will store the parsing as a `CalcFunctionNode` in the provenance
:param retrieved_temp: None or str, abspath to the temporary folder.
:return: a tuple of the parsed results and the `CalcFunctionNode` representing the process of parsing
"""
from aiida.engine import calcfunction, Process
from aiida.orm import Str

parser = cls(node=node)

@calcfunction
def parse_calcfunction(**kwargs):
"""A wrapper function that will turn calling the `Parser.parse` method into a `CalcFunctionNode`.
:param kwargs: keyword arguments that are passed to `Parser.parse` after it has been constructed
"""
if 'retrieved_temporary_folder' in kwargs:
string = kwargs.pop('retrieved_temporary_folder').value
kwargs['retrieved_temporary_folder'] = string

exit_code = parser.parse(**kwargs)
outputs = parser.outputs

if exit_code and exit_code.status:
process = Process.current()
process.out_many(outputs)
return exit_code

return dict(outputs)

inputs = {'metadata': {'store_provenance': store_provenance}}
inputs.update(parser.get_outputs_for_parsing())
if retrieved_temp is not None:
inputs['retrieved_temporary_folder'] = Str(retrieved_temp)

return parse_calcfunction.run_get_node(**inputs)

return _parse_from_node


@pytest.fixture
def parser_fixture_path():
"""Fixture to obtain the absolute path of the ``test/parsers/fixtures`` folder."""
basepath = os.path.dirname(os.path.abspath(__file__))
return os.path.join(basepath, 'parsers', 'fixtures')

27 changes: 23 additions & 4 deletions tests/parsers/test_projwfc.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from aiida import orm
from aiida.common import AttributeDict, LinkType
from aiida.plugins import CalculationFactory

from aiida_quantumespresso.parsers.parse_raw.projwfc import parse_lowdin_charges

Expand Down Expand Up @@ -37,24 +38,42 @@ def projwfc_inputs(generate_calc_job_node, fixture_computer_localhost, generate_
params = orm.Dict(dict={'number_of_spin_components': 1})
params.add_incoming(parent_calcjob, link_type=LinkType.CREATE, link_label='output_parameters')
params.store()
parameters = {'PROJWFC': {'emin': -1, 'emax': 1, 'DeltaE': 0.01, 'ngauss': 0, 'degauss': 0.01}}
inputs = {
'parameters': orm.Dict(dict=parameters),
'parent_folder': parent_calcjob.outputs.remote_folder,
}

return AttributeDict(inputs)


def test_pw_link_spec():
"""Test the ``PwCalculation`` input/output link names are as required for ``ProjwfcParser``.
``ProjwfcParser`` relies on extracting data from the parent ``PwCalculation``.
This test safeguards against changes in the link names not being propagated to this parser.
"""
pw_calc = CalculationFactory('quantumespresso.pw')
pw_spec = pw_calc.spec()
assert 'structure' in pw_spec.inputs, list(pw_spec.inputs.keys())
assert 'kpoints' in pw_spec.inputs, list(pw_spec.inputs.keys())
assert 'output_parameters' in pw_spec.outputs, list(pw_spec.outputs.keys())
assert 'output_structure' in pw_spec.outputs, list(pw_spec.outputs.keys())


def test_projwfc_default(
fixture_database, fixture_computer_localhost, generate_calc_job_node, generate_parser, projwfc_inputs,
data_regression
fixture_database, fixture_computer_localhost, generate_calc_job_node, generate_parser, parse_from_node,
parser_fixture_path, projwfc_inputs, data_regression
):
"""Test ``ProjwfcParser`` on the results of a simple ``projwfc.x`` calculation."""
entry_point_calc_job = 'quantumespresso.projwfc'
entry_point_parser = 'quantumespresso.projwfc'

node = generate_calc_job_node(entry_point_calc_job, fixture_computer_localhost, 'default', projwfc_inputs)
node = generate_calc_job_node(entry_point_calc_job, fixture_computer_localhost, 'default', inputs=projwfc_inputs)
parser = generate_parser(entry_point_parser)
results, calcfunction = parser.parse_from_node(node, store_provenance=False)
results, calcfunction = parse_from_node(
parser, node, store_provenance=False, retrieved_temp=os.path.join(parser_fixture_path, 'projwfc', 'default')
)

assert calcfunction.is_finished, calcfunction.exception
assert calcfunction.is_finished_ok, calcfunction.exit_message
Expand Down

0 comments on commit e395edd

Please sign in to comment.