matid 2.2.0__tar.gz → 2.3.0__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 (53) hide show
  1. matid-2.3.0/PKG-INFO +110 -0
  2. {matid-2.2.0 → matid-2.3.0}/matid/clustering/sbc.py +39 -18
  3. {matid-2.2.0 → matid-2.3.0}/matid/core/distances.py +70 -0
  4. {matid-2.2.0 → matid-2.3.0}/matid/core/linkedunits.py +28 -1
  5. {matid-2.2.0 → matid-2.3.0}/matid/core/periodicfinder.py +173 -69
  6. {matid-2.2.0 → matid-2.3.0}/matid/ext/celllist.cpp +116 -30
  7. {matid-2.2.0 → matid-2.3.0}/matid/ext/celllist.h +25 -3
  8. {matid-2.2.0 → matid-2.3.0}/matid/ext/ext.cpp +9 -2
  9. {matid-2.2.0 → matid-2.3.0}/matid/ext/geometry.cpp +46 -14
  10. {matid-2.2.0 → matid-2.3.0}/matid/ext/geometry.h +7 -6
  11. {matid-2.2.0 → matid-2.3.0}/matid/geometry/geometry.py +94 -62
  12. matid-2.3.0/matid.egg-info/PKG-INFO +110 -0
  13. {matid-2.2.0 → matid-2.3.0}/pyproject.toml +11 -6
  14. matid-2.2.0/PKG-INFO +0 -313
  15. matid-2.2.0/matid.egg-info/PKG-INFO +0 -313
  16. {matid-2.2.0 → matid-2.3.0}/LICENSE +0 -0
  17. {matid-2.2.0 → matid-2.3.0}/MANIFEST.in +0 -0
  18. {matid-2.2.0 → matid-2.3.0}/README.md +0 -0
  19. {matid-2.2.0 → matid-2.3.0}/matid/__init__.py +0 -0
  20. {matid-2.2.0 → matid-2.3.0}/matid/classification/__init__.py +0 -0
  21. {matid-2.2.0 → matid-2.3.0}/matid/classification/classifications.py +0 -0
  22. {matid-2.2.0 → matid-2.3.0}/matid/classification/classifier.py +0 -0
  23. {matid-2.2.0 → matid-2.3.0}/matid/clustering/__init__.py +0 -0
  24. {matid-2.2.0 → matid-2.3.0}/matid/clustering/cluster.py +0 -0
  25. {matid-2.2.0 → matid-2.3.0}/matid/core/__init__.py +0 -0
  26. {matid-2.2.0 → matid-2.3.0}/matid/core/lattice.py +0 -0
  27. {matid-2.2.0 → matid-2.3.0}/matid/core/system.py +0 -0
  28. {matid-2.2.0 → matid-2.3.0}/matid/data/__init__.py +0 -0
  29. {matid-2.2.0 → matid-2.3.0}/matid/data/alphabet_data.py +0 -0
  30. {matid-2.2.0 → matid-2.3.0}/matid/data/constants.py +0 -0
  31. {matid-2.2.0 → matid-2.3.0}/matid/data/element_data.py +0 -0
  32. {matid-2.2.0 → matid-2.3.0}/matid/data/symmetry_data.py +0 -0
  33. {matid-2.2.0 → matid-2.3.0}/matid/geometry/__init__.py +0 -0
  34. {matid-2.2.0 → matid-2.3.0}/matid/symmetry/__init__.py +0 -0
  35. {matid-2.2.0 → matid-2.3.0}/matid/symmetry/symmetryanalyzer.py +0 -0
  36. {matid-2.2.0 → matid-2.3.0}/matid/symmetry/wyckoffset.py +0 -0
  37. {matid-2.2.0 → matid-2.3.0}/matid/utils/__init__.py +0 -0
  38. {matid-2.2.0 → matid-2.3.0}/matid/utils/exceptions.py +0 -0
  39. {matid-2.2.0 → matid-2.3.0}/matid/utils/segfault_protect.py +0 -0
  40. {matid-2.2.0 → matid-2.3.0}/matid/utils/surfacegenerator.py +0 -0
  41. {matid-2.2.0 → matid-2.3.0}/matid/utils/symmetry_info_generation/generate_symmetry_info.py +0 -0
  42. {matid-2.2.0 → matid-2.3.0}/matid/utils/symmetry_info_generation/query_continuous_translations.py +0 -0
  43. {matid-2.2.0 → matid-2.3.0}/matid/utils/symmetry_info_generation/query_conventional_transform_info.py +0 -0
  44. {matid-2.2.0 → matid-2.3.0}/matid/utils/symmetry_info_generation/query_normalizers.py +0 -0
  45. {matid-2.2.0 → matid-2.3.0}/matid/utils/symmetry_info_generation/query_space_group_info.py +0 -0
  46. {matid-2.2.0 → matid-2.3.0}/matid/utils/symmetry_info_generation/query_wyckoff_sets.py +0 -0
  47. {matid-2.2.0 → matid-2.3.0}/matid.egg-info/SOURCES.txt +0 -0
  48. {matid-2.2.0 → matid-2.3.0}/matid.egg-info/dependency_links.txt +0 -0
  49. {matid-2.2.0 → matid-2.3.0}/matid.egg-info/requires.txt +0 -0
  50. {matid-2.2.0 → matid-2.3.0}/matid.egg-info/top_level.txt +0 -0
  51. {matid-2.2.0 → matid-2.3.0}/requirements.txt +0 -0
  52. {matid-2.2.0 → matid-2.3.0}/setup.cfg +0 -0
  53. {matid-2.2.0 → matid-2.3.0}/setup.py +0 -0
matid-2.3.0/PKG-INFO ADDED
@@ -0,0 +1,110 @@
1
+ Metadata-Version: 2.4
2
+ Name: matid
3
+ Version: 2.3.0
4
+ Summary: MatID is a Python package for identifying and analyzing atomistic systems based on their structure.
5
+ Author: Lauri Himanen
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Documentation, https://fairmat-nfdi.github.io/matid/
8
+ Project-URL: Source, https://github.com/fairmat-nfdi/matid
9
+ Keywords: atoms,structure,materials,science,crystal,symmetry
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Topic :: Scientific/Engineering :: Physics
13
+ Classifier: Programming Language :: Python
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: numpy
25
+ Requires-Dist: ase
26
+ Requires-Dist: spglib>=2.0.0
27
+ Requires-Dist: networkx>=2.4
28
+ Provides-Extra: dev
29
+ Requires-Dist: coverage; extra == "dev"
30
+ Requires-Dist: pytest; extra == "dev"
31
+ Requires-Dist: ruff; extra == "dev"
32
+ Requires-Dist: pydoc-markdown; extra == "dev"
33
+ Requires-Dist: genbadge[coverage]; extra == "dev"
34
+ Dynamic: license-file
35
+
36
+ <img src="https://raw.githubusercontent.com/fairmat-nfdi/matid/main/docs/static/img/logo.png" width="300">
37
+
38
+ ![Build status](https://github.com/fairmat-nfdi/matid/actions/workflows/test.yml/badge.svg)
39
+ ![Coverage Status](./reports/coverage/coverage-badge.svg)
40
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
41
+
42
+ MatID is a Python package for identifying and analyzing atomistic systems based
43
+ on their structure.
44
+
45
+ # Documentation
46
+ For more details and tutorials, visit the documentation at:
47
+ [https://fairmat-nfdi.github.io/matid/](https://fairmat-nfdi.github.io/matid/)
48
+
49
+ You can find even more details in the following open-access articles:
50
+
51
+ - [Automated identification of bulk structures, two-dimensional materials, and interfaces using symmetry-based clustering](<https://doi.org/10.1038/s41524-024-01498-x>)
52
+ - [Materials structure genealogy and high-throughput topological classification of surfaces and 2D materials](<https://doi.org/10.1038/s41524-018-0107-6>)
53
+
54
+ ## Example: Surface detection and analysis
55
+
56
+ ```python
57
+ import ase.io
58
+ from ase.visualize import view
59
+
60
+ from matid.clustering import SBC
61
+ from matid.symmetry import SymmetryAnalyzer
62
+
63
+ # Load structure from a file
64
+ system = ase.io.read('data/system.xyz')
65
+
66
+ # Find interesting substructures using Symmetry-based Clustering (SBC)
67
+ sbc = SBC()
68
+ clusters = sbc.get_clusters(system)
69
+
70
+ # Analyze each found cluster printing out the indices of the atoms belonging to
71
+ # this cluster and visualizing the conventional cell from which the cluster was
72
+ # built from.
73
+ for cluster in clusters:
74
+
75
+ # Get the indices of the atoms belonging to this cluster
76
+ indices = cluster.indices
77
+ print(indices)
78
+
79
+ # Get the dimensionality of the cluster
80
+ dimensionality = cluster.get_dimensionality()
81
+ print(dimensionality)
82
+
83
+ # Get the cell from which the cluster is constructed from. The periodicity
84
+ # of this cell indicates in which directions the unit cell has been found to
85
+ # be repeated in (at least once, possibly infinitely).
86
+ cell = cluster.get_cell()
87
+ n_repeated_directions = sum(cell.get_pbc())
88
+ print(n_repeated_directions)
89
+
90
+ # Analyze some symmetry properties of the underlying cell to better identify
91
+ # the material from which the cluster has been constructed from.
92
+ analyzer = SymmetryAnalyzer(cell, symmetry_tol=0.5)
93
+ conv_sys = analyzer.get_conventional_system()
94
+ view(conv_sys)
95
+ ```
96
+
97
+ # Installation
98
+
99
+ ## pip
100
+ ```sh
101
+ pip install matid
102
+ ```
103
+
104
+ ## From source
105
+ ```sh
106
+ git clone https://github.com/fairmat-nfdi/matid.git
107
+ cd matid
108
+ pip install .
109
+ ```
110
+
@@ -5,7 +5,7 @@ import ase.geometry
5
5
 
6
6
  import matid.geometry
7
7
  from matid.clustering.cluster import Cluster
8
- from matid.core.periodicfinder import PeriodicFinder
8
+ from matid.core.periodicfinder import PeriodicFinder, get_region_cell_list
9
9
 
10
10
 
11
11
  class SBC:
@@ -115,16 +115,25 @@ class SBC:
115
115
 
116
116
  # Calculate the distances here once if they have not been provided. A
117
117
  # finite radial cutoff is used: the clustering only ever consults local
118
- # distances (spans within max_cell_size, and the smaller merge_radius /
119
- # bond_threshold thresholds). Pairs beyond the cutoff are reported as
120
- # infinite, which fail those same thresholds, so the result is identical
121
- # to using an infinite cutoff while being dramatically faster.
118
+ # distances. Pairs beyond the cutoff are reported as infinite, which
119
+ # fail the same thresholds, so the result is identical to using an
120
+ # infinite cutoff while being dramatically faster. The distances are
121
+ # consulted with the following thresholds:
122
+ # - Cell spans: raw distance < max_cell_size
123
+ # - Cluster localization: distance - radii < merge_radius
124
+ # - Cluster cleaning and dimensionality: distance - radii <= bond_threshold
125
+ # The cutoff is the smallest one that covers all of these, as the number
126
+ # of stored neighbours grows with the cube of the cutoff. A small margin
127
+ # protects against floating point differences at the boundary.
122
128
  max_radii = radii.max()
123
- cutoff = max(max_cell_size, merge_radius, bond_threshold) + 2 * max_radii
129
+ cutoff = (
130
+ max(max_cell_size, max(merge_radius, bond_threshold) + 2 * max_radii) + 1e-6
131
+ )
124
132
  distances = matid.geometry.get_distances(system_copy, radii, cutoff=cutoff)
125
133
 
126
134
  # Iteratively search for new clusters until whole system is covered
127
135
  periodic_finder = PeriodicFinder(angle_tol=angle_tol)
136
+ cell_list = get_region_cell_list(system_copy, pos_tol)
128
137
  indices = set(list(range(len(system_copy))))
129
138
  clusters = []
130
139
  while len(indices) != 0:
@@ -138,6 +147,7 @@ class SBC:
138
147
  overlap_threshold=overlap_threshold,
139
148
  distances=distances,
140
149
  return_mask=True,
150
+ cell_list=cell_list,
141
151
  )
142
152
 
143
153
  # All neighbours that the periodic finder has tested are removed
@@ -286,15 +296,15 @@ class SBC:
286
296
  """
287
297
  # Get all overlapping atoms, and the regions with which they overlap
288
298
  overlap_map = defaultdict(list)
289
- for i in range(len(system)):
290
- for cluster in clusters:
291
- if i in cluster.indices:
292
- overlap_map[i].append(cluster)
299
+ for cluster in clusters:
300
+ for i in set(cluster.indices):
301
+ overlap_map[int(i)].append(cluster)
293
302
 
294
303
  # Assign each overlapping atom to the cluster that is "nearest". Notice
295
- # that we do not update the regions during the process.
296
- # positions = system.get_positions()
297
- for i, i_clusters in overlap_map.items():
304
+ # that we do not update the regions during the process. The atoms are
305
+ # processed in ascending index order.
306
+ for i in sorted(overlap_map):
307
+ i_clusters = overlap_map[i]
298
308
  if len(i_clusters) > 1:
299
309
  surrounding_indices = set(
300
310
  np.where(distances.get_radii_distance_row(i) < merge_radius)[0]
@@ -332,11 +342,22 @@ class SBC:
332
342
  for cluster in clusters:
333
343
  # If the cluster cleaning fails, the cluster is not reported
334
344
  try:
335
- dbscan_clusters = matid.geometry.get_clusters(
336
- cluster._get_distance_matrix_radii_mic(),
337
- bond_threshold,
338
- min_samples=1,
339
- )
345
+ # The connectivity is resolved from the sparse neighbour list
346
+ # instead of a dense distance submatrix, whose size would
347
+ # scale quadratically with the cluster size.
348
+ if bond_threshold > 0:
349
+ rows, cols = cluster._distances.get_radii_distance_edges(
350
+ cluster.indices, bond_threshold
351
+ )
352
+ dbscan_clusters = matid.geometry.get_clusters_from_edges(
353
+ len(cluster.indices), rows, cols
354
+ )
355
+ else:
356
+ dbscan_clusters = matid.geometry.get_clusters(
357
+ cluster._get_distance_matrix_radii_mic(),
358
+ bond_threshold,
359
+ min_samples=1,
360
+ )
340
361
  except Exception:
341
362
  continue
342
363
  largest_indices = max(dbscan_clusters, key=len)
@@ -68,6 +68,32 @@ class Distances:
68
68
  self._row_ptr = np.searchsorted(sorted_row, np.arange(n + 1))
69
69
  return self
70
70
 
71
+ @classmethod
72
+ def from_csr(cls, n, row_ptr, col, distance, displacement, factor, radii):
73
+ """Construct a sparse-backed Distances from a CSR neighbour list, where
74
+ the neighbours of atom ``i`` are stored in the slice
75
+ ``row_ptr[i]:row_ptr[i + 1]``. The arrays are used as is, without
76
+ copying.
77
+
78
+ Args:
79
+ n: Number of atoms.
80
+ row_ptr: Row offsets, shape (n + 1,).
81
+ col, distance, displacement, factor: Neighbour data in the same
82
+ format as for :meth:`from_sparse`, grouped by row.
83
+ radii: Per-atom radii, shape (n,).
84
+ """
85
+ self = cls.__new__(cls)
86
+ self._dense = False
87
+ self._n = n
88
+ self._radii = np.asarray(radii)
89
+ self.cell_list = None
90
+ self._row_ptr = np.asarray(row_ptr)
91
+ self._col = np.asarray(col)
92
+ self._dist = np.asarray(distance)
93
+ self._disp = np.asarray(displacement)
94
+ self._fac = np.asarray(factor)
95
+ return self
96
+
71
97
  # ------------------------------------------------------------------
72
98
  # Dense matrix properties (only valid in dense / infinite-cutoff mode).
73
99
  # ------------------------------------------------------------------
@@ -166,3 +192,47 @@ class Distances:
166
192
  di = np.arange(k)
167
193
  out[di, di] = -2.0 * self._radii[indices]
168
194
  return out
195
+
196
+ def get_radii_distance_edges(self, indices, threshold):
197
+ """Returns the local index pairs ``(li, lj)`` with ``li <= lj`` for which
198
+ ``dist_matrix_radii_mic[np.ix_(indices, indices)] <= threshold``,
199
+ including the diagonal. The pairs are returned in row-major order.
200
+
201
+ This is a sparse alternative to :meth:`get_radii_distance_submatrix`
202
+ for finding connectivity within a large group of atoms: the memory
203
+ footprint scales with the number of connected pairs instead of the
204
+ square of the number of atoms.
205
+
206
+ Returns:
207
+ Two integer arrays ``(li, lj)``.
208
+ """
209
+ indices = np.asarray(indices, dtype=np.intp)
210
+ k = len(indices)
211
+ if self._dense:
212
+ submatrix = self._dist_matrix_radii_mic[np.ix_(indices, indices)]
213
+ li, lj = np.nonzero(np.triu(submatrix <= threshold))
214
+ return li, lj
215
+
216
+ # Gather the neighbour list entries of all requested rows at once.
217
+ g2l = np.full(self._n, -1, dtype=np.intp)
218
+ g2l[indices] = np.arange(k)
219
+ starts = self._row_ptr[indices]
220
+ lengths = self._row_ptr[indices + 1] - starts
221
+ li = np.repeat(np.arange(k), lengths)
222
+ offsets = np.cumsum(lengths) - lengths
223
+ entries = np.arange(lengths.sum()) + np.repeat(starts - offsets, lengths)
224
+ cols = self._col[entries]
225
+ lj = g2l[cols]
226
+ sel = lj > li
227
+ li, lj, gi, cols = li[sel], lj[sel], indices[li[sel]], cols[sel]
228
+ dist = self._dist[entries[sel]] - (self._radii[gi] + self._radii[cols])
229
+ sel = dist <= threshold
230
+ li, lj = li[sel], lj[sel]
231
+
232
+ # Add the diagonal and order the pairs row-major.
233
+ diag = np.arange(k)
234
+ diag = diag[-2.0 * self._radii[indices] <= threshold]
235
+ li = np.concatenate((li, diag))
236
+ lj = np.concatenate((lj, diag))
237
+ order = np.lexsort((lj, li))
238
+ return li[order], lj[order]
@@ -32,13 +32,40 @@ class LinkedUnitCollection(dict):
32
32
  self.system = system
33
33
  self.cell = cell
34
34
  self.is_2d = is_2d
35
- self._search_graph = nx.MultiDiGraph()
35
+ self._search_graph_cache = nx.MultiDiGraph()
36
+ self._search_graph_log = []
37
+ self._search_graph_n_replayed = 0
36
38
  self._index_cell_map = {}
37
39
  self._used_points = set()
38
40
  self._basis_indices = None
39
41
  self._pos_tol = None
40
42
  dict.__init__(self)
41
43
 
44
+ def add_search_edge(self, cell_index, seed_index, target_cell, match, multiplier):
45
+ """Records a link between two cells that was found during the search.
46
+
47
+ Creating the networkx graph is comparatively expensive and it is not
48
+ needed by all users. The links are therefore only recorded here and
49
+ the graph is created on demand when _search_graph is accessed.
50
+ """
51
+ self._search_graph_log.append(
52
+ (cell_index, seed_index, target_cell, match, multiplier)
53
+ )
54
+
55
+ @property
56
+ def _search_graph(self):
57
+ """A networkx.MultiDiGraph of the links between the cells that were
58
+ found during the search."""
59
+ G = self._search_graph_cache
60
+ log = self._search_graph_log
61
+ for i in range(self._search_graph_n_replayed, len(log)):
62
+ cell_index, seed_index, target_cell, match, multiplier = log[i]
63
+ G.add_node(cell_index, index=seed_index)
64
+ G.add_node(target_cell, index=match)
65
+ G.add_edge(cell_index, target_cell, multiplier=multiplier)
66
+ self._search_graph_n_replayed = len(log)
67
+ return G
68
+
42
69
  def __setitem__(self, key, value):
43
70
  # Transform key to tuple, check length
44
71
  try: