Skip to content

Commit

Permalink
Added conversion module
Browse files Browse the repository at this point in the history
  • Loading branch information
LaurentRDC committed Nov 12, 2018
1 parent 363c450 commit c0f370d
Show file tree
Hide file tree
Showing 13 changed files with 129 additions and 118 deletions.
1 change: 1 addition & 0 deletions crystals/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from .atom_data import NUM_TO_ELEM
from .base import AtomicStructure
from .base import Base
from .conversion import ase_atoms
from .crystal import Crystal
from .crystal import symmetry_expansion
from .lattice import Lattice
Expand Down
28 changes: 1 addition & 27 deletions crystals/atom.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ def __repr__(self):

# TODO: add `distance_from` function for atoms on a lattice
def __sub__(self, other):
""" Distance between two atoms in **fractional** coordinates. """
return np.linalg.norm(self.coords_fractional - other.coords_fractional)

def __eq__(self, other):
Expand Down Expand Up @@ -124,33 +125,6 @@ def atomic_number(self):
def mass(self):
return ELEM_TO_MASS[self.element]

def ase_atom(self, **kwargs):
"""
Returns an ``ase.Atom`` object.
Parameters
----------
kwargs
Keyword arguments are passed to the ``ase.Atom`` constructor.
Returns
-------
atom: ase.Atom
Raises
------
ImportError : If ASE is not installed
"""
import ase

return ase.Atom(
symbol=self.element,
position=self.coords_cartesian,
magmom=self.magmom,
mass=self.mass,
**kwargs
)

@property
def coords_cartesian(self):
"""
Expand Down
53 changes: 53 additions & 0 deletions crystals/conversion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
"""
Conversion between ``crystals`` data structures and other modules.
"""
import numpy as np

from .atom import Atom
from .lattice import Lattice
from .crystal import Crystal

try:
import ase
except ImportError:
WITH_ASE = False
else:
WITH_ASE = True


def ase_atoms(crystal, **kwargs):
"""
Convert a ``crystals.Crystal`` object into an ``ase.Atoms`` object.
Keyword arguments are passed to ``ase.Atoms`` constructor.
Parameters
----------
crystal : crystals.Crystal
Crystal to be converted.
Returns
-------
atoms : ase.Atoms
Group of atoms ready for ASE's routines.
Raises
------
ImportError : If ASE is not installed
"""
if not WITH_ASE:
raise ImportError("ASE is not installed/importable.")

return ase.Atoms(
symbols=[
ase.Atom(
symbol=atom.element,
position=atom.coords_cartesian,
magmom=atom.magmom,
mass=atom.mass,
)
for atom in crystal
],
cell=np.array(crystal.lattice_vectors),
**kwargs
)
26 changes: 0 additions & 26 deletions crystals/crystal.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,32 +275,6 @@ def primitive(self, symprec=1e-2):
unitcell=atoms, lattice_vectors=lattice_vectors, source=self.source
)

def ase_atoms(self, **kwargs):
"""
Create an ASE Atoms object from a Crystal.
Parameters
----------
kwargs
Keyword arguments are passed to ase.Atoms constructor.
Returns
-------
atoms : ase.Atoms
Group of atoms ready for ASE's routines.
Raises
------
ImportError : If ASE is not installed
"""
from ase import Atoms

return Atoms(
symbols=[atm.ase_atom() for atm in iter(self)],
cell=np.array(self.lattice_vectors),
**kwargs
)

def symmetry(self, symprec=1e-2, angle_tolerance=-1.0):
"""
Returns a dictionary containing space-group information. This information
Expand Down
8 changes: 4 additions & 4 deletions docs/classes/crystals.Atom.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ crystals.Atom
.. autosummary::

~Atom.__init__
~Atom.ase_atom
~Atom.debye_waller_factor
~Atom.from_ase
~Atom.transform

Expand All @@ -28,11 +26,13 @@ crystals.Atom
.. autosummary::

~Atom.atomic_number
~Atom.coords
~Atom.coords_cartesian
~Atom.coords_fractional
~Atom.displacement
~Atom.element
~Atom.lattice
~Atom.magmom
~Atom.mass
~Atom.xyz
~Atom.occupancy


1 change: 0 additions & 1 deletion docs/classes/crystals.Crystal.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ crystals.Crystal
.. autosummary::

~Crystal.__init__
~Crystal.ase_atoms
~Crystal.frac_mesh
~Crystal.from_ase
~Crystal.from_cif
Expand Down
6 changes: 6 additions & 0 deletions docs/functions/crystals.Crystal.from_ase.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
crystals.Crystal.from\_ase
==========================

.. currentmodule:: crystals

.. automethod:: Crystal.from_ase
6 changes: 6 additions & 0 deletions docs/functions/crystals.ase_atoms.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
crystals.ase\_atoms
===================

.. currentmodule:: crystals

.. autofunction:: ase_atoms
10 changes: 5 additions & 5 deletions docs/guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -284,10 +284,10 @@ between :class:`ase.Atoms` and :class:`crystals.Crystal` at will.
To create an :class:`ase.Atoms` object from a :class:`Crystal`, use the :meth:`Crystal.ase_atoms` method::

>>> from ase.calculators.abinit import Abinit
>>> from crystals import Crystal
>>> from crystals import Crystal, ase_atoms
>>>
>>> gold = Crystal.from_database('Au')
>>> ase_gold = gold.ase_atoms(calculator = Abinit(...))
>>> ase_gold = ase_atoms(gold, calculator = Abinit(...))

All keywords of the :class:`ase.Atoms` constructor are supported. To get back to a :class:`Crystal` instance::

Expand All @@ -312,15 +312,15 @@ constructor.
:class:`Atom` instances are hashable; they can be used as ``dict`` keys or stored in a ``set``.

Since we are most concerned with atoms in crystals, the coordinates here are assumed to be fractional.
The real-space position with respect to a :class:`Crystal` or :class:`Lattice` can be accessed using the
:meth:`xyz` method::
If the atom was created as part of a structure, the real-space position with respect to its parent (:class:`Crystal`
or :class:`Lattice) can be accessed using the :meth:`Atom.coords_cartesian` method::

>>> from crystals import Crystal
>>> graphite = Crystal.from_database('C')
>>>
>>> carbon = list(graphite)[-1]
>>> fractional = carbon.coords_fractional
>>> real = carbon.coords_cartesian(lattice = graphite)
>>> real = carbon.coords_cartesian

The distance between two atoms can be calculated by taking their difference::

Expand Down
11 changes: 10 additions & 1 deletion docs/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,13 @@ To help with fleshing out unit cell atoms from symmetry operators:
:nosignatures:

symmetry_expansion
lattice_system
lattice_system

Conversion between ``Crystal`` and other packages

.. autosummary::
:toctree: functions/
:nosignatures:

ase_atoms
Crystal.from_ase
20 changes: 0 additions & 20 deletions tests/test_atom.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
import unittest
from copy import deepcopy
from random import choice
from random import randint
from random import random
from random import seed
Expand All @@ -15,30 +14,11 @@
seed(23)
np.random.seed(23)

try:
import ase

ASE = True
except ImportError:
ASE = False


def random_transform():
return rotation_matrix(random(), axis=np.random.random((3,)))


@unittest.skipIf(not ASE, "ASE not installed or importable")
class TestASEAtom(unittest.TestCase):
def setUp(self):
self.atom = Atom(randint(1, 103), coords=np.random.random((3,)))

def test_back_and_forth(self):
""" Test that conversion from crystals.Atom and ase.Atom is working """
to_ase = self.atom.ase_atom()
atom2 = Atom.from_ase(to_ase)
self.assertEqual(self.atom, atom2)


class TestAtom(unittest.TestCase):
def setUp(self):
self.atom = Atom(randint(1, 103), coords=np.random.random((3,)))
Expand Down
43 changes: 43 additions & 0 deletions tests/test_conversion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-

import unittest
from random import choice
from random import randint

import numpy as np

from crystals import Crystal
from crystals import ase_atoms

try:
import ase
except ImportError:
WITH_ASE = False
else:
WITH_ASE = True


@unittest.skipIf(not WITH_ASE, "ASE not installed or importable")
class TestAseAtoms(unittest.TestCase):
def setUp(self):
name = choice(list(Crystal.builtins))
self.crystal = Crystal.from_database(name)

def test_construction(self):
""" Test that ase_atoms returns without error """
to_ase = ase_atoms(self.crystal)
self.assertEqual(len(self.crystal), len(to_ase))

def test_back_and_forth(self):
""" Test conversion to and from ase Atoms """
to_ase = ase_atoms(self.crystal)
crystal2 = Crystal.from_ase(to_ase)

# ase has different handling of coordinates which can lead to
# rounding beyond 1e-3. Therefore, we cannot compare directly sets
# self.assertSetEqual(set(self.crystal), set(crystal2))
self.assertEqual(len(self.crystal), len(crystal2))


if __name__ == "__main__":
unittest.main()
34 changes: 0 additions & 34 deletions tests/test_crystal.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,8 @@
from contextlib import suppress
from copy import copy
from copy import deepcopy
from itertools import permutations
from math import radians
from pathlib import Path
from random import choice
from random import seed

import numpy as np

Expand All @@ -20,15 +17,6 @@
from crystals.affine import rotation_matrix
from crystals.affine import transform

# seed(23)

try:
import ase
except ImportError:
ASE = False
else:
ASE = True


def connection_available():
""" Returns whether or not an internet connection is available """
Expand All @@ -42,28 +30,6 @@ def connection_available():
return False


@unittest.skipIf(not ASE, "ASE not importable")
class TestAseAtoms(unittest.TestCase):
def setUp(self):
name = choice(list(Crystal.builtins))
self.crystal = Crystal.from_database(name)

def test_construction(self):
""" Test that ase_atoms returns without error """
to_ase = self.crystal.ase_atoms()
self.assertEqual(len(self.crystal), len(to_ase))

def test_back_and_forth(self):
""" Test conversion to and from ase Atoms """
to_ase = self.crystal.ase_atoms()
crystal2 = Crystal.from_ase(to_ase)

# ase has different handling of coordinates which can lead to
# rounding beyond 1e-3. Therefore, we cannot compare directly sets
# self.assertSetEqual(set(self.crystal), set(crystal2))
self.assertEqual(len(self.crystal), len(crystal2))


class TestSpglibMethods(unittest.TestCase):
def test_symmetry_graphite(self):
""" Test that Crystal.symmetry() works correctly for graphite """
Expand Down

0 comments on commit c0f370d

Please sign in to comment.