diff --git a/.docker/Dockerfile b/.docker/Dockerfile new file mode 100644 index 000000000..26e83cc3a --- /dev/null +++ b/.docker/Dockerfile @@ -0,0 +1,25 @@ +FROM ghcr.io/aiidateam/aiida-core-with-services:latest + +USER root + +ARG QE_VERSION +ENV QE_VERSION ${QE_VERSION} + +# Install aiida-quantumespresso from source code +COPY --from=src . /tmp/aiida-quantumespresso +RUN pip install /tmp/aiida-quantumespresso --no-cache-dir && \ + rm -rf /tmp/aiida-quantumespresso + +# Install quantum espresso from conda (the latest version) +RUN mamba install -y -c conda-forge qe=${QE_VERSION} && \ + mamba clean --all -f -y && \ + fix-permissions "${CONDA_DIR}" && \ + fix-permissions "/home/${SYSTEM_USER}" + +COPY scripts/60-pseudo-code-setup.sh /etc/init/run-before-daemon-start/ + +# Static example files +RUN mkdir -p /opt/examples +COPY _static/ /opt/examples + +USER ${SYSTEM_UID} diff --git a/.docker/_static/Si.cif b/.docker/_static/Si.cif new file mode 100644 index 000000000..deb07daa9 --- /dev/null +++ b/.docker/_static/Si.cif @@ -0,0 +1,28 @@ +# generated using pymatgen +data_Si +_symmetry_space_group_name_H-M 'P 1' +_cell_length_a 3.86697465 +_cell_length_b 3.86697465 +_cell_length_c 3.86697465 +_cell_angle_alpha 60.00000000 +_cell_angle_beta 60.00000000 +_cell_angle_gamma 60.00000000 +_symmetry_Int_Tables_number 1 +_chemical_formula_structural Si +_chemical_formula_sum Si2 +_cell_volume 40.88829285 +_cell_formula_units_Z 2 +loop_ + _symmetry_equiv_pos_site_id + _symmetry_equiv_pos_as_xyz + 1 'x, y, z' +loop_ + _atom_site_type_symbol + _atom_site_label + _atom_site_symmetry_multiplicity + _atom_site_fract_x + _atom_site_fract_y + _atom_site_fract_z + _atom_site_occupancy + Si Si0 1 0.75000000 0.75000000 0.75000000 1 + Si Si1 1 0.50000000 0.50000000 0.50000000 1 diff --git a/.docker/docker-bake.hcl b/.docker/docker-bake.hcl new file mode 100644 index 000000000..89a2fe946 --- /dev/null +++ b/.docker/docker-bake.hcl @@ -0,0 +1,38 @@ +# docker-bake.hcl +variable "ORGANIZATION" { + default = "aiidateam" +} + +variable "REGISTRY" { + default = "docker.io/" +} + +variable "PLATFORMS" { + default = ["linux/amd64"] +} + +variable "QE_VERSION" { + default = "7.2" +} + +function "tags" { + params = [image] + result = [ + "${REGISTRY}${ORGANIZATION}/${image}:newly-baked" + ] +} + +group "default" { + targets = ["aiida-quantumespresso"] +} + +target "aiida-quantumespresso" { + tags = tags("aiida-quantumespresso") + contexts = { + src = ".." + } + platforms = "${PLATFORMS}" + args = { + "QE_VERSION" = "${QE_VERSION}" + } +} diff --git a/.docker/docker-compose.aiida-quantumespresso.yml b/.docker/docker-compose.aiida-quantumespresso.yml new file mode 100644 index 000000000..ab8725175 --- /dev/null +++ b/.docker/docker-compose.aiida-quantumespresso.yml @@ -0,0 +1,15 @@ +--- +version: '3.4' + +services: + + aiida: + image: ${REGISTRY:-}${BASE_IMAGE:-aiidateam/aiida-quantumespresso}:${TAG:-latest} + environment: + TZ: Europe/Zurich + SETUP_DEFAULT_AIIDA_PROFILE: 'true' + #volumes: + # - aiida-home-folder:/home/aiida + +volumes: + aiida-home-folder: diff --git a/.docker/requirements.txt b/.docker/requirements.txt new file mode 100644 index 000000000..3ba15482b --- /dev/null +++ b/.docker/requirements.txt @@ -0,0 +1,8 @@ +docker +pre-commit +pytest +requests +tabulate +pytest-docker +docker-compose +pyyaml<=5.3.1 diff --git a/.docker/scripts/60-pseudo-code-setup.sh b/.docker/scripts/60-pseudo-code-setup.sh new file mode 100755 index 000000000..6189df238 --- /dev/null +++ b/.docker/scripts/60-pseudo-code-setup.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# If lock file exists, then we have already run this script +if [ -f ${HOME}/.lock_pseudo_code_setup ]; then + exit 0 +else + touch ${HOME}/.lock_pseudo_code_setup +fi + +# Install pseudopotential libraries +aiida-pseudo install sssp --functional PBE -p efficiency +aiida-pseudo install sssp --functional PBE -p precision +aiida-pseudo install sssp --functional PBEsol -p efficiency +aiida-pseudo install sssp --functional PBEsol -p precision + +# Loop over executables to set up +for code_name in pw ph; do + # Set up caching + verdi config set -a caching.enabled_for aiida.calculations:quantumespresso.${code_name} + # Set up code + verdi code create core.code.installed \ + --non-interactive \ + --label ${code_name}-${QE_VERSION} \ + --description "${code_name}.x code on localhost" \ + --default-calc-job-plugin quantumespresso.${code_name} \ + --computer localhost --prepend-text 'eval "$(conda shell.posix hook)"\nconda activate base\nexport OMP_NUM_THREADS=1' \ + --filepath-executable ${code_name}.x +done + +# Import example structures +verdi data core.structure import ase /opt/examples/Si.cif diff --git a/.docker/tests/conftest.py b/.docker/tests/conftest.py new file mode 100644 index 000000000..1ce7f8b0b --- /dev/null +++ b/.docker/tests/conftest.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# pylint: disable=missing-docstring, redefined-outer-name +import json +import re + +import pytest + + +@pytest.fixture(scope='session') +def docker_compose_file(pytestconfig): # pylint: disable=unused-argument + return f'docker-compose.aiida-quantumespresso.yml' + + +@pytest.fixture(scope='session') +def docker_compose(docker_services): + # pylint: disable=protected-access + return docker_services._docker_compose + + +def is_container_ready(docker_compose): + output = docker_compose.execute('exec -T aiida verdi status').decode().strip() + return 'Connected to RabbitMQ' in output and 'Daemon is running' in output + + +@pytest.fixture(scope='session', autouse=True) +def _docker_service_wait(docker_services): + """Container startup wait.""" + docker_compose = docker_services._docker_compose + + docker_services.wait_until_responsive(timeout=120.0, pause=0.1, check=lambda: is_container_ready(docker_compose)) + + +@pytest.fixture +def container_user(): + return 'aiida' + + +@pytest.fixture +def aiida_exec(docker_compose): + + def execute(command, user=None, **kwargs): + if user: + command = f'exec -T --user={user} aiida {command}' + else: + command = f'exec -T aiida {command}' + return docker_compose.execute(command, **kwargs) + + return execute + + +@pytest.fixture +def qe_version(aiida_exec): + info = json.loads(aiida_exec('mamba list -n base --json --full-name qe').decode())[0] + return info['version'] + + +@pytest.fixture +def sssp_version(aiida_exec): + output = aiida_exec('aiida-pseudo list').decode().strip() + return re.search(r'SSSP/(\d+\.\d+)/PBE/efficiency', output).group(1) diff --git a/.docker/tests/test_aiida.py b/.docker/tests/test_aiida.py new file mode 100644 index 000000000..c0293c049 --- /dev/null +++ b/.docker/tests/test_aiida.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# pylint: disable=missing-docstring + + +def test_verdi_status(aiida_exec, container_user): + output = aiida_exec('verdi status', user=container_user).decode().strip() + assert 'Connected to RabbitMQ' in output + assert 'Daemon is running' in output + + # check that we have suppressed the warnings + assert 'Warning' not in output + + +def test_computer_setup_success(aiida_exec, container_user): + output = aiida_exec('verdi computer test localhost', user=container_user).decode().strip() + + assert 'Success' in output + assert 'Failed' not in output + + +def test_run_real_pw_computation(aiida_exec, container_user, qe_version, sssp_version): + import re + + output = aiida_exec('verdi data core.structure import ase /opt/examples/Si.cif', + user=container_user).decode().strip() + + # Find pk + pk = re.search(r'PK = (\d+)', output).group(1) + + cmd = f'aiida-quantumespresso calculation launch pw -X pw-{qe_version}@localhost -F SSSP/{sssp_version}/PBE/efficiency -S {pk} -k 1 1 1' + output = aiida_exec(cmd, user=container_user).decode().strip() + + assert 'terminated with state: finished [0]' in output diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 000000000..e3151c395 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,81 @@ +--- +name: Build, test and push Docker Images + +on: + pull_request: + paths-ignore: + - "docs/**" + - "tests/**" + push: + branches: + - main + tags: + - "v*" + paths-ignore: + - "docs/**" + - "tests/**" + workflow_dispatch: + +# https://docs.github.com/en/actions/using-jobs/using-concurrency +concurrency: + # only cancel in-progress jobs or runs for the current workflow - matches against branch & tags + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + amd64-build: + runs-on: ubuntu-latest + defaults: + run: + shell: bash + working-directory: .docker + + steps: + - name: Checkout Repo ⚡️ + uses: actions/checkout@v3 + - name: Set Up Python 🐍 + uses: actions/setup-python@v4 + with: + python-version: 3.x + + - name: Install Dev Dependencies 📦 + run: | + pip install --upgrade pip + pip install --upgrade -r requirements.txt + + - name: Build image + run: docker buildx bake -f docker-bake.hcl --set *.platform=linux/amd64 --load + env: + # Full logs for CI build + BUILDKIT_PROGRESS: plain + + - name: Run tests ✅ + run: TAG=newly-baked python -m pytest -s tests + + - name: Docker meta 📝 + id: meta + uses: docker/metadata-action@v4 + with: + images: | + name=ghcr.io/aiidateam/aiida-quantumespresso + tags: | + type=edge,enable={{is_default_branch}} + type=sha,enable=${{ github.ref_type != 'tag' }} + type=ref,event=pr + type=match,pattern=v(\d+\.\d+.\d+),group=1 + type=raw,value={{tag}},enable=${{ startsWith(github.ref, 'refs/tags/v') }} + + - name: Login to Container Registry 🔑 + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set tags for image and push 🏷️📤💾 + run: | + declare -a arr=(${{ steps.meta.outputs.tags }}) + for tag in "${arr[@]}"; do + docker tag aiidateam/aiida-quantumespresso:newly-baked ${tag} + docker push ${tag} + done