Skip to content

Commit

Permalink
Add support for electron-phonon calculations
Browse files Browse the repository at this point in the history
Make several changes in the `q2r.x` and `matdyn.x` plugins to provide
support for electron-phonon calculations:

* Adapt the `prepare_for_submission` scripts to allow remote copying of
the `elph_dir` for both plugins in case `la2F` is set to true.
* Add support to the `MatDynCalculation` plugin for setting `dos` to
true, and in this case converting the `kpoints` input to the `nkX`
tags instead of providing them as a list.
* For `matdyn.x` calculations where `dos` is true, parse the phonon DOS
instead of the phonon bands, and provide this as an output.
* For the `MatdynCalculation` plugin, providing the `parent_folder` input
currently only makes sense for electron-phonon calculations, so a
validator is added to check this. The remote copy/symlink list is also
overriden by the `elph_dir` to avoid adding it to the default `out`
directory of the `NamelistsCalculation`, which is typically no longer
required for the `matdyn.x` step.
  • Loading branch information
mbercx committed Jul 20, 2022
1 parent 144cf5d commit 35d355e
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 24 deletions.
70 changes: 59 additions & 11 deletions src/aiida_quantumespresso/calculations/matdyn.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -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',
Expand All @@ -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',
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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.
"""
Expand All @@ -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
1 change: 1 addition & 0 deletions src/aiida_quantumespresso/calculations/ph.py
Original file line number Diff line number Diff line change
Expand Up @@ -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-')

Expand Down
42 changes: 40 additions & 2 deletions src/aiida_quantumespresso/calculations/q2r.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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']
Expand All @@ -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
47 changes: 36 additions & 11 deletions src/aiida_quantumespresso/parsers/matdyn.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
Expand All @@ -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:
Expand All @@ -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

Expand Down

0 comments on commit 35d355e

Please sign in to comment.