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 Wrapper for Atomsk Structure Creation #260

Merged
merged 15 commits into from
Sep 16, 2021
201 changes: 201 additions & 0 deletions pyiron_atomistics/atomistics/structure/factories/atomsk.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
# coding: utf-8
# Copyright (c) Max-Planck-Institut für Eisenforschung GmbH - Computational Materials Design (CM) Department
# Distributed under the terms of "New BSD License", see the LICENSE file.

import subprocess
import tempfile
import os.path
import shutil
import io

from pyiron_atomistics.atomistics.structure.atoms import ase_to_pyiron

from ase.io import read, write
import numpy as np

__author__ = "Marvin Poul"
__copyright__ = (
"Copyright 2021, Max-Planck-Institut für Eisenforschung GmbH - "
"Computational Materials Design (CM) Department"
)
__version__ = "1.0"
__maintainer__ = "Marvin Poul"
__email__ = "poul@mpie.de"
__status__ = "production"
__date__ = "Jun 30, 2021"

_ATOMSK_EXISTS = shutil.which("atomsk") != None

class AtomskError(Exception):
pass

class AtomskBuilder:
"""Class to build CLI arguments to Atomsk."""

def __init__(self):
self._options = []
self._structure = None

@classmethod
def create(cls, lattice, a, *species, c=None, hkl=None):
"""
Instiate new builder and add create mode.

See https://atomsk.univ-lille.fr/doc/en/mode_create.html or :method:`.AtomskFactory.create` for arguments.

Returns:
:class:`.AtomskBuilder`: self
"""

self = cls()

a_and_c = str(a) if c is None else f"{a} {c}"
line = f"--create {lattice} {a_and_c} {' '.join(species)}"
if hkl is not None:
pmrv marked this conversation as resolved.
Show resolved Hide resolved
if np.asarray(hkl).shape != (3, 3):
raise ValueError(f"hkl must have shape 3x3 if provided, not {hkl}!")
line += f" orient {' '.join(hkl[0])} {' '.join(hkl[1])} {' '.join(hkl[2])}"
# TODO: check len(species) etc. with the document list of supported phases
self._options.append(line)
return self

@classmethod
def modify(cls, structure):
""""
Instiate new builder to modify and existing structure.

See :method:`.AtomskFactory.modify` for arguments.

Returns:
:class:`.AtomskBuilder`: self
"""
self = cls()
self._structure = structure
self._options.append("input.exyz")
return self

def duplicate(self, nx, ny=None, nz=None):
"""
See https://atomsk.univ-lille.fr/doc/en/option_duplicate.html

Args:
nx (int): replicas in x directions
ny (int, optional): replicas in y directions, default to nx
nz (int, optional): replicas in z directions, default to ny

Returns:
:class:`.AtomskBuilder`: self
"""
if ny is None: ny = nx
if nz is None: nz = ny
self._options.append(f"-duplicate {nx} {ny} {nz}")
return self

def build(self):
"""
Call Atomsk with the options accumulated so far.

Returns:
:class:`.Atoms`: new structure
"""
self._options.append("- exyz") # output to stdout as exyz format
with tempfile.TemporaryDirectory() as temp_dir:
if self._structure is not None:
write(os.path.join(temp_dir, "input.exyz"), self._structure, format="extxyz")
proc = subprocess.run(["atomsk", *" ".join(self._options).split()],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to do something about the dependencies. Is it sufficient to conda-install atomsk to get this running, or would I further need to modify the command line name after installation? Is atomsk available across all architectures? How can we ImportAlarm something that's not a python package? Since we're working in atomistics and not contrib I'd like to get at least some of these answered before we merge...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Installing the conda package is enough, but there's only a linux build on conda, not sure if a windows build is possible. Using ImportAlarm is not possible since it's not a python package, but I could modify the StructureFactory to only export atomsk if it is found in the PATH.

capture_output=True, cwd=temp_dir)
output = proc.stdout.decode("utf8")
for l in output.split("\n"):
if l.strip().startswith("X!X ERROR:"):
raise AtomskError(f"atomsk returned error: {output}")
return ase_to_pyiron(read(io.StringIO(output), format="extxyz"))

def __getattr__(self, name):
# magic method to map method calls of the form self.foo_bar to options like -foo-bar; arguments converted str
# and appended after option, keyword arguments are mapped to strings like 'key value'
def meth(*args, **kwargs):
args_str = " ".join(map(str, args))
kwargs_str = " ".join(f"{k} {v}" for k, v in kwargs.items())
self._options.append(f"-{name.replace('_', '-')} {args_str} {kwargs_str}")
return self
return meth

class AtomskFactory:
"""
Wrapper around the atomsk CLI.

Use :method:`.create()` to create a new structure and :method:`.modify()` to pass an existing structure to atomsk.
Both of them return a :class:`.AtomskBuilder`, which has methods named like the flags of atomsk. Calling them with
the appropriate arguments adds the flags to the command line. Once you added all flags, call
:method:`.AtomskBuilder.build()` to create the new structure. All methods to add flags return the
:class:`AtomskBuilder` instance they are called on to allow method chaining.

>>> from pyiron_atomistics import Project
>>> pr = Project('atomsk')
>>> pr.create.structure.atomsk.create("fcc", 3.6, "Cu").duplicate(2, 1, 1).build()
Cu: [0. 0. 0.]
Cu: [1.8 1.8 0. ]
Cu: [0. 1.8 1.8]
Cu: [1.8 0. 1.8]
Cu: [3.6 0. 0. ]
Cu: [5.4 1.8 0. ]
Cu: [3.6 1.8 1.8]
Cu: [5.4 0. 1.8]
pbc: [ True True True]
cell:
Cell([7.2, 3.6, 3.6])
>>> s = pr.create.structure.atomsk.create("fcc", 3.6, "Cu").duplicate(2, 1, 1).build()
>>> pr.create.structure.atomsk.modify(s).cell("add", 3, "x").build()
Cu: [0. 0. 0.]
Cu: [1.8 1.8 0. ]
Cu: [0. 1.8 1.8]
Cu: [1.8 0. 1.8]
Cu: [3.6 0. 0. ]
Cu: [5.4 1.8 0. ]
Cu: [3.6 1.8 1.8]
Cu: [5.4 0. 1.8]
pbc: [ True True True]
cell:
Cell([10.2, 3.6, 3.6])

Methods that you call on :class:`.AtomskBuilder` are automatically translated into options, translating '_' in the
method name to '-' and appending all arguments as strings. All atomsk options are therefore supported, but no error
checking is performed whether the translated options exist or follow the syntax prescribed by atomsk, except for
special cases defined on the class.
"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copypasta examples to unit tests folder? Will need to be excluded from the CI until we get the dependencies better sorted, but should at least be there for local machines to run.


def create(self, lattice, a, *species, c=None, hkl=None):
"""
Create a new structure with Atomsk.

See https://atomsk.univ-lille.fr/doc/en/mode_create.html for supported lattices.

Call :method:`.AtomskBuilder.build()` on the returned object to actually create a structure.

Args:
lattice (str): lattice type to create
a (float): first lattice parameter
*species (list of str): chemical short symbols for the type of atoms to create, length depends on lattice
type
c (float, optional): third lattice parameter, only necessary for some lattice types
hkl (array of int, (3,3)): three hkl vectors giving the crystallographic axes that should point along the x,
y, z directions

Returns:
AtomskBuilder: builder instances
"""
return AtomskBuilder.create(lattice, a, *species, c=c, hkl=hkl)

def modify(self, structure):
"""
Modify existing structure with Atomsk.

Call :method:`.AtomskBuilder.build()` on the returned object to actually create a structure.

Args:
structure (:class:`.Atoms`): input structure

Returns:
AtomskBuilder: builder instances
"""
return AtomskBuilder.modify(structure)
9 changes: 8 additions & 1 deletion pyiron_atomistics/atomistics/structure/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
)
import numpy as np
from pyiron_atomistics.atomistics.structure.factories.ase import AseFactory
from pyiron_atomistics.atomistics.structure.factories.atomsk import AtomskFactory, _ATOMSK_EXISTS
from pyiron_atomistics.atomistics.structure.factories.aimsgb import AimsgbFactory
from pyiron_atomistics.atomistics.structure.pyironase import publication as publication_ase
from pyiron_atomistics.atomistics.structure.atoms import CrystalStructure, Atoms, \
Expand All @@ -48,16 +49,22 @@

s = Settings()


class StructureFactory(PyironFactory):
def __init__(self):
self._ase = AseFactory()
if _ATOMSK_EXISTS:
self._atomsk = AtomskFactory()
self._aimsgb = AimsgbFactory()

@property
def ase(self):
return self._ase

if _ATOMSK_EXISTS:
@property
def atomsk(self):
return self._atomsk

@property
def aimsgb(self):
return self._aimsgb
Expand Down
57 changes: 57 additions & 0 deletions tests/atomistics/structure/factories/test_atomsk.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# coding: utf-8
# Copyright (c) Max-Planck-Institut für Eisenforschung GmbH - Computational Materials Design (CM) Department
# Distributed under the terms of "New BSD License", see the LICENSE file.

from pyiron_atomistics._tests import TestWithProject
from pyiron_atomistics.atomistics.structure.factories.atomsk import AtomskFactory, AtomskError, _ATOMSK_EXISTS

if _ATOMSK_EXISTS:
class TestAtomskFactory(TestWithProject):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.atomsk = AtomskFactory()

def test_create_fcc_cubic(self):
"""Should create cubic fcc cell."""

try:
structure = self.atomsk.create('fcc', 3.6, 'Cu').build()
except Exception as e:
self.fail(f"atomsk create fails with {e}.")

self.assertEqual(len(structure), 4, "Wrong number of atoms.")
self.assertTrue(all(structure.cell[i, i] == 3.6 for i in range(3)),
"Wrong lattice parameters {structure.cell} != 3.6.")

try:
duplicate = self.atomsk.create('fcc', 3.6, 'Cu').duplicate(2, 1, 1).build()
except Exception as e:
self.fail(f"chaining duplicate after atomsk create fails with {e}.")

self.assertEqual(len(duplicate), 8, "Wrong number of atoms.")
self.assertEqual(duplicate.cell[0, 0], 7.2,
"Wrong lattice parameter in duplicate direction.")

def test_modify(self):
"""Should correctly modify passed in structures."""

structure = self.atomsk.create('fcc', 3.6, 'Cu').build()
try:
duplicate = self.atomsk.modify(structure).duplicate(2, 1, 1).build()
except Exception as e:
self.fail(f"atomsk modify fails with {e}.")

self.assertEqual(len(structure) * 2, len(duplicate), "Wrong number of atoms.")

def test_error(self):
"""Should raise AtomskError on errors during call to atomsk."""

try:
self.atomsk.create('fcc', 3.6, 'Cu').foo_bar(42).build()
except AtomskError:
pass
except Exception as e:
self.fail(f"Wrong error {e} raised on non-existing option passed.")
else:
self.fail("No error raised on non-existing option passed.")