Coverage for ase / cluster / factory.py: 80.00%
150 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-30 08:22 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-30 08:22 +0000
1# fmt: off
4import numpy as np
6from ase.cluster.base import ClusterBase
7from ase.cluster.cluster import Cluster
8from ase.data import atomic_numbers as ref_atomic_numbers
9from ase.spacegroup import Spacegroup
12class ClusterFactory(ClusterBase):
13 directions = [[1, 0, 0],
14 [0, 1, 0],
15 [0, 0, 1]]
17 atomic_basis = np.array([[0., 0., 0.]])
19 element_basis: list[int] | None = None
21 # Make it possible to change the class of the object returned.
22 Cluster = Cluster
24 def __call__(self, symbols, surfaces, layers, latticeconstant=None,
25 center=None, vacuum=0.0, debug=0):
26 self.debug = debug
28 # Interpret symbol
29 self.set_atomic_numbers(symbols)
31 # Interpret lattice constant
32 if latticeconstant is None:
33 if self.element_basis is None:
34 self.lattice_constant = self.get_lattice_constant()
35 else:
36 raise ValueError(
37 "A lattice constant must be specified for a compound")
38 else:
39 self.lattice_constant = latticeconstant
41 self.set_basis()
43 if self.debug:
44 print("Lattice constant(s):", self.lattice_constant)
45 print("Lattice basis:\n", self.lattice_basis)
46 print("Resiprocal basis:\n", self.resiproc_basis)
47 print("Atomic basis:\n", self.atomic_basis)
49 self.set_surfaces_layers(surfaces, layers)
50 self.set_lattice_size(center)
52 if self.debug:
53 print("Center position:", self.center.round(2))
54 print("Base lattice size:", self.size)
56 cluster = self.make_cluster(vacuum)
57 cluster.symmetry = self.xtal_name
58 cluster.surfaces = self.surfaces.copy()
59 cluster.lattice_basis = self.lattice_basis.copy()
60 cluster.atomic_basis = self.atomic_basis.copy()
61 cluster.resiproc_basis = self.resiproc_basis.copy()
62 return cluster
64 def make_cluster(self, vacuum):
65 # Make the base crystal by repeating the unit cell
66 size = np.array(self.size)
67 translations = np.zeros((size.prod(), 3))
68 for h in range(size[0]):
69 for k in range(size[1]):
70 for l_ in range(size[2]):
71 i = h * (size[1] * size[2]) + k * size[2] + l_
72 translations[i] = np.dot([h, k, l_], self.lattice_basis)
74 atomic_basis = np.dot(self.atomic_basis, self.lattice_basis)
75 positions = np.zeros((len(translations) * len(atomic_basis), 3))
76 numbers = np.zeros(len(positions))
77 n = len(atomic_basis)
78 for i, trans in enumerate(translations):
79 positions[n * i:n * (i + 1)] = atomic_basis + trans
80 numbers[n * i:n * (i + 1)] = self.atomic_numbers
82 # Remove all atoms that is outside the defined surfaces
83 for s, l in zip(self.surfaces, self.layers):
84 n = self.miller_to_direction(s)
85 rmax = self.get_layer_distance(s, l + 0.1)
87 r = np.dot(positions - self.center, n)
88 mask = np.less(r, rmax)
90 if self.debug > 1:
91 print("Cutting %s at %i layers ~ %.3f A" % (s, l, rmax))
93 positions = positions[mask]
94 numbers = numbers[mask]
96 atoms = self.Cluster(symbols=numbers, positions=positions)
98 atoms.cell = (1, 1, 1) # XXX ugly hack to center around zero
99 atoms.center(about=(0, 0, 0))
100 atoms.cell[:] = 0
101 return atoms
103 def set_atomic_numbers(self, symbols):
104 "Extract atomic number from element"
105 # The types that can be elements: integers and strings
106 atomic_numbers = []
107 if self.element_basis is None:
108 if isinstance(symbols, str):
109 atomic_numbers.append(ref_atomic_numbers[symbols])
110 elif isinstance(symbols, int):
111 atomic_numbers.append(symbols)
112 else:
113 raise TypeError("The symbol argument must be a " +
114 "string or an atomic number.")
115 element_basis = [0] * len(self.atomic_basis)
116 else:
117 if isinstance(symbols, (list, tuple)):
118 nsymbols = len(symbols)
119 else:
120 nsymbols = 0
122 nelement_basis = max(self.element_basis) + 1
123 if nsymbols != nelement_basis:
124 raise TypeError("The symbol argument must be a sequence " +
125 "of length %d" % (nelement_basis,) +
126 " (one for each kind of lattice position")
128 for s in symbols:
129 if isinstance(s, str):
130 atomic_numbers.append(ref_atomic_numbers[s])
131 elif isinstance(s, int):
132 atomic_numbers.append(s)
133 else:
134 raise TypeError("The symbol argument must be a " +
135 "string or an atomic number.")
136 element_basis = self.element_basis
138 self.atomic_numbers = [atomic_numbers[n] for n in element_basis]
139 assert len(self.atomic_numbers) == len(self.atomic_basis)
141 def set_lattice_size(self, center):
142 if center is None:
143 offset = np.zeros(3)
144 else:
145 offset = np.array(center)
146 if (offset > 1.0).any() or (offset < 0.0).any():
147 raise ValueError(
148 "Center offset must lie within the lattice unit cell.")
150 max = np.ones(3)
151 min = -np.ones(3)
152 v = np.linalg.inv(self.lattice_basis.T)
153 for s, l in zip(self.surfaces, self.layers):
154 n = self.miller_to_direction(s) * self.get_layer_distance(s, l)
155 k = np.round(np.dot(v, n), 2)
156 for i in range(3):
157 if k[i] > 0.0:
158 k[i] = np.ceil(k[i])
159 elif k[i] < 0.0:
160 k[i] = np.floor(k[i])
162 if self.debug > 1:
163 print(
164 "Spaning %i layers in %s in lattice basis ~ %s" %
165 (l, s, k))
167 max[k > max] = k[k > max]
168 min[k < min] = k[k < min]
170 self.center = np.dot(offset - min, self.lattice_basis)
171 self.size = (max - min + np.ones(3)).astype(int)
173 def set_surfaces_layers(self, surfaces, layers):
174 if len(surfaces) != len(layers):
175 raise ValueError(
176 "Improper size of surface and layer arrays: %i != %i"
177 % (len(surfaces), len(layers)))
179 sg = Spacegroup(self.spacegroup)
180 surfaces = np.array(surfaces)
181 layers = np.array(layers)
183 for i, s in enumerate(surfaces):
184 s = reduce_miller(s)
185 surfaces[i] = s
187 surfaces_full = surfaces.copy()
188 layers_full = layers.copy()
190 for s, l in zip(surfaces, layers):
191 equivalent_surfaces = sg.equivalent_reflections(s.reshape(-1, 3))
193 for es in equivalent_surfaces:
194 # If the equivalent surface (es) is not in the surface list,
195 # then append it.
196 if not np.equal(es, surfaces_full).all(axis=1).any():
197 surfaces_full = np.append(
198 surfaces_full, es.reshape(1, 3), axis=0)
199 layers_full = np.append(layers_full, l)
201 self.surfaces = surfaces_full.copy()
202 self.layers = layers_full.copy()
204 def get_resiproc_basis(self, basis):
205 """Returns the resiprocal basis to a given lattice (crystal) basis"""
206 k = 1 / np.dot(basis[0], cross(basis[1], basis[2]))
208 # The same as the inversed basis matrix transposed
209 return k * np.array([cross(basis[1], basis[2]),
210 cross(basis[2], basis[0]),
211 cross(basis[0], basis[1])])
213# Helping functions
216def cross(a, b):
217 """The cross product of two vectors."""
218 return np.array([a[1] * b[2] - b[1] * a[2],
219 a[2] * b[0] - b[2] * a[0],
220 a[0] * b[1] - b[0] * a[1]])
223def GCD(a, b):
224 """Greatest Common Divisor of a and b."""
225 # print "--"
226 while a != 0:
227 # print a,b,">",
228 a, b = b % a, a
229 # print a,b
230 return b
233def reduce_miller(hkl):
234 """Reduce Miller index to the lowest equivalent integers."""
235 hkl = np.array(hkl)
236 old = hkl.copy()
238 d = GCD(GCD(hkl[0], hkl[1]), hkl[2])
239 while d != 1:
240 hkl = hkl // d
241 d = GCD(GCD(hkl[0], hkl[1]), hkl[2])
243 if np.dot(old, hkl) > 0:
244 return hkl
245 else:
246 return -hkl