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/plane.rb
ADDED
@@ -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
|
data/lib/aims/volume.rb
ADDED
@@ -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
|