diff --git a/.ci_support/environment.yml b/.ci_support/environment.yml index bb24db424..0221d7965 100644 --- a/.ci_support/environment.yml +++ b/.ci_support/environment.yml @@ -1,18 +1,20 @@ channels: - conda-forge dependencies: -- aimsgb =0.1.3 +- aimsgb =1.0.1 - ase =3.22.1 - coveralls - coverage - codacy-coverage -- matplotlib-base =3.7.1 -- numpy =1.24.3 -- phonopy =2.19.1 -- pymatgen =2023.5.31 +- matplotlib-base =3.7.2 +- nglview =3.0.6 +- numpy =1.25.1 +- phonopy =2.20.0 +- plotly =5.15.0 +- pymatgen =2023.7.17 - pyscal =2.10.18 -- scikit-learn =1.2.2 -- scipy =1.10.1 +- scikit-learn =1.3.0 +- scipy =1.11.1 - spglib =2.0.2 - sqsgenerator =0.2 - pyxtal =0.5.8 diff --git a/.ci_support/environment_mini.yml b/.ci_support/environment_mini.yml new file mode 100644 index 000000000..887013ec6 --- /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.2 +- numpy =1.25.1 +- scipy =1.11.1 \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..46e2183a0 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: +- package-ecosystem: pip + directory: "/" + schedule: + interval: weekly + day: monday + open-pull-requests-limit: 10 \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9b8559a16..e24827f76 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 with: - python-version: "3.10" + python-version: "3.11" - name: Install dependencies run: >- 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: diff --git a/.github/workflows/pypicheck.yml b/.github/workflows/pypicheck.yml index 6cc28ed1b..da2583c4f 100644 --- a/.github/workflows/pypicheck.yml +++ b/.github/workflows/pypicheck.yml @@ -20,7 +20,7 @@ jobs: - name: Setup Mambaforge uses: conda-incubator/setup-miniconda@v2 with: - python-version: '3.10' + python-version: '3.11' miniforge-variant: Mambaforge channels: conda-forge channel-priority: strict diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index 085e17db1..6d96f23b1 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -44,11 +44,6 @@ jobs: label: linux-64-py-3-9 prefix: /usr/share/miniconda3/envs/my-env - - operating-system: ubuntu-latest - python-version: '3.8' - label: linux-64-py-3-8 - prefix: /usr/share/miniconda3/envs/my-env - steps: - uses: actions/checkout@v2 - name: Setup environment diff --git a/setup.py b/setup.py index f78b92096..c9bcb43e6 100644 --- a/setup.py +++ b/setup.py @@ -31,19 +31,19 @@ packages=find_packages(exclude=["*tests*", "*docs*", "*binder*", "*conda*", "*notebooks*", "*.ci_support*"]), install_requires=[ 'ase==3.22.1', - 'matplotlib==3.7.1', # ase already requires matplotlib - 'numpy==1.24.3', # ase already requires numpy - 'scipy==1.10.1', # ase already requires scipy + 'matplotlib==3.7.2', # ase already requires matplotlib + 'numpy==1.25.1', # ase already requires numpy + 'scipy==1.11.1', # ase already requires scipy ], extras_require={ - "grainboundary": ['aimsgb==0.1.3', 'pymatgen==2023.5.31'], + "grainboundary": ['aimsgb==1.0.1', 'pymatgen==2023.7.17'], "pyscal": ['pyscal2==2.10.18'], - "nglview": ['nglview==3.0.5'], - "plotly": ['plotly==5.14.1'], - "clusters": ['scikit-learn==1.2.2'], + "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.5.31'], - "phonopy": ['phonopy==2.19.1', 'spglib==2.0.2'], + "surface": ['spglib==2.0.2', 'pymatgen==2023.7.17'], + "phonopy": ['phonopy==2.20.0', 'spglib==2.0.2'], "pyxtal": ['pyxtal==0.5.8'] }, cmdclass=versioneer.get_cmdclass(), 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")