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.
@@ -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