aims 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +100 -0
- data/bin/aims_output.rb +175 -0
- data/bin/aims_summary.rb +28 -0
- data/lib/aims.rb +35 -0
- data/lib/aims/atom.rb +197 -0
- data/lib/aims/bond.rb +46 -0
- data/lib/aims/cube.rb +46 -0
- data/lib/aims/geometry.rb +545 -0
- data/lib/aims/geometry_parser.rb +56 -0
- data/lib/aims/output.rb +484 -0
- data/lib/aims/plane.rb +74 -0
- data/lib/aims/vectorize.rb +22 -0
- data/lib/aims/volume.rb +137 -0
- data/lib/aims/wurtzite.rb +48 -0
- data/lib/aims/zinc_blende.rb +311 -0
- data/spec/atom_spec.rb +40 -0
- data/spec/bond_spec.rb +0 -0
- data/spec/output_spec.rb +42 -0
- data/spec/zinc_blende_spec.rb +63 -0
- metadata +82 -0
data/lib/aims/bond.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
module Aims
|
2
|
+
|
3
|
+
# A representation of a bond between two atoms
|
4
|
+
# Bonds are not directional, so two bonds are equal if they bond the same atoms regradless of order
|
5
|
+
class Bond
|
6
|
+
|
7
|
+
attr_accessor :atoms
|
8
|
+
|
9
|
+
# Initialize a bond between two atoms
|
10
|
+
def initialize(atom1, atom2)
|
11
|
+
self.atoms = [atom1, atom2]
|
12
|
+
end
|
13
|
+
|
14
|
+
# Two bonds are equal iff the set of atoms
|
15
|
+
# in both bonds is the same.
|
16
|
+
def ==(bond)
|
17
|
+
# Take the difference between the two sets
|
18
|
+
diff1 = (self.atoms - bond.atoms)
|
19
|
+
diff2 = (bond.atoms - self.atoms)
|
20
|
+
# the lists are the same if both sets are empty
|
21
|
+
diff1.empty? & diff2.empty?
|
22
|
+
end
|
23
|
+
|
24
|
+
# Two bonds are eql? iff the set of atoms
|
25
|
+
# in both bonds is the same.
|
26
|
+
def eql?(bond)
|
27
|
+
self == bond
|
28
|
+
end
|
29
|
+
|
30
|
+
# Implementation of hash for equality testing
|
31
|
+
def hash
|
32
|
+
self.atoms.hash
|
33
|
+
end
|
34
|
+
|
35
|
+
# The bond length is the distance between the two atoms
|
36
|
+
def length
|
37
|
+
self.atoms[0].distance_to(self.atoms[1])
|
38
|
+
end
|
39
|
+
|
40
|
+
# Access the atoms in the bond
|
41
|
+
def [](i)
|
42
|
+
atoms[i]
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
data/lib/aims/cube.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
|
2
|
+
class Cube
|
3
|
+
|
4
|
+
def parse(io)
|
5
|
+
# First two lines are comments
|
6
|
+
io.readline
|
7
|
+
io.readline
|
8
|
+
# Third line is number of atoms and origin
|
9
|
+
natoms, origin_x, origin_y, origin_z = io.readline.split.collect{|s| s.to_f}
|
10
|
+
|
11
|
+
# 4th, 5th, and 6th lines define the voxels
|
12
|
+
n1, n1x, n1y, n1z = io.readline.split.collect{|str| str.to_f}
|
13
|
+
n2, n2x, n2y, n2z = io.readline.split.collect{|str| str.to_f}
|
14
|
+
n3, n3x, n3y, n3z = io.readline.split.collect{|str| str.to_f}
|
15
|
+
|
16
|
+
# Next the positions of the atoms
|
17
|
+
natoms.to_i.times do
|
18
|
+
# Read in the atoms
|
19
|
+
atomic_number, not_sure, x, y, z = io.readline.split.collect{|str| str.to_f}\
|
20
|
+
end
|
21
|
+
|
22
|
+
vals = Array.new(n1*n2*n3)
|
23
|
+
nread = 0
|
24
|
+
offset = 0
|
25
|
+
io.each_line{|line|
|
26
|
+
arr = line.split
|
27
|
+
nread = arr.size
|
28
|
+
vals[offset...offset+nread] = line.split
|
29
|
+
offset = offset+nread
|
30
|
+
}
|
31
|
+
|
32
|
+
# Finally the field
|
33
|
+
n1.to_i.times do |i|
|
34
|
+
n2.to_i.times do |j|
|
35
|
+
n3.to_i.times do |k|
|
36
|
+
v = vals.shift.to_f
|
37
|
+
x = i*n1x + j*n2x + k*n3x
|
38
|
+
y = i*n1y + j*n2y + k*n3y
|
39
|
+
z = i*n1z + j*n2z + k*n3z
|
40
|
+
puts [x, y, z, v].join(" ")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
@@ -0,0 +1,545 @@
|
|
1
|
+
module Aims
|
2
|
+
|
3
|
+
# Geometry is one of the core classes in the Aims module.
|
4
|
+
# It is a container class for Atoms and can optionally include
|
5
|
+
# lattice vectors for periodic systems.
|
6
|
+
#
|
7
|
+
# There are a number of utility methods for manipulating the geometry
|
8
|
+
# such as Geometry#rotate, Geometry#copy, Geometry#repeat, and Geometry#displace
|
9
|
+
#
|
10
|
+
# It is possible to define a collection of clip_planes using add_plane, add_plane_miller, or
|
11
|
+
# add_plane_cartesian to selectively hide atoms from the geometry.
|
12
|
+
# The planes can be defined in the coordinate system of the geometry or
|
13
|
+
# using miller indices if the miller index of the x and y axes have been specified
|
14
|
+
# with set_miller_indices.
|
15
|
+
#
|
16
|
+
# Once the miller indices have been defined, Geometry#align_x can be used to rotate the
|
17
|
+
# Geometry about the z-axis and align the x-axis with a given miller index.
|
18
|
+
class Geometry
|
19
|
+
|
20
|
+
include Vectorize
|
21
|
+
include Enumerable
|
22
|
+
|
23
|
+
# A three element array of lattice vectors for periodic systems
|
24
|
+
attr_accessor :lattice_vectors
|
25
|
+
|
26
|
+
# An array of Aims::Bond objects calculated when the Geometry is defined.
|
27
|
+
attr_accessor :bonds
|
28
|
+
|
29
|
+
# clip_planes is an array of planes
|
30
|
+
# The planes are defined with respect to the vectors millerX and millerZ
|
31
|
+
# which define the miller indices corresponding to the x and z direction
|
32
|
+
# Only atoms behind all the planes are visible
|
33
|
+
# This can be used to define a solid
|
34
|
+
attr_accessor :clip_planes
|
35
|
+
protected :clip_planes, :clip_planes=
|
36
|
+
|
37
|
+
# Rotation matrix for converting vectors in cartesian coordinates
|
38
|
+
# (the native coordinate system of the geometry) into vectors in
|
39
|
+
# the miller coordinate space. See set_miller_indices
|
40
|
+
attr_accessor :cart_to_miller
|
41
|
+
protected :cart_to_miller, :cart_to_miller=
|
42
|
+
|
43
|
+
# Rotation matrix for converting vectors from miller coordiantes
|
44
|
+
# to cartesian coordiantes. See set_miller_indices
|
45
|
+
attr_accessor :miller_to_cart
|
46
|
+
protected :miller_to_cart, :miller_to_cart=
|
47
|
+
|
48
|
+
# Initialize a Geometry with a list of atoms, and an optional list of lattice vectors
|
49
|
+
def initialize(atoms, vectors = nil, dont_make_bonds = false)
|
50
|
+
|
51
|
+
# Do some basic validation
|
52
|
+
unless atoms.is_a? Array
|
53
|
+
raise "Atoms must be an array!"
|
54
|
+
end
|
55
|
+
|
56
|
+
if atoms.empty?
|
57
|
+
raise "Atoms array is empty!"
|
58
|
+
end
|
59
|
+
|
60
|
+
atoms.each{|a|
|
61
|
+
unless a.is_a? Atom
|
62
|
+
raise "Atoms array contains invalid object type #{a.class}!"
|
63
|
+
end
|
64
|
+
}
|
65
|
+
# Ok. I'm satisfied
|
66
|
+
self.atoms = atoms
|
67
|
+
|
68
|
+
# Now check the lattice vectors
|
69
|
+
if vectors
|
70
|
+
self.lattice_vectors = vectors.collect{|v|
|
71
|
+
if v.is_a? Vector and v.size == 3
|
72
|
+
v
|
73
|
+
elsif v.is_a? Array and v.size == 3
|
74
|
+
Vector.elements(v)
|
75
|
+
else
|
76
|
+
raise "Invalid lattice vector"
|
77
|
+
end
|
78
|
+
}
|
79
|
+
unless self.lattice_vectors.size == 3
|
80
|
+
raise "There must be 3 lattice vectors, not #{self.lattice_vectors.size}."
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
@clip_planes = []
|
85
|
+
make_bonds unless dont_make_bonds
|
86
|
+
end
|
87
|
+
|
88
|
+
# The geometry is empty if there are no atoms.
|
89
|
+
def empty?
|
90
|
+
@atoms.empty?
|
91
|
+
end
|
92
|
+
|
93
|
+
# Define the atoms in basis of this Unit Cell
|
94
|
+
def atoms=(listOfAtoms)
|
95
|
+
@atoms = listOfAtoms
|
96
|
+
recache_visible_atoms
|
97
|
+
end
|
98
|
+
|
99
|
+
# Return the atoms in this unit cell. By default returns
|
100
|
+
# only the visible atoms, but this method will return all of the atoms
|
101
|
+
# if called with (visibleOnly = false)
|
102
|
+
def atoms(visibleOnly = true)
|
103
|
+
if visibleOnly and 0 < @clip_planes.size
|
104
|
+
@visibleAtoms
|
105
|
+
else
|
106
|
+
@atoms
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Generate and cache bonds for this geometry.
|
111
|
+
# A bond will be generated for every pair of atoms closer than +bond_length+
|
112
|
+
def make_bonds(bond_length = 4.0)
|
113
|
+
# initialize an empty array
|
114
|
+
self.bonds = Array.new
|
115
|
+
|
116
|
+
# Make bonds between all atoms
|
117
|
+
stack = atoms.dup
|
118
|
+
|
119
|
+
atom1 = stack.pop
|
120
|
+
while (not stack.empty?)
|
121
|
+
stack.each{|atom2|
|
122
|
+
b = Bond.new(atom1, atom2)
|
123
|
+
self.bonds << b if b.length < bond_length
|
124
|
+
}
|
125
|
+
atom1 = stack.pop
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# Add a clip Plane to the unit cell
|
130
|
+
# recache the visible atoms if called with +recache = true+ (the Default)
|
131
|
+
def add_plane(aPlane, recache = true)
|
132
|
+
self.clip_planes << aPlane
|
133
|
+
recache_visible_atoms if recache
|
134
|
+
end
|
135
|
+
|
136
|
+
# Add a clipping plane defined by the outward normal (h,k,l)
|
137
|
+
# and a point on the plane (x,y,z)
|
138
|
+
# Only points behind the plane will be kept. Points in front
|
139
|
+
# of the plane will be invisible. They are not really gone, but
|
140
|
+
# can be returned by moving or removing the plane
|
141
|
+
def add_plane_miller(h,k,l,x,y,z)
|
142
|
+
normal = self.cartesian_from_miller(h, k, l)
|
143
|
+
self.add_plane_cartesian(normal[0], normal[1], normal[2], x, y, z)
|
144
|
+
end
|
145
|
+
|
146
|
+
# Add a clipping plane defined by the outward normal (nx,ny,nz)
|
147
|
+
# and a point on the plane (x,y,z)
|
148
|
+
# Only points behind the plane will be kept. Points in front
|
149
|
+
# of the plane will be invisible. They are not really gone, but
|
150
|
+
# can be returned by moving or removing the plane
|
151
|
+
def add_plane_cartesian(nx, ny, nz, x, y, z)
|
152
|
+
add_plane(Plane.new(nx, ny, nz, x, y, z), true)
|
153
|
+
end
|
154
|
+
|
155
|
+
# Clear the array of clip planes
|
156
|
+
def clear_planes
|
157
|
+
self.clip_planes.clear
|
158
|
+
recache_visible_atoms
|
159
|
+
end
|
160
|
+
|
161
|
+
# Remove a specific clip plane
|
162
|
+
def remove_plane(aPlane)
|
163
|
+
self.clip_planes.reject!{|p| p == aPlane}
|
164
|
+
recache_visible_atoms
|
165
|
+
end
|
166
|
+
|
167
|
+
# Define the [h, k, l] vectors for each cartesian direction x, y, and z
|
168
|
+
# There must be at least two vectors provided, and they must be orthogonal
|
169
|
+
# The z vector, if provided, must also be orthogonal. If it is not
|
170
|
+
# provided it will be calculated as the cross product of x and y
|
171
|
+
#
|
172
|
+
# These vectors will populate a matrix that for calculation of the
|
173
|
+
# miller indices for any arbitrary vector in cartesian space, and
|
174
|
+
# the calculation of the cartesian space vector for any arbitrary miller index
|
175
|
+
# See miller_from_cartesion and cartesian_from_miller for more details.
|
176
|
+
#
|
177
|
+
# The default miller indices are parallel to the cartesian indices. That is
|
178
|
+
# the default x-axes points in (100), y points along (010) and z points along (001)
|
179
|
+
#
|
180
|
+
def set_miller_indices(x, y, z=nil)
|
181
|
+
raise "Vectors must be orthogonal" unless 0 == dot(x,y)
|
182
|
+
if z
|
183
|
+
raise "Vectors must be orthogonal" unless 0 == dot(x,z)
|
184
|
+
else
|
185
|
+
z = cross(x, y)
|
186
|
+
end
|
187
|
+
self.cart_to_miller = Matrix[[x[0], y[0], z[0]],
|
188
|
+
[x[1], y[1], z[1]],
|
189
|
+
[x[2], y[2], z[2]]]
|
190
|
+
self.miller_to_cart = cart_to_miller.inverse
|
191
|
+
return nil
|
192
|
+
end
|
193
|
+
|
194
|
+
# Rotate the atoms about the z-axis so that the
|
195
|
+
# vector given by the miller index [h,k,l] is aligned
|
196
|
+
# with the cartesian unit vector [1,0,0]
|
197
|
+
# The vector [h,k,l] must be orthogonal to the defined
|
198
|
+
# miller index of the z-direction
|
199
|
+
def align_x(h,k,l)
|
200
|
+
millerZ = self.cart_to_miller.column(2)
|
201
|
+
unless 0 == dot([h,k,l], millerZ)
|
202
|
+
raise "Specified vector [#{[h,k,l].join(',')}] is not orthogonal to z-axis [#{millerZ.to_a.join(',')}]"
|
203
|
+
end
|
204
|
+
|
205
|
+
# Define the current x axis and the new x-axis
|
206
|
+
millerX = self.cart_to_miller.column(0)
|
207
|
+
newX = Vector[h,k,l]
|
208
|
+
|
209
|
+
# Find the angle between the current x direction and the new x-direction
|
210
|
+
angle = acos(dot(newX*(1/newX.r), millerX*(1/millerX.r)))*180/Math::PI
|
211
|
+
|
212
|
+
#Make the rotation in azimuth
|
213
|
+
self.rotate(0, angle)
|
214
|
+
|
215
|
+
end
|
216
|
+
|
217
|
+
# Given a vector (x,y,z) in cartesian coordinates,
|
218
|
+
# return the miller-index corresponding to that vector's direction
|
219
|
+
def miller_from_cartesian(x, y, z)
|
220
|
+
if self.cart_to_miller
|
221
|
+
self.cart_to_miller*Vector[x, y, z]
|
222
|
+
else
|
223
|
+
nil
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
# Given a miller index (h,k,l),
|
228
|
+
# return a vector in cartesian coordinates pointing in that direction
|
229
|
+
def cartesian_from_miller(h, k, l)
|
230
|
+
|
231
|
+
if self.miller_to_cart
|
232
|
+
self.miller_to_cart*Vector[h, k, l]
|
233
|
+
else
|
234
|
+
nil
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
# Recompute the atoms that are behind all the clip planes
|
239
|
+
# Atoms that are in front of any clip-plane are considered invisible.
|
240
|
+
def recache_visible_atoms
|
241
|
+
|
242
|
+
plane_count = (@clip_planes ? @clip_planes.length : 0)
|
243
|
+
return if plane_count == 0
|
244
|
+
|
245
|
+
if @visibleAtoms
|
246
|
+
@visibleAtoms.clear
|
247
|
+
else
|
248
|
+
@visibleAtoms = []
|
249
|
+
end
|
250
|
+
@atoms.each{|a|
|
251
|
+
i = plane_count
|
252
|
+
@clip_planes.each{|p|
|
253
|
+
i = i-1 if 0 >= p.distance_to_point(a.x, a.y, a.z)
|
254
|
+
}
|
255
|
+
@visibleAtoms << a if i == 0
|
256
|
+
}
|
257
|
+
|
258
|
+
make_bonds
|
259
|
+
end
|
260
|
+
|
261
|
+
# Rotate the geometry in 3 dimensions. The rotation is
|
262
|
+
# first about the x-axis then about the z-axis.
|
263
|
+
# * +az+ The azimuthal rotation in degrees about the z-axis
|
264
|
+
# * +alt+ The rotation in altitude in degrees about the x-axis
|
265
|
+
#
|
266
|
+
# Returns a new Aims#Geometry object.
|
267
|
+
def rotate(alt, az)
|
268
|
+
altrad = Math::PI/180*alt
|
269
|
+
azrad = Math::PI/180*az
|
270
|
+
|
271
|
+
sinalt = Math::sin(altrad)
|
272
|
+
cosalt = Math::cos(altrad)
|
273
|
+
sinaz = Math::sin(azrad)
|
274
|
+
cosaz = Math::cos(azrad)
|
275
|
+
|
276
|
+
mat1 = Matrix[[cosaz, -sinaz, 0], [sinaz, cosaz, 0], [0, 0, 1]]
|
277
|
+
mat2 = Matrix[[1.0, 0.0, 0.0], [0.0, cosalt, -sinalt], [0.0, sinalt, cosalt]]
|
278
|
+
mat = mat1*mat2
|
279
|
+
newatoms = atoms.collect{|a|
|
280
|
+
a.rotate(mat)
|
281
|
+
}
|
282
|
+
newvectors = lattice_vectors.collect{|v|
|
283
|
+
mat*v
|
284
|
+
}
|
285
|
+
uc = Geometry.new(newatoms, newvectors)
|
286
|
+
uc.cart_to_miller = mat*self.cart_to_miller
|
287
|
+
uc.miller_to_cart = uc.cart_to_miller.inverse
|
288
|
+
|
289
|
+
return uc
|
290
|
+
end
|
291
|
+
|
292
|
+
# Return a new Aims#Geometry that is a deep copy of this geometry.
|
293
|
+
def copy
|
294
|
+
newAtoms = []
|
295
|
+
newVecs = []
|
296
|
+
@atoms.each{|a|
|
297
|
+
newAtoms << a.copy
|
298
|
+
}
|
299
|
+
@lattice_vectors.each{|v|
|
300
|
+
newVecs << v*1
|
301
|
+
}
|
302
|
+
uc = Geometry.new(newAtoms, newVecs)
|
303
|
+
uc.clip_planes = self.clip_planes
|
304
|
+
uc.miller_to_cart = self.miller_to_cart
|
305
|
+
uc.cart_to_miller = self.cart_to_miller
|
306
|
+
uc.recache_visible_atoms
|
307
|
+
return uc
|
308
|
+
end
|
309
|
+
|
310
|
+
# Return the number of atoms in this unit cell.
|
311
|
+
def size
|
312
|
+
atoms.size
|
313
|
+
end
|
314
|
+
|
315
|
+
# Return a two element array contaiing two artificial Aims::Atom objects whose coordinates
|
316
|
+
# represent the lower-left and upper-right corners of the Geometry's bounding box.
|
317
|
+
def bounding_box(visible_only = true)
|
318
|
+
maxX = atoms(visible_only).first.x
|
319
|
+
maxY = atoms(visible_only).first.y
|
320
|
+
maxZ = atoms(visible_only).first.z
|
321
|
+
minX = maxX
|
322
|
+
minY = maxY
|
323
|
+
minZ = maxZ
|
324
|
+
|
325
|
+
atoms(visible_only).each{|a|
|
326
|
+
if a.x > maxX
|
327
|
+
maxX = a.x
|
328
|
+
elsif a.x < minX
|
329
|
+
minX = a.x
|
330
|
+
end
|
331
|
+
|
332
|
+
if a.y > maxY
|
333
|
+
maxY = a.y
|
334
|
+
elsif a.y < minY
|
335
|
+
minY = a.y
|
336
|
+
end
|
337
|
+
|
338
|
+
if a.z > maxZ
|
339
|
+
maxZ = a.z
|
340
|
+
elsif a.z < minZ
|
341
|
+
minZ = a.z
|
342
|
+
end
|
343
|
+
}
|
344
|
+
|
345
|
+
[Atom.new(maxX, maxY, maxZ), Atom.new(minX, minY, minZ)]
|
346
|
+
end
|
347
|
+
|
348
|
+
# Return an Atom whose coordinates are the center of the unit-cell.
|
349
|
+
def center(visible_only = true)
|
350
|
+
bounds = bounding_box(visible_only)
|
351
|
+
x = (bounds[0].x + bounds[1].x)/2.0
|
352
|
+
y = (bounds[0].y + bounds[1].y)/2.0
|
353
|
+
z = (bounds[0].z + bounds[1].z)/2.0
|
354
|
+
return Atom.new(x,y,z)
|
355
|
+
end
|
356
|
+
|
357
|
+
# Yield to each atom in the unit cell
|
358
|
+
def each
|
359
|
+
self.atoms.each{|a|
|
360
|
+
yield a
|
361
|
+
}
|
362
|
+
end
|
363
|
+
|
364
|
+
# Remove the specified atom from the unit cell
|
365
|
+
def remove_atom(atom)
|
366
|
+
atoms.reject!{|a|
|
367
|
+
a.id == atom.id
|
368
|
+
}
|
369
|
+
# Force a rehash of nearest-neighbor tree
|
370
|
+
@tree = nil
|
371
|
+
end
|
372
|
+
|
373
|
+
# Return the Atom at the given index
|
374
|
+
def [](index)
|
375
|
+
atoms[index]
|
376
|
+
end
|
377
|
+
|
378
|
+
# Return a new unit cell with all the atoms displaced by the amount x,y,z
|
379
|
+
def displace(x,y,z)
|
380
|
+
Geometry.new(atoms.collect{|a|
|
381
|
+
a.displace(x,y,z)
|
382
|
+
}, self.lattice_vectors)
|
383
|
+
#TODO copy miller indices
|
384
|
+
end
|
385
|
+
|
386
|
+
# Repeat a unit cell nx,ny,nz times in the directions
|
387
|
+
# of the lattice vectors.
|
388
|
+
# Negative values of nx,ny or nz results in displacement in the
|
389
|
+
# negative direction of the lattice vectors
|
390
|
+
def repeat(nx=1, ny=1, nz=1)
|
391
|
+
|
392
|
+
raise "Not a periodic system." if self.lattice_vectors.nil?
|
393
|
+
|
394
|
+
u = self.copy
|
395
|
+
v1 = self.lattice_vectors[0]
|
396
|
+
v2 = self.lattice_vectors[1]
|
397
|
+
v3 = self.lattice_vectors[2]
|
398
|
+
|
399
|
+
nx_sign = (0 < nx) ? 1 : -1
|
400
|
+
ny_sign = (0 < ny) ? 1 : -1
|
401
|
+
nz_sign = (0 < nz) ? 1 : -1
|
402
|
+
|
403
|
+
new_atoms = []
|
404
|
+
nx.abs.times do |i|
|
405
|
+
ny.abs.times do |j|
|
406
|
+
nz.abs.times do |k|
|
407
|
+
new_atoms << self.displace(nx_sign*i*v1[0] + ny_sign*j*v2[0] + nz_sign*k*v3[0],
|
408
|
+
nx_sign*i*v1[1] + ny_sign*j*v2[1] + nz_sign*k*v3[1],
|
409
|
+
nx_sign*i*v1[2] + ny_sign*j*v2[2] + nz_sign*k*v3[2]).atoms
|
410
|
+
end
|
411
|
+
end
|
412
|
+
end
|
413
|
+
|
414
|
+
u.atoms = new_atoms.flatten
|
415
|
+
u.lattice_vectors = [Vector[nx.abs*v1[0], nx.abs*v1[1], nx.abs*v1[2]],
|
416
|
+
Vector[ny.abs*v2[0], ny.abs*v2[1], ny.abs*v2[2]],
|
417
|
+
Vector[nz.abs*v3[0], nz.abs*v3[1], nz.abs*v3[2]]]
|
418
|
+
u.make_bonds
|
419
|
+
return u
|
420
|
+
end
|
421
|
+
|
422
|
+
# Concatenate the atoms from another unit cell to this unit cell.
|
423
|
+
# Currently does no validation on lattice vectors on miller vectors
|
424
|
+
def <<(aGeometry)
|
425
|
+
self.atoms.concat(aGeometry.atoms)
|
426
|
+
self.make_bonds
|
427
|
+
return self
|
428
|
+
end
|
429
|
+
|
430
|
+
# Print all the atoms joined by a newline
|
431
|
+
def to_s
|
432
|
+
self.atoms.collect{|a| a.to_s}.join("\n")
|
433
|
+
end
|
434
|
+
|
435
|
+
# Return a string formatted in the Aims geometry.in format.
|
436
|
+
def format_geometry_in
|
437
|
+
output = ""
|
438
|
+
if self.lattice_vectors
|
439
|
+
output << self.lattice_vectors.collect{|v| "lattice_vector #{v[0]} #{v[1]} #{v[2]}"}.join("\n")
|
440
|
+
output << "\n"
|
441
|
+
end
|
442
|
+
output << self.atoms.collect{|a| a.format_geometry_in}.join("\n")
|
443
|
+
|
444
|
+
output
|
445
|
+
end
|
446
|
+
|
447
|
+
# return a string in xyz format
|
448
|
+
def format_xyz
|
449
|
+
output = self.atoms.size.to_s + "\n"
|
450
|
+
output << "Aims Geometry \n"
|
451
|
+
self.atoms.each{ |a|
|
452
|
+
output << [a.species, a.x.to_s, a.y.to_s, a.z.to_s].join("\t") + "\n"
|
453
|
+
}
|
454
|
+
output
|
455
|
+
end
|
456
|
+
|
457
|
+
# Find the difference between this cell and another cell
|
458
|
+
# Return a cell with Pseudo-Atoms whose positions are really the differences
|
459
|
+
def delta(aCell)
|
460
|
+
raise "Cells do not have the same number of atoms" unless self.atoms.size == aCell.atoms.size
|
461
|
+
|
462
|
+
pseudo_atoms = []
|
463
|
+
self.atoms.size.times {|i|
|
464
|
+
a1 = self.atoms[i]
|
465
|
+
a2 = aCell.atoms[i]
|
466
|
+
raise "Species do not match" unless a1.species == a2.species
|
467
|
+
a = Atom.new
|
468
|
+
a.species = a1.species
|
469
|
+
a.x = a1.x - a2.x
|
470
|
+
a.y = a1.y - a2.y
|
471
|
+
a.z = a1.z - a2.z
|
472
|
+
pseudo_atoms << a
|
473
|
+
}
|
474
|
+
Geometry.new(pseudo_atoms)
|
475
|
+
end
|
476
|
+
|
477
|
+
|
478
|
+
# Move all atoms inside the primitive volume defined by the
|
479
|
+
# six planes of the lattice vectors
|
480
|
+
def correct
|
481
|
+
|
482
|
+
# Hash for storing bounding planes and the out-of-plane vector
|
483
|
+
# by which each atom will be displaced to move it into the primitive volume
|
484
|
+
# key = bounding plane
|
485
|
+
# value = out-of-plane lattice vector used to displace atoms
|
486
|
+
planes_vecs = {}
|
487
|
+
|
488
|
+
# Define the primitive volume as six planes by
|
489
|
+
# finding the normal to each pair of lattice vectors
|
490
|
+
# and making a plane with this normal that includes
|
491
|
+
# 1. The point at the head of the third lattice vector
|
492
|
+
# and pointing in the positive direction
|
493
|
+
#
|
494
|
+
# 2. The point at the tail of the third lattice vector
|
495
|
+
# and pointing in the negative direction
|
496
|
+
#
|
497
|
+
(0..2).each do |i|
|
498
|
+
out_vector = lattice_vectors[i] # The out of plane vector
|
499
|
+
plane_vectors = lattice_vectors.reject{|v| v == out_vector} # The in plane vectors
|
500
|
+
norm = cross(plane_vectors[0], plane_vectors[1])
|
501
|
+
# if the norm has a component in the direction of the out of plane vector
|
502
|
+
# then use the head of the out-of-plane vector as the intersection point
|
503
|
+
# otherwise use the tail (the origin)
|
504
|
+
if 0 < dot(norm, out_vector)
|
505
|
+
# First plane is in direction of norm and intersects head
|
506
|
+
# Displace vector is +1 if plane intersects tail and -1 if plane intersects head.
|
507
|
+
planes_vecs[Plane.new(norm[0], norm[1], norm[2], out_vector[0], out_vector[1], out_vector[2])] = out_vector*(-1)
|
508
|
+
# Second plane is opposite direction of norm and intersects tail
|
509
|
+
planes_vecs[Plane.new(-norm[0], -norm[1], -norm[2], 0, 0, 0)] = out_vector*1
|
510
|
+
else
|
511
|
+
# First plane is in opposite direction of norm and intersects head
|
512
|
+
planes_vecs[Plane.new(-norm[0], -norm[1], -norm[2], out_vector[0], out_vector[1], out_vector[2])] = out_vector*(-1)
|
513
|
+
# Second plane is in direction of norm and intersects tail
|
514
|
+
planes_vecs[Plane.new(norm[0], norm[1], norm[2], 0, 0, 0)] = out_vector*1
|
515
|
+
end
|
516
|
+
end
|
517
|
+
|
518
|
+
# Make a coyp of the unit cell
|
519
|
+
new_unit_cell = self.copy
|
520
|
+
|
521
|
+
# Move each atom behind all the planes
|
522
|
+
new_unit_cell.atoms(false).each do |atom|
|
523
|
+
planes_vecs.each_pair do |p, v|
|
524
|
+
if p.distance_to_point(0,0,0) == 0
|
525
|
+
# If the plane intersects the origin then
|
526
|
+
# move atoms not on the plane (inequality)
|
527
|
+
while p.distance_to_point(atom.x, atom.y, atom.z) > 0
|
528
|
+
atom.displace!(v[0], v[1], v[2])
|
529
|
+
end
|
530
|
+
else
|
531
|
+
# Move atoms that lie on the plane if the plane doesn't intersect the origin
|
532
|
+
while p.distance_to_point(atom.x, atom.y, atom.z) >= 0
|
533
|
+
atom.displace!(v[0], v[1], v[2])
|
534
|
+
end
|
535
|
+
end
|
536
|
+
end
|
537
|
+
end
|
538
|
+
new_unit_cell.atoms.uniq!
|
539
|
+
new_unit_cell.make_bonds
|
540
|
+
return new_unit_cell
|
541
|
+
|
542
|
+
end
|
543
|
+
|
544
|
+
end
|
545
|
+
end
|