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

CLI: separate established families from non-established ones #94

Closed
Closed
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
15 changes: 12 additions & 3 deletions aiida_pseudo/cli/family.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from aiida.cmdline.utils import decorators, echo

from ..groups.mixins import RecommendedCutoffMixin
from .params import arguments, options
from .params import arguments, options, types
from .root import cmd_root


Expand Down Expand Up @@ -57,7 +57,9 @@ def cmd_family_cutoffs():


@cmd_family_cutoffs.command('set')
@arguments.PSEUDO_POTENTIAL_FAMILY()
@arguments.PSEUDO_POTENTIAL_FAMILY(
type=types.PseudoPotentialFamilyParam(exclude=('pseudo.family.sssp', 'pseudo.family.pseudo_dojo'))
)
@click.argument('cutoffs', type=click.File(mode='rb'))
@options.STRINGENCY(required=True)
@options.UNIT()
Expand Down Expand Up @@ -86,8 +88,15 @@ def cmd_family_cutoffs_set(family, cutoffs, stringency, unit): # noqa: D301
except ValueError as exception:
raise click.BadParameter(f'`{cutoffs.name}` contains invalid JSON: {exception}', param_hint='CUTOFFS')

cutoffs_dict = dict()
for element, values in data.items():
try:
cutoffs_dict[element] = {'cutoff_wfc': values['cutoff_wfc'], 'cutoff_rho': values['cutoff_rho']}
except KeyError as exception:
raise click.BadParameter(f'`{cutoffs.name}` is missing cutoffs for element: {element}') from exception

try:
family.set_cutoffs(data, stringency, unit=unit)
family.set_cutoffs(cutoffs_dict, stringency, unit=unit)
except ValueError as exception:
raise click.BadParameter(f'`{cutoffs.name}` contains invalid cutoffs: {exception}', param_hint='CUTOFFS')

Expand Down
111 changes: 78 additions & 33 deletions aiida_pseudo/cli/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import pathlib
import shutil
import tempfile
import tarfile

import click
import requests
Expand Down Expand Up @@ -167,54 +168,28 @@ def download_pseudo_dojo(
@options.FUNCTIONAL(type=click.Choice(['PBE', 'PBEsol']), default='PBE', show_default=True)
@options.PROTOCOL(type=click.Choice(['efficiency', 'precision']), default='efficiency', show_default=True)
@options.DOWNLOAD_ONLY()
@options.FROM_DOWNLOAD()
@options.TRACEBACK()
@decorators.with_dbenv()
def cmd_install_sssp(version, functional, protocol, download_only, traceback):
def cmd_install_sssp(version, functional, protocol, download_only, from_download, traceback):
"""Install an SSSP configuration.

The SSSP configuration will be automatically downloaded from the Materials Cloud Archive entry to create a new
`SsspFamily`.
"""
# pylint: disable=too-many-locals
# pylint: disable=too-many-locals, too-many-statements
from aiida.common.files import md5_file
from aiida.orm import Group, QueryBuilder

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

configuration = SsspConfiguration(version, functional, protocol)
label = SsspFamily.format_configuration_label(configuration)
description = f'SSSP v{version} {functional} {protocol} installed with aiida-pseudo v{__version__}'

if configuration not in SsspFamily.valid_configurations:
echo.echo_critical(f'{version} {functional} {protocol} is not a valid SSSP configuration')

if not download_only and QueryBuilder().append(SsspFamily, filters={'label': label}).first():
echo.echo_critical(f'{SsspFamily.__name__}<{label}> is already installed')

with tempfile.TemporaryDirectory() as dirpath:

dirpath = pathlib.Path(dirpath)
filepath_archive = dirpath / 'archive.tar.gz'
filepath_metadata = dirpath / 'metadata.json'

download_sssp(configuration, filepath_archive, filepath_metadata, traceback)
def install_sssp(filepath_archive, filepath_metadata, label, description):

description += f'\nArchive pseudos md5: {md5_file(filepath_archive)}'
description += f'\nPseudo metadata md5: {md5_file(filepath_metadata)}'

if download_only:
for filepath in [filepath_archive, filepath_metadata]:
filepath_target = pathlib.Path.cwd() / filepath.name
if filepath_target.exists():
echo.echo_warning(f'the file `{filepath_target}` already exists, skipping.')
else:
# Cannot use ``pathlib.Path.rename`` because this will fail if it moves across file systems.
shutil.move(filepath, filepath_target)
echo.echo_success(f'`{filepath_target.name}` written to the current directory.')
return

with open(filepath_metadata, 'rb') as handle:
handle.seek(0)
metadata = json.load(handle)
Expand All @@ -237,6 +212,63 @@ def cmd_install_sssp(version, functional, protocol, download_only, traceback):

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

if download_only and from_download is not None:
raise click.BadParameter(
'cannot specify both `--download-only` and `--from-download`.',
param_hint="'--download-only' / '--from-download'"
)

configuration = SsspConfiguration(version, functional, protocol)

if configuration not in SsspFamily.valid_configurations:
echo.echo_critical(f'{version} {functional} {protocol} is not a valid SSSP configuration')

with tempfile.TemporaryDirectory() as dirpath:

dirpath = pathlib.Path(dirpath)

filepath_archive = dirpath / 'archive.tar.gz'
filepath_metadata = dirpath / 'metadata.json'

if from_download is not None:

tarball_path = pathlib.Path(from_download).absolute()
with tarfile.open(tarball_path, 'r') as handle:
handle.extractall(dirpath)

filepath_configuration = dirpath / 'configuration.json'

with filepath_configuration.open('r') as handle:
configuration = SsspConfiguration(**json.load(handle))
else:
download_sssp(configuration, filepath_archive, filepath_metadata, traceback)

label = SsspFamily.format_configuration_label(configuration)
description = (
f'SSSP v{configuration.version} {configuration.functional} {configuration.protocol} '
f'installed with aiida-pseudo v{__version__}'
)
if not download_only and QueryBuilder().append(SsspFamily, filters={'label': label}).first():
echo.echo_critical(f'{SsspFamily.__name__}<{label}> is already installed')

if download_only:
filepath_configuration = dirpath / 'configuration.json'

with filepath_configuration.open('w') as handle:
handle.write(json.dumps(configuration._asdict()))

tarball_path = pathlib.Path.cwd() / f'{label}.aiida_pseudo'.replace('/', '_')

if tarball_path.exists():
echo.echo_critical(f'the file `{tarball_path}` already exists.')

with tarfile.open(tarball_path, 'w') as tarball_file:
for filepath in [filepath_configuration, filepath_metadata, filepath_archive]:
tarball_file.add(filepath, filepath.name)
return

install_sssp(filepath_archive, filepath_metadata, label, description)


@cmd_install.command('pseudo-dojo')
@options.VERSION(type=click.Choice(['0.4', '1.0']), default='0.4', show_default=True)
Expand All @@ -246,10 +278,12 @@ def cmd_install_sssp(version, functional, protocol, download_only, traceback):
@options.PSEUDO_FORMAT(type=click.Choice(['psp8', 'upf', 'psml', 'jthxml']), default='psp8', show_default=True)
@options.DEFAULT_STRINGENCY(type=click.Choice(['low', 'normal', 'high']), default='normal', show_default=True)
@options.DOWNLOAD_ONLY()
@options.FROM_DOWNLOAD()
@options.TRACEBACK()
@decorators.with_dbenv()
def cmd_install_pseudo_dojo(
version, functional, relativistic, protocol, pseudo_format, default_stringency, download_only, traceback
version, functional, relativistic, protocol, pseudo_format, default_stringency, download_only, from_download,
traceback
):
"""Install a PseudoDojo configuration.

Expand Down Expand Up @@ -281,6 +315,12 @@ def cmd_install_pseudo_dojo(
)
# yapf: enable

if download_only and from_download is not None:
raise click.BadParameter(
'cannot specify both `--download-only` and `--from-download`.',
param_hint="'--download-only' / '--from-download'"
)

try:
pseudo_type = pseudo_type_mapping[pseudo_format]
except KeyError:
Expand All @@ -298,11 +338,16 @@ def cmd_install_pseudo_dojo(

with tempfile.TemporaryDirectory() as dirpath:

dirpath = pathlib.Path(dirpath)
if from_download is not None:
dirpath = pathlib.Path(from_download).absolute()
else:
dirpath = pathlib.Path(dirpath)

filepath_archive = dirpath / 'archive.tgz'
filepath_metadata = dirpath / 'metadata.tgz'

download_pseudo_dojo(configuration, filepath_archive, filepath_metadata, traceback)
if from_download is None:
download_pseudo_dojo(configuration, filepath_archive, filepath_metadata, traceback)

description += f'\nArchive pseudos md5: {md5_file(filepath_archive)}'
description += f'\nPseudo metadata md5: {md5_file(filepath_metadata)}'
Expand Down
9 changes: 9 additions & 0 deletions aiida_pseudo/cli/params/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,12 @@
'pseudopotential family.'
)
)

FROM_DOWNLOAD = OverridableOption(
'--from-download',
type=click.STRING,
required=False,
default=None,
show_default=False,
help='Install the pseudpotential family from the archive and metadata downloaded with the `--download-only` option.'
)
21 changes: 21 additions & 0 deletions aiida_pseudo/cli/params/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,27 @@ class PseudoPotentialFamilyParam(GroupParamType):

name = 'pseudo_family'

def __init__(self, exclude: typing.Optional[typing.List[str]] = None, **kwargs):
"""Construct the parameter.

:param exclude: an optional list of values that should be considered invalid and will raise ``BadParameter``.
"""
super().__init__(**kwargs)
self.exclude = exclude

def convert(self, value, param, ctx):
"""Convert the entry point name to the corresponding class.

:param value: entry point name that should correspond to subclass of `PseudoPotentialFamily` group plugin
mbercx marked this conversation as resolved.
Show resolved Hide resolved
:return: the `PseudoPotentialFamily` subclass
mbercx marked this conversation as resolved.
Show resolved Hide resolved
:raises: `click.BadParameter` if the entry point cannot be loaded or is not subclass of `PseudoPotentialFamily`
"""
family = super().convert(value, param, ctx)

if self.exclude and family.type_string in self.exclude:
raise click.BadParameter(f'Cannot modify cutoffs for established family `{family}`.')
mbercx marked this conversation as resolved.
Show resolved Hide resolved
return family


class PseudoPotentialFamilyTypeParam(click.ParamType):
"""Parameter type for `click` commands to define a subclass of `PseudoPotentialFamily`."""
Expand Down
3 changes: 3 additions & 0 deletions docs/source/_static/aiida-pseudo-custom.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.bigfont {
font-size: 140%;
}
16 changes: 13 additions & 3 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ['sphinx_copybutton', 'autoapi.extension', 'sphinx_click']
extensions = ['sphinx_copybutton', 'autoapi.extension', 'sphinx_click', 'sphinx.ext.intersphinx']

# Settings for the `sphinx_copybutton` extension
copybutton_selector = 'div:not(.no-copy)>div.highlight pre'
Expand All @@ -42,6 +42,11 @@
autoapi_dirs = ['../../aiida_pseudo']
autoapi_ignore = ['*cli*']

# Settings for the `sphinx.ext.intersphinx` extension
intersphinx_mapping = {
'aiida': ('http://aiida_core.readthedocs.io/en/latest/', None),
}

# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']

Expand All @@ -57,16 +62,21 @@
#
html_theme = 'sphinx_book_theme'
html_static_path = ['_static']
html_css_files = ['aiida-custom.css']
html_css_files = [
'aiida-custom.css',
'aiida-pseudo-custom.css'
]
html_theme_options = {
'home_page_in_toc': True,
'repository_url': 'https://github.com/aiidateam/aiida-pseudo',
'repository_branch': 'master',
'use_repository_button': True,
'use_issues_button': True,
'use_fullscreen_button': False,
'path_to_docs': 'docs',
'use_edit_page_button': True,
'extra_navbar': ''
'extra_navbar': '',
'extra_navbar': '<p>Made possible by the support of <a href="http://nccr-marvel.ch/" target="_blank"> NCCR MARVEL</a>, <a href="http://www.max-centre.eu/" target="_blank"> MaX CoE</a> and the <a href="https://www.materialscloud.org/swissuniversities" target="_blank"> swissuniversities P-5 project</a>.</p>'
}
html_domain_indices = True
html_logo = '_static/logo.png'
Loading