crystalcell 0.0.0 → 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGES ADDED
@@ -0,0 +1,13 @@
1
+ = crystalcell Changelog
2
+
3
+ == Master (for 0.0.1)
4
+ * Change indent char from tab char to two spaces.
5
+ * Introduce module CrystalCell as new namespace.
6
+ * Add Cell#symmetry_operations and Cell#axis_independencies.
7
+ They depends on spglib.
8
+ * Getspg loading is changed.
9
+ * Adjust to mageo-0.0.2.
10
+ * Adjust to builtinextension-0.1.0
11
+
12
+ == Version 0.0.0
13
+ * Initial release.
data/Gemfile CHANGED
@@ -7,11 +7,12 @@ source "http://rubygems.org"
7
7
  # Include everything needed to run rake, tests, features, etc.
8
8
  group :development do
9
9
  gem "rdoc", "~> 3.12"
10
- gem "bundler", "~> 1.1.3"
10
+ gem "bundler", "~> 1.3.5"
11
11
  gem "jeweler", "~> 1.8.3"
12
12
  gem "simplecov", ">= 0"
13
- gem "mageo", ">= 0.0.0"
14
- gem "malge", ">= 0.0.1"
13
+ gem "malge", ">= 0.0.6"
14
+ gem "mageo", ">= 0.0.2"
15
15
  gem "maset", ">= 0.0.0"
16
- gem "builtinextension", ">= 0.0.3"
16
+ gem "builtinextension", ">= 0.1.0"
17
+ #gem "psych", ">= 0"
17
18
  end
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.0
1
+ 0.0.1
@@ -0,0 +1,80 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "crystalcell"
8
+ s.version = "0.0.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["ippei94da"]
12
+ s.date = "2013-04-18"
13
+ s.description = "This gem provides Cell, LatticeAxes, Atom classes, and so on.\n And this provides simple treatment of a periodic boundary condition.\n "
14
+ s.email = "ippei94da@gmail.com"
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ "CHANGES",
22
+ "Gemfile",
23
+ "LICENSE.txt",
24
+ "README.rdoc",
25
+ "Rakefile",
26
+ "VERSION",
27
+ "crystalcell.gemspec",
28
+ "lib/crystalcell.rb",
29
+ "lib/crystalcell/atom.rb",
30
+ "lib/crystalcell/cell.rb",
31
+ "lib/crystalcell/element.rb",
32
+ "lib/crystalcell/latticeaxes.rb",
33
+ "lib/crystalcell/periodiccell.rb",
34
+ "test/helper.rb",
35
+ "test/test_atom.rb",
36
+ "test/test_cell.rb",
37
+ "test/test_element.rb",
38
+ "test/test_latticeaxes.rb",
39
+ "test/test_periodiccell.rb"
40
+ ]
41
+ s.homepage = "http://github.com/ippei94da/crystalcell"
42
+ s.licenses = ["MIT"]
43
+ s.require_paths = ["lib"]
44
+ s.rubygems_version = "1.8.11"
45
+ s.summary = "Classes around a cell in crystallography"
46
+
47
+ if s.respond_to? :specification_version then
48
+ s.specification_version = 3
49
+
50
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
51
+ s.add_development_dependency(%q<rdoc>, ["~> 3.12"])
52
+ s.add_development_dependency(%q<bundler>, ["~> 1.3.5"])
53
+ s.add_development_dependency(%q<jeweler>, ["~> 1.8.3"])
54
+ s.add_development_dependency(%q<simplecov>, [">= 0"])
55
+ s.add_development_dependency(%q<malge>, [">= 0.0.6"])
56
+ s.add_development_dependency(%q<mageo>, [">= 0.0.2"])
57
+ s.add_development_dependency(%q<maset>, [">= 0.0.0"])
58
+ s.add_development_dependency(%q<builtinextension>, [">= 0.1.0"])
59
+ else
60
+ s.add_dependency(%q<rdoc>, ["~> 3.12"])
61
+ s.add_dependency(%q<bundler>, ["~> 1.3.5"])
62
+ s.add_dependency(%q<jeweler>, ["~> 1.8.3"])
63
+ s.add_dependency(%q<simplecov>, [">= 0"])
64
+ s.add_dependency(%q<malge>, [">= 0.0.6"])
65
+ s.add_dependency(%q<mageo>, [">= 0.0.2"])
66
+ s.add_dependency(%q<maset>, [">= 0.0.0"])
67
+ s.add_dependency(%q<builtinextension>, [">= 0.1.0"])
68
+ end
69
+ else
70
+ s.add_dependency(%q<rdoc>, ["~> 3.12"])
71
+ s.add_dependency(%q<bundler>, ["~> 1.3.5"])
72
+ s.add_dependency(%q<jeweler>, ["~> 1.8.3"])
73
+ s.add_dependency(%q<simplecov>, [">= 0"])
74
+ s.add_dependency(%q<malge>, [">= 0.0.6"])
75
+ s.add_dependency(%q<mageo>, [">= 0.0.2"])
76
+ s.add_dependency(%q<maset>, [">= 0.0.0"])
77
+ s.add_dependency(%q<builtinextension>, [">= 0.1.0"])
78
+ end
79
+ end
80
+
data/lib/crystalcell.rb CHANGED
@@ -0,0 +1,11 @@
1
+ require "rubygems"
2
+ require "mageo"
3
+
4
+ #include Mageo
5
+ module CrystalCell; end
6
+
7
+ require "crystalcell/latticeaxes.rb"
8
+ require "crystalcell/element.rb"
9
+ require "crystalcell/atom.rb"
10
+ require "crystalcell/cell.rb"
11
+ require "crystalcell/periodiccell.rb"
@@ -1,133 +1,130 @@
1
-
2
1
  raise "Atom class need ruby version later than 1.9." if RUBY_VERSION.to_f < 1.9
3
2
 
4
-
5
- require "rubygems"
6
- gem "mageo"
7
- require "mageo/vector3dinternal.rb"
8
-
9
3
  # Class for an atom in a cell.
10
4
  # This class doesn't have lattice information.
11
5
  # Forbid changes in internal information. When you want to change, you can make a new instance.
12
6
  # This class is not assumeed in a periodic condition.
13
- class Atom
14
- class TypeError < Exception ; end
15
-
16
- # Do not change :position to attr_accessor.
17
- # This must be Vector3DInternal instance.
18
- attr_reader :movable_flags
19
- attr_accessor :element, :name, :position
20
-
21
- # Arguments:
22
- # elements:
23
- # Identifier of a element.
24
- # This may be String like "Li", Integer like 0, or other class instances.
25
- # Why doesn't this class have concrete name of elements?
26
- # The reason is below:
27
- # - This class is assumed to be used in Cell class instances.
28
- # When we treat symmetry of the cell, the elements of atoms are not necessary information.
29
- # This specification enable highly abstract level of treatment of cells.
30
- # - POSCAR file of VASP (before ver.4 series) don't have element information.
31
- # This specification enable to use POSCAR directly without POTCAR or element indication.
32
- # position:
33
- # Coordinates of an atom.
34
- # This argument 'position' is mainly assumed as a internal coordinate.
35
- # name:
36
- # Identifier of an atom, e.g., "Li1" or "O23".
37
- # movable_flags:
38
- # Movable flags of an atom in each of three directions as a Array of three items.
39
- def initialize(element, position, name = nil, movable_flags = nil)
40
- #p position
41
- #p position.methods.sort
42
- raise TypeError, "Position doesn't have []: (#{position.inspect})" unless position.methods.include?(:[])
43
- raise TypeError, "Number of items in position is not 3: (#{position})" if position.size != 3
44
- raise TypeError, "Coordinate is not a Float: (#{position})" if position[0].class != Float
45
- raise TypeError, "Coordinate is not a Float: (#{position})" if position[1].class != Float
46
- raise TypeError, "Coordinate is not a Float: (#{position})" if position[2].class != Float
47
-
48
- @element = element
49
- set_position(position)
50
- @name = name
51
- @movable_flags = movable_flags
52
- end
53
-
54
- ## Class methods
55
-
56
- # Equivalence checking (class method version).
57
- # Return true when all the condition below are satisfied:
58
- # - same element identifier.
59
- # - difference of coordinates is within tolerance.
60
- # Note that distance cannot be used since this class doesn't have a lattice axes.
61
- # Imagine the shape of tolerant zone is a hexahedron.
62
- def self.equal_in_delta?(atom0, atom1, tol = 0.0)
63
- return false if atom0.element != atom1.element
64
- 3.times do |i|
65
- return false if ((atom0.position[i] - atom1.position[i]).abs > tol)
66
- end
67
- return true
68
- end
69
-
70
- ## Instance methods
71
-
72
- # Restricted equivalence checking.
73
- # Return true when all the condition below are satisfied:
74
- # - same element identifier.
75
- # - difference of coordinates is the same.
76
- # This method should be used after well-consideration.
77
- # Arbitrary coordinates are not generally the same like Float instances.
78
- # It is a good idea to use in test scripts.
79
- def ==(other)
80
- #pp self
81
- #pp other
82
- self.class.equal_in_delta?(self, other, 0.0)
83
- end
84
-
85
- # Equivalence checking (instance method version) .
86
- # Return true when all the condition below are satisfied:
87
- # - same element identifier.
88
- # - difference of coordinates is within tolerance.
89
- def equal_in_delta?(other, tol = 0.0)
90
- self.class.equal_in_delta?(self, other, tol)
91
- end
92
-
93
- #Return Vector3DInternal instance consist of coordinates between 0 and 1.
94
- def internal_coordinates
95
- result = @position.map{ |coord| coord - coord.floor }
96
- return Vector3DInternal[ *result ]
97
- end
98
-
99
- # Return Vector3DInternal instance consist of integers,
100
- # by which self.internal_coordinates translate to self.position.
101
- def translation_symmetry_operation
102
- result = @position.map{ |coord| coord.floor }
103
- return Vector3DInternal[ *result ]
104
- end
105
-
106
- # Overwrite position.
107
- def set_position(position)
108
- #pp position
109
- #pp position.class
110
- raise TypeError, "#{position.class}" if position.class == Vector3D
111
- @position = position.to_v3di
112
- end
113
-
114
- # Translate atom position (destructive).
115
- # Argument 'vec' should be given as internal vector,
116
- # which is an Array or Vector3DInternal instance but Vector3D instance.
117
- def translate!(vec)
118
- raise TypeError if vec.class == Vector3D
119
- raise TypeError if vec.size != 3
120
- self.set_position(@position + Vector3DInternal[ *vec ])
121
- end
122
-
123
- # Translate atom position (nondestructive).
124
- # Argument 'vec' should be given as internal vector,
125
- # which is an Array or Vector3DInternal instance but Vector3D instance.
126
- def translate(vec)
127
- tmp = Marshal.load(Marshal.dump(self))
128
- tmp.translate!(vec)
129
- return tmp
130
- end
7
+ class CrystalCell::Atom
8
+
9
+ include Mageo
10
+
11
+ class TypeError < Exception ; end
12
+
13
+ # Do not change :position to attr_accessor.
14
+ # This must be Vector3DInternal instance.
15
+ attr_reader :movable_flags
16
+ attr_accessor :element, :name, :position
17
+
18
+ # Arguments:
19
+ # elements:
20
+ # Identifier of a element.
21
+ # This may be String like "Li", Integer like 0, or other class instances.
22
+ # Why doesn't this class have concrete name of elements?
23
+ # The reason is below:
24
+ # - This class is assumed to be used in Cell class instances.
25
+ # When we treat symmetry of the cell, the elements of atoms are not necessary information.
26
+ # This specification enable highly abstract level of treatment of cells.
27
+ # - POSCAR file of VASP (before ver.4 series) don't have element information.
28
+ # This specification enable to use POSCAR directly without POTCAR or element indication.
29
+ # position:
30
+ # Coordinates of an atom.
31
+ # This argument 'position' is mainly assumed as a internal coordinate.
32
+ # name:
33
+ # Identifier of an atom, e.g., "Li1" or "O23".
34
+ # movable_flags:
35
+ # Movable flags of an atom in each of three directions as a Array of three items.
36
+ def initialize(element, position, name = nil, movable_flags = nil)
37
+ #p position
38
+ #p position.methods.sort
39
+ raise TypeError, "Position doesn't have []: (#{position.inspect})" unless position.methods.include?(:[])
40
+ raise TypeError, "Number of items in position is not 3: (#{position})" if position.size != 3
41
+ raise TypeError, "Coordinate is not a Float: (#{position})" if position[0].class != Float
42
+ raise TypeError, "Coordinate is not a Float: (#{position})" if position[1].class != Float
43
+ raise TypeError, "Coordinate is not a Float: (#{position})" if position[2].class != Float
44
+
45
+ @element = element
46
+ set_position(position)
47
+ @name = name
48
+ @movable_flags = movable_flags
49
+ end
50
+
51
+ ## Class methods
52
+
53
+ # Equivalence checking (class method version).
54
+ # Return true when all the condition below are satisfied:
55
+ # - same element identifier.
56
+ # - difference of coordinates is within tolerance.
57
+ # Note that distance cannot be used since this class doesn't have a lattice axes.
58
+ # Imagine the shape of tolerant zone is a hexahedron.
59
+ def self.equal_in_delta?(atom0, atom1, tol = 0.0)
60
+ return false if atom0.element != atom1.element
61
+ 3.times do |i|
62
+ return false if ((atom0.position[i] - atom1.position[i]).abs > tol)
63
+ end
64
+ return true
65
+ end
66
+
67
+ ## Instance methods
68
+
69
+ # Restricted equivalence checking.
70
+ # Return true when all the condition below are satisfied:
71
+ # - same element identifier.
72
+ # - difference of coordinates is the same.
73
+ # This method should be used after well-consideration.
74
+ # Arbitrary coordinates are not generally the same like Float instances.
75
+ # It is a good idea to use in test scripts.
76
+ def ==(other)
77
+ #pp self
78
+ #pp other
79
+ self.class.equal_in_delta?(self, other, 0.0)
80
+ end
81
+
82
+ # Equivalence checking (instance method version) .
83
+ # Return true when all the condition below are satisfied:
84
+ # - same element identifier.
85
+ # - difference of coordinates is within tolerance.
86
+ def equal_in_delta?(other, tol = 0.0)
87
+ self.class.equal_in_delta?(self, other, tol)
88
+ end
89
+
90
+ #Return Vector3DInternal instance consist of coordinates between 0 and 1.
91
+ def internal_coordinates
92
+ result = @position.map{ |coord| coord - coord.floor }
93
+ return Vector3DInternal[ *result ]
94
+ end
95
+
96
+ # Return Vector3DInternal instance consist of integers,
97
+ # by which self.internal_coordinates translate to self.position.
98
+ def translation_symmetry_operation
99
+ result = @position.map{ |coord| coord.floor }
100
+ return Vector3DInternal[ *result ]
101
+ end
102
+
103
+ # Overwrite position.
104
+ def set_position(position)
105
+ #pp position
106
+ #pp position.class
107
+ raise TypeError, "#{position.class}" if position.class == Vector3D
108
+ @position = position.to_v3di
109
+ end
110
+
111
+ # Translate atom position (destructive).
112
+ # Argument 'vec' should be given as internal vector,
113
+ # which is an Array or Vector3DInternal instance but Vector3D instance.
114
+ def translate!(vec)
115
+ raise TypeError if vec.class == Vector3D
116
+ raise TypeError if vec.size != 3
117
+ self.set_position(@position + Vector3DInternal[ *vec ])
118
+ end
119
+
120
+ # Translate atom position (nondestructive).
121
+ # Argument 'vec' should be given as internal vector,
122
+ # which is an Array or Vector3DInternal instance but Vector3D instance.
123
+ def translate(vec)
124
+ tmp = Marshal.load(Marshal.dump(self))
125
+ tmp.translate!(vec)
126
+ return tmp
127
+ end
131
128
 
132
129
  end
133
130
 
@@ -3,545 +3,625 @@
3
3
  require "pp"
4
4
  require "matrix"
5
5
 
6
- require "crystalcell/atom.rb"
7
- require "crystalcell/latticeaxes.rb"
8
-
9
6
  require "rubygems"
10
7
  gem "builtinextension"
11
- require "array_select_indices.rb"
12
-
13
- gem "mageo"
14
- require "mageo/vector3d.rb"
15
- require "mageo/vector3dinternal.rb"
8
+ require "array/selectindices.rb"
16
9
 
17
10
  gem "maset"
18
11
  require "maset/mapping.rb"
19
12
 
20
- # Class for crystal cell with lattice axes and atoms.
21
- # Symmetry operations are not considered in this class.
22
- # A sub class SymmetricCell can do, which overrides equal_in_delta methods.
23
- #
24
- # Written by Ippei Kishida [2010-12-19].
13
+ #pp Getspg.methods.sort
14
+ #pp Getspg
15
+ #include Getspg
16
+
17
+ #Class for crystal cell with lattice axes and atoms.
18
+ #Symmetry operations are not considered in this class.
19
+ #A sub class SymmetricCell can do, which overrides equal_in_delta methods.
20
+ #
21
+ #Written by Ippei Kishida [2010-12-19].
22
+ #
23
+ ##Cell
24
+ # セル内の原子は、内部的には配列として保持するが、
25
+ # この順序は制御できないものとする。
26
+ # たとえば Li, Ge, O の順序にソートされているなどと思ってはいけない。
27
+ # 順序に依存するプログラムを作ってはいけない。
28
+ #
29
+ ##Note:
30
+ #Cell クラスは元素情報をネイティブには持たない
31
+ # ボツ案:
32
+ # たとえば構成元素の情報を持ち、
33
+ # さらに Atom クラスインスタンスも持つとする。
34
+ # 原子の追加の仕方によっては、
35
+ # Atoms クラスの元素情報と矛盾するという状況は十分に考えられる。
25
36
  #
26
- ## Cell
27
- # セル内の原子は、内部的には配列として保持するが、
28
- # この順序は制御できないものとする。
29
- # たとえば Li, Ge, O の順序にソートされているなどと思ってはいけない。
30
- # 順序に依存するプログラムを作ってはいけない。
31
- #
32
- ## Note:
33
- # Cell クラスは元素情報をネイティブには持たない
34
- # ボツ案:
35
- # たとえば構成元素の情報を持ち、
36
- # さらに Atom クラスインスタンスも持つとする。
37
- # 原子の追加の仕方によっては、
38
- # Atoms クラスの元素情報と矛盾するという状況は十分に考えられる。
39
- #
40
- # 構成元素として Li があっても、
41
- # Li 原子リストが空リストだったらその元素はあると判定されるべきか、
42
- # 疑問が生じる。
43
- class Cell
44
-
45
- class NoAtomError < Exception; end
46
- class AxesMismatchError < Exception; end
47
- class AxesRangeError < Exception; end
48
- class SameAxesError < Exception; end
49
- class TypeError < Exception; end
50
- class ArgumentError < Exception; end # その他的エラー
51
-
52
- attr_reader :element_names, :atoms, :axes
53
- attr_accessor :comment
54
-
55
- # Argument 'axes' must have :to_a method and expressed in 3x3 Array.
56
- def initialize(axes, atoms = [])
57
- #raise CellTypeError unless axes.is_a?(Axes)
58
- if axes.class == LatticeAxes
59
- @axes = axes
60
- else
61
- @axes = LatticeAxes.new( axes.to_a )
62
- end
63
-
64
- atoms.each do |atom|
65
- #pp atom
66
- unless atom.is_a?(Atom)
67
- raise CellTypeError,
68
- "#{atom} is not a kind of Atom."
69
- end
70
- end
71
- @atoms = atoms
72
- end
73
-
74
- # セルに原子を追加する。
75
- def add_atom(atom)
76
- #raise "Cell::add_atom, 2nd argument must be Array." if pos.class != Array
77
- raise CellTypeError unless atom.is_a?(Atom)
78
- @atoms << atom
79
- end
80
-
81
- # Delete an atom from a cell.
82
- # i は Cell クラスが保持している原子の番号。
83
- # Cell クラスは原子を配列として保持しており、
84
- # その番号を指定すると考えると分かり易かろう。
85
- def delete_atom( i )
86
- #raise "Atom ID[#{i}] not exist" if @atoms[i] == nil
87
- @atoms.delete_at( i )
88
- end
89
-
90
- # 全ての原子の元素情報のリストを返す。
91
- # unique なものを抽出したりはしない。
92
- # unique なものが必要なら返り値に .uniq をつければ良い。
93
- # e.g., #=> ['Li', 'N', 'Li']
94
- # e.g., #=> [0, 1, 2, 1]
95
- def elements
96
- @atoms.collect{ |i| i.element }
97
- end
98
-
99
- # 全ての原子の位置情報のリストを返す。
100
- def positions
101
- @atoms.collect{ |i| i.position }
102
- end
103
-
104
- # 元素情報が elem の原子の index を配列にまとめて返す。
105
- # index は原子の永続的な id ではない。
106
- # Array#select は index ではなく要素そのものを配列にして返すので、少し違う。
107
- def select_indices( &block )
108
- return @atoms.select_indices( &block )
109
- end
110
-
111
- # Set element name to each atom in self.
112
- # Argument 'elems' is a list of new names, which has [] method. e.g.,
113
- # 1. Array, [ 'Li', 'O' ]
114
- # 2. Hash , { 0 => 'Li', 1 => 'O' ]
115
- # 3. Hash , { 'Li' => 'Na' }
116
- # 1. and 2. of the above examples induce the same result.
117
- # Case 1. can be convenient for element names of array from POTCAR.
118
- #
119
- # The atoms with the name which is not included the hash key do not change their names.
120
- def set_elements( elems )
121
- @atoms.each do |atom|
122
- begin
123
- new_elem = elems[ atom.element ]
124
- rescue
125
- next
126
- end
127
- next if new_elem == nil
128
- atom.element = new_elem
129
- end
130
- end
131
-
132
- #セルを拡張したスーパーセルを考えたとき、中に含まれる原子のリストを返す。
133
- #引数の意味は以下の通り。
134
- #a_min : a 軸方向のセルの方向を整数で示したときの最小値
135
- #a_max : a 軸方向のセルの方向を整数で示したときの最大値
136
- #b_min : b 軸方向のセルの方向を整数で示したときの最小値
137
- #b_max : b 軸方向のセルの方向を整数で示したときの最大値
138
- #c_min : c 軸方向のセルの方向を整数で示したときの最小値
139
- #c_max : c 軸方向のセルの方向を整数で示したときの最大値
140
- #-1, 1, -1, 1, -1, 1 と指定すれば 3x3x3 の 27倍体積の構造になる。
141
- def atoms_in_supercell( a_min, a_max, b_min, b_max, c_min, c_max )
142
- results = []
143
- @atoms.each do |atom|
144
- a_min.upto( a_max ) do |a|
145
- b_min.upto( b_max ) do |b|
146
- c_min.upto( c_max ) do |c|
147
- results << Atom.new( atom.element, (atom.position.to_v3di + [ a, b, c ].to_v3di).to_a )
148
- end
149
- end
150
- end
151
- end
152
- results
153
- end
154
-
155
- #他のセルと格子定数が等価であれば true を、そうでなければ false を返す。
156
- #other: 他のセル
157
- #length_ratio: 長さ(a, b, c) の許容値を比で指定
158
- #angle_tolerance: 角度(alpha, beta, gamma) の許容値を角度の値で指定
159
- def equal_lattice_in_delta?( other, length_ratio, angle_tolerance )
160
- @axes.equal_in_delta?(
161
- LatticeAxes.new( other.axes.to_a ), length_ratio, angle_tolerance
162
- )
163
- end
164
-
165
- #含まれる全原子が等価比較で一対一対応が付けられれば true を返す。
166
- #Cell に保持される順番に関係なく、等価な原子同士が一対一に対応づけられるかで
167
- #チェックする。
168
- def equal_atoms_in_delta?( other, position_tolerance )
169
- return false unless Mapping::map?(@atoms, other.atoms ){ |i,j| i.equal_in_delta?( j, position_tolerance ) }
170
- return true
171
- end
172
-
173
- # 等価判定。
174
- # 格子定数の長さの比率の許容値、格子定数の角度の許容値、原子座標の許容値。
175
- def equal_in_delta?( other, length_ratio, angle_tolerance, position_tolerance )
176
- return false unless equal_lattice_in_delta?(other, length_ratio, angle_tolerance)
177
- return false unless equal_atoms_in_delta?(other, position_tolerance)
178
- return true
179
- end
180
-
181
- # 等価判定。
182
- # 「==」による等価判定は実数の等価判定と同じく、基本的には使うべきではない。
183
- # しかし、これを定義しておくとテストが楽になることが多い。
184
- def ==( other )
185
- #pp axes;
186
- #pp other.axes;
187
-
188
- return false unless self.axes == other.axes #equal_in_delta( 0.0, 0.0, 0.0 ) とすると計算誤差でうまくいかないことがある。
189
- equal_atoms_in_delta?( other, 0.0 )
190
- end
191
-
192
- # 2つの地点間の距離を返す。
193
- # それぞれ、内部座標 Vector3DInternal クラスインスタンスなら絶対座標に変換される。
194
- # 絶対座標ならばそのまま計算する。
195
- # Vector3D Vector3DInternal 以外のクラスなら例外 Cell::TypeError を投げる。
196
- # 周期性を考慮したりはしない。
197
- # 周期性を考慮した距離は PeriodicCell#nearest_distance で行うべき。
198
- def distance( pos0, pos1 )
199
- if ((pos0.class != Vector3DInternal) && (pos0.class != Vector3D))
200
- raise Cell::TypeError
201
- end
202
- if ((pos1.class != Vector3DInternal) && (pos1.class != Vector3D))
203
- raise Cell::TypeError
204
- end
205
-
206
- v0 = pos0.to_v3d(@axes) if pos0.class == Vector3DInternal
207
- v1 = pos1.to_v3d(@axes) if pos1.class == Vector3DInternal
208
-
209
- (v0 - v1).r
210
- end
211
-
212
- # Dump string in POSCAR format.
213
- # Argument <io> can be a file handle or nil.
214
- # POSCAR を作るには、元素の順番を指定する必要があるので
215
- # それを element_order で指定している。
216
- # element_order の要素と == で一致する Atom instance を
217
- # それぞれ全て出力する。
218
- # e.g.,
219
- # cell.dump_poscar( STDOUT ) #=> output to stdout.
220
- # cell.dump_poscar( fileIo ) #=> output to file.
221
- # cell.dump_poscar( nil ) #=> return in String instance.
222
- # cell.dump_poscar #=> return in String instance.
223
- def dump_poscar( element_order, io = nil )
224
- if (io == nil)
225
- return create_poscar( element_order )
226
- else
227
- io.puts create_poscar( element_order )
228
- end
229
- end
230
-
231
- # Cell rotation.( Destructive method)
232
- # Argument 'matrix' is 3x3 Array of float.
233
- # This method does not modify the position to the range between 0 and 1,
234
- # even if it was out of range.
235
- def rotate!( matrix )
236
- @atoms.each { |atom|
237
- old_pos = atom.position
238
- new_pos = [0.0, 0.0, 0.0]
239
- 3.times do |y|
240
- 3.times do |x|
241
- new_pos[y] += (matrix[y][x] * old_pos[x])
242
- end
243
- end
244
- atom.set_position( new_pos )
245
- }
246
- end
247
-
248
- # Cell rotation.( Nondestructive method)
249
- # Argument 'matrix' is 3x3 Array of float.
250
- # This method does not modify the position to the range between 0 and 1,
251
- # even if it was out of range.
252
- def rotate( matrix )
253
- t = Marshal.load( Marshal.dump( self ) )
254
- t.rotate!( matrix )
255
- return t
256
- end
257
-
258
- #並進移動を行う破壊的メソッド。
259
- #ary は Float 3 要素の配列。
260
- def translate!( ary )
261
- @atoms.each { |atom| atom.translate!( ary ) }
262
- end
263
-
264
- #並進移動を行う非破壊的メソッド。
265
- #ary は Float 3 要素の配列。
266
- def translate( ary )
267
- t = Marshal.load( Marshal.dump( self ) )
268
- t.translate!( ary )
269
- return t
270
- end
271
-
272
- # Return arithmetic mean of atomic positions in an internal coordinates.
273
- # Raise 'Cell::NoAtomError' if no atoms included in self.
274
- def center_of_atoms
275
- raise Cell::NoAtomError if @atoms.size == 0
276
-
277
- vec = Vector3DInternal[ 0.0, 0.0, 0.0 ]
278
- @atoms.each { |i|
279
- 3.times { |j| vec[j] += i.position[j] }
280
- }
281
- vec *= 1.0/ @atoms.size
282
- end
283
-
284
- # Calculate volume.
285
- def calc_volume
286
- axes = @axes.to_a.map { |i| Vector3D[*i] }
287
- vA, vB, vC = axes[0..2]
288
- Vector3D.scalar_triple_product( vA, vB, vC ).abs
289
- end
290
-
291
- # Generate a new cell with the same lattice consants,
292
- # containing atoms of indicated elements.
293
- # Argument 'elems' must be an array of element names.
294
- # 含まれる @atoms の順序は、保存される。元素ごとに並び換えたりしない。
295
- # Atom.element elems の要素のどれかと完全一致しているもののみ対象となる。
296
- # サブクラスのインスタンスで実行した場合には、
297
- # サブクラスのインスタンスとして生成する。
298
- def cell_of_elements( elems )
299
- result = self.class.new( @axes )
300
- @atoms.each do |atom|
301
- result.add_atom(atom) if elems.include?( atom.element )
302
- end
303
- return result
304
- end
305
-
306
- # 格子定数の同じ2つのセルを合わせて、全ての原子が含まれる1つのセルを返す
307
- # 非破壊的メソッド。
308
- # 2つのセルの格子定数が異なれば例外 Cell::AxesMismatchError を発生させる。
309
- # 内部的には @atoms はレシーバの @atoms のあとに引数の @atoms を追加した形になる。
310
- # comment は空文字になる。
311
- # 原子座標の重複チェックなどは行わない。
312
- def unite( cell )
313
- #raise Cell::AxesMismatchError unless @axes == cell.axes
314
- result = Marshal.load( Marshal.dump( self ) )
315
- cell.atoms.each do |atom|
316
- result.add_atom(atom)
317
- end
318
- return result
319
- end
320
-
321
- # 任意の格子軸のベクトルを反転する破壊的メソッド。
322
- # 大まかなイメージとしては、
323
- # 格子軸の原点をセルを構成する8つの頂点のどれかに移動する操作と考えれば良い。
324
- # ただし厳密には、格子ベクトルは LatticeAxes.new によって triangulate されるため、
325
- # b 軸を反転させた時は a 軸も反転する。( b 軸の y成分を正にするため)
326
- # c 軸を反転させた時は a, b 軸も反転する。( c 軸の z成分を正にするため)
327
- # セルの形状、内部のモチーフは保存する。
328
- # 原子の絶対座標は移動せず、内部座標の表現が変わる。
329
- # 引数 axis_id は 0, 1, 2 のいずれかの値を取り、それぞれ x, y, z軸を表す。
330
- # x, y, z軸の関係は、右手系と左手系が入れ替わる。
331
- def inverse_axis!( axis_id )
332
- axis_id = axis_id.to_i
333
- raise Cell::AxesRangeError if ( axis_id < 0 || 2 < axis_id )
334
-
335
- axes = []
336
- 3.times do |i|
337
- if ( i == axis_id )
338
- axes << @axes[ i ] * (-1.0)
339
- else
340
- axes << @axes[ i ]
341
- end
342
- end
343
- @axes = LatticeAxes.new( axes )
344
-
345
- atoms = []
346
- @atoms.each do |atom|
347
- position = []
348
- 3.times do |i|
349
- if i == axis_id
350
- position[i] = atom.position[i] * (-1)
351
- else
352
- position[i] = atom.position[i]
353
- end
354
- end
355
- atom.position = Vector3DInternal[*position]
356
- end
357
- end
358
-
359
- #def inverse_axis!( axis_id )
360
- # axis_id = axis_id.to_i
361
- # raise Cell::AxesRangeError if ( axis_id < 0 || 2 < axis_id )
362
-
363
- # axes = []
364
- # 3.times do |i|
365
- # if ( i == axis_id )
366
- # axes << @axes[ i ] * (-1.0)
367
- # else
368
- # axes << @axes[ i ]
369
- # end
370
- # end
371
- # @axes = LatticeAxes.new( axes )
372
-
373
- # atoms = []
374
- # @atoms.each do |atom|
375
- # position = []
376
- # 3.times do |i|
377
- # if i == axis_id
378
- # position[i] = atom.position[i] * (-1)
379
- # else
380
- # position[i] = atom.position[i]
381
- # end
382
- # end
383
- # atom.position
384
- # atoms << Atom.new( atom.element, position, atom.name, atom.movable_flags )
385
- # end
386
- # @atoms = atoms
387
- #end
388
-
389
- # inverse_axis! の非破壊版。
390
- def inverse_axis( axis_id )
391
- result = Marshal.load( Marshal.dump( self ) )
392
- result.inverse_axis!( axis_id )
393
- return result
394
- end
395
-
396
- # 2つの格子ベクトルを交換する破壊的メソッド。
397
- # Argument 'axis_ids' must have 2 items of integer.
398
- # 0, 1, and 2 mean x, y, and z axes, respectively.
399
- # この範囲の整数でなければ例外 Cell::AxesRangeError.
400
- # axis_ids に含まれる 2つの数字が同じならば
401
- # 例外 Cell::SameAxesError.
402
- def exchange_axes!( axis_ids )
403
- raise ArgumentError if axis_ids.size != 2
404
- axis_ids.each{ |i| raise AxesRangeError if ( i < 0 || 2 < i ) }
405
- raise Cell::SameAxesError if ( axis_ids[0] == axis_ids[1] )
406
-
407
- # 格子定数を交換。
408
- axes = @axes.axes
409
- axes[ axis_ids[0]], axes[ axis_ids[1]] = axes[ axis_ids[1]], axes[ axis_ids[0]]
410
- @axes = LatticeAxes.new( axes )
411
-
412
- # 内部座標を交換。
413
- new_atoms = []
414
- @atoms.each do |atom|
415
- new_pos = atom.position
416
- new_pos[ axis_ids[0]], new_pos[ axis_ids[1]] =
417
- new_pos[ axis_ids[1]], new_pos[ axis_ids[0]]
418
- new_atoms << Atom.new( atom.element, new_pos, atom.name, atom.movable_flags )
419
- end
420
- end
421
-
422
- # exchange_axes! の非破壊版。
423
- def exchange_axes( axis_ids )
424
- result = Marshal.load( Marshal.dump( self ) )
425
- result.exchange_axes!( axis_ids )
426
- return result
427
- end
428
-
429
- # 鏡像となるセルに変換する破壊的メソッド。
430
- def reflect!
431
- axes = @axes.to_a
432
- axes[0][0] *= -1
433
- @axes = LatticeAxes.new( axes )
434
- end
435
-
436
- # 鏡像となるセルに変換する非破壊的メソッド。
437
- def reflect
438
- result = Marshal.load( Marshal.dump( self ) )
439
- result.reflect!
440
- return result
441
- end
442
-
443
- # rotation translation からなる操作(e.g., 対称操作)
444
- # を加えたセルを返す。
445
- def operate(rotation, translation)
446
- rotation = Matrix[*rotation]
447
- translation = translation.to_v3d
448
- new_atoms = atoms.map do |atom|
449
- position = atom.position.to_v3d([
450
- [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0],
451
- ])
452
- new_pos = (rotation * position + translation).to_a.to_v3di
453
- Atom.new(atom.element, new_pos, atom.name)
454
- end
455
- Cell.new(@axes, new_atoms)
456
- end
457
-
458
- private
459
-
460
- # POSCAR の内容の文字列を生成。
461
- # 文字列の配列ではなく、改行文字を含む1つの文字列である点に注意。
462
- #
463
- # VASP の挙動として、Selective dynamics 指定ありの時に
464
- # 原子に T or F 指定していなかったり 3要素に足りなかったりすれば、
465
- # error となって実行されない。
466
- # なので dump_poscar では Selective dynamics 指定が必要な時には
467
- # 全ての原子に T/F を記述する。
468
- # POSCAR から生成された Cell の場合は Selective dynamics がついていれば
469
- # 全ての原子に 3つの T/F が付いていることを前提としても良いだろう。
470
- # 原子を追加するとかで、一部の原子の movable_flags nil になっているときは、
471
- # デフォルト値として [ true, true, true ] を入れることにする。
472
- # nil ならば false を連想すると思うが、敢えて true で埋めている理由は、
473
- # Selective dynamics をつけていない状態で VASP は全方向に緩和する、
474
- # すなわち T T T と指定したことと同じになっているから。
475
- # 換言すればこのデフォルト値の設定は VASP に合わせた仕様ということになる。
476
- # VASP に由来する仕様が Cell クラスに持ち込まれていることになるが、
477
- # VASP へのインターフェイスである POSCAR ファイルへの書き出しに限定されるので
478
- # 他への影響はほとんどなく、気にしなくて良いだろう。
479
- def create_poscar( element_order )
480
- #element_order と elements が一対一対応していなければ raise
481
- raise "Cell::create_poscar, element_order mismatches to elements." if (! Mapping::map?( elements.uniq, element_order ){ |i, j| i == j } )
482
-
483
- results = []
484
- results << @comment
485
- results << "1.0" #scale
486
- 3.times do |i|
487
- results << sprintf( "%20.14f %20.14f %20.14f", @axes[i][0], @axes[i][1], @axes[i][2]
488
- )
489
- end
490
-
491
- ##collect information
492
- elem_list = Hash.new
493
- element_order.each do |elem|
494
- elem_list[ elem ] = @atoms.select{ |atom| atom.element == elem }
495
- end
496
-
497
- ##numbers of element atoms
498
- tmp = ''
499
- element_order.each do |elem|
500
- tmp += " #{elem_list[elem].size.to_s}"
501
- end
502
- results << tmp
503
-
504
- ##Selective dynamics
505
- ##どれか1つでも getMovableFlag が真であれば Selective dynamics をオンにする
506
- selective_dynamics = false
507
- @atoms.each do |atom|
508
- if atom.movable_flags
509
- selective_dynamics = true
510
- results << "Selective dynamics"
511
- break
512
- end
513
- end
514
-
515
- element_order.each do |elem|
516
- elem_list[ elem ].each do |atom|
517
- if atom.movable_flags
518
- selective_dynamics = true
519
- break
520
- end
521
- end
522
- break if selective_dynamics
523
- end
524
-
525
- results << "Direct" #now, Direct only
526
-
527
- ##positions of atoms
528
- element_order.each do |elem|
529
- elem_list[ elem ].each do |atom|
530
- tmp = sprintf( "%20.14f %20.14f %20.14f", * atom.position )
531
- if selective_dynamics
532
- if atom.movable_flags == nil
533
- tmp += " T T T"
534
- else
535
- atom.movable_flags.each do |mov|
536
- ( mov == true ) ? tmp += " T" : tmp += " F"
537
- end
538
- end
539
- end
540
- results << tmp
541
- end
542
- end
543
-
544
- results.join("\n")
545
- end
37
+ # 構成元素として Li があっても、
38
+ # Li 原子リストが空リストだったらその元素はあると判定されるべきか、
39
+ # 疑問が生じる。
40
+ class CrystalCell::Cell
41
+
42
+ begin
43
+ require "getspg.so"
44
+ include Getspg
45
+ rescue LoadError
46
+ #Do nothing.
47
+ #To use basic functions even in environments without spglib.
48
+ end
49
+
50
+ include Mageo
51
+
52
+ class NoAtomError < Exception; end
53
+ class AxesMismatchError < Exception; end
54
+ class AxesRangeError < Exception; end
55
+ class SameAxesError < Exception; end
56
+ class TypeError < Exception; end
57
+ class ArgumentError < Exception; end #その他的エラー
58
+ class NoSpglibError < Exception; end
59
+
60
+ attr_reader :element_names, :atoms, :axes
61
+ attr_accessor :comment
62
+
63
+ #Argument 'axes' must have :to_a method and expressed in 3x3 Array.
64
+ def initialize(axes, atoms = [])
65
+ #raise CellTypeError unless axes.is_a?(Axes)
66
+ if axes.class == CrystalCell::LatticeAxes
67
+ @axes = axes
68
+ else
69
+ @axes = CrystalCell::LatticeAxes.new( axes.to_a )
70
+ end
71
+
72
+ atoms.each do |atom|
73
+ #pp atom
74
+ unless atom.is_a?(CrystalCell::Atom)
75
+ raise CellTypeError,
76
+ "#{atom} is not a kind of CrystalCell::Atom."
77
+ end
78
+ end
79
+ @atoms = atoms
80
+ end
81
+
82
+ #セルに原子を追加する。
83
+ def add_atom(atom)
84
+ #raise "Cell::add_atom, 2nd argument must be Array." if pos.class != Array
85
+ raise CellTypeError unless atom.is_a?(CrystalCell::Atom)
86
+ @atoms << atom
87
+ end
88
+
89
+ #Delete an atom from a cell.
90
+ #i は Cell クラスが保持している原子の番号。
91
+ #Cell クラスは原子を配列として保持しており、
92
+ #その番号を指定すると考えると分かり易かろう。
93
+ def delete_atom( i )
94
+ #raise "CrystalCell::Atom ID[#{i}] not exist" if @atoms[i] == nil
95
+ @atoms.delete_at( i )
96
+ end
97
+
98
+ #全ての原子の元素情報のリストを返す。
99
+ #unique なものを抽出したりはしない。
100
+ #unique なものが必要なら返り値に .uniq をつければ良い。
101
+ #e.g., #=> ['Li', 'N', 'Li']
102
+ #e.g., #=> [0, 1, 2, 1]
103
+ def elements
104
+ @atoms.collect{ |i| i.element }
105
+ end
106
+
107
+ #全ての原子の位置情報のリストを返す。
108
+ def positions
109
+ @atoms.collect{ |i| i.position }
110
+ end
111
+
112
+ #元素情報が elem の原子の index を配列にまとめて返す。
113
+ #index は原子の永続的な id ではない。
114
+ #Array#select は index ではなく要素そのものを配列にして返すので、少し違う。
115
+ def select_indices( &block )
116
+ return @atoms.select_indices( &block )
117
+ end
118
+
119
+ #Set element name to each atom in self.
120
+ #Argument 'elems' is a list of new names, which has [] method. e.g.,
121
+ # 1. Array, [ 'Li', 'O' ]
122
+ # 2. Hash , { 0 => 'Li', 1 => 'O' ]
123
+ # 3. Hash , { 'Li' => 'Na' }
124
+ #1. and 2. of the above examples induce the same result.
125
+ #Case 1. can be convenient for element names of array from POTCAR.
126
+ #
127
+ #The atoms with the name which is not included the hash key do not change their names.
128
+ def set_elements( elems )
129
+ @atoms.each do |atom|
130
+ begin
131
+ new_elem = elems[ atom.element ]
132
+ rescue
133
+ next
134
+ end
135
+ next if new_elem == nil
136
+ atom.element = new_elem
137
+ end
138
+ end
139
+
140
+ #セルを拡張したスーパーセルを考えたとき、中に含まれる原子のリストを返す。
141
+ #引数の意味は以下の通り。
142
+ #a_min : a 軸方向のセルの方向を整数で示したときの最小値
143
+ #a_max : a 軸方向のセルの方向を整数で示したときの最大値
144
+ #b_min : b 軸方向のセルの方向を整数で示したときの最小値
145
+ #b_max : b 軸方向のセルの方向を整数で示したときの最大値
146
+ #c_min : c 軸方向のセルの方向を整数で示したときの最小値
147
+ #c_max : c 軸方向のセルの方向を整数で示したときの最大値
148
+ #-1, 1, -1, 1, -1, 1 と指定すれば 3x3x3 の 27倍体積の構造になる。
149
+ def atoms_in_supercell( a_min, a_max, b_min, b_max, c_min, c_max )
150
+ results = []
151
+ @atoms.each do |atom|
152
+ a_min.upto( a_max ) do |a|
153
+ b_min.upto( b_max ) do |b|
154
+ c_min.upto( c_max ) do |c|
155
+ results << CrystalCell::Atom.new( atom.element, (atom.position.to_v3di + [ a, b, c ].to_v3di).to_a )
156
+ end
157
+ end
158
+ end
159
+ end
160
+ results
161
+ end
162
+
163
+ #他のセルと格子定数が等価であれば true を、そうでなければ false を返す。
164
+ #other: 他のセル
165
+ #length_ratio: 長さ(a, b, c) の許容値を比で指定
166
+ #angle_tolerance: 角度(alpha, beta, gamma) の許容値を角度の値で指定
167
+ def equal_lattice_in_delta?( other, length_ratio, angle_tolerance )
168
+ @axes.equal_in_delta?(
169
+ CrystalCell::LatticeAxes.new( other.axes.to_a ), length_ratio, angle_tolerance
170
+ )
171
+ end
172
+
173
+ #含まれる全原子が等価比較で一対一対応が付けられれば true を返す。
174
+ #Cell に保持される順番に関係なく、等価な原子同士が一対一に対応づけられるかで
175
+ #チェックする。
176
+ def equal_atoms_in_delta?( other, position_tolerance )
177
+ return false unless Mapping::map?(@atoms, other.atoms ){ |i,j| i.equal_in_delta?( j, position_tolerance ) }
178
+ return true
179
+ end
180
+
181
+ #等価判定。
182
+ #格子定数の長さの比率の許容値、格子定数の角度の許容値、原子座標の許容値。
183
+ def equal_in_delta?( other, length_ratio, angle_tolerance, position_tolerance )
184
+ return false unless equal_lattice_in_delta?(other, length_ratio, angle_tolerance)
185
+ return false unless equal_atoms_in_delta?(other, position_tolerance)
186
+ return true
187
+ end
188
+
189
+ #等価判定。
190
+ #「==」による等価判定は実数の等価判定と同じく、基本的には使うべきではない。
191
+ #しかし、これを定義しておくとテストが楽になることが多い。
192
+ def ==( other )
193
+ #pp axes;
194
+ #pp other.axes;
195
+
196
+ return false unless self.axes == other.axes #equal_in_delta( 0.0, 0.0, 0.0 ) とすると計算誤差でうまくいかないことがある。
197
+ equal_atoms_in_delta?( other, 0.0 )
198
+ end
199
+
200
+ #2つの地点間の距離を返す。
201
+ #それぞれ、内部座標 Vector3DInternal クラスインスタンスなら絶対座標に変換される。
202
+ #絶対座標ならばそのまま計算する。
203
+ #Vector3D か Vector3DInternal 以外のクラスなら例外 Cell::TypeError を投げる。
204
+ #周期性を考慮したりはしない。
205
+ #周期性を考慮した距離は PeriodicCell#nearest_distance で行うべき。
206
+ def distance( pos0, pos1 )
207
+ if ((pos0.class != Vector3DInternal) && (pos0.class != Vector3D))
208
+ raise CrystalCell::Cell::TypeError
209
+ end
210
+ if ((pos1.class != Vector3DInternal) && (pos1.class != Vector3D))
211
+ raise CrystalCell::Cell::TypeError
212
+ end
213
+
214
+ v0 = pos0.to_v3d(@axes) if pos0.class == Vector3DInternal
215
+ v1 = pos1.to_v3d(@axes) if pos1.class == Vector3DInternal
216
+
217
+ (v0 - v1).r
218
+ end
219
+
220
+ #Dump string in POSCAR format.
221
+ #Argument <io> can be a file handle or nil.
222
+ #POSCAR を作るには、元素の順番を指定する必要があるので
223
+ #それを element_order で指定している。
224
+ #element_order の要素と == で一致する CrystalCell::Atom instance
225
+ #それぞれ全て出力する。
226
+ #e.g.,
227
+ # cell.dump_poscar( STDOUT ) #=> output to stdout.
228
+ # cell.dump_poscar( fileIo ) #=> output to file.
229
+ # cell.dump_poscar( nil ) #=> return in String instance.
230
+ # cell.dump_poscar #=> return in String instance.
231
+ def dump_poscar( element_order, io = nil )
232
+ if (io == nil)
233
+ return create_poscar( element_order )
234
+ else
235
+ io.puts create_poscar( element_order )
236
+ end
237
+ end
238
+
239
+ #Cell rotation.( Destructive method)
240
+ #Argument 'matrix' is 3x3 Array of float.
241
+ #This method does not modify the position to the range between 0 and 1,
242
+ #even if it was out of range.
243
+ def rotate!( matrix )
244
+ @atoms.each { |atom|
245
+ old_pos = atom.position
246
+ new_pos = [0.0, 0.0, 0.0]
247
+ 3.times do |y|
248
+ 3.times do |x|
249
+ new_pos[y] += (matrix[y][x] * old_pos[x])
250
+ end
251
+ end
252
+ atom.set_position( new_pos )
253
+ }
254
+ end
255
+
256
+ #Cell rotation.( Nondestructive method)
257
+ #Argument 'matrix' is 3x3 Array of float.
258
+ #This method does not modify the position to the range between 0 and 1,
259
+ #even if it was out of range.
260
+ def rotate( matrix )
261
+ t = Marshal.load( Marshal.dump( self ) )
262
+ t.rotate!( matrix )
263
+ return t
264
+ end
265
+
266
+ #並進移動を行う破壊的メソッド。
267
+ #ary は Float 3 要素の配列。
268
+ def translate!( ary )
269
+ @atoms.each { |atom| atom.translate!( ary ) }
270
+ end
271
+
272
+ #並進移動を行う非破壊的メソッド。
273
+ #ary は Float 3 要素の配列。
274
+ def translate( ary )
275
+ t = Marshal.load( Marshal.dump( self ) )
276
+ t.translate!( ary )
277
+ return t
278
+ end
279
+
280
+ #Return arithmetic mean of atomic positions in an internal coordinates.
281
+ #Raise 'Cell::NoAtomError' if no atoms included in self.
282
+ def center_of_atoms
283
+ raise CrystalCell::Cell::NoAtomError if @atoms.size == 0
284
+
285
+ vec = Vector3DInternal[ 0.0, 0.0, 0.0 ]
286
+ @atoms.each { |i|
287
+ 3.times { |j| vec[j] += i.position[j] }
288
+ }
289
+ vec *= 1.0/ @atoms.size
290
+ end
291
+
292
+ #Calculate volume.
293
+ def calc_volume
294
+ axes = @axes.to_a.map { |i| Vector3D[*i] }
295
+ vA, vB, vC = axes[0..2]
296
+ Vector3D.scalar_triple_product( vA, vB, vC ).abs
297
+ end
298
+
299
+ #Generate a new cell with the same lattice consants,
300
+ #containing atoms of indicated elements.
301
+ #Argument 'elems' must be an array of element names.
302
+ #含まれる @atoms の順序は、保存される。元素ごとに並び換えたりしない。
303
+ #CrystalCell::Atom.element elems の要素のどれかと完全一致しているもののみ対象となる。
304
+ #サブクラスのインスタンスで実行した場合には、
305
+ #サブクラスのインスタンスとして生成する。
306
+ def cell_of_elements( elems )
307
+ result = self.class.new( @axes )
308
+ @atoms.each do |atom|
309
+ result.add_atom(atom) if elems.include?( atom.element )
310
+ end
311
+ return result
312
+ end
313
+
314
+ #格子定数の同じ2つのセルを合わせて、全ての原子が含まれる1つのセルを返す
315
+ #非破壊的メソッド。
316
+ #2つのセルの格子定数が異なれば例外 Cell::AxesMismatchError を発生させる。
317
+ #内部的には @atoms はレシーバの @atoms のあとに引数の @atoms を追加した形になる。
318
+ #comment は空文字になる。
319
+ #原子座標の重複チェックなどは行わない。
320
+ def unite( cell )
321
+ #raise Cell::AxesMismatchError unless @axes == cell.axes
322
+ result = Marshal.load( Marshal.dump( self ) )
323
+ cell.atoms.each do |atom|
324
+ result.add_atom(atom)
325
+ end
326
+ return result
327
+ end
328
+
329
+ #任意の格子軸のベクトルを反転する破壊的メソッド。
330
+ #大まかなイメージとしては、
331
+ #格子軸の原点をセルを構成する8つの頂点のどれかに移動する操作と考えれば良い。
332
+ # ただし厳密には、格子ベクトルは LatticeAxes.new によって triangulate されるため、
333
+ # b 軸を反転させた時は a 軸も反転する。( b 軸の y成分を正にするため)
334
+ # c 軸を反転させた時は a, b 軸も反転する。( c 軸の z成分を正にするため)
335
+ #セルの形状、内部のモチーフは保存する。
336
+ #原子の絶対座標は移動せず、内部座標の表現が変わる。
337
+ #引数 axis_id は 0, 1, 2 のいずれかの値を取り、それぞれ x, y, z軸を表す。
338
+ #x, y, z軸の関係は、右手系と左手系が入れ替わる。
339
+ def inverse_axis!( axis_id )
340
+ axis_id = axis_id.to_i
341
+ raise CrystalCell::Cell::AxesRangeError if ( axis_id < 0 || 2 < axis_id )
342
+
343
+ axes = []
344
+ 3.times do |i|
345
+ if ( i == axis_id )
346
+ axes << @axes[ i ] * (-1.0)
347
+ else
348
+ axes << @axes[ i ]
349
+ end
350
+ end
351
+ @axes = CrystalCell::LatticeAxes.new( axes )
352
+
353
+ atoms = []
354
+ @atoms.each do |atom|
355
+ position = []
356
+ 3.times do |i|
357
+ if i == axis_id
358
+ position[i] = atom.position[i] * (-1)
359
+ else
360
+ position[i] = atom.position[i]
361
+ end
362
+ end
363
+ atom.position = Vector3DInternal[*position]
364
+ end
365
+ end
366
+
367
+ #def inverse_axis!( axis_id )
368
+ #axis_id = axis_id.to_i
369
+ #raise Cell::AxesRangeError if ( axis_id < 0 || 2 < axis_id )
370
+
371
+ #axes = []
372
+ #3.times do |i|
373
+ # if ( i == axis_id )
374
+ # axes << @axes[ i ] * (-1.0)
375
+ # else
376
+ # axes << @axes[ i ]
377
+ # end
378
+ #end
379
+ #@axes = CrystalCell::LatticeAxes.new( axes )
380
+
381
+ #atoms = []
382
+ #@atoms.each do |atom|
383
+ # position = []
384
+ # 3.times do |i|
385
+ # if i == axis_id
386
+ # position[i] = atom.position[i] * (-1)
387
+ # else
388
+ # position[i] = atom.position[i]
389
+ # end
390
+ # end
391
+ # atom.position
392
+ # atoms << CrystalCell::Atom.new( atom.element, position, atom.name, atom.movable_flags )
393
+ #end
394
+ #@atoms = atoms
395
+ #end
396
+
397
+ #inverse_axis! の非破壊版。
398
+ def inverse_axis( axis_id )
399
+ result = Marshal.load( Marshal.dump( self ) )
400
+ result.inverse_axis!( axis_id )
401
+ return result
402
+ end
403
+
404
+ #2つの格子ベクトルを交換する破壊的メソッド。
405
+ #Argument 'axis_ids' must have 2 items of integer.
406
+ #0, 1, and 2 mean x, y, and z axes, respectively.
407
+ #この範囲の整数でなければ例外 Cell::AxesRangeError.
408
+ #axis_ids に含まれる 2つの数字が同じならば
409
+ #例外 Cell::SameAxesError.
410
+ def exchange_axes!( axis_ids )
411
+ raise ArgumentError if axis_ids.size != 2
412
+ axis_ids.each{ |i| raise AxesRangeError if ( i < 0 || 2 < i ) }
413
+ raise CrystalCell::Cell::SameAxesError if ( axis_ids[0] == axis_ids[1] )
414
+
415
+ #格子定数を交換。
416
+ axes = @axes.axes
417
+ axes[ axis_ids[0]], axes[ axis_ids[1]] = axes[ axis_ids[1]], axes[ axis_ids[0]]
418
+ @axes = CrystalCell::LatticeAxes.new( axes )
419
+
420
+ #内部座標を交換。
421
+ new_atoms = []
422
+ @atoms.each do |atom|
423
+ new_pos = atom.position
424
+ new_pos[ axis_ids[0]], new_pos[ axis_ids[1]] =
425
+ new_pos[ axis_ids[1]], new_pos[ axis_ids[0]]
426
+ new_atoms << CrystalCell::Atom.new( atom.element, new_pos, atom.name, atom.movable_flags )
427
+ end
428
+ end
429
+
430
+ #exchange_axes! の非破壊版。
431
+ def exchange_axes( axis_ids )
432
+ result = Marshal.load( Marshal.dump( self ) )
433
+ result.exchange_axes!( axis_ids )
434
+ return result
435
+ end
436
+
437
+ #鏡像となるセルに変換する破壊的メソッド。
438
+ def reflect!
439
+ axes = @axes.to_a
440
+ axes[0][0] *= -1
441
+ @axes = CrystalCell::LatticeAxes.new( axes )
442
+ end
443
+
444
+ #鏡像となるセルに変換する非破壊的メソッド。
445
+ def reflect
446
+ result = Marshal.load( Marshal.dump( self ) )
447
+ result.reflect!
448
+ return result
449
+ end
450
+
451
+ #rotation と translation からなる操作(e.g., 対称操作)
452
+ #を加えたセルを返す。
453
+ def operate(rotation, translation)
454
+ rotation = Matrix[*rotation]
455
+ translation = translation.to_v3d
456
+ new_atoms = atoms.map do |atom|
457
+ position = atom.position.to_v3d([
458
+ [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0],
459
+ ])
460
+ new_pos = (rotation * position + translation).to_a.to_v3di
461
+ CrystalCell::Atom.new(atom.element, new_pos, atom.name)
462
+ end
463
+ CrystalCell::Cell.new(@axes, new_atoms)
464
+ end
465
+
466
+ #Return information of axes symmetry.
467
+ #E.g.,
468
+ # [true , true , true ] when a = b = c, like cubic
469
+ # [true , false, false] when a = b != c, like hexagonal, trigonal, tetragonal
470
+ # [false, true , false] (same as above)
471
+ # [false, false, true ] (same as above)
472
+ # [false, false, false] when a != b != c, like triclinic, monoclinic, orthorhombic
473
+ def axis_independencies(symprec, angle_tolerance)
474
+ rotations = symmetry_operations(symprec, angle_tolerance).map {|oper| oper[:rotation]}
475
+
476
+ results = [true, true, true]
477
+ rotations.each do |rot|
478
+ 3.times do |i|
479
+ 3.times do |j|
480
+ next if rot[i][j] == 0
481
+ next if i == j
482
+ results[i] = false
483
+ results[j] = false
484
+ end
485
+ end
486
+ end
487
+ return results
488
+ end
489
+
490
+ private
491
+
492
+ #Return rotations of symmetry operations.
493
+ def symmetry_operations(symprec, angle_tolerance)
494
+ #begin
495
+ # require "getspg.so"
496
+ #rescue LoadError
497
+ # raise LoadError,
498
+ # "LoadError: 'spglib' seems not to be installed into the system."
499
+ #end
500
+
501
+ unless defined? Getspg
502
+ raise NoSpglibError, "symmetry_operations() is called without spglib."
503
+ end
504
+
505
+ #pp lattice # => [[2.0, 0.0, 0.0], [1.2246063538223773e-16, 2.0, 0.0], [1.2246063538223773e-16, 1.2246063538223773e-16, 2.0]]
506
+ ##vasp の lattice 行と比べて転置しているのに注意。
507
+ #pp position #=>[[0.0, 0.0, 0.0], [0.5, 0.0, 0.0], [0.5, 0.5, 0.0], [0.5, 0.5, 0.5]]
508
+ #pp types #=>[1, 2, 3, 3]
509
+ #pp symprec #=>1.0e-05
510
+ #pp angle_tolerance #=>-1.0
511
+ axes_t = @axes.to_a.transpose
512
+
513
+ poss = positions.map {|pos| pos.to_a}
514
+
515
+ table = {}
516
+ types = elements.map do |elem|
517
+ table[elem] = ((table.size) +1) unless table.keys.include? elem
518
+ table[elem]
519
+ end
520
+
521
+ #pp axes_t, poss, types, symprec, angle_tolerance
522
+
523
+ spgnum, spg, hallnum, hall_symbol, t_mat, o_shift,
524
+ rotations, translations, wyckoffs =
525
+ #include Getspg
526
+ #pp Getspg.methods.sort
527
+ #Getspg::get_dataset(axes_t, poss, types, symprec, angle_tolerance)
528
+ get_dataset(axes_t, poss, types, symprec, angle_tolerance)
529
+
530
+ results = []
531
+ rotations.size.times do |index|
532
+ results << {
533
+ :rotation => rotations[index],
534
+ :translation => translations[index]
535
+ }
536
+ end
537
+ return results
538
+ end
539
+
540
+ #POSCAR の内容の文字列を生成。
541
+ #文字列の配列ではなく、改行文字を含む1つの文字列である点に注意。
542
+ #
543
+ #VASP の挙動として、Selective dynamics 指定ありの時に
544
+ #原子に T or F 指定していなかったり 3要素に足りなかったりすれば、
545
+ #error となって実行されない。
546
+ #なので dump_poscar では Selective dynamics 指定が必要な時には
547
+ #全ての原子に T/F を記述する。
548
+ #POSCAR から生成された Cell の場合は Selective dynamics がついていれば
549
+ #全ての原子に 3つの T/F が付いていることを前提としても良いだろう。
550
+ #原子を追加するとかで、一部の原子の movable_flags が nil になっているときは、
551
+ #デフォルト値として [ true, true, true ] を入れることにする。
552
+ #nil ならば false を連想すると思うが、敢えて true で埋めている理由は、
553
+ #Selective dynamics をつけていない状態で VASP は全方向に緩和する、
554
+ #すなわち T T T と指定したことと同じになっているから。
555
+ #換言すればこのデフォルト値の設定は VASP に合わせた仕様ということになる。
556
+ #VASP に由来する仕様が Cell クラスに持ち込まれていることになるが、
557
+ #VASP へのインターフェイスである POSCAR ファイルへの書き出しに限定されるので
558
+ #他への影響はほとんどなく、気にしなくて良いだろう。
559
+ def create_poscar( element_order )
560
+ #element_order と elements が一対一対応していなければ raise
561
+ raise "Cell::create_poscar, element_order mismatches to elements." if (! Mapping::map?( elements.uniq, element_order ){ |i, j| i == j } )
562
+
563
+ results = []
564
+ results << @comment
565
+ results << "1.0" #scale
566
+ 3.times do |i|
567
+ results << sprintf( "%20.14f %20.14f %20.14f", @axes[i][0], @axes[i][1], @axes[i][2]
568
+ )
569
+ end
570
+
571
+ ##collect information
572
+ elem_list = Hash.new
573
+ element_order.each do |elem|
574
+ elem_list[ elem ] = @atoms.select{ |atom| atom.element == elem }
575
+ end
576
+
577
+ ##numbers of element atoms
578
+ tmp = ''
579
+ element_order.each do |elem|
580
+ tmp += " #{elem_list[elem].size.to_s}"
581
+ end
582
+ results << tmp
583
+
584
+ ##Selective dynamics
585
+ ##どれか1つでも getMovableFlag が真であれば Selective dynamics をオンにする
586
+ selective_dynamics = false
587
+ @atoms.each do |atom|
588
+ if atom.movable_flags
589
+ selective_dynamics = true
590
+ results << "Selective dynamics"
591
+ break
592
+ end
593
+ end
594
+
595
+ element_order.each do |elem|
596
+ elem_list[ elem ].each do |atom|
597
+ if atom.movable_flags
598
+ selective_dynamics = true
599
+ break
600
+ end
601
+ end
602
+ break if selective_dynamics
603
+ end
604
+
605
+ results << "Direct" #now, Direct only
606
+
607
+ ##positions of atoms
608
+ element_order.each do |elem|
609
+ elem_list[ elem ].each do |atom|
610
+ tmp = sprintf( "%20.14f %20.14f %20.14f", * atom.position )
611
+ if selective_dynamics
612
+ if atom.movable_flags == nil
613
+ tmp += " T T T"
614
+ else
615
+ atom.movable_flags.each do |mov|
616
+ ( mov == true ) ? tmp += " T" : tmp += " F"
617
+ end
618
+ end
619
+ end
620
+ results << tmp
621
+ end
622
+ end
623
+
624
+ results.join("\n")
625
+ end
546
626
 
547
627
  end