From af6decd3add2ef77dbefc3f3c66136a84891dd98 Mon Sep 17 00:00:00 2001 From: Marvin Poul Date: Wed, 19 Jul 2023 16:15:36 +0200 Subject: [PATCH 1/9] Add pyxtal wrapper Co-authored-by: Jan Janssen --- structuretoolkit/build/pyxtal.py | 102 +++++++++++++++++++++++++++++++ tests/test_pyxtal.py | 36 +++++++++++ 2 files changed, 138 insertions(+) create mode 100644 structuretoolkit/build/pyxtal.py create mode 100644 tests/test_pyxtal.py diff --git a/structuretoolkit/build/pyxtal.py b/structuretoolkit/build/pyxtal.py new file mode 100644 index 000000000..30ca60c4b --- /dev/null +++ b/structuretoolkit/build/pyxtal.py @@ -0,0 +1,102 @@ +# 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 + +from ase import Atoms +from tqdm.auto import tqdm +from pyxtal import pyxtal as _pyxtal +from pyxtal.msg import Comp_CompatibilityError +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 + """ + 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 diff --git a/tests/test_pyxtal.py b/tests/test_pyxtal.py new file mode 100644 index 000000000..727aacf34 --- /dev/null +++ b/tests/test_pyxtal.py @@ -0,0 +1,36 @@ +from unittest import TestCase +from ase import Atoms +from structuretoolkit.build.pyxtal import pyxtal + +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!"): + pyxtal(1, species=['Fe'], num_ions=[1,2]) + + with self.assertRaises(ValueError, msg="No error raised when num_ions and species do not match!"): + pyxtal(1, species=['Fe', 'Cr'], num_ions=[1]) + + try: + 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!"): + 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(pyxtal(1, species=['Fe'], num_ions=[1]), Atoms, + "returned not an Atoms with scalar arguments") + self.assertIsInstance(pyxtal([1,2], species=['Fe'], num_ions=[1]), list, + "returned not a StructureStorage with multiple groups") + self.assertIsInstance(pyxtal(1, species=['Fe'], num_ions=[1], repeat=5), list, + "returned not a StructureStorage with repeat given") + self.assertEqual(len(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 pyxtal(1, species=['Fe'], num_ions=[1], repeat=5)), + "returned list should contain only dicts") From db701556bfaaa69a4a2f4ba54f63db5ad11f1122 Mon Sep 17 00:00:00 2001 From: Marvin Poul Date: Wed, 19 Jul 2023 16:25:06 +0200 Subject: [PATCH 2/9] Add dependency --- .ci_support/environment.yml | 1 + setup.py | 1 + 2 files changed, 2 insertions(+) diff --git a/.ci_support/environment.yml b/.ci_support/environment.yml index 94a73ba6b..bb24db424 100644 --- a/.ci_support/environment.yml +++ b/.ci_support/environment.yml @@ -15,3 +15,4 @@ dependencies: - scipy =1.10.1 - spglib =2.0.2 - sqsgenerator =0.2 +- pyxtal =0.5.8 diff --git a/setup.py b/setup.py index 0d034a3d4..f78b92096 100644 --- a/setup.py +++ b/setup.py @@ -44,6 +44,7 @@ "symmetry": ['spglib==2.0.2'], "surface": ['spglib==2.0.2', 'pymatgen==2023.5.31'], "phonopy": ['phonopy==2.19.1', 'spglib==2.0.2'], + "pyxtal": ['pyxtal==0.5.8'] }, cmdclass=versioneer.get_cmdclass(), ) From 9feddded30d8e837f27e20667242118882b0c032 Mon Sep 17 00:00:00 2001 From: Marvin Poul Date: Wed, 19 Jul 2023 16:58:26 +0200 Subject: [PATCH 3/9] Skip test if pyxtal not installed Co-authored-by: Jan Janssen --- tests/test_pyxtal.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_pyxtal.py b/tests/test_pyxtal.py index 727aacf34..5dd250fb9 100644 --- a/tests/test_pyxtal.py +++ b/tests/test_pyxtal.py @@ -2,6 +2,15 @@ from ase import Atoms from structuretoolkit.build.pyxtal import pyxtal + +try: + import pyxtal + skip_pyxtal_test = False +except ImportError: + skip_pyxtal_test = True + + +@unittest.skipIf(skip_pyxtal_test, "pyxtal is not installed, so the pyxtal tests are skipped.") class TestPyxtal(TestCase): def test_args_raised(self): From 34e83bf64162f96dae8721e27ab1f16a696cecc0 Mon Sep 17 00:00:00 2001 From: Marvin Poul Date: Wed, 19 Jul 2023 17:05:12 +0200 Subject: [PATCH 4/9] Restructure so that pyxtal is optional gracefully --- structuretoolkit/build/pyxtal.py | 175 ++++++++++++++++--------------- 1 file changed, 91 insertions(+), 84 deletions(-) diff --git a/structuretoolkit/build/pyxtal.py b/structuretoolkit/build/pyxtal.py index 30ca60c4b..67b6b455d 100644 --- a/structuretoolkit/build/pyxtal.py +++ b/structuretoolkit/build/pyxtal.py @@ -5,98 +5,105 @@ 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 tqdm.auto import tqdm -from pyxtal import pyxtal as _pyxtal -from pyxtal.msg import Comp_CompatibilityError from structuretoolkit.common.helper import center_coordinates_in_unit_cell +try: + from pyxtal import pyxtal as _pyxtal + from pyxtal.msg import Comp_CompatibilityError -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) + 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. - 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. + `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) - 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 + 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. - 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 + 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 - 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 - """ - 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)) + 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 - def generate(group): - s = _pyxtal() - try: - s.from_random( - dim=dim, group=group, species=species, numIons=num_ions, **kwargs + 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 + """ + if len(species) != len(num_ions): + raise ValueError( + "species and num_ions must be of same length, " + f"not {species} and {num_ions}!" ) - 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 + stoich = "".join(f"{s}{n}" for s, n in zip(species, num_ions)) - # 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 + 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 +except ImportError: + warnings.warning("pyxtal not installed!") From 7eecb9c2c065eac38df4b4d048fb88160b6685cd Mon Sep 17 00:00:00 2001 From: Marvin Poul Date: Wed, 19 Jul 2023 17:12:53 +0200 Subject: [PATCH 5/9] Add missing import --- tests/test_pyxtal.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_pyxtal.py b/tests/test_pyxtal.py index 5dd250fb9..d9b9b01b8 100644 --- a/tests/test_pyxtal.py +++ b/tests/test_pyxtal.py @@ -1,4 +1,4 @@ -from unittest import TestCase +from unittest import TestCase, skipIf from ase import Atoms from structuretoolkit.build.pyxtal import pyxtal @@ -10,7 +10,7 @@ skip_pyxtal_test = True -@unittest.skipIf(skip_pyxtal_test, "pyxtal is not installed, so the pyxtal tests are skipped.") +@skipIf(skip_pyxtal_test, "pyxtal is not installed, so the pyxtal tests are skipped.") class TestPyxtal(TestCase): def test_args_raised(self): From e9bd816a3172819fc3d4091a332c83044939dfcb Mon Sep 17 00:00:00 2001 From: Marvin Poul Date: Wed, 19 Jul 2023 17:49:55 +0200 Subject: [PATCH 6/9] Swap imports to avoid shadowing pyxtal --- tests/test_pyxtal.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_pyxtal.py b/tests/test_pyxtal.py index d9b9b01b8..c9213f205 100644 --- a/tests/test_pyxtal.py +++ b/tests/test_pyxtal.py @@ -1,8 +1,3 @@ -from unittest import TestCase, skipIf -from ase import Atoms -from structuretoolkit.build.pyxtal import pyxtal - - try: import pyxtal skip_pyxtal_test = False @@ -10,6 +5,11 @@ skip_pyxtal_test = True +from unittest import TestCase +from ase import Atoms +from structuretoolkit.build.pyxtal import pyxtal + + @skipIf(skip_pyxtal_test, "pyxtal is not installed, so the pyxtal tests are skipped.") class TestPyxtal(TestCase): From ff4f61316a60834ad16050f9223a9ed69c441eab Mon Sep 17 00:00:00 2001 From: Marvin Poul Date: Wed, 19 Jul 2023 18:02:48 +0200 Subject: [PATCH 7/9] Update test_pyxtal.py --- tests/test_pyxtal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pyxtal.py b/tests/test_pyxtal.py index c9213f205..54e50b651 100644 --- a/tests/test_pyxtal.py +++ b/tests/test_pyxtal.py @@ -5,7 +5,7 @@ skip_pyxtal_test = True -from unittest import TestCase +from unittest import TestCase, skipIf from ase import Atoms from structuretoolkit.build.pyxtal import pyxtal From 9738ea5d23f62c45acf64902e28815438aa8fd9e Mon Sep 17 00:00:00 2001 From: Jan Janssen Date: Wed, 19 Jul 2023 13:58:37 -0600 Subject: [PATCH 8/9] Updates to pyxtal --- structuretoolkit/build/__init__.py | 1 + structuretoolkit/build/pyxtal.py | 109 ----------------------------- structuretoolkit/build/random.py | 107 ++++++++++++++++++++++++++++ tests/test_pyxtal.py | 28 ++++---- 4 files changed, 122 insertions(+), 123 deletions(-) delete mode 100644 structuretoolkit/build/pyxtal.py create mode 100644 structuretoolkit/build/random.py diff --git a/structuretoolkit/build/__init__.py b/structuretoolkit/build/__init__.py index fdc7eebf3..27251a9a5 100644 --- a/structuretoolkit/build/__init__.py +++ b/structuretoolkit/build/__init__.py @@ -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, diff --git a/structuretoolkit/build/pyxtal.py b/structuretoolkit/build/pyxtal.py deleted file mode 100644 index 67b6b455d..000000000 --- a/structuretoolkit/build/pyxtal.py +++ /dev/null @@ -1,109 +0,0 @@ -# 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 - -try: - from pyxtal import pyxtal as _pyxtal - from pyxtal.msg import Comp_CompatibilityError - - 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 - """ - 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 -except ImportError: - warnings.warning("pyxtal not installed!") diff --git a/structuretoolkit/build/random.py b/structuretoolkit/build/random.py new file mode 100644 index 000000000..c84289171 --- /dev/null +++ b/structuretoolkit/build/random.py @@ -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 diff --git a/tests/test_pyxtal.py b/tests/test_pyxtal.py index 54e50b651..b0f9ff9aa 100644 --- a/tests/test_pyxtal.py +++ b/tests/test_pyxtal.py @@ -1,3 +1,8 @@ +from unittest import TestCase, skipIf +from ase import Atoms +import structuretoolkit as stk + + try: import pyxtal skip_pyxtal_test = False @@ -5,11 +10,6 @@ skip_pyxtal_test = True -from unittest import TestCase, skipIf -from ase import Atoms -from structuretoolkit.build.pyxtal import pyxtal - - @skipIf(skip_pyxtal_test, "pyxtal is not installed, so the pyxtal tests are skipped.") class TestPyxtal(TestCase): @@ -17,29 +17,29 @@ 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!"): - pyxtal(1, species=['Fe'], num_ions=[1,2]) + 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!"): - pyxtal(1, species=['Fe', 'Cr'], num_ions=[1]) + stk.build.pyxtal(1, species=['Fe', 'Cr'], num_ions=[1]) try: - pyxtal([193, 194], ['Mg'], num_ions=[1], allow_exceptions=True) + 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!"): - pyxtal(194, ['Mg'], num_ions=[1], allow_exceptions=False) + 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(pyxtal(1, species=['Fe'], num_ions=[1]), Atoms, + self.assertIsInstance(stk.build.pyxtal(1, species=['Fe'], num_ions=[1]), Atoms, "returned not an Atoms with scalar arguments") - self.assertIsInstance(pyxtal([1,2], species=['Fe'], num_ions=[1]), list, + self.assertIsInstance(stk.build.pyxtal([1, 2], species=['Fe'], num_ions=[1]), list, "returned not a StructureStorage with multiple groups") - self.assertIsInstance(pyxtal(1, species=['Fe'], num_ions=[1], repeat=5), list, + self.assertIsInstance(stk.build.pyxtal(1, species=['Fe'], num_ions=[1], repeat=5), list, "returned not a StructureStorage with repeat given") - self.assertEqual(len(pyxtal(1, species=['Fe'], num_ions=[1], repeat=5)), 5, + 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 pyxtal(1, species=['Fe'], num_ions=[1], repeat=5)), + 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") From 9d600339e92122ebc4610408176538145f30bcc5 Mon Sep 17 00:00:00 2001 From: Jan Janssen Date: Wed, 19 Jul 2023 14:12:06 -0600 Subject: [PATCH 9/9] Separate minimal environment and full environment --- .ci_support/environment_mini.yml | 10 ++++++++++ .github/workflows/mini.yml | 10 +--------- 2 files changed, 11 insertions(+), 9 deletions(-) create mode 100644 .ci_support/environment_mini.yml diff --git a/.ci_support/environment_mini.yml b/.ci_support/environment_mini.yml new file mode 100644 index 000000000..b49126baf --- /dev/null +++ b/.ci_support/environment_mini.yml @@ -0,0 +1,10 @@ +channels: +- conda-forge +dependencies: +- ase =3.22.1 +- coveralls +- coverage +- codacy-coverage +- matplotlib-base =3.7.1 +- numpy =1.24.3 +- scipy =1.10.1 \ No newline at end of file diff --git a/.github/workflows/mini.yml b/.github/workflows/mini.yml index 980a95508..069022437 100644 --- a/.github/workflows/mini.yml +++ b/.github/workflows/mini.yml @@ -19,15 +19,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: Setup environment - run: | - cp .ci_support/environment.yml environment.yml - sed -i '/aimsgb/d' environment.yml - sed -i '/phonopy/d' environment.yml - sed -i '/pymatgen/d' environment.yml - sed -i '/pyscal/d' environment.yml - sed -i '/scikit-learn/d' environment.yml - sed -i '/spglib/d' environment.yml - sed -i '/sqsgenerator/d' environment.yml + run: cp .ci_support/environment_mini.yml environment.yml - name: Setup Mambaforge uses: conda-incubator/setup-miniconda@v2 with: