Skip to content
Merged
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
5 changes: 3 additions & 2 deletions .ci_support/environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ dependencies:
- codacy-coverage
- matplotlib-base =3.7.2
- nglview =3.0.6
- numpy =1.25.1
- numpy =1.24.3
- phonopy =2.20.0
- plotly =5.15.0
- pymatgen =2023.7.17
- pymatgen =2023.7.20
- pyscal =2.10.18
- scikit-learn =1.3.0
- scipy =1.11.1
- spglib =2.0.2
- sqsgenerator =0.2
- pyxtal =0.5.8
2 changes: 1 addition & 1 deletion .ci_support/environment_mini.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ dependencies:
- codacy-coverage
- matplotlib-base =3.7.2
- numpy =1.25.1
- scipy =1.11.1
- scipy =1.11.1
7 changes: 4 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,19 @@
install_requires=[
'ase==3.22.1',
'matplotlib==3.7.2', # ase already requires matplotlib
'numpy==1.25.1', # ase already requires numpy
'numpy==1.24.3', # ase already requires numpy
'scipy==1.11.1', # ase already requires scipy
],
extras_require={
"grainboundary": ['aimsgb==1.0.1', 'pymatgen==2023.7.17'],
"grainboundary": ['aimsgb==1.0.1', 'pymatgen==2023.7.20'],
"pyscal": ['pyscal2==2.10.18'],
"nglview": ['nglview==3.0.6'],
"plotly": ['plotly==5.15.0'],
"clusters": ['scikit-learn==1.3.0'],
"symmetry": ['spglib==2.0.2'],
"surface": ['spglib==2.0.2', 'pymatgen==2023.7.17'],
"surface": ['spglib==2.0.2', 'pymatgen==2023.7.20'],
"phonopy": ['phonopy==2.20.0', 'spglib==2.0.2'],
"pyxtal": ['pyxtal==0.5.8']
},
cmdclass=versioneer.get_cmdclass(),
)
1 change: 1 addition & 0 deletions structuretoolkit/build/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
get_grainboundary_info
)
from structuretoolkit.build.compound import B2, C14, C15, C36, D03
from structuretoolkit.build.random import pyxtal
from structuretoolkit.build.sqs import sqs_structures
from structuretoolkit.build.surface import (
high_index_surface,
Expand Down
107 changes: 107 additions & 0 deletions structuretoolkit/build/random.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# 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 typing import Union, List, Tuple
import warnings

try:
from tqdm.auto import tqdm
except ImportError:
tqdm = lambda x: x

from ase import Atoms
from structuretoolkit.common.helper import center_coordinates_in_unit_cell


def pyxtal(
group: Union[int, List[int]],
species: Tuple[str],
num_ions: Tuple[int],
dim=3,
repeat=1,
allow_exceptions=True,
**kwargs,
) -> Union[Atoms, List[dict]]:
"""
Generate random crystal structures with PyXtal.

`group` must be between 1 and the largest possible value for the given dimensionality:
dim=3 => 1 - 230 (space groups)
dim=2 => 1 - 80 (layer groups)
dim=1 => 1 - 75 (rod groups)
dim=0 => 1 - 58 (point groups)

When `group` is passed as a list of integers or `repeat>1`, generate multiple structures and return them in a list
of dicts containing the keys `atoms`, `symmetry` and `repeat` for the ASE structure, the symmetry group
number and which iteration it is, respectively.

Args:
group (list of int, or int): the symmetry group to generate or a list of them
species (tuple of str): which species to include, defines the stoichiometry together with `num_ions`
num_ions (tuple of int): how many of each species to include, defines the stoichiometry together with `species`
dim (int): dimensionality of the symmetry group, 0 is point groups, 1 is rod groups, 2 is layer groups and 3 is space groups
repeat (int): how many random structures to generate
allow_exceptions (bool): when generating multiple structures, silence errors when the requested stoichiometry and symmetry group are incompatible
**kwargs: passed to `pyxtal.pyxtal` function verbatim

Returns:
:class:`~.Atoms`: the generated structure, if repeat==1 and only one symmetry group is requested
list of dict of all generated structures, if repeat>1 or multiple symmetry groups are requested

Raises:
ValueError: if `species` and `num_ions` are not of the same length
ValueError: if stoichiometry and symmetry group are incompatible and allow_exceptions==False or only one structure is requested
"""
from pyxtal import pyxtal as _pyxtal
from pyxtal.msg import Comp_CompatibilityError

if len(species) != len(num_ions):
raise ValueError(
"species and num_ions must be of same length, "
f"not {species} and {num_ions}!"
)
stoich = "".join(f"{s}{n}" for s, n in zip(species, num_ions))

def generate(group):
s = _pyxtal()
try:
s.from_random(
dim=dim, group=group, species=species, numIons=num_ions, **kwargs
)
except Comp_CompatibilityError as e:
if not allow_exceptions:
raise ValueError(
f"Symmetry group {group} incompatible with stoichiometry {stoich}!"
) from None
else:
return None
s = s.to_ase()
s = center_coordinates_in_unit_cell(structure=s)
return s

# return a single structure
if repeat == 1 and isinstance(group, int):
allow_exceptions = False
return generate(group)
else:
structures = []
if isinstance(group, int):
group = [group]
failed_groups = []
for g in tqdm(group, desc="Spacegroups"):
for i in range(repeat):
s = generate(g)
if s is None:
failed_groups.append(g)
continue
structures.append({
"atoms": s,
"symmetry": g,
"repeat": i
})
if len(failed_groups) > 0:
warnings.warn(
f'Groups [{", ".join(map(str,failed_groups))}] could not be generated with stoichiometry {stoich}!'
)
return structures
45 changes: 45 additions & 0 deletions tests/test_pyxtal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from unittest import TestCase, skipIf
from ase import Atoms
import structuretoolkit as stk


try:
import pyxtal
skip_pyxtal_test = False
except ImportError:
skip_pyxtal_test = True


@skipIf(skip_pyxtal_test, "pyxtal is not installed, so the pyxtal tests are skipped.")
class TestPyxtal(TestCase):

def test_args_raised(self):
"""pyxtal should raise appropriate errors when called with wrong arguments"""

with self.assertRaises(ValueError, msg="No error raised when num_ions and species do not match!"):
stk.build.pyxtal(1, species=['Fe'], num_ions=[1,2])

with self.assertRaises(ValueError, msg="No error raised when num_ions and species do not match!"):
stk.build.pyxtal(1, species=['Fe', 'Cr'], num_ions=[1])

try:
stk.build.pyxtal([193, 194], ['Mg'], num_ions=[1], allow_exceptions=True)
except ValueError:
self.fail("Error raised even though allow_exceptions=True was passed!")

with self.assertRaises(ValueError, msg="No error raised even though allow_exceptions=False was passed!"):
stk.build.pyxtal(194, ['Mg'], num_ions=[1], allow_exceptions=False)

def test_return_value(self):
"""pyxtal should either return Atoms or list of dict, depending on arguments"""

self.assertIsInstance(stk.build.pyxtal(1, species=['Fe'], num_ions=[1]), Atoms,
"returned not an Atoms with scalar arguments")
self.assertIsInstance(stk.build.pyxtal([1, 2], species=['Fe'], num_ions=[1]), list,
"returned not a StructureStorage with multiple groups")
self.assertIsInstance(stk.build.pyxtal(1, species=['Fe'], num_ions=[1], repeat=5), list,
"returned not a StructureStorage with repeat given")
self.assertEqual(len(stk.build.pyxtal(1, species=['Fe'], num_ions=[1], repeat=5)), 5,
"returned number of structures did not match given repeat")
self.assertTrue(all(isinstance(d, dict) for d in stk.build.pyxtal(1, species=['Fe'], num_ions=[1], repeat=5)),
"returned list should contain only dicts")