aims 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,74 @@
1
+ module Aims
2
+
3
+ # A Class representing a plane.
4
+ # Internally stored in Hessian Normal Form.
5
+ # 0 = Ax + By + Cz - D
6
+ #
7
+ # (A,B,C) is the plane normal.
8
+ # D is the distance from the origin.
9
+ class Plane
10
+ attr_reader :a, :b, :c, :d
11
+
12
+ # Initialize this plane with the normal (a,b,c) and a
13
+ # point (x,y,z) on the plane
14
+ def initialize(a, b, c, x=0, y=0, z=0)
15
+ @a = a
16
+ @b = b
17
+ @c = c
18
+ if (@a == 0 and @b == 0 and @c == 0)
19
+ raise "Invalid definition of plane."
20
+ end
21
+ @d = a*x + b*y + c*z
22
+ end
23
+
24
+ # return some arbitrary point on the plane
25
+ def any_point_on_plane
26
+
27
+ unless (@c == 0)
28
+ return Vector[0, 0, @d/@c]
29
+ end
30
+
31
+ unless (@b == 0)
32
+ return Vector[0, @d/@b, 0]
33
+ end
34
+
35
+ unless (@a == 0)
36
+ return Vector[@d/@a, 0, 0]
37
+ end
38
+
39
+ # Actually if we get to this point, the plane undetermined and all of R3 satisfies the definition
40
+ return Vector[0,0,0]
41
+ end
42
+
43
+ # Return the distance to point (x,y,z)
44
+ #
45
+ # distance = D - Ax - By - Cz
46
+ def distance_to_point(x, y, z)
47
+ a*x + b*y + c*z - d
48
+ end
49
+
50
+ # Return the unit normal Vector[a, b, c]
51
+ def unit_normal
52
+ v = Vector[@a, @b, @c]
53
+ v*(1/v.r)
54
+ end
55
+
56
+ # The equation for the interstion of a ray and a plane
57
+ # NOT YET IMPLEMENTED
58
+ def intersection_with_ray(a, b)
59
+ raise "Sorry. Plane#intersection_with_ray is not yet implemented"
60
+ end
61
+
62
+ # Displace this plane a distance in the direction of its normal
63
+ def displace_along_normal(distance)
64
+ @d += distance
65
+ end
66
+
67
+ # Two planes are equal if there ABCD parameters are equal
68
+ def ==(aPlane)
69
+ @a == aPlane.a and @b == aPlane.b and @c == aPlane.c and @d == aPlane.d
70
+ end
71
+ alias_method :eql?, :==
72
+
73
+ end
74
+ end
@@ -0,0 +1,22 @@
1
+ module Aims
2
+ module Vectorize
3
+
4
+ # Dot product of two n-element arrays
5
+ def dot(a, b)
6
+ unless a.size == b.size
7
+ raise "Vectors must be the same length"
8
+ end
9
+
10
+ # Make element-by-element array of pairs
11
+ (a.to_a).zip(b.to_a).inject(0) {|tot, pair| tot = tot + pair[0]*pair[1]}
12
+ end
13
+
14
+ # Cross product of two arrays of length 3
15
+ def cross(b,c)
16
+ unless b.size == 3 and c.size == 3
17
+ raise "Vectors must be of length 3"
18
+ end
19
+ Vector[b[1]*c[2] - b[2]*c[1], b[2]*c[0] - b[0]*c[2], b[0]*c[1] - b[1]*c[0]]
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,137 @@
1
+ module Aims
2
+
3
+ # A volume is defined by a minimum of four planes that intersect in a minimum
4
+ # of four points. The normals of the planes must all point outward. This is tested
5
+ #
6
+ class Volume
7
+
8
+ include Vectorize
9
+
10
+ # Quick recursive method for calculating combinations
11
+ def Volume.choose(list, num, head = [])
12
+
13
+ _head = head.dup
14
+ _list = list.dup
15
+ _num = num
16
+
17
+ if _num == 0
18
+ return [_head]
19
+ end
20
+
21
+ new_heads = []
22
+ while _list.size > _num-1
23
+ h = _head + [_list.shift]
24
+ new_heads += Volume.choose(_list, num-1, h)
25
+ end
26
+ return new_heads
27
+
28
+ end
29
+
30
+
31
+ # Return an array of tuples that define the
32
+ # vertices of intersection of these planes
33
+ # Vertices are removed that lie in front of any plane
34
+ def Volume.intersection_points(planes)
35
+ combos = Volume.choose(planes, 3)
36
+ points = []
37
+ combos.each{|c|
38
+ n1 = c[0].unit_normal
39
+ n2 = c[1].unit_normal
40
+ n3 = c[2].unit_normal
41
+ d = Matrix[n1, n2,n3].transpose.det
42
+
43
+ # The determinant is zero if any two planes are parallel
44
+ unless (d == 0)
45
+ p1 = c[0].any_point_on_plane
46
+ p2 = c[1].any_point_on_plane
47
+ p3 = c[2].any_point_on_plane
48
+
49
+ # This defines the point of intersection of three planes.
50
+ points << (cross(n2,n3)*dot(p1, n1) + cross(n3,n1)*dot(p2,n2) + cross(n1,n2)*dot(p3, n3))*(1/d)
51
+ end
52
+ }
53
+
54
+ # Only keep the points that are behind all planes
55
+ keepers = []
56
+ points.each{|pt|
57
+ keep = true
58
+ planes.each {|pl|
59
+ keep = (pl.distance_to_point(pt[0], pt[1], pt[2]) <= 0)
60
+ break unless keep
61
+ }
62
+ keepers << pt if keep
63
+ }
64
+ return keepers
65
+ end
66
+
67
+ def initialize(planes)
68
+
69
+ points = Volume.intersection_points(planes)
70
+ if (4 > points.size)
71
+ raise "Planes do not intersect in a closed volume."
72
+ end
73
+ @points = points
74
+ @planes = planes
75
+ end
76
+
77
+ # Return the bounding box for this volume
78
+ def bounding_box
79
+ unless @bbox
80
+ p = @points[0]
81
+ minX = p[0]
82
+ maxX = p[0]
83
+ minY = p[1]
84
+ maxY = p[1]
85
+ minZ = p[2]
86
+ maxZ = p[2]
87
+ @points.each{|p|
88
+ minX = p[0] if p[0] < minX
89
+ maxX = p[0] if p[0] > maxX
90
+ minY = p[1] if p[1] < minY
91
+ maxY = p[1] if p[1] > maxY
92
+ minZ = p[2] if p[2] < minZ
93
+ maxZ = p[2] if p[2] > maxZ
94
+ }
95
+ @max = Vector[maxX, maxY,maxZ]
96
+ @min = Vector[minX, minY, minZ]
97
+ @bbox = Volume.new([Plane.new(-1,0,0, minX, minY, minZ),
98
+ Plane.new(0,-1,0, minX, minY, minZ),
99
+ Plane.new(0,0,-1, minX, minY, minZ),
100
+ Plane.new(1,0,0, maxX, maxY, maxZ),
101
+ Plane.new(0,1,0, maxX, maxY, maxZ),
102
+ Plane.new(0,0,1, maxX, maxY, maxZ)])
103
+ end
104
+ @bbox
105
+ end
106
+
107
+ # Return the point on the bounding box that represents the maximum value
108
+ # of any cartesian coordinate (the upper right corner)
109
+ def max_point
110
+ # generate the bounding box if not already done
111
+ bounding_box
112
+ # return the max
113
+ @max
114
+ end
115
+
116
+ # Return the point on the bounding box that represents the minimum value
117
+ # of any cartesian coordinate (the lower left corner).
118
+ def min_point
119
+ # generate the bounding box if not already done
120
+ bounding_box
121
+ # return the min
122
+ @min
123
+ end
124
+
125
+ # A volume contains a point if it lies behind all the planes
126
+ def contains_point(x,y,z)
127
+ behind = true
128
+ @planes.each{|p|
129
+ behind = (0 >= p.distance_to_point(x,y,z))
130
+ break if not behind
131
+ }
132
+ return behind
133
+ end
134
+
135
+ end
136
+
137
+ end
@@ -0,0 +1,48 @@
1
+
2
+ require 'mathn'
3
+
4
+ module Aims
5
+ class Wurtzite
6
+
7
+ include Math
8
+
9
+ attr_accessor :lattice_const, :cation, :anion
10
+
11
+ # Initialize the wurtzite Geometry
12
+ # cation and anion are the atomic
13
+ # species occupying the two different sub-lattices.
14
+ # lattice_const specifies the lattice constant
15
+ def initialize(cation, anion, lattice_const)
16
+ self.lattice_const = lattice_const
17
+ self.cation = cation
18
+ self.anion = anion
19
+ end
20
+
21
+ def get_bulk
22
+ # The lattice constant
23
+ a = (ARGV[0] || 4.0).to_f
24
+ c = 1.63299*a # sqrt(8/3)a
25
+ #a = 6.19231 # Strained GaSb, Unstrained GaSb is 6.22
26
+ #a = 5.75 # Unstrained GaAs
27
+
28
+ # The atoms on a HCP
29
+ as1 = Atom.new(0,0,0,'As')
30
+ as2 = as1.displace(0.5*a, 0.33333*a, 0.5*c)
31
+
32
+ ga1 = Atom.new(0.0, 0.0, c*0.386, 'Ga')
33
+ ga2 = ga1.displace(0.5*a,0.33333*a, 0.5*c)
34
+
35
+ # The lattice Vectors
36
+ v1 = Vector[1.0, 0.0, 0.0]*a
37
+ v2 = Vector[0.5, 0.5*sqrt(3), 0.0]*a
38
+ v3 = Vector[0.0, 0.0, 1.0]*c
39
+
40
+ # The unit cell
41
+ wz = Geometry.new([as1,ga1,as2,ga2], [v1, v2, v3])
42
+
43
+ # wz.set_miller_indices(millerX, millerY, millerZ)
44
+ return wz
45
+ end
46
+ end
47
+ end
48
+
@@ -0,0 +1,311 @@
1
+ require 'mathn'
2
+
3
+ module Aims
4
+
5
+ # Factory class for generating slabs for various crystal surfaces
6
+ # of specified thickness and with the specified vacuum.
7
+ # Example:
8
+ # zb = ZincBlende.new("Ga", "As", 5.65)
9
+ # zb.get_bulk -> # returns the a unit cell with two atoms
10
+ # zb.get_111B_surface(7, 20, 3) # returns a slab of 111B 7 monolayers thick, with 20 angstrom of vacuum, and the bottom 3 layers constrained
11
+ class ZincBlende
12
+
13
+ include Vectorize
14
+ include Math
15
+
16
+ attr_accessor :lattice_const, :cation, :anion
17
+
18
+ # Initialize the zinc-blende Geometry
19
+ # cation and anion are the atomic
20
+ # species occupying the two different sub-lattices.
21
+ # lattice_const specifies the lattice constant
22
+ def initialize(cation, anion, lattice_const)
23
+ self.lattice_const = lattice_const
24
+ self.cation = cation
25
+ self.anion = anion
26
+ end
27
+
28
+ # Return the traditional unit cell of bulk zinc blende
29
+ def get_bulk
30
+ b = 0.25*self.lattice_const
31
+ a1 = Atom.new(0, 0, 0, self.cation)
32
+ a2 = Atom.new(b, b, b, self.anion)
33
+
34
+ v1 = Vector[0.5, 0.5, 0.0]*self.lattice_const
35
+ v2 = Vector[0.5, 0.0, 0.5]*self.lattice_const
36
+ v3 = Vector[0.0, 0.5, 0.5]*self.lattice_const
37
+ zb = Geometry.new([a1, a2], [v1, v2, v3])
38
+
39
+ millerx = [1, 0, 0]
40
+ millery = [0, 1, 0]
41
+ millerz = [0, 0, 1]
42
+
43
+ zb.set_miller_indices(millerx, millery, millerz)
44
+
45
+ return zb
46
+ end
47
+
48
+ # Fill the given volume with atoms
49
+ def fill_volume(volume)
50
+
51
+ # First fill a cube that bounds the volume
52
+ max = volume.max_point
53
+ min = volume.min_point
54
+
55
+ dx = max[0] - min[0]
56
+ dy = max[1] - min[1]
57
+ dz = max[2] - min[2]
58
+
59
+ bulk = get_bulk
60
+
61
+ # This inverse matrix gives the number of repetitions
62
+ m = Matrix[[dx,0,0], [0,dy,0], [0,0,dz]]
63
+ v = Matrix[bulk.lattice_vectors[0].to_a,
64
+ bulk.lattice_vectors[1].to_a,
65
+ bulk.lattice_vectors[2].to_a]
66
+ rep_mat = m*(v.inverse)
67
+
68
+ # The only way I can figure out how to do this for an
69
+ # arbitrary set of lattice vectors is to fill the volume
70
+ # out along each edge of the super-cube and then eliminate duplicates
71
+ atoms = []
72
+ 3.times do |i|
73
+ # this vector is the number of repetitions in the unit cell
74
+ # to fill the volume out along the i-th edge of the super-cube
75
+ n_repeat = rep_mat.row(i)
76
+
77
+ # Give the proper sign to the repeat
78
+ nx = (n_repeat[0] < 0) ? n_repeat[0].floor-1 : n_repeat[0].ceil+1
79
+ ny = (n_repeat[1] < 0) ? n_repeat[1].floor-1 : n_repeat[1].ceil+1
80
+ nz = (n_repeat[2] < 0) ? n_repeat[2].floor-1 : n_repeat[2].ceil+1
81
+
82
+ atoms += bulk.repeat(nx, ny, nz).atoms.find_all{|a| volume.contains_point(a.x, a.y, a.z)}
83
+ end
84
+ Geometry.new(atoms.uniq)
85
+ end
86
+
87
+ # Return a unit cell for a slab of 001
88
+ # Specify the number of atomic monolayers,
89
+ # the vacuum thickness in angstrom,
90
+ # and the number of layers to constrain at the base of the slab
91
+ def get_001_surface(monolayers, vacuum, constrain_layers = 0)
92
+ anion = Atom.new(0,0,0,self.cation)
93
+ cation = Atom.new(0.25*self.lattice_const, 0.25*self.lattice_const, 0.25*self.lattice_const, self.anion)
94
+ v1 = Vector[0.5, 0.5, 0]*self.lattice_const
95
+ v2 = Vector[-0.5,0.5,0]*self.lattice_const
96
+ v3 = Vector[0.5, 0, 0.5]*self.lattice_const
97
+
98
+ zb = Geometry.new([anion, cation], [v1,v2,v3])
99
+ millerX = [1,0,0]
100
+ millerY = [0,1,0]
101
+ millerZ = [0,0,1]
102
+ zb.set_miller_indices(millerX, millerY, millerZ)
103
+
104
+ # Repeat the unit cell. The unit cell is a bi-layer so divide by 2
105
+ zb = zb.repeat(1,1,(monolayers/2).ceil)
106
+
107
+ if 0 < vacuum
108
+ # Add vacuum
109
+ monolayerSep = v3[2]/2
110
+ zb.lattice_vectors[2] = Vector[0, 0, monolayers*monolayerSep.abs + vacuum.to_f]
111
+ # Move everything into a nice tidy unit cell.
112
+ zb = zb.correct
113
+ end
114
+
115
+ minZ = zb.atoms.min{|a,b| a.z <=> b.z}.z
116
+
117
+ # Reject the top layer of atoms if an odd number of monolayers was requested.
118
+ # This is necessary because the primitive cell is a bilayer
119
+ zb.atoms.reject! {|a|
120
+ a.z >= (minZ + monolayerSep.abs*monolayers)
121
+ }
122
+
123
+ # Constrain the bottom layers
124
+ zb.atoms.each{|a|
125
+ if (a.z < minZ + monolayerSep.abs*constrain_layers)
126
+ a.constrain = ".true."
127
+ end
128
+ }
129
+
130
+ # Return the completed unit cell
131
+ return zb
132
+ end
133
+ alias_method :get_100_surface, :get_001_surface
134
+ alias_method :get_010_surface, :get_001_surface
135
+
136
+ # Return a unit cell for a slab of 111A (anion terminated)
137
+ # specify the number of atomic monolayers,
138
+ # the vacuum thickness in angstrom,
139
+ # and the number of layers to constrain at the base of the slab
140
+ def get_111A_surface(monolayers, vacuum, constrain_layers = 0)
141
+ # get the 111B surface, then reflect it about z=0
142
+ get_111_surface("A", monolayers, vacuum, constrain_layers)
143
+ end
144
+
145
+ # Return a unit cell for a slab of 111A (cation terminated)
146
+ # specify the number of atomic monolayers,
147
+ # the vacuum thickness in angstrom,
148
+ # and the number of layers to constrain at the base of the slab
149
+ def get_111B_surface(monolayers, vacuum, constrain_layers = 0)
150
+ get_111_surface("B", monolayers, vacuum, constrain_layers)
151
+ end
152
+
153
+ # Return a unit cell for a slab of 111
154
+ # dir is either "A" or "B" for the cation or anion terminated slab
155
+ # specify the number of atomic monolayers
156
+ # and the vacuum thickness in angstrom
157
+ def get_111_surface(dir, monolayers, vacuum, constrain_layers = 0)
158
+
159
+ if dir == "A"
160
+ top_atom = self.anion
161
+ bot_atom = self.cation
162
+ elsif dir == "B"
163
+ top_atom = self.cation
164
+ bot_atom = self.anion
165
+ else
166
+ raise "Direction must be either A or B"
167
+ end
168
+
169
+ # The atoms on a FCC
170
+ as1 = Atom.new(0.0, 0.0, 0.0, top_atom)
171
+ ga1 = Atom.new(0.0, 0.0, -sqrt(3)/4*self.lattice_const, bot_atom)
172
+
173
+ # The lattice Vectors
174
+ v1 = Vector[0.5*sqrt(2), 0.0, 0.0]*self.lattice_const
175
+ v2 = Vector[sqrt(2)*0.25, sqrt(6)*0.25, 0.0]*self.lattice_const
176
+ v3 = Vector[sqrt(2)*0.25, sqrt(2.0/3.0)*0.25, -1*sqrt(4.0/3.0)*0.5]*self.lattice_const
177
+
178
+ # The unit cell
179
+ zb = Geometry.new([as1, ga1], [v1, v2, v3])
180
+
181
+ # The Miller Indices
182
+ millerX = [-1, 1, 0] # Orientation of the crystal pointing in the cartesian +x axis
183
+ millerY = [1, 1, -2] # Orientation of the crystal pointing in the cartesian +y axis
184
+ millerZ = [-1, -1, -1] # Orientation of the crystal pointing in the cartesian +z axis
185
+
186
+ zb.set_miller_indices(millerX, millerY, millerZ)
187
+
188
+ # Repeat the unit cell and add vacuum
189
+ if 0 < vacuum
190
+ # We actually repeat the unit cell monolayers+1 times because
191
+ # I will strip off the top and bottom atoms to make the proper surface
192
+ zb = zb.repeat(1,1,monolayers+1)
193
+
194
+ bilayerSep = v3[2]
195
+ zb.lattice_vectors[2] = Vector[0, 0, monolayers*(bilayerSep.abs) + vacuum]
196
+
197
+ # Strip off the top and bottom atom
198
+ minZ = zb.atoms.min{|a,b| a.z <=> b.z}.z
199
+ maxZ = zb.atoms.max{|a,b| a.z <=> b.z}.z
200
+
201
+ zb.atoms.reject!{|a| a.z == maxZ}
202
+ zb.atoms.reject!{|a| a.z == minZ}
203
+
204
+ # Constrain the bottom layers if requested
205
+ if 0 < constrain_layers
206
+ # get the min again because we removed the atoms at minZ above
207
+ minZ = zb.atoms.min{|a,b| a.z <=> b.z}.z
208
+ constrain_below = minZ + bilayerSep.abs*constrain_layers
209
+ zb.atoms.each{|a|
210
+ if (a.z < constrain_below)
211
+ a.constrain = ".true."
212
+ end
213
+ }
214
+ end
215
+ end
216
+
217
+ zb
218
+ end
219
+
220
+ # return a unit cell for a slab of 112
221
+ # specify the number of atomic monolayers and the vacuum thickness in angstrom
222
+ def get_112_surface(monolayers, vacuum=0, constrain_layers = 0)
223
+ atom1 = Atom.new(0,0,0,self.cation)
224
+ atom2 = Atom.new(self.lattice_const*sqrt(3)/2, 0, 0, self.anion)
225
+
226
+ v1 = Vector[sqrt(3), 0, 0]*self.lattice_const
227
+ v2 = Vector[0, sqrt(2)/2, 0]*self.lattice_const
228
+ v3 = Vector[1/sqrt(3), 1/(sqrt(3)*2), -1/(sqrt(3)*2)]*self.lattice_const
229
+
230
+ millerX = Vector[1, 1, -2];
231
+ millerY = Vector[-1, 1, 0];
232
+ millerZ = Vector[-1, -1, -1]
233
+
234
+ # The unit cell
235
+ zb = Geometry.new([atom1, atom2], [v1, v2, v3])
236
+ zb.set_miller_indices(millerX, millerY, millerZ)
237
+
238
+ # Repeat the unit cell
239
+ zb = zb.repeat(1,1,monolayers)
240
+
241
+
242
+ if 0 < vacuum
243
+ # Add vacuum
244
+ monolayerSep = v3[2]
245
+ zb.lattice_vectors[2] = Vector[0, 0, (monolayers*monolayerSep).abs + vacuum.to_f]
246
+ # Move everything into a nice tidy unit cell.
247
+ zb = zb.correct
248
+ end
249
+
250
+ # # Constrain the bottom 2 layers
251
+ # zb.atoms.each{|a|
252
+ # if (a.z < monolayerSep*2)
253
+ # a.constrain = ".true."
254
+ # end
255
+ # }
256
+
257
+
258
+ # Return the completed unit cell
259
+ return zb
260
+ end
261
+
262
+
263
+ # Return a unit cell for a slab of 110
264
+ # specify the number of atomic monolayers
265
+ # and the vacuum thickness in angstrom
266
+ def get_110_surface(monolayers, vacuum=0, constrain_layers = 0)
267
+
268
+ # The atoms on a FCC
269
+ atom1 = Atom.new(0,0,0,self.cation)
270
+ atom2 = Atom.new(self.lattice_const*1/(2*sqrt(2)), self.lattice_const*0.25, 0.0, self.anion)
271
+
272
+ # The lattice Vectors
273
+ v1 = Vector[1/sqrt(2), 0.0, 0.0]*self.lattice_const
274
+ v2 = Vector[0.0, 1.0, 0.0]*self.lattice_const
275
+ v3 = Vector[1/(2*sqrt(2)), -0.5, 1/(2*sqrt(2))]*self.lattice_const
276
+
277
+ # The miller indices for each primitive cartesian direction
278
+ millerX = Vector[1, -1, 0]
279
+ millerY = Vector[0, 0, 1]
280
+ millerZ = Vector[1, 1, 0]
281
+
282
+ # The unit cell
283
+ zb = Geometry.new([atom1, atom2], [v1, v2, v3])
284
+ zb.set_miller_indices(millerX, millerY, millerZ)
285
+
286
+ # Repeat the unit cell
287
+ zb = zb.repeat(1,1,monolayers)
288
+
289
+
290
+ if 0 < vacuum
291
+ # Add vacuum
292
+ monolayerSep = v3[2]
293
+ zb.lattice_vectors[2] = Vector[0, 0, monolayers*monolayerSep.abs + vacuum.to_f]
294
+ # Move everything into a nice tidy unit cell.
295
+ zb = zb.correct
296
+ end
297
+
298
+ # # Constrain the bottom layers
299
+ zb.atoms.each{|a|
300
+ if (a.z < monolayerSep*constrain_layers)
301
+ a.constrain = ".true."
302
+ end
303
+ }
304
+
305
+
306
+ # Return the completed unit cell
307
+ return zb
308
+ end
309
+ end
310
+
311
+ end