Skip to content

Commit

Permalink
Merge branch 'main' into hq
Browse files Browse the repository at this point in the history
  • Loading branch information
unkcpz authored Aug 19, 2024
2 parents 2f956fd + 8855969 commit 76d08f3
Show file tree
Hide file tree
Showing 13 changed files with 470 additions and 245 deletions.
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@

## About

This is a early-development implementation of an AiiDAlab application for Quantum ESPRESSO workflow.
The app allows the execution of a workflow with Quantum ESPRESSO that includes the selection of an input structure, its relaxation, and the bands structure calculation.
This is an AiiDAlab application for Quantum ESPRESSO workflows.
The app allows the execution of a workflow with Quantum ESPRESSO that includes the selection of an input structure, its relaxation, the bands structure calculation, and more!

**The app is currently in an early development stage!**

Expand Down Expand Up @@ -41,6 +41,16 @@ Then, you can run the integration tests with:
pytest --driver Chrome tests_integration
```

### Published Docker images

Supported tags released on [Github Container Registry](https://ghcr.io/aiidalab):

- `edge` – the latest commit on the default branch (`main`)
- `latest` – the latest stable release
- `$version` – the version of a specific release (ex. `2022.1001`)

Pull requests into the default branch are further released on ghcr.io with the `pr-###` tag to simplify the testing of development versions.

## For maintainers

To create a new release, clone the repository, install development dependencies with `pip install '.[dev]'`, and then execute `bumpver update`.
Expand Down
23 changes: 12 additions & 11 deletions src/aiidalab_qe/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,6 @@

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_and_setup as install_and_setup_qe_codes,
)

# The default profile name of AiiDAlab container.
_DEFAULT_PROFILE = "default"

Expand All @@ -24,10 +18,14 @@ def cli():
@click.option("-f", "--force", is_flag=True)
@click.option("--computer")
@click.option("-p", "--profile", default=_DEFAULT_PROFILE)
def install_qe(force, profile, computer):

def install_qe(force, profile):
from aiida import load_profile
from aiidalab_qe.setup.codes import codes_are_setup, install

load_profile(profile)
try:
for msg in install_and_setup_qe_codes(computer=computer, force=force):
for msg in install(computer=computer, force=force):
click.echo(msg)
assert codes_are_setup(computer=computer)
click.secho("Codes are setup!", fg="green")
Expand All @@ -48,7 +46,8 @@ 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
from aiida import load_profile
from aiidalab_qe.setup.pseudos import install

load_profile(profile)

Expand All @@ -71,7 +70,7 @@ def install_pseudos(profile, source):
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
from aiidalab_qe.setup.pseudos import EXPECTED_PSEUDOS, _install_pseudos

try:
for progress in _install_pseudos(
Expand All @@ -93,9 +92,11 @@ def download_pseudos(dest):
)
@click.option("-p", "--profile", default=_DEFAULT_PROFILE)
def test_plugin(plugin_name, profile):
load_profile(profile)
from aiida import load_profile
from aiidalab_qe.app.utils import test_plugin_functionality

load_profile(profile)

try:
success, message = test_plugin_functionality(plugin_name)
if success:
Expand Down
2 changes: 1 addition & 1 deletion src/aiidalab_qe/app/configuration/advanced.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
from aiida_quantumespresso.workflows.pw.base import PwBaseWorkChain
from aiidalab_qe.app.parameters import DEFAULT_PARAMETERS
from aiidalab_qe.common.panel import Panel
from aiidalab_qe.common.setup_pseudos import PseudoFamily
from aiidalab_qe.common.widgets import HubbardWidget
from aiidalab_qe.setup.pseudos import PseudoFamily

from .pseudos import PseudoFamilySelector, PseudoSetter

Expand Down
2 changes: 1 addition & 1 deletion src/aiidalab_qe/app/configuration/pseudos.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from aiida.plugins import DataFactory, GroupFactory
from aiida_quantumespresso.workflows.pw.base import PwBaseWorkChain
from aiidalab_qe.app.parameters import DEFAULT_PARAMETERS
from aiidalab_qe.common.setup_pseudos import (
from aiidalab_qe.setup.pseudos import (
PSEUDODOJO_VERSION,
SSSP_VERSION,
PseudoFamily,
Expand Down
7 changes: 3 additions & 4 deletions src/aiidalab_qe/common/setup_codes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,9 @@

import ipywidgets as ipw
import traitlets
from filelock import FileLock, Timeout

from aiida.common.exceptions import NotExistent
from aiida.orm import load_code
from aiidalab_qe.common.widgets import ProgressBar
from ..setup.codes import QE_VERSION, install
from .widgets import ProgressBar

__all__ = [
"QESetupWidget",
Expand Down Expand Up @@ -251,6 +249,7 @@ def _setup(computer):
"Installation process did not finish in the expected time."
) from None

FN_DO_NOT_SETUP = Path.cwd().joinpath(".do-not-setup-on-localhost")

class QESetupWidget(ipw.VBox):
installed = traitlets.Bool(allow_none=True).tag(readonly=True)
Expand Down
222 changes: 2 additions & 220 deletions src/aiidalab_qe/common/setup_pseudos.py
Original file line number Diff line number Diff line change
@@ -1,230 +1,12 @@
from __future__ import annotations

import os
from collections.abc import Iterable
from dataclasses import dataclass, field
from pathlib import Path
from subprocess import run
from threading import Thread

import ipywidgets as ipw
import traitlets
from aiida_pseudo.groups.family import PseudoPotentialFamily
from filelock import FileLock, Timeout

from aiida.orm import QueryBuilder
from aiidalab_qe.common.widgets import ProgressBar

SSSP_VERSION = "1.3"
PSEUDODOJO_VERSION = "0.4"

EXPECTED_PSEUDOS = {
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",
f"PseudoDojo/{PSEUDODOJO_VERSION}/PBE/FR/standard/upf",
f"PseudoDojo/{PSEUDODOJO_VERSION}/PBEsol/FR/standard/upf",
f"PseudoDojo/{PSEUDODOJO_VERSION}/PBE/FR/stringent/upf",
f"PseudoDojo/{PSEUDODOJO_VERSION}/PBEsol/FR/stringent/upf",
}


FN_LOCKFILE = Path.home().joinpath(".install-sssp.lock")


@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/<version>/<functional>/<accuracy>
# PseudoDojo: PseudoDojo/<version>/<functional>/<relativistic>/<accuracy>/<file_type>
# where <relativistic> is either 'SR' or 'FR' and <file_type> 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,
filters={
"or": [
{"label": {"like": "SSSP/%"}},
{"label": {"like": "PseudoDojo/%"}},
]
},
project="label",
)
labels = set(qb.all(flat=True))
return EXPECTED_PSEUDOS - labels


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,
]
)

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_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 := pseudos_to_install()) > 0:
yield "Installing...", 0.1
for progress in _install_pseudos(pseudos, download_only, cwd):
yield "Installing...", progress

except Timeout:
# Assume that the installation was triggered by a different process.
yield (
"Installation was already started elsewhere, waiting for it to finish...",
ProgressBar.AnimationRate(1.0),
)
with FileLock(FN_LOCKFILE, timeout=300):
if len(pseudos_to_install()) > 0:
raise RuntimeError(
"Installation process did not finish in the expected time."
) from None
from ..setup.pseudos import install, pseudos_to_install
from .widgets import ProgressBar


class PseudosInstallWidget(ProgressBar):
Expand Down
5 changes: 5 additions & 0 deletions src/aiidalab_qe/setup/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Package for the QE app CLI for setting up codes and pseudos"""

from .codes import QE_VERSION

__all__ = ["QE_VERSION"]
Loading

0 comments on commit 76d08f3

Please sign in to comment.