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.
- matid-2.3.0/PKG-INFO +110 -0
- {matid-2.2.0 → matid-2.3.0}/matid/clustering/sbc.py +39 -18
- {matid-2.2.0 → matid-2.3.0}/matid/core/distances.py +70 -0
- {matid-2.2.0 → matid-2.3.0}/matid/core/linkedunits.py +28 -1
- {matid-2.2.0 → matid-2.3.0}/matid/core/periodicfinder.py +173 -69
- {matid-2.2.0 → matid-2.3.0}/matid/ext/celllist.cpp +116 -30
- {matid-2.2.0 → matid-2.3.0}/matid/ext/celllist.h +25 -3
- {matid-2.2.0 → matid-2.3.0}/matid/ext/ext.cpp +9 -2
- {matid-2.2.0 → matid-2.3.0}/matid/ext/geometry.cpp +46 -14
- {matid-2.2.0 → matid-2.3.0}/matid/ext/geometry.h +7 -6
- {matid-2.2.0 → matid-2.3.0}/matid/geometry/geometry.py +94 -62
- matid-2.3.0/matid.egg-info/PKG-INFO +110 -0
- {matid-2.2.0 → matid-2.3.0}/pyproject.toml +11 -6
- matid-2.2.0/PKG-INFO +0 -313
- matid-2.2.0/matid.egg-info/PKG-INFO +0 -313
- {matid-2.2.0 → matid-2.3.0}/LICENSE +0 -0
- {matid-2.2.0 → matid-2.3.0}/MANIFEST.in +0 -0
- {matid-2.2.0 → matid-2.3.0}/README.md +0 -0
- {matid-2.2.0 → matid-2.3.0}/matid/__init__.py +0 -0
- {matid-2.2.0 → matid-2.3.0}/matid/classification/__init__.py +0 -0
- {matid-2.2.0 → matid-2.3.0}/matid/classification/classifications.py +0 -0
- {matid-2.2.0 → matid-2.3.0}/matid/classification/classifier.py +0 -0
- {matid-2.2.0 → matid-2.3.0}/matid/clustering/__init__.py +0 -0
- {matid-2.2.0 → matid-2.3.0}/matid/clustering/cluster.py +0 -0
- {matid-2.2.0 → matid-2.3.0}/matid/core/__init__.py +0 -0
- {matid-2.2.0 → matid-2.3.0}/matid/core/lattice.py +0 -0
- {matid-2.2.0 → matid-2.3.0}/matid/core/system.py +0 -0
- {matid-2.2.0 → matid-2.3.0}/matid/data/__init__.py +0 -0
- {matid-2.2.0 → matid-2.3.0}/matid/data/alphabet_data.py +0 -0
- {matid-2.2.0 → matid-2.3.0}/matid/data/constants.py +0 -0
- {matid-2.2.0 → matid-2.3.0}/matid/data/element_data.py +0 -0
- {matid-2.2.0 → matid-2.3.0}/matid/data/symmetry_data.py +0 -0
- {matid-2.2.0 → matid-2.3.0}/matid/geometry/__init__.py +0 -0
- {matid-2.2.0 → matid-2.3.0}/matid/symmetry/__init__.py +0 -0
- {matid-2.2.0 → matid-2.3.0}/matid/symmetry/symmetryanalyzer.py +0 -0
- {matid-2.2.0 → matid-2.3.0}/matid/symmetry/wyckoffset.py +0 -0
- {matid-2.2.0 → matid-2.3.0}/matid/utils/__init__.py +0 -0
- {matid-2.2.0 → matid-2.3.0}/matid/utils/exceptions.py +0 -0
- {matid-2.2.0 → matid-2.3.0}/matid/utils/segfault_protect.py +0 -0
- {matid-2.2.0 → matid-2.3.0}/matid/utils/surfacegenerator.py +0 -0
- {matid-2.2.0 → matid-2.3.0}/matid/utils/symmetry_info_generation/generate_symmetry_info.py +0 -0
- {matid-2.2.0 → matid-2.3.0}/matid/utils/symmetry_info_generation/query_continuous_translations.py +0 -0
- {matid-2.2.0 → matid-2.3.0}/matid/utils/symmetry_info_generation/query_conventional_transform_info.py +0 -0
- {matid-2.2.0 → matid-2.3.0}/matid/utils/symmetry_info_generation/query_normalizers.py +0 -0
- {matid-2.2.0 → matid-2.3.0}/matid/utils/symmetry_info_generation/query_space_group_info.py +0 -0
- {matid-2.2.0 → matid-2.3.0}/matid/utils/symmetry_info_generation/query_wyckoff_sets.py +0 -0
- {matid-2.2.0 → matid-2.3.0}/matid.egg-info/SOURCES.txt +0 -0
- {matid-2.2.0 → matid-2.3.0}/matid.egg-info/dependency_links.txt +0 -0
- {matid-2.2.0 → matid-2.3.0}/matid.egg-info/requires.txt +0 -0
- {matid-2.2.0 → matid-2.3.0}/matid.egg-info/top_level.txt +0 -0
- {matid-2.2.0 → matid-2.3.0}/requirements.txt +0 -0
- {matid-2.2.0 → matid-2.3.0}/setup.cfg +0 -0
- {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
|
+

|
|
39
|
+

|
|
40
|
+
[](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
|
|
119
|
-
#
|
|
120
|
-
# infinite
|
|
121
|
-
#
|
|
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 =
|
|
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
|
|
290
|
-
for
|
|
291
|
-
|
|
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
|
-
#
|
|
297
|
-
for i
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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.
|
|
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:
|