Skip to content

Commit

Permalink
Merge pull request #127 from aiidateam/release/1.3.0
Browse files Browse the repository at this point in the history
Release/1.3.0
  • Loading branch information
yakutovicha authored Feb 21, 2021
2 parents 3aec461 + b66e6f4 commit b98cda8
Show file tree
Hide file tree
Showing 31 changed files with 313 additions and 202 deletions.
39 changes: 39 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Continuous Integration

on:
[push, pull_request]

jobs:

test-plugin:

runs-on: ubuntu-latest
timeout-minutes: 30

steps:

- uses: actions/checkout@v2

- name: Install docker
run: |
sudo apt-get update
sudo apt-get install apt-transport-https ca-certificates curl software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
sudo apt-get update
sudo apt-get install docker-ce
- name: Build the Docker image
run: |
docker build -t aiida_cp2k_test .
- name: Run aiida_cp2k_test image and test the plugin inside the corresponding container.
run: |
export DOCKERID=`docker run -d aiida_cp2k_test`
docker exec --tty $DOCKERID wait-for-services
docker logs $DOCKERID
docker exec --tty --user aiida $DOCKERID /bin/bash -l -c 'cd /opt/aiida-cp2k/ && pre-commit run --all-files || ( git status --short ; git diff ; exit 1 )'
docker exec --tty --user aiida $DOCKERID /bin/bash -l -c 'cd /opt/aiida-cp2k/ && py.test --cov aiida_cp2k --cov-append .'
docker exec --tty --user aiida $DOCKERID /bin/bash -l -c 'cd /opt/aiida-cp2k/docs && make'
25 changes: 0 additions & 25 deletions .travis.yml

This file was deleted.

2 changes: 1 addition & 1 deletion aiida_cp2k/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
###############################################################################
"""AiiDA-CP2K plugins, parsers, workflows, etc ..."""

__version__ = "1.2.1"
__version__ = "1.3.0"

# EOF
86 changes: 64 additions & 22 deletions aiida_cp2k/calculations/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
from operator import add

from aiida.engine import CalcJob
from aiida.orm import Computer, Dict, SinglefileData, StructureData, RemoteData, BandsData
from aiida.common import CalcInfo, CodeInfo, InputValidationError
from aiida.orm import Computer, Dict, RemoteData, SinglefileData
from aiida.plugins import DataFactory

from ..utils.datatype_helpers import (
validate_basissets,
Expand All @@ -24,6 +25,10 @@
)
from ..utils import Cp2kInput

BandsData = DataFactory('array.bands') # pylint: disable=invalid-name
StructureData = DataFactory('structure') # pylint: disable=invalid-name
KpointsData = DataFactory('array.kpoints') # pylint: disable=invalid-name


class Cp2kCalculation(CalcJob):
"""This is a Cp2kCalculation, subclass of JobCalculation, to prepare input for an ab-initio CP2K calculation.
Expand All @@ -46,15 +51,18 @@ def define(cls, spec):
super(Cp2kCalculation, cls).define(spec)

# Input parameters.
spec.input('parameters', valid_type=Dict, help='the input parameters')
spec.input('structure', valid_type=StructureData, required=False, help='the main input structure')
spec.input('settings', valid_type=Dict, required=False, help='additional input parameters')
spec.input('resources', valid_type=dict, required=False, help='special settings')
spec.input('parent_calc_folder', valid_type=RemoteData, required=False, help='remote folder used for restarts')
spec.input('parameters', valid_type=Dict, help='The input parameters.')
spec.input('structure', valid_type=StructureData, required=False, help='The main input structure.')
spec.input('settings', valid_type=Dict, required=False, help='Optional input parameters.')
spec.input('parent_calc_folder',
valid_type=RemoteData,
required=False,
help='Working directory of a previously ran calculation to restart from.')
spec.input('kpoints', valid_type=KpointsData, required=False, help='Input kpoint mesh.')
spec.input_namespace('file',
valid_type=(SinglefileData, StructureData),
required=False,
help='additional input files',
help='Additional input files.',
dynamic=True)

spec.input_namespace(
Expand Down Expand Up @@ -101,7 +109,7 @@ def define(cls, spec):
spec.exit_code(301, 'ERROR_OUTPUT_READ', message='The output file could not be read.')
spec.exit_code(302, 'ERROR_OUTPUT_PARSE', message='The output file could not be parsed.')
spec.exit_code(303, 'ERROR_OUTPUT_INCOMPLETE', message='The output file was incomplete.')
spec.exit_code(304, 'ERROR_OUTPUT_CONTAINS_ABORT', message='The output file contains the word "ABORT"')
spec.exit_code(304, 'ERROR_OUTPUT_CONTAINS_ABORT', message='The output file contains the word "ABORT".')
spec.exit_code(312, 'ERROR_STRUCTURE_PARSE', message='The output structure could not be parsed.')
spec.exit_code(350, 'ERROR_UNEXPECTED_PARSER_EXCEPTION', message='The parser raised an unexpected exception.')

Expand All @@ -114,9 +122,12 @@ def define(cls, spec):
message='The ionic minimization cycle did not converge for the given thresholds.')

# Output parameters.
spec.output('output_parameters', valid_type=Dict, required=True, help='the results of the calculation')
spec.output('output_structure', valid_type=StructureData, required=False, help='optional relaxed structure')
spec.output('output_bands', valid_type=BandsData, required=False, help='optional band structure')
spec.output('output_parameters',
valid_type=Dict,
required=True,
help='The output dictionary containing results of the calculation.')
spec.output('output_structure', valid_type=StructureData, required=False, help='The relaxed output structure.')
spec.output('output_bands', valid_type=BandsData, required=False, help='Computed electronic band structure.')
spec.default_output_node = 'output_parameters'

spec.outputs.dynamic = True
Expand All @@ -130,7 +141,7 @@ def prepare_for_submission(self, folder):

# pylint: disable=too-many-statements,too-many-branches

# create cp2k input file
# Create cp2k input file.
inp = Cp2kInput(self.inputs.parameters.get_dict())
inp.add_keyword("GLOBAL/PROJECT", self._DEFAULT_PROJECT_NAME)

Expand Down Expand Up @@ -160,6 +171,16 @@ def prepare_for_submission(self, folder):
validate_pseudos(inp, self.inputs.pseudos, self.inputs.structure if 'structure' in self.inputs else None)
write_pseudos(inp, self.inputs.pseudos, folder)

# Kpoints.
if 'kpoints' in self.inputs:
try:
mesh, _ = self.inputs.kpoints.get_kpoints_mesh()
except AttributeError:
raise InputValidationError("K-point sampling for SCF must be given in mesh form.")

inp.add_keyword('FORCE_EVAL/DFT/KPOINTS', {'WAVEFUNCTIONS': ' COMPLEX', 'FULL_GRID': '.TRUE.'})
inp.add_keyword('FORCE_EVAL/DFT/KPOINTS/SCHEME MONKHORST-PACK', f'{mesh[0]} {mesh[1]} {mesh[2]}')

with io.open(folder.get_abs_path(self._DEFAULT_INPUT_FILE), mode="w", encoding="utf-8") as fobj:
try:
fobj.write(inp.render())
Expand Down Expand Up @@ -214,21 +235,42 @@ def prepare_for_submission(self, folder):

# Check for left over settings.
if settings:
raise InputValidationError("The following keys have been found " +
"in the settings input node {}, ".format(self.pk) + "but were not understood: " +
",".join(settings.keys()))
raise InputValidationError(
f"The following keys have been found in the settings input node {self.pk}, but were not understood: " +
",".join(settings.keys()))

return calcinfo

@staticmethod
def _write_structure(structure, folder, name):
"""Function that writes a structure and takes care of element tags."""

# Create file with the structure.
s_ase = structure.get_ase()
elem_tags = ['' if t == 0 else str(t) for t in s_ase.get_tags()]
elem_symbols = list(map(add, s_ase.get_chemical_symbols(), elem_tags))
elem_coords = ['{:25.16f} {:25.16f} {:25.16f}'.format(p[0], p[1], p[2]) for p in s_ase.get_positions()]
xyz = _atoms_to_xyz(structure.get_ase())
with io.open(folder.get_abs_path(name), mode="w", encoding="utf-8") as fobj:
fobj.write(u'{}\n\n'.format(len(elem_coords)))
fobj.write(u'\n'.join(map(add, elem_symbols, elem_coords)))
fobj.write(xyz)


def kind_names(atoms):
"""Get atom kind names from ASE atoms based on tags.
Simply append the tag to element symbol. E.g., 'H' with tag 1 becomes 'H1'.
Note: This mirrors the behavior of StructureData.get_kind_names()
:param atoms: ASE atoms instance
:returns: list of kind names
"""
elem_tags = ['' if t == 0 else str(t) for t in atoms.get_tags()]
return list(map(add, atoms.get_chemical_symbols(), elem_tags))


def _atoms_to_xyz(atoms):
"""Converts ASE atoms to string, taking care of element tags.
:param atoms: ASE Atoms instance
:returns: str (in xyz format)
"""
elem_symbols = kind_names(atoms)
elem_coords = ['{:25.16f} {:25.16f} {:25.16f}'.format(p[0], p[1], p[2]) for p in atoms.get_positions()]
xyz = f'{len(elem_coords)}\n\n'
xyz += u'\n'.join(map(add, elem_symbols, elem_coords))
return xyz
2 changes: 1 addition & 1 deletion aiida_cp2k/parsers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from aiida.plugins import DataFactory

StructureData = DataFactory('structure') # pylint: disable=invalid-name
BandsData = DataFactory('array.bands') # pylint: disable=invalid-name


class Cp2kBaseParser(Parser):
Expand Down Expand Up @@ -97,7 +98,6 @@ class Cp2kAdvancedParser(Cp2kBaseParser):
def _parse_stdout(self):
"""Advanced CP2K output file parser."""

from aiida.orm import BandsData
from aiida_cp2k.utils import parse_cp2k_output_advanced

fname = self.node.process_class._DEFAULT_OUTPUT_FILE # pylint: disable=protected-access
Expand Down
41 changes: 16 additions & 25 deletions aiida_cp2k/utils/datatype_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,14 @@ def _validate_gdt_namespace(entries, gdt_cls, attr):

for kind, gdt_instance in _unpack(entries):
if not isinstance(gdt_instance, gdt_cls):
return "invalid {attr} for '{kind}' specified".format(attr=attr, kind=kind)
return f"invalid {attr} for '{kind}' specified"

identifier = _identifier(gdt_instance)

if identifier in identifiers:
# note: this should be possible for basissets with different versions
# but at this point we should require some format for the key to match it
return "{attr} for kind {gdt_instance.element} ({gdt_instance.name}) specified multiple times".format(
attr=attr, gdt_instance=gdt_instance)
return f"{attr} for kind {gdt_instance.element} ({gdt_instance.name}) specified multiple times"

identifiers += [identifier]

Expand Down Expand Up @@ -113,17 +112,14 @@ def validate_basissets(inp, basissets, structure):
try:
basisset_used[(element, bsname)] += 1
except KeyError:
raise InputValidationError(("'BASIS_SET {bstype} {bsname}' for element {element} (from kind {kind})"
" not found in basissets input namespace").format(bsname=bsname,
bstype=bstype,
element=element,
kind=kind))
raise InputValidationError(f"'BASIS_SET {bstype} {bsname}' for element {element} (from kind {kind})"
" not found in basissets input namespace")

if basisset_kw_used:
for (sym, name), used in basisset_used.items():
if not used:
raise InputValidationError("Basis sets provided in calculation for kind {sym} ({name}),"
" but not used in input".format(sym=sym, name=name))
raise InputValidationError(f"Basis sets provided in calculation for kind {sym} ({name}),"
" but not used in input")
# if all basissets are referenced in the input, we're done
return

Expand All @@ -144,9 +140,8 @@ def validate_basissets(inp, basissets, structure):
bstype = "ORB"

if label not in allowed_labels:
raise InputValidationError("Basis sets provided in calculation for kind {bset.element} ({bset.name}),"
" with label {label} could not be matched to a kind in the structure".format(
bset=bset, label=label))
raise InputValidationError(f"Basis sets provided in calculation for kind {bset.element} ({bset.name}),"
f" with label {label} could not be matched to a kind in the structure")

if "SUBSYS" not in inp["FORCE_EVAL"]:
inp["FORCE_EVAL"]["SUBSYS"] = {}
Expand All @@ -160,7 +155,7 @@ def validate_basissets(inp, basissets, structure):
inp["FORCE_EVAL"]["SUBSYS"]["KIND"].append({"_": label})
kind_sec = inp["FORCE_EVAL"]["SUBSYS"]["KIND"][-1]

kind_sec["BASIS_SET"] = "{bstype} {bset.name}".format(bstype=bstype, bset=bset)
kind_sec["BASIS_SET"] = f"{bstype} {bset.name}"
if "ELEMENT" not in kind_sec:
kind_sec["ELEMENT"] = bset.element

Expand Down Expand Up @@ -208,17 +203,14 @@ def validate_pseudos(inp, pseudos, structure):
try:
pseudo_used[(element, pname)] += 1
except KeyError:
raise InputValidationError(("'POTENTIAL {ptype} {pname}' for element {element} (from kind {kind})"
" not found in pseudos input namespace").format(pname=pname,
ptype=ptype,
element=element,
kind=kind))
raise InputValidationError(f"'POTENTIAL {ptype} {pname}' for element {element} (from kind {kind})"
" not found in pseudos input namespace")

if pseudo_kw_used:
for (sym, name), used in pseudo_used.items():
if not used:
raise InputValidationError("Pseudos provided in calculation for kind {sym} ({name}),"
" but not used in input".format(sym=sym, name=name))
raise InputValidationError(f"Pseudos provided in calculation for kind {sym} ({name}),"
" but not used in input")
return

if not structure: # no support for COORD section (yet)
Expand All @@ -233,9 +225,8 @@ def validate_pseudos(inp, pseudos, structure):

for label, pseudo in pseudos.items():
if label not in allowed_labels:
raise InputValidationError("Pseudo provided in calculation for kind {pseudo.element} ({pseudo.name}),"
" with label {label} could not be matched to a kind in the structure".format(
pseudo=pseudo, label=label))
raise InputValidationError(f"Pseudo provided in calculation for kind {pseudo.element} ({pseudo.name}),"
f" with label {label} could not be matched to a kind in the structure")

if "SUBSYS" not in inp["FORCE_EVAL"]:
inp["FORCE_EVAL"]["SUBSYS"] = {}
Expand All @@ -249,7 +240,7 @@ def validate_pseudos(inp, pseudos, structure):
inp["FORCE_EVAL"]["SUBSYS"]["KIND"].append({"_": label})
kind_sec = inp["FORCE_EVAL"]["SUBSYS"]["KIND"][-1]

kind_sec["POTENTIAL"] = "GTH {pseudo.name}".format(pseudo=pseudo)
kind_sec["POTENTIAL"] = f"GTH {pseudo.name}"

if "ELEMENT" not in kind_sec:
kind_sec["ELEMENT"] = pseudo.element
Expand Down
14 changes: 7 additions & 7 deletions aiida_cp2k/utils/input_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,29 +150,29 @@ def _render_section(output, params, indent=0):
for key, val in sorted(params.items()):
# keys are not case-insensitive, ensure that they follow the current scheme
if key.upper() != key:
raise ValueError("keyword '{}' not upper case".format(key))
raise ValueError(f"keyword '{key}' not upper case.")

if key.startswith(("@", "$")):
raise ValueError("CP2K preprocessor directives not supported")
raise ValueError("CP2K preprocessor directives not supported.")

if isinstance(val, Mapping):
line = "{}&{}".format(' ' * indent, key)
line = f"{' ' * indent}&{key}"
if "_" in val: # if there is a section parameter, add it
line += " {}".format(val.pop('_'))
line += f" {val.pop('_')}"
output.append(line)
Cp2kInput._render_section(output, val, indent + 3)
output.append('{}&END {}'.format(' ' * indent, key))
output.append(f"{' ' * indent}&END {key}")

elif isinstance(val, Sequence) and not isinstance(val, str):
for listitem in val:
Cp2kInput._render_section(output, {key: listitem}, indent)

elif isinstance(val, bool):
val_str = '.TRUE.' if val else '.FALSE.'
output.append('{}{} {}'.format(' ' * indent, key, val_str))
output.append(f"{' ' * indent}{key} {val_str}")

else:
output.append('{}{} {}'.format(' ' * indent, key, val))
output.append(f"{' ' * indent}{key} {val}")


@calcfunction
Expand Down
Loading

0 comments on commit b98cda8

Please sign in to comment.