From b8fb12adf0836dfe522db57f9193fef3f67e1527 Mon Sep 17 00:00:00 2001 From: Miki Bonacci Date: Mon, 25 Nov 2024 22:25:33 +0100 Subject: [PATCH 1/6] Adding magnetic utils for atomistic StructureData Very similar to HubbardUtils. --- src/aiida_quantumespresso/utils/magnetic.py | 90 +++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 src/aiida_quantumespresso/utils/magnetic.py diff --git a/src/aiida_quantumespresso/utils/magnetic.py b/src/aiida_quantumespresso/utils/magnetic.py new file mode 100644 index 00000000..c301c519 --- /dev/null +++ b/src/aiida_quantumespresso/utils/magnetic.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +"""Utility class for handling the :class:`aiida_quantumespresso.data.hubbard_structure.HubbardStructureData`.""" +# pylint: disable=no-name-in-module +from itertools import product +import os +from typing import Tuple, Union + +from aiida import orm +from aiida.engine import calcfunction +from aiida.orm import StructureData as LegacyStructureData +from aiida.plugins import DataFactory +import numpy as np + +StructureData = DataFactory('atomistic.structure') + + +class MagneticUtils: + """Class to manage the magnetic structure of the atomistic `LegacyStructureData`. + It contains methods to manipulate the magnetic structure in such a way to produce the correct input for QuantumESPRESSO calculations. + """ + + def __init__( + self, + structure: StructureData, + ): + """Set a the `StructureData` to manipulate.""" + if isinstance(structure, StructureData): + if 'magmoms' not in structure.get_defined_properties(): + raise ValueError('The input structure does not contain magnetic moments.') + self.structure = structure + else: + raise ValueError('input is not of type atomistic `StructureData') + + def generate_magnetic_namelist(self, parameters): + """Generate the magnetic namelist for Quantum ESPRESSO. + :param parameters: dictionary of inputs for the Quantum ESPRESSO calculation. + """ + if 'nspin' not in parameters['SYSTEM'] and 'noncolin' not in parameters['SYSTEM']: + raise ValueError("The input parameters must contain the 'nspin' or the 'noncolin' key.") + + namelist = {'starting_magnetization': {}, 'angle1': {}, 'angle2': {}} + + if parameters['SYSTEM'].get('nspin', None) == 2: + namelist.pop('angle1') + namelist.pop('angle2') + if self.structure.is_collinear: + for kind, magmom in zip(self.structure.kinds, self.structure.magmoms): + # this should be fixed, now only magmom_z is considered... + if magmom[2] != 0: + namelist['starting_magnetization'][kind] = magmom[2] + else: + raise NotImplementedError( + 'The input structure is not collinear, but you choose collinear calculations.' + ) + elif parameters['SYSTEM']['noncolin']: + for site in self.structure.sites: + for variable in namelist.keys(): + namelist[variable][site.kinds] = site.get_magmom_coord(coord='spherical')[variable] + + return namelist + + +@calcfunction +def generate_structure_with_magmoms(input_structure=StructureData, input_magnetic_moments=orm.List): + """Generate a new structure with the magnetic moments for each site. + :param input_structure: the input structure to add the magnetic moments. + :param input_magnetic_moments: the magnetic moments for each site, represented as a float (see below). + + For now, only supports collinear magnetic moments, i.e. atomic magnetizations (along z axis). + """ + magmoms = input_magnetic_moments.get_list() + if len(input_structure.sites) != len(magmoms): + raise ValueError('The input structure and the magnetic moments must have the same length.') + + mutable_structure = input_structure.get_value() + mutable_structure.clear_sites() + for site, magmom in zip(input_structure.sites, magmoms): + mutable_structure.add_atom( + **{ + 'positions': site.positions, + 'symbols': site.symbols, + 'kinds': site.kinds, + 'weights': site.weights, + 'magmoms': [0, 0, magmom] if isinstance(magmom, float) else magmom # 3D vector + } + ) + + output_structure = StructureData.from_mutable(mutable_structure, detect_kinds=True) + + return output_structure \ No newline at end of file From bc79498a6acaf6e6383d3080263bad186e5a1726 Mon Sep 17 00:00:00 2001 From: Miki Bonacci Date: Mon, 25 Nov 2024 22:46:22 +0100 Subject: [PATCH 2/6] Supporting output_structure of type atomistic `StructureData`. --- .../parsers/parse_raw/base.py | 31 ++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/aiida_quantumespresso/parsers/parse_raw/base.py b/src/aiida_quantumespresso/parsers/parse_raw/base.py index 7e535f95..a77f5307 100644 --- a/src/aiida_quantumespresso/parsers/parse_raw/base.py +++ b/src/aiida_quantumespresso/parsers/parse_raw/base.py @@ -2,7 +2,10 @@ """A basic parser for the common format of QE.""" import re -from aiida.orm import StructureData +from aiida.plugins import DataFactory +from aiida.orm import StructureData as LegacyStructureData + +from aiida_atomistic import StructureDataMutable, StructureData __all__ = ('convert_qe_time_to_sec', 'convert_qe_to_aiida_structure', 'convert_qe_to_kpoints') @@ -40,25 +43,37 @@ def convert_qe_time_to_sec(timestr): def convert_qe_to_aiida_structure(output_dict, input_structure=None): - """Convert the dictionary parsed from the Quantum ESPRESSO output into ``StructureData``.""" + """Convert the dictionary parsed from the Quantum ESPRESSO output into ``StructureData``. + If we have an ``orm.StructureData`` as input, we return an ``orm.StructureData`` instance, + otherwise we always return an aiida-atomistic ``StructureData``. + """ cell_dict = output_dict['cell'] # Without an input structure, try to recreate the structure from the output if not input_structure: - structure = StructureData(cell=cell_dict['lattice_vectors']) + structure = StructureDataMutable() + structure.set_cell=cell_dict['lattice_vectors'] for kind_name, position in output_dict['atoms']: symbol = re.sub(r'\d+', '', kind_name) - structure.append_atom(position=position, symbols=symbol, name=kind_name) + structure.add_atom(position=position, symbols=symbol, name=kind_name) + + return StructureData.from_mutable(structure) else: - structure = input_structure.clone() - structure.reset_cell(cell_dict['lattice_vectors']) - new_pos = [i[1] for i in cell_dict['atoms']] - structure.reset_sites_positions(new_pos) + if isinstance(input_structure, LegacyStructureData): + structure = input_structure.clone() + structure.reset_cell(cell_dict['lattice_vectors']) + new_pos = [i[1] for i in cell_dict['atoms']] + structure.reset_sites_positions(new_pos) + elif isinstance(input_structure, StructureData): + structure = input_structure.get_value() + structure.set_cell(cell_dict['lattice_vectors']) + for site,position in zip(structure.sites,[i[1] for i in cell_dict['atoms']]): + site.position = position return structure From 7a8254d0d3bfe62584720a3b763c2b89f084df26 Mon Sep 17 00:00:00 2001 From: Miki Bonacci Date: Thu, 5 Dec 2024 18:39:36 +0100 Subject: [PATCH 3/6] providing support for aiida-atomistic in Pw CalcJob and BaseWorkChain --- .../calculations/__init__.py | 51 ++++++++++++-- src/aiida_quantumespresso/calculations/pw.py | 7 +- .../workflows/protocols/utils.py | 67 +++++++++++++++++++ .../workflows/pw/base.py | 54 +++++++++++++-- 4 files changed, 166 insertions(+), 13 deletions(-) diff --git a/src/aiida_quantumespresso/calculations/__init__.py b/src/aiida_quantumespresso/calculations/__init__.py index 05bd80e7..a3733179 100644 --- a/src/aiida_quantumespresso/calculations/__init__.py +++ b/src/aiida_quantumespresso/calculations/__init__.py @@ -18,6 +18,7 @@ from aiida_quantumespresso.data.hubbard_structure import HubbardStructureData from aiida_quantumespresso.utils.convert import convert_input_to_namelist_entry from aiida_quantumespresso.utils.hubbard import HubbardUtils +from aiida_quantumespresso.utils.magnetic import MagneticUtils from .base import CalcJob from .helpers import QEInputValidationError @@ -25,6 +26,15 @@ LegacyUpfData = DataFactory('core.upf') UpfData = DataFactory('pseudo.upf') +LegacyStructureData = DataFactory('core.structure') # pylint: disable=invalid-name + +try: + StructureData = DataFactory('atomistic.structure') +except exceptions.MissingEntryPointError: + structures_classes = (LegacyStructureData,) +else: + structures_classes = (LegacyStructureData, StructureData) + class BasePwCpInputGenerator(CalcJob): """Base `CalcJob` for implementations for pw.x and cp.x of Quantum ESPRESSO.""" @@ -94,6 +104,8 @@ class BasePwCpInputGenerator(CalcJob): _use_kpoints = False + supported_properties = ['magmoms', 'hubbard'] + @classproperty def xml_filenames(cls): """Return a list of XML output filenames that can be written by a calculation. @@ -116,7 +128,7 @@ def define(cls, spec): spec.input('metadata.options.input_filename', valid_type=str, default=cls._DEFAULT_INPUT_FILE) spec.input('metadata.options.output_filename', valid_type=str, default=cls._DEFAULT_OUTPUT_FILE) spec.input('metadata.options.withmpi', valid_type=bool, default=True) # Override default withmpi=False - spec.input('structure', valid_type=orm.StructureData, + spec.input('structure', valid_type=(structures_classes), help='The input structure.') spec.input('parameters', valid_type=orm.Dict, help='The input parameters that are to be used to construct the input file.') @@ -168,6 +180,21 @@ def validate_inputs(cls, value, port_namespace): if any(key not in port_namespace for key in ('pseudos', 'structure')): return + if not isinstance(value['structure'], LegacyStructureData): + # we have the atomistic StructureData, so we need to check if all the defined properties are supported + plugin_check = value['structure'].check_plugin_support(cls.supported_properties) + if len(plugin_check) > 0: + raise NotImplementedError( + f'The input structure contains one or more unsupported properties \ + for this process: {plugin_check}' + ) + + if value['structure'].is_alloy or value['structure'].has_vacancies: + raise exceptions.InputValidationError( + 'The structure is an alloy or has vacancies. This is not allowed for \ + aiida-quantumespresso input structures.' + ) + # At this point, both ports are part of the namespace, and both are required so return an error message if any # of the two is missing. for key in ('pseudos', 'structure'): @@ -702,9 +729,17 @@ def _generate_PWCPinputdata(cls, parameters, settings, pseudos, structure, kpoin kpoints_card = ''.join(kpoints_card_list) del kpoints_card_list - # HUBBARD CARD - hubbard_card = HubbardUtils(structure).get_hubbard_card() if isinstance(structure, HubbardStructureData) \ - else None + # HUBBARD CARD and MAGNETIC NAMELIST + hubbard_card = None + magnetic_namelist = None + if isinstance(structure, HubbardStructureData): + hubbard_card = HubbardUtils(structure).get_hubbard_card() + elif len(structures_classes) == 2 and not isinstance(structure, LegacyStructureData): + # this means that we have the atomistic StructureData. + hubbard_card = HubbardUtils(structure).get_hubbard_card() if 'hubbard' \ + in structure.get_defined_properties() else None + magnetic_namelist = MagneticUtils(structure).generate_magnetic_namelist(input_params) if 'magmoms' in \ + structure.get_defined_properties() else None # =================== NAMELISTS AND CARDS ======================== try: @@ -734,6 +769,14 @@ def _generate_PWCPinputdata(cls, parameters, settings, pseudos, structure, kpoin 'namelists using the NAMELISTS inside the `settings` input node' ) from exception + if magnetic_namelist is not None: + if input_params['SYSTEM'].get('nspin', 1) == 1 and not input_params['SYSTEM'].get('noncolin', False): + raise exceptions.InputValidationError( + 'The structure has magnetic moments but the inputs are not set for \ + a magnetic calculation (`nspin`, `noncolin`)' + ) + input_params['SYSTEM'].update(magnetic_namelist) + inputfile = '' for namelist_name in namelists_toprint: inputfile += f'&{namelist_name}\n' diff --git a/src/aiida_quantumespresso/calculations/pw.py b/src/aiida_quantumespresso/calculations/pw.py index 9a489e67..9df223f8 100644 --- a/src/aiida_quantumespresso/calculations/pw.py +++ b/src/aiida_quantumespresso/calculations/pw.py @@ -5,10 +5,13 @@ from aiida import orm from aiida.common.lang import classproperty +from aiida.orm import StructureData as LegacyStructureData from aiida.plugins import factories from aiida_quantumespresso.calculations import BasePwCpInputGenerator +StructureData = factories.DataFactory('atomistic.structure') + class PwCalculation(BasePwCpInputGenerator): """`CalcJob` implementation for the pw.x code of Quantum ESPRESSO.""" @@ -69,13 +72,13 @@ def define(cls, spec): 'will not fail if the XML file is missing in the retrieved folder.') spec.input('kpoints', valid_type=orm.KpointsData, help='kpoint mesh or kpoint path') - spec.input('hubbard_file', valid_type=orm.SinglefileData, required=False, + spec.input('hubbard_file', valid_type=(StructureData, LegacyStructureData), required=False, help='SinglefileData node containing the output Hubbard parameters from a HpCalculation') spec.inputs.validator = cls.validate_inputs spec.output('output_parameters', valid_type=orm.Dict, help='The `output_parameters` output node of the successful calculation.') - spec.output('output_structure', valid_type=orm.StructureData, required=False, + spec.output('output_structure', valid_type=(StructureData, LegacyStructureData), required=False, help='The `output_structure` output node of the successful calculation if present.') spec.output('output_trajectory', valid_type=orm.TrajectoryData, required=False) spec.output('output_band', valid_type=orm.BandsData, required=False, diff --git a/src/aiida_quantumespresso/workflows/protocols/utils.py b/src/aiida_quantumespresso/workflows/protocols/utils.py index 664fe17c..ed43b797 100644 --- a/src/aiida_quantumespresso/workflows/protocols/utils.py +++ b/src/aiida_quantumespresso/workflows/protocols/utils.py @@ -152,3 +152,70 @@ def get_starting_magnetization( starting_magnetization[kind.name] = magnetization return starting_magnetization + + +def get_starting_magnetization_noncolin( + structure: StructureData, + pseudo_family: PseudoPotentialFamily, + initial_magnetic_moments: Optional[dict] = None +) -> tuple: + """Return the dictionary with starting magnetization for each kind in the structure. + + :param structure: the structure. + :param pseudo_family: pseudopotential family. + :param initial_magnetic_moments: dictionary mapping each kind in the structure to its magnetic moment. + :returns: dictionary of starting magnetizations. + """ + # try: + # structure.mykinds + # except AttributeError: + # raise TypeError(f"structure<{structure.pk}> do not have magmom") + starting_magnetization = {} + angle1 = {} + angle2 = {} + + if initial_magnetic_moments is not None: + + nkinds = len(structure.kinds) + + if sorted(initial_magnetic_moments.keys()) != sorted(structure.get_kind_names()): + raise ValueError(f'`initial_magnetic_moments` needs one value for each of the {nkinds} kinds.') + + for kind in structure.kinds: + magmom = initial_magnetic_moments[kind.name] + if isinstance(magmom, Union[int, float]): + starting_magnetization[kind.name] = magmom / pseudo_family.get_pseudo(element=kind.symbol).z_valence + angle1[kind.name] = 0.0 + angle2[kind.name] = 0.0 + else: # tuple of 3 float (r, theta, phi) + starting_magnetization[kind.name + ] = 2 * magmom[0] / pseudo_family.get_pseudo(element=kind.symbol).z_valence + angle1[kind.name] = magmom[1] + angle2[kind.name] = magmom[2] + try: + structure.mykinds + except AttributeError: + # Normal StructureData, no magmom in structure + magnetic_parameters = get_magnetization_parameters() + + for kind in structure.kinds: + magnetic_moment = magnetic_parameters[kind.symbol]['magmom'] + + if magnetic_moment == 0: + magnetization = magnetic_parameters['default_magnetization'] + else: + z_valence = pseudo_family.get_pseudo(element=kind.symbol).z_valence + magnetization = magnetic_moment / float(z_valence) + + starting_magnetization[kind.name] = magnetization + angle1[kind.name] = 0.0 + angle2[kind.name] = 0.0 + else: + # Self defined myStructureData, read magmom from structure + for kind in structure.mykinds: + magmom = kind.get_magmom_coord() + starting_magnetization[kind.name] = 2 * magmom[0] / pseudo_family.get_pseudo(element=kind.symbol).z_valence + angle1[kind.name] = magmom[1] + angle2[kind.name] = magmom[2] + + return starting_magnetization, angle1, angle2 diff --git a/src/aiida_quantumespresso/workflows/pw/base.py b/src/aiida_quantumespresso/workflows/pw/base.py index 4a3ab038..76445ff6 100644 --- a/src/aiida_quantumespresso/workflows/pw/base.py +++ b/src/aiida_quantumespresso/workflows/pw/base.py @@ -4,7 +4,7 @@ from aiida.common import AttributeDict, exceptions from aiida.common.lang import type_check from aiida.engine import BaseRestartWorkChain, ExitCode, ProcessHandlerReport, process_handler, while_ -from aiida.plugins import CalculationFactory, GroupFactory +from aiida.plugins import CalculationFactory, DataFactory, GroupFactory from aiida_quantumespresso.calculations.functions.create_kpoints_from_distance import create_kpoints_from_distance from aiida_quantumespresso.common.types import ElectronicType, RestartType, SpinType @@ -17,6 +17,12 @@ PseudoDojoFamily = GroupFactory('pseudo.family.pseudo_dojo') CutoffsPseudoPotentialFamily = GroupFactory('pseudo.family.cutoffs') +try: + StructureData = DataFactory('atomistic.structure') + HAS_ATOMISTIC = True +except ImportError: + HAS_ATOMISTIC = False + class PwBaseWorkChain(ProtocolMixin, BaseRestartWorkChain): """Workchain to run a Quantum ESPRESSO pw.x calculation with automated error handling and restarts.""" @@ -131,7 +137,11 @@ def get_builder_from_protocol( the ``CalcJobs`` that are nested in this work chain. :return: a process builder instance with all inputs defined ready for launch. """ - from aiida_quantumespresso.workflows.protocols.utils import get_starting_magnetization, recursive_merge + from aiida_quantumespresso.workflows.protocols.utils import ( + get_starting_magnetization, + get_starting_magnetization_noncolin, + recursive_merge, + ) if isinstance(code, str): code = orm.load_code(code) @@ -143,7 +153,7 @@ def get_builder_from_protocol( if electronic_type not in [ElectronicType.METAL, ElectronicType.INSULATOR]: raise NotImplementedError(f'electronic type `{electronic_type}` is not supported.') - if spin_type not in [SpinType.NONE, SpinType.COLLINEAR]: + if spin_type not in [SpinType.NONE, SpinType.COLLINEAR, SpinType.NON_COLLINEAR]: raise NotImplementedError(f'spin type `{spin_type}` is not supported.') if initial_magnetic_moments is not None and spin_type is not SpinType.COLLINEAR: @@ -189,10 +199,21 @@ def get_builder_from_protocol( parameters['SYSTEM'].pop('degauss') parameters['SYSTEM'].pop('smearing') - if spin_type is SpinType.COLLINEAR: - starting_magnetization = get_starting_magnetization(structure, pseudo_family, initial_magnetic_moments) - parameters['SYSTEM']['starting_magnetization'] = starting_magnetization - parameters['SYSTEM']['nspin'] = 2 + if isinstance(structure, orm.StructureData): + if spin_type is SpinType.COLLINEAR: + starting_magnetization = get_starting_magnetization(structure, pseudo_family, initial_magnetic_moments) + parameters['SYSTEM']['starting_magnetization'] = starting_magnetization + parameters['SYSTEM']['nspin'] = 2 + + if spin_type is SpinType.NON_COLLINEAR: + starting_magnetization_noncolin, angle1, angle2 = get_starting_magnetization_noncolin( + structure=structure, pseudo_family=pseudo_family, initial_magnetic_moments=initial_magnetic_moments + ) + parameters['SYSTEM']['starting_magnetization'] = starting_magnetization_noncolin + parameters['SYSTEM']['angle1'] = angle1 + parameters['SYSTEM']['angle2'] = angle2 + parameters['SYSTEM']['noncolin'] = True + parameters['SYSTEM']['nspin'] = 4 # If overrides are provided, they are considered absolute if overrides: @@ -284,6 +305,25 @@ def validate_kpoints(self): self.ctx.inputs.kpoints = kpoints + def validate_structure(self,): + """Validate the structure input for the workflow. + + This method checks if the structure has atomistic properties and if it is supported by the PwCalculation plugin. + If the structure contains unsupported properties, a new structure is generated without those properties. + + Modifies: + self.inputs.pw.structure: Updates the structure to a new one without unsupported properties if necessary. + """ + if HAS_ATOMISTIC: + # do we want to do this, or return a warning, or except? + from aiida_atomistic.data.structure.utils import generate_striped_structure # pylint: disable=import-error + plugin_check = self.inputs.pw.structure.check_plugin_support(PwCalculation.supported_properties) + if len(plugin_check) > 0: + # Generate a new StructureData without the unsupported properties. + self.inputs.pw.structure = generate_striped_structure( + self.inputs.pw.structure, orm.List(list(plugin_check)) + ) + def set_restart_type(self, restart_type, parent_folder=None): """Set the restart type for the next iteration.""" From 22d5ecf7ea905d756a7b198537ae27f8f5b00df7 Mon Sep 17 00:00:00 2001 From: Miki Bonacci Date: Thu, 5 Dec 2024 21:44:08 +0100 Subject: [PATCH 4/6] Optional support for aiida-atomistic We do a try/except/else import. Also, support in PwRelaxWorkChain. --- .../calculations/__init__.py | 2 +- src/aiida_quantumespresso/calculations/pw.py | 10 ++++-- .../parsers/parse_raw/base.py | 32 +++++++++++++------ src/aiida_quantumespresso/utils/magnetic.py | 30 ++++++++++------- .../workflows/pw/relax.py | 14 ++++++-- 5 files changed, 61 insertions(+), 27 deletions(-) diff --git a/src/aiida_quantumespresso/calculations/__init__.py b/src/aiida_quantumespresso/calculations/__init__.py index a3733179..9947f725 100644 --- a/src/aiida_quantumespresso/calculations/__init__.py +++ b/src/aiida_quantumespresso/calculations/__init__.py @@ -128,7 +128,7 @@ def define(cls, spec): spec.input('metadata.options.input_filename', valid_type=str, default=cls._DEFAULT_INPUT_FILE) spec.input('metadata.options.output_filename', valid_type=str, default=cls._DEFAULT_OUTPUT_FILE) spec.input('metadata.options.withmpi', valid_type=bool, default=True) # Override default withmpi=False - spec.input('structure', valid_type=(structures_classes), + spec.input('structure', valid_type=structures_classes, help='The input structure.') spec.input('parameters', valid_type=orm.Dict, help='The input parameters that are to be used to construct the input file.') diff --git a/src/aiida_quantumespresso/calculations/pw.py b/src/aiida_quantumespresso/calculations/pw.py index 9df223f8..91536b08 100644 --- a/src/aiida_quantumespresso/calculations/pw.py +++ b/src/aiida_quantumespresso/calculations/pw.py @@ -4,13 +4,19 @@ import warnings from aiida import orm +from aiida.common import exceptions from aiida.common.lang import classproperty from aiida.orm import StructureData as LegacyStructureData from aiida.plugins import factories from aiida_quantumespresso.calculations import BasePwCpInputGenerator -StructureData = factories.DataFactory('atomistic.structure') +try: + StructureData = factories.DataFactory('atomistic.structure') +except exceptions.MissingEntryPointError: + structures_classes = (LegacyStructureData,) +else: + structures_classes = (LegacyStructureData, StructureData) class PwCalculation(BasePwCpInputGenerator): @@ -72,7 +78,7 @@ def define(cls, spec): 'will not fail if the XML file is missing in the retrieved folder.') spec.input('kpoints', valid_type=orm.KpointsData, help='kpoint mesh or kpoint path') - spec.input('hubbard_file', valid_type=(StructureData, LegacyStructureData), required=False, + spec.input('hubbard_file', valid_type=structures_classes, required=False, help='SinglefileData node containing the output Hubbard parameters from a HpCalculation') spec.inputs.validator = cls.validate_inputs diff --git a/src/aiida_quantumespresso/parsers/parse_raw/base.py b/src/aiida_quantumespresso/parsers/parse_raw/base.py index a77f5307..fb1ef77a 100644 --- a/src/aiida_quantumespresso/parsers/parse_raw/base.py +++ b/src/aiida_quantumespresso/parsers/parse_raw/base.py @@ -2,10 +2,15 @@ """A basic parser for the common format of QE.""" import re -from aiida.plugins import DataFactory from aiida.orm import StructureData as LegacyStructureData +from aiida.plugins import DataFactory -from aiida_atomistic import StructureDataMutable, StructureData +try: + StructureData = DataFactory('atomistic.structure') +except exceptions.MissingEntryPointError: + structures_classes = (LegacyStructureData,) +else: + structures_classes = (LegacyStructureData, StructureData) __all__ = ('convert_qe_time_to_sec', 'convert_qe_to_aiida_structure', 'convert_qe_to_kpoints') @@ -53,14 +58,23 @@ def convert_qe_to_aiida_structure(output_dict, input_structure=None): # Without an input structure, try to recreate the structure from the output if not input_structure: - structure = StructureDataMutable() - structure.set_cell=cell_dict['lattice_vectors'] + if isinstance(input_structure, LegacyStructureData): + structure = LegacyStructureData() + structure.set_cell=cell_dict['lattice_vectors'] + + for kind_name, position in output_dict['atoms']: + symbol = re.sub(r'\d+', '', kind_name) + structure.append_atom(position=position, symbols=symbol, name=kind_name) + + else: + structure = StructureDataMutable() + structure.set_cell=cell_dict['lattice_vectors'] + + for kind_name, position in output_dict['atoms']: + symbol = re.sub(r'\d+', '', kind_name) + structure.append_atom(positions=position, symbols=symbol, kinds=kind_name) - for kind_name, position in output_dict['atoms']: - symbol = re.sub(r'\d+', '', kind_name) - structure.add_atom(position=position, symbols=symbol, name=kind_name) - - return StructureData.from_mutable(structure) + structure = StructureData.from_mutable(structure) else: diff --git a/src/aiida_quantumespresso/utils/magnetic.py b/src/aiida_quantumespresso/utils/magnetic.py index c301c519..9cdd634e 100644 --- a/src/aiida_quantumespresso/utils/magnetic.py +++ b/src/aiida_quantumespresso/utils/magnetic.py @@ -1,27 +1,31 @@ # -*- coding: utf-8 -*- """Utility class for handling the :class:`aiida_quantumespresso.data.hubbard_structure.HubbardStructureData`.""" # pylint: disable=no-name-in-module -from itertools import product -import os -from typing import Tuple, Union from aiida import orm +from aiida.common.exceptions import MissingEntryPointError from aiida.engine import calcfunction from aiida.orm import StructureData as LegacyStructureData from aiida.plugins import DataFactory -import numpy as np -StructureData = DataFactory('atomistic.structure') +try: + StructureData = DataFactory('atomistic.structure') +except MissingEntryPointError: + structures_classes = (LegacyStructureData,) +else: + structures_classes = (LegacyStructureData, StructureData) -class MagneticUtils: +class MagneticUtils: # pylint: disable=too-few-public-methods """Class to manage the magnetic structure of the atomistic `LegacyStructureData`. - It contains methods to manipulate the magnetic structure in such a way to produce the correct input for QuantumESPRESSO calculations. + + It contains methods to manipulate the magne tic structure in such a way to produce + the correct input for QuantumESPRESSO calculations. """ def __init__( self, - structure: StructureData, + structure: structures_classes, ): """Set a the `StructureData` to manipulate.""" if isinstance(structure, StructureData): @@ -33,6 +37,7 @@ def __init__( def generate_magnetic_namelist(self, parameters): """Generate the magnetic namelist for Quantum ESPRESSO. + :param parameters: dictionary of inputs for the Quantum ESPRESSO calculation. """ if 'nspin' not in parameters['SYSTEM'] and 'noncolin' not in parameters['SYSTEM']: @@ -54,15 +59,16 @@ def generate_magnetic_namelist(self, parameters): ) elif parameters['SYSTEM']['noncolin']: for site in self.structure.sites: - for variable in namelist.keys(): - namelist[variable][site.kinds] = site.get_magmom_coord(coord='spherical')[variable] + for variable, value in namelist.items(): + value[site.kinds] = site.get_magmom_coord(coord='spherical')[variable] return namelist @calcfunction -def generate_structure_with_magmoms(input_structure=StructureData, input_magnetic_moments=orm.List): +def generate_structure_with_magmoms(input_structure: structures_classes, input_magnetic_moments: orm.List): """Generate a new structure with the magnetic moments for each site. + :param input_structure: the input structure to add the magnetic moments. :param input_magnetic_moments: the magnetic moments for each site, represented as a float (see below). @@ -87,4 +93,4 @@ def generate_structure_with_magmoms(input_structure=StructureData, input_magneti output_structure = StructureData.from_mutable(mutable_structure, detect_kinds=True) - return output_structure \ No newline at end of file + return output_structure diff --git a/src/aiida_quantumespresso/workflows/pw/relax.py b/src/aiida_quantumespresso/workflows/pw/relax.py index 2888a3e3..3fd16b30 100644 --- a/src/aiida_quantumespresso/workflows/pw/relax.py +++ b/src/aiida_quantumespresso/workflows/pw/relax.py @@ -4,7 +4,8 @@ from aiida.common import AttributeDict, exceptions from aiida.common.lang import type_check from aiida.engine import ToContext, WorkChain, append_, if_, while_ -from aiida.plugins import CalculationFactory, WorkflowFactory +from aiida.orm import StructureData as LegacyStructureData +from aiida.plugins import CalculationFactory, DataFactory, WorkflowFactory from aiida_quantumespresso.common.types import RelaxType from aiida_quantumespresso.utils.mapping import prepare_process_inputs @@ -14,6 +15,13 @@ PwCalculation = CalculationFactory('quantumespresso.pw') PwBaseWorkChain = WorkflowFactory('quantumespresso.pw.base') +try: + StructureData = DataFactory('atomistic.structure') +except exceptions.MissingEntryPointError: + structures_classes = (LegacyStructureData,) +else: + structures_classes = (LegacyStructureData, StructureData) + def validate_inputs(inputs, _): """Validate the top level namespace.""" @@ -38,7 +46,7 @@ def define(cls, spec): exclude=('clean_workdir', 'pw.structure', 'pw.parent_folder'), namespace_options={'required': False, 'populate_defaults': False, 'help': 'Inputs for the `PwBaseWorkChain` for the final scf.'}) - spec.input('structure', valid_type=orm.StructureData, help='The inputs structure.') + spec.input('structure', valid_type=structures_classes, help='The inputs structure.') spec.input('meta_convergence', valid_type=orm.Bool, default=lambda: orm.Bool(True), help='If `True` the workchain will perform a meta-convergence on the cell volume.') spec.input('max_meta_convergence_iterations', valid_type=orm.Int, default=lambda: orm.Int(5), @@ -65,7 +73,7 @@ def define(cls, spec): spec.exit_code(402, 'ERROR_SUB_PROCESS_FAILED_FINAL_SCF', message='the final scf PwBaseWorkChain sub process failed') spec.expose_outputs(PwBaseWorkChain, exclude=('output_structure',)) - spec.output('output_structure', valid_type=orm.StructureData, required=False, + spec.output('output_structure', valid_type=structures_classes, required=False, help='The successfully relaxed structure.') # yapf: enable From 1dfd4ed54f89e670e7cbe16ae59f46a76ab0a91e Mon Sep 17 00:00:00 2001 From: Miki Bonacci Date: Thu, 5 Dec 2024 21:48:27 +0100 Subject: [PATCH 5/6] introducing support in bands.py --- src/aiida_quantumespresso/workflows/pw/bands.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/aiida_quantumespresso/workflows/pw/bands.py b/src/aiida_quantumespresso/workflows/pw/bands.py index d4d0a32e..5db883b4 100644 --- a/src/aiida_quantumespresso/workflows/pw/bands.py +++ b/src/aiida_quantumespresso/workflows/pw/bands.py @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- """Workchain to compute a band structure for a given structure using Quantum ESPRESSO pw.x.""" from aiida import orm -from aiida.common import AttributeDict +from aiida.common import AttributeDict, exceptions from aiida.engine import ToContext, WorkChain, if_ +from aiida.orm import StructureData as LegacyStructureData +from aiida.plugins import DataFactory from aiida_quantumespresso.calculations.functions.seekpath_structure_analysis import seekpath_structure_analysis from aiida_quantumespresso.utils.mapping import prepare_process_inputs @@ -11,6 +13,13 @@ from ..protocols.utils import ProtocolMixin +try: + StructureData = DataFactory('atomistic.structure') +except exceptions.MissingEntryPointError: + structures_classes = (LegacyStructureData,) +else: + structures_classes = (LegacyStructureData, StructureData) + def validate_inputs(inputs, ctx=None): # pylint: disable=unused-argument """Validate the inputs of the entire input namespace.""" @@ -61,7 +70,7 @@ def define(cls, spec): spec.expose_inputs(PwBaseWorkChain, namespace='bands', exclude=('clean_workdir', 'pw.structure', 'pw.kpoints', 'pw.kpoints_distance', 'pw.parent_folder'), namespace_options={'help': 'Inputs for the `PwBaseWorkChain` for the BANDS calculation.'}) - spec.input('structure', valid_type=orm.StructureData, help='The inputs structure.') + spec.input('structure', valid_type=structures_classes, help='The inputs structure.') spec.input('clean_workdir', valid_type=orm.Bool, default=lambda: orm.Bool(False), help='If `True`, work directories of all called calculation will be cleaned at the end of execution.') spec.input('nbands_factor', valid_type=orm.Float, required=False, @@ -97,7 +106,7 @@ def define(cls, spec): message='The scf PwBasexWorkChain sub process failed') spec.exit_code(403, 'ERROR_SUB_PROCESS_FAILED_BANDS', message='The bands PwBasexWorkChain sub process failed') - spec.output('primitive_structure', valid_type=orm.StructureData, + spec.output('primitive_structure', valid_type=structures_classes, required=False, help='The normalized and primitivized structure for which the bands are computed.') spec.output('seekpath_parameters', valid_type=orm.Dict, From 94b6d8881675abf78fea558259f186d32b417c23 Mon Sep 17 00:00:00 2001 From: Miki Bonacci Date: Thu, 5 Dec 2024 22:37:46 +0100 Subject: [PATCH 6/6] Structure_classes tuple instead of explicit tuple of types for output structure of pw calcjob. --- .../functions/seekpath_structure_analysis.py | 13 ++++++++++++- src/aiida_quantumespresso/calculations/pw.py | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/aiida_quantumespresso/calculations/functions/seekpath_structure_analysis.py b/src/aiida_quantumespresso/calculations/functions/seekpath_structure_analysis.py index 5d89dc64..1af3fdcc 100644 --- a/src/aiida_quantumespresso/calculations/functions/seekpath_structure_analysis.py +++ b/src/aiida_quantumespresso/calculations/functions/seekpath_structure_analysis.py @@ -1,10 +1,18 @@ # -*- coding: utf-8 -*- """Calcfunction to primitivize a structure and return high symmetry k-point path through its Brillouin zone.""" +from aiida.common import exceptions from aiida.engine import calcfunction from aiida.orm import Data +from aiida.plugins import DataFactory from aiida_quantumespresso.data.hubbard_structure import HubbardStructureData +try: + StructureData = DataFactory('atomistic.structure') + HAS_ATOMISTIC = True +except exceptions.MissingEntryPointError: + HAS_ATOMISTIC = False + @calcfunction def seekpath_structure_analysis(structure, **kwargs): @@ -32,7 +40,10 @@ def seekpath_structure_analysis(structure, **kwargs): result = get_explicit_kpoints_path(structure, **unwrapped_kwargs) - if isinstance(structure, HubbardStructureData): + if HAS_ATOMISTIC: + if isinstance(structure, StructureData): + raise NotImplementedError('This function does not yet support the conversion into atomistic instances.') + elif isinstance(structure, HubbardStructureData): result['primitive_structure'] = update_structure_with_hubbard(result['primitive_structure'], structure) result['conv_structure'] = update_structure_with_hubbard(result['conv_structure'], structure) diff --git a/src/aiida_quantumespresso/calculations/pw.py b/src/aiida_quantumespresso/calculations/pw.py index 91536b08..1d44f8f0 100644 --- a/src/aiida_quantumespresso/calculations/pw.py +++ b/src/aiida_quantumespresso/calculations/pw.py @@ -84,7 +84,7 @@ def define(cls, spec): spec.output('output_parameters', valid_type=orm.Dict, help='The `output_parameters` output node of the successful calculation.') - spec.output('output_structure', valid_type=(StructureData, LegacyStructureData), required=False, + spec.output('output_structure', valid_type=structures_classes, required=False, help='The `output_structure` output node of the successful calculation if present.') spec.output('output_trajectory', valid_type=orm.TrajectoryData, required=False) spec.output('output_band', valid_type=orm.BandsData, required=False,