From 355020ca9ea63db0803ecc5746f93a879f488696 Mon Sep 17 00:00:00 2001 From: Lorenzo <79980269+bastonero@users.noreply.github.com> Date: Thu, 6 Apr 2023 12:04:41 +0200 Subject: [PATCH] Add the `HubbardStructureData` data plugin (#849) Quantum ESPRESSO implements a new syntax for Hubbard parameters starting from `v7.1` onwards, see the following URL for details: https://gitlab.com/QEF/q-e/-/commit/39a77cecc02e6a43ca33e The `pw.x` now needs a new `CARD` in the input file. The following new modules are added: - `common.hubbard`: Contains a code, structure agnostic data class describing Hubbard information, such as parameters, projectors and formulation. - `data.hubbard_structure`: Contains a new code agnostic data plugin, the `HubbardStructureData`, for storing Hubbard information along with its associated `StructureData` in the provenance graph. - `utils.hubbard`: A specific, dedicated QuantumESPRESSO utility module for handling and manipulating `HubbardStructureData` nodes. The new data class and plugin implement Hubbard information in a code agnostic manner, i.e. not specific to Quantum ESPRESSO, in the hope that they can be shared with plugins for other codes that also support Hubbard corrections, and they have no constraints for our particular plugin implementation. The conversion of the abstract Hubbard parameters to Quantum ESPRRESSO input is handled through the `utils.hubbard` module. Co-authored-by: Marnik Bercx Co-authored-by: Sebastiaan Huber --- pyproject.toml | 4 +- .../calculations/__init__.py | 8 + src/aiida_quantumespresso/common/__init__.py | 2 + src/aiida_quantumespresso/common/hubbard.py | 161 ++++++++ .../data/hubbard_structure.py | 225 +++++++++++ src/aiida_quantumespresso/utils/hubbard.py | 353 ++++++++++++++++++ tests/common/__init__.py | 2 + tests/common/test_hubbard.py | 157 ++++++++ tests/conftest.py | 8 +- tests/data/__init__.py | 0 tests/data/test_hubbard_structure.py | 150 ++++++++ tests/utils/fixtures/hubbard/HUBBARD.dat | 3 + tests/utils/fixtures/hubbard/HUBBARD_2.dat | 4 + tests/utils/fixtures/hubbard/HUBBARD_3.dat | 5 + tests/utils/test_hubbard.py | 237 ++++++++++++ 15 files changed, 1315 insertions(+), 4 deletions(-) create mode 100644 src/aiida_quantumespresso/common/hubbard.py create mode 100644 src/aiida_quantumespresso/data/hubbard_structure.py create mode 100644 src/aiida_quantumespresso/utils/hubbard.py create mode 100644 tests/common/__init__.py create mode 100644 tests/common/test_hubbard.py create mode 100644 tests/data/__init__.py create mode 100644 tests/data/test_hubbard_structure.py create mode 100644 tests/utils/fixtures/hubbard/HUBBARD.dat create mode 100644 tests/utils/fixtures/hubbard/HUBBARD_2.dat create mode 100644 tests/utils/fixtures/hubbard/HUBBARD_3.dat create mode 100644 tests/utils/test_hubbard.py diff --git a/pyproject.toml b/pyproject.toml index 4daf4f057..0bc952d52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ dependencies = [ 'importlib_resources', 'jsonschema', 'numpy', + 'pydantic', 'packaging', 'qe-tools~=2.0', 'xmlschema~=1.2,>=1.2.5' @@ -86,6 +87,7 @@ aiida-quantumespresso = 'aiida_quantumespresso.cli:cmd_root' [project.entry-points.'aiida.data'] 'quantumespresso.force_constants' = 'aiida_quantumespresso.data.force_constants:ForceConstantsData' +'quantumespresso.hubbard_structure' = 'aiida_quantumespresso.data.hubbard_structure:HubbardStructureData' [project.entry-points.'aiida.parsers'] 'quantumespresso.cp' = 'aiida_quantumespresso.parsers.cp:CpParser' @@ -150,7 +152,7 @@ ignore = [ ] [tool.pylint.master] -load-plugins = ['pylint_aiida'] +load-plugins = ['pylint_aiida','pylint.extensions.no_self_use'] [tool.pylint.format] max-line-length = 120 diff --git a/src/aiida_quantumespresso/calculations/__init__.py b/src/aiida_quantumespresso/calculations/__init__.py index 381fe22f7..1f110ba62 100644 --- a/src/aiida_quantumespresso/calculations/__init__.py +++ b/src/aiida_quantumespresso/calculations/__init__.py @@ -15,7 +15,9 @@ from aiida.plugins import DataFactory from qe_tools.converters import get_parameters_from_cell +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 .base import CalcJob from .helpers import QEInputValidationError @@ -685,6 +687,10 @@ 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 + # =================== NAMELISTS AND CARDS ======================== try: namelists_toprint = settings.pop('NAMELISTS') @@ -728,6 +734,8 @@ def _generate_PWCPinputdata(cls, parameters, settings, pseudos, structure, kpoin if cls._use_kpoints: inputfile += kpoints_card inputfile += cell_parameters_card + if hubbard_card is not None: + inputfile += hubbard_card # Generate additional cards bases on input parameters and settings that are subclass specific tail = cls._generate_PWCP_input_tail(input_params=input_params, settings=settings) diff --git a/src/aiida_quantumespresso/common/__init__.py b/src/aiida_quantumespresso/common/__init__.py index e69de29bb..db88cc449 100644 --- a/src/aiida_quantumespresso/common/__init__.py +++ b/src/aiida_quantumespresso/common/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Common modules to be shared with other aiida plugins.""" diff --git a/src/aiida_quantumespresso/common/hubbard.py b/src/aiida_quantumespresso/common/hubbard.py new file mode 100644 index 000000000..2682a18ab --- /dev/null +++ b/src/aiida_quantumespresso/common/hubbard.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- +"""Utility class and functions for HubbardStructureData.""" +# pylint: disable=no-name-in-module, invalid-name +from typing import List, Literal, Tuple + +from pydantic import BaseModel, conint, constr, validator + +__all__ = ('HubbardParameters', 'Hubbard') + + +class HubbardParameters(BaseModel): + """Class for describing onsite and intersite Hubbard interaction parameters. + + .. note: allowed manifold formats are: + * {N}{L} (2 characters) + * {N1}{L1}-{N2}{L2} (5 characters) + + N = quantum number (1,2,3,...); L = orbital letter (s,p,d,f,g,h) + """ + + atom_index: conint(strict=True, ge=0) + """Atom index in the abstract structure.""" + + atom_manifold: constr(strip_whitespace=True, to_lower=True, min_length=2, max_length=5) + """Atom manifold (syntax is `3d`, `3d-2p`).""" + + neighbour_index: conint(strict=True, ge=0) + """Neighbour index in the abstract structure.""" + + neighbour_manifold: constr(strip_whitespace=True, to_lower=True, min_length=2, max_length=5) + """Atom manifold (syntax is `3d`, `3d-2p`).""" + + translation: Tuple[conint(strict=True), conint(strict=True), conint(strict=True)] + """Translation vector referring to the neighbour atom, (3,) shape list of ints.""" + + value: float + """Value of the Hubbard parameter, expessed in eV.""" + + hubbard_type: Literal['Ueff', 'U', 'V', 'J', 'B', 'E2', 'E3'] + """Type of the Hubbard parameters used (`Ueff`, `U`, `V`, `J`, `B`, `E2`, `E3`).""" + + @validator('atom_manifold', 'neighbour_manifold') # cls is mandatory to use + def check_manifolds(cls, value): # pylint: disable=no-self-argument, no-self-use + """Check the validity of the manifold input. + + Allowed formats are: + * {N}{L} (2 characters) + * {N1}{L1}-{N2}{L2} (5 characters) + + N = quantum number (1,2,3,...); L = orbital letter (s,p,d,f,g,h) + """ + length = len(value) + if length not in [2, 5]: + raise ValueError(f'invalid length ``{length}``. Only 2 or 5.') + if length == 2: + if not value[0] in [str(_ + 1) for _ in range(6)]: + raise ValueError(f'invalid quantum number {value[0]}') + if not value[1] in ['s', 'p', 'd', 'f', 'h']: + raise ValueError(f'invalid manifold symbol {value[1]}') + if length == 5: + if not value[2] == '-': + raise ValueError(f'the separator {value[0]} is not allowed. Only `-`') + if not value[3] in [str(_ + 1) for _ in range(6)]: + raise ValueError(f'the quantum number {value[0]} is not correct') + if not value[4] in ['s', 'p', 'd', 'f', 'h']: + raise ValueError(f'the manifold number {value[1]} is not correct') + return value + + def to_tuple(self) -> Tuple[int, str, int, str, float, Tuple[int, int, int], str]: + """Return the parameters as a tuple. + + The parameters have the following order: + * atom_index + * atom_manifold + * neighbour_index + * neighbour_manifold + * value + * translationr + * hubbard_type + """ + return ( + self.atom_index, self.atom_manifold, self.neighbour_index, self.neighbour_manifold, self.value, + self.translation, self.hubbard_type + ) + + @staticmethod + def from_tuple(hubbard_parameters: Tuple[int, str, int, str, float, Tuple[int, int, int], str]): + """Return a :meth:`~aiida_quantumespresso.common.hubbard.HubbardParameters` instance from a list. + + The parameters within the list must have the following order: + * atom_index + * atom_manifold + * neighbour_index + * neighbour_manifold + * value + * translation + * hubbard_type + """ + keys = [ + 'atom_index', + 'atom_manifold', + 'neighbour_index', + 'neighbour_manifold', + 'value', + 'translation', + 'hubbard_type', + ] + return HubbardParameters(**dict(zip(keys, hubbard_parameters))) + + +class Hubbard(BaseModel): + """Class for complete description of Hubbard interactions.""" + + parameters: List[HubbardParameters] + """List of :meth:`~aiida_quantumespress.common.hubbard.HubbardParameters`.""" + + projectors: Literal['atomic', + 'ortho-atomic', + 'norm-atomic', + 'wannier-functions', + 'pseudo-potentials', + ] = 'ortho-atomic' + """Name of the projectors used. Allowed values are: + 'atomic', 'ortho-atomic', 'norm-atomic', 'wannier-functions', 'pseudo-potentials'.""" + + formulation: Literal['dudarev', 'liechtenstein'] = 'dudarev' + """Hubbard formulation used. Allowed values are: 'dudarev', `liechtenstein`.""" + + def to_list(self) -> List[Tuple[int, str, int, str, float, Tuple[int, int, int], str]]: + """Return the Hubbard `parameters` as a list of lists. + + The parameters have the following order within each list: + * atom_index + * atom_manifold + * neighbour_index + * neighbour_manifold + * value + * translation + * hubbard_type + """ + return [params.to_tuple() for params in self.parameters] + + @staticmethod + def from_list( + parameters: List[Tuple[int, str, int, str, float, Tuple[int, int, int], str]], + projectors: str = 'ortho-atomic', + formulation: str = 'dudarev', + ): + """Return a :meth:`~aiida_quantumespresso.common.hubbard.Hubbard` instance from a list of tuples. + + Each list must contain the hubbard parameters in the following order: + * atom_index + * atom_manifold + * neighbour_index + * neighbour_manifold + * value + * translation + * hubbard_type + """ + parameters = [HubbardParameters.from_tuple(value) for value in parameters] + return Hubbard(parameters=parameters, projectors=projectors, formulation=formulation) diff --git a/src/aiida_quantumespresso/data/hubbard_structure.py b/src/aiida_quantumespresso/data/hubbard_structure.py new file mode 100644 index 000000000..71a2fe32f --- /dev/null +++ b/src/aiida_quantumespresso/data/hubbard_structure.py @@ -0,0 +1,225 @@ +# -*- coding: utf-8 -*- +"""Data plugin that represents a crystal structure with Hubbard parameters.""" +import json +from typing import List, Tuple, Union + +from aiida.orm import StructureData +import numpy as np + +from aiida_quantumespresso.common.hubbard import Hubbard, HubbardParameters + +__all__ = ('HubbardStructureData',) + + +class HubbardStructureData(StructureData): + """Structure data containing code agnostic info on Hubbard parameters.""" + + _hubbard_filename = 'hubbard.json' + + def __init__( + self, + cell: List[List[float]], + sites: List[Tuple[str, str, Tuple[float, float, float]]], + pbc: Tuple[bool, bool, bool] = (True, True, True), + hubbard: Hubbard = None, + **kwargs, + ): + """Set a ``HubbardStructureData`` instance. + + :param cell: (3,3) shape list of floats + :param pbc: (3,) shape list in bools + :param sites: list of lists, each of the shape [symbol, name, position], + where position is a (3,) shape list of floats + :param hubbard: a :py:meth:`~aiida_quantumespresso.common.hubbrd.Hubbard` istance + """ + super().__init__(cell=cell, **kwargs) + self.sites = sites + self.pbc = pbc + self.hubbard = Hubbard(parameters=[]) if hubbard is None else hubbard + + @property + def sites(self): + """Return the :meth:`aiida.core.Sites`.""" + return super().sites + + @sites.setter + def sites(self, values: List[Tuple[str, str, Tuple[float, float, float]]]): + """Set the :meth:`aiida.core.Sites`. + + :param values: list of sites, each as [symbol, name, (3,) shape list of positions] + """ + self.clear_sites() + for simbols, kind_name, position in values: + self.append_atom(symbols=simbols, name=kind_name, position=position) + + @property + def hubbard(self) -> Hubbard: + """Get the `Hubbard` instance. + + :returns: a :py:meth:`~aiida_quantumespresso.common.hubbard.Hubbard` instance. + """ + with self.base.repository.open(self._hubbard_filename, mode='rb') as handle: + return Hubbard.parse_raw(json.load(handle)) + + @hubbard.setter + def hubbard(self, hubbard: Hubbard): + """Set the full Hubbard information.""" + if not isinstance(hubbard, Hubbard): + raise ValueError('the input is not of type `Hubbard`') + + serialized = json.dumps(hubbard.json()) + self.base.repository.put_object_from_bytes(serialized.encode('utf-8'), self._hubbard_filename) + + @staticmethod + def from_structure( + structure: StructureData, + hubbard: Union[Hubbard, None] = None, + ): + """Return an instance of ``HubbardStructureData`` from a ``StructureData`` node. + + :param structure: :meth:`aiida.orm.StructureData` instance + :param hubbad: :meth:`~aiida_quantumespresso.common.hubbard.Hubbard` instance + :returns: ``HubbardStructureData`` instance + """ + sites = [[structure.get_kind(site.kind_name).symbol, site.kind_name, site.position] for site in structure.sites] + cell = structure.cell + pbc = structure.pbc + + return HubbardStructureData(cell=cell, pbc=pbc, sites=sites, hubbard=hubbard) + + def append_hubbard_parameter( + self, + atom_index: int, + atom_manifold: str, + neighbour_index: int, + neighbour_manifold: str, + value: float, + translation: Tuple[int, int, int] = None, + hubbard_type: str = 'Ueff', + ): + """Append a :meth:`~aiida_quantumespresso.common.hubbard.HubbardParameters``. + + :param atom_index: atom index in unitcell + :param atom_manifold: atomic manifold (e.g. 3d, 3d-2p) + :param neighbour_index: neighbouring atom index in unitcell + :param neighbour_manifold: neighbour manifold (e.g. 3d, 3d-2p) + :param value: value of the Hubbard parameter, in eV + :param translation: (3,) list of ints, describing the translation vector + associated with the neighbour atom, defaults to None + :param hubbard_type: hubbard type (U, V, J, ...), defaults to 'Ueff' + (see :meth:`~aiida_quantumespresso.common.hubbard.Hubbard` for full allowed values) + """ + pymat = self.get_pymatgen_structure() + sites = pymat.sites + + if translation is None: + _, translation = sites[atom_index].distance_and_image(sites[neighbour_index]) + translation = np.array(translation, dtype=np.int64).tolist() + + hp_tuple = (atom_index, atom_manifold, neighbour_index, neighbour_manifold, value, translation, hubbard_type) + parameters = HubbardParameters.from_tuple(hp_tuple) + hubbard = self.hubbard + + if parameters not in hubbard.parameters: + hubbard.parameters.append(parameters) + self.hubbard = hubbard + + def pop_hubbard_parameters(self, index: int): + """Pop Hubbard parameters in the list. + + :param index: index of the Hubbard parameters to pop + """ + hubbard = self.hubbard + hubbard.parameters.pop(index) + self.hubbard = hubbard + + def clear_hubbard_parameters(self): + """Clear all the Hubbard parameters.""" + hubbard = self.hubbard + hubbard.parameters = [] + self.hubbard = hubbard + + def initialize_intersites_hubbard( + self, + atom_name: str, + atom_manifold: str, + neighbour_name: str, + neighbour_manifold: str, + value: float = 1e-8, + hubbard_type: str = 'V', + use_kinds: bool = True, + ): + """Initialize and append intersite Hubbard values between an atom and its neighbour(s). + + .. note:: this only initialize the value between the first neighbour. In case + `use_kinds` is False, all the possible combination of couples having + kind name equal to symbol are initialized. + + :param atom_name: atom name in unitcell + :param atom_manifold: atomic manifold (e.g. 3d, 3d-2p) + :param neighbour_index: neighbouring atom name in unitcell + :param neighbour_manifold: neighbour manifold (e.g. 3d, 3d-2p) + :param value: value of the Hubbard parameter, in eV + :param hubbard_type: hubbard type (U, V, J, ...), defaults to 'V' + (see :meth:`~aiida_quantumespresso.common.hubbard.Hubbard` for full allowed values) + :param use_kinds: whether to use kinds for initializing the parameters; when False, it + initializes all the ``Kinds`` matching the ``atom_name`` + """ + sites = self.get_pymatgen_structure().sites + + function = self._get_one_kind_index if use_kinds else self._get_symbol_indices + atom_indices = function(atom_name) + neigh_indices = function(neighbour_name) + + if atom_indices is None or neigh_indices is None: + raise ValueError('species or kind names not in structure') + + for atom_index in atom_indices: + for neighbour_index in neigh_indices: + _, translation = sites[atom_index].distance_and_image(sites[neighbour_index]) + translation = np.array(translation, dtype=np.int64).tolist() + args = ( + atom_index, atom_manifold, neighbour_index, neighbour_manifold, value, translation, hubbard_type + ) + self.append_hubbard_parameter(*args) + + def initialize_onsites_hubbard( + self, + atom_name: str, + atom_manifold: str, + value: float = 1e-8, + hubbard_type: str = 'Ueff', + use_kinds: bool = True, + ): + """Initialize and append onsite Hubbard values of atoms with specific name. + + :param atom_name: atom name in unitcell + :param atom_manifold: atomic manifold (e.g. 3d, 3d-2p) + :param value: value of the Hubbard parameter, in eV + :param hubbard_type: hubbard type (U, J, ...), defaults to 'Ueff' + (see :meth:`~aiida_quantumespresso.common.hubbard.Hubbard` for full allowed values) + :param use_kinds: whether to use kinds for initializing the parameters; when False, it + initializes all the ``Kinds`` matching the ``atom_name`` + """ + function = self._get_one_kind_index if use_kinds else self._get_symbol_indices + atom_indices = function(atom_name) + + if atom_indices is None: + raise ValueError('species or kind names not in structure') + + for atom_index in atom_indices: + args = (atom_index, atom_manifold, atom_index, atom_manifold, value, [0, 0, 0], hubbard_type) + self.append_hubbard_parameter(*args) + + def _get_one_kind_index(self, kind_name: str) -> List[int]: + """Return the first site index matching with `kind_name`.""" + for i, site in enumerate(self.sites): + if site.kind_name == kind_name: + return [i] + + def _get_symbol_indices(self, symbol: str) -> List[int]: + """Return one site index for each kind name matching symbol.""" + site_kindnames = self.get_site_kindnames() + matching_kinds = [kind.name for kind in self.kinds if symbol in kind.symbol] + + return [site_kindnames.index(kind) for kind in matching_kinds] diff --git a/src/aiida_quantumespresso/utils/hubbard.py b/src/aiida_quantumespresso/utils/hubbard.py new file mode 100644 index 000000000..62cedacd1 --- /dev/null +++ b/src/aiida_quantumespresso/utils/hubbard.py @@ -0,0 +1,353 @@ +# -*- coding: utf-8 -*- +"""Utility class for handling the :class:`data.hubbard_structure.HubbardStructureData`.""" +# pylint: disable=no-name-in-module +from itertools import product +import os +from typing import List, Tuple, Union + +from aiida.orm import StructureData + +from aiida_quantumespresso.common.hubbard import Hubbard +from aiida_quantumespresso.data.hubbard_structure import HubbardStructureData + +__all__ = ( + 'HubbardUtils', + 'get_supercell_atomic_index', + 'get_index_and_translation', + 'get_hubbard_indices', + 'is_intersite_hubbard', +) + +QE_TRANSLATIONS = list(list(item) for item in product((-1, 0, 1), repeat=3)) +first = QE_TRANSLATIONS.pop(13) +QE_TRANSLATIONS.insert(0, first) +QE_TRANSLATIONS = tuple(tuple(item) for item in QE_TRANSLATIONS) + + +class HubbardUtils: + """Utility class for handling `HubbardStructureData` for QuantumESPRESSO.""" + + def __init__( + self, + hubbard_structure: HubbardStructureData, + ): + """Set a the `HubbardStructureData` to manipulate.""" + if isinstance(hubbard_structure, HubbardStructureData): + self._hubbard_structure = hubbard_structure + else: + raise ValueError('input is not of type `HubbardStructureData') + + @property + def hubbard_structure(self) -> HubbardStructureData: + """Return the HubbardStructureData.""" + return self._hubbard_structure + + def get_hubbard_card(self) -> str: + """Return QuantumESPRESSO `HUBBARD` input card for `pw.x`.""" + hubbard = self.hubbard_structure.hubbard + hubbard_parameters = hubbard.parameters + sites = self.hubbard_structure.sites + natoms = len(sites) + + lines = [f'HUBBARD\t{hubbard.projectors}\n'] + + for param in hubbard_parameters: + atom_i = sites[param.atom_index].kind_name + atom_j = sites[param.neighbour_index].kind_name + index_i = param.atom_index + 1 # QE indices start from 1 + index_j = get_supercell_atomic_index(param.neighbour_index, natoms, param.translation) + 1 + man_i = param.atom_manifold + man_j = param.neighbour_manifold + value = param.value + + if hubbard.formulation not in ['dudarev', 'liechtenstein']: + raise ValueError(f'Hubbard formulation {hubbard.formulation} is not implemented.') + + if hubbard.formulation == 'liechtenstein': + line = f'{pre}\t{atom_i}-{man_i} \t{value}' + + # This variable is to meet QE implementation. If intersite interactions + # (+V) are present, onsite parameters might not be relabelled by the ``hp.x`` + # code, causing a subsequent ``pw.x`` calculation to crash. That is, + # we need to avoid writing "U Co-3d 5.0", but instead "V Co-3d Co-3d 1 1 5.0". + is_intersite = is_intersite_hubbard(hubbard=hubbard) + if hubbard.formulation == 'dudarev': + if param.hubbard_type == 'J': + pre = 'J' + elif not is_intersite and atom_i == atom_j and param.atom_manifold == param.neighbour_manifold: + pre = 'U' + else: + pre = 'V' + + if pre in ['U', 'J']: + line = f'{pre}\t{atom_i}-{man_i}\t{value}' + else: + line = f'{pre}\t{atom_i}-{man_i}\t{atom_j}-{man_j}\t{index_i}\t{index_j}\t{value}' + + line += '\n' + if line not in lines: + lines.append(line) + + return ' '.join(lines) + + def parse_hubbard_dat(self, filepath: Union[str, os.PathLike]): + """Parse the `HUBBARD.dat` of QuantumESPRESSO file associated to the current structure. + + This function is needed for parsing the HUBBARD.dat file generated in a `hp.x` calculation. + + .. note:: overrides current Hubbard information. + + :param filepath: the filepath of the *HUBBARD.dat* to parse + """ + self.hubbard_structure.clear_hubbard_parameters() + natoms = len(self.hubbard_structure.sites) + hubbard_data = [] + + with open(filepath, encoding='utf-8') as handle: + lines = handle.readlines() + for line in lines: + if line.strip().split()[0] != '#': + hubbard_data.append(list(line.strip().split())) + + projectors = hubbard_data.pop(0)[1] + if projectors.startswith(('(', '[', '{')): + projectors = projectors[1:-1] + + # Samples of parsed array are: + # ['U', 'Co-3d', '6.0'] + # ['V', 'Co-3d', 'O-2p', '1', '4', '6.0'] + for data in hubbard_data: + + if data[0] == 'U': + manifold = data[1].split('-') + index = int(self.hubbard_structure._get_one_kind_index(manifold.pop(0))[0]) # pylint: disable=protected-access + manifold = '-'.join(manifold) + args = (index, manifold, index, manifold, float(data[2]), (0, 0, 0), 'U') + else: + manifolds = [] + for i in [1, 2]: + manifold = data[i].split('-') + manifold.pop(0) # removing atom name + manifolds.append('-'.join(manifold)) + + # -1 because QE index starts from 1 + index_i, _ = get_index_and_translation(int(data[3]) - 1, natoms) + index_j, tra = get_index_and_translation(int(data[4]) - 1, natoms) + + args = (index_i, manifolds[0], index_j, manifolds[1], float(data[5]), tuple(tra), data[0]) + + self.hubbard_structure.append_hubbard_parameter(*args) + + hubbard = self.hubbard_structure.hubbard + hubbard.projectors = projectors + self.hubbard_structure.hubbard = hubbard + + def get_hubbard_file(self) -> str: + """Return QuantumESPRESSO ``parameters.in`` data for ``pw.x```.""" + hubbard = self.hubbard_structure.hubbard + hubbard_parameters = hubbard.parameters + sites = self.hubbard_structure.sites + natoms = len(sites) + + if not hubbard.formulation == 'dudarev': + raise ValueError('only `dudarev` formulation is implemented') + + card = '#\tAtom 1\tAtom 2\tHubbard V (eV)\n' + + for param in hubbard_parameters: + index_i = param.atom_index + 1 # QE indices start from 1 + index_j = get_supercell_atomic_index(param.neighbour_index, natoms, param.translation) + 1 + value = param.value + + line = f'\t{index_i}\t{index_j}\t{value}' + line += '\n' + card += line + + return card + + def reorder_atoms(self): + """Reorder the atoms with with the kinds in the right order necessary for an ``hp.x`` calculation. + + An ``HpCalculation`` which restarts from a completed ``PwCalculation``, requires that the all + Hubbard atoms appear first in the atomic positions card of the ``PwCalculation`` input file. + This order is based on the order of the kinds in the structure. + So a suitable structure has all Hubbard kinds in the begining of kinds list. + + .. note:: overrides current ``HubbardStructureData`` + """ + from copy import deepcopy + + structure = self.hubbard_structure # current + reordered = structure.clone() # to be set at the end + reordered.clear_kinds() + + hubbard = structure.hubbard.copy() + parameters = hubbard.to_list() + + sites = structure.sites + indices = get_hubbard_indices(hubbard=hubbard) + hubbard_kinds = list(set(sites[index].kind_name for index in indices)) + hubbard_kinds.sort(reverse=False) + + ordered_sites = [] + + # We define a map from ``index`` to ``site specifications``. We need the complete + # specification, as we will loose track of the index ordering with the following shuffle. + # The ``index`` are needed for re-indexing later the hubbard parameters. + index_map = {index: site.get_raw() for index, site in enumerate(sites) if site.kind_name in hubbard_kinds} + + while hubbard_kinds: + + hubbard_kind = hubbard_kinds.pop() + hubbard_sites = [s for s in sites if s.kind_name == hubbard_kind] + remaining_sites = [s for s in sites if not s.kind_name == hubbard_kind] + + ordered_sites.extend(hubbard_sites) + sites = remaining_sites + + # Extend the current site list with the remaining non-hubbard sites + ordered_sites.extend(sites) + + for site in ordered_sites: + + if site.kind_name not in reordered.get_kind_names(): + kind = structure.get_kind(site.kind_name) + reordered.append_kind(kind) + + reordered.append_site(site) + + reordered_parameters = [] + + # Reordered site map, to match with raw sites of ``index_map`` + site_map = [site.get_raw() for site in reordered.sites] + + for parameter in parameters: + new_parameter = list(deepcopy(parameter)) + new_parameter[0] = site_map.index(index_map[parameter[0]]) # atom index + new_parameter[2] = site_map.index(index_map[parameter[2]]) # neighbour index + reordered_parameters.append(new_parameter) + + # making sure we keep track of the other info as well + args = (reordered_parameters, hubbard.projectors, hubbard.formulation) + hubbard = hubbard.from_list(*args) + reordered.hubbard = hubbard + + self._hubbard_structure = reordered + + def is_to_reorder(self) -> bool: + """Return whether the atoms should be reordered for an ``hp.x`` calculation.""" + indices = get_hubbard_indices(self.hubbard_structure.hubbard) + indices.sort() + + return indices != list(range(len(indices))) + + def get_hubbard_for_supercell(self, supercell: StructureData, thr: float = 1e-3) -> HubbardStructureData: + """Return the ``HubbbardStructureData`` for a supercell. + + .. note:: the two structure need to be commensurate (no rigid rotations) + + .. warning:: **always check** that the energy calculation of a pristine supercell + structure obtained through this method is the same as the unitcell (within numerical noise) + + :returns: a new ``HubbbardStructureData`` with all the mapped Hubbard parameters + """ + import numpy as np + + uc_pymat = self.hubbard_structure.get_pymatgen_structure() + sc_pymat = supercell.get_pymatgen_structure() + uc_positions = uc_pymat.cart_coords # positions in Cartesian coordinates + sc_positions = sc_pymat.cart_coords + uc_cell = uc_pymat.lattice.matrix + uc_cell_inv = np.linalg.inv(uc_cell) + + hubbard = self.hubbard_structure.hubbard + sc_hubbard_parameters = [] + + # Dumb, but fairly safe, way to map all the hubbard parameters. + # The idea is to map for each interaction in unitcell the + # correspective one in supercell matching all the positions. + for param in hubbard.parameters: + # i -> atom_index | j -> neighbour_index + uc_i_position = uc_positions[param.atom_index] + uc_j_position = uc_positions[param.neighbour_index] + sc_i_indices, sc_j_index, sc_i_translations = [], [], [] + + # Each atom in supercell is matched if a unitcell + # translation vector is found. + for i, position in enumerate(sc_positions): + translation = np.dot(position - uc_i_position, uc_cell_inv) + translation_int = np.rint(translation) + if np.all(np.isclose(translation, translation_int, thr)): + sc_i_translations.append(translation_int.tolist()) + sc_i_indices.append(i) + + translation = np.dot(position - uc_j_position, uc_cell_inv) + translation_int = np.rint(translation) + if np.all(np.isclose(translation, translation_int, thr)): + uc_j_translation = np.array(translation_int) + sc_j_index = i + + # The position of the neighbour must be still translated; + # This might happen in the supercell itself, or outside, thus + # we neeed to recompute its position and its translation vector in supercell. + sc_j_position = sc_positions[sc_j_index] + + for sc_i_index, sc_i_translation in zip(sc_i_indices, sc_i_translations): + j_position = sc_j_position + np.dot(sc_i_translation - uc_j_translation + param.translation, uc_cell) + local_site = sc_pymat.get_sites_in_sphere(pt=j_position, r=thr)[0] # pymatgen PeriodicSite + + sc_hubbard_parameter = [ + int(sc_i_index), + param.atom_manifold, + int(local_site.index), # otherwise the class validator complains + param.neighbour_manifold, + param.value, + np.array(local_site.image, dtype=np.int64).tolist(), # otherwise the class validator complains + param.hubbard_type, + ] + + sc_hubbard_parameters.append(sc_hubbard_parameter) + + args = (sc_hubbard_parameters, hubbard.projectors, hubbard.formulation) + new_hubbard = Hubbard.from_list(*args) + + return HubbardStructureData.from_structure(structure=supercell, hubbard=new_hubbard) + + +def get_supercell_atomic_index(index: int, num_sites: int, translation: List[Tuple[int, int, int]]) -> int: + """Return the atomic index in 3x3x3 supercell. + + :param index: atomic index in unit cell + :param num_sites: number of sites in structure + :param translation: (3,) shape list of int referring to the translated atom in the 3x3x3 supercell + + :returns: atomic index in supercell standardized with the QuantumESPRESSO loop + """ + return index + QE_TRANSLATIONS.index(translation) * num_sites + + +def get_index_and_translation(index: int, num_sites: int) -> Tuple[int, List[Tuple[int, int, int]]]: + """Return the atomic index in unitcell and the associated translation from a 3x3x3 QuantumESPRESSO supercell index. + + :param index: atomic index + :param num_sites: number of sites in structure + :returns: tuple (index, (3,) shape list of ints) + """ + from math import floor + + number = floor(index / num_sites) # associated supercell number + return (index - num_sites * number, QE_TRANSLATIONS[number]) + + +def get_hubbard_indices(hubbard: Hubbard) -> List[int]: + """Return the set list of Hubbard indices.""" + atom_indices = {parameters.atom_index for parameters in hubbard.parameters} + neigh_indices = {parameters.neighbour_index for parameters in hubbard.parameters} + atom_indices.update(neigh_indices) + return list(atom_indices) + + +def is_intersite_hubbard(hubbard: Hubbard) -> bool: + """Return whether `Hubbard` contains intersite interactions (+V).""" + couples = [(param.atom_index != param.neighbour_index) for param in hubbard.parameters] + return any(couples) diff --git a/tests/common/__init__.py b/tests/common/__init__.py new file mode 100644 index 000000000..161b4f90e --- /dev/null +++ b/tests/common/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Tests for :mod:`~aiida_quantumespresso.common`.""" diff --git a/tests/common/test_hubbard.py b/tests/common/test_hubbard.py new file mode 100644 index 000000000..d9b612ea8 --- /dev/null +++ b/tests/common/test_hubbard.py @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- +"""Tests for :py:mod:`~aiida_quantumespresso.common.hubbard`.""" +# pylint: disable=redefined-outer-name +from copy import deepcopy + +from pydantic import ValidationError +import pytest + +from aiida_quantumespresso.common.hubbard import Hubbard, HubbardParameters + +VALID_PARAMETERS = { + 'atom_index': 0, + 'atom_manifold': '3d', + 'neighbour_index': 1, + 'neighbour_manifold': '2p', + 'translation': (0, 0, 0), + 'value': 5.0, + 'hubbard_type': 'U', +} + + +@pytest.fixture +def get_hubbard_parameters(): + """Return an `HubbardParameters` intstance.""" + + def _get_hubbard_parameters(overrides=None): + """Return an `HubbardParameters` intstance.""" + inputs = deepcopy(VALID_PARAMETERS) + + if overrides: + inputs.update(overrides) + + return HubbardParameters(**inputs) + + return _get_hubbard_parameters + + +@pytest.fixture +def get_hubbard(): + """Return an `Hubbard` intstance.""" + + def _get_hubbard(): + """Return an `Hubbard` intstance.""" + param = HubbardParameters(**VALID_PARAMETERS) + + return Hubbard(parameters=[param, param]) + + return _get_hubbard + + +def test_safe_hubbard_parameters(get_hubbard_parameters): + """Test valid inputs are stored correctly for py:meth:`HubbardParameters`.""" + params = get_hubbard_parameters().dict() + assert params == VALID_PARAMETERS + + +def test_from_to_list_parameters(get_hubbard_parameters): + """Test py:meth:`HubbardParameters.to_tuple` and py:meth:`HubbardParameters.from_tuple`.""" + param = get_hubbard_parameters() + hp_tuple = (0, '3d', 1, '2p', 5.0, (0, 0, 0), 'U') + assert param.to_tuple() == hp_tuple + param = HubbardParameters.from_tuple(hp_tuple) + assert param.dict() == VALID_PARAMETERS + + +@pytest.mark.parametrize( + 'overrides', [{ + 'atom_index': 0 + }, { + 'atom_manifold': '3d-2p' + }, { + 'translation': (0, -1, +1) + }, { + 'hubbard_type': 'B' + }] +) +def test_valid_hubbard_parameters(get_hubbard_parameters, overrides): + """Test valid inputs for py:meth:`HubbardParameters`.""" + hp_dict = get_hubbard_parameters(overrides=overrides).dict() + new_dict = deepcopy(VALID_PARAMETERS) + new_dict.update(overrides) + assert hp_dict == new_dict + + +@pytest.mark.parametrize(('overrides', 'match'), ( + ({ + 'atom_index': -1 + }, r'ensure this value is greater than or equal to 0'), + ( + { + 'atom_index': 0.5 + }, + r'value is not a valid integer', + ), + ( + { + 'atom_manifold': '3z' + }, + r'invalid manifold symbol z', + ), + ( + { + 'atom_manifold': '3d2p' + }, + r'invalid length ``4``. Only 2 or 5', + ), + ( + { + 'atom_manifold': '3d-3p-2s' + }, + r'ensure this value has at most 5 characters', + ), + ( + { + 'translation': (0, 0) + }, + r'wrong tuple length 2, expected 3', + ), + ( + { + 'translation': (0, 0, 0, 0) + }, + r'wrong tuple length 4, expected 3', + ), + ( + { + 'translation': (0, 0, -1.5) + }, + r'value is not a valid integer', + ), + ( + { + 'hubbard_type': 'L' + }, + r"permitted: 'Ueff', 'U', 'V', 'J', 'B', 'E2', 'E3'", + ), +)) +def test_invalid_hubbard_parameters(get_hubbard_parameters, overrides, match): + """Test invalid inputs for py:meth:`HubbardParameters`.""" + with pytest.raises(ValidationError, match=match): + get_hubbard_parameters(overrides=overrides) + + +def test_from_to_list_hubbard(get_hubbard): + """Test py:meth:`Hubbard.to_list` and py:meth:`Hubbard.from_list`.""" + hubbard = get_hubbard() + hp_tuple = (0, '3d', 1, '2p', 5.0, (0, 0, 0), 'U') + + hubbard_list = [hp_tuple, hp_tuple] + assert hubbard.to_list() == hubbard_list + + hubbard = Hubbard.from_list(hubbard_list) + assert hubbard.dict() == { + 'parameters': [VALID_PARAMETERS, VALID_PARAMETERS], + 'projectors': 'ortho-atomic', + 'formulation': 'dudarev', + } diff --git a/tests/conftest.py b/tests/conftest.py index 4176f03a1..d93215538 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -367,12 +367,14 @@ def _generate_structure(structure_id='silicon'): """ from aiida.orm import StructureData - if structure_id == 'silicon': + if structure_id.startswith('silicon'): + name1 = 'Si0' if structure_id.endswith('kinds') else 'Si' + name2 = 'Si1' if structure_id.endswith('kinds') else 'Si' param = 5.43 cell = [[param / 2., param / 2., 0], [param / 2., 0, param / 2.], [0, param / 2., param / 2.]] structure = 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') + structure.append_atom(position=(0., 0., 0.), symbols='Si', name=name1) + structure.append_atom(position=(param / 4., param / 4., param / 4.), symbols='Si', name=name2) elif structure_id == 'water': structure = StructureData(cell=[[5.29177209, 0., 0.], [0., 5.29177209, 0.], [0., 0., 5.29177209]]) structure.append_atom(position=[12.73464656, 16.7741411, 24.35076238], symbols='H', name='H') diff --git a/tests/data/__init__.py b/tests/data/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/test_hubbard_structure.py b/tests/data/test_hubbard_structure.py new file mode 100644 index 000000000..7cd9ae032 --- /dev/null +++ b/tests/data/test_hubbard_structure.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- +"""Tests for the :mod:`data.hubbard_structure` module.""" +# pylint: disable=redefined-outer-name,protected-access +import pytest + +from aiida_quantumespresso.data.hubbard_structure import HubbardStructureData + + +@pytest.fixture +def generate_hubbard(): + """Return a `Hubbard` instance.""" + + def _generate_hubbard(): + from aiida_quantumespresso.common.hubbard import Hubbard + return Hubbard.from_list([(0, '1s', 0, '1s', 5.0, (0, 0, 0), 'Ueff')]) + + return _generate_hubbard + + +@pytest.fixture +def generate_hubbard_structure(generate_structure): + """Return a `HubbardStructureData` instance.""" + + def _generate_hubbard_structure(): + from aiida_quantumespresso.common.hubbard import Hubbard + structure = generate_structure('silicon-kinds') + hp_list = [(0, '1s', 0, '1s', 5.0, (0, 0, 0), 'Ueff')] + hubbard = Hubbard.from_list(hp_list) + return HubbardStructureData.from_structure(structure=structure, hubbard=hubbard) + + return _generate_hubbard_structure + + +@pytest.mark.usefixtures('aiida_profile') +def test_valid_init(generate_hubbard): + """Test the constructor.""" + cell = [[1, 0, 0], [0, 1, 0], [0, 0, 1]] + sites = [['Si', 'Si0', (0, 0, 0)]] + hubbard = generate_hubbard() + hubbard_structure = HubbardStructureData(cell=cell, sites=sites, hubbard=hubbard) + assert hubbard_structure.cell == cell + assert hubbard_structure.kinds[0].symbol == sites[0][0] + assert hubbard_structure.sites[0].kind_name == sites[0][1] + assert hubbard_structure.sites[0].position == sites[0][2] + assert hubbard_structure.hubbard == hubbard + + +@pytest.mark.usefixtures('aiida_profile') +def test_from_structure(generate_structure, generate_hubbard): + """Test the `from_structure` method.""" + structure = generate_structure() + hubbard = generate_hubbard() + hubbard_structure = HubbardStructureData.from_structure(structure=structure, hubbard=hubbard) + assert hubbard_structure.cell == structure.cell + assert len(hubbard_structure.sites) == len(structure.sites) + assert hubbard_structure.hubbard == hubbard # sanity check + + structure = generate_structure('silicon-kinds') + hubbard_structure = HubbardStructureData.from_structure(structure=structure) + assert hubbard_structure.get_site_kindnames() == structure.get_site_kindnames() + assert len(hubbard_structure.kinds) == 2 + + +@pytest.mark.usefixtures('aiida_profile') +def test_append_hubbard_parameters(generate_hubbard_structure): + """Test the `append_hubbard_parameters` method.""" + from aiida_quantumespresso.common.hubbard import HubbardParameters + hubbard_structure = generate_hubbard_structure() + args = (0, '1s', 1, '1s', 5.0, (0, 0, 0), 'U') + hubbard_structure.append_hubbard_parameter(*args) + params = HubbardParameters.from_tuple(args) + assert len(hubbard_structure.hubbard.parameters) == 2 + assert params == hubbard_structure.hubbard.parameters[1] + + +@pytest.mark.usefixtures('aiida_profile') +def test_pop_hubbard_parameters(generate_hubbard_structure): + """Test the `pop_hubbard_parameters` method.""" + hubbard_structure = generate_hubbard_structure() + hubbard_structure.pop_hubbard_parameters(0) + assert len(hubbard_structure.hubbard.parameters) == 0 + + +@pytest.mark.usefixtures('aiida_profile') +def test_clear_hubbard_parameters(generate_hubbard_structure): + """Test the `clear_hubbard_parameters` method.""" + hubbard_structure = generate_hubbard_structure() + hubbard_structure.clear_hubbard_parameters() + assert len(hubbard_structure.hubbard.parameters) == 0 + + +@pytest.mark.usefixtures('aiida_profile') +def test_is_storable(generate_hubbard_structure): + """Test the storing does not throw errors.""" + hubbard_structure = generate_hubbard_structure() + hubbard_structure.store() + assert hubbard_structure.is_stored + + +@pytest.mark.usefixtures('aiida_profile') +def test_initialize_intersites_hubbard(generate_hubbard_structure): + """Test the `initialize_intersites_hubbard` method.""" + hubbard_structure = generate_hubbard_structure() + hubbard_structure.initialize_intersites_hubbard('Si', '1s', 'Si', '2s', 0, 'Ueff', False) + + # !WARNING! This is not the expected behavior, as we would like it to initialize + # intersites among first neighbours. The method was designed for different + # interacting species. We may want to improve it for this special cases, + # although it is still rather futuristic. + assert hubbard_structure.hubbard.parameters[1].to_tuple() == (0, '1s', 0, '2s', 0.0, (0, 0, 0), 'Ueff') + + hubbard_structure.clear_hubbard_parameters() + hubbard_structure.initialize_intersites_hubbard('Si0', '1s', 'Si1', '2s', 0.0, 'Ueff') + assert (0, '1s', 1, '2s', 0, (-1, 0, 0), 'Ueff') in hubbard_structure.hubbard.to_list() + assert len(hubbard_structure.hubbard.parameters) == 1 + + with pytest.raises(ValueError): + hubbard_structure.initialize_intersites_hubbard('Mg', '1s', 'Si1', '2s', 0, 'Ueff') + + +@pytest.mark.usefixtures('aiida_profile') +def test_initialize_onsites_hubbard(generate_hubbard_structure): + """Test the `initialize_onsites_hubbard` method.""" + hubbard_structure = generate_hubbard_structure() + + hubbard_structure.clear_hubbard_parameters() + hubbard_structure.initialize_onsites_hubbard('Si', '1s', 0.0, 'Ueff', False) + + assert (0, '1s', 0, '1s', 0, (0, 0, 0), 'Ueff') in hubbard_structure.hubbard.to_list() + assert len(hubbard_structure.hubbard.parameters) == 2 + + hubbard_structure.clear_hubbard_parameters() + hubbard_structure.initialize_onsites_hubbard('Si0', '1s', 0.0, 'Ueff', True) + + assert len(hubbard_structure.hubbard.parameters) == 1 + + +@pytest.mark.usefixtures('aiida_profile') +def test_get_one_kind_index(generate_hubbard_structure): + """Test the `_get_one_kind_index` method.""" + hubbard_structure = generate_hubbard_structure() + assert hubbard_structure._get_one_kind_index('Si0') == [0] + assert hubbard_structure._get_one_kind_index('Si1') == [1] + + +@pytest.mark.usefixtures('aiida_profile') +def test_get_symbol_indices(generate_hubbard_structure): + """Test the `_get_symbol_indices` method.""" + hubbard_structure = generate_hubbard_structure() + assert hubbard_structure._get_symbol_indices('Si') == [0, 1] diff --git a/tests/utils/fixtures/hubbard/HUBBARD.dat b/tests/utils/fixtures/hubbard/HUBBARD.dat new file mode 100644 index 000000000..be180aa45 --- /dev/null +++ b/tests/utils/fixtures/hubbard/HUBBARD.dat @@ -0,0 +1,3 @@ +# Copy this data in the pw.x input file for DFT+Hubbard calculations +HUBBARD {ortho-atomic} +U Co-3d 6.0799 diff --git a/tests/utils/fixtures/hubbard/HUBBARD_2.dat b/tests/utils/fixtures/hubbard/HUBBARD_2.dat new file mode 100644 index 000000000..7c4f51858 --- /dev/null +++ b/tests/utils/fixtures/hubbard/HUBBARD_2.dat @@ -0,0 +1,4 @@ +# Copy this data in the pw.x input file for DFT+Hubbard calculations +HUBBARD (atomic) +V Co-3d O-2p 1 11 0.3787 +V Co-3d O-2p 1 22 0.3772 diff --git a/tests/utils/fixtures/hubbard/HUBBARD_3.dat b/tests/utils/fixtures/hubbard/HUBBARD_3.dat new file mode 100644 index 000000000..506dffcc3 --- /dev/null +++ b/tests/utils/fixtures/hubbard/HUBBARD_3.dat @@ -0,0 +1,5 @@ +# Copy this data in the pw.x input file for DFT+Hubbard calculations +HUBBARD (atomic) +V Co-3d Co-3d 1 1 6.3334 +V Co-3d O-2p 1 11 0.3787 +V Co-3d O-2p 1 22 0.3772 diff --git a/tests/utils/test_hubbard.py b/tests/utils/test_hubbard.py new file mode 100644 index 000000000..8846b6a21 --- /dev/null +++ b/tests/utils/test_hubbard.py @@ -0,0 +1,237 @@ +# -*- coding: utf-8 -*- +"""Tests for the :mod:`utils.hubbard` module.""" +# pylint: disable=redefined-outer-name +import os + +import pytest + +from aiida_quantumespresso.common.hubbard import Hubbard +from aiida_quantumespresso.utils.hubbard import HubbardUtils + + +@pytest.fixture +def generate_hubbard_structure(): + """Return a `HubbardStructureData` instance.""" + + def _generate_hubbard_structure(parameters=None, projectors=None, formulation=None): + from aiida.orm import StructureData + + from aiida_quantumespresso.data.hubbard_structure import HubbardStructureData + + a, b, c, d = 1.40803, 0.81293, 4.68453, 1.62585 # pylint: disable=invalid-name + cell = [[a, -b, c], [0.0, d, c], [-a, -b, c]] # pylint: disable=invalid-name + positions = [[0, 0, 0], [0, 0, 3.6608], [0, 0, 10.392], [0, 0, 7.0268]] + symbols = ['Co', 'O', 'O', 'Li'] + structure = StructureData(cell=cell) + for position, symbol in zip(positions, symbols): + structure.append_atom(position=position, symbols=symbol) + + if parameters is None: + parameters = [(0, '3d', 0, '3d', 5.0, (0, 0, 0), 'U'), (0, '3d', 1, '2p', 1.0, (0, 0, 0), 'V')] + if projectors is None: + projectors = 'ortho-atomic' + if formulation is None: + formulation = 'dudarev' + args = (parameters, projectors, formulation) + hubbard = Hubbard.from_list(*args) + + return HubbardStructureData.from_structure(structure=structure, hubbard=hubbard) + + return _generate_hubbard_structure + + +@pytest.fixture +def generate_hubbard_utils(generate_hubbard_structure): + """Return a `HubbardUtils` instance.""" + + def _generate_hubbard_utils(): + return HubbardUtils(hubbard_structure=generate_hubbard_structure()) + + return _generate_hubbard_utils + + +@pytest.fixture +def filepath_hubbard(filepath_tests): + """Return the absolute filepath to the directory containing the file `fixtures`.""" + return os.path.join(filepath_tests, 'utils', 'fixtures', 'hubbard') + + +@pytest.mark.usefixtures('aiida_profile') +def test_valid_init(generate_hubbard_utils, generate_hubbard_structure): + """Test the constructor.""" + hubbard_utils = generate_hubbard_utils() + assert hubbard_utils.hubbard_structure.hubbard == generate_hubbard_structure().hubbard + + +@pytest.mark.usefixtures('aiida_profile') +def test_is_intersite(generate_hubbard_structure): + """Test the `is_intersite_hubbard` method.""" + from aiida_quantumespresso.utils.hubbard import is_intersite_hubbard + assert is_intersite_hubbard(generate_hubbard_structure().hubbard) + + +@pytest.mark.parametrize(('filename', 'projectors'), ( + ('HUBBARD.dat', 'ortho-atomic'), + ('HUBBARD_2.dat', 'atomic'), + ('HUBBARD_3.dat', 'atomic'), +)) +@pytest.mark.usefixtures('aiida_profile') +def test_invertibility(generate_hubbard_utils, filepath_hubbard, filename, projectors): + """Test the invertibility of the `get_hubbard_card` and `parse_hubbard_card` method. + + We test both methods at once, first parsing and then matching the generated card + with the raw parsed file. While this may seem trivial, the parsing does not store + the indecis, but the translation vectors. + Thus, mathematically we are testing ~ F^-1[F(x)] = x + """ + hubbard_utils = generate_hubbard_utils() + filepath = os.path.join(filepath_hubbard, filename) + hubbard_utils.parse_hubbard_dat(filepath) + + hubbard = hubbard_utils.hubbard_structure.hubbard + assert hubbard.projectors == projectors + assert hubbard.formulation == 'dudarev' + + hubbard_data = [] + with open(filepath, encoding='utf-8') as file: + lines = file.readlines() + for line in lines: + if line.strip().split()[0] != '#': + hubbard_data.append(tuple(line.strip().split())) + + hubbard_data.pop(0) # removing header + + parsed_data = [] + card = hubbard_utils.get_hubbard_card() + card = card.splitlines() + for line in card: + parsed_data.append(tuple(line.strip().split())) + + parsed_data.pop(0) # removing header + + assert len(hubbard_data) == len(parsed_data) + + for array, parsed_array in zip(hubbard_data, parsed_data): + assert array == parsed_array + + +@pytest.mark.parametrize(('parameters', 'values'), ( + ( + [[3, '1s', 3, '1s', 0.0, [0, 0, 0], 'U']], + [ + ['Li', 'Co', 'O', 'O'], + [[0, '1s', 0, '1s', 0.0, [0, 0, 0], 'U']], + ], + ), + ( + [[1, '1s', 1, '1s', 0.0, [0, 0, 0], 'U'], [2, '2p', 2, '2p', 0.0, [0, 0, 0], 'U']], + [ + ['O', 'O', 'Co', 'Li'], + [[0, '1s', 0, '1s', 0.0, [0, 0, 0], 'U'], [1, '2p', 1, '2p', 0.0, [0, 0, 0], 'U']], + ], + ), + ( + [[0, '3d', 0, '3d', 5.0, [0, 0, 0], 'U'], [3, '1s', 3, '1s', 0.0, [0, 0, 0], 'U']], + [ + ['Li', 'Co', 'O', 'O'], + [[0, '1s', 0, '1s', 0.0, [0, 0, 0], 'U'], [1, '3d', 1, '3d', 5.0, [0, 0, 0], 'U']], + ], + ), + ( + [[1, '1s', 3, '2p', 0.0, [0, 0, 0], 'U']], + [['O', 'O', 'Li', 'Co'], [[0, '1s', 2, '2p', 0.0, [0, 0, 0], 'U']]], + ), +)) +@pytest.mark.usefixtures('aiida_profile') +def test_reorder_atoms(generate_hubbard_structure, parameters, values): + """Test the `reorder_atoms` method. + + .. note:: the test is strictly related to the sort logic implemented. If + this was about to change, this test will most probably fail, and rearrangement + of the input ``values`` should be performed. + """ + # Sanity check - we must not override with default value + projectors = 'atomic' + formulation = 'liechtenstein' + args = (parameters, projectors, formulation) + hubbard_structure = generate_hubbard_structure(*args) + hubbard_utils = HubbardUtils(hubbard_structure=hubbard_structure) + hubbard_utils.reorder_atoms() + + sites = hubbard_utils.hubbard_structure.sites + for name, site in zip(values[0], sites): + assert site.kind_name == name + + expected = Hubbard.from_list(values[1]).to_list() + assert hubbard_utils.hubbard_structure.hubbard.projectors == projectors + assert hubbard_utils.hubbard_structure.hubbard.formulation == formulation + for param in hubbard_utils.hubbard_structure.hubbard.to_list(): + assert param in expected + + +@pytest.mark.usefixtures('aiida_profile') +def test_is_to_reorder(generate_hubbard_structure): + """Test the `is_to_reorder` method.""" + parameters = [[1, '1s', 3, '2p', 0.0, [0, 0, 0], 'U']] + hubbard_structure = generate_hubbard_structure(parameters) + hubbard_utils = HubbardUtils(hubbard_structure=hubbard_structure) + assert hubbard_utils.is_to_reorder() + + parameters = [[0, '1s', 0, '2p', 0.0, [0, 0, 0], 'U']] + hubbard_structure = generate_hubbard_structure(parameters) + hubbard_utils = HubbardUtils(hubbard_structure=hubbard_structure) + assert not hubbard_utils.is_to_reorder() + + +@pytest.mark.usefixtures('aiida_profile') +def test_reorder_supercell_atoms(generate_hubbard_structure): + """Test the `reorder_atoms` method with a supercell.""" + from aiida.orm import StructureData + parameters = [ + (0, '3d', 0, '3d', 5.0, (0, 0, 0), 'U'), + (0, '3d', 1, '2p', 5.0, (0, 0, 0), 'U'), + ] + hubbard_structure = generate_hubbard_structure(parameters=parameters) + hubbard_utils = HubbardUtils(hubbard_structure=hubbard_structure) + + pymatgen = hubbard_structure.get_pymatgen_structure() + pymatgen.make_supercell([2, 2, 2]) + supercell = StructureData(pymatgen=pymatgen) + hubbard_supercell = hubbard_utils.get_hubbard_for_supercell(supercell=supercell, thr=1e-5) + supercell_utils = HubbardUtils(hubbard_supercell) + supercell_utils.reorder_atoms() + hubbard_supercell = supercell_utils.hubbard_structure + + for parameters in hubbard_supercell.hubbard.to_list(): + if parameters[0] == parameters[2]: + assert hubbard_supercell.sites[parameters[0]].kind_name == 'Co' + + +@pytest.mark.usefixtures('aiida_profile') +def test_hubbard_for_supercell(generate_hubbard_structure): + """Test the `get_hubbard_for_supercell` method. + + .. warning:: we only test for the ``U`` case, assuming that if U is transfered + to the supercell correctly, any parameter will. + """ + from aiida.orm import StructureData + parameters = [[3, '1s', 3, '2s', 5.0, [0, 0, 0], 'U']] # Li atom + projectors = 'atomic' + formulation = 'liechtenstein' + args = (parameters, projectors, formulation) + hubbard_structure = generate_hubbard_structure(*args) + hubbard_utils = HubbardUtils(hubbard_structure=hubbard_structure) + + pymatgen = hubbard_structure.get_pymatgen_structure() + pymatgen.make_supercell([2, 2, 2]) + supercell = StructureData(pymatgen=pymatgen) + + hubbard_supercell = hubbard_utils.get_hubbard_for_supercell(supercell=supercell, thr=1e-5) + + assert hubbard_supercell.hubbard.projectors == projectors + assert hubbard_supercell.hubbard.formulation == formulation + for parameters in hubbard_supercell.hubbard.to_list(): + assert parameters[0] == parameters[2] + assert parameters[1] == '1s' + assert parameters[3] == '2s' + assert hubbard_supercell.sites[parameters[0]].kind_name == 'Li'