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

Add a test for a full silicon SCF workchain #9

Merged
merged 11 commits into from
Nov 7, 2024
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:

strategy:
matrix:
python-version: ['3.8', '3.9', '3.10', '3.11']
python-version: ['3.9', '3.10', '3.11', '3.12']

services:
postgres:
Expand Down
8 changes: 4 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,22 @@ classifiers = [
'Framework :: AiiDA',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11'
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
]
keywords = ['aiida', 'workflows']
requires-python = '>=3.8'
dependencies = [
'aiida_core[atomic_tools]>=2.0',
'aiida_core[atomic_tools]>=2.5',
'aiida-pseudo',
'click~=8.0',
'h5py',
'importlib_resources',
'jsonschema',
'numpy',
'packaging'
'packaging',
'pymatgen',
]

Expand Down
255 changes: 65 additions & 190 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,132 +1,36 @@
# -*- coding: utf-8 -*-
# pylint: disable=redefined-outer-name,too-many-statements
"""Initialise a text database and profile for pytest."""
import logging
import os

import pytest

pytest_plugins = ['aiida.manage.tests.pytest_fixtures'] # pylint: disable=invalid-name


@pytest.fixture(scope='session')
def filepath_tests():
"""Return the absolute filepath of the `tests` folder.
.. warning:: if this file moves with respect to the `tests` folder, the implementation should change.
:return: absolute filepath of `tests` folder which is the basepath for all test resources.
"""
return os.path.dirname(os.path.abspath(__file__))


@pytest.fixture
def filepath_fixtures(filepath_tests):
"""Return the absolute filepath to the directory containing the file `fixtures`."""
return os.path.join(filepath_tests, 'fixtures')


@pytest.fixture(scope='function')
def fixture_sandbox():
"""Return a `SandboxFolder`."""
from aiida.common.folders import SandboxFolder
with SandboxFolder() as folder:
yield folder


@pytest.fixture
def fixture_localhost(aiida_localhost):
"""Return a localhost `Computer`."""
localhost = aiida_localhost
localhost.set_default_mpiprocs_per_machine(1)
return localhost


@pytest.fixture
def fixture_code(fixture_localhost):
"""Return a ``Code`` instance configured to run calculations of given entry point on localhost ``Computer``."""

def _fixture_code(entry_point_name):
from aiida.common import exceptions
from aiida.orm import Code

label = f'test.{entry_point_name}'

try:
return Code.objects.get(label=label) # pylint: disable=no-member
except exceptions.NotExistent:
return Code(
label=label,
input_plugin_name=entry_point_name,
remote_computer_exec=[fixture_localhost, '/bin/true'],
)

return _fixture_code


@pytest.fixture
def serialize_builder():
"""Serialize the given process builder into a dictionary with nodes turned into their value representation.
:param builder: the process builder to serialize
:return: dictionary
"""

def serialize_data(data):
# pylint: disable=too-many-return-statements
from aiida.orm import BaseType, Code, Dict
from aiida.plugins import DataFactory

StructureData = DataFactory('core.structure')
UpfData = DataFactory('pseudo.upf')

if isinstance(data, dict):
return {key: serialize_data(value) for key, value in data.items()}

if isinstance(data, BaseType):
return data.value

if isinstance(data, Code):
return data.full_label
pytest_plugins = 'aiida.manage.tests.pytest_fixtures'

if isinstance(data, Dict):
return data.get_dict()
_LOGGER = logging.getLogger(__name__)
_julia_project_path = os.path.join(__file__, "..", "julia_environment")

if isinstance(data, StructureData):
return data.get_formula()

if isinstance(data, UpfData):
return f'{data.element}<md5={data.md5}>'
def pytest_sessionstart():
"""Instantiates the test Julia environment before any test runs."""
import subprocess

return data

def _serialize_builder(builder):
return serialize_data(builder._inputs(prune=True)) # pylint: disable=protected-access

return _serialize_builder
# Pkg.Registry.add() seems necessary for GitHub Actions
subprocess.run(['julia', f'--project={_julia_project_path}', '-e', 'using Pkg; Pkg.Registry.add(); Pkg.resolve(); Pkg.precompile();'], check=True)


@pytest.fixture
def generate_calc_job():
"""Fixture to construct a new `CalcJob` instance and call `prepare_for_submission` for testing `CalcJob` classes.
The fixture will return the `CalcInfo` returned by `prepare_for_submission` and the temporary folder that was passed
to it, into which the raw input files will have been written.
"""

def _generate_calc_job(folder, entry_point_name, inputs=None):
"""Fixture to generate a mock `CalcInfo` for testing calculation jobs."""
from aiida.engine.utils import instantiate_process
from aiida.manage.manager import get_manager
from aiida.plugins import CalculationFactory

manager = get_manager()
runner = manager.get_runner()

process_class = CalculationFactory(entry_point_name)
process = instantiate_process(runner, process_class, **inputs)

calc_info = process.prepare_for_submission(folder)

return calc_info

return _generate_calc_job
def get_dftk_code(aiida_local_code_factory):
"""Return an ``InstalledCode`` instance configured to run DFTK calculations on localhost."""

def _get_code():
return aiida_local_code_factory(
'dftk',
'julia',
label='dftk',
prepend_text=f"""\
export JULIA_PROJECT="{_julia_project_path}"
""",
)

return _get_code

@pytest.fixture
def generate_structure():
Expand All @@ -144,6 +48,12 @@ def _generate_structure(structure_id='silicon'):
structure = StructureData(cell=cell)
structure.append_atom(position=(0., 0., 0.), symbols='Si', name='Si')
structure.append_atom(position=(param / 4., param / 4., param / 4.), symbols='Si', name='Si')
elif structure_id == 'silicon_perturbed':
param = 5.43
cell = [[param / 2., param / 2., 0], [param / 2., 0, param / 2.], [0, param / 2., param / 2.]]
structure = StructureData(cell=cell)
structure.append_atom(position=(0.05, 0., 0.), symbols='Si', name='Si')
structure.append_atom(position=(param / 4., param / 4., param / 4.), symbols='Si', name='Si')
elif structure_id == 'water':
structure = StructureData(cell=[[5.29177209, 0., 0.], [0., 5.29177209, 0.], [0., 0., 5.29177209]])
structure.append_atom(position=[12.73464656, 16.7741411, 24.35076238], symbols='H', name='H')
Expand Down Expand Up @@ -177,90 +87,55 @@ def _generate_kpoints_mesh(npoints):

return _generate_kpoints_mesh


@pytest.fixture(scope='session')
def generate_parser():
"""Fixture to load a parser class for testing parsers."""

def _generate_parser(entry_point_name):
"""Fixture to load a parser class for testing parsers.
:param entry_point_name: entry point name of the parser class
:return: the `Parser` sub class
"""
from aiida.plugins import ParserFactory
return ParserFactory(entry_point_name)

return _generate_parser


# TODO: It would be nicer to automatically download the psp through aiida-pseudo
@pytest.fixture
def generate_remote_data():
"""Return a `RemoteData` node."""

def _generate_remote_data(computer, remote_path, entry_point_name=None):
"""Return a `KpointsData` with a mesh of npoints in each direction."""
from aiida.common.links import LinkType
from aiida.orm import CalcJobNode, RemoteData
from aiida.plugins.entry_point import format_entry_point_string

entry_point = format_entry_point_string('aiida.calculations', entry_point_name)
def load_psp():
"""Return the pd_nc_sr_pbe_standard_0.4.1_upf pseudopotential for an element"""

remote = RemoteData(remote_path=remote_path)
remote.computer = computer
def _load_psp(element: str):
from aiida import plugins
from pathlib import Path

if entry_point_name is not None:
creator = CalcJobNode(computer=computer, process_type=entry_point)
creator.set_option('resources', {'num_machines': 1, 'num_mpiprocs_per_machine': 1})
remote.base.links.add_incoming(creator, link_type=LinkType.CREATE, link_label='remote_folder')
creator.store()
if element != "Si":
raise ValueError("Only the Si psp is available for the moment.")

return remote
UpfData = plugins.DataFactory('pseudo.upf')
with open((Path(__file__) / ".." / "pseudos" / (element + ".upf")).resolve(), "rb") as stream:
return UpfData(stream)

return _generate_remote_data
return _load_psp


# TODO: Something like this should exist in aiida! We shouldn't have to do it ourselves just to capture why the test failed.
@pytest.fixture
def generate_bands_data():
"""Return a `BandsData` node."""

def _generate_bands_data():
"""Return a `BandsData` instance with some basic `kpoints` and `bands` arrays."""
from aiida.plugins import DataFactory
import numpy
BandsData = DataFactory('core.array.bands') #pylint: disable=invalid-name
bands_data = BandsData()

bands_data.set_kpoints(numpy.array([[0., 0., 0.], [0.625, 0.25, 0.625]]))

bands_data.set_bands(
numpy.array([[-5.64024889, 6.66929678, 6.66929678, 6.66929678, 8.91047649],
[-1.71354964, -0.74425095, 1.82242466, 3.98697455, 7.37979746]]),
units='eV'
)
def submit_and_await_success(submit_and_await):
"""
Submits a process or process builder to the engine.
Validates that the process succeeds, or logs a report if it doesn't.
"""

return bands_data
def _submit_and_await_success(*args, **kwargs):
from aiida.cmdline.utils.common import get_calcjob_report, get_workchain_report
from aiida.orm.nodes.process import CalcJobNode, WorkChainNode

return _generate_bands_data
result = submit_and_await(*args, **kwargs)

if result.exit_status != 0:
if isinstance(result, CalcJobNode):
_LOGGER.warning("Report of CalcJobNode:")
_LOGGER.warning(get_calcjob_report(result))
elif isinstance(result, WorkChainNode):
_LOGGER.warning("Report of WorkChainNode:")
_LOGGER.warning(get_workchain_report(result, "REPORT"))

@pytest.fixture
def generate_workchain():
"""Generate an instance of a `WorkChain`."""

def _generate_workchain(entry_point, inputs):
"""Generate an instance of a `WorkChain` with the given entry point and inputs.
:param entry_point: entry point name of the work chain subclass.
:param inputs: inputs to be passed to process construction.
:return: a `WorkChain` instance.
"""
from aiida.engine.utils import instantiate_process
from aiida.manage.manager import get_manager
from aiida.plugins import WorkflowFactory
# Also log reports of all child calcjobs:
for link in result.base.links.get_outgoing(CalcJobNode):
if isinstance(link.node, CalcJobNode):
_LOGGER.warning("Report of child CalcJobNode:")
_LOGGER.warning(get_calcjob_report(link.node))

process_class = WorkflowFactory(entry_point)
runner = get_manager().get_runner()
process = instantiate_process(runner, process_class, **inputs)
assert result.exit_status == 0

return process
return result

return _generate_workchain
return _submit_and_await_success
2 changes: 2 additions & 0 deletions tests/julia_environment/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
LocalPreferences.toml
Manifest.toml
5 changes: 5 additions & 0 deletions tests/julia_environment/Project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[deps]
AiidaDFTK = "26386dbc-b74b-4d9a-b75a-41d28ada84fc"

[compat]
AiidaDFTK = "0.1"
Loading
Loading