diff --git a/docs/source/installation.rst b/docs/source/installation.rst
index 8be5010..024da7b 100644
--- a/docs/source/installation.rst
+++ b/docs/source/installation.rst
@@ -144,6 +144,34 @@ flight: a task whose worker disappears may be retried on the next one, in the sa
directory as the interrupted run. ``koopmans backend uninstall`` removes the whole
setup, database included.
+*******************
+ Pseudopotentials
+*******************
+
+You do not have to install pseudopotentials up front. The ``pseudo_library`` keyword of
+your input file names a family, and ``koopmans`` downloads that family the first time it
+is needed. It can fetch `PseudoDojo `_, `SSSP
+`_ and `SG15
+`_ families, named like
+
+- ``PseudoDojo/0.4/LDA/SR/standard/upf``
+- ``SSSP/1.3/PBEsol/efficiency``
+- ``SG15/1.2/PBE/SR``
+
+A family that you install yourself works just as well, under whatever label you give it:
+``koopmans`` downloads a family only when no installed one carries the label you asked
+for. This is the route for pseudopotentials it cannot fetch — the full-relativistic LDA
+sets that spin-orbit calculations need, for instance. Point ``aiida-pseudo`` at a
+directory holding one file per element:
+
+.. code-block:: console
+
+ $ aiida-pseudo install family my-lda-fr
+
+Then set ``pseudo_library`` to ``my-lda-fr``. A family installed this way publishes no
+recommended cutoffs, so state ``calculator_parameters.ecutwfc`` in your input file;
+``ecutrho`` follows at four times it, the ratio norm-conserving pseudopotentials use.
+
****************************
Installing for development
****************************
diff --git a/src/koopmans/aiida/setup/pseudos.py b/src/koopmans/aiida/setup/pseudos.py
index ae54449..15fa7f2 100644
--- a/src/koopmans/aiida/setup/pseudos.py
+++ b/src/koopmans/aiida/setup/pseudos.py
@@ -14,17 +14,16 @@
def ensure_pseudo_family_installed(pseudo_family: str) -> None:
"""Ensure a pseudopotential family is installed, installing it if necessary.
- Supports PseudoDojo families with labels like:
+ Any already-installed family is used as it stands, whatever its label. A
+ label that names no installed family is downloaded, which koopmans can do
+ for three families:
'PseudoDojo/0.4/LDA/SR/standard/upf'
-
- SSSP families with labels like:
'SSSP/1.3/PBEsol/efficiency'
-
- And SG15 ONCV families with labels like:
'SG15/1.2/PBE/SR'
Raises:
- ValueError: If the family format is not recognized or installation fails.
+ ValueError: If no family carries the label and koopmans cannot
+ download it, or if the download fails.
"""
from aiida.common.exceptions import NotExistent
from aiida_pseudo.groups.family import PseudoPotentialFamily
@@ -58,7 +57,10 @@ def pseudo_family_has_cutoffs(pseudo_family: str) -> bool:
def install_pseudo_family(pseudo_family: str) -> None:
- """Install a pseudopotential family. Parse the label and dispatch."""
+ """Download and install a pseudopotential family. Parse the label and dispatch.
+
+ No family may already carry the label.
+ """
parts = pseudo_family.split("/")
if parts[0] == "PseudoDojo" and len(parts) == 6:
@@ -69,8 +71,16 @@ def install_pseudo_family(pseudo_family: str) -> None:
_install_sg15_family(pseudo_family, parts)
else:
raise ValueError(
- f"Unrecognized pseudo family format: '{pseudo_family}'. "
- "Expected 'PseudoDojo/version/functional/relativistic/protocol/format', "
+ f"No installed pseudopotential family has the label '{pseudo_family}', and "
+ "koopmans cannot download one under that label.\n"
+ "Install the pseudopotentials yourself, from a directory holding one file "
+ "per element:\n"
+ f" aiida-pseudo install family {pseudo_family}\n"
+ "A family installed this way publishes no recommended cutoffs, so set "
+ "`calculator_parameters.ecutwfc` in your input file; `ecutrho` follows at "
+ "four times it.\n"
+ "Alternatively, name a family koopmans can download for you, as "
+ "'PseudoDojo/version/functional/relativistic/protocol/format', "
"'SSSP/version/functional/protocol', "
"or 'SG15/version/functional/relativistic'."
)
diff --git a/src/koopmans/input_file/workflow.py b/src/koopmans/input_file/workflow.py
index 513d42e..79dbda8 100644
--- a/src/koopmans/input_file/workflow.py
+++ b/src/koopmans/input_file/workflow.py
@@ -64,7 +64,7 @@ class WorkflowConfig(BaseModel):
default=True, description="whether or not to calculate the screening parameters ab-initio"
)
pseudo_library: str = Field(
- description="the pseudopotential library to use (for valid options, run `koopmans pseudos list`)"
+ description="the label of the pseudopotential family to use. Any family you have installed yourself is used as it stands, whatever its label; a label naming no installed family is downloaded, which koopmans can do for 'PseudoDojo/version/functional/relativistic/protocol/format', 'SSSP/version/functional/protocol' and 'SG15/version/functional/relativistic'. A family that publishes no recommended cutoffs takes them from `calculator_parameters.ecutwfc` instead"
)
screening_method: CalculateScreeningMethod = Field(
default=CalculateScreeningMethod.DSCF,
diff --git a/tests/fixtures.py b/tests/fixtures.py
index fef014c..caf7085 100644
--- a/tests/fixtures.py
+++ b/tests/fixtures.py
@@ -521,11 +521,11 @@ def _install_fake_family(
The streams are enough for ``UpfData`` validation, not physically
meaningful pseudos. ``cutoffs=True`` builds a
``CutoffsPseudoPotentialFamily`` with recommended cutoffs — needed by
- builders that call ``get_builder_from_protocol`` eagerly at build time
- (the aiida-qe protocol machinery only accepts SSSP, PseudoDojo, or a
- cutoffs family); plain families cover ``ensure_pseudo_family_installed``.
- ``recommended_cutoffs=False`` leaves that cutoffs family with no stringency
- defined, the shape ``_install_sg15_family`` produces.
+ a build that states none of its own; ``cutoffs=False`` builds a plain
+ ``PseudoPotentialFamily``, the shape both ``aiida-pseudo install family``
+ and ``_install_sg15_family`` produce.
+ ``recommended_cutoffs=False`` leaves the cutoffs family with no stringency
+ defined, the shape ``-F pseudo.family.cutoffs`` produces on its own.
``has_so=True`` marks every pseudo fully relativistic.
"""
from aiida.common.exceptions import NotExistent
@@ -575,11 +575,12 @@ def fake_sg15_cutoffs_family(aiida_profile: Any) -> Any:
@pytest.fixture
def fake_sg15_family_without_cutoffs(aiida_profile: Any) -> Any:
- """Install ``SG15/1.2/PBE/FR`` in the shape ``_install_sg15_family`` produces.
+ """Install ``SG15/1.2/PBE/FR`` as a cutoffs family with no stringency defined.
- A ``CutoffsPseudoPotentialFamily`` with no stringency defined, so it can
- recommend no cutoffs. A label of its own so it coexists with the other
- SG15 fixtures in one session profile.
+ The half-configured shape a user reaches by passing
+ ``-F pseudo.family.cutoffs`` and never running ``aiida-pseudo family
+ cutoffs set``: it can recommend no cutoffs. A label of its own so it
+ coexists with the other SG15 fixtures in one session profile.
"""
return _install_fake_family(
"SG15/1.2/PBE/FR", {"Si": 4.0}, cutoffs=True, recommended_cutoffs=False
diff --git a/tests/test_pseudos.py b/tests/test_pseudos.py
new file mode 100644
index 0000000..07a9028
--- /dev/null
+++ b/tests/test_pseudos.py
@@ -0,0 +1,55 @@
+"""Tests for resolving the ``pseudo_library`` label to a pseudopotential family."""
+
+from __future__ import annotations
+
+from typing import Any
+
+import pytest
+
+
+class TestEnsurePseudoFamilyInstalled:
+ """``ensure_pseudo_family_installed`` downloads only what is missing."""
+
+ def test_installed_family_under_arbitrary_label_is_used_as_is(
+ self, fake_user_built_family: Any, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
+ """A label matching no download format resolves to the installed family."""
+ from koopmans.aiida.setup import pseudos
+
+ def _fail(label: str) -> None:
+ raise AssertionError(f"attempted to download '{label}'")
+
+ monkeypatch.setattr(pseudos, "install_pseudo_family", _fail)
+
+ pseudos.ensure_pseudo_family_installed(fake_user_built_family.label)
+
+ def test_uninstallable_label_reports_both_routes(self, aiida_profile_clean: Any) -> None:
+ """An unknown label names the install command and the download formats."""
+ from koopmans.aiida.setup import pseudos
+
+ with pytest.raises(ValueError) as excinfo:
+ pseudos.ensure_pseudo_family_installed("my-gaas-fr")
+
+ message = str(excinfo.value)
+ assert "No installed pseudopotential family has the label 'my-gaas-fr'" in message
+ assert "aiida-pseudo install family my-gaas-fr\n" in message
+ assert "calculator_parameters.ecutwfc" in message
+ assert "PseudoDojo/version/functional/relativistic/protocol/format" in message
+
+ def test_the_install_command_asks_for_no_cutoffs_family(self, aiida_profile_clean: Any) -> None:
+ """The command offered is the plain-family one, with no cutoffs to set.
+
+ A family built with ``-F pseudo.family.cutoffs`` and no
+ ``aiida-pseudo family cutoffs set`` can recommend no cutoffs, which is
+ the half-configured shape the message must not send anyone to; the
+ cutoffs come from the input file instead.
+ """
+ from koopmans.aiida.setup import pseudos
+
+ with pytest.raises(ValueError) as excinfo:
+ pseudos.ensure_pseudo_family_installed("my-gaas-fr")
+
+ message = str(excinfo.value)
+ assert "-F" not in message
+ assert "pseudo.family.cutoffs" not in message
+ assert "cutoffs set" not in message