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..9a7e0f9f0 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,86 @@ 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( + 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(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..f9aff1683 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,68 @@ 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(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)