diff --git a/docker/Dockerfile b/docker/Dockerfile index 91caf41c..a92fecfe 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -11,8 +11,14 @@ USER ${NB_USER} RUN cd ${PREINSTALL_APP_FOLDER} && \ # Remove all untracked files and directories. For example the setup lock flag file. git clean -fx && \ - # It is important to install from `aiidalab install` to mimic the same installation operation from the app store. - aiidalab install --yes --python "/opt/conda/bin/python" "quantum-espresso@file://${PREINSTALL_APP_FOLDER}" && \ + # It is important to install from `aiidalab install` to mimic the exact installation operation as + # from the app store. + # The command wil first install the dependencies from list by parsing setup config files, + # (for `aiidalab/aiidalab<23.03.2` the `setup.py` should be in the root folder of the app https://github.com/aiidalab/aiidalab/pull/382). + # and then the app and restart the daemon in the end. + # But since the aiida profile not yet exists, the daemon restart will fail but it is not a problem. + # Because we only need the dependencies to be installed. + aiidalab install --yes --python ${CONDA_DIR}/bin/python "quantum-espresso@file://${PREINSTALL_APP_FOLDER}" && \ fix-permissions "${CONDA_DIR}" && \ fix-permissions "/home/${NB_USER}" @@ -28,6 +34,11 @@ RUN mamba create -p /opt/conda/envs/quantum-espresso --yes \ fix-permissions "${CONDA_DIR}" && \ fix-permissions "/home/${NB_USER}" +# Download the QE pseudopotentials to the folder for afterware installation. +ENV PSEUDO_FOLDER ${CONDA_DIR}/pseudo +RUN mkdir -p ${PSEUDO_FOLDER} && \ + python -m aiidalab_qe download-pseudos --dest ${PSEUDO_FOLDER} + COPY before-notebook.d/* /usr/local/bin/before-notebook.d/ WORKDIR "/home/${NB_USER}" diff --git a/docker/before-notebook.d/71_install-qeapp.sh b/docker/before-notebook.d/71_install-qeapp.sh index 44393627..849d9284 100644 --- a/docker/before-notebook.d/71_install-qeapp.sh +++ b/docker/before-notebook.d/71_install-qeapp.sh @@ -13,11 +13,9 @@ else fi # Install the pseudo libraries if not already installed. -# This can be simplified and accelerated once the following PR is merged: -# https://github.com/aiidateam/aiida-pseudo/pull/135 if aiida-pseudo list | grep -q "no pseudo potential families"; then echo "Installing pseudo potential families." - python -m aiidalab_qe install-pseudos + python -m aiidalab_qe install-pseudos --source ${PSEUDO_FOLDER} else echo "Pseudo potential families are already installed." fi diff --git a/setup.cfg b/setup.cfg index fc196c45..2db382c9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,6 +28,7 @@ install_requires = Jinja2~=3.0 aiida-quantumespresso~=4.3.0 aiidalab-widgets-base==2.1.0a0 + aiida-pseudo~=1.4 filelock~=3.8 importlib-resources~=5.2 widget-bandsplot~=0.5.1 diff --git a/src/aiidalab_qe/__main__.py b/src/aiidalab_qe/__main__.py index e1a635db..70301696 100644 --- a/src/aiidalab_qe/__main__.py +++ b/src/aiidalab_qe/__main__.py @@ -1,12 +1,16 @@ """For running the app from the command line used for post_install script. """ +from pathlib import Path + import click from aiida import load_profile from aiidalab_qe.common.setup_codes import codes_are_setup from aiidalab_qe.common.setup_codes import install as install_qe_codes -from aiidalab_qe.common.setup_pseudos import install as setup_pseudos + +# The default profile name of AiiDAlab container. +_DEFAULT_PROFILE = "default" @click.group() @@ -16,7 +20,7 @@ def cli(): @cli.command() @click.option("-f", "--force", is_flag=True) -@click.option("-p", "--profile", default="default") +@click.option("-p", "--profile", default=_DEFAULT_PROFILE) def install_qe(force, profile): load_profile(profile) try: @@ -29,16 +33,51 @@ def install_qe(force, profile): @cli.command() -@click.option("-p", "--profile", default="default") -def install_pseudos(profile): +@click.option("-p", "--profile", default=_DEFAULT_PROFILE, help="AiiDA profile name.") +@click.option( + "source", + "--source", + default=None, + help="The source folder to install from local.", + type=click.Path(exists=True, path_type=Path, resolve_path=True), +) +def install_pseudos(profile, source): + """Install pseudopotentials from a local folder if source is specified, + otherwise download from remote repositories. + """ + from aiidalab_qe.common.setup_pseudos import install + load_profile(profile) + try: - for msg, _ in setup_pseudos(): + for msg, _ in install(cwd=source, download_only=False): click.echo(msg) click.secho("Pseudopotentials are installed!", fg="green") except Exception as error: raise click.ClickException(f"Failed to set up pseudo potentials: {error}") +@cli.command() +@click.option( + "dest", + "--dest", + default=None, + help="The dest folder where to download the pseudos.", + type=click.Path(exists=True, path_type=Path, resolve_path=True), +) +def download_pseudos(dest): + from aiidalab_qe.common.setup_pseudos import EXPECTED_PSEUDOS, _install_pseudos + + try: + for progress in _install_pseudos( + EXPECTED_PSEUDOS, download_only=True, cwd=dest + ): + click.echo(progress) + click.secho("Pseudopotentials are downloaded!", fg="green") + + except Exception as error: + raise click.ClickException(f"Failed to download pseudo potentials: {error}") + + if __name__ == "__main__": cli() diff --git a/src/aiidalab_qe/app/configuration/advanced.py b/src/aiidalab_qe/app/configuration/advanced.py index f45b9c79..755ec624 100644 --- a/src/aiidalab_qe/app/configuration/advanced.py +++ b/src/aiidalab_qe/app/configuration/advanced.py @@ -16,6 +16,7 @@ from aiidalab_qe.app.parameters import DEFAULT_PARAMETERS from aiidalab_qe.common.panel import Panel +from aiidalab_qe.common.setup_pseudos import PseudoFamily from .pseudos import PseudoFamilySelector, PseudoSetter @@ -262,8 +263,9 @@ def set_panel_value(self, parameters): """Set the panel value from the given parameters.""" if "pseudo_family" in parameters: + pseudo_family_string = parameters["pseudo_family"] self.pseudo_family_selector.load_from_pseudo_family( - parameters.get("pseudo_family") + PseudoFamily.from_string(pseudo_family_string) ) if "pseudos" in parameters["pw"]: self.pseudo_setter.set_pseudos(parameters["pw"]["pseudos"], {}) @@ -295,11 +297,16 @@ def reset(self): with self.hold_trait_notifications(): # Reset protocol dependent settings self._update_settings_from_protocol(self.protocol) - self.pseudo_family_selector.load_from_pseudo_family( - DEFAULT_PARAMETERS["advanced"]["pseudo_family"] - ) + + # reset the pseudo family + pseudo_family_dict = DEFAULT_PARAMETERS["advanced"]["pseudo_family"] + pseudo_family = PseudoFamily(**pseudo_family_dict) + + self.pseudo_family_selector.load_from_pseudo_family(pseudo_family) + # reset total charge self.total_charge.value = DEFAULT_PARAMETERS["advanced"]["tot_charge"] + # reset the override checkbox self.override.value = False self.smearing.reset() diff --git a/src/aiidalab_qe/app/configuration/pseudos.py b/src/aiidalab_qe/app/configuration/pseudos.py index 17e00645..012c4195 100644 --- a/src/aiidalab_qe/app/configuration/pseudos.py +++ b/src/aiidalab_qe/app/configuration/pseudos.py @@ -3,7 +3,6 @@ import io import re -from dataclasses import dataclass import ipywidgets as ipw import traitlets as tl @@ -14,6 +13,11 @@ from aiidalab_widgets_base.utils import StatusHTML from aiidalab_qe.app.parameters import DEFAULT_PARAMETERS +from aiidalab_qe.common.setup_pseudos import ( + PSEUDODOJO_VERSION, + SSSP_VERSION, + PseudoFamily, +) UpfData = DataFactory("pseudo.upf") SsspFamily = GroupFactory("pseudo.family.sssp") @@ -21,51 +25,6 @@ CutoffsPseudoPotentialFamily = GroupFactory("pseudo.family.cutoffs") -@dataclass(frozen=True) -class PseudoFamily: - library: str - version: str - functional: str - accuracy: str - relativistic: str | None = None - file_type: str | None = None - - @classmethod - def from_string(cls, pseudo_family_string: str) -> PseudoFamily: - """Initialize from a pseudo family string.""" - # We support two pseudo families: SSSP and PseudoDojo - # They are formatted as follows: - # SSSP: SSSP/// - # PseudoDojo: PseudoDojo///// - # where is either 'SR' or 'FR' and is either 'upf' or 'psml' - # Before we unify the format of family strings, the conditions below are necessary - # to distinguish between the two families - library = pseudo_family_string.split("/")[0] - if library == "SSSP": - version, functional, accuracy = pseudo_family_string.split("/")[1:] - relativistic = None - file_type = None - elif library == "PseudoDojo": - ( - version, - functional, - relativistic, - accuracy, - file_type, - ) = pseudo_family_string.split("/")[1:] - else: - raise ValueError(f"Unknown pseudo family {pseudo_family_string}") - - return cls( - library=library, - version=version, - functional=functional, - accuracy=accuracy, - relativistic=relativistic, - file_type=file_type, - ) - - class PseudoFamilySelector(ipw.VBox): title = ipw.HTML( """
@@ -189,12 +148,13 @@ def set_value(self, _=None): """ library, accuracy = self.library_selection.value.split() functional = self.dft_functional.value + # XXX (jusong.yu): a validator is needed to check the family string is consistent with the list of pseudo families defined in the setup_pseudos.py if library == "PseudoDojo": - pseudo_family_string = f"PseudoDojo/0.4/{functional}/SR/{accuracy}/upf" + pseudo_family_string = ( + f"PseudoDojo/{PSEUDODOJO_VERSION}/{functional}/SR/{accuracy}/upf" + ) elif library == "SSSP": - # XXX (unkcpz): the version is hard coded here which bring the - # the risk of inconsistency when the version is changed in the future, we want to have a centralized place to store the information. - pseudo_family_string = f"SSSP/1.2/{functional}/{accuracy}" + pseudo_family_string = f"SSSP/{SSSP_VERSION}/{functional}/{accuracy}" else: raise ValueError( f"Unknown pseudo family {self.override_protocol_pseudo_family.value}" @@ -240,13 +200,12 @@ def _update_settings_from_protocol(self, protocol): pseudo_family_string = PwBaseWorkChain.get_protocol_inputs(protocol)[ "pseudo_family" ] + pseudo_family = PseudoFamily.from_string(pseudo_family_string) - self.load_from_pseudo_family(pseudo_family_string) + self.load_from_pseudo_family(pseudo_family) - def load_from_pseudo_family(self, pseudo_family_string: str): + def load_from_pseudo_family(self, pseudo_family: PseudoFamily): """Reload the widget from the given pseudo family string.""" - pseudo_family = PseudoFamily.from_string(pseudo_family_string) - with self.hold_trait_notifications(): # will trigger the callback to set the value of widgets self.library_selection.value = ( diff --git a/src/aiidalab_qe/app/parameters/qeapp.yaml b/src/aiidalab_qe/app/parameters/qeapp.yaml index 3901183a..613f25dc 100644 --- a/src/aiidalab_qe/app/parameters/qeapp.yaml +++ b/src/aiidalab_qe/app/parameters/qeapp.yaml @@ -15,7 +15,11 @@ workchain: ## Advanced pw settings advanced: - pseudo_family: SSSP/1.2/PBEsol/efficiency + pseudo_family: + library: SSSP + version: 1.2 + functional: PBEsol + accuracy: efficiency tot_charge: 0 ## Codes diff --git a/src/aiidalab_qe/app/submission/__init__.py b/src/aiidalab_qe/app/submission/__init__.py index dbe247c3..372df860 100644 --- a/src/aiidalab_qe/app/submission/__init__.py +++ b/src/aiidalab_qe/app/submission/__init__.py @@ -23,12 +23,6 @@ from .resource import ParallelizationSettings, ResourceSelectionWidget -PROTOCOL_PSEUDO_MAP = { - "fast": "SSSP/1.2/PBE/efficiency", - "moderate": "SSSP/1.2/PBE/efficiency", - "precise": "SSSP/1.2/PBE/precision", -} - class SubmitQeAppWorkChainStep(ipw.VBox, WizardAppWidgetStep): """Step for submission of a bands workchain.""" diff --git a/src/aiidalab_qe/common/setup_pseudos.py b/src/aiidalab_qe/common/setup_pseudos.py index 44fd1ad3..12a556cb 100644 --- a/src/aiidalab_qe/common/setup_pseudos.py +++ b/src/aiidalab_qe/common/setup_pseudos.py @@ -1,8 +1,12 @@ # -*- coding: utf-8 -*- +from __future__ import annotations + import os +from dataclasses import dataclass, field from pathlib import Path from subprocess import run from threading import Thread +from typing import Iterable import ipywidgets as ipw import traitlets @@ -12,22 +16,95 @@ from aiidalab_qe.common.widgets import ProgressBar +SSSP_VERSION = "1.2" +PSEUDODOJO_VERSION = "0.4" + EXPECTED_PSEUDOS = { - "SSSP/1.2/PBE/efficiency", - "SSSP/1.2/PBE/precision", - "SSSP/1.2/PBEsol/efficiency", - "SSSP/1.2/PBEsol/precision", - "PseudoDojo/0.4/PBE/SR/standard/upf", - "PseudoDojo/0.4/PBEsol/SR/standard/upf", - "PseudoDojo/0.4/PBE/SR/stringent/upf", - "PseudoDojo/0.4/PBEsol/SR/stringent/upf", + f"SSSP/{SSSP_VERSION}/PBE/efficiency", + f"SSSP/{SSSP_VERSION}/PBE/precision", + f"SSSP/{SSSP_VERSION}/PBEsol/efficiency", + f"SSSP/{SSSP_VERSION}/PBEsol/precision", + f"PseudoDojo/{PSEUDODOJO_VERSION}/PBE/SR/standard/upf", + f"PseudoDojo/{PSEUDODOJO_VERSION}/PBEsol/SR/standard/upf", + f"PseudoDojo/{PSEUDODOJO_VERSION}/PBE/SR/stringent/upf", + f"PseudoDojo/{PSEUDODOJO_VERSION}/PBEsol/SR/stringent/upf", } FN_LOCKFILE = Path.home().joinpath(".install-sssp.lock") -def pseudos_to_install(): +@dataclass +class PseudoFamily: + """The dataclass to deal with pseudo family strings. + + Attributes: + library: the library name of the pseudo family, e.g. SSSP or PseudoDojo. + cmd_library_name: the sub command name used in aiida-pseudo command line. + version: the version of the pseudo family, e.g. 1.2 + functional: the functional of the pseudo family, e.g. PBE, PBEsol. + accuracy: the accuracy of the pseudo family, which is protocol in aiida-pseudo, e.g. efficiency, precision, standard, stringent. + relativistic: the relativistic treatment of the pseudo family, e.g. SR, FR. + file_type: the file type of the pseudo family, e.g. upf, psml, currently only used for PseudoDojo. + """ + + library: str + version: str + functional: str + accuracy: str + cmd_library_name: str = field(init=False) + relativistic: str | None = None + file_type: str | None = None + + def __post_init__(self): + """Post init operations and checks.""" + if self.library == "SSSP": + self.cmd_library_name = "sssp" + elif self.library == "PseudoDojo": + self.cmd_library_name = "pseudo-dojo" + else: + raise ValueError(f"Unknown pseudo library {self.library}") + + @classmethod + def from_string(cls, pseudo_family_string: str) -> PseudoFamily: + """Initialize from a pseudo family string.""" + # We support two pseudo families: SSSP and PseudoDojo + # They are formatted as follows: + # SSSP: SSSP/// + # PseudoDojo: PseudoDojo///// + # where is either 'SR' or 'FR' and is either 'upf' or 'psml' + # Before we unify the format of family strings, the conditions below are necessary + # to distinguish between the two families + library = pseudo_family_string.split("/")[0] + if library == "SSSP": + version, functional, accuracy = pseudo_family_string.split("/")[1:] + relativistic = None + file_type = None + elif library == "PseudoDojo": + ( + version, + functional, + relativistic, + accuracy, + file_type, + ) = pseudo_family_string.split("/")[1:] + else: + raise ValueError( + f"Not able to parse valid library name from {pseudo_family_string}" + ) + + return cls( + library=library, + version=version, + functional=functional, + accuracy=accuracy, + relativistic=relativistic, + file_type=file_type, + ) + + +def pseudos_to_install() -> set[str]: + """Query the database and return the list of pseudopotentials that are not installed.""" qb = QueryBuilder() qb.append( PseudoPotentialFamily, @@ -43,56 +120,95 @@ def pseudos_to_install(): return EXPECTED_PSEUDOS - labels -def install_pseudos(pseudo_set): - env = os.environ.copy() - env["PATH"] = f"{env['PATH']}:{Path.home().joinpath('.local', 'bin')}" - - def run_(*args, **kwargs): - return run(*args, env=env, capture_output=True, check=True, **kwargs) - - mult = 1 / len(pseudo_set) - for i, pseudo in enumerate(pseudo_set): - yield mult * i - if pseudo.startswith("SSSP"): - p_family, p_version, p_func, p_type = pseudo.split("/") - cmds = [ - "aiida-pseudo", - "install", - p_family.lower(), - "-x", - p_func, - "-p", - p_type, - "-v", - "1.2", - ] - elif pseudo.startswith("PseudoDojo"): - p_family, p_version, p_func, p_rel, p_type, p_format = pseudo.split("/") - cmds = [ - "aiida-pseudo", - "install", - "pseudo-dojo", - "-x", - p_func, - "-r", - p_rel, - "-p", - p_type, - "-f", - p_format, - "-v", - "0.4", +def _construct_cmd( + pseudo_family_string: str, download_only: bool = False, cwd: Path | None = None +) -> list: + """Construct the command for installation of pseudopotentials. + + If ``cwd`` is not None, and ``download_only`` is True the, only download the + pseudopotential files to the ``cwd`` folder. + If ``download_only`` is False and ``cwd`` is not None, the the pseudos will be installed from the ``cwd`` where the pseudos are downloaded to. + + NOTE: download_only has nothing to do with cwd, it will not download the pseudos to cwd if cwd is specified. + The control to download to cwd is in the ``_install_pseudos`` function below. + """ + pseudo_family = PseudoFamily.from_string(pseudo_family_string) + + # the library used in command line is lowercase + # e.g. SSSP -> sssp and PseudoDojo -> pseudo-dojo + library = pseudo_family.cmd_library_name + version = pseudo_family.version + functional = pseudo_family.functional + accuracy = pseudo_family.accuracy + cmd = [ + "aiida-pseudo", + "install", + library, + "--functional", + functional, + "--version", + version, + "-p", # p for protocol which is the accuracy of the library + accuracy, + ] + + # extra arguments for PseudoDojo + if library == "pseudo-dojo": + relativistic = pseudo_family.relativistic + file_type = pseudo_family.file_type + cmd.extend( + [ + "--relativistic", + relativistic, + "--pseudo-format", + file_type, ] - run_(cmds) + ) + + if download_only: + cmd.append("--download-only") + + # if cwd source folder specified, then install the pseudos from the folder + # download file name is replace `/` with `_` of the pseudo family string with `.aiida_pseudo` extension + if not download_only and cwd is not None: + file_path = cwd / f"{pseudo_family_string.replace('/', '_')}.aiida_pseudo" + if file_path.exists(): + cmd.extend(["--from-download", str(file_path)]) + + return cmd + +def run_cmd(cmd: list, env: dict | None = None, cwd: Path | None = None): + """Run the command with specific env in the workdir specified.""" + run(cmd, env=env, cwd=cwd, capture_output=True, check=True) -def install(): + +def _install_pseudos( + pseudo_families: set[str], download_only: bool = False, cwd: Path | None = None +) -> Iterable[float]: + """Go through the list of pseudo families and install them.""" + env = os.environ.copy() + env["PATH"] = f"{env['PATH']}:{Path.home() / '.local' / 'bin'}" + + mult = 1.0 / len(pseudo_families) + yield mult * 0 + for i, pseudo_family in enumerate(pseudo_families): + cmd = _construct_cmd(pseudo_family, download_only, cwd=cwd) + + run_cmd(cmd, env=env, cwd=cwd) + + yield mult * (i + 1) + + +def install( + download_only: bool = False, cwd: Path | None = None +) -> Iterable[tuple[str, float]]: yield "Checking installation status...", 0.1 try: with FileLock(FN_LOCKFILE, timeout=5): - if len(pseudos_to_install()) > 0: + if len(pseudos := pseudos_to_install()) > 0: yield "Installing...", 0.1 - for progress in install_pseudos(pseudos_to_install()): + for progress in _install_pseudos(pseudos, download_only, cwd): yield "Installing...", progress except Timeout: diff --git a/tests/conftest.py b/tests/conftest.py index e9a8eac6..d73f258f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -162,7 +162,7 @@ def _generate_projection_data(): return _generate_projection_data -@pytest.fixture(scope="session", autouse=True) +@pytest.fixture(scope="function") def sssp(aiida_profile, generate_upf_data): """Create an SSSP pseudo potential family from scratch.""" from aiida.common.constants import elements @@ -222,7 +222,7 @@ def sssp(aiida_profile, generate_upf_data): return family -@pytest.fixture(scope="session") +@pytest.fixture(scope="function") def generate_upf_data(): """Return a `UpfData` instance for the given element a file for which should exist in `tests/fixtures/pseudos`.""" @@ -298,7 +298,12 @@ def _smearing_settings_generator(**kwargs): def app(pw_code, dos_code, projwfc_code): from aiidalab_qe.app.main import App + # Since we use `qe_auto_setup=False`, which will skip the pseudo library installation + # we need to mock set the installation status to `True` to avoid the blocker message pop up in the + # submmision step. app = App(qe_auto_setup=False) + app.submit_step.sssp_installation_status.installed = True + # set up codes app.submit_step.pw_code.refresh() app.submit_step.codes["dos"].refresh() diff --git a/tests/test_app.py b/tests/test_app.py index 256b1cb6..65bfac6c 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1,3 +1,10 @@ +import pytest + + +# TODO: (jusong.yu) I have to add this fixture after I change sssp fixture from session to function +# The fixtures are a bit messy, we need to clean it up +# Same for other tests that use sssp fixture +@pytest.mark.usefixtures("sssp") def test_reload_and_reset(submit_app_generator, generate_qeapp_workchain): """Test if the GUI paramters can be reload and reset properly""" wkchain = generate_qeapp_workchain( diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 00000000..53c33430 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,102 @@ +import time + +import aiida +from click.testing import CliRunner, Result + +import aiidalab_qe.__main__ as cli + +# To learn more about testing click applications, see: https://click.palletsprojects.com/en/8.1.x/testing/ + + +def test_download_and_install_pseudos(tmp_path, aiida_profile, monkeypatch): + """Test download pseudos to a specified directory and install them. + And test install pseudos without download. + Compare the time elapsed between the two methods. + + Act: Run the command `aiidalab-qe download-pseudos --dest `. + Assert: All pseudos are downloaded to the specified directory. + + Act: Run the command `aiidalab-qe install-pseudos --source `. + Assert: All pseudos are installed from the specified directory. + + Act: Run the command `aiidalab-qe install-pseudos`. + Assert: All pseudos are installed from the default directory, but slow. + + Note: this test is slow, it takes about ~30 seconds to run. + """ + from aiidalab_qe.common.setup_pseudos import ( + PSEUDODOJO_VERSION, + SSSP_VERSION, + pseudos_to_install, + ) + + # Mock the EXPECTED_PSEUDOS to speed up the test + MOCK_EXPECTED_PSEUDOS = { + f"SSSP/{SSSP_VERSION}/PBE/efficiency", + f"PseudoDojo/{PSEUDODOJO_VERSION}/PBEsol/SR/standard/upf", + f"PseudoDojo/{PSEUDODOJO_VERSION}/PBE/SR/stringent/upf", + f"PseudoDojo/{PSEUDODOJO_VERSION}/PBEsol/SR/stringent/upf", + } + # mock the EXPECTED_PSEUDOS + monkeypatch.setattr( + "aiidalab_qe.common.setup_pseudos.EXPECTED_PSEUDOS", + MOCK_EXPECTED_PSEUDOS, + ) + + # clean the profile database + aiida_profile.clear_profile() + + runner: CliRunner = CliRunner() + + start = time.time() + result: Result = runner.invoke(cli.download_pseudos, ["--dest", str(tmp_path)]) + download_time = time.time() - start + + assert result.exit_code == 0 + assert "Pseudopotentials are downloaded!" in result.output + + files = [f for f in tmp_path.glob("**/*") if f.is_file()] + + for pseudo in MOCK_EXPECTED_PSEUDOS: + filename = f"{pseudo.replace('/', '_')}.aiida_pseudo" + assert filename in [f.name for f in files] + + # Install the pseudos from the tmp_path + + # Check that the pseudos are not installed yet + assert len(pseudos_to_install()) == len(MOCK_EXPECTED_PSEUDOS) + + profile = aiida.get_profile() + + start = time.time() + result: Result = runner.invoke( + cli.install_pseudos, ["--source", tmp_path, "--profile", profile.name] + ) + install_time = time.time() - start + + print(f"Download time: {download_time}, install time: {install_time}") + + assert result.exit_code == 0 + assert "Pseudopotentials are installed!" in result.output + + assert len(pseudos_to_install()) == 0 + + # clean the profile database for the next test + aiida_profile.clear_profile() + + # Check that the pseudos are not installed yet + assert len(pseudos_to_install()) == len(MOCK_EXPECTED_PSEUDOS) + + profile = aiida.get_profile() + start = time.time() + result: Result = runner.invoke(cli.install_pseudos, ["--profile", profile.name]) + install_without_download_time = time.time() - start + print(f"Install without download time: {install_without_download_time}") + + assert result.exit_code == 0 + assert "Pseudopotentials are installed!" in result.output + + assert len(pseudos_to_install()) == 0 + + # Check the install without download is slower than install with download + assert install_without_download_time > install_time diff --git a/tests/test_codes.py b/tests/test_codes.py index edaa9e57..37f46dd4 100644 --- a/tests/test_codes.py +++ b/tests/test_codes.py @@ -1,3 +1,7 @@ +import pytest + + +@pytest.mark.usefixtures("sssp") def test_code_not_selected(submit_app_generator): """Test if there is an error when the code is not selected.""" app = submit_app_generator() @@ -5,6 +9,7 @@ def test_code_not_selected(submit_app_generator): app.submit_step._create_builder() +@pytest.mark.usefixtures("sssp") def test_set_selected_codes(submit_app_generator): """Test set_selected_codes method.""" from aiidalab_qe.app.submission import SubmitQeAppWorkChainStep @@ -34,21 +39,23 @@ def test_update_codes_display(): assert submit.codes["dos"].layout.display == "block" +@pytest.mark.usefixtures("sssp") def test_identify_submission_blockers(app): """Test identify_submission_blockers method.""" submit = app.submit_step blockers = list(submit._identify_submission_blockers()) - # there is one blocker: ['The SSSP library is not installed.'] - assert len(blockers) == 1 + assert len(blockers) == 0 + submit.input_parameters = {"workchain": {"properties": ["pdos"]}} blockers = list(submit._identify_submission_blockers()) - assert len(blockers) == 1 + + assert len(blockers) == 0 # set dos code to None, will introduce another blocker dos_value = submit.codes["dos"].value submit.codes["dos"].value = None blockers = list(submit._identify_submission_blockers()) - assert len(blockers) == 2 + assert len(blockers) == 1 # set dos code back will remove the blocker submit.codes["dos"].value = dos_value blockers = list(submit._identify_submission_blockers()) - assert len(blockers) == 1 + assert len(blockers) == 0 diff --git a/tests/test_plugins_bands.py b/tests/test_plugins_bands.py index 136f5658..a819f1ea 100644 --- a/tests/test_plugins_bands.py +++ b/tests/test_plugins_bands.py @@ -1,3 +1,7 @@ +import pytest + + +@pytest.mark.usefixtures("sssp") def test_result(generate_qeapp_workchain): from widget_bandsplot import BandsPlotWidget @@ -12,6 +16,7 @@ def test_result(generate_qeapp_workchain): assert isinstance(result.children[0], BandsPlotWidget) +@pytest.mark.usefixtures("sssp") def test_structure_1d(generate_qeapp_workchain, generate_structure_data): structure = generate_structure_data("silicon", pbc=(True, False, False)) wkchain = generate_qeapp_workchain(structure=structure) @@ -20,6 +25,7 @@ def test_structure_1d(generate_qeapp_workchain, generate_structure_data): assert len(wkchain.inputs.bands.bands_kpoints.labels) == 2 +@pytest.mark.usefixtures("sssp") def test_structure_2d(generate_qeapp_workchain, generate_structure_data): structure = generate_structure_data("silicon", pbc=(True, True, False)) wkchain = generate_qeapp_workchain(structure=structure) diff --git a/tests/test_plugins_pdos.py b/tests/test_plugins_pdos.py index 1f12717a..b625143a 100644 --- a/tests/test_plugins_pdos.py +++ b/tests/test_plugins_pdos.py @@ -1,3 +1,7 @@ +import pytest + + +@pytest.mark.usefixtures("sssp") def test_result(generate_qeapp_workchain): from aiidalab_qe.plugins.pdos.result import Result, export_pdos_data @@ -10,6 +14,7 @@ def test_result(generate_qeapp_workchain): assert len(result.children) == 2 +@pytest.mark.usefixtures("sssp") def test_result_spin(generate_qeapp_workchain): from aiidalab_qe.plugins.pdos.result import Result, export_pdos_data @@ -22,6 +27,7 @@ def test_result_spin(generate_qeapp_workchain): assert len(result.children) == 2 +@pytest.mark.usefixtures("sssp") def test_result_group_by(generate_qeapp_workchain): from aiidalab_qe.plugins.pdos.result import Result, export_pdos_data diff --git a/tests/test_pseudo.py b/tests/test_pseudo.py index 4b0aa681..a6628da6 100644 --- a/tests/test_pseudo.py +++ b/tests/test_pseudo.py @@ -1,5 +1,140 @@ +import pytest from aiida import orm +from aiidalab_qe.common.setup_pseudos import ( + PSEUDODOJO_VERSION, + SSSP_VERSION, + _construct_cmd, + _install_pseudos, + pseudos_to_install, +) + + +def test_setup_pseudos_cmd(tmp_path): + """Test _construct_cmd function in setup_pseudos.py.""" + + # SSSP family + pseudo_family = f"SSSP/{SSSP_VERSION}/PBE/efficiency" + cmd = _construct_cmd(pseudo_family) + assert cmd == [ + "aiida-pseudo", + "install", + "sssp", + "--functional", + "PBE", + "--version", + f"{SSSP_VERSION}", + "-p", + "efficiency", + ] + + # PseudoDojo family + pseudo_family = f"PseudoDojo/{PSEUDODOJO_VERSION}/PBEsol/SR/standard/upf" + cmd = _construct_cmd(pseudo_family) + assert cmd == [ + "aiida-pseudo", + "install", + "pseudo-dojo", + "--functional", + "PBEsol", + "--version", + f"{PSEUDODOJO_VERSION}", + "-p", + "standard", + "--relativistic", + "SR", + "--pseudo-format", + "upf", + ] + + # with download_only option + pseudo_family = f"PseudoDojo/{PSEUDODOJO_VERSION}/PBEsol/SR/standard/upf" + cmd = _construct_cmd(pseudo_family, download_only=True) + assert cmd == [ + "aiida-pseudo", + "install", + "pseudo-dojo", + "--functional", + "PBEsol", + "--version", + f"{PSEUDODOJO_VERSION}", + "-p", + "standard", + "--relativistic", + "SR", + "--pseudo-format", + "upf", + "--download-only", + ] + + # with cwd option to specify the source folder + pseudo_family = f"PseudoDojo/{PSEUDODOJO_VERSION}/PBEsol/SR/standard/upf" + cmd = _construct_cmd(pseudo_family, cwd=tmp_path) + + # since the source file not exist, the cmd should be the same as above + assert "--from-download" not in cmd + + # mock the source file + source_file = tmp_path / "PseudoDojo_0.4_PBEsol_SR_standard_upf.aiida_pseudo" + source_file.touch() + cmd = _construct_cmd(pseudo_family, cwd=tmp_path) + assert cmd == [ + "aiida-pseudo", + "install", + "pseudo-dojo", + "--functional", + "PBEsol", + "--version", + "0.4", + "-p", + "standard", + "--relativistic", + "SR", + "--pseudo-format", + "upf", + "--from-download", + f"{str(tmp_path)}/PseudoDojo_0.4_PBEsol_SR_standard_upf.aiida_pseudo", + ] + + +@pytest.mark.usefixtures("aiida_profile_clean") +def test_pseudos_installation(): + """Test install_pseudos""" + # Test by compare the pseudos_to_install before and after the installation + assert len(pseudos_to_install()) == 8 + EXPECTED_PSEUDOS = { + f"PseudoDojo/{PSEUDODOJO_VERSION}/PBE/SR/standard/upf", + f"SSSP/{SSSP_VERSION}/PBE/efficiency", + } + + # Install the pseudos + [_ for _ in _install_pseudos(EXPECTED_PSEUDOS)] + + # Two pseudos are installed + assert len(pseudos_to_install()) == 6 + + +@pytest.mark.usefixtures("aiida_profile_clean") +def test_download_and_install_pseudo_from_file(tmp_path): + """Test download and install pseudo from file.""" + assert len(pseudos_to_install()) == 8 + EXPECTED_PSEUDOS = { + f"PseudoDojo/{PSEUDODOJO_VERSION}/PBE/SR/standard/upf", + f"SSSP/{SSSP_VERSION}/PBE/efficiency", + } + + # Download the pseudos to the tmp_path but not install + [_ for _ in _install_pseudos(EXPECTED_PSEUDOS, download_only=True, cwd=tmp_path)] + + assert len(pseudos_to_install()) == 8 + assert len(list(tmp_path.iterdir())) == 2 + + # Install the pseudos from the tmp_path + [_ for _ in _install_pseudos(EXPECTED_PSEUDOS, cwd=tmp_path)] + + # Two pseudos are installed + assert len(pseudos_to_install()) == 6 + def test_pseudos_family_selector_widget(): """Test the pseudos widget.""" @@ -26,6 +161,7 @@ def test_pseudos_family_selector_widget(): assert w.value == "PseudoDojo/0.4/PBE/SR/stringent/upf" +@pytest.mark.usefixtures("sssp") def test_pseudos_setter_widget(generate_structure_data, generate_upf_data): """Test the pseudo setter widget.""" from aiidalab_qe.app.configuration.pseudos import PseudoSetter diff --git a/tests_integration/test_image.py b/tests_integration/test_image.py index 99ac840f..1a4841b2 100755 --- a/tests_integration/test_image.py +++ b/tests_integration/test_image.py @@ -1,3 +1,4 @@ +import pytest import requests @@ -7,9 +8,22 @@ def test_notebook_service_available(notebook_service): assert response.status_code == 200 -def test_verdi_status(aiidalab_exec, nb_user, notebook_service): +@pytest.mark.usefixtures("notebook_service") +def test_verdi_status(aiidalab_exec, nb_user): # Check the aiida service is running and connected to RabbitMQ # The notebook_service fixture is needed to wait for the services to be up output = aiidalab_exec("verdi status", user=nb_user).decode().strip() assert "Connected to RabbitMQ" in output assert "Daemon is running" in output + + +@pytest.mark.usefixtures("notebook_service") +def test_pseudos_families_are_installed(aiidalab_exec, nb_user): + # Check the aiida service is running and connected to RabbitMQ + # The notebook_service fixture is needed to wait for the services to be up + output = aiidalab_exec("aiida-pseudo list", user=nb_user).decode().strip() + assert "SSSP" in output + assert "PseudoDojo" in output + + # Two lines of header, 8 pseudos + assert len(output.splitlines()) == 10