structuretoolkit 0.0.43__tar.gz → 0.0.45__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 (35) hide show
  1. {structuretoolkit-0.0.43 → structuretoolkit-0.0.45}/PKG-INFO +5 -5
  2. {structuretoolkit-0.0.43 → structuretoolkit-0.0.45}/pyproject.toml +5 -5
  3. structuretoolkit-0.0.45/src/structuretoolkit/_version.py +24 -0
  4. {structuretoolkit-0.0.43 → structuretoolkit-0.0.45}/src/structuretoolkit/analyse/dscribe.py +1 -1
  5. {structuretoolkit-0.0.43 → structuretoolkit-0.0.45}/src/structuretoolkit/analyse/neighbors.py +48 -49
  6. {structuretoolkit-0.0.43 → structuretoolkit-0.0.45}/src/structuretoolkit/analyse/phonopy.py +2 -0
  7. {structuretoolkit-0.0.43 → structuretoolkit-0.0.45}/src/structuretoolkit/analyse/pyscal.py +2 -2
  8. {structuretoolkit-0.0.43 → structuretoolkit-0.0.45}/src/structuretoolkit/analyse/snap.py +11 -11
  9. {structuretoolkit-0.0.43 → structuretoolkit-0.0.45}/src/structuretoolkit/analyse/spatial.py +20 -17
  10. {structuretoolkit-0.0.43 → structuretoolkit-0.0.45}/src/structuretoolkit/analyse/strain.py +20 -10
  11. {structuretoolkit-0.0.43 → structuretoolkit-0.0.45}/src/structuretoolkit/analyse/symmetry.py +13 -12
  12. {structuretoolkit-0.0.43 → structuretoolkit-0.0.45}/src/structuretoolkit/build/__init__.py +5 -3
  13. {structuretoolkit-0.0.43 → structuretoolkit-0.0.45}/src/structuretoolkit/build/compound.py +2 -2
  14. {structuretoolkit-0.0.43 → structuretoolkit-0.0.45}/src/structuretoolkit/build/geometry.py +3 -6
  15. {structuretoolkit-0.0.43 → structuretoolkit-0.0.45}/src/structuretoolkit/build/materialsproject.py +4 -2
  16. {structuretoolkit-0.0.43 → structuretoolkit-0.0.45}/src/structuretoolkit/build/mesh.py +10 -8
  17. {structuretoolkit-0.0.43 → structuretoolkit-0.0.45}/src/structuretoolkit/build/sqs/_interface.py +42 -30
  18. {structuretoolkit-0.0.43 → structuretoolkit-0.0.45}/src/structuretoolkit/build/sqs/_types.py +5 -4
  19. {structuretoolkit-0.0.43 → structuretoolkit-0.0.45}/src/structuretoolkit/common/helper.py +11 -11
  20. {structuretoolkit-0.0.43 → structuretoolkit-0.0.45}/src/structuretoolkit/visualize.py +8 -3
  21. structuretoolkit-0.0.43/src/structuretoolkit/_version.py +0 -34
  22. {structuretoolkit-0.0.43 → structuretoolkit-0.0.45}/.gitignore +0 -0
  23. {structuretoolkit-0.0.43 → structuretoolkit-0.0.45}/LICENSE +0 -0
  24. {structuretoolkit-0.0.43 → structuretoolkit-0.0.45}/README.md +0 -0
  25. {structuretoolkit-0.0.43 → structuretoolkit-0.0.45}/src/structuretoolkit/__init__.py +0 -0
  26. {structuretoolkit-0.0.43 → structuretoolkit-0.0.45}/src/structuretoolkit/analyse/__init__.py +0 -0
  27. {structuretoolkit-0.0.43 → structuretoolkit-0.0.45}/src/structuretoolkit/analyse/distance.py +0 -0
  28. {structuretoolkit-0.0.43 → structuretoolkit-0.0.45}/src/structuretoolkit/build/aimsgb.py +0 -0
  29. {structuretoolkit-0.0.43 → structuretoolkit-0.0.45}/src/structuretoolkit/build/sqs/__init__.py +10 -10
  30. {structuretoolkit-0.0.43 → structuretoolkit-0.0.45}/src/structuretoolkit/build/surface.py +0 -0
  31. {structuretoolkit-0.0.43 → structuretoolkit-0.0.45}/src/structuretoolkit/common/__init__.py +0 -0
  32. {structuretoolkit-0.0.43 → structuretoolkit-0.0.45}/src/structuretoolkit/common/error.py +0 -0
  33. {structuretoolkit-0.0.43 → structuretoolkit-0.0.45}/src/structuretoolkit/common/phonopy.py +0 -0
  34. {structuretoolkit-0.0.43 → structuretoolkit-0.0.45}/src/structuretoolkit/common/pymatgen.py +0 -0
  35. {structuretoolkit-0.0.43 → structuretoolkit-0.0.45}/src/structuretoolkit/common/pyscal.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: structuretoolkit
3
- Version: 0.0.43
3
+ Version: 0.0.45
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
@@ -46,8 +46,8 @@ Classifier: Programming Language :: Python :: 3.12
46
46
  Classifier: Programming Language :: Python :: 3.13
47
47
  Classifier: Topic :: Scientific/Engineering :: Physics
48
48
  Requires-Python: <3.15,>=3.10
49
- Requires-Dist: ase<=3.28.0,>=3.24.0
50
- Requires-Dist: numpy<=2.4.3,>=1.23.5
49
+ Requires-Dist: ase<=3.29.0,>=3.24.0
50
+ Requires-Dist: numpy<=2.4.6,>=1.23.5
51
51
  Requires-Dist: scipy<=1.17.1,>=1.15.0
52
52
  Provides-Extra: clusters
53
53
  Requires-Dist: scikit-learn==1.8.0; extra == 'clusters'
@@ -64,10 +64,10 @@ Requires-Dist: pymatgen==2026.3.23; extra == 'mp-api'
64
64
  Provides-Extra: nglview
65
65
  Requires-Dist: nglview<=4.0.1,>=2.7.7; extra == 'nglview'
66
66
  Provides-Extra: phonopy
67
- Requires-Dist: phonopy<=3.5.1,>=2.21.2; extra == 'phonopy'
67
+ Requires-Dist: phonopy<=4.0.1,>=2.21.2; extra == 'phonopy'
68
68
  Requires-Dist: spglib==2.7.0; extra == 'phonopy'
69
69
  Provides-Extra: plotly
70
- Requires-Dist: plotly<=6.6.0,>=6.0.0; extra == 'plotly'
70
+ Requires-Dist: plotly<=6.8.0,>=6.0.0; extra == 'plotly'
71
71
  Provides-Extra: pyscal
72
72
  Requires-Dist: pyscal3<=3.3.2,>=3.2.5; extra == 'pyscal'
73
73
  Provides-Extra: surface
@@ -1,5 +1,5 @@
1
1
  [build-system]
2
- requires = ["ase", "numpy", "scipy", "hatchling==1.29.0", "hatch-vcs==0.5.0"]
2
+ requires = ["ase", "numpy", "scipy", "hatchling==1.30.1", "hatch-vcs==0.5.0"]
3
3
  build-backend = "hatchling.build"
4
4
 
5
5
  [project]
@@ -23,8 +23,8 @@ classifiers = [
23
23
  "Programming Language :: Python :: 3.13",
24
24
  ]
25
25
  dependencies = [
26
- "ase>=3.24.0,<=3.28.0",
27
- "numpy>=1.23.5,<=2.4.3",
26
+ "ase>=3.24.0,<=3.29.0",
27
+ "numpy>=1.23.5,<=2.4.6",
28
28
  "scipy>=1.15.0,<=1.17.1",
29
29
  ]
30
30
  dynamic = ["version"]
@@ -43,7 +43,7 @@ grainboundary = [
43
43
  pyscal = ["pyscal3>=3.2.5,<=3.3.2"]
44
44
  nglview = ["nglview>=2.7.7,<=4.0.1"]
45
45
  matplotlib = ["matplotlib==3.10.9"]
46
- plotly = ["plotly>=6.0.0,<=6.6.0"]
46
+ plotly = ["plotly>=6.0.0,<=6.8.0"]
47
47
  clusters = ["scikit-learn==1.8.0"]
48
48
  symmetry = ["spglib==2.7.0"]
49
49
  surface = [
@@ -51,7 +51,7 @@ surface = [
51
51
  "pymatgen==2026.3.23",
52
52
  ]
53
53
  phonopy = [
54
- "phonopy>=2.21.2,<=3.5.1",
54
+ "phonopy>=2.21.2,<=4.0.1",
55
55
  "spglib==2.7.0",
56
56
  ]
57
57
  mp-api = [
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.0.45'
22
+ __version_tuple__ = version_tuple = (0, 0, 45)
23
+
24
+ __commit_id__ = commit_id = None
@@ -11,7 +11,7 @@ def soap_descriptor_per_atom(
11
11
  rbf: str = "gto",
12
12
  weighting: np.ndarray | None = None,
13
13
  average: str = "off",
14
- compression: dict = None,
14
+ compression: dict | None = None,
15
15
  species: list | None = None,
16
16
  periodic: bool = True,
17
17
  sparse: bool = False,
@@ -3,6 +3,7 @@
3
3
 
4
4
  import itertools
5
5
  import warnings
6
+ from typing import Any, cast
6
7
 
7
8
  import numpy as np
8
9
  from ase.atoms import Atoms
@@ -56,18 +57,18 @@ class Tree:
56
57
  Args:
57
58
  ref_structure (ase.atoms.Atoms): Reference structure.
58
59
  """
59
- self._distances = None
60
- self._vectors = None
61
- self._indices = None
60
+ self._distances: Any = None
61
+ self._vectors: Any = None
62
+ self._indices: Any = None
62
63
  self._mode = {"filled": True, "ragged": False, "flattened": False}
63
- self._extended_positions = None
64
- self._positions = None
65
- self._wrapped_indices = None
66
- self._extended_indices = None
64
+ self._extended_positions: Any = None
65
+ self._positions: Any = None
66
+ self._wrapped_indices: Any = None
67
+ self._extended_indices: Any = None
67
68
  self._ref_structure = ref_structure.copy()
68
69
  self.wrap_positions = False
69
- self._tree = None
70
- self.num_neighbors = None
70
+ self._tree: Any = None
71
+ self.num_neighbors: int | None = None
71
72
  self.cutoff_radius = np.inf
72
73
  self._norm_order = 2
73
74
 
@@ -91,6 +92,7 @@ class Tree:
91
92
  for k, v in self._mode.items():
92
93
  if v:
93
94
  return k
95
+ raise ValueError("No neighbor representation mode is active")
94
96
 
95
97
  def _set_mode(self, new_mode: str) -> None:
96
98
  """
@@ -172,6 +174,7 @@ class Tree:
172
174
  return self._contract(value, ref_vector=ref_vector)
173
175
  elif key == "flattened":
174
176
  return value[self._distances < np.inf]
177
+ raise ValueError(f"Unknown neighbor representation mode: {key}")
175
178
 
176
179
  @property
177
180
  def distances(self) -> np.ndarray:
@@ -257,7 +260,7 @@ class Tree:
257
260
  + " with the correct norm_order value"
258
261
  )
259
262
 
260
- def _get_max_length(self, ref_vector: np.ndarray | None = None) -> int:
263
+ def _get_max_length(self, ref_vector: np.ndarray | None = None) -> int | None:
261
264
  """
262
265
  Get the maximum length of the reference vector.
263
266
 
@@ -277,9 +280,7 @@ class Tree:
277
280
  return None
278
281
  return max(len(dd[dd < np.inf]) for dd in ref_vector)
279
282
 
280
- def _contract(
281
- self, value: np.ndarray, ref_vector: np.ndarray | None = None
282
- ) -> np.ndarray:
283
+ def _contract(self, value: np.ndarray, ref_vector: np.ndarray | None = None):
283
284
  """
284
285
  Contract the given value based on the specified reference vector.
285
286
 
@@ -297,7 +298,7 @@ class Tree:
297
298
  for vv, dist in zip(value, self.filled.distances, strict=True)
298
299
  ]
299
300
 
300
- def _allow_ragged_to_mode(self, new_bool: bool) -> str:
301
+ def _allow_ragged_to_mode(self, new_bool: bool | None) -> str:
301
302
  """
302
303
  Set the representation mode based on the value of new_bool.
303
304
 
@@ -504,6 +505,8 @@ class Tree:
504
505
  if self.num_neighbors is None:
505
506
  self.num_neighbors = num_neighbors
506
507
  self.cutoff_radius = cutoff_radius
508
+ assert num_neighbors is not None
509
+ assert self.num_neighbors is not None
507
510
  if num_neighbors > self.num_neighbors:
508
511
  warnings.warn(
509
512
  "Taking a larger search area after initialization has the risk of "
@@ -582,7 +585,7 @@ class Tree:
582
585
  def _get_neighborhood(
583
586
  self,
584
587
  positions: np.ndarray,
585
- num_neighbors: int = 12,
588
+ num_neighbors: int | None = 12,
586
589
  cutoff_radius: float = np.inf,
587
590
  exclude_self: bool = False,
588
591
  width_buffer: float = 1.2,
@@ -611,7 +614,7 @@ class Tree:
611
614
  cutoff_radius=cutoff_radius,
612
615
  width_buffer=width_buffer,
613
616
  )
614
- if num_neighbors is not None:
617
+ if num_neighbors is not None and self.num_neighbors is not None:
615
618
  self.num_neighbors -= 1
616
619
  max_column = np.sum(distances < np.inf, axis=-1).max()
617
620
  self._distances = distances[..., start_column:max_column]
@@ -620,7 +623,7 @@ class Tree:
620
623
  self._positions = positions
621
624
  return self
622
625
 
623
- def _check_width(self, width: float, pbc: list[bool, bool, bool]) -> bool:
626
+ def _check_width(self, width: float, pbc: np.ndarray) -> bool:
624
627
  """
625
628
  Check if the width of the layer exceeds the specified value.
626
629
 
@@ -706,26 +709,19 @@ class Tree:
706
709
  radius. For automated uses, see Atoms.analyse.pyscal_steinhardt_parameter()
707
710
  """
708
711
  random_rotation = Rotation.from_mrp(np.random.random(3)).as_matrix()
709
- return np.sqrt(
710
- 4
711
- * np.pi
712
- / (2 * l + 1)
713
- * np.sum(
714
- [
715
- np.absolute(
716
- self.get_spherical_harmonics(
717
- l=l,
718
- m=m,
719
- cutoff_radius=cutoff_radius,
720
- rotation=random_rotation,
721
- )
722
- )
723
- ** 2
724
- for m in np.arange(-l, l + 1)
725
- ],
726
- axis=0,
712
+ harmonic_values: list[np.ndarray] = [
713
+ np.absolute(
714
+ self.get_spherical_harmonics(
715
+ l=l,
716
+ m=m,
717
+ cutoff_radius=cutoff_radius,
718
+ rotation=random_rotation,
719
+ )
727
720
  )
728
- )
721
+ ** 2
722
+ for m in np.arange(-l, l + 1)
723
+ ]
724
+ return np.sqrt(4 * np.pi / (2 * l + 1) * np.sum(harmonic_values, axis=0))
729
725
 
730
726
  @staticmethod
731
727
  def _get_all_possible_pairs(l: int) -> np.ndarray:
@@ -827,8 +823,8 @@ class Neighbors(Tree):
827
823
  """
828
824
  super().__init__(ref_structure=ref_structure)
829
825
  self._tolerance = tolerance
830
- self._cluster_vecs = None
831
- self._cluster_dist = None
826
+ self._cluster_vecs: Any = None
827
+ self._cluster_dist: Any = None
832
828
 
833
829
  def __repr__(self):
834
830
  """
@@ -1072,7 +1068,7 @@ class Neighbors(Tree):
1072
1068
 
1073
1069
  def find_neighbors_by_vector(
1074
1070
  self, vector: np.ndarray, return_deviation: bool = False
1075
- ) -> np.ndarray:
1071
+ ) -> np.ndarray | tuple[np.ndarray, np.ndarray]:
1076
1072
  """
1077
1073
  Args:
1078
1074
  vector (list/np.ndarray): vector by which positions are translated (and neighbors are searched)
@@ -1323,9 +1319,9 @@ class Neighbors(Tree):
1323
1319
  ind_shell = []
1324
1320
  for d, i in zip(dist, ind, strict=True):
1325
1321
  id_list = get_cluster(d[d < radius], i[d < radius])
1326
- ia_shells_dict = {}
1322
+ ia_shells_dict: dict[str, list[list[int]]] = {}
1327
1323
  for i_shell_list in id_list:
1328
- ia_shell_dict = {}
1324
+ ia_shell_dict: dict[str, list[int]] = {}
1329
1325
  for i_s in i_shell_list:
1330
1326
  el = el_list[i_s]
1331
1327
  if el not in ia_shell_dict:
@@ -1382,14 +1378,17 @@ def get_neighbors(
1382
1378
  Returns:
1383
1379
  Neighbors: An instance of the Neighbors class with the neighbor indices, distances, and vectors.
1384
1380
  """
1385
- neigh = _get_neighbors(
1386
- structure=structure,
1387
- num_neighbors=num_neighbors,
1388
- tolerance=tolerance,
1389
- id_list=id_list,
1390
- cutoff_radius=cutoff_radius,
1391
- width_buffer=width_buffer,
1392
- norm_order=norm_order,
1381
+ neigh = cast(
1382
+ Neighbors,
1383
+ _get_neighbors(
1384
+ structure=structure,
1385
+ num_neighbors=num_neighbors,
1386
+ tolerance=tolerance,
1387
+ id_list=id_list,
1388
+ cutoff_radius=cutoff_radius,
1389
+ width_buffer=width_buffer,
1390
+ norm_order=norm_order,
1391
+ ),
1393
1392
  )
1394
1393
  neigh._set_mode(mode)
1395
1394
  return neigh
@@ -47,4 +47,6 @@ def get_equivalent_atoms(
47
47
  ops = spglib.get_symmetry(
48
48
  cell=unitcell.totuple(), symprec=symprec, angle_tolerance=angle_tolerance
49
49
  )
50
+ if ops is None:
51
+ raise RuntimeError("spglib could not determine symmetry")
50
52
  return ops["equivalent_atoms"]
@@ -27,7 +27,7 @@ def get_steinhardt_parameters(
27
27
  n_clusters: int | None = 2,
28
28
  q: tuple | None = None,
29
29
  averaged: bool = False,
30
- ) -> tuple[np.ndarray] | tuple[np.ndarray, np.ndarray]:
30
+ ) -> np.ndarray | tuple[np.ndarray, np.ndarray]:
31
31
  """
32
32
  Calculate Steinhardts parameters
33
33
 
@@ -162,7 +162,7 @@ def get_diamond_structure_descriptors(
162
162
 
163
163
  def get_adaptive_cna_descriptors(
164
164
  structure: Atoms, mode: str = "total", ovito_compatibility: bool = False
165
- ) -> np.ndarray:
165
+ ) -> dict | np.ndarray:
166
166
  """
167
167
  Use common neighbor analysis
168
168
 
@@ -63,7 +63,7 @@ def get_snap_descriptors_per_atom(
63
63
  structure: Atoms,
64
64
  atom_types: list[str],
65
65
  twojmax: int = 6,
66
- element_radius: list[int] = None,
66
+ element_radius: list[float] | None = None,
67
67
  rcutfac: float = 1.0,
68
68
  rfac0: float = 0.99363,
69
69
  rmin0: float = 0.0,
@@ -114,7 +114,7 @@ def get_snap_descriptor_derivatives(
114
114
  structure: Atoms,
115
115
  atom_types: list[str],
116
116
  twojmax: int = 6,
117
- element_radius: list[int] = None,
117
+ element_radius: list[float] | None = None,
118
118
  rcutfac: float = 1.0,
119
119
  rfac0: float = 0.99363,
120
120
  rmin0: float = 0.0,
@@ -161,7 +161,7 @@ def get_snap_descriptor_derivatives(
161
161
  )
162
162
 
163
163
 
164
- def get_snap_descriptor_names(twojmax: int) -> np.ndarray:
164
+ def get_snap_descriptor_names(twojmax: int) -> list[list[float]]:
165
165
  """
166
166
  Get names of the SNAP descriptors
167
167
 
@@ -268,8 +268,8 @@ def _set_ase_structure(lmp, structure: Atoms):
268
268
  elem_all = np.array([el_dict[el] + 1 for el in structure.get_chemical_symbols()])
269
269
  lmp.create_atoms(
270
270
  n=len(structure),
271
- id=None,
272
- type=(len(elem_all) * c_int)(*elem_all),
271
+ atomid=None,
272
+ atype=(len(elem_all) * c_int)(*elem_all),
273
273
  x=(len(positions) * c_double)(*positions),
274
274
  v=None,
275
275
  image=None,
@@ -547,16 +547,16 @@ def _extract_computes_snap(
547
547
  Returns:
548
548
  np.ndarray: Output of the LAMMPS compute command
549
549
  """
550
- lmp_atom_ids = lmp.numpy.extract_atom_iarray("id", num_atoms).flatten()
551
- cond = np.all(lmp_atom_ids == 1 + np.arange(num_atoms))
550
+ lmp_atom_ids = lmp.numpy.extract_atom(name="id", nelem=num_atoms).flatten()
551
+ cond = bool(np.all(lmp_atom_ids == 1 + np.arange(num_atoms)))
552
552
  assert cond, "LAMMPS seems to have lost atoms"
553
553
 
554
554
  # Extract types
555
- lmp_types = lmp.numpy.extract_atom_iarray(name="type", nelem=num_atoms).flatten()
555
+ lmp_types = lmp.numpy.extract_atom(name="type", nelem=num_atoms).flatten()
556
556
  lmp_volume = lmp.get_thermo("vol")
557
557
 
558
558
  # Extract Bsum
559
- _extract_compute_np(lmp, "b_sum", 0, 1, (n_coeff))
559
+ _extract_compute_np(lmp, "b_sum", 0, 1, (n_coeff,))
560
560
 
561
561
  # Extract B
562
562
  lmp_barr = _extract_compute_np(lmp, "b", 1, 2, (num_atoms, n_coeff))
@@ -571,13 +571,13 @@ def _extract_computes_snap(
571
571
 
572
572
  lmp_dbarr = _extract_compute_np(lmp, "db", 1, 2, (num_atoms, num_types, 3, n_coeff))
573
573
  lmp_dbsum = _extract_compute_np(lmp, "db_sum", 0, 1, (num_types, 3, n_coeff))
574
- cond = np.allclose(lmp_dbsum, lmp_dbarr.sum(axis=0), rtol=1e-12, atol=1e-12)
574
+ cond = bool(np.allclose(lmp_dbsum, lmp_dbarr.sum(axis=0), rtol=1e-12, atol=1e-12))
575
575
  assert cond, "db_sum doesn't match sum of db"
576
576
  db_atom = np.transpose(lmp_dbarr, (0, 2, 1, 3))
577
577
 
578
578
  lmp_vbarr = _extract_compute_np(lmp, "vb", 1, 2, (num_atoms, num_types, 6, n_coeff))
579
579
  lmp_vbsum = _extract_compute_np(lmp, "vb_sum", 0, 1, (num_types, 6, n_coeff))
580
- cond = np.allclose(lmp_vbsum, lmp_vbarr.sum(axis=0), rtol=1e-12, atol=1e-12)
580
+ cond = bool(np.allclose(lmp_vbsum, lmp_vbarr.sum(axis=0), rtol=1e-12, atol=1e-12))
581
581
  assert cond, "vb_sum doesn't match sum of vb"
582
582
  vb_sum = np.transpose(lmp_vbsum, (1, 0, 2)) / lmp_volume * eV_div_A3_to_bar
583
583
 
@@ -2,6 +2,7 @@
2
2
  # Distributed under the terms of "New BSD License", see the LICENSE file.
3
3
 
4
4
  from collections.abc import Callable
5
+ from typing import Any
5
6
 
6
7
  import numpy as np
7
8
  from ase.atoms import Atoms
@@ -74,10 +75,10 @@ def create_gridpoints(
74
75
  """
75
76
  cell = get_vertical_length(structure=structure)
76
77
  n_points = (n_gridpoints_per_angstrom * cell).astype(int)
77
- positions = np.meshgrid(
78
+ grids = np.meshgrid(
78
79
  *[np.linspace(0, 1, n_points[i], endpoint=False) for i in range(3)]
79
80
  )
80
- positions = np.stack(positions, axis=-1).reshape(-1, 3)
81
+ positions = np.stack(grids, axis=-1).reshape(-1, 3)
81
82
  return np.einsum("ji,nj->ni", structure.cell, positions)
82
83
 
83
84
 
@@ -226,7 +227,7 @@ class Interstitials:
226
227
  q_eps: float = 0.3,
227
228
  var_ratio: float = 5.0,
228
229
  min_samples: int | None = None,
229
- neigh_args: dict = None,
230
+ neigh_args: dict | None = None,
230
231
  **kwargs,
231
232
  ):
232
233
  """
@@ -266,7 +267,7 @@ class Interstitials:
266
267
  self._neigh = get_neighbors(
267
268
  structure=structure, num_neighbors=num_neighbors, **neigh_args
268
269
  )
269
- self.workflow = [
270
+ self.workflow: list[dict[str, Any]] = [
270
271
  {
271
272
  "f": remove_too_close,
272
273
  "kwargs": {"structure": structure, "min_distance": min_distance},
@@ -290,7 +291,7 @@ class Interstitials:
290
291
  },
291
292
  },
292
293
  ]
293
- self._positions = None
294
+ self._positions: np.ndarray | None = None
294
295
  self.structure = structure
295
296
 
296
297
  def run_workflow(
@@ -312,7 +313,8 @@ class Interstitials:
312
313
  if positions is None:
313
314
  positions = self.initial_positions.copy()
314
315
  for ii, ww in enumerate(self.workflow):
315
- positions = ww["f"](positions=positions, **ww["kwargs"])
316
+ f: Callable[..., np.ndarray] = ww["f"]
317
+ positions = f(positions=positions, **ww["kwargs"])
316
318
  if ii == steps:
317
319
  return positions
318
320
  return positions
@@ -338,6 +340,7 @@ class Interstitials:
338
340
  if self._positions is None:
339
341
  self._positions = self.run_workflow()
340
342
  self._neigh = self.neigh.get_neighborhood(self._positions)
343
+ assert self._positions is not None
341
344
  return self._positions
342
345
 
343
346
  @property
@@ -408,7 +411,7 @@ def get_interstitials(
408
411
  q_eps: float = 0.3,
409
412
  var_ratio: float = 5.0,
410
413
  min_samples: int | None = None,
411
- neigh_args: dict = None,
414
+ neigh_args: dict | None = None,
412
415
  **kwargs,
413
416
  ) -> Interstitials:
414
417
  """
@@ -449,9 +452,9 @@ def get_interstitials(
449
452
  )
450
453
 
451
454
 
452
- get_interstitials.__doc__ = (
453
- Interstitials.__doc__.replace("Class", "Function") + Interstitials.__init__.__doc__
454
- )
455
+ get_interstitials.__doc__ = (Interstitials.__doc__ or "").replace(
456
+ "Class", "Function"
457
+ ) + (Interstitials.__init__.__doc__ or "")
455
458
 
456
459
 
457
460
  def get_layers(
@@ -459,8 +462,8 @@ def get_layers(
459
462
  distance_threshold: float = 0.01,
460
463
  id_list: list[int] | None = None,
461
464
  wrap_atoms: bool = True,
462
- planes: np.ndarray = None,
463
- cluster_method: str = None,
465
+ planes: np.ndarray | None = None,
466
+ cluster_method: Any | None = None,
464
467
  ) -> np.ndarray:
465
468
  """
466
469
  Get an array of layer numbers.
@@ -512,10 +515,10 @@ def get_layers(
512
515
  structure=structure, width=distance_threshold, return_indices=True
513
516
  )
514
517
  if id_list is not None:
515
- id_list = np.arange(len(structure))[np.array(id_list)]
516
- id_list = np.any(id_list[:, np.newaxis] == indices[np.newaxis, :], axis=0)
517
- positions = positions[id_list]
518
- indices = indices[id_list]
518
+ id_arr = np.arange(len(structure))[np.array(id_list)]
519
+ id_mask = np.any(id_arr[:, np.newaxis] == indices[np.newaxis, :], axis=0)
520
+ positions = positions[id_mask]
521
+ indices = indices[id_mask]
519
522
  else:
520
523
  positions = structure.positions
521
524
  if id_list is not None:
@@ -694,7 +697,7 @@ def get_cluster_positions(
694
697
  eps: float = 1.0,
695
698
  buffer_width: float | None = None,
696
699
  return_labels: bool = False,
697
- ) -> np.ndarray:
700
+ ) -> np.ndarray | tuple[np.ndarray, np.ndarray]:
698
701
  """
699
702
  Cluster positions according to the distances. Clustering algorithm uses DBSCAN:
700
703
 
@@ -1,3 +1,5 @@
1
+ from typing import Union
2
+
1
3
  import numpy as np
2
4
  from ase.atoms import Atoms
3
5
  from scipy.spatial.transform import Rotation
@@ -47,18 +49,19 @@ class Strain:
47
49
  """
48
50
  self.structure = structure
49
51
  self.ref_structure = ref_structure
50
- self._num_neighbors = num_neighbors
52
+ self._num_neighbors: int | None = num_neighbors
51
53
  self.only_bulk_type = only_bulk_type
52
- self._crystal_phase = None
53
- self._ref_coord = None
54
- self._coords = None
55
- self._rotations = None
54
+ self._crystal_phase: str | None = None
55
+ self._ref_coord: np.ndarray | None = None
56
+ self._coords: np.ndarray | None = None
57
+ self._rotations: np.ndarray | None = None
56
58
 
57
59
  @property
58
60
  def num_neighbors(self) -> int:
59
61
  """Number of neighbors to consider the local frame. Should be the coordination number."""
60
62
  if self._num_neighbors is None:
61
63
  self._num_neighbors = self._get_number_of_neighbors(self.crystal_phase)
64
+ assert self._num_neighbors is not None
62
65
  return self._num_neighbors
63
66
 
64
67
  @property
@@ -66,6 +69,7 @@ class Strain:
66
69
  """Majority crystal phase calculated via common neighbor analysis."""
67
70
  if self._crystal_phase is None:
68
71
  self._crystal_phase = self._get_majority_phase(self.ref_structure)
72
+ assert self._crystal_phase is not None
69
73
  return self._crystal_phase
70
74
 
71
75
  @property
@@ -79,7 +83,8 @@ class Strain:
79
83
  than the bulk.
80
84
  """
81
85
  return np.array(
82
- self.structure.analyse.pyscal_cna_adaptive(mode="str") != self.crystal_phase
86
+ get_adaptive_cna_descriptors(structure=self.structure, mode="str")
87
+ != self.crystal_phase
83
88
  )
84
89
 
85
90
  def _get_perpendicular_unit_vectors(
@@ -197,6 +202,7 @@ class Strain:
197
202
  w_second = self.ref_coord[dist.argmin(axis=1)].copy()
198
203
  second_rot = self._get_rotation_from_vectors(v, w_second, all_vecs[:, 0])
199
204
  self._rotations = np.einsum("nij,njk->nik", second_rot, first_rot)
205
+ assert self._rotations is not None
200
206
  return self._rotations
201
207
 
202
208
  @staticmethod
@@ -219,7 +225,7 @@ class Strain:
219
225
  return np.argmin(distances, axis=-1)
220
226
 
221
227
  @staticmethod
222
- def _get_majority_phase(structure: Atoms) -> np.ndarray:
228
+ def _get_majority_phase(structure: Atoms) -> str:
223
229
  """
224
230
  Get the majority crystal phase in the structure based on the common neighbor analysis (CNA) descriptors.
225
231
 
@@ -227,10 +233,12 @@ class Strain:
227
233
  structure (ase.atoms.Atoms): The structure to analyze.
228
234
 
229
235
  Returns:
230
- np.ndarray: The crystal phase with the highest count.
236
+ str: The crystal phase with the highest count.
231
237
  """
232
238
  cna = get_adaptive_cna_descriptors(structure=structure)
233
- return np.asarray(list(cna.keys()))[np.argmax(list(cna.values()))]
239
+ if not isinstance(cna, dict):
240
+ raise TypeError("Expected CNA descriptors as a dictionary")
241
+ return str(np.asarray(list(cna.keys()))[np.argmax(list(cna.values()))])
234
242
 
235
243
  @staticmethod
236
244
  def _get_number_of_neighbors(crystal_phase: str) -> int:
@@ -265,6 +273,7 @@ class Strain:
265
273
  self._ref_coord = get_neighbors(
266
274
  structure=self.ref_structure, num_neighbors=self.num_neighbors
267
275
  ).vecs[0]
276
+ assert self._ref_coord is not None
268
277
  return self._ref_coord
269
278
 
270
279
  @property
@@ -279,6 +288,7 @@ class Strain:
279
288
  self._coords = get_neighbors(
280
289
  structure=self.structure, num_neighbors=self.num_neighbors
281
290
  ).vecs
291
+ assert self._coords is not None
282
292
  return self._coords
283
293
 
284
294
  @property
@@ -320,7 +330,7 @@ def get_strain(
320
330
  num_neighbors: int | None = None,
321
331
  only_bulk_type: bool = False,
322
332
  return_object: bool = False,
323
- ):
333
+ ) -> Union[np.ndarray, "Strain"]:
324
334
  """
325
335
  Calculate local strain of each atom following the Lagrangian strain tensor:
326
336
 
@@ -63,7 +63,7 @@ class Symmetry(dict):
63
63
  self._symprec = symprec
64
64
  self._angle_tolerance = angle_tolerance
65
65
  self.epsilon = epsilon
66
- self._permutations = None
66
+ self._permutations: np.ndarray | None = None
67
67
  for k, v in self._get_symmetry(
68
68
  symprec=symprec, angle_tolerance=angle_tolerance
69
69
  ).items():
@@ -209,6 +209,7 @@ class Symmetry(dict):
209
209
  if np.ptp(distances) > self._symprec:
210
210
  raise AssertionError("Neighbor search failed")
211
211
  self._permutations = self._permutations.argsort(axis=-1)
212
+ assert self._permutations is not None
212
213
  return self._permutations
213
214
 
214
215
  def symmetrize_vectors(
@@ -369,12 +370,12 @@ class Symmetry(dict):
369
370
  )
370
371
  if space_group is None:
371
372
  raise SymmetryError(spglib.error.get_error_message())
372
- space_group = space_group.split()
373
- if len(space_group) == 1:
374
- return {"Number": ast.literal_eval(space_group[0])}
373
+ parts = space_group.split()
374
+ if len(parts) == 1:
375
+ return {"Number": ast.literal_eval(parts[0])}
375
376
  return {
376
- "InternationalTableSymbol": space_group[0],
377
- "Number": ast.literal_eval(space_group[1]),
377
+ "InternationalTableSymbol": parts[0],
378
+ "Number": ast.literal_eval(parts[1]),
378
379
  }
379
380
 
380
381
  def get_primitive_cell(
@@ -442,7 +443,7 @@ class Symmetry(dict):
442
443
  mesh: np.ndarray,
443
444
  is_shift: np.ndarray = np.zeros(3, dtype="intc"),
444
445
  is_time_reversal: bool = True,
445
- ) -> np.ndarray:
446
+ ) -> tuple[np.ndarray, np.ndarray]:
446
447
  """
447
448
  Get the irreducible reciprocal mesh points.
448
449
 
@@ -452,21 +453,21 @@ class Symmetry(dict):
452
453
  is_time_reversal (bool, optional): Whether to consider time reversal symmetry. Defaults to True.
453
454
 
454
455
  Returns:
455
- ndarray: The irreducible reciprocal mesh points.
456
+ tuple: The irreducible reciprocal mesh points (mapping, grid_points).
456
457
 
457
458
  Raises:
458
459
  SymmetryError: If the irreducible reciprocal mesh points cannot be obtained.
459
460
  """
460
- mesh = spglib.get_ir_reciprocal_mesh(
461
+ result = spglib.get_ir_reciprocal_mesh(
461
462
  mesh=mesh,
462
463
  cell=self._get_spglib_cell(),
463
464
  is_shift=is_shift,
464
465
  is_time_reversal=is_time_reversal,
465
466
  symprec=self._symprec,
466
467
  )
467
- if mesh is None:
468
+ if result is None:
468
469
  raise SymmetryError(spglib.error.get_error_message())
469
- return mesh
470
+ return result
470
471
 
471
472
 
472
473
  def _get_inner_slicer(n: int, i: int) -> tuple:
@@ -481,7 +482,7 @@ def _get_inner_slicer(n: int, i: int) -> tuple:
481
482
  tuple: Inner slicer tuple.
482
483
 
483
484
  """
484
- s = [None for _ in range(n)]
485
+ s: list[None | slice] = [None for _ in range(n)]
485
486
  s[0] = slice(None)
486
487
  s[i] = slice(None)
487
488
  return tuple(s)
@@ -1,11 +1,13 @@
1
1
  from structuretoolkit.build.aimsgb import get_grainboundary_info, grainboundary
2
2
  from structuretoolkit.build.compound import B2, C14, C15, C36, D03
3
- from structuretoolkit.build.mesh import create_mesh
4
- from structuretoolkit.build.sqs import sqs_structures
5
3
  from structuretoolkit.build.materialsproject import (
6
- search as materialsproject_search,
7
4
  by_id as materialsproject_by_id,
8
5
  )
6
+ from structuretoolkit.build.materialsproject import (
7
+ search as materialsproject_search,
8
+ )
9
+ from structuretoolkit.build.mesh import create_mesh
10
+ from structuretoolkit.build.sqs import sqs_structures
9
11
  from structuretoolkit.build.surface import (
10
12
  get_high_index_surface_info,
11
13
  high_index_surface,
@@ -6,7 +6,7 @@ from ase.spacegroup import crystal
6
6
  from structuretoolkit.analyse.neighbors import get_neighbors
7
7
 
8
8
 
9
- def B2(element_a: str, element_b: str, a: float | None = None):
9
+ def B2(element_a: str, element_b: str, a: float | None = None) -> Atoms:
10
10
  """
11
11
  Builds a cubic $AB$ B2 structure of interpenetrating simple cubic lattices.
12
12
 
@@ -207,7 +207,7 @@ def D03(element_a: str, element_b: str, a: float | None = None) -> Atoms:
207
207
  )
208
208
 
209
209
 
210
- def _bcc_lattice_constant_from_nn_distance(element: Atoms):
210
+ def _bcc_lattice_constant_from_nn_distance(element: str) -> float:
211
211
  """
212
212
  Build a BCC lattice constant by making the BCC have the same nearest neighbour distance as the regular cell.
213
213
 
@@ -51,8 +51,7 @@ def repulse(
51
51
  """
52
52
  if not inplace:
53
53
  structure = structure.copy()
54
- if axis is None:
55
- axis = slice(None)
54
+ ax: int | slice = axis if axis is not None else slice(None)
56
55
  for _ in range(iterations):
57
56
  neigh = get_neighbors(structure, num_neighbors=1)
58
57
  dd = neigh.distances[:, 0]
@@ -78,7 +77,7 @@ def repulse(
78
77
  disp = np.clip(min_dist - dd[I], 0, step_size)
79
78
 
80
79
  displacement = disp[:, None] * vv # (N_close, 3)
81
- structure.positions[I, axis] -= displacement[:, axis]
80
+ structure.positions[I, ax] -= displacement[:, ax]
82
81
 
83
82
  else:
84
83
  raise RuntimeError(f"repulse did not converge within {iterations} iterations")
@@ -86,9 +85,7 @@ def repulse(
86
85
  return structure
87
86
 
88
87
 
89
- def merge(
90
- structure: "ase.Atoms", cutoff: float = 1.8, iterations: int = 10
91
- ) -> "ase.Atoms":
88
+ def merge(structure: Atoms, cutoff: float = 1.8, iterations: int = 10) -> Atoms:
92
89
  """Merge pairs of atoms that are closer than ``cutoff`` by collapsing each
93
90
  pair to their midpoint and deleting one of the two atoms.
94
91
 
@@ -1,6 +1,8 @@
1
- from typing import Any, Iterable
2
- from collections.abc import Generator
1
+ from collections.abc import Generator, Iterable
2
+ from typing import Any
3
+
3
4
  from ase.atoms import Atoms
5
+
4
6
  from structuretoolkit.common.pymatgen import pymatgen_to_ase
5
7
 
6
8
 
@@ -12,7 +12,7 @@ class MeshInputError(ValueError):
12
12
 
13
13
  def create_mesh(
14
14
  cell: ase.atoms.Atoms | np.ndarray | list | float,
15
- n_mesh: int | list[int, int, int] = 10,
15
+ n_mesh: int | list[int] | None = 10,
16
16
  density: float | None = None,
17
17
  endpoint: bool = False,
18
18
  ):
@@ -32,20 +32,22 @@ def create_mesh(
32
32
  Returns:
33
33
  (3, n, n, n)-array: mesh
34
34
  """
35
- cell = get_cell(cell)
35
+ cell = np.asarray(get_cell(cell), dtype=float)
36
+ n_arr: np.ndarray
36
37
  if n_mesh is None:
37
38
  if density is None:
38
39
  raise MeshInputError("either n_mesh or density must be specified")
39
- n_mesh = np.rint(np.linalg.norm(cell, axis=-1) / density).astype(int)
40
+ n_arr = np.rint(np.linalg.norm(cell, axis=-1) / density).astype(int)
40
41
  elif density is not None:
41
42
  raise MeshInputError(
42
43
  "You cannot set n_mesh at density at the same time. Set one of them to None"
43
44
  )
44
- n_mesh = np.atleast_1d(n_mesh).astype(int)
45
- if len(n_mesh) == 1:
46
- n_mesh = np.repeat(n_mesh, 3)
47
- elif len(n_mesh) != 3:
45
+ else:
46
+ n_arr = np.atleast_1d(n_mesh).astype(int)
47
+ if len(n_arr) == 1:
48
+ n_arr = np.repeat(n_arr, 3)
49
+ elif len(n_arr) != 3:
48
50
  raise MeshInputError("n_mesh must be a 3-dim vector")
49
- linspace = [np.linspace(0, 1, nn, endpoint=endpoint) for nn in n_mesh]
51
+ linspace = [np.linspace(0, 1, nn, endpoint=endpoint) for nn in n_arr]
50
52
  x_mesh = np.meshgrid(*linspace, indexing="ij")
51
53
  return np.einsum("ixyz,ij->jxyz", x_mesh, cell)
@@ -1,18 +1,21 @@
1
1
  from __future__ import annotations
2
- from ase.atoms import Atoms
2
+
3
+ from collections.abc import Iterator
3
4
  from threading import Event, Thread
4
- from typing import overload, Literal, TypeVar, Generic, Any, Iterator
5
+ from typing import Any, Generic, Literal, TypeVar, cast, overload
6
+
7
+ from ase.atoms import Atoms
5
8
 
6
9
  from ._types import (
7
10
  Composition,
8
- ShellWeights,
9
- ShellRadii,
10
- SublatticeMode,
11
11
  IterationMode,
12
+ LogLevel,
12
13
  Prec,
14
+ ShellRadii,
15
+ ShellWeights,
13
16
  SqsResultInteract,
14
17
  SqsResultSplit,
15
- LogLevel,
18
+ SublatticeMode,
16
19
  )
17
20
 
18
21
  R = TypeVar("R", SqsResultInteract, SqsResultSplit)
@@ -21,12 +24,12 @@ T = TypeVar("T")
21
24
 
22
25
  class _SqsResultProxy(Generic[R]):
23
26
  def __init__(self, result: R):
24
- self._result = result
27
+ self._result: R = result
25
28
 
26
29
  def atoms(self) -> Atoms:
27
30
  from sqsgenerator import to_ase
28
31
 
29
- return to_ase(self._result.structure())
32
+ return to_ase(cast(Any, self._result).structure())
30
33
 
31
34
  def __getattr__(self, item: str) -> Any:
32
35
  return getattr(self._result, item)
@@ -186,24 +189,28 @@ def sqs_structures(
186
189
 
187
190
  from sqsgenerator import parse_config
188
191
  from sqsgenerator.core import (
189
- ParseError,
190
192
  LogLevel as SqsLogLevel,
193
+ )
194
+ from sqsgenerator.core import (
195
+ ParseError,
191
196
  SqsCallbackContext,
197
+ )
198
+ from sqsgenerator.core import (
192
199
  optimize as sqs_optimize,
193
200
  )
194
201
 
195
- config = dict(
196
- prec=precision,
197
- iteration_mode=iteration_mode,
198
- sublattice_mode=sublattice_mode,
199
- structure=dict(
200
- lattice=structure.cell.array.tolist(),
201
- coords=structure.get_scaled_positions().tolist(),
202
- species=structure.get_atomic_numbers().tolist(),
203
- ),
204
- iterations=iterations,
205
- max_results_per_objective=max_results_per_objective,
206
- )
202
+ config: dict[str, Any] = {
203
+ "prec": precision,
204
+ "iteration_mode": iteration_mode,
205
+ "sublattice_mode": sublattice_mode,
206
+ "structure": {
207
+ "lattice": structure.cell.array.tolist(),
208
+ "coords": structure.get_scaled_positions().tolist(),
209
+ "species": structure.get_atomic_numbers().tolist(),
210
+ },
211
+ "iterations": iterations,
212
+ "max_results_per_objective": max_results_per_objective,
213
+ }
207
214
  if atol is not None:
208
215
  config["atol"] = atol
209
216
  if rtol is not None:
@@ -216,7 +223,7 @@ def sqs_structures(
216
223
  f"Invalid supercell: {supercell}. All dimensions must be positive integers."
217
224
  )
218
225
 
219
- def _preprocess_for_mode(v: T | list[T] | None) -> list[T] | None:
226
+ def _preprocess_for_mode(v: T | list[T] | None) -> T | list[T] | None:
220
227
  match sublattice_mode:
221
228
  case "interact":
222
229
  return v
@@ -227,15 +234,20 @@ def sqs_structures(
227
234
  f"Invalid sublattice mode: {sublattice_mode}. Use 'interact' or 'split'."
228
235
  )
229
236
 
230
- if (composition := _preprocess_for_mode(composition)) is not None:
231
- config["composition"] = composition
232
- if (shell_weights := _preprocess_for_mode(shell_weights)) is not None:
233
- config["shell_weights"] = shell_weights
234
- if (shell_radii := _preprocess_for_mode(shell_radii)) is not None:
235
- config["shell_radii"] = shell_radii
237
+ composition_processed = cast(Any, _preprocess_for_mode(composition))
238
+ if composition_processed is not None:
239
+ config["composition"] = composition_processed
240
+ shell_weights_processed = cast(Any, _preprocess_for_mode(shell_weights))
241
+ if shell_weights_processed is not None:
242
+ config["shell_weights"] = shell_weights_processed
243
+ shell_radii_processed = cast(Any, _preprocess_for_mode(shell_radii))
244
+ if shell_radii_processed is not None:
245
+ config["shell_radii"] = shell_radii_processed
236
246
  if objective is None:
237
- objective = 0.0 if sublattice_mode == "interact" else [0.0] * len(composition)
238
- config["target_objective"] = _preprocess_for_mode(objective)
247
+ objective = (
248
+ 0.0 if sublattice_mode == "interact" else [0.0] * len(composition_processed)
249
+ )
250
+ config["target_objective"] = cast(Any, _preprocess_for_mode(objective))
239
251
 
240
252
  if num_threads is not None:
241
253
  if num_threads > 0:
@@ -1,6 +1,7 @@
1
+ from typing import Literal, Protocol, TypeAlias, overload
2
+
1
3
  import numpy as np
2
4
  from ase import Atoms
3
- from typing import Literal, TypeAlias, Protocol, overload, Union
4
5
 
5
6
  Shell: TypeAlias = int
6
7
 
@@ -124,11 +125,11 @@ Element = Literal[
124
125
  "Fl",
125
126
  ]
126
127
 
127
- Site = Union[str, list[int]]
128
+ Site = str | list[int]
128
129
 
129
130
  Prec = Literal["single", "double"]
130
131
 
131
- Composition = dict[Element | Literal["sites"], Union[int, Site]]
132
+ Composition = dict[Element | Literal["sites"], int | Site]
132
133
 
133
134
  ShellWeights = dict[Shell, float]
134
135
 
@@ -137,7 +138,7 @@ ShellRadii = list[float]
137
138
  LogLevel = Literal["warn", "info", "debug", "error", "trace"]
138
139
 
139
140
 
140
- class SroParameter:
141
+ class SroParameter(Protocol):
141
142
  @property
142
143
  def i(self) -> int: ...
143
144
 
@@ -53,8 +53,8 @@ def get_extended_positions(
53
53
  width /= get_vertical_length(structure=structure, norm_order=norm_order)
54
54
  rep = 2 * np.ceil(width).astype(int) * structure.pbc + 1
55
55
  rep = [np.arange(r) - int(r / 2) for r in rep]
56
- meshgrid = np.meshgrid(rep[0], rep[1], rep[2])
57
- meshgrid = np.stack(meshgrid, axis=-1).reshape(-1, 3)
56
+ rep_grids = np.meshgrid(rep[0], rep[1], rep[2])
57
+ meshgrid = np.stack(rep_grids, axis=-1).reshape(-1, 3)
58
58
  v_repeated = np.einsum("ni,ij->nj", meshgrid, structure.cell)
59
59
  v_repeated = v_repeated[:, np.newaxis, :] + positions[np.newaxis, :, :]
60
60
  v_repeated = v_repeated.reshape(-1, 3)
@@ -184,7 +184,7 @@ def set_indices(structure: Atoms, indices: np.ndarray) -> Atoms:
184
184
  return structure
185
185
 
186
186
 
187
- def get_average_of_unique_labels(labels: np.ndarray, values: np.ndarray) -> float:
187
+ def get_average_of_unique_labels(labels: np.ndarray, values: np.ndarray) -> np.ndarray:
188
188
  """
189
189
 
190
190
  This function returns the average values of those elements, which share the same labels
@@ -251,20 +251,20 @@ def apply_strain(
251
251
  If `lagrangian`, epsilon is given by `(F^T * F - 1) / 2`. It raises an error if
252
252
  the strain is not symmetric (if the shear components are given).
253
253
  """
254
- epsilon = np.array([epsilon]).flatten()
255
- if len(epsilon) == 3 or len(epsilon) == 1:
256
- epsilon = epsilon * np.eye(3)
257
- epsilon = epsilon.reshape(3, 3)
258
- if epsilon.min() < -1.0:
254
+ eps: np.ndarray = np.array([epsilon]).flatten()
255
+ if len(eps) == 3 or len(eps) == 1:
256
+ eps = eps * np.eye(3)
257
+ eps = eps.reshape(3, 3)
258
+ if eps.min() < -1.0:
259
259
  raise ValueError("Strain value too negative")
260
260
  structure_copy = structure.copy() if return_box else structure
261
261
  cell = structure_copy.cell.copy()
262
262
  if mode == "linear":
263
- F = epsilon + np.eye(3)
263
+ F = eps + np.eye(3)
264
264
  elif mode == "lagrangian":
265
- if not np.allclose(epsilon, epsilon.T):
265
+ if not np.allclose(eps, eps.T):
266
266
  raise ValueError("Strain must be symmetric if `mode = 'lagrangian'`")
267
- E, V = np.linalg.eigh(2 * epsilon + np.eye(3))
267
+ E, V = np.linalg.eigh(2 * eps + np.eye(3))
268
268
  F = np.einsum("ik,k,jk->ij", V, np.sqrt(E), V)
269
269
  else:
270
270
  raise ValueError("mode must be `linear` or `lagrangian`")
@@ -153,7 +153,7 @@ def plot3d(
153
153
  spacefill=spacefill,
154
154
  particle_size=particle_size,
155
155
  background=background,
156
- color_scheme=color_scheme,
156
+ color_scheme=color_scheme if color_scheme is not None else "element",
157
157
  )
158
158
  else:
159
159
  raise ValueError("plot method not recognized")
@@ -453,15 +453,20 @@ def _plot3d(
453
453
  vector_field is not None and vector_field is not None
454
454
  ): # WARNING: There must be a bug here...
455
455
  try:
456
- if vector_color.shape != np.ones((len(structure), 3)).shape:
456
+ if (
457
+ vector_color is not None
458
+ and vector_color.shape != np.ones((len(structure), 3)).shape
459
+ ):
457
460
  vector_color = np.outer(
458
461
  np.ones(len(structure)),
459
462
  vector_color / np.linalg.norm(vector_color),
460
463
  )
461
464
  except AttributeError:
462
- vector_color = np.ones((len(structure), 3)) * vector_color
465
+ if vector_color is not None:
466
+ vector_color = np.ones((len(structure), 3)) * vector_color
463
467
 
464
468
  if vector_field is not None:
469
+ assert vector_color is not None
465
470
  for arr, pos, col in zip(vector_field, positions, vector_color, strict=True):
466
471
  view.shape.add_arrow(list(pos), list(pos + arr), list(col), 0.2)
467
472
 
@@ -1,34 +0,0 @@
1
- # file generated by setuptools-scm
2
- # don't change, don't track in version control
3
-
4
- __all__ = [
5
- "__version__",
6
- "__version_tuple__",
7
- "version",
8
- "version_tuple",
9
- "__commit_id__",
10
- "commit_id",
11
- ]
12
-
13
- TYPE_CHECKING = False
14
- if TYPE_CHECKING:
15
- from typing import Tuple
16
- from typing import Union
17
-
18
- VERSION_TUPLE = Tuple[Union[int, str], ...]
19
- COMMIT_ID = Union[str, None]
20
- else:
21
- VERSION_TUPLE = object
22
- COMMIT_ID = object
23
-
24
- version: str
25
- __version__: str
26
- __version_tuple__: VERSION_TUPLE
27
- version_tuple: VERSION_TUPLE
28
- commit_id: COMMIT_ID
29
- __commit_id__: COMMIT_ID
30
-
31
- __version__ = version = '0.0.43'
32
- __version_tuple__ = version_tuple = (0, 0, 43)
33
-
34
- __commit_id__ = commit_id = None
@@ -1,19 +1,19 @@
1
+ from ._interface import sqs_structures
1
2
  from ._types import (
2
- SqsResultSplit,
3
- SqsResult,
4
- SqsResultInteract,
5
- SublatticeMode,
6
- IterationMode,
7
- Site,
3
+ Composition,
8
4
  Element,
5
+ IterationMode,
6
+ Prec,
7
+ Shell,
9
8
  ShellRadii,
10
9
  ShellWeights,
11
- Shell,
12
- Composition,
13
- Prec,
10
+ Site,
11
+ SqsResult,
12
+ SqsResultInteract,
13
+ SqsResultSplit,
14
14
  SroParameter,
15
+ SublatticeMode,
15
16
  )
16
- from ._interface import sqs_structures
17
17
 
18
18
  __all__ = [
19
19
  "Composition",