Skip to content

Commit babfea7

Browse files
authored
Small cluster issue (#26)
* Trying out a fix for the small cluster problem. * Updated formatting. * Fixed formatting. * Simplified logic. * Improvement. * Fixed linting. * Updated workflows.
1 parent b4cd5a9 commit babfea7

6 files changed

Lines changed: 88 additions & 33 deletions

File tree

.github/workflows/wheels.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
- uses: actions/checkout@v4
1515

1616
- name: Build wheels
17-
uses: pypa/cibuildwheel@v2.18.0
17+
uses: pypa/cibuildwheel@v2.20.0
1818
env:
1919
CIBW_SKIP: cp36-*
2020

matid/core/periodicfinder.py

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ def get_region(
124124
neighbour_factors,
125125
search_mask,
126126
) = self._find_possible_bases(system, seed_index)
127-
proto_cell, offset, dim = self._find_proto_cell(
127+
proto_cell, offset, dim, n_periodic_spans_selected = self._find_proto_cell(
128128
system,
129129
seed_index,
130130
possible_spans,
@@ -153,7 +153,11 @@ def get_region(
153153

154154
i_indices = unit_collection.get_basis_indices()
155155

156-
if len(i_indices) > 0:
156+
# The region is accepted if it either has more than 1+dim atoms in
157+
# it or one of the spans is periodic. In many cases the prototype
158+
# cell can be found (seed + number of spans), but it cannot really
159+
# be extended to cover any significant portion of the system.
160+
if len(i_indices) > 1 + dim or n_periodic_spans_selected >= 1:
157161
region = unit_collection
158162
region._pos_tol = pos_tol
159163

@@ -223,6 +227,8 @@ def _find_proto_cell(
223227
Returns:
224228
ase.Atoms: A system representing the best cell that was found
225229
np.ndarray: Position of the seed atom in the cell
230+
int: Dimensionality of the found system
231+
int: Number of periodic unit cell vectors used
226232
"""
227233
positions = system.get_positions()
228234
numbers = system.get_atomic_numbers()
@@ -342,18 +348,26 @@ def _find_proto_cell(
342348
| (metric >= 0.75 * n_neighbours)
343349
)[0]
344350

345-
if len(valid_span_indices) == 0:
346-
return None, None, None
351+
total_valid_spans = len(valid_span_indices)
352+
if total_valid_spans == 0:
353+
return None, None, None, None
347354

348355
# Find the best basis
349356
valid_span_metrics = metric[valid_span_indices]
350357
valid_spans = possible_spans[valid_span_indices]
351358
best_combo = self._find_best_basis(valid_spans, valid_span_metrics)
352359
dim = len(best_combo)
353360

361+
# Check how many of the periodic spans are still selected as prototype
362+
# unit cell vectors
363+
selected_spans = range(total_valid_spans - n_periodic_spans, total_valid_spans)
364+
n_periodic_spans_selected = sum(
365+
span_index in best_combo for span_index in selected_spans
366+
)
367+
354368
# Currently 1D is not handled
355369
if dim == 1:
356-
return None, None, 1
370+
return None, None, 1, None
357371

358372
best_spans = valid_spans[best_combo]
359373
n_spans = len(best_spans)
@@ -382,7 +396,7 @@ def _find_proto_cell(
382396

383397
# If the seed atom is not in a valid graph, no region could be found.
384398
if seed_group_index is None:
385-
return None, None, None
399+
return None, None, None, None
386400

387401
# Notice that the seed group index can get updated by the cell search if
388402
# an atom is dropped out of the cell due to appearing too infrequently.
@@ -409,7 +423,7 @@ def _find_proto_cell(
409423
pos_tol,
410424
)
411425
if proto_cell is None:
412-
return None, None, None
426+
return None, None, None, None
413427

414428
two_valid_spans = n_spans == 2
415429
if n_spans == 3:
@@ -424,7 +438,7 @@ def _find_proto_cell(
424438
proto_cell, bond_threshold
425439
)
426440
except MatIDError:
427-
return None, None, None
441+
return None, None, None, None
428442
if dimensionality != 3:
429443
# Try if the cell can be "reduced" to a 2D material
430444
if dimensionality == 2:
@@ -458,20 +472,18 @@ def _find_proto_cell(
458472
two_valid_spans = True
459473
n_spans = 2
460474
else:
461-
return None, None, None
475+
return None, None, None, None
462476

463477
if two_valid_spans:
464478
# If the best 2D vectors consists only of the simulation basis cell
465479
# vectors, check that these vectors are below a predefined size.
466480
# Otherwise the cell cannot be accepted because there is not enough
467481
# statistics about the cell contents to distinguish outliers.
468482
if n_periodic_spans > 0:
469-
periodic_span_indices = valid_span_indices[-n_periodic_spans:]
470-
best_span_ind = valid_span_indices[best_combo]
471-
if set(best_span_ind).issubset(set(periodic_span_indices)):
483+
if n_periodic_spans_selected == 2:
472484
cell_lens = np.linalg.norm(best_spans, axis=1)
473485
if np.any(cell_lens > self.max_2d_single_cell_size):
474-
return None, None, None
486+
return None, None, None, None
475487

476488
# Check the dimensionality
477489
dimensionality, cluster_labels = matid.geometry.get_dimensionality(
@@ -482,7 +494,7 @@ def _find_proto_cell(
482494
# has multiple stacked 2D sheets with identical periodicity. In
483495
# this case the unit cell should only comprise of atoms in the
484496
# cluster where the seed atom is in.
485-
for i_index, i_cluster in enumerate(cluster_labels):
497+
for i_cluster in cluster_labels:
486498
try:
487499
seed_group_index = i_cluster.index(seed_group_index)
488500
except ValueError:
@@ -498,13 +510,13 @@ def _find_proto_cell(
498510
proto_cell, bond_threshold
499511
)
500512
if dimensionality is None:
501-
return None, None, None
513+
return None, None, None, None
502514
else:
503515
if dimensionality != 2:
504-
return None, None, None
516+
return None, None, None, None
505517
else:
506518
if dimensionality != 2:
507-
return None, None, None
519+
return None, None, None, None
508520

509521
# Check the cell thickness. 2D materials that are thicker than a
510522
# specified threshold are not accepted.
@@ -514,16 +526,16 @@ def _find_proto_cell(
514526
offset = proto_cell.get_positions()[seed_group_index]
515527
thickness = matid.geometry.get_thickness(proto_cell, 2)
516528
if thickness > self.max_2d_cell_height:
517-
return None, None, None
529+
return None, None, None, None
518530

519531
# Check that the final proto cell atoms don't overlap
520532
if proto_cell is not None:
521533
dist_proto_cell = matid.geometry.get_distances(proto_cell).dist_matrix_mic
522534
dist_proto_cell = dist_proto_cell[np.triu_indices(dist_proto_cell.shape[0])]
523535
if dist_proto_cell.min() < overlap_threshold:
524-
return None, None, None
536+
return None, None, None, None
525537

526-
return proto_cell, offset, n_spans
538+
return proto_cell, offset, n_spans, n_periodic_spans_selected
527539

528540
def _find_graphs(
529541
self,

matid/symmetry/symmetryanalyzer.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1139,8 +1139,7 @@ def _find_wyckoff_ground_state(
11391139
i_n = r["wyckoff_positions"].get((w, z))
11401140
if i_n is not None:
11411141
n_atoms_map[i_n].append(r)
1142-
if i_n > n_atoms_max:
1143-
n_atoms_max = i_n
1142+
n_atoms_max = max(i_n, n_atoms_max)
11441143
if n_atoms_max != 0:
11451144
representations = n_atoms_map[n_atoms_max]
11461145
if len(representations) == 1:

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = 'matid'
7-
version = '2.1.0'
7+
version = '2.1.1'
88
description = 'MatID is a Python package for identifying and analyzing atomistic systems based on their structure.'
99
readme = "README.md"
1010
authors = [{ name = "Lauri Himanen" }]

tests/clustering/test_sbc.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
import ase.io
66
from ase import Atoms
77
from ase.build import bulk
8+
from ase.visualize import view
89

9-
from conftest import surface, stack, rattle, assert_topology
10+
from conftest import surface, stack, rattle, assert_topology, create_graphene, create_fe
1011
from matid.clustering import SBC, Cluster
1112

1213

@@ -302,6 +303,53 @@ def test_clustering_coincidental_patterns(system, clusters_expected):
302303
assert_topology(results, clusters_expected)
303304

304305

306+
@pytest.mark.parametrize(
307+
"system, clusters_expected",
308+
[
309+
pytest.param(
310+
create_fe(),
311+
[
312+
Cluster(
313+
[0, 1],
314+
dimensionality=3,
315+
),
316+
],
317+
id="clusters that consist only of the 3D prototype cell but contain a periodic span should be accepted",
318+
),
319+
pytest.param(
320+
create_graphene(),
321+
[
322+
Cluster(
323+
[0, 1],
324+
dimensionality=2,
325+
),
326+
],
327+
id="clusters that consist only of the 2D prototype cell but contain a periodic span should be accepted",
328+
),
329+
pytest.param(
330+
ase.build.surface(
331+
ase.build.bulk(
332+
"MoC", crystalstructure="rocksalt", a=4.38, b=4.38, c=4.38, alpha=90
333+
),
334+
(0, 0, 1),
335+
5,
336+
vacuum=5,
337+
tol=1e-10,
338+
periodic=True,
339+
)
340+
* [3, 3, 1],
341+
[],
342+
id="clusters that consist only of the prototype cell but don't contain a periodic span should be ignored",
343+
),
344+
],
345+
)
346+
def test_small_clusters(system, clusters_expected):
347+
"""Tests that small clusters that do not cover"""
348+
system = rattle(system, 0.1)
349+
results = SBC().get_clusters(system, pos_tol=0.01)
350+
assert_topology(results, clusters_expected)
351+
352+
305353
fcc_clusters = [Cluster(range(len(surface_fcc)), dimensionality=0)]
306354
single_atom_clusters = []
307355

tests/performance/performance.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -198,14 +198,10 @@ def plot(show):
198198
i_timemin = times.min()
199199
i_nmax = n_atoms.max()
200200
i_nmin = n_atoms.min()
201-
if i_timemax > timemax:
202-
timemax = i_timemax
203-
if i_timemin < timemin:
204-
timemin = i_timemin
205-
if i_nmax > nmax:
206-
nmax = i_nmax
207-
if i_nmin < nmin:
208-
nmin = i_nmin
201+
timemax = max(i_timemax, timemax)
202+
timemin = min(i_timemin, timemin)
203+
nmax = max(i_nmax, nmax)
204+
nmin = min(i_nmin, nmin)
209205
# ax1.fill_between(n_atoms, times_mean - times_std, times_mean + times_std, color=color, alpha=0.3)
210206
ax1.plot(
211207
n_atoms,

0 commit comments

Comments
 (0)