Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RecommendedCutoffMixin: store the unit of cutoffs in extras #57

Merged
merged 2 commits into from
Apr 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 3 additions & 7 deletions aiida_pseudo/cli/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ def cmd_install_sssp(version, functional, protocol, traceback):
from aiida.orm import Group, QueryBuilder

from aiida_pseudo import __version__
from aiida_pseudo.common import units
from aiida_pseudo.groups.family import SsspConfiguration, SsspFamily
from .utils import attempt, create_family_from_archive

Expand Down Expand Up @@ -136,13 +135,10 @@ def cmd_install_sssp(version, functional, protocol, traceback):
echo.echo_critical(msg)

# Cutoffs are in Rydberg but need to be stored in the family in electronvolt.
cutoffs[element] = {
'cutoff_wfc': values['cutoff_wfc'] * units.RY_TO_EV,
'cutoff_rho': values['cutoff_rho'] * units.RY_TO_EV,
}
cutoffs[element] = {'cutoff_wfc': values['cutoff_wfc'], 'cutoff_rho': values['cutoff_rho']}

family.description = description
family.set_cutoffs({'normal': cutoffs})
family.set_cutoffs({'normal': cutoffs}, unit='Ry')

echo.echo_success(f'installed `{label}` containing {family.count()} pseudo potentials')

Expand Down Expand Up @@ -264,6 +260,6 @@ def cmd_install_pseudo_dojo(version, functional, relativistic, protocol, pseudo_
echo.echo_warning(msg)

family.description = description
family.set_cutoffs(cutoffs, default_stringency=default_stringency)
family.set_cutoffs(cutoffs, default_stringency=default_stringency, unit='Eh')
sphuber marked this conversation as resolved.
Show resolved Hide resolved

echo.echo_success(f'installed `{label}` containing {family.count()} pseudo potentials')
5 changes: 3 additions & 2 deletions aiida_pseudo/common/units.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
"""Module with constants for unit conversions."""
from pint import UnitRegistry

RY_TO_EV = 13.6056917253 # Taken from `qe_tools.constants` v2.0
HA_TO_EV = RY_TO_EV * 2.0
# This unit registry singleton should be used to construct new quantities with a unit and to convert them to other units
U = UnitRegistry()
5 changes: 1 addition & 4 deletions aiida_pseudo/groups/family/pseudo_dojo.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

from aiida.common.exceptions import ParsingError

from aiida_pseudo.common import units
from aiida_pseudo.data.pseudo import UpfData, PsmlData, Psp8Data, JthXmlData
from ..mixins import RecommendedCutoffMixin
from .pseudo import PseudoPotentialFamily
Expand Down Expand Up @@ -232,9 +231,7 @@ def get_cutoffs_from_djrepo(cls, djrepo, pseudo_type):
except KeyError as exception:
raise ParsingError(f'stringency `{stringency}` is not defined in the djrepo `hints`') from exception

ecutwfc = ecutwfc * units.HA_TO_EV
ecutrho = ecutwfc * dual
cutoffs[stringency] = {'cutoff_wfc': ecutwfc, 'cutoff_rho': ecutrho}
cutoffs[stringency] = {'cutoff_wfc': ecutwfc, 'cutoff_rho': ecutwfc * dual}

return cutoffs

Expand Down
54 changes: 49 additions & 5 deletions aiida_pseudo/groups/mixins/cutoffs.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from aiida.common.lang import type_check
from aiida.plugins import DataFactory

from aiida_pseudo.common.units import U

StructureData = DataFactory('structure') # pylint: disable=invalid-name

__all__ = ('RecommendedCutoffMixin',)
Expand All @@ -17,11 +19,14 @@ class RecommendedCutoffMixin:
functions and the charge density. The units have to be in electronvolt.
"""

DEFAULT_UNIT = 'eV'

_key_cutoffs = '_cutoffs'
_key_cutoffs_unit = '_cutoffs_unit'
_key_default_stringency = '_default_stringency'

@classmethod
def validate_cutoffs(cls, elements: set, cutoffs: dict) -> None:
@staticmethod
def validate_cutoffs(elements: set, cutoffs: dict) -> None:
"""Validate a cutoff dictionary for a given set of elements.

:param elements: set of elements for which to validate the cutoffs dictionary.
Expand Down Expand Up @@ -57,6 +62,22 @@ def validate_cutoffs(cls, elements: set, cutoffs: dict) -> None:
f'invalid cutoff values for stringency `{stringency}` and element {element}: {values}'
)

@staticmethod
def validate_cutoffs_unit(unit: str) -> None:
"""Validate the cutoffs unit.

The unit should be a name that is recognized by the ``pint`` library to be a unit of energy.

:raises ValueError: if an invalid unit is specified.
"""
type_check(unit, str)

if unit not in U:
raise ValueError(f'`{unit}` is not a valid unit.')

if not U.Quantity(1, unit).check('[energy]'):
raise ValueError(f'`{unit}` is not a valid energy unit.')

def validate_stringency(self, stringency: str) -> None:
"""Validate a cutoff stringency.

Expand Down Expand Up @@ -94,7 +115,7 @@ def get_cutoff_stringencies(self) -> tuple:
"""
return tuple(self._get_cutoffs().keys())

def set_cutoffs(self, cutoffs: dict, default_stringency: str = None) -> None:
def set_cutoffs(self, cutoffs: dict, default_stringency: str = None, unit: str = None) -> None:
"""Set the recommended cutoffs for the pseudos in this family.

.. note:: units of the cutoffs should be in electronvolt.
Expand All @@ -107,16 +128,21 @@ def set_cutoffs(self, cutoffs: dict, default_stringency: str = None) -> None:
:param default_stringency: the default stringency to be used when ``get_recommended_cutoffs`` is called. If is
possible to not specify this if and only if the cutoffs only contain a single stringency set. That one will
then automatically be set as default.
:param unit: string definition of a unit of energy as recognized by the ``UnitRegistry`` of the ``pint`` lib.
:raises ValueError: if the cutoffs have an invalid format or the default stringency is invalid.
"""
unit = unit or self.DEFAULT_UNIT

self.validate_cutoffs(set(self.elements), cutoffs)
self.validate_cutoffs_unit(unit)

if default_stringency is None and len(cutoffs) != 1:
raise ValueError('have to explicitly specify a default stringency when specifying multiple cutoff sets.')

default_stringency = default_stringency or list(cutoffs.keys())[0]

self.set_extra(self._key_cutoffs, cutoffs)
self.set_extra(self._key_cutoffs_unit, unit)
mbercx marked this conversation as resolved.
Show resolved Hide resolved
self.set_extra(self._key_default_stringency, default_stringency)

def get_cutoffs(self, stringency=None) -> Union[dict, None]:
Expand All @@ -133,23 +159,28 @@ def get_cutoffs(self, stringency=None) -> Union[dict, None]:
except KeyError as exception:
raise ValueError(f'stringency `{stringency}` is not defined for this family.') from exception

def get_recommended_cutoffs(self, *, elements=None, structure=None, stringency=None):
def get_recommended_cutoffs(self, *, elements=None, structure=None, stringency=None, unit=None):
"""Return tuple of recommended wavefunction and density cutoffs for the given elements or ``StructureData``.

.. note:: at least one and only one of arguments ``elements`` or ``structure`` should be passed.

:param elements: single or tuple of elements.
:param structure: a ``StructureData`` node.
:param stringency: optional stringency if different from the default.
:param unit: string definition of a unit of energy as recognized by the ``UnitRegistry`` of the ``pint`` lib.
:return: tuple of recommended wavefunction and density cutoff.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is the unit parameter? 😁

:raises ValueError: if the requested stringency is not defined for this family.
:raises ValueError: if optional unit specified is invalid.
"""
if (elements is None and structure is None) or (elements is not None and structure is not None):
raise ValueError('at least one and only one of `elements` or `structure` should be defined')

type_check(elements, (tuple, str), allow_none=True)
type_check(structure, StructureData, allow_none=True)

if unit is not None:
self.validate_cutoffs_unit(unit)

if structure is not None:
symbols = structure.get_symbols_set()
elif isinstance(elements, tuple):
Expand All @@ -162,8 +193,21 @@ def get_recommended_cutoffs(self, *, elements=None, structure=None, stringency=N
cutoffs = self.get_cutoffs(stringency=stringency)

for element in symbols:
values = cutoffs[element]

if unit is not None:
current_unit = self.get_cutoffs_unit()
values = {k: U.Quantity(v, current_unit).to(unit).to_tuple()[0] for k, v in cutoffs[element].items()}
else:
values = cutoffs[element]

cutoffs_wfc.append(values['cutoff_wfc'])
cutoffs_rho.append(values['cutoff_rho'])

return (max(cutoffs_wfc), max(cutoffs_rho))

def get_cutoffs_unit(self) -> str:
"""Return the cutoffs unit.

:return: the string representation of the unit of the cutoffs.
"""
return self.get_extra(self._key_cutoffs_unit, self.DEFAULT_UNIT)
1 change: 1 addition & 0 deletions setup.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"aiida-core~=1.4",
"click~=7.0",
"click-completion~=0.5",
"pint~=0.16.1",
"requests~=2.20",
"sqlalchemy<1.4"
],
Expand Down
6 changes: 4 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,21 +117,23 @@ def _get_pseudo_potential_data(element='Ar', entry_point=None) -> PseudoPotentia

@pytest.fixture
def get_pseudo_family(tmpdir, filepath_pseudos):
"""Return a factory for a `PseudoPotentialFamily` instance."""
"""Return a factory for a ``PseudoPotentialFamily`` instance."""

def _get_pseudo_family(
label='family',
cls=PseudoPotentialFamily,
pseudo_type=PseudoPotentialData,
elements=None,
cutoffs=None,
unit=None,
default_stringency=None
) -> PseudoPotentialFamily:
"""Return an instance of `PseudoPotentialFamily` or subclass containing the given elements.
sphuber marked this conversation as resolved.
Show resolved Hide resolved

:param elements: optional list of elements to include instead of all the available ones
:params cutoffs: optional dictionary of cutoffs to specify. Needs to respect the format expected by the method
`aiida_pseudo.groups.mixins.cutoffs.RecommendedCutoffMixin.set_cutoffs`.
:param unit: string definition of a unit of energy as recognized by the ``UnitRegistry`` of the ``pint`` lib.
:param default_stringency: string with the default stringency name, if not specified, the first one specified in
the ``cutoffs`` argument will be used if specified.
:return: the pseudo family
Expand All @@ -155,7 +157,7 @@ def _get_pseudo_family(

if cutoffs is not None and isinstance(family, CutoffsFamily):
default_stringency = default_stringency or list(cutoffs.keys())[0]
family.set_cutoffs(cutoffs, default_stringency)
family.set_cutoffs(cutoffs, default_stringency, unit)

return family

Expand Down
Loading