Coverage for /builds/ase/ase/ase/build/surfaces_with_termination.py: 98.73%
79 statements
« prev ^ index » next coverage.py v7.5.3, created at 2025-08-02 00:12 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2025-08-02 00:12 +0000
1# fmt: off
3import numpy as np
5from ase.build.general_surface import surface
6from ase.geometry import get_layers
7from ase.symbols import string2symbols
10def surfaces_with_termination(lattice, indices, layers, vacuum=None, tol=1e-10,
11 termination=None, return_all=False,
12 verbose=False):
13 """Create surface from a given lattice and Miller indices with a given
14 termination
16 Parameters
17 ==========
18 lattice: Atoms object or str
19 Bulk lattice structure of alloy or pure metal. Note that the
20 unit-cell must be the conventional cell - not the primitive cell.
21 One can also give the chemical symbol as a string, in which case the
22 correct bulk lattice will be generated automatically.
23 indices: sequence of three int
24 Surface normal in Miller indices (h,k,l).
25 layers: int
26 Number of equivalent layers of the slab. (not the same as the layers
27 you choose from for terminations)
28 vacuum: float
29 Amount of vacuum added on both sides of the slab.
30 termination: str
31 the atoms you wish to be in the top layer. There may be many such
32 terminations, this function returns all terminations with the same
33 atomic composition.
34 e.g. 'O' will return oxygen terminated surfaces.
35 e.g.'TiO' returns surfaces terminated with layers containing both
36 O and Ti
37 Returns:
38 return_surfs: List
39 a list of surfaces that match the specifications given
41 """
42 lats = translate_lattice(lattice, indices)
43 return_surfs = []
44 check = []
45 check2 = []
46 for item in lats:
47 too_similar = False
48 surf = surface(item, indices, layers, vacuum=vacuum, tol=tol)
49 surf.wrap(pbc=[True] * 3) # standardize slabs
51 positions = surf.get_scaled_positions().flatten()
52 for i, value in enumerate(positions):
53 if value >= 1 - tol: # move things closer to zero within tol
54 positions[i] -= 1
55 surf.set_scaled_positions(np.reshape(positions, (len(surf), 3)))
56 # rep = find_z_layers(surf)
57 z_layers, _hs = get_layers(surf, (0, 0, 1)) # just z layers matter
58 # get the indicies of the atoms in the highest layer
59 top_layer = [
60 i for i, val in enumerate(
61 z_layers == max(z_layers)) if val]
63 if termination is not None:
64 comp = [surf.get_chemical_symbols()[a] for a in top_layer]
65 term = string2symbols(termination)
66 # list atoms in top layer and not in requested termination
67 check = [a for a in comp if a not in term]
68 # list of atoms in requested termination and not in top layer
69 check2 = [a for a in term if a not in comp]
70 if len(return_surfs) > 0:
71 pos_diff = [a.get_positions() - surf.get_positions()
72 for a in return_surfs]
73 for i, su in enumerate(pos_diff):
74 similarity_test = su.flatten() < tol * 1000
75 if similarity_test.all():
76 # checks if surface is too similar to another surface
77 too_similar = True
78 if too_similar:
79 continue
80 if return_all is True:
81 pass
82 elif check != [] or check2 != []:
83 continue
84 return_surfs.append(surf)
85 return return_surfs
88def translate_lattice(lattice, indices, tol=10**-3):
89 """translates a bulk unit cell along a normal vector given by the a set of
90 miller indices to the next symetric position. This is used to control the
91 termination of the surface in the smart_surface command
92 Parameters:
93 ==========
94 lattice: Atoms object
95 atoms object of the bulk unit cell
96 indices: 1x3 list,tuple, or numpy array
97 the miller indices you wish to cut along.
98 returns:
99 lattice_list: list of Atoms objects
100 a list of all the different translations of the unit cell that will
101 yield different terminations of a surface cut along the miller
102 indices provided.
103 """
104 lattice_list = []
105 cell = lattice.get_cell()
106 pt = [0, 0, 0]
107 h, k, l = indices # noqa (E741 ambiguous name 'l')
108 millers = list(indices)
109 for index, item in enumerate(millers):
110 if item == 0:
111 millers[index] = 10**9 # make zeros large numbers
112 elif pt == [0, 0, 0]: # for numerical stability
113 pt = list(cell[index] / float(item) / np.linalg.norm(cell[index]))
114 h1, k1, l1 = millers
115 N = np.array(cell[0] / h1 + cell[1] / k1 + cell[2] / l1)
116 n = N / np.linalg.norm(N) # making a unit vector normal to cut plane
117 # finding distance from cut plan vector
118 d = [np.round(np.dot(n, (a - pt)) * n, 5) for
119 a in lattice.get_scaled_positions()]
120 duplicates = []
121 for i, item in enumerate(d):
122 g = [True for a in d[i + 1:] if np.linalg.norm(a - item) < tol]
123 if g != []:
124 duplicates.append(i)
125 duplicates.reverse()
126 for i in duplicates:
127 del d[i]
128 # put distance to the plane at the end of the array
129 for i, item in enumerate(d):
130 d[i] = np.append(item,
131 np.dot(n, (lattice.get_scaled_positions()[i] - pt)))
132 d = np.array(d)
133 d = d[d[:, 3].argsort()] # sort by distance to the plane
134 d = [a[:3] for a in d] # remove distance
135 d = list(d) # make it a list again
136 for i in d:
137 """
138 The above method gives you the boundries of between terminations that
139 will allow you to build a complete set of terminations. However, it
140 does not return all the boundries. Thus you must check both above and
141 below the boundary, and not stray too far from the boundary. If you move
142 too far away, you risk hitting another boundary you did not find.
143 """
144 lattice1 = lattice.copy()
145 displacement = (h * cell[0] + k * cell[1] + l * cell[2]) \
146 * (i + 10 ** -8)
147 lattice1.positions -= displacement
148 lattice_list.append(lattice1)
149 lattice1 = lattice.copy()
150 displacement = (h * cell[0] + k * cell[1] + l * cell[2]) \
151 * (i - 10 ** -8)
152 lattice1.positions -= displacement
153 lattice_list.append(lattice1)
154 return lattice_list