diff --git a/src/aiida_quantumespresso/calculations/matdyn.py b/src/aiida_quantumespresso/calculations/matdyn.py index 79beac1dd..1e3c92c12 100644 --- a/src/aiida_quantumespresso/calculations/matdyn.py +++ b/src/aiida_quantumespresso/calculations/matdyn.py @@ -1,8 +1,12 @@ # -*- coding: utf-8 -*- """`CalcJob` implementation for the matdyn.x code of Quantum ESPRESSO.""" +from pathlib import Path + from aiida import orm +from aiida_quantumespresso.calculations import _uppercase_dict from aiida_quantumespresso.calculations.namelists import NamelistsCalculation +from aiida_quantumespresso.calculations.ph import PhCalculation from aiida_quantumespresso.data.force_constants import ForceConstantsData @@ -31,10 +35,12 @@ def define(cls, spec): super().define(spec) spec.input('force_constants', valid_type=ForceConstantsData, required=True) spec.input('kpoints', valid_type=orm.KpointsData, help='Kpoints on which to calculate the phonon frequencies.') + spec.input('parent_folder', valid_type=orm.RemoteData, required=False) spec.inputs.validator = cls._validate_inputs spec.output('output_parameters', valid_type=orm.Dict) - spec.output('output_phonon_bands', valid_type=orm.BandsData) + spec.output('output_phonon_bands', valid_type=orm.BandsData, required=False) + spec.output('output_phonon_dos', valid_type=orm.XyData, required=False) spec.default_output_node = 'output_parameters' spec.exit_code(310, 'ERROR_OUTPUT_STDOUT_READ', @@ -43,6 +49,8 @@ def define(cls, spec): message='The stdout output file was incomplete probably because the calculation got interrupted.') spec.exit_code(330, 'ERROR_OUTPUT_FREQUENCIES', message='The output frequencies file could not be read from the retrieved folder.') + spec.exit_code(330, 'ERROR_OUTPUT_DOS', + message='The output DOS file could not be read from the retrieved folder.') spec.exit_code(410, 'ERROR_OUTPUT_KPOINTS_MISSING', message='Number of kpoints not found in the output data') spec.exit_code(411, 'ERROR_OUTPUT_KPOINTS_INCOMMENSURATE', @@ -60,6 +68,9 @@ def _validate_inputs(value, _): if parameters.get('INPUT', {}).get('flfrc', None) is not None: return '`INPUT.flfrc` is set automatically from the `force_constants` input.' + if 'parent_folder' in value and not parameters.get('INPUT').get('la2F', False): + return 'The `parent_folder` input is only used to calculate the el-ph coefficients (`la2F = .true.`)' + def generate_input_file(self, parameters): """Generate namelist input_file content given a dict of parameters. @@ -68,21 +79,31 @@ def generate_input_file(self, parameters): :return: 'str' containing the input_file content a plain text. """ kpoints = self.inputs.kpoints + append_string = '' + parameters.setdefault('INPUT', {})['flfrc'] = self.inputs.force_constants.filename - file_content = super().generate_input_file(parameters) - try: - kpoints_list = kpoints.get_kpoints() - except AttributeError: - kpoints_list = kpoints.get_kpoints_mesh(print_list=True) + # Calculating DOS requires (nk1,nk2,nk3), see + # https://gitlab.com/QEF/q-e/-/blob/develop/PHonon/PH/matdyn.f90#L72-73 + if parameters['INPUT'].get('dos', False): + kpoints_mesh = kpoints.get_kpoints_mesh()[0] + parameters['INPUT']['nk1'] = kpoints_mesh[0] + parameters['INPUT']['nk2'] = kpoints_mesh[1] + parameters['INPUT']['nk3'] = kpoints_mesh[2] + else: + try: + kpoints_list = kpoints.get_kpoints() + except AttributeError: + kpoints_list = kpoints.get_kpoints_mesh(print_list=True) - kpoints_string = [f'{len(kpoints_list)}'] - for kpoint in kpoints_list: - kpoints_string.append('{:18.10f} {:18.10f} {:18.10f}'.format(*kpoint)) # pylint: disable=consider-using-f-string + kpoints_string = [f'{len(kpoints_list)}'] + for kpoint in kpoints_list: + kpoints_string.append('{:18.10f} {:18.10f} {:18.10f}'.format(*kpoint)) # pylint: disable=consider-using-f-string + append_string = '\n'.join(kpoints_string) + '\n' - file_content += '\n'.join(kpoints_string) + '\n' + file_content = super().generate_input_file(parameters) - return file_content + return file_content + append_string def prepare_for_submission(self, folder): """Prepare the calculation job for submission by transforming input nodes into input files. @@ -91,6 +112,10 @@ def prepare_for_submission(self, folder): contains lists of files that need to be copied to the remote machine before job submission, as well as file lists that are to be retrieved after job completion. + After calling the method of the parent `NamelistsCalculation` class, the input parameters are checked to see + if the `la2F` tag is set to true. In this case the electron-phonon directory is added to the remote symlink or + copy list, depending on the settings. + :param folder: a sandbox folder to temporarily write files on disk. :return: :py:`~aiida.common.datastructures.CalcInfo` instance. """ @@ -99,4 +124,27 @@ def prepare_for_submission(self, folder): force_constants = self.inputs.force_constants calcinfo.local_copy_list.append((force_constants.uuid, force_constants.filename, force_constants.filename)) + if 'settings' in self.inputs: + settings = _uppercase_dict(self.inputs.settings.get_dict(), dict_name='settings') + else: + settings = {} + + if 'parameters' in self.inputs: + parameters = _uppercase_dict(self.inputs.parameters.get_dict(), dict_name='parameters') + else: + parameters = {} + + source = self.inputs.get('parent_folder', None) + + if source is not None and parameters.get('INPUT').get('la2F', False): + + # pylint: disable=protected-access + dirpath = Path(source.get_remote_path()) / PhCalculation._FOLDER_ELECTRON_PHONON + remote_list = [(source.computer.uuid, str(dirpath), PhCalculation._FOLDER_ELECTRON_PHONON)] + + if settings.pop('PARENT_FOLDER_SYMLINK', False): + calcinfo.remote_symlink_list = remote_list + else: + calcinfo.remote_copy_list = remote_list + return calcinfo diff --git a/src/aiida_quantumespresso/calculations/ph.py b/src/aiida_quantumespresso/calculations/ph.py index 33c199701..3f73075bd 100644 --- a/src/aiida_quantumespresso/calculations/ph.py +++ b/src/aiida_quantumespresso/calculations/ph.py @@ -36,6 +36,7 @@ class PhCalculation(CalcJob): _DVSCF_PREFIX = 'dvscf' _DRHO_STAR_EXT = 'drho_rot' _FOLDER_DYNAMICAL_MATRIX = 'DYN_MAT' + _FOLDER_ELECTRON_PHONON = 'elph_dir' _VERBOSITY = 'high' _OUTPUT_DYNAMICAL_MATRIX_PREFIX = os.path.join(_FOLDER_DYNAMICAL_MATRIX, 'dynamical-matrix-') diff --git a/src/aiida_quantumespresso/calculations/q2r.py b/src/aiida_quantumespresso/calculations/q2r.py index 1aeed34ab..88640b223 100644 --- a/src/aiida_quantumespresso/calculations/q2r.py +++ b/src/aiida_quantumespresso/calculations/q2r.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- """`CalcJob` implementation for the q2r.x code of Quantum ESPRESSO.""" -import os +from pathlib import Path from aiida import orm +from aiida_quantumespresso.calculations import _uppercase_dict from aiida_quantumespresso.calculations.namelists import NamelistsCalculation from aiida_quantumespresso.calculations.ph import PhCalculation from aiida_quantumespresso.data.force_constants import ForceConstantsData @@ -14,7 +15,7 @@ class Q2rCalculation(NamelistsCalculation): _FORCE_CONSTANTS_NAME = 'real_space_force_constants.dat' _OUTPUT_SUBFOLDER = PhCalculation._FOLDER_DYNAMICAL_MATRIX # pylint: disable=protected-access - _INPUT_SUBFOLDER = os.path.join('.', PhCalculation._FOLDER_DYNAMICAL_MATRIX) # pylint: disable=protected-access + _INPUT_SUBFOLDER = PhCalculation._FOLDER_DYNAMICAL_MATRIX # pylint: disable=protected-access _default_parent_output_folder = PhCalculation._FOLDER_DYNAMICAL_MATRIX # pylint: disable=protected-access _default_namelists = ['INPUT'] @@ -40,3 +41,40 @@ def define(cls, spec): spec.exit_code(330, 'ERROR_READING_FORCE_CONSTANTS_FILE', message='The force constants file could not be read.') # yapf: enable + + def prepare_for_submission(self, folder): + """Prepare the calculation job for submission by transforming input nodes into input files. + + In addition to the input files being written to the sandbox folder, a `CalcInfo` instance will be returned that + contains lists of files that need to be copied to the remote machine before job submission, as well as file + lists that are to be retrieved after job completion. + + After calling the method of the parent `NamelistsCalculation` class, the input parameters are checked to see + if the `la2F` tag is set to true. In this case the electron-phonon directory is added to the remote symlink or + copy list, depending on the settings. + + :param folder: a sandbox folder to temporarily write files on disk. + :return: :py:`~aiida.common.datastructures.CalcInfo` instance. + """ + calcinfo = super().prepare_for_submission(folder) + + if 'settings' in self.inputs: + settings = _uppercase_dict(self.inputs.settings.get_dict(), dict_name='settings') + else: + settings = {} + + parameters = self.inputs.parameters.get_dict() + source = self.inputs.get('parent_folder', None) + + if source is not None: + + if parameters.get('INPUT').get('la2F', False): + + symlink = settings.pop('PARENT_FOLDER_SYMLINK', False) + remote_list = calcinfo.remote_symlink_list if symlink else calcinfo.remote_copy_list + + # pylint: disable=protected-access + dirpath = Path(source.get_remote_path()) / PhCalculation._FOLDER_ELECTRON_PHONON + remote_list.append((source.computer.uuid, str(dirpath), PhCalculation._FOLDER_ELECTRON_PHONON)) + + return calcinfo diff --git a/src/aiida_quantumespresso/parsers/matdyn.py b/src/aiida_quantumespresso/parsers/matdyn.py index f840284ed..3e8040675 100644 --- a/src/aiida_quantumespresso/parsers/matdyn.py +++ b/src/aiida_quantumespresso/parsers/matdyn.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- from aiida import orm +import numpy from qe_tools import CONSTANTS +from aiida_quantumespresso.calculations import _uppercase_dict from aiida_quantumespresso.calculations.matdyn import MatdynCalculation from .base import Parser @@ -15,6 +17,7 @@ def parse(self, **kwargs): retrieved = self.retrieved filename_stdout = self.node.get_option('output_filename') filename_frequencies = MatdynCalculation._PHONON_FREQUENCIES_NAME + filename_dos = MatdynCalculation._PHONON_DOS_NAME if filename_stdout not in retrieved.list_object_names(): return self.exit(self.exit_codes.ERROR_OUTPUT_STDOUT_READ) @@ -23,7 +26,10 @@ def parse(self, **kwargs): return self.exit(self.exit_codes.ERROR_OUTPUT_STDOUT_INCOMPLETE) if filename_frequencies not in retrieved.list_object_names(): - return self.exit(self.exit_codes.ERROR_OUTPUT_STDOUT_READ) + return self.exit(self.exit_codes.ERROR_OUTPUT_FREQUENCIES) + + if filename_dos not in retrieved.list_object_names(): + return self.exit(self.exit_codes.ERROR_OUTPUT_DOS) # Extract the kpoints from the input data and create the `KpointsData` for the `BandsData` try: @@ -36,23 +42,42 @@ def parse(self, **kwargs): parsed_data = parse_raw_matdyn_phonon_file(retrieved.get_object_content(filename_frequencies)) - try: - num_kpoints = parsed_data.pop('num_kpoints') - except KeyError: - return self.exit(self.exit_codes.ERROR_OUTPUT_KPOINTS_MISSING) + if 'parameters' in self.node.inputs: + parameters = _uppercase_dict(self.node.inputs.parameters.get_dict(), dict_name='parameters') + else: + parameters = {} + + if parameters.get('INPUT', {}).get('dos', False): + parsed_data.pop('phonon_bands') + + with retrieved.open(filename_dos) as handle: + dos_array = numpy.genfromtxt(handle) + + output_dos = orm.XyData() + output_dos.set_x(dos_array[:, 0], 'frequency', 'cm^(-1)') + output_dos.set_y(dos_array[:, 1], 'dos', 'states * cm') + + self.out('output_phonon_dos', output_dos) + + else: + if num_kpoints != kpoints.shape[0]: + return self.exit(self.exit_codes.ERROR_OUTPUT_KPOINTS_INCOMMENSURATE) + + try: + num_kpoints = parsed_data.pop('num_kpoints') + except KeyError: + return self.exit(self.exit_codes.ERROR_OUTPUT_KPOINTS_MISSING) - if num_kpoints != kpoints.shape[0]: - return self.exit(self.exit_codes.ERROR_OUTPUT_KPOINTS_INCOMMENSURATE) + output_bands = orm.BandsData() + output_bands.set_kpointsdata(kpoints_for_bands) + output_bands.set_bands(parsed_data.pop('phonon_bands'), units='THz') - output_bands = orm.BandsData() - output_bands.set_kpointsdata(kpoints_for_bands) - output_bands.set_bands(parsed_data.pop('phonon_bands'), units='THz') + self.out('output_phonon_bands', output_bands) for message in parsed_data['warnings']: self.logger.error(message) self.out('output_parameters', orm.Dict(parsed_data)) - self.out('output_phonon_bands', output_bands) return