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

Code/Pseudos setup: Separate frontend from backend #796

Merged
merged 5 commits into from
Aug 19, 2024
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
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
18 changes: 10 additions & 8 deletions src/aiidalab_qe/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +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 as install_qe_codes

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

Expand All @@ -22,9 +18,12 @@ def cli():
@click.option("-f", "--force", is_flag=True)
@click.option("-p", "--profile", default=_DEFAULT_PROFILE)
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_qe_codes(force=force):
for msg in install(force=force):
click.echo(msg)
assert codes_are_setup()
click.secho("Codes are setup!", fg="green")
Expand All @@ -45,7 +44,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 @@ -68,7 +68,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 @@ -90,9 +90,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
201 changes: 2 additions & 199 deletions src/aiidalab_qe/common/setup_codes.py
Original file line number Diff line number Diff line change
@@ -1,215 +1,18 @@
from pathlib import Path
from shutil import which
from subprocess import CalledProcessError, run
from threading import Thread

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

FN_LOCKFILE = Path.home().joinpath(".install-qe-on-localhost.lock")
FN_DO_NOT_SETUP = Path.cwd().joinpath(".do-not-setup-on-localhost")

QE_VERSION = "7.2"


def get_qe_env():
# QE is already pre-installed in the QE image
path = Path(f"/opt/conda/envs/quantum-espresso-{QE_VERSION}")
if path.exists():
return path
else:
return Path.home().joinpath(".conda", "envs", f"quantum-espresso-{QE_VERSION}")


# Add all QE codes with the calcjob entry point in the aiida-quantumespresso.
CODE_NAMES = (
"pw",
"projwfc",
"dos",
"cp",
"epw",
"matdyn",
"neb",
"open_grid",
"ph",
"pp",
"pw2gw",
"pw2wannier90",
"q2r",
"xspectra",
"hp",
)


def qe_installed():
return get_qe_env().exists()


def install_qe():
run(
[
"conda",
"create",
"--yes",
"--override-channels",
"--channel",
"conda-forge",
"--prefix",
str(get_qe_env()),
f"qe={QE_VERSION}",
],
capture_output=True,
check=True,
)


def _code_is_setup(name):
try:
load_code(f"{name}-{QE_VERSION}@localhost")
except NotExistent:
return False
else:
return True


def codes_are_setup():
return all(_code_is_setup(code_name) for code_name in CODE_NAMES)


def _generate_header_to_setup_code():
"""Generate the header string to setup a code for a given computer."""
header_code = """
from aiida.orm.nodes.data.code.installed import InstalledCode
from aiida.orm import load_computer
from aiida import load_profile
load_profile()

"""
return header_code


def _generate_string_to_setup_code(code_name, computer_name="localhost"):
"""Generate the Python string to setup an AiiDA code for a given computer.

Tries to load an existing code and if not existent,
generates Python code to create and store a new code setup."""
try:
load_code(f"{code_name}-{QE_VERSION}@{computer_name}")
except NotExistent:
label = f"{code_name}-{QE_VERSION}"
description = f"{code_name}.x ({QE_VERSION}) setup by AiiDAlab."
filepath_executable = get_qe_env().joinpath("bin", f"{code_name}.x")
default_calc_job_plugin = f"quantumespresso.{code_name}"
prepend_text = f'eval "$(conda shell.posix hook)"\\nconda activate {get_qe_env()}\\nexport OMP_NUM_THREADS=1'
python_code = """
computer = load_computer('{}')
code = InstalledCode(computer=computer,
label='{}',
description='{}',
filepath_executable='{}',
default_calc_job_plugin='{}',
prepend_text='{}'
)

code.store()
""".format( # noqa: UP032
computer_name,
label,
description,
filepath_executable,
default_calc_job_plugin,
prepend_text,
)
return python_code
else:
# the code already exists
return ""


def setup_codes():
python_code = _generate_header_to_setup_code()
for code_name in CODE_NAMES:
python_code += _generate_string_to_setup_code(code_name)
try:
run(["python", "-c", python_code], capture_output=True, check=True)
except CalledProcessError as error:
raise RuntimeError(f"Failed to setup codes: {error}") from None


def install(force=False):
"""Install Quantum ESPRESSO and the corresponding AiiDA codes.

Args:
force: Ignore previously failed attempts and install anyways.
"""
# Check for "do not install file" and skip actual check. The purpose of
# this file is to not re-try this process on every app start in case that
# there are issues.
if not force and FN_DO_NOT_SETUP.exists():
raise RuntimeError("Installation failed in previous attempt.")

yield "Checking installation status..."

conda_installed = which("conda")
try:
with FileLock(FN_LOCKFILE, timeout=5):
# We assume that if the codes are already setup, everything is in
# order. Only if they are not present, should we take action,
# however we only do so if the environment has a conda binary
# present (`which conda`). If that is not the case then we assume
# that this is a custom user environment in which case we also take
# no further action.
if codes_are_setup():
return # Already setup

if not conda_installed:
raise RuntimeError(
"Unable to automatically install Quantum ESPRESSO, conda "
"is not available."
)

if not qe_installed():
# First, install Quantum ESPRESSO.
yield "Installing QE..."
try:
install_qe()
except CalledProcessError as error:
raise RuntimeError(
f"Failed to create conda environment: {error}"
) from None

# After installing QE, we install the corresponding
# AiiDA codes:
python_code = _generate_header_to_setup_code()
for code_name in CODE_NAMES:
if not _code_is_setup(code_name):
yield f"Preparing setup script for ({code_name})..."
code_string = _generate_string_to_setup_code(code_name)
python_code += code_string
try:
yield "Setting up all codes..."
run(["python", "-c", python_code], capture_output=True, check=True)
except CalledProcessError as error:
raise RuntimeError(f"Failed to setup codes: {error}") from None

except Timeout:
# Assume that the installation was triggered by a different process.
yield "Installation was already started, waiting for it to finish..."
with FileLock(FN_LOCKFILE, timeout=120):
if not codes_are_setup():
raise RuntimeError(
"Installation process did not finish in the expected time."
) from None


class QESetupWidget(ipw.VBox):
unkcpz marked this conversation as resolved.
Show resolved Hide resolved
installed = traitlets.Bool(allow_none=True).tag(readonly=True)
Expand Down
Loading
Loading