Coverage for /builds/ase/ase/ase/cluster/factory.py: 80.13%
151 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
3from typing import List, Optional
5import numpy as np
7from ase.cluster.base import ClusterBase
8from ase.cluster.cluster import Cluster
9from ase.data import atomic_numbers as ref_atomic_numbers
10from ase.spacegroup import Spacegroup
13class ClusterFactory(ClusterBase):
14 directions = [[1, 0, 0],
15 [0, 1, 0],
16 [0, 0, 1]]
18 atomic_basis = np.array([[0., 0., 0.]])
20 element_basis: Optional[List[int]] = None
22 # Make it possible to change the class of the object returned.
23 Cluster = Cluster
25 def __call__(self, symbols, surfaces, layers, latticeconstant=None,
26 center=None, vacuum=0.0, debug=0):
27 self.debug = debug
29 # Interpret symbol
30 self.set_atomic_numbers(symbols)
32 # Interpret lattice constant
33 if latticeconstant is None:
34 if self.element_basis is None:
35 self.lattice_constant = self.get_lattice_constant()
36 else:
37 raise ValueError(
38 "A lattice constant must be specified for a compound")
39 else:
40 self.lattice_constant = latticeconstant
42 self.set_basis()
44 if self.debug:
45 print("Lattice constant(s):", self.lattice_constant)
46 print("Lattice basis:\n", self.lattice_basis)
47 print("Resiprocal basis:\n", self.resiproc_basis)
48 print("Atomic basis:\n", self.atomic_basis)
50 self.set_surfaces_layers(surfaces, layers)
51 self.set_lattice_size(center)
53 if self.debug:
54 print("Center position:", self.center.round(2))
55 print("Base lattice size:", self.size)
57 cluster = self.make_cluster(vacuum)
58 cluster.symmetry = self.xtal_name
59 cluster.surfaces = self.surfaces.copy()
60 cluster.lattice_basis = self.lattice_basis.copy()
61 cluster.atomic_basis = self.atomic_basis.copy()
62 cluster.resiproc_basis = self.resiproc_basis.copy()
63 return cluster
65 def make_cluster(self, vacuum):
66 # Make the base crystal by repeating the unit cell
67 size = np.array(self.size)
68 translations = np.zeros((size.prod(), 3))
69 for h in range(size[0]):
70 for k in range(size[1]):
71 for l_ in range(size[2]):
72 i = h * (size[1] * size[2]) + k * size[2] + l_
73 translations[i] = np.dot([h, k, l_], self.lattice_basis)
75 atomic_basis = np.dot(self.atomic_basis, self.lattice_basis)
76 positions = np.zeros((len(translations) * len(atomic_basis), 3))
77 numbers = np.zeros(len(positions))
78 n = len(atomic_basis)
79 for i, trans in enumerate(translations):
80 positions[n * i:n * (i + 1)] = atomic_basis + trans
81 numbers[n * i:n * (i + 1)] = self.atomic_numbers
83 # Remove all atoms that is outside the defined surfaces
84 for s, l in zip(self.surfaces, self.layers):
85 n = self.miller_to_direction(s)
86 rmax = self.get_layer_distance(s, l + 0.1)
88 r = np.dot(positions - self.center, n)
89 mask = np.less(r, rmax)
91 if self.debug > 1:
92 print("Cutting %s at %i layers ~ %.3f A" % (s, l, rmax))
94 positions = positions[mask]
95 numbers = numbers[mask]
97 atoms = self.Cluster(symbols=numbers, positions=positions)
99 atoms.cell = (1, 1, 1) # XXX ugly hack to center around zero
100 atoms.center(about=(0, 0, 0))
101 atoms.cell[:] = 0
102 return atoms
104 def set_atomic_numbers(self, symbols):
105 "Extract atomic number from element"
106 # The types that can be elements: integers and strings
107 atomic_numbers = []
108 if self.element_basis is None:
109 if isinstance(symbols, str):
110 atomic_numbers.append(ref_atomic_numbers[symbols])
111 elif isinstance(symbols, int):
112 atomic_numbers.append(symbols)
113 else:
114 raise TypeError("The symbol argument must be a " +
115 "string or an atomic number.")
116 element_basis = [0] * len(self.atomic_basis)
117 else:
118 if isinstance(symbols, (list, tuple)):
119 nsymbols = len(symbols)
120 else:
121 nsymbols = 0
123 nelement_basis = max(self.element_basis) + 1
124 if nsymbols != nelement_basis:
125 raise TypeError("The symbol argument must be a sequence " +
126 "of length %d" % (nelement_basis,) +
127 " (one for each kind of lattice position")
129 for s in symbols:
130 if isinstance(s, str):
131 atomic_numbers.append(ref_atomic_numbers[s])
132 elif isinstance(s, int):
133 atomic_numbers.append(s)
134 else:
135 raise TypeError("The symbol argument must be a " +
136 "string or an atomic number.")
137 element_basis = self.element_basis
139 self.atomic_numbers = [atomic_numbers[n] for n in element_basis]
140 assert len(self.atomic_numbers) == len(self.atomic_basis)
142 def set_lattice_size(self, center):
143 if center is None:
144 offset = np.zeros(3)
145 else:
146 offset = np.array(center)
147 if (offset > 1.0).any() or (offset < 0.0).any():
148 raise ValueError(
149 "Center offset must lie within the lattice unit cell.")
151 max = np.ones(3)
152 min = -np.ones(3)
153 v = np.linalg.inv(self.lattice_basis.T)
154 for s, l in zip(self.surfaces, self.layers):
155 n = self.miller_to_direction(s) * self.get_layer_distance(s, l)
156 k = np.round(np.dot(v, n), 2)
157 for i in range(3):
158 if k[i] > 0.0:
159 k[i] = np.ceil(k[i])
160 elif k[i] < 0.0:
161 k[i] = np.floor(k[i])
163 if self.debug > 1:
164 print(
165 "Spaning %i layers in %s in lattice basis ~ %s" %
166 (l, s, k))
168 max[k > max] = k[k > max]
169 min[k < min] = k[k < min]
171 self.center = np.dot(offset - min, self.lattice_basis)
172 self.size = (max - min + np.ones(3)).astype(int)
174 def set_surfaces_layers(self, surfaces, layers):
175 if len(surfaces) != len(layers):
176 raise ValueError(
177 "Improper size of surface and layer arrays: %i != %i"
178 % (len(surfaces), len(layers)))
180 sg = Spacegroup(self.spacegroup)
181 surfaces = np.array(surfaces)
182 layers = np.array(layers)
184 for i, s in enumerate(surfaces):
185 s = reduce_miller(s)
186 surfaces[i] = s
188 surfaces_full = surfaces.copy()
189 layers_full = layers.copy()
191 for s, l in zip(surfaces, layers):
192 equivalent_surfaces = sg.equivalent_reflections(s.reshape(-1, 3))
194 for es in equivalent_surfaces:
195 # If the equivalent surface (es) is not in the surface list,
196 # then append it.
197 if not np.equal(es, surfaces_full).all(axis=1).any():
198 surfaces_full = np.append(
199 surfaces_full, es.reshape(1, 3), axis=0)
200 layers_full = np.append(layers_full, l)
202 self.surfaces = surfaces_full.copy()
203 self.layers = layers_full.copy()
205 def get_resiproc_basis(self, basis):
206 """Returns the resiprocal basis to a given lattice (crystal) basis"""
207 k = 1 / np.dot(basis[0], cross(basis[1], basis[2]))
209 # The same as the inversed basis matrix transposed
210 return k * np.array([cross(basis[1], basis[2]),
211 cross(basis[2], basis[0]),
212 cross(basis[0], basis[1])])
214# Helping functions
217def cross(a, b):
218 """The cross product of two vectors."""
219 return np.array([a[1] * b[2] - b[1] * a[2],
220 a[2] * b[0] - b[2] * a[0],
221 a[0] * b[1] - b[0] * a[1]])
224def GCD(a, b):
225 """Greatest Common Divisor of a and b."""
226 # print "--"
227 while a != 0:
228 # print a,b,">",
229 a, b = b % a, a
230 # print a,b
231 return b
234def reduce_miller(hkl):
235 """Reduce Miller index to the lowest equivalent integers."""
236 hkl = np.array(hkl)
237 old = hkl.copy()
239 d = GCD(GCD(hkl[0], hkl[1]), hkl[2])
240 while d != 1:
241 hkl = hkl // d
242 d = GCD(GCD(hkl[0], hkl[1]), hkl[2])
244 if np.dot(old, hkl) > 0:
245 return hkl
246 else:
247 return -hkl