structuretoolkit 0.0.41__tar.gz → 0.0.43__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. structuretoolkit-0.0.43/.gitignore +217 -0
  2. {structuretoolkit-0.0.41 → structuretoolkit-0.0.43}/PKG-INFO +11 -9
  3. {structuretoolkit-0.0.41 → structuretoolkit-0.0.43}/pyproject.toml +11 -8
  4. {structuretoolkit-0.0.41 → structuretoolkit-0.0.43}/src/structuretoolkit/_version.py +2 -2
  5. {structuretoolkit-0.0.41 → structuretoolkit-0.0.43}/src/structuretoolkit/analyse/symmetry.py +22 -7
  6. {structuretoolkit-0.0.41 → structuretoolkit-0.0.43}/src/structuretoolkit/build/__init__.py +6 -0
  7. structuretoolkit-0.0.43/src/structuretoolkit/build/geometry.py +141 -0
  8. structuretoolkit-0.0.43/src/structuretoolkit/build/materialsproject.py +138 -0
  9. structuretoolkit-0.0.43/src/structuretoolkit/build/sqs/__init__.py +33 -0
  10. structuretoolkit-0.0.43/src/structuretoolkit/build/sqs/_interface.py +310 -0
  11. structuretoolkit-0.0.43/src/structuretoolkit/build/sqs/_types.py +195 -0
  12. {structuretoolkit-0.0.41 → structuretoolkit-0.0.43}/src/structuretoolkit/common/pymatgen.py +1 -1
  13. structuretoolkit-0.0.41/.gitignore +0 -10
  14. structuretoolkit-0.0.41/src/structuretoolkit/build/sqs.py +0 -236
  15. {structuretoolkit-0.0.41 → structuretoolkit-0.0.43}/LICENSE +0 -0
  16. {structuretoolkit-0.0.41 → structuretoolkit-0.0.43}/README.md +0 -0
  17. {structuretoolkit-0.0.41 → structuretoolkit-0.0.43}/src/structuretoolkit/__init__.py +0 -0
  18. {structuretoolkit-0.0.41 → structuretoolkit-0.0.43}/src/structuretoolkit/analyse/__init__.py +0 -0
  19. {structuretoolkit-0.0.41 → structuretoolkit-0.0.43}/src/structuretoolkit/analyse/distance.py +0 -0
  20. {structuretoolkit-0.0.41 → structuretoolkit-0.0.43}/src/structuretoolkit/analyse/dscribe.py +0 -0
  21. {structuretoolkit-0.0.41 → structuretoolkit-0.0.43}/src/structuretoolkit/analyse/neighbors.py +0 -0
  22. {structuretoolkit-0.0.41 → structuretoolkit-0.0.43}/src/structuretoolkit/analyse/phonopy.py +0 -0
  23. {structuretoolkit-0.0.41 → structuretoolkit-0.0.43}/src/structuretoolkit/analyse/pyscal.py +0 -0
  24. {structuretoolkit-0.0.41 → structuretoolkit-0.0.43}/src/structuretoolkit/analyse/snap.py +0 -0
  25. {structuretoolkit-0.0.41 → structuretoolkit-0.0.43}/src/structuretoolkit/analyse/spatial.py +0 -0
  26. {structuretoolkit-0.0.41 → structuretoolkit-0.0.43}/src/structuretoolkit/analyse/strain.py +0 -0
  27. {structuretoolkit-0.0.41 → structuretoolkit-0.0.43}/src/structuretoolkit/build/aimsgb.py +0 -0
  28. {structuretoolkit-0.0.41 → structuretoolkit-0.0.43}/src/structuretoolkit/build/compound.py +0 -0
  29. {structuretoolkit-0.0.41 → structuretoolkit-0.0.43}/src/structuretoolkit/build/mesh.py +0 -0
  30. {structuretoolkit-0.0.41 → structuretoolkit-0.0.43}/src/structuretoolkit/build/surface.py +0 -0
  31. {structuretoolkit-0.0.41 → structuretoolkit-0.0.43}/src/structuretoolkit/common/__init__.py +0 -0
  32. {structuretoolkit-0.0.41 → structuretoolkit-0.0.43}/src/structuretoolkit/common/error.py +0 -0
  33. {structuretoolkit-0.0.41 → structuretoolkit-0.0.43}/src/structuretoolkit/common/helper.py +0 -0
  34. {structuretoolkit-0.0.41 → structuretoolkit-0.0.43}/src/structuretoolkit/common/phonopy.py +0 -0
  35. {structuretoolkit-0.0.41 → structuretoolkit-0.0.43}/src/structuretoolkit/common/pyscal.py +0 -0
  36. {structuretoolkit-0.0.41 → structuretoolkit-0.0.43}/src/structuretoolkit/visualize.py +0 -0
@@ -0,0 +1,217 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ develop-eggs/
12
+ dist/
13
+ downloads/
14
+ eggs/
15
+ .eggs/
16
+ lib/
17
+ lib64/
18
+ parts/
19
+ sdist/
20
+ var/
21
+ wheels/
22
+ share/python-wheels/
23
+ *.egg-info/
24
+ .installed.cfg
25
+ *.egg
26
+ MANIFEST
27
+
28
+ # PyInstaller
29
+ # Usually these files are written by a python script from a template
30
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
31
+ *.manifest
32
+ *.spec
33
+
34
+ # Installer logs
35
+ pip-log.txt
36
+ pip-delete-this-directory.txt
37
+
38
+ # Unit test / coverage reports
39
+ htmlcov/
40
+ .tox/
41
+ .nox/
42
+ .coverage
43
+ .coverage.*
44
+ .cache
45
+ nosetests.xml
46
+ coverage.xml
47
+ *.cover
48
+ *.py.cover
49
+ .hypothesis/
50
+ .pytest_cache/
51
+ cover/
52
+
53
+ # Translations
54
+ *.mo
55
+ *.pot
56
+
57
+ # Django stuff:
58
+ *.log
59
+ local_settings.py
60
+ db.sqlite3
61
+ db.sqlite3-journal
62
+
63
+ # Flask stuff:
64
+ instance/
65
+ .webassets-cache
66
+
67
+ # Scrapy stuff:
68
+ .scrapy
69
+
70
+ # Sphinx documentation
71
+ docs/_build/
72
+
73
+ # PyBuilder
74
+ .pybuilder/
75
+ target/
76
+
77
+ # Jupyter Notebook
78
+ .ipynb_checkpoints
79
+
80
+ # IPython
81
+ profile_default/
82
+ ipython_config.py
83
+
84
+ # pyenv
85
+ # For a library or package, you might want to ignore these files since the code is
86
+ # intended to run in multiple environments; otherwise, check them in:
87
+ # .python-version
88
+
89
+ # pipenv
90
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
91
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
92
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
93
+ # install all needed dependencies.
94
+ # Pipfile.lock
95
+
96
+ # UV
97
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
98
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
99
+ # commonly ignored for libraries.
100
+ # uv.lock
101
+
102
+ # poetry
103
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
104
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
105
+ # commonly ignored for libraries.
106
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
107
+ # poetry.lock
108
+ # poetry.toml
109
+
110
+ # pdm
111
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
112
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
113
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
114
+ # pdm.lock
115
+ # pdm.toml
116
+ .pdm-python
117
+ .pdm-build/
118
+
119
+ # pixi
120
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
121
+ # pixi.lock
122
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
123
+ # in the .venv directory. It is recommended not to include this directory in version control.
124
+ .pixi
125
+
126
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
127
+ __pypackages__/
128
+
129
+ # Celery stuff
130
+ celerybeat-schedule
131
+ celerybeat.pid
132
+
133
+ # Redis
134
+ *.rdb
135
+ *.aof
136
+ *.pid
137
+
138
+ # RabbitMQ
139
+ mnesia/
140
+ rabbitmq/
141
+ rabbitmq-data/
142
+
143
+ # ActiveMQ
144
+ activemq-data/
145
+
146
+ # SageMath parsed files
147
+ *.sage.py
148
+
149
+ # Environments
150
+ .env
151
+ .envrc
152
+ .venv
153
+ env/
154
+ venv/
155
+ ENV/
156
+ env.bak/
157
+ venv.bak/
158
+
159
+ # Spyder project settings
160
+ .spyderproject
161
+ .spyproject
162
+
163
+ # Rope project settings
164
+ .ropeproject
165
+
166
+ # mkdocs documentation
167
+ /site
168
+
169
+ # mypy
170
+ .mypy_cache/
171
+ .dmypy.json
172
+ dmypy.json
173
+
174
+ # Pyre type checker
175
+ .pyre/
176
+
177
+ # pytype static type analyzer
178
+ .pytype/
179
+
180
+ # Cython debug symbols
181
+ cython_debug/
182
+
183
+ # PyCharm
184
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
185
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
186
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
187
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
188
+ # .idea/
189
+
190
+ # Abstra
191
+ # Abstra is an AI-powered process automation framework.
192
+ # Ignore directories containing user credentials, local state, and settings.
193
+ # Learn more at https://abstra.io/docs
194
+ .abstra/
195
+
196
+ # Visual Studio Code
197
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
198
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
199
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
200
+ # you could uncomment the following to ignore the entire vscode folder
201
+ # .vscode/
202
+ # Temporary file for partial code execution
203
+ tempCodeRunnerFile.py
204
+
205
+ # Ruff stuff:
206
+ .ruff_cache/
207
+
208
+ # PyPI configuration file
209
+ .pypirc
210
+
211
+ # Marimo
212
+ marimo/_static/
213
+ marimo/_lsp/
214
+ __marimo__/
215
+
216
+ # Streamlit
217
+ .streamlit/secrets.toml
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: structuretoolkit
3
- Version: 0.0.41
3
+ Version: 0.0.43
4
4
  Summary: build, analyse and visualise atomistic structures for materials science
5
5
  Project-URL: Homepage, https://github.com/pyiron/structuretoolkit
6
6
  Project-URL: Documentation, https://github.com/pyiron/structuretoolkit
@@ -41,35 +41,37 @@ Classifier: Development Status :: 5 - Production/Stable
41
41
  Classifier: Intended Audience :: Science/Research
42
42
  Classifier: License :: OSI Approved :: BSD License
43
43
  Classifier: Operating System :: OS Independent
44
- Classifier: Programming Language :: Python :: 3.10
45
44
  Classifier: Programming Language :: Python :: 3.11
46
45
  Classifier: Programming Language :: Python :: 3.12
47
46
  Classifier: Programming Language :: Python :: 3.13
48
47
  Classifier: Topic :: Scientific/Engineering :: Physics
49
48
  Requires-Python: <3.15,>=3.10
50
- Requires-Dist: ase<=3.28.0,>=3.20.1
49
+ Requires-Dist: ase<=3.28.0,>=3.24.0
51
50
  Requires-Dist: numpy<=2.4.3,>=1.23.5
52
- Requires-Dist: scipy<=1.17.0,>=1.15.0
51
+ Requires-Dist: scipy<=1.17.1,>=1.15.0
53
52
  Provides-Extra: clusters
54
53
  Requires-Dist: scikit-learn==1.8.0; extra == 'clusters'
55
54
  Provides-Extra: dscribe
56
55
  Requires-Dist: dscribe==2.1.2; extra == 'dscribe'
57
56
  Provides-Extra: grainboundary
58
57
  Requires-Dist: aimsgb<=1.1.1,>=1.0.2; extra == 'grainboundary'
59
- Requires-Dist: pymatgen<=2026.3.23,>=2022.2.1; extra == 'grainboundary'
58
+ Requires-Dist: pymatgen==2026.3.23; extra == 'grainboundary'
60
59
  Provides-Extra: matplotlib
61
- Requires-Dist: matplotlib==3.10.8; extra == 'matplotlib'
60
+ Requires-Dist: matplotlib==3.10.9; extra == 'matplotlib'
61
+ Provides-Extra: mp-api
62
+ Requires-Dist: mp-api==0.45.15; extra == 'mp-api'
63
+ Requires-Dist: pymatgen==2026.3.23; extra == 'mp-api'
62
64
  Provides-Extra: nglview
63
65
  Requires-Dist: nglview<=4.0.1,>=2.7.7; extra == 'nglview'
64
66
  Provides-Extra: phonopy
65
- Requires-Dist: phonopy<=3.2.1,>=2.16.2; extra == 'phonopy'
67
+ Requires-Dist: phonopy<=3.5.1,>=2.21.2; extra == 'phonopy'
66
68
  Requires-Dist: spglib==2.7.0; extra == 'phonopy'
67
69
  Provides-Extra: plotly
68
- Requires-Dist: plotly<=6.6.0,>=4.14.3; extra == 'plotly'
70
+ Requires-Dist: plotly<=6.6.0,>=6.0.0; extra == 'plotly'
69
71
  Provides-Extra: pyscal
70
72
  Requires-Dist: pyscal3<=3.3.2,>=3.2.5; extra == 'pyscal'
71
73
  Provides-Extra: surface
72
- Requires-Dist: pymatgen<=2026.3.23,>=2022.2.1; extra == 'surface'
74
+ Requires-Dist: pymatgen==2026.3.23; extra == 'surface'
73
75
  Requires-Dist: spglib==2.7.0; extra == 'surface'
74
76
  Provides-Extra: symmetry
75
77
  Requires-Dist: spglib==2.7.0; extra == 'symmetry'
@@ -18,15 +18,14 @@ classifiers = [
18
18
  "License :: OSI Approved :: BSD License",
19
19
  "Intended Audience :: Science/Research",
20
20
  "Operating System :: OS Independent",
21
- "Programming Language :: Python :: 3.10",
22
21
  "Programming Language :: Python :: 3.11",
23
22
  "Programming Language :: Python :: 3.12",
24
23
  "Programming Language :: Python :: 3.13",
25
24
  ]
26
25
  dependencies = [
27
- "ase>=3.20.1,<=3.28.0",
26
+ "ase>=3.24.0,<=3.28.0",
28
27
  "numpy>=1.23.5,<=2.4.3",
29
- "scipy>=1.15.0,<=1.17.0",
28
+ "scipy>=1.15.0,<=1.17.1",
30
29
  ]
31
30
  dynamic = ["version"]
32
31
 
@@ -39,22 +38,26 @@ Repository = "https://github.com/pyiron/structuretoolkit"
39
38
  dscribe = ["dscribe==2.1.2"]
40
39
  grainboundary = [
41
40
  "aimsgb>=1.0.2,<=1.1.1",
42
- "pymatgen>=2022.2.1,<=2026.3.23",
41
+ "pymatgen==2026.3.23",
43
42
  ]
44
43
  pyscal = ["pyscal3>=3.2.5,<=3.3.2"]
45
44
  nglview = ["nglview>=2.7.7,<=4.0.1"]
46
- matplotlib = ["matplotlib==3.10.8"]
47
- plotly = ["plotly>=4.14.3,<=6.6.0"]
45
+ matplotlib = ["matplotlib==3.10.9"]
46
+ plotly = ["plotly>=6.0.0,<=6.6.0"]
48
47
  clusters = ["scikit-learn==1.8.0"]
49
48
  symmetry = ["spglib==2.7.0"]
50
49
  surface = [
51
50
  "spglib==2.7.0",
52
- "pymatgen>=2022.2.1,<=2026.3.23",
51
+ "pymatgen==2026.3.23",
53
52
  ]
54
53
  phonopy = [
55
- "phonopy>=2.16.2,<=3.2.1",
54
+ "phonopy>=2.21.2,<=3.5.1",
56
55
  "spglib==2.7.0",
57
56
  ]
57
+ mp-api = [
58
+ "mp-api==0.45.15",
59
+ "pymatgen==2026.3.23",
60
+ ]
58
61
 
59
62
  [tool.ruff]
60
63
  exclude = [".ci_support", "tests", "setup.py", "_version.py"]
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.0.41'
32
- __version_tuple__ = version_tuple = (0, 0, 41)
31
+ __version__ = version = '0.0.43'
32
+ __version_tuple__ = version_tuple = (0, 0, 43)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -4,6 +4,7 @@
4
4
  import ast
5
5
  import dataclasses
6
6
  import string
7
+ import warnings
7
8
 
8
9
  import numpy as np
9
10
  import spglib
@@ -400,26 +401,40 @@ class Symmetry(dict):
400
401
  >>> symmetry = Symmetry(structure)
401
402
  >>> len(symmetry.get_primitive_cell()) == len(basis)
402
403
  True
404
+
405
+ .. warning::
406
+ Custom arrays defined in the base structures
407
+ :attr:`ase.atoms.Atoms.arrays` and other state (.info, .calc, etc.) are not copied to the new structure!
403
408
  """
409
+ if not all(self._structure.pbc):
410
+ raise ValueError("Can only symmetrize periodic structures.")
404
411
  ret = spglib.standardize_cell(
405
412
  self._get_spglib_cell(use_elements=use_elements, use_magmoms=use_magmoms),
406
413
  to_primitive=not standardize,
407
414
  )
408
415
  if ret is None:
409
416
  raise SymmetryError(spglib.error.get_error_message())
410
- cell, positions, indices = ret
411
- positions = (cell.T @ positions.T).T
412
- new_structure = self._structure.copy()
413
- new_structure.cell = cell
414
- new_structure = new_structure[: len(indices)]
417
+ cell, scaled_positions, indices = ret
415
418
  indices_dict = {
416
419
  v: k
417
420
  for k, v in structuretoolkit.common.helper.get_species_indices_dict(
418
421
  structure=self._structure
419
422
  ).items()
420
423
  }
421
- new_structure.symbols = [indices_dict[i] for i in indices]
422
- new_structure.positions = positions
424
+ symbols = [indices_dict[i] for i in indices]
425
+ new_structure = type(self._structure)(
426
+ symbols=symbols,
427
+ scaled_positions=scaled_positions,
428
+ cell=cell,
429
+ pbc=[True, True, True],
430
+ )
431
+ keys = set(self._structure.arrays) - {"numbers", "positions"}
432
+ if len(keys) > 0:
433
+ warnings.warn(
434
+ f"Custom arrays {keys} do not carry over to new structure!",
435
+ stacklevel=2,
436
+ )
437
+
423
438
  return new_structure
424
439
 
425
440
  def get_ir_reciprocal_mesh(
@@ -2,6 +2,10 @@ from structuretoolkit.build.aimsgb import get_grainboundary_info, grainboundary
2
2
  from structuretoolkit.build.compound import B2, C14, C15, C36, D03
3
3
  from structuretoolkit.build.mesh import create_mesh
4
4
  from structuretoolkit.build.sqs import sqs_structures
5
+ from structuretoolkit.build.materialsproject import (
6
+ search as materialsproject_search,
7
+ by_id as materialsproject_by_id,
8
+ )
5
9
  from structuretoolkit.build.surface import (
6
10
  get_high_index_surface_info,
7
11
  high_index_surface,
@@ -19,4 +23,6 @@ __all__ = [
19
23
  "sqs_structures",
20
24
  "get_high_index_surface_info",
21
25
  "high_index_surface",
26
+ "materialsproject_search",
27
+ "materialsproject_by_id",
22
28
  ]
@@ -0,0 +1,141 @@
1
+ """Utilities that operate purely geometric aspects of structures."""
2
+
3
+ import numpy as np
4
+ from ase.atoms import Atoms
5
+
6
+ from structuretoolkit.analyse import get_neighbors
7
+
8
+
9
+ def repulse(
10
+ structure: Atoms,
11
+ min_dist: float = 1.5,
12
+ step_size: float = 0.2,
13
+ axis: int | None = None,
14
+ iterations: int = 100,
15
+ inplace: bool = False,
16
+ ) -> Atoms:
17
+ """Iteratively displace atoms apart until all interatomic distances exceed a minimum threshold.
18
+
19
+ For each pair of atoms closer than ``min_dist``, the atom is displaced away from its nearest
20
+ neighbour by up to ``step_size`` along the direction of the interatomic vector. The loop
21
+ repeats until all nearest-neighbour distances satisfy the minimum criterion or the iteration
22
+ limit is reached.
23
+
24
+ Args:
25
+ structure (:class:`ase.Atoms`):
26
+ Structure to modify.
27
+ min_dist (float):
28
+ Minimum interatomic distance (in Å) to enforce between every pair of atoms.
29
+ Defaults to 1.5.
30
+ step_size (float):
31
+ Maximum displacement (in Å) applied to a single atom per iteration.
32
+ Smaller values give smoother convergence but require more iterations.
33
+ Defaults to 0.2.
34
+ axis (int or None):
35
+ Cartesian axis index (0, 1, or 2) along which displacements are restricted.
36
+ When *None* (default) displacements are applied in all three directions.
37
+ iterations (int):
38
+ Maximum number of displacement steps before raising a :class:`RuntimeError`.
39
+ Defaults to 100.
40
+ inplace (bool):
41
+ If *True*, the positions of ``structure`` are modified directly.
42
+ If *False* (default), a copy is made and the original is left unchanged.
43
+
44
+ Returns:
45
+ :class:`ase.Atoms`: The structure with adjusted atomic positions. This is the
46
+ same object as ``structure`` when ``inplace=True``, or a new copy otherwise.
47
+
48
+ Raises:
49
+ RuntimeError: If the minimum distance criterion is not satisfied within
50
+ ``iterations`` steps.
51
+ """
52
+ if not inplace:
53
+ structure = structure.copy()
54
+ if axis is None:
55
+ axis = slice(None)
56
+ for _ in range(iterations):
57
+ neigh = get_neighbors(structure, num_neighbors=1)
58
+ dd = neigh.distances[:, 0]
59
+ if dd.min() >= min_dist:
60
+ break
61
+
62
+ I = dd < min_dist
63
+
64
+ dd_I = dd[I]
65
+ vv = neigh.vecs[I, 0, :].copy()
66
+ # Avoid division by zero for coincident atoms (distance == 0).
67
+ # Assign opposite fallback directions based on atom-index ordering so
68
+ # the two coincident atoms separate rather than move together.
69
+ atom_indices = np.where(I)[0]
70
+ zero_mask = dd_I == 0
71
+ if np.any(zero_mask):
72
+ neighbor_indices = neigh.indices[atom_indices[zero_mask], 0]
73
+ sign = np.where(atom_indices[zero_mask] < neighbor_indices, 1.0, -1.0)
74
+ vv[zero_mask] = sign[:, None] * np.array([1.0, 0.0, 0.0])
75
+ safe_dd = np.where(dd_I > 0, dd_I, 1.0)
76
+ vv /= safe_dd[:, None]
77
+
78
+ disp = np.clip(min_dist - dd[I], 0, step_size)
79
+
80
+ displacement = disp[:, None] * vv # (N_close, 3)
81
+ structure.positions[I, axis] -= displacement[:, axis]
82
+
83
+ else:
84
+ raise RuntimeError(f"repulse did not converge within {iterations} iterations")
85
+
86
+ return structure
87
+
88
+
89
+ def merge(
90
+ structure: "ase.Atoms", cutoff: float = 1.8, iterations: int = 10
91
+ ) -> "ase.Atoms":
92
+ """Merge pairs of atoms that are closer than ``cutoff`` by collapsing each
93
+ pair to their midpoint and deleting one of the two atoms.
94
+
95
+ The operation is applied repeatedly (up to ``iterations`` times) to handle
96
+ cases where a merge creates new close contacts.
97
+
98
+ .. note::
99
+ The structure is modified **in place**. Pass a copy if you need the
100
+ original to remain unchanged.
101
+
102
+ Args:
103
+ structure (:class:`ase.Atoms`):
104
+ Structure to modify.
105
+ cutoff (float):
106
+ Distance threshold in Ångström below which two atoms are
107
+ considered clashing and will be merged. Defaults to ``1.8``.
108
+ iterations (int):
109
+ Maximum number of recursive merge passes. Defaults to ``10``.
110
+
111
+ Returns:
112
+ :class:`ase.Atoms`: The modified structure with clashing atom pairs
113
+ replaced by single atoms at their midpoints.
114
+ """
115
+ neigh = get_neighbors(structure, 1)
116
+ clashing = np.argwhere(neigh.distances[:, 0] < cutoff).ravel()
117
+ if len(clashing) == 0:
118
+ return structure
119
+
120
+ moving = []
121
+ deleting = []
122
+
123
+ for c in clashing:
124
+ if c in deleting:
125
+ continue
126
+
127
+ moving.append(c)
128
+ deleting.append(neigh.indices[c, 0])
129
+
130
+ structure.positions[moving] += neigh.vecs[moving, 0] / 2
131
+ del structure[deleting]
132
+
133
+ if iterations > 0:
134
+ return merge(structure, cutoff=cutoff, iterations=iterations - 1)
135
+ return structure
136
+
137
+
138
+ __all__ = [
139
+ "merge",
140
+ "repulse",
141
+ ]
@@ -0,0 +1,138 @@
1
+ from typing import Any, Iterable
2
+ from collections.abc import Generator
3
+ from ase.atoms import Atoms
4
+ from structuretoolkit.common.pymatgen import pymatgen_to_ase
5
+
6
+
7
+ def search(
8
+ chemsys: str | list[str], fields: Iterable[str] = (), api_key=None, **kwargs
9
+ ) -> Generator[dict[str, Any], None, None]:
10
+ """
11
+ Search the database for all structures matching the given query.
12
+
13
+ Note that `chemsys` takes distinct values for unaries, binaries and so! A query with `chemsys=["Fe", "O"]` will
14
+ return iron and oxygen structures but not iron oxide. Similarly `chemsys=["Fe-O"]` will
15
+ not return unary structures.
16
+
17
+ All keyword arguments for filtering from the original API are supported. See the
18
+ `original docs <https://docs.materialsproject.org/downloading-data/using-the-api>`_ for them.
19
+
20
+ Search for all iron structures:
21
+
22
+ >>> irons = structuretoolkit.build.materialsproject.search("Fe")
23
+ >>> len(list(irons))
24
+ 10
25
+
26
+ Search for all structures with Al, Li that are on the T=0 convex hull:
27
+
28
+ >>> alli = structuretoolkit.build.materialsproject.search(['Al', 'Li', 'Al-Li'], is_stable=True)
29
+ >>> len(list(alli))
30
+ 6
31
+
32
+ Usage is only possible with an API key obtained from the Materials Project. To do this, create an account with
33
+ them, login and access `this webpage <https://next-gen.materialsproject.org/api#api-key>`.
34
+
35
+ Once you have a key, either pass it as the `api_key` parameter or export an
36
+ environment variable, called `MP_API_KEY`, in your shell setup.
37
+
38
+ Args:
39
+ chemsys (str, list of str): confine search to given elements; either an element symbol or multiple element
40
+ symbols separated by dashes; if a list of strings is given return structures matching either of them
41
+ fields (iterable of str): pass as `fields` to :meth:`mp_api.MPRester.summary.search` to request additional
42
+ database entries beyond the structure
43
+ api_key (str, optional): if your API key is not exported in the environment flag MP_API_KEY, pass it here
44
+ **kwargs: passed verbatim to :meth:`mp_api.MPRester.summary.search` to further filter the results
45
+
46
+ Returns:
47
+ list of dict: one dictionary for each search results with at least keys
48
+ 'material_id': database key of the hit
49
+ 'structure': ASE atoms object
50
+ plus any requested via `fields`.
51
+ """
52
+ from mp_api.client import MPRester
53
+
54
+ rest_kwargs = {
55
+ "use_document_model": False, # returns results as dictionaries
56
+ "include_user_agent": True, # send some additional software version info to MP
57
+ }
58
+ if api_key is not None:
59
+ rest_kwargs["api_key"] = api_key
60
+ with MPRester(**rest_kwargs) as mpr:
61
+ results = mpr.summary.search(
62
+ chemsys=chemsys,
63
+ **kwargs,
64
+ fields=list(fields) + ["structure", "material_id"],
65
+ )
66
+ for r in results:
67
+ if "structure" in r:
68
+ r["structure"] = pymatgen_to_ase(r["structure"])
69
+ yield r
70
+
71
+
72
+ def by_id(
73
+ material_id: str | int,
74
+ final: bool = True,
75
+ conventional_unit_cell: bool = False,
76
+ api_key=None,
77
+ ) -> Atoms | list[Atoms]:
78
+ """
79
+ Retrieve a structure by material id.
80
+
81
+ This is how you would ask for the iron ground state:
82
+
83
+ >>> structuretoolkit.build.materialsproject.by_id('mp-13')
84
+ Fe: [0. 0. 0.]
85
+ tags:
86
+ spin: [(0: 2.214)]
87
+ pbc: [ True True True]
88
+ cell:
89
+ Cell([[2.318956, 0.000185, -0.819712], [-1.159251, 2.008215, -0.819524], [2.5e-05, 0.000273, 2.459206]])
90
+
91
+ Usage is only possible with an API key obtained from the Materials Project. To do this, create an account with
92
+ them, login and access `this webpage <https://next-gen.materialsproject.org/api#api-key>`.
93
+
94
+ Once you have a key, either pass it as the `api_key` parameter or export an
95
+ environment variable, called `MP_API_KEY`, in your shell setup.
96
+
97
+ Args:
98
+ material_id (str): the id assigned to a structure by the materials project
99
+ api_key (str, optional): if your API key is not exported in the environment flag MP_API_KEY, pass it here
100
+ final (bool, optional): if set to False, returns the list of initial structures,
101
+ else returns the final structure. (Default is True)
102
+ conventional_unit_cell (bool, optional): if set to True, returns the standard conventional unit cell.
103
+ (Default is False)
104
+
105
+ Returns:
106
+ :class:`~.Atoms`: requested final structure if final is True
107
+ list of :class:~.Atoms`: a list of initial (pre-relaxation) structures if final is False
108
+
109
+ Raises:
110
+ ValueError: material id does not exist
111
+ """
112
+ from mp_api.client import MPRester
113
+
114
+ rest_kwargs = {
115
+ "include_user_agent": True, # send some additional software version info to MP
116
+ }
117
+ if api_key is not None:
118
+ rest_kwargs["api_key"] = api_key
119
+ with MPRester(**rest_kwargs) as mpr:
120
+ if final:
121
+ return pymatgen_to_ase(
122
+ mpr.get_structure_by_material_id(
123
+ material_id=material_id,
124
+ final=final,
125
+ conventional_unit_cell=conventional_unit_cell,
126
+ )
127
+ )
128
+ else:
129
+ return [
130
+ pymatgen_to_ase(mpr_structure)
131
+ for mpr_structure in (
132
+ mpr.get_structure_by_material_id(
133
+ material_id=material_id,
134
+ final=final,
135
+ conventional_unit_cell=conventional_unit_cell,
136
+ )
137
+ )
138
+ ]