From 03a4d91b805bb4005610da6968d04aa881ece3a9 Mon Sep 17 00:00:00 2001 From: Dominik Gresch Date: Thu, 4 Jun 2020 15:59:39 +0200 Subject: [PATCH] Allow explicitly specifying 'ibrav'. If an 'ibrav' value other than zero is specified in the SYSTEM inputs, the cell of the input structure is converted into the appropriate A, B, C, cosAB, cosAC, cosBC values. As a check, the cell is reconstructed using qe_tools. The cells are compared element-wise, with an absolute tolerance controlled by the 'IBRAV_CELL_TOLERANCE' setting. --- .../calculations/__init__.py | 30 +++++- aiida_quantumespresso/calculations/cp.py | 1 - .../calculations/helpers/__init__.py | 1 - aiida_quantumespresso/calculations/pw.py | 1 - aiida_quantumespresso/tools/pwinputparser.py | 3 - .../user_guide/calculation_plugins/cp.rst | 3 +- .../user_guide/calculation_plugins/pw.rst | 3 +- setup.json | 4 +- tests/calculations/test_pw.py | 97 +++++++++++++++++++ tests/calculations/test_pw/test_pw_ibrav.in | 24 +++++ tests/tools/test_immigrate.py | 64 +++++++++++- 11 files changed, 216 insertions(+), 15 deletions(-) create mode 100644 tests/calculations/test_pw/test_pw_ibrav.in diff --git a/aiida_quantumespresso/calculations/__init__.py b/aiida_quantumespresso/calculations/__init__.py index 43a6d827e..81243c7f5 100644 --- a/aiida_quantumespresso/calculations/__init__.py +++ b/aiida_quantumespresso/calculations/__init__.py @@ -7,9 +7,12 @@ from aiida.common import datastructures, exceptions from aiida.common.lang import classproperty +from qe_tools.converters import get_parameters_from_cell + from aiida_quantumespresso.utils.convert import convert_input_to_namelist_entry from .base import CalcJob +from .helpers import QEInputValidationError class BasePwCpInputGenerator(CalcJob): @@ -285,10 +288,16 @@ def _generate_PWCPinputdata(cls, parameters, settings, pseudos, structure, kpoin # ============ I prepare the input site data ============= # ------------ CELL_PARAMETERS ----------- - cell_parameters_card = 'CELL_PARAMETERS angstrom\n' - for vector in structure.cell: - cell_parameters_card += ('{0:18.10f} {1:18.10f} {2:18.10f}' - '\n'.format(*vector)) + + # Specify cell parameters only if 'ibrav' is either zero or + # unspecified. + if input_params.get('SYSTEM', {}).get('ibrav', 0) == 0: + cell_parameters_card = 'CELL_PARAMETERS angstrom\n' + for vector in structure.cell: + cell_parameters_card += ('{0:18.10f} {1:18.10f} {2:18.10f}' + '\n'.format(*vector)) + else: + cell_parameters_card = '' # ------------- ATOMIC_SPECIES ------------ atomic_species_card_list = [] @@ -448,7 +457,18 @@ def _generate_PWCPinputdata(cls, parameters, settings, pseudos, structure, kpoin # Set some variables (look out at the case! NAMELISTS should be # uppercase, internal flag names must be lowercase) input_params.setdefault('SYSTEM', {}) - input_params['SYSTEM']['ibrav'] = 0 + input_params['SYSTEM'].setdefault('ibrav', 0) + ibrav = input_params['SYSTEM']['ibrav'] + if ibrav != 0: + try: + structure_parameters = get_parameters_from_cell( + ibrav=ibrav, + cell=structure.get_attribute('cell'), + tolerance=settings.pop('IBRAV_CELL_TOLERANCE', 1e-6) + ) + except ValueError as exc: + raise QEInputValidationError('Cannot get structure parameters from cell: {}'.format(exc)) from exc + input_params['SYSTEM'].update(structure_parameters) input_params['SYSTEM']['nat'] = len(structure.sites) input_params['SYSTEM']['ntyp'] = len(structure.kinds) diff --git a/aiida_quantumespresso/calculations/cp.py b/aiida_quantumespresso/calculations/cp.py index 4f6266272..085b2c39a 100644 --- a/aiida_quantumespresso/calculations/cp.py +++ b/aiida_quantumespresso/calculations/cp.py @@ -52,7 +52,6 @@ class CpCalculation(BasePwCpInputGenerator): ('CONTROL', 'pseudo_dir'), # set later ('CONTROL', 'outdir'), # set later ('CONTROL', 'prefix'), # set later - ('SYSTEM', 'ibrav'), # set later ('SYSTEM', 'celldm'), ('SYSTEM', 'nat'), # set later ('SYSTEM', 'ntyp'), # set later diff --git a/aiida_quantumespresso/calculations/helpers/__init__.py b/aiida_quantumespresso/calculations/helpers/__init__.py index 4eeb2b005..b7c66f7b4 100644 --- a/aiida_quantumespresso/calculations/helpers/__init__.py +++ b/aiida_quantumespresso/calculations/helpers/__init__.py @@ -158,7 +158,6 @@ def pw_input_helper(input_params, structure, stop_at_first_error=False, flat_mod i.lower() for i in [ 'pseudo_dir', 'outdir', - 'ibrav', 'celldm', 'nat', 'ntyp', diff --git a/aiida_quantumespresso/calculations/pw.py b/aiida_quantumespresso/calculations/pw.py index 267893373..42513e011 100644 --- a/aiida_quantumespresso/calculations/pw.py +++ b/aiida_quantumespresso/calculations/pw.py @@ -27,7 +27,6 @@ class PwCalculation(BasePwCpInputGenerator): ('CONTROL', 'pseudo_dir'), ('CONTROL', 'outdir'), ('CONTROL', 'prefix'), - ('SYSTEM', 'ibrav'), ('SYSTEM', 'celldm'), ('SYSTEM', 'nat'), ('SYSTEM', 'ntyp'), diff --git a/aiida_quantumespresso/tools/pwinputparser.py b/aiida_quantumespresso/tools/pwinputparser.py index b2804eb5c..86b794d8f 100644 --- a/aiida_quantumespresso/tools/pwinputparser.py +++ b/aiida_quantumespresso/tools/pwinputparser.py @@ -96,9 +96,6 @@ def create_builder_from_file(input_folder, input_file_name, code, metadata, pseu builder.structure = parsed_file.get_structuredata() builder.kpoints = parsed_file.get_kpointsdata() - if parsed_file.namelists['SYSTEM']['ibrav'] != 0: - raise NotImplementedError('Found ibrav != 0: `aiida-quantumespresso` currently only supports ibrav = 0.') - # Then, strip the namelist items that the plugin doesn't allow or sets later. # NOTE: If any of the position or cell units are in alat or crystal # units, that will be taken care of by the input parsing tools, and diff --git a/docs/source/user_guide/calculation_plugins/cp.rst b/docs/source/user_guide/calculation_plugins/cp.rst index 3b49a21e8..d300db850 100644 --- a/docs/source/user_guide/calculation_plugins/cp.rst +++ b/docs/source/user_guide/calculation_plugins/cp.rst @@ -35,7 +35,6 @@ Inputs 'CONTROL', 'pseudo_dir': pseudopotential directory 'CONTROL', 'outdir': scratch directory 'CONTROL', 'prefix': file prefix - 'SYSTEM', 'ibrav': cell shape 'SYSTEM', 'celldm': cell dm 'SYSTEM', 'nat': number of atoms 'SYSTEM', 'ntyp': number of species @@ -48,6 +47,8 @@ Inputs Those keywords should not be specified, otherwise the submission will fail. + The `SYSTEM`, `ibrav` keyword is optional. If it is not specified, `ibrav=0` is used. When a non-zero `ibrav` is given, `aiida-quantumespresso` automatically extracts the cell parameters. As a consistency check, the cell is re-constructed from these parameters and compared to the input cell. The input structure needs to match the convention detailed in the `pw.x documentation `_. The tolerance used in this check can be adjusted with the `IBRAV_CELL_TOLERANCE` key in the `settings` dictionary. It defines the absolute tolerance on each element of the cell matrix. + * **structure**, class :py:class:`StructureData ` The initial ionic configuration of the CP molecular dynamics. * **settings**, class :py:class:`Dict ` (optional) diff --git a/docs/source/user_guide/calculation_plugins/pw.rst b/docs/source/user_guide/calculation_plugins/pw.rst index 7e2ce8d6a..dc60bd6d8 100644 --- a/docs/source/user_guide/calculation_plugins/pw.rst +++ b/docs/source/user_guide/calculation_plugins/pw.rst @@ -81,7 +81,6 @@ This can then be used directly in the process builder of for example a ``PwCalcu 'CONTROL', 'pseudo_dir': pseudopotential directory 'CONTROL', 'outdir': scratch directory 'CONTROL', 'prefix': file prefix - 'SYSTEM', 'ibrav': cell shape 'SYSTEM', 'celldm': cell dm 'SYSTEM', 'nat': number of atoms 'SYSTEM', 'ntyp': number of species @@ -94,6 +93,8 @@ This can then be used directly in the process builder of for example a ``PwCalcu Those keywords should not be specified, otherwise the submission will fail. + The `SYSTEM`, `ibrav` keyword is optional. If it is not specified, `ibrav=0` is used. When a non-zero `ibrav` is given, `aiida-quantumespresso` automatically extracts the cell parameters. As a consistency check, the cell is re-constructed from these parameters and compared to the input cell. The input structure needs to match the convention detailed in the `pw.x documentation `_. The tolerance used in this check can be adjusted with the `IBRAV_CELL_TOLERANCE` key in the `settings` dictionary. It defines the absolute tolerance on each element of the cell matrix. + * **structure**, class :py:class:`StructureData ` * **settings**, class :py:class:`Dict ` (optional) An optional dictionary that activates non-default operations. For a list of possible diff --git a/setup.json b/setup.json index 2cdbad8a8..98ee55c3f 100644 --- a/setup.json +++ b/setup.json @@ -92,7 +92,9 @@ "aiida_core[atomic_tools]~=1.2", "packaging", "qe-tools~=2.0rc1", - "xmlschema~=1.2" + "xmlschema~=1.2", + "numpy", + "scipy" ], "license": "MIT License", "name": "aiida_quantumespresso", diff --git a/tests/calculations/test_pw.py b/tests/calculations/test_pw.py index 6028cf20a..0033f4146 100644 --- a/tests/calculations/test_pw.py +++ b/tests/calculations/test_pw.py @@ -1,6 +1,12 @@ # -*- coding: utf-8 -*- """Tests for the `PwCalculation` class.""" + +import pytest + +from aiida import orm from aiida.common import datastructures +from aiida_quantumespresso.utils.resources import get_default_options +from aiida_quantumespresso.calculations.helpers import QEInputValidationError def test_pw_default(fixture_sandbox, generate_calc_job, generate_inputs_pw, file_regression): @@ -30,3 +36,94 @@ def test_pw_default(fixture_sandbox, generate_calc_job, generate_inputs_pw, file # Checks on the files written to the sandbox folder as raw input assert sorted(fixture_sandbox.get_content_list()) == sorted(['aiida.in', 'pseudo', 'out']) file_regression.check(input_written, encoding='utf-8', extension='.in') + + +def test_pw_ibrav( + aiida_profile, fixture_sandbox, generate_calc_job, fixture_code, generate_kpoints_mesh, generate_upf_data, + file_regression +): + """Test a `PwCalculation` where `ibrav` is explicitly specified.""" + entry_point_name = 'quantumespresso.pw' + + parameters = {'CONTROL': {'calculation': 'scf'}, 'SYSTEM': {'ecutrho': 240.0, 'ecutwfc': 30.0, 'ibrav': 2}} + + # The structure needs to be rotated in the same way QE does it for ibrav=2. + param = 5.43 + cell = [[-param / 2., 0, param / 2.], [0, param / 2., param / 2.], [-param / 2., param / 2., 0]] + structure = orm.StructureData(cell=cell) + structure.append_atom(position=(0., 0., 0.), symbols='Si', name='Si') + structure.append_atom(position=(param / 4., param / 4., param / 4.), symbols='Si', name='Si') + + upf = generate_upf_data('Si') + inputs = { + 'code': fixture_code(entry_point_name), + 'structure': structure, + 'kpoints': generate_kpoints_mesh(2), + 'parameters': orm.Dict(dict=parameters), + 'pseudos': { + 'Si': upf + }, + 'metadata': { + 'options': get_default_options() + } + } + + calc_info = generate_calc_job(fixture_sandbox, entry_point_name, inputs) + + cmdline_params = ['-in', 'aiida.in'] + local_copy_list = [(upf.uuid, upf.filename, u'./pseudo/Si.upf')] + retrieve_list = ['aiida.out', './out/aiida.save/data-file-schema.xml', './out/aiida.save/data-file.xml'] + retrieve_temporary_list = [['./out/aiida.save/K*[0-9]/eigenval*.xml', '.', 2]] + + # Check the attributes of the returned `CalcInfo` + assert isinstance(calc_info, datastructures.CalcInfo) + assert sorted(calc_info.cmdline_params) == sorted(cmdline_params) + assert sorted(calc_info.local_copy_list) == sorted(local_copy_list) + assert sorted(calc_info.retrieve_list) == sorted(retrieve_list) + assert sorted(calc_info.retrieve_temporary_list) == sorted(retrieve_temporary_list) + assert sorted(calc_info.remote_symlink_list) == sorted([]) + + with fixture_sandbox.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.get_content_list()) == sorted(['aiida.in', 'pseudo', 'out']) + file_regression.check(input_written, encoding='utf-8', extension='.in') + + +def test_pw_wrong_ibrav( + aiida_profile, + fixture_sandbox, + generate_calc_job, + fixture_code, + generate_kpoints_mesh, + generate_upf_data, +): + """Test that a `PwCalculation` with an incorrect `ibrav` raises.""" + entry_point_name = 'quantumespresso.pw' + + parameters = {'CONTROL': {'calculation': 'scf'}, 'SYSTEM': {'ecutrho': 240.0, 'ecutwfc': 30.0, 'ibrav': 2}} + + # Here we use the wrong order of unit cell vectors on purpose. + param = 5.43 + cell = [[0, param / 2., param / 2.], [-param / 2., 0, param / 2.], [-param / 2., param / 2., 0]] + structure = orm.StructureData(cell=cell) + structure.append_atom(position=(0., 0., 0.), symbols='Si', name='Si') + structure.append_atom(position=(param / 4., param / 4., param / 4.), symbols='Si', name='Si') + + upf = generate_upf_data('Si') + inputs = { + 'code': fixture_code(entry_point_name), + 'structure': structure, + 'kpoints': generate_kpoints_mesh(2), + 'parameters': orm.Dict(dict=parameters), + 'pseudos': { + 'Si': upf + }, + 'metadata': { + 'options': get_default_options() + } + } + + with pytest.raises(QEInputValidationError): + generate_calc_job(fixture_sandbox, entry_point_name, inputs) diff --git a/tests/calculations/test_pw/test_pw_ibrav.in b/tests/calculations/test_pw/test_pw_ibrav.in new file mode 100644 index 000000000..dc5efacd7 --- /dev/null +++ b/tests/calculations/test_pw/test_pw_ibrav.in @@ -0,0 +1,24 @@ +&CONTROL + calculation = 'scf' + outdir = './out/' + prefix = 'aiida' + pseudo_dir = './pseudo/' + verbosity = 'high' +/ +&SYSTEM + a = 5.4300000000d+00 + ecutrho = 2.4000000000d+02 + ecutwfc = 3.0000000000d+01 + ibrav = 2 + nat = 2 + ntyp = 1 +/ +&ELECTRONS +/ +ATOMIC_SPECIES +Si 28.0855 Si.upf +ATOMIC_POSITIONS angstrom +Si 0.0000000000 0.0000000000 0.0000000000 +Si 1.3575000000 1.3575000000 1.3575000000 +K_POINTS automatic +2 2 2 0 0 0 diff --git a/tests/tools/test_immigrate.py b/tests/tools/test_immigrate.py index 41f308033..fc4a37a2b 100644 --- a/tests/tools/test_immigrate.py +++ b/tests/tools/test_immigrate.py @@ -2,6 +2,8 @@ """Tests for immigrating `PwCalculation`s.""" import os +import numpy as np + from aiida_quantumespresso.tools.pwinputparser import create_builder_from_file @@ -47,10 +49,70 @@ def test_create_builder(fixture_sandbox, fixture_code, generate_upf_data, genera }, 'SYSTEM': { 'ecutrho': 240.0, - 'ecutwfc': 30.0 + 'ecutwfc': 30.0, + 'ibrav': 0, + } + } + assert 'kpoints' in builder + assert 'structure' in builder + + generate_calc_job(fixture_sandbox, entry_point_name, builder) + + +def test_create_builder_nonzero_ibrav( + aiida_profile, fixture_sandbox, fixture_code, generate_upf_data, generate_calc_job +): + """Test the `create_builder_from_file` method that parses an existing `pw.x` folder into a process builder. + + The input file used is the one generated for `tests.calculations.test_pw.test_pw_ibrav`. + """ + entry_point_name = 'quantumespresso.pw' + code = fixture_code(entry_point_name) + + metadata = { + 'options': { + 'resources': { + 'num_machines': 1, + 'num_mpiprocs_per_machine': 32, + }, + 'max_memory_kb': 1000, + 'max_wallclock_seconds': 60 * 60 * 12, + 'withmpi': True, + } + } + + in_foldername = os.path.join('tests', 'calculations', 'test_pw') + in_folderpath = os.path.abspath(in_foldername) + + upf_foldername = os.path.join('tests', 'fixtures', 'pseudos') + upf_folderpath = os.path.abspath(upf_foldername) + si_upf = generate_upf_data('Si') + si_upf.store() + + builder = create_builder_from_file( + in_folderpath, 'test_pw_ibrav.in', code, metadata, upf_folderpath, use_first=True + ) + + assert builder['code'] == code + assert builder['metadata'] == metadata + pseudo_hash = si_upf.get_hash() + assert pseudo_hash is not None + assert builder['pseudos']['Si'].get_hash() == pseudo_hash + assert builder['parameters'].get_dict() == { + 'CONTROL': { + 'calculation': 'scf', + 'verbosity': 'high' + }, + 'SYSTEM': { + 'ecutrho': 240.0, + 'ecutwfc': 30.0, + 'ibrav': 2, } } assert 'kpoints' in builder assert 'structure' in builder + param = 5.43 + np.testing.assert_allclose([[-param / 2., 0, param / 2.], [0, param / 2., param / 2.], [-param / 2., param / 2., 0] + ], builder.structure.get_attribute('cell')) generate_calc_job(fixture_sandbox, entry_point_name, builder)