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

ld1 calcjob and parser for sssp app to run pseudo generation #170

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
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
Empty file.
44 changes: 44 additions & 0 deletions aiida_sssp_workflow/calculations/ld1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from aiida import orm, plugins
from aiida.common.datastructures import CalcInfo, CodeInfo
from aiida.common.folders import Folder
from aiida.engine import CalcJob
from aiida.engine.processes.process_spec import CalcJobProcessSpec

UpfData = plugins.DataFactory("pseudo.upf")


class Ld1Calculation(CalcJob):
@classmethod
def define(cls, spec: CalcJobProcessSpec):
"""Define the specification"""

super().define(spec)
# atom_info part read from configuration cards
spec.input("filename", valid_type=orm.Str)
spec.input("parameters", valid_type=orm.SinglefileData)
spec.output("output_pseudo", valid_type=UpfData)

spec.inputs["metadata"]["options"]["input_filename"].default = "aiida.in"
spec.inputs["metadata"]["options"]["output_filename"].default = "aiida.out"
spec.inputs["metadata"]["options"]["parser_name"].default = "sssp.pseudo.ld1"
spec.inputs["metadata"]["options"]["resources"].default = {
"num_machines": 1,
"num_mpiprocs_per_machine": 1,
}

def prepare_for_submission(self, folder: Folder) -> CalcInfo:
"""Prepare the calculation for submission"""

with folder.open(self.options.input_filename, "w", encoding="utf8") as handle:
handle.write(self.inputs.parameters.get_content())

codeinfo = CodeInfo()
codeinfo.code_uuid = self.inputs.code.uuid
codeinfo.stdin_name = self.options.input_filename
codeinfo.stdout_name = self.options.output_filename

calcinfo = CalcInfo()
calcinfo.codes_info = [codeinfo]
calcinfo.retrieve_list = [self.options.output_filename, self.inputs.filename.value]

return calcinfo
Empty file.
Empty file.
21 changes: 21 additions & 0 deletions aiida_sssp_workflow/parsers/ld1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import io

from aiida.parsers import Parser
from aiida import plugins

UpfData = plugins.DataFactory('pseudo.upf')

class Ld1Parser(Parser):
"""Parser for `Ld1Calculation` parse output to pseudo"""

def parse(self, **kwargs):
"""Parse the contets of output of ld1 to pseudo files"""

output_folder = self.retrieved

with output_folder.open(self.node.inputs.filename.value, 'r') as handle:
upf_content = handle.read()

pseudo = UpfData.get_or_create(io.BytesIO(upf_content.encode('utf-8')))
self.out('output_pseudo', pseudo.clone().store())

Empty file.
51 changes: 51 additions & 0 deletions examples/example_ld1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import io
from aiida.engine import run_get_node
from aiida import orm
from aiida.plugins import CalculationFactory

Ld1Calculation = CalculationFactory('sssp.pseudo.ld1')

from aiida_sssp_workflow.calculations.ld1 import Ld1Calculation

if __name__ == "__main__":
inp_str = """ &input
zed=3,
rel=0,
config='[He] 2s1 2p0',
iswitch=3,
dft='PBE'
/
&inputp
lpaw=.false.,
pseudotype=3,
file_pseudopw='pseudo.upf',
author='anonymous',
lloc=-1,
rcloc=0.6
which_augfun='PSQ',
rmatch_augfun_nc=.true.,
tm=.true.
/
4
1S 1 0 2.00 0.00 0.80 1.00 0.0
2S 2 0 1.00 0.00 0.80 1.00 0.0
2P 2 1 0.00 0.00 0.85 1.20 0.0
2P 2 1 0.00 1.00 0.85 1.20 0.0
"""

input = {
'code': orm.load_code("ld1@localhost"),
'filename': orm.Str('pseudo.upf'),
'parameters': orm.SinglefileData(file=io.BytesIO(inp_str.encode('utf-8'))),
'metadata': {
# 'dry_run': True,
'options': {
'resources': {
'num_machines': int(1)
},
'max_wallclock_seconds': int(60),
'withmpi': False,
},
}
}
run_get_node(Ld1Calculation, **input)
8 changes: 8 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ exclude = tests, test*
[options.entry_points]
console_scripts =
aiida-sssp-workflow = aiida_sssp_workflow.cli:cmd_root
aiida.parsers =
sssp.pseudo.ld1 = aiida_sssp_workflow.parsers.ld1:Ld1Parser
aiida.calculations =
sssp.pseudo.ld1 = aiida_sssp_workflow.calculations.ld1:Ld1Calculation
sssp_workflow.birch_murnaghan_fit = aiida_sssp_workflow.calculations.birch_murnaghan_fit:birch_murnaghan_fit
sssp_workflow.calculate_delta = aiida_sssp_workflow.calculations.calculate_delta:calculate_delta
sssp_workflow.calculate_delta_volume = aiida_sssp_workflow.calculations.calculate_delta_volume:calculate_delta_volume
Expand All @@ -53,6 +56,11 @@ aiida.workflows =
dev =
bumpver==2021.1114
pre-commit==2.11.1
tests =
pytest==6.2.5
pytest-regressions==2.3.1
pg8000==1.13
pgtest==1.3.2

[options.package_data]
aiida_sssp_workflow.protocol = bands.yml, criteria.yml, control.yml, converge.yml, delta.yml
Expand Down
220 changes: 204 additions & 16 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,217 @@
# -*- coding: utf-8 -*-
"""fixtures"""
import io
import pytest
import os
import shutil

import pytest
from collections.abc import Mapping

pytest_plugins = ["aiida.manage.tests.pytest_fixtures"]

# Directory where to store outputs for known inputs (usually tests/data)
DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data")
STATIC_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "_static")
pytest_plugins = ['aiida.manage.tests.pytest_fixtures'] # pylint: disable=invalid-name

@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``."""

@pytest.fixture(scope="function")
def pp_silicon_sg15():
"""
Create a aiida-pseudo pp data of sg15 silicon
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(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

@pytest.fixture
def generate_inputs_ld1(fixture_code):
"""Generate default inputs for a `OncvPseudoCalculation."""

def _generate_inputs_ld1(filename="pseudo.upf", inp_str=None):
"""Generate default inputs for a `Ld1Calculation."""
from aiida import orm

inp = inp_str or f""" &input
zed=3,
rel=0,
config='default',
iswitch=3,
dft='PBE'
/
&inputp
lpaw=.false.,
pseudotype=1,
file_pseudopw='{filename}',
author='anonymous',
lloc=-1,
rcloc=0.1
which_augfun='PSQ',
rmatch_augfun_nc=.true.,
tm=.true.
/
4
1S 1 0 2.00 0.00 0.80 1.00 0.0
2S 2 0 1.00 0.00 0.80 1.00 0.0
2P 2 1 0.00 0.00 0.85 1.20 0.0
2P 2 1 0.00 1.00 0.85 1.20 0.0"""

parameters = orm.SinglefileData(file=io.BytesIO(inp.encode('utf-8')))
inputs = {
'code': fixture_code('sssp.pseudo.ld1'),
'filename': orm.Str(f"{filename}"),
'parameters': parameters,
'metadata': {
'options': {
'resources': {
'num_machines': int(1)
},
'max_wallclock_seconds': int(60),
'withmpi': False,
}
}
}

return inputs

return _generate_inputs_ld1

@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.
"""
from aiida import plugins

UpfData = plugins.DataFactory("pseudo.upf")
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


@pytest.fixture
def generate_calc_job_node(fixture_localhost):
"""Fixture to generate a mock `CalcJobNode` for testing parsers."""

def flatten_inputs(inputs, prefix=''):
"""Flatten inputs recursively like :meth:`aiida.engine.processes.process::Process._flatten_inputs`."""
flat_inputs = []
for key, value in inputs.items():
if isinstance(value, Mapping):
flat_inputs.extend(flatten_inputs(value, prefix=prefix + key + '__'))
else:
flat_inputs.append((prefix + key, value))
return flat_inputs

def _generate_calc_job_node(
entry_point_name, computer=None, test_name=None, inputs=None, attributes=None
):
"""Fixture to generate a mock `CalcJobNode` for testing parsers.
:param entry_point_name: entry point name of the calculation class
:param computer: a `Computer` instance
:param test_name: relative path of directory with test output files in the `fixtures/{entry_point_name}` folder.
:param inputs: any optional nodes to add as input links to the corrent CalcJobNode
:param attributes: any optional attributes to set on the node

:return: `CalcJobNode` instance with an attached `FolderData` as the `retrieved` node.
"""
from aiida import orm
from aiida.common import LinkType
from aiida.plugins.entry_point import format_entry_point_string

if computer is None:
computer = fixture_localhost

filepath_folder = None

if test_name is not None:
basepath = os.path.dirname(os.path.abspath(__file__))
filepath_folder = os.path.join(basepath, 'parsers', 'fixtures', test_name)

entry_point = format_entry_point_string('aiida.calculations', entry_point_name)

node = orm.CalcJobNode(computer=computer, process_type=entry_point)
node.set_attribute('input_filename', 'aiida.in')
node.set_attribute('output_filename', 'aiida.out')
node.set_attribute('error_filename', 'aiida.err')
node.set_option('resources', {'num_machines': 1, 'num_mpiprocs_per_machine': 1})
node.set_option('max_wallclock_seconds', 1800)

if attributes:
node.set_attribute_many(attributes)

if inputs:
metadata = inputs.pop('metadata', {})
options = metadata.get('options', {})

for name, option in options.items():
node.set_option(name, option)

for link_label, input_node in flatten_inputs(inputs):
input_node.store()
node.add_incoming(input_node, link_type=LinkType.INPUT_CALC, link_label=link_label)

node.store()

if filepath_folder:
retrieved = orm.FolderData()
retrieved.put_object_from_tree(filepath_folder)

retrieved.add_incoming(node, link_type=LinkType.CREATE, link_label='retrieved')
retrieved.store()

pp_name = "Si_ONCV_PBE-1.2.upf"
pp_path = os.path.join(STATIC_DIR, pp_name)
remote_folder = orm.RemoteData(computer=computer, remote_path='/tmp')
remote_folder.add_incoming(node, link_type=LinkType.CREATE, link_label='remote_folder')
remote_folder.store()

with open(pp_path, "rb") as stream:
pseudo = UpfData(stream)
return node

yield pseudo
return _generate_calc_job_node
Loading