aims 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|