Repository: pyiron/structuretoolkit (language: python)

Issue:
Merge main



Discussion / hints:

Fix patch:
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/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


Test patch:
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/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")