root / ase / lattice / bravais.py @ 14
Historique | Voir | Annoter | Télécharger (18,3 ko)
| 1 |
"""Bravais.py - class for generating Bravais lattices etc.
|
|---|---|
| 2 |
|
| 3 |
This is a base class for numerous classes setting up pieces of crystal.
|
| 4 |
"""
|
| 5 |
|
| 6 |
import math |
| 7 |
import numpy as np |
| 8 |
from ase.atoms import Atoms |
| 9 |
import ase.data |
| 10 |
|
| 11 |
class Bravais: |
| 12 |
"""Bravais lattice factory.
|
| 13 |
|
| 14 |
This is a base class for the objects producing various lattices
|
| 15 |
(SC, FCC, ...).
|
| 16 |
"""
|
| 17 |
|
| 18 |
# The following methods are NOT defined here, but must be defined
|
| 19 |
# in classes inhering from Bravais:
|
| 20 |
# get_lattice_constant
|
| 21 |
# make_crystal_basis
|
| 22 |
# The following class attributes are NOT defined here, but must be defined
|
| 23 |
# in classes inhering from Bravais:
|
| 24 |
# int_basis
|
| 25 |
# inverse_basis
|
| 26 |
|
| 27 |
other = {0:(1,2), 1:(2,0), 2:(0,1)}
|
| 28 |
|
| 29 |
# For Bravais lattices with a basis, set the basis here. Leave as
|
| 30 |
# None if no basis is present.
|
| 31 |
bravais_basis = None
|
| 32 |
|
| 33 |
# If more than one type of element appear in the crystal, give the
|
| 34 |
# order here. For example, if two elements appear in a 3:1 ratio,
|
| 35 |
# bravais_basis could contain four vectors, and element_basis
|
| 36 |
# could be (0,0,1,0) - the third atom in the basis is different
|
| 37 |
# from the other three. Leave as None if all atoms are of the
|
| 38 |
# same type.
|
| 39 |
element_basis = None
|
| 40 |
|
| 41 |
# How small numbers should be considered zero in the unit cell?
|
| 42 |
chop_tolerance = 1e-10
|
| 43 |
|
| 44 |
def __call__(self, symbol, |
| 45 |
directions=(None,None,None), miller=(None,None,None), |
| 46 |
size=(1,1,1), latticeconstant=None, |
| 47 |
pbc=True, align=True, debug=0): |
| 48 |
"Create a lattice."
|
| 49 |
self.size = size
|
| 50 |
self.pbc = pbc
|
| 51 |
self.debug = debug
|
| 52 |
self.process_element(symbol)
|
| 53 |
self.find_directions(directions, miller)
|
| 54 |
if self.debug: |
| 55 |
self.print_directions_and_miller()
|
| 56 |
self.convert_to_natural_basis()
|
| 57 |
if self.debug >= 2: |
| 58 |
self.print_directions_and_miller(" (natural basis)") |
| 59 |
if latticeconstant is None: |
| 60 |
if self.element_basis is None: |
| 61 |
self.latticeconstant = self.get_lattice_constant() |
| 62 |
else:
|
| 63 |
raise ValueError,\ |
| 64 |
"A lattice constant must be specified for a compound"
|
| 65 |
else:
|
| 66 |
self.latticeconstant = latticeconstant
|
| 67 |
if self.debug: |
| 68 |
print "Expected number of atoms in unit cell:", self.calc_num_atoms() |
| 69 |
if self.debug >= 2: |
| 70 |
print "Bravais lattice basis:", self.bravais_basis |
| 71 |
if self.bravais_basis is not None: |
| 72 |
print " ... in natural basis:", self.natural_bravais_basis |
| 73 |
self.make_crystal_basis()
|
| 74 |
self.make_unit_cell()
|
| 75 |
if align:
|
| 76 |
self.align()
|
| 77 |
return self.make_list_of_atoms() |
| 78 |
|
| 79 |
def align(self): |
| 80 |
"Align the first axis along x-axis and the second in the x-y plane."
|
| 81 |
degree = 180/np.pi
|
| 82 |
if self.debug >= 2: |
| 83 |
print "Basis before alignment:" |
| 84 |
print self.basis |
| 85 |
if self.basis[0][0]**2 + self.basis[0][2]**2 < 0.01 * self.basis[0][1]**2: |
| 86 |
# First basis vector along y axis - rotate 90 deg along z
|
| 87 |
t = np.array([[0, -1, 0], |
| 88 |
[1, 0, 0], |
| 89 |
[0, 0, 1]], np.float) |
| 90 |
self.basis = np.dot(self.basis, t) |
| 91 |
transf = t |
| 92 |
if self.debug >= 2: |
| 93 |
print "Rotating -90 degrees around z axis for numerical stability." |
| 94 |
print self.basis |
| 95 |
else:
|
| 96 |
transf = np.identity(3, np.float)
|
| 97 |
assert abs(np.linalg.det(transf) - 1) < 1e-6 |
| 98 |
# Rotate first basis vector into xy plane
|
| 99 |
theta = math.atan2(self.basis[0,2], self.basis[0,0]) |
| 100 |
t = np.array([[np.cos(theta), 0, -np.sin(theta)],
|
| 101 |
[ 0, 1, 0 ], |
| 102 |
[np.sin(theta), 0, np.cos(theta) ]])
|
| 103 |
self.basis = np.dot(self.basis, t) |
| 104 |
transf = np.dot(transf, t) |
| 105 |
if self.debug >= 2: |
| 106 |
print "Rotating %f degrees around y axis." % (-theta*degree,) |
| 107 |
print self.basis |
| 108 |
assert abs(np.linalg.det(transf) - 1) < 1e-6 |
| 109 |
# Rotate first basis vector to point along x axis
|
| 110 |
theta = math.atan2(self.basis[0,1], self.basis[0,0]) |
| 111 |
t = np.array([[np.cos(theta), -np.sin(theta), 0],
|
| 112 |
[np.sin(theta), np.cos(theta), 0],
|
| 113 |
[ 0, 0, 1]]) |
| 114 |
self.basis = np.dot(self.basis, t) |
| 115 |
transf = np.dot(transf, t) |
| 116 |
if self.debug >= 2: |
| 117 |
print "Rotating %f degrees around z axis." % (-theta*degree,) |
| 118 |
print self.basis |
| 119 |
assert abs(np.linalg.det(transf) - 1) < 1e-6 |
| 120 |
# Rotate second basis vector into xy plane
|
| 121 |
theta = math.atan2(self.basis[1,2], self.basis[1,1]) |
| 122 |
t = np.array([[1, 0, 0], |
| 123 |
[0, np.cos(theta), -np.sin(theta)],
|
| 124 |
[0, np.sin(theta), np.cos(theta)]])
|
| 125 |
self.basis = np.dot(self.basis, t) |
| 126 |
transf = np.dot(transf, t) |
| 127 |
if self.debug >= 2: |
| 128 |
print "Rotating %f degrees around x axis." % (-theta*degree,) |
| 129 |
print self.basis |
| 130 |
assert abs(np.linalg.det(transf) - 1) < 1e-6 |
| 131 |
# Now we better rotate the atoms as well
|
| 132 |
self.atoms = np.dot(self.atoms, transf) |
| 133 |
# ... and rotate miller_basis
|
| 134 |
self.miller_basis = np.dot(self.miller_basis, transf) |
| 135 |
|
| 136 |
def make_list_of_atoms(self): |
| 137 |
"Repeat the unit cell."
|
| 138 |
nrep = self.size[0] * self.size[1] * self.size[2] |
| 139 |
if nrep <= 0: |
| 140 |
raise ValueError, "Cannot create a non-positive number of unit cells" |
| 141 |
# Now the unit cells must be merged.
|
| 142 |
a2 = [] |
| 143 |
e2 = [] |
| 144 |
for i in xrange(self.size[0]): |
| 145 |
offset = self.basis[0] * i |
| 146 |
a2.append(self.atoms + offset[np.newaxis,:])
|
| 147 |
e2.append(self.elements)
|
| 148 |
atoms = np.concatenate(a2) |
| 149 |
elements = np.concatenate(e2) |
| 150 |
a2 = [] |
| 151 |
e2 = [] |
| 152 |
for j in xrange(self.size[1]): |
| 153 |
offset = self.basis[1] * j |
| 154 |
a2.append(atoms + offset[np.newaxis,:]) |
| 155 |
e2.append(elements) |
| 156 |
atoms = np.concatenate(a2) |
| 157 |
elements = np.concatenate(e2) |
| 158 |
a2 = [] |
| 159 |
e2 = [] |
| 160 |
for k in xrange(self.size[2]): |
| 161 |
offset = self.basis[2] * k |
| 162 |
a2.append(atoms + offset[np.newaxis,:]) |
| 163 |
e2.append(elements) |
| 164 |
atoms = np.concatenate(a2) |
| 165 |
elements = np.concatenate(e2) |
| 166 |
del a2, e2
|
| 167 |
assert len(atoms) == nrep * len(self.atoms) |
| 168 |
basis = np.array([[self.size[0],0,0], |
| 169 |
[0,self.size[1],0], |
| 170 |
[0,0,self.size[2]]]) |
| 171 |
basis = np.dot(basis, self.basis)
|
| 172 |
|
| 173 |
# Tiny elements should be replaced by zero. The cutoff is
|
| 174 |
# determined by chop_tolerance which is a class attribute.
|
| 175 |
basis = np.where(np.abs(basis) < self.chop_tolerance,
|
| 176 |
0.0, basis)
|
| 177 |
|
| 178 |
# None should be replaced, and memory should be freed.
|
| 179 |
lattice = Lattice(positions=atoms, cell=basis, numbers=elements, |
| 180 |
pbc=self.pbc)
|
| 181 |
lattice.millerbasis = self.miller_basis
|
| 182 |
# Add info for lattice.surface.AddAdsorbate
|
| 183 |
lattice._addsorbate_info_size = np.array(self.size[:2]) |
| 184 |
return lattice
|
| 185 |
|
| 186 |
def process_element(self, element): |
| 187 |
"Extract atomic number from element"
|
| 188 |
# The types that can be elements: integers and strings
|
| 189 |
if self.element_basis is None: |
| 190 |
if isinstance(element, type("string")): |
| 191 |
self.atomicnumber = ase.data.atomic_numbers[element]
|
| 192 |
elif isinstance(element, int): |
| 193 |
self.atomicnumber = element
|
| 194 |
else:
|
| 195 |
raise TypeError("The symbol argument must be a string or an atomic number.") |
| 196 |
else:
|
| 197 |
atomicnumber = [] |
| 198 |
try:
|
| 199 |
if len(element) != max(self.element_basis) + 1: |
| 200 |
oops = True
|
| 201 |
else:
|
| 202 |
oops = False
|
| 203 |
except TypeError: |
| 204 |
oops = True
|
| 205 |
if oops:
|
| 206 |
raise TypeError( |
| 207 |
("The symbol argument must be a sequence of length %d"
|
| 208 |
+" (one for each kind of lattice position")
|
| 209 |
% (max(self.element_basis)+1,)) |
| 210 |
for e in element: |
| 211 |
if isinstance(e, type("string")): |
| 212 |
atomicnumber.append(ase.data.atomic_numbers[e]) |
| 213 |
elif isinstance(element, int): |
| 214 |
atomicnumber.append(e) |
| 215 |
else:
|
| 216 |
raise TypeError("The symbols argument must be a sequence of strings or atomic numbers.") |
| 217 |
self.atomicnumber = [atomicnumber[i] for i in self.element_basis] |
| 218 |
assert len(self.atomicnumber) == len(self.bravais_basis) |
| 219 |
|
| 220 |
def convert_to_natural_basis(self): |
| 221 |
"Convert directions and miller indices to the natural basis."
|
| 222 |
self.directions = np.dot(self.directions, self.inverse_basis) |
| 223 |
if self.bravais_basis is not None: |
| 224 |
self.natural_bravais_basis = np.dot(self.bravais_basis, |
| 225 |
self.inverse_basis)
|
| 226 |
for i in (0,1,2): |
| 227 |
self.directions[i] = reduceindex(self.directions[i]) |
| 228 |
for i in (0,1,2): |
| 229 |
(j,k) = self.other[i]
|
| 230 |
self.miller[i] = reduceindex(self.handedness * |
| 231 |
cross(self.directions[j],
|
| 232 |
self.directions[k]))
|
| 233 |
|
| 234 |
def calc_num_atoms(self): |
| 235 |
v = int(round(abs(np.linalg.det(self.directions)))) |
| 236 |
if self.bravais_basis is None: |
| 237 |
return v
|
| 238 |
else:
|
| 239 |
return v * len(self.bravais_basis) |
| 240 |
|
| 241 |
def make_unit_cell(self): |
| 242 |
"Make the unit cell."
|
| 243 |
# Make three loops, and find the positions in the integral
|
| 244 |
# lattice. Each time a position is found, the atom is placed
|
| 245 |
# in the real unit cell by put_atom().
|
| 246 |
self.natoms = self.calc_num_atoms() |
| 247 |
self.nput = 0 |
| 248 |
self.atoms = np.zeros((self.natoms,3), np.float) |
| 249 |
self.elements = np.zeros(self.natoms, np.int) |
| 250 |
self.farpoint = farpoint = sum(self.directions) |
| 251 |
#printprogress = self.debug and (len(self.atoms) > 250)
|
| 252 |
percent = 0
|
| 253 |
# Find the radius of the sphere containing the whole system
|
| 254 |
sqrad = 0
|
| 255 |
for i in (0,1): |
| 256 |
for j in (0,1): |
| 257 |
for k in (0,1): |
| 258 |
vect = (i * self.directions[0] + |
| 259 |
j * self.directions[1] + |
| 260 |
k * self.directions[2]) |
| 261 |
if np.dot(vect,vect) > sqrad:
|
| 262 |
sqrad = np.dot(vect,vect) |
| 263 |
del i,j,k
|
| 264 |
# Loop along first crystal axis (i)
|
| 265 |
for (istart, istep) in ((0,1), (-1,-1)): |
| 266 |
i = istart |
| 267 |
icont = True
|
| 268 |
while icont:
|
| 269 |
nj = 0
|
| 270 |
for (jstart, jstep) in ((0,1), (-1,-1)): |
| 271 |
j = jstart |
| 272 |
jcont = True
|
| 273 |
while jcont:
|
| 274 |
nk = 0
|
| 275 |
for (kstart, kstep) in ((0,1), (-1,-1)): |
| 276 |
k = kstart |
| 277 |
#print "Starting line i=%d, j=%d, k=%d, step=(%d,%d,%d)" % (i,j,k,istep,jstep,kstep)
|
| 278 |
kcont = True
|
| 279 |
while kcont:
|
| 280 |
# Now (i,j,k) loops over Z^3, except that
|
| 281 |
# the loops can be cut off when we get outside
|
| 282 |
# the unit cell.
|
| 283 |
point = np.array((i,j,k)) |
| 284 |
if self.inside(point): |
| 285 |
self.put_atom(point)
|
| 286 |
nk += 1
|
| 287 |
nj += 1
|
| 288 |
# Is k too high?
|
| 289 |
if np.dot(point,point) > sqrad:
|
| 290 |
assert not self.inside(point) |
| 291 |
kcont = False
|
| 292 |
k += kstep |
| 293 |
# Is j too high?
|
| 294 |
if i*i+j*j > sqrad:
|
| 295 |
jcont = False
|
| 296 |
j += jstep |
| 297 |
# Is i too high?
|
| 298 |
if i*i > sqrad:
|
| 299 |
icont = False
|
| 300 |
i += istep |
| 301 |
#if printprogress:
|
| 302 |
# perce = int(100*self.nput / len(self.atoms))
|
| 303 |
# if perce > percent + 10:
|
| 304 |
# print ("%d%%" % perce),
|
| 305 |
# percent = perce
|
| 306 |
assert(self.nput == self.natoms) |
| 307 |
|
| 308 |
def inside(self, point): |
| 309 |
"Is a point inside the unit cell?"
|
| 310 |
return (np.dot(self.miller[0], point) >= 0 and |
| 311 |
np.dot(self.miller[0], point - self.farpoint) < 0 and |
| 312 |
np.dot(self.miller[1], point) >= 0 and |
| 313 |
np.dot(self.miller[1], point - self.farpoint) < 0 and |
| 314 |
np.dot(self.miller[2], point) >= 0 and |
| 315 |
np.dot(self.miller[2], point - self.farpoint) < 0) |
| 316 |
|
| 317 |
def put_atom(self, point): |
| 318 |
"Place an atom given its integer coordinates."
|
| 319 |
if self.bravais_basis is None: |
| 320 |
# No basis - just place a single atom
|
| 321 |
pos = np.dot(point, self.crystal_basis)
|
| 322 |
if self.debug >= 2: |
| 323 |
print ("Placing an atom at (%d,%d,%d) ~ (%.3f, %.3f, %.3f)." |
| 324 |
% (tuple(point) + tuple(pos))) |
| 325 |
self.atoms[self.nput] = pos |
| 326 |
self.elements[self.nput] = self.atomicnumber |
| 327 |
self.nput += 1 |
| 328 |
else:
|
| 329 |
for i, offset in enumerate(self.natural_bravais_basis): |
| 330 |
pos = np.dot(point + offset, self.crystal_basis)
|
| 331 |
if self.debug >= 2: |
| 332 |
print ("Placing an atom at (%d+%f, %d+%f, %d+%f) ~ (%.3f, %.3f, %.3f)." |
| 333 |
% (point[0], offset[0], point[1], offset[1], |
| 334 |
point[2], offset[2], pos[0], pos[1], pos[2])) |
| 335 |
self.atoms[self.nput] = pos |
| 336 |
if self.element_basis is None: |
| 337 |
self.elements[self.nput] = self.atomicnumber |
| 338 |
else:
|
| 339 |
self.elements[self.nput] = self.atomicnumber[i] |
| 340 |
self.nput += 1 |
| 341 |
|
| 342 |
def find_directions(self, directions, miller): |
| 343 |
"Find missing directions and miller indices from the specified ones."
|
| 344 |
directions = list(directions)
|
| 345 |
miller = list(miller)
|
| 346 |
# If no directions etc are specified, use a sensible default.
|
| 347 |
if directions == [None, None, None] and miller == [None, None, None]: |
| 348 |
directions = [[1,0,0], [0,1,0], [0,0,1]] |
| 349 |
# Now fill in missing directions and miller indices. This is an
|
| 350 |
# iterative process.
|
| 351 |
change = 1
|
| 352 |
while change:
|
| 353 |
change = False
|
| 354 |
missing = 0
|
| 355 |
for i in (0,1,2): |
| 356 |
(j,k) = self.other[i]
|
| 357 |
if directions[i] is None: |
| 358 |
missing += 1
|
| 359 |
if miller[j] is not None and miller[k] is not None: |
| 360 |
directions[i] = reduceindex(cross(miller[j], |
| 361 |
miller[k])) |
| 362 |
change = True
|
| 363 |
if self.debug >= 2: |
| 364 |
print "Calculating directions[%d] from miller indices" % i |
| 365 |
if miller[i] is None: |
| 366 |
missing += 1
|
| 367 |
if directions[j] is not None and directions[k] is not None: |
| 368 |
miller[i] = reduceindex(cross(directions[j], |
| 369 |
directions[k])) |
| 370 |
change = True
|
| 371 |
if self.debug >= 2: |
| 372 |
print "Calculating miller[%d] from directions" % i |
| 373 |
if missing:
|
| 374 |
raise ValueError, "Specification of directions and miller indices is incomplete." |
| 375 |
# Make sure that everything is Numeric arrays
|
| 376 |
self.directions = np.array(directions)
|
| 377 |
self.miller = np.array(miller)
|
| 378 |
# Check for left-handed coordinate system
|
| 379 |
if np.linalg.det(self.directions) < 0: |
| 380 |
print "WARNING: Creating a left-handed coordinate system!" |
| 381 |
self.miller = -self.miller |
| 382 |
self.handedness = -1 |
| 383 |
else:
|
| 384 |
self.handedness = 1 |
| 385 |
# Now check for consistency
|
| 386 |
for i in (0,1,2): |
| 387 |
(j,k) = self.other[i]
|
| 388 |
m = reduceindex(self.handedness *
|
| 389 |
cross(self.directions[j], self.directions[k])) |
| 390 |
if sum(np.not_equal(m, self.miller[i])): |
| 391 |
print "ERROR: Miller index %s is inconsisten with directions %d and %d" % (i,j,k) |
| 392 |
print "Miller indices:" |
| 393 |
print str(self.miller) |
| 394 |
print "Directions:" |
| 395 |
print str(self.directions) |
| 396 |
raise ValueError, "Inconsistent specification of miller indices and directions." |
| 397 |
|
| 398 |
def print_directions_and_miller(self, txt=""): |
| 399 |
"Print direction vectors and Miller indices."
|
| 400 |
print "Direction vectors of unit cell%s:" % (txt,) |
| 401 |
for i in (0,1,2): |
| 402 |
print " ", self.directions[i] |
| 403 |
print "Miller indices of surfaces%s:" % (txt,) |
| 404 |
for i in (0,1,2): |
| 405 |
print " ", self.miller[i] |
| 406 |
|
| 407 |
class MillerInfo: |
| 408 |
"""Mixin class to provide information about Miller indices."""
|
| 409 |
def miller_to_direction(self, miller): |
| 410 |
"""Returns the direction corresponding to a given Miller index."""
|
| 411 |
return np.dot(miller, self.millerbasis) |
| 412 |
|
| 413 |
class Lattice(Atoms, MillerInfo): |
| 414 |
"""List of atoms initially containing a regular lattice of atoms.
|
| 415 |
|
| 416 |
A part from the usual list of atoms methods this list of atoms type
|
| 417 |
also has a method, `miller_to_direction`, used to convert from Miller
|
| 418 |
indices to directions in the coordinate system of the lattice.
|
| 419 |
"""
|
| 420 |
pass
|
| 421 |
|
| 422 |
# Helper functions
|
| 423 |
def cross(a, b): |
| 424 |
"""The cross product of two vectors."""
|
| 425 |
return np.array((a[1]*b[2] - b[1]*a[2], |
| 426 |
a[2]*b[0] - b[2]*a[0], |
| 427 |
a[0]*b[1] - b[0]*a[1])) |
| 428 |
|
| 429 |
def gcd(a,b): |
| 430 |
"""Greatest Common Divisor of a and b."""
|
| 431 |
while a != 0: |
| 432 |
a,b = b%a,a |
| 433 |
return b
|
| 434 |
|
| 435 |
def reduceindex(M): |
| 436 |
"Reduce Miller index to the lowest equivalent integers."
|
| 437 |
oldM = M |
| 438 |
g = gcd(M[0], M[1]) |
| 439 |
h = gcd(g, M[2])
|
| 440 |
while h != 1: |
| 441 |
M = M/h |
| 442 |
g = gcd(M[0], M[1]) |
| 443 |
h = gcd(g, M[2])
|
| 444 |
if np.dot(oldM, M) > 0: |
| 445 |
return M
|
| 446 |
else:
|
| 447 |
return -M
|
| 448 |
|