Skip to content

Commit 6bb2938

Browse files
jinlhr542janosh
andauthored
Fix lll_reduce for slab generation (materialsproject#3927)
* Update surface and interface generating functions 1. fixing problem for the lll_reduce process when making slabs, doing mapping before updating the structure 2. allow to set ftol of the termination distances for hierarchical cluster so that some non-identical terminations close to each other can be identified 3. allow to add index for terminations so that terminations with the same space group can be distinguished Interfaces made by identical slabs can be non-identical because the relative transformation of the misorientation and the termination variation do not ensure symmetry, especially when the film and substrate have different point groups. Therefore, the termination finding function should allow to generate all the possible terminations. This can help others to develop more robust algorithm to group the equivalent interfaces made by different terminations. --------- Signed-off-by: Jason Xie <[email protected]> Signed-off-by: Jason Xie <[email protected]> Co-authored-by: Janosh Riebesell <[email protected]>
1 parent 1ff1ba5 commit 6bb2938

File tree

5 files changed

+99
-31
lines changed

5 files changed

+99
-31
lines changed

src/pymatgen/analysis/interfaces/coherent_interfaces.py

+31-11
Original file line numberDiff line numberDiff line change
@@ -34,22 +34,32 @@ def __init__(
3434
film_miller: Tuple3Ints,
3535
substrate_miller: Tuple3Ints,
3636
zslgen: ZSLGenerator | None = None,
37+
termination_ftol: float = 0.25,
38+
label_index: bool = False, # necessary to add index to termination
39+
filter_out_sym_slabs: bool = True,
3740
):
3841
"""
3942
Args:
40-
substrate_structure: structure of substrate
41-
film_structure: structure of film
42-
film_miller: miller index of the film layer
43-
substrate_miller: miller index for the substrate layer
44-
zslgen: BiDirectionalZSL if you want custom lattice matching tolerances for coherency.
43+
substrate_structure (Structure): substrate structure
44+
film_structure (Structure): film structure
45+
film_miller (tuple[int, int, int]): miller index for the film layer
46+
substrate_miller (tuple[int, int, int]): miller index for the substrate layer
47+
zslgen (ZSLGenerator | None): BiDirectionalZSL if you want custom lattice matching tolerances for coherency.
48+
termination_ftol (float): tolerance to distinguish different terminating atomic planes.
49+
label_index (bool): If True add an extra index at the beginning of the termination label.
50+
filter_out_sym_slabs (bool): If True filter out identical slabs with different terminations.
51+
This might need to be set as False to find more non-identical terminations because slab
52+
identity separately does not mean combinational identity.
4553
"""
4654
# Bulk structures
4755
self.substrate_structure = substrate_structure
4856
self.film_structure = film_structure
4957
self.film_miller = film_miller
5058
self.substrate_miller = substrate_miller
5159
self.zslgen = zslgen or ZSLGenerator(bidirectional=True)
52-
60+
self.termination_ftol = termination_ftol
61+
self.label_index = label_index
62+
self.filter_out_sym_slabs = filter_out_sym_slabs
5363
self._find_matches()
5464
self._find_terminations()
5565

@@ -131,14 +141,24 @@ def _find_terminations(self):
131141
reorient_lattice=False, # This is necessary to not screw up the lattice
132142
)
133143

134-
film_slabs = film_sg.get_slabs()
135-
sub_slabs = sub_sg.get_slabs()
136-
144+
film_slabs = film_sg.get_slabs(ftol=self.termination_ftol, filter_out_sym_slabs=self.filter_out_sym_slabs)
145+
sub_slabs = sub_sg.get_slabs(ftol=self.termination_ftol, filter_out_sym_slabs=self.filter_out_sym_slabs)
137146
film_shifts = [slab.shift for slab in film_slabs]
138-
film_terminations = [label_termination(slab) for slab in film_slabs]
147+
148+
if self.label_index:
149+
film_terminations = [
150+
label_termination(slab, self.termination_ftol, t_idx) for t_idx, slab in enumerate(film_slabs, start=1)
151+
]
152+
else:
153+
film_terminations = [label_termination(slab, self.termination_ftol) for slab in film_slabs]
139154

140155
sub_shifts = [slab.shift for slab in sub_slabs]
141-
sub_terminations = [label_termination(slab) for slab in sub_slabs]
156+
if self.label_index:
157+
sub_terminations = [
158+
label_termination(slab, self.termination_ftol, t_idx) for t_idx, slab in enumerate(sub_slabs, start=1)
159+
]
160+
else:
161+
sub_terminations = [label_termination(slab, self.termination_ftol) for slab in sub_slabs]
142162

143163
self._terminations = {
144164
(film_label, sub_label): (film_shift, sub_shift)

src/pymatgen/core/interface.py

+14-4
Original file line numberDiff line numberDiff line change
@@ -2843,8 +2843,14 @@ def from_slabs(
28432843
return iface
28442844

28452845

2846-
def label_termination(slab: Structure) -> str:
2847-
"""Label the slab surface termination."""
2846+
def label_termination(slab: Structure, ftol: float = 0.25, t_idx: int | None = None) -> str:
2847+
"""Label the slab surface termination.
2848+
2849+
Args:
2850+
slab (Slab): film or substrate slab to label termination for
2851+
ftol (float): tolerance for terminating position hierarchical clustering
2852+
t_idx (None | int): if not None, adding an extra index to the termination label output
2853+
"""
28482854
frac_coords = slab.frac_coords
28492855
n = len(frac_coords)
28502856

@@ -2867,7 +2873,7 @@ def label_termination(slab: Structure) -> str:
28672873

28682874
condensed_m = squareform(dist_matrix)
28692875
z = linkage(condensed_m)
2870-
clusters = fcluster(z, 0.25, criterion="distance")
2876+
clusters = fcluster(z, ftol, criterion="distance")
28712877

28722878
clustered_sites: dict[int, list[Site]] = {c: [] for c in clusters}
28732879
for idx, cluster in enumerate(clusters):
@@ -2880,7 +2886,11 @@ def label_termination(slab: Structure) -> str:
28802886

28812887
sp_symbol = SpacegroupAnalyzer(top_plane, symprec=0.1).get_space_group_symbol()
28822888
form = top_plane.reduced_formula
2883-
return f"{form}_{sp_symbol}_{len(top_plane)}"
2889+
2890+
if t_idx is None:
2891+
return f"{form}_{sp_symbol}_{len(top_plane)}"
2892+
2893+
return f"{t_idx}_{form}_{sp_symbol}_{len(top_plane)}"
28842894

28852895

28862896
def count_layers(struct: Structure, el: Element | None = None) -> int:

src/pymatgen/core/surface.py

+20-15
Original file line numberDiff line numberDiff line change
@@ -1134,10 +1134,10 @@ def get_slab(
11341134
if self.lll_reduce:
11351135
# Sanitize Slab (LLL reduction + site sorting + map frac_coords)
11361136
lll_slab = struct.copy(sanitize=True)
1137-
struct = lll_slab
11381137

11391138
# Apply reduction on the scaling factor
11401139
mapping = lll_slab.lattice.find_mapping(struct.lattice)
1140+
struct = lll_slab
11411141
if mapping is None:
11421142
raise RuntimeError("LLL reduction has failed")
11431143
scale_factor = np.dot(mapping[2], scale_factor)
@@ -1194,6 +1194,7 @@ def get_slabs(
11941194
symmetrize: bool = False,
11951195
repair: bool = False,
11961196
ztol: float = 0,
1197+
filter_out_sym_slabs: bool = True,
11971198
) -> list[Slab]:
11981199
"""Generate slabs with shift values calculated from the internal
11991200
gen_possible_terminations func. If the user decide to avoid breaking
@@ -1217,6 +1218,7 @@ def get_slabs(
12171218
can lead to many more possible slabs.
12181219
ztol (float): Fractional tolerance for determine overlapping z-ranges,
12191220
smaller ztol might result in more possible Slabs.
1221+
filter_out_sym_slabs (bool): If True filter out identical slabs with different terminations.
12201222
12211223
Returns:
12221224
list[Slab]: All possible Slabs of a particular surface,
@@ -1342,22 +1344,25 @@ def get_z_ranges(
13421344
slabs.append(self.repair_broken_bonds(slab=slab, bonds=bonds))
13431345

13441346
# Filter out surfaces that might be the same
1345-
matcher = StructureMatcher(ltol=tol, stol=tol, primitive_cell=False, scale=False)
1347+
if filter_out_sym_slabs:
1348+
matcher = StructureMatcher(ltol=tol, stol=tol, primitive_cell=False, scale=False)
1349+
1350+
final_slabs: list[Slab] = []
1351+
for group in matcher.group_structures(slabs):
1352+
# For each unique slab, symmetrize the
1353+
# surfaces by removing sites from the bottom
1354+
if symmetrize:
1355+
sym_slabs = self.nonstoichiometric_symmetrized_slab(group[0])
1356+
final_slabs.extend(sym_slabs)
1357+
else:
1358+
final_slabs.append(group[0])
13461359

1347-
final_slabs: list[Slab] = []
1348-
for group in matcher.group_structures(slabs):
1349-
# For each unique slab, symmetrize the
1350-
# surfaces by removing sites from the bottom
1360+
# Filter out similar surfaces generated by symmetrization
13511361
if symmetrize:
1352-
sym_slabs = self.nonstoichiometric_symmetrized_slab(group[0])
1353-
final_slabs.extend(sym_slabs)
1354-
else:
1355-
final_slabs.append(group[0])
1356-
1357-
# Filter out similar surfaces generated by symmetrization
1358-
if symmetrize:
1359-
matcher_sym = StructureMatcher(ltol=tol, stol=tol, primitive_cell=False, scale=False)
1360-
final_slabs = [group[0] for group in matcher_sym.group_structures(final_slabs)]
1362+
matcher_sym = StructureMatcher(ltol=tol, stol=tol, primitive_cell=False, scale=False)
1363+
final_slabs = [group[0] for group in matcher_sym.group_structures(final_slabs)]
1364+
else:
1365+
final_slabs = slabs
13611366

13621367
return cast(list[Slab], sorted(final_slabs, key=lambda slab: slab.energy))
13631368

tests/analysis/interfaces/test_coherent_interface.py

+33
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
import unittest
4+
35
from numpy.testing import assert_allclose
46

57
from pymatgen.analysis.interfaces.coherent_interfaces import (
@@ -8,6 +10,9 @@
810
get_2d_transform,
911
get_rot_3d_for_2d,
1012
)
13+
from pymatgen.analysis.interfaces.substrate_analyzer import SubstrateAnalyzer
14+
from pymatgen.core.lattice import Lattice
15+
from pymatgen.core.structure import Structure
1116
from pymatgen.symmetry.analyzer import SpacegroupAnalyzer
1217
from pymatgen.util.testing import PymatgenTest
1318

@@ -44,3 +49,31 @@ def test_coherent_interface_builder(self):
4449
# SP: this test is super fragile and the result fluctuates between 6, 30 and 42 for
4550
# no apparent reason. The author should fix this.
4651
assert len(list(builder.get_interfaces(termination=("O2_Pmmm_1", "Si_R-3m_1")))) >= 6
52+
53+
54+
class TestCoherentInterfaceBuilder(unittest.TestCase):
55+
def setUp(self):
56+
# build substrate & film structure
57+
basis = [[0, 0, 0], [0.25, 0.25, 0.25]]
58+
self.substrate = Structure(Lattice.cubic(a=5.431), ["Si", "Si"], basis)
59+
self.film = Structure(Lattice.cubic(a=5.658), ["Ge", "Ge"], basis)
60+
61+
def test_termination_searching(self):
62+
sub_analyzer = SubstrateAnalyzer()
63+
matches = list(sub_analyzer.calculate(substrate=self.substrate, film=self.film))
64+
cib = CoherentInterfaceBuilder(
65+
film_structure=self.film,
66+
substrate_structure=self.substrate,
67+
film_miller=matches[0].film_miller,
68+
substrate_miller=matches[0].substrate_miller,
69+
zslgen=sub_analyzer,
70+
termination_ftol=1e-4,
71+
label_index=True,
72+
filter_out_sym_slabs=False,
73+
)
74+
assert cib.terminations == [
75+
("1_Ge_P4/mmm_1", "1_Si_P4/mmm_1"),
76+
("1_Ge_P4/mmm_1", "2_Si_P4/mmm_1"),
77+
("2_Ge_P4/mmm_1", "1_Si_P4/mmm_1"),
78+
("2_Ge_P4/mmm_1", "2_Si_P4/mmm_1"),
79+
], "termination results wrong"

tests/core/test_surface.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -686,7 +686,7 @@ def test_previous_reconstructions(self):
686686
assert any(len(match.group_structures([struct, slab])) == 1 for slab in slabs)
687687

688688

689-
class MillerIndexFinderTests(PymatgenTest):
689+
class TestMillerIndexFinder(PymatgenTest):
690690
def setUp(self):
691691
self.cscl = Structure.from_spacegroup("Pm-3m", Lattice.cubic(4.2), ["Cs", "Cl"], [[0, 0, 0], [0.5, 0.5, 0.5]])
692692
self.Fe = Structure.from_spacegroup("Im-3m", Lattice.cubic(2.82), ["Fe"], [[0, 0, 0]])

0 commit comments

Comments
 (0)