From f43c138258de079c490fe3af69a805112bf8d4c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edward=20Linsc-Bot=20=F0=9F=A4=96?= <310205674+elinsc-bot@users.noreply.github.com> Date: Fri, 7 Aug 2026 18:13:58 +0200 Subject: [PATCH] Install SG15 as a family that recommends no cutoffs A fresh SG15 install produced a family whose class carries cutoff stringencies while defining none, so asking it for recommended cutoffs raised "no default stringency has been defined". - Build the family as a plain `PseudoPotentialFamily` rather than one that promises cutoff stringencies SG15 does not publish. - Drive the installer in tests from a synthetic archive served through a patched `urlopen`, so no test reaches the network. Co-Authored-By: Claude Opus 5 (1M context) --- src/koopmans/aiida/setup/pseudos.py | 10 ++-- tests/test_pseudo_install.py | 86 +++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 tests/test_pseudo_install.py diff --git a/src/koopmans/aiida/setup/pseudos.py b/src/koopmans/aiida/setup/pseudos.py index 2b57f90e..c4135290 100644 --- a/src/koopmans/aiida/setup/pseudos.py +++ b/src/koopmans/aiida/setup/pseudos.py @@ -179,9 +179,9 @@ def _install_sssp_family(label: str, parts: list[str]) -> None: # SG15 ONCV is published as a single frozen tarball on quantum-simulation.org. It # bundles every version x relativistic variant in one flat archive; the label's # version/relativistic parts select which subset of UPFs to install. There is no -# upstream ``aiida-pseudo`` installer for SG15, so we install as a plain -# ``CutoffsPseudoPotentialFamily`` so recommended cutoffs can be attached later -# via ``family.set_cutoffs`` without a reinstall. +# upstream ``aiida-pseudo`` installer for SG15, so we build the family ourselves. +# SG15 publishes no recommended cutoffs, so it is a plain +# ``PseudoPotentialFamily`` and ``ecutwfc``/``ecutrho`` come from the input file. _SG15_ARCHIVE_URL = ( "http://www.quantum-simulation.org/potentials/sg15_oncv/sg15_oncv_upf_2020-02-06.tar.gz" ) @@ -199,7 +199,7 @@ def _install_sg15_family(label: str, parts: list[str]) -> None: import urllib.request from aiida_pseudo.data.pseudo import UpfData - from aiida_pseudo.groups.family import CutoffsPseudoPotentialFamily + from aiida_pseudo.groups.family import PseudoPotentialFamily _, version, functional, relativistic = parts @@ -257,6 +257,6 @@ def _install_sg15_family(label: str, parts: list[str]) -> None: "The archive layout may have changed." ) - family = CutoffsPseudoPotentialFamily.create_from_folder(flat, label, pseudo_type=UpfData) + family = PseudoPotentialFamily.create_from_folder(flat, label, pseudo_type=UpfData) click.echo(f" Successfully installed '{label}' ({family.count()} pseudopotentials)") diff --git a/tests/test_pseudo_install.py b/tests/test_pseudo_install.py new file mode 100644 index 00000000..9b68d6ce --- /dev/null +++ b/tests/test_pseudo_install.py @@ -0,0 +1,86 @@ +"""Tests for the SG15 pseudopotential family installer. + +The SG15 archive is never downloaded: a synthetic tarball built from the +``fake_upf_content`` streams is served through a patched ``urlopen``, with the +pinned checksum swapped for that tarball's own. +""" + +from __future__ import annotations + +import hashlib +import io +import tarfile +import urllib.request +from typing import Any + +import pytest + +from tests.fixtures import fake_upf_content + +SG15_LABEL = "SG15/1.2/PBE/SR" + +# One member per (element, version, relativistic variant) the flat archive +# bundles, so the installer has to select the label's subset rather than take +# whatever it finds. +_ARCHIVE_MEMBERS = { + "sg15_oncv_upf_2020-02-06/Si_ONCV_PBE-1.2.upf": ("Si", 4.0), + "sg15_oncv_upf_2020-02-06/O_ONCV_PBE-1.2.upf": ("O", 6.0), + "sg15_oncv_upf_2020-02-06/Si_ONCV_PBE-1.0.upf": ("Si", 4.0), + "sg15_oncv_upf_2020-02-06/Si_ONCV_PBE_FR-1.2.upf": ("Si", 4.0), +} + + +def _synthetic_archive() -> bytes: + """Return a gzipped tarball shaped like the published SG15 archive.""" + buffer = io.BytesIO() + with tarfile.open(fileobj=buffer, mode="w:gz") as tar: + for name, (element, z_valence) in _ARCHIVE_MEMBERS.items(): + payload = fake_upf_content(element, z_valence).encode("utf-8") + info = tarfile.TarInfo(name) + info.size = len(payload) + tar.addfile(info, io.BytesIO(payload)) + return buffer.getvalue() + + +@pytest.fixture +def offline_sg15_archive(monkeypatch: pytest.MonkeyPatch) -> bytes: + """Serve the synthetic archive from ``urlopen`` and pin its checksum.""" + from koopmans.aiida.setup import pseudos + + archive = _synthetic_archive() + monkeypatch.setattr(pseudos, "_SG15_ARCHIVE_SHA256", hashlib.sha256(archive).hexdigest()) + monkeypatch.setattr(urllib.request, "urlopen", lambda url: io.BytesIO(archive)) + return archive + + +class TestInstallSg15Family: + """The class the installer builds, and the pseudos it selects.""" + + def test_installs_a_family_that_recommends_no_cutoffs( + self, aiida_profile_clean: Any, offline_sg15_archive: bytes + ) -> None: + """SG15 publishes no cutoffs, so the family must not claim to have any.""" + from aiida_pseudo.groups.family import PseudoPotentialFamily + from aiida_pseudo.groups.mixins import RecommendedCutoffMixin + + from koopmans.aiida.setup.pseudos import install_pseudo_family + + install_pseudo_family(SG15_LABEL) + + family = PseudoPotentialFamily.collection.get(label=SG15_LABEL) + assert type(family) is PseudoPotentialFamily + assert not isinstance(family, RecommendedCutoffMixin) + + def test_installs_only_the_labelled_version_and_variant( + self, aiida_profile_clean: Any, offline_sg15_archive: bytes + ) -> None: + """The 1.0 and fully relativistic members of the archive stay out.""" + from aiida_pseudo.groups.family import PseudoPotentialFamily + + from koopmans.aiida.setup.pseudos import install_pseudo_family + + install_pseudo_family(SG15_LABEL) + + family = PseudoPotentialFamily.collection.get(label=SG15_LABEL) + assert {pseudo.element for pseudo in family.nodes} == {"Si", "O"} + assert family.count() == 2