From e395edd9a460e99c1064db350147bfc350446125 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Sun, 20 Oct 2019 13:56:39 +0100 Subject: [PATCH] Add default test for ``ProjwfcCalculation`` and move aiida.out to ``retrieve_temporary_list`` --- .../calculations/namelists.py | 11 +++- aiida_quantumespresso/calculations/projwfc.py | 4 ++ aiida_quantumespresso/parsers/projwfc.py | 22 +++++-- tests/calculations/test_projwfc.py | 53 +++++++++++++++++ .../test_projwfc/test_projwfc_default.in | 15 +++++ tests/conftest.py | 59 +++++++++++++++++-- tests/parsers/test_projwfc.py | 27 +++++++-- 7 files changed, 176 insertions(+), 15 deletions(-) create mode 100644 tests/calculations/test_projwfc.py create mode 100644 tests/calculations/test_projwfc/test_projwfc_default.in diff --git a/aiida_quantumespresso/calculations/namelists.py b/aiida_quantumespresso/calculations/namelists.py index 87db89a93..be927e263 100644 --- a/aiida_quantumespresso/calculations/namelists.py +++ b/aiida_quantumespresso/calculations/namelists.py @@ -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' @@ -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) diff --git a/aiida_quantumespresso/calculations/projwfc.py b/aiida_quantumespresso/calculations/projwfc.py index 34748a8ea..3b3240b9f 100644 --- a/aiida_quantumespresso/calculations/projwfc.py +++ b/aiida_quantumespresso/calculations/projwfc.py @@ -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): @@ -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.' ) diff --git a/aiida_quantumespresso/parsers/projwfc.py b/aiida_quantumespresso/parsers/projwfc.py index 05e8b5c4e..64744c804 100644 --- a/aiida_quantumespresso/parsers/projwfc.py +++ b/aiida_quantumespresso/parsers/projwfc.py @@ -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 @@ -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: @@ -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: diff --git a/tests/calculations/test_projwfc.py b/tests/calculations/test_projwfc.py new file mode 100644 index 000000000..73a74e2f6 --- /dev/null +++ b/tests/calculations/test_projwfc.py @@ -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') diff --git a/tests/calculations/test_projwfc/test_projwfc_default.in b/tests/calculations/test_projwfc/test_projwfc_default.in new file mode 100644 index 000000000..592c7bfd8 --- /dev/null +++ b/tests/calculations/test_projwfc/test_projwfc_default.in @@ -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. +/ diff --git a/tests/conftest.py b/tests/conftest.py index b6bc52fcb..1a1ece237 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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 @@ -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): @@ -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) @@ -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') - diff --git a/tests/parsers/test_projwfc.py b/tests/parsers/test_projwfc.py index 43ce25ee0..9f6afa18a 100644 --- a/tests/parsers/test_projwfc.py +++ b/tests/parsers/test_projwfc.py @@ -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 @@ -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