Skip to content

Commit

Permalink
TMP COMMIT for projwfc.x changes
Browse files Browse the repository at this point in the history
  • Loading branch information
mbercx committed Jan 22, 2025
1 parent c84ef21 commit 7edefbe
Show file tree
Hide file tree
Showing 9 changed files with 2,201 additions and 461 deletions.
184 changes: 184 additions & 0 deletions aiida_quantumespresso/parsers/parse_raw/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
# -*- coding: utf-8 -*-
# pylint: disable=redefined-argument-from-local
"""A basic parser for the common format of QE."""

__all__ = ('parse_output_base', 'parse_output_error', 'convert_qe_time_to_sec', 'convert_qe2aiida_structure')


def parse_output_base(filecontent, codename=None, message_map=None):
"""Parse the output file of a QE calculation, just checking for basic content like JOB DONE, errors with %%%% etc.
:param filecontent: a string with the output file content
:param codename: the string printed both in the header and near the walltime.
If passed, a few more things are parsed (e.g. code version, walltime, ...)
:returns: tuple of two dictionaries, with the parsed data and log messages, respectively
"""
from aiida_quantumespresso.utils.mapping import get_logging_container

keys = ['error', 'warning']

if message_map is not None and (not isinstance(message_map, dict) or any(key not in message_map for key in keys)):
raise RuntimeError(f'invalid format `message_map`: should be dictionary with two keys {keys}')

logs = get_logging_container()
parsed_data = {}

lines = filecontent if isinstance(filecontent, list) else filecontent.split('\n')

for line in lines:
if 'JOB DONE' in line:
break
else:
logs.error.append('ERROR_OUTPUT_STDOUT_INCOMPLETE')

if codename is not None:

codestring = f'Program {codename}'

for line_number, line in enumerate(lines):

if codestring in line and 'starts on' in line:
parsed_data['code_version'] = line.split(codestring)[1].split('starts on')[0].strip()

# Parse the walltime
if codename in line and 'WALL' in line:
try:
time = line.split('CPU')[1].split('WALL')[0].strip()
parsed_data['wall_time'] = time
except (ValueError, IndexError):
logs.warnings.append('ERROR_PARSING_WALLTIME')
else:
try:
parsed_data['wall_time_seconds'] = convert_qe_time_to_sec(time)
except ValueError:
logs.warnings.append('ERROR_CONVERTING_WALLTIME_TO_SECONDS')

# Parse an error message with optional mapping of the message
if '%%%%%%%%%%%%%%' in line:
parse_output_error(lines, line_number, logs, message_map)

return parsed_data, logs


def parse_output_error(lines, line_number_start, logs, message_map=None):
"""Parse a Quantum ESPRESSO error message which appears between two lines marked by ``%%%%%%%%``).
:param lines: a list of strings gotten by splitting the standard output content on newlines
:param line_number_start: the line at which we identified some ``%%%%%%%%``
:param logs: a logging container from `aiida_quantumespresso.utils.mapping.get_logging_container`
"""

def map_message(message, message_map, logs):

# Match any known error and warning messages
for marker, message in message_map['error'].items():
if marker in line:
if message is None:
message = line
logs.error.append(message)

for marker, message in message_map['warning'].items():
if marker in line:
if message is None:
message = line
logs.warning.append(message)

# First determine the line that closes the error block which is also marked by ``%%%%%%%`` in the line
for line_number, line in enumerate(lines[line_number_start + 1:]):
if '%%%%%%%%%%%%' in line:
line_number_end = line_number
break
else:
return

# Get the set of unique lines between the error indicators and pass them through the message map, or if not provided
# simply append the message to the `error` list of the logs container
for message in set(lines[line_number_start:line_number_end]):
if message_map is not None:
map_message(message, message_map, logs)
else:
logs.error(message)

return


def convert_qe_time_to_sec(timestr):
"""Given the walltime string of Quantum Espresso, converts it in a number of seconds (float)."""
rest = timestr.strip()

if 'd' in rest:
days, rest = rest.split('d')
else:
days = '0'

if 'h' in rest:
hours, rest = rest.split('h')
else:
hours = '0'

if 'm' in rest:
minutes, rest = rest.split('m')
else:
minutes = '0'

if 's' in rest:
seconds, rest = rest.split('s')
else:
seconds = '0.'

if rest.strip():
raise ValueError(f"Something remained at the end of the string '{timestr}': '{rest}'")

num_seconds = float(seconds) + float(minutes) * 60. + float(hours) * 3600. + float(days) * 86400.

return num_seconds


def convert_qe2aiida_structure(output_dict, input_structure=None):
"""Receives the dictionary cell parsed from quantum espresso Convert it into an AiiDA structure object."""
from aiida.plugins import DataFactory

StructureData = DataFactory('structure')

cell_dict = output_dict['cell']

# If I don't have any help, I will set up the cell as it is in QE
if not input_structure:

struc = StructureData(cell=cell_dict['lattice_vectors'])
for atom in cell_dict['atoms']:
struc.append_atom(position=tuple(atom[1]), symbols=[atom[0]])

else:

struc = input_structure.clone()
struc.reset_cell(cell_dict['lattice_vectors'])
new_pos = [i[1] for i in cell_dict['atoms']]
struc.reset_sites_positions(new_pos)

return struc


def convert_qe_to_kpoints(xml_dict, structure):
"""Build the output kpoints from the raw parsed data.
:param parsed_parameters: the raw parsed data
:return: a `KpointsData` or None
"""
from aiida.orm import KpointsData

k_points_list = xml_dict.get('k_points', None)
k_points_units = xml_dict.get('k_points_units', None)
k_points_weights_list = xml_dict.get('k_points_weights', None)

if k_points_list is None or k_points_weights_list is None:
return None

if k_points_units != '1 / angstrom':
raise ValueError('k-points are not expressed in reciprocal cartesian coordinates')

kpoints = KpointsData()
kpoints.set_cell_from_structure(structure)
kpoints.set_kpoints(k_points_list, cartesian=True, weights=k_points_weights_list)

return kpoints
11 changes: 7 additions & 4 deletions src/aiida_quantumespresso/calculations/projwfc.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,11 @@ class ProjwfcCalculation(NamelistsCalculation):

xml_path = Path(NamelistsCalculation._default_parent_output_folder
).joinpath(f'{NamelistsCalculation._PREFIX}.save', 'data-file-schema.xml')
_internal_retrieve_list = [
NamelistsCalculation._PREFIX + '.pdos*',
]

# The XML file is added to the temporary retrieve list since it is required for parsing, but already in the
# repository of a an ancestor calculation.
_retrieve_temporary_list = [
NamelistsCalculation._PREFIX + '.pdos*',
xml_path.as_posix(),
]

Expand All @@ -61,7 +60,11 @@ def define(cls, spec):
spec.exit_code(301, 'ERROR_NO_RETRIEVED_TEMPORARY_FOLDER',
message='The retrieved temporary folder could not be accessed.')
spec.exit_code(303, 'ERROR_OUTPUT_XML_MISSING',
message='The retrieved folder did not contain the required XML file.')
message='The retrieved folder did not contain the required required XML file.')
spec.exit_code(310, 'ERROR_OUTPUT_STDOUT_READ',
message='The stdout output file could not be read.')
spec.exit_code(312, 'ERROR_OUTPUT_STDOUT_INCOMPLETE',
message='The stdout output file was incomplete probably because the calculation got interrupted.')
spec.exit_code(320, 'ERROR_OUTPUT_XML_READ',
message='The XML output file could not be read.')
spec.exit_code(321, 'ERROR_OUTPUT_XML_PARSE',
Expand Down
Loading

0 comments on commit 7edefbe

Please sign in to comment.