aims_project 0.3.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,95 @@
1
+ #
2
+ # copyright 2012, Joshua Shapiro
3
+ # joshua.shapiro@gmail.com
4
+ #
5
+ # This file is part of AimsProjectManager.
6
+ #
7
+ # AimsProjectManager is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU General Public License as published by
9
+ # the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # AimsProjectManager is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU General Public License for more details.
16
+ #
17
+ #
18
+ module Aims
19
+
20
+ # Adds display behavior to atoms.
21
+ class Atom
22
+
23
+ @@black = nil
24
+ @@white = nil
25
+ @@blue = nil
26
+ @@red = nil
27
+ @@green = nil
28
+ @@dark = nil
29
+ @@light = nil
30
+ @@yellow = nil
31
+ @@orange = nil
32
+ @@graypurple = nil
33
+
34
+ def material
35
+ unless @@yellow
36
+ @@yellow = AimsProject::Material.new(1,1,0,1)
37
+ end
38
+
39
+ unless @@black
40
+ @@lback = AimsProject::Material.new(0,0,0,1)
41
+ end
42
+
43
+ unless @@white
44
+ @@lback = AimsProject::Material.new(1,1,1,1)
45
+ end
46
+
47
+ unless @@blue
48
+ @@blue = AimsProject::Material.new(0,0,1,1)
49
+ end
50
+
51
+ unless @@red
52
+ @@red = AimsProject::Material.new(1,0.4,0.4,1)
53
+ end
54
+
55
+ unless @@green
56
+ @@green = AimsProject::Material.new(0,1,0,1)
57
+ end
58
+
59
+ unless @@orange
60
+ @@orange = AimsProject::Material.new(1, 0.5, 0, 1)
61
+ end
62
+
63
+ unless @@graypurple
64
+ @@graypurple = AimsProject::Material.new(0.45, 0.55, 0.55, 1)
65
+ end
66
+
67
+ unless @@dark
68
+ @@dark = AimsProject::Material.new(0.2,0.2,0.2,1)
69
+ end
70
+
71
+ unless @@light
72
+ @@light = AimsProject::Material.new(1,1,1,1)
73
+ end
74
+
75
+ case self.species
76
+ when /Ga/
77
+ @@dark
78
+ when /As/
79
+ @@light
80
+ when /In/
81
+ @@graypurple
82
+ when /P/
83
+ @@orange
84
+ when /Si/
85
+ @@yellow
86
+ when /C/
87
+ @@red
88
+ else
89
+ @@blue
90
+ end
91
+ end
92
+ end
93
+
94
+
95
+ end
@@ -0,0 +1,406 @@
1
+
2
+ require 'fileutils'
3
+ require 'erb'
4
+ require 'date'
5
+
6
+ #
7
+ # A calculation is a combination of a geometry, a control file, and an output
8
+ # Each calculation runs in its own directory
9
+ # A special file named .calc_status in the calculation directory reveals the status of the calculation
10
+ # The format of this file is still under consideration.
11
+ # possible options are:
12
+ # • A single word indicating the status
13
+ # • A single word on the first line and unstructured text following (for comments, history, errors)
14
+ # • YAML
15
+ #
16
+ # The calculation progresses through the following state machine
17
+ # STAGED:
18
+ # This is the initial stage, before the calculation has been
19
+ # moved to the computation server and queued for execution.
20
+ # Valid inputs are CANCEL, ENQUEUE
21
+ # QUEUED:
22
+ # Calculation is uploaded to computation server and queued for execution.
23
+ # Valid inputs are CANCEL, PROGRESS
24
+ # RUNNING:
25
+ # Calculation is in progress.
26
+ # Valid inputs are CANCEL
27
+ # COMPLETE:
28
+ # Calculation is completed
29
+ # No valid inputs
30
+ # ABORTED:
31
+ module AimsProject
32
+
33
+ class Calculation
34
+
35
+ # The name of the geometry file
36
+ attr_accessor :geometry
37
+
38
+ # The name of the control file
39
+ attr_accessor :control
40
+
41
+ # The current calculation status
42
+ attr_accessor :status
43
+
44
+ # The calculation subdirectory, (can be nil)
45
+ attr_accessor :calc_subdir
46
+
47
+ # An array of (#to_s) items that are the calculation history
48
+ attr_accessor :history
49
+
50
+ # Timestamp indicating creation of this calculation
51
+ attr_writer :created_at
52
+ def created_at
53
+ @created_at = cast_as_date(@created_at)
54
+ end
55
+
56
+ # Timestamp indicating last update of this calculation
57
+ # (currently only updates when saved)
58
+ attr_writer :updated_at
59
+ def updated_at
60
+ # Cast value to Date
61
+ # Do this in the accessor because loading from YAML bypasses the setter method
62
+ @updated_at = cast_as_date(@updated_at)
63
+ end
64
+
65
+ def cast_as_date(obj)
66
+ if obj.is_a? Date or obj.is_a? Time or obj.is_a? DateTime
67
+ obj.to_datetime
68
+ elsif obj.is_a? String
69
+ DateTime.parse(obj)
70
+ # unless s
71
+ # s = DateTime.strptime(obj, "%F %T %z")
72
+ # end
73
+ # unless s
74
+ # s = DateTime.strptime(obj, "%FT%s%z")
75
+ # end
76
+ # s
77
+ else
78
+ DateTime.new(0)
79
+ end
80
+ end
81
+
82
+ # Find all calculations in the current directory
83
+ # with a given status
84
+ def Calculation.find_all(status)
85
+ # Catch all root calculations
86
+ calculations = Dir.glob File.join(AimsProject::CALCULATION_DIR, "*", AimsProject::CALC_STATUS_FILENAME)
87
+ # Catch all sub calculations
88
+ calculations << Dir.glob(File.join(AimsProject::CALCULATION_DIR, "*", "*", AimsProject::CALC_STATUS_FILENAME))
89
+ calculations.collect{|calc_status_file|
90
+ calc_dir = File.dirname(calc_status_file)
91
+ calc = Calculation.load(calc_dir)
92
+ if (status == calc.status)
93
+ calc
94
+ else
95
+ nil
96
+ end
97
+ }.compact
98
+ end
99
+
100
+ # Load a calculation from the serialized yaml file in the given directory
101
+ # raises an *ObjectFileNotFoundException* if the specified directory does
102
+ # not contain a yaml serialization of the calculation.
103
+ # raises a *CorruptObjectFileException* if the yaml file cannot be de-serialized
104
+ def Calculation.load(dir)
105
+
106
+ calc_file = File.join(dir, AimsProject::CALC_STATUS_FILENAME)
107
+ raise ObjectFileNotFoundException.new(calc_file) unless File.exists?(calc_file)
108
+
109
+ f = File.open(calc_file, 'r')
110
+ calc_obj = YAML.load(f)
111
+ f.close
112
+
113
+ raise CorruptObjectFileException.new(calc_file) unless calc_obj
114
+ return calc_obj
115
+ end
116
+
117
+ # Create a calculation and the corresponding
118
+ # directory structure given a geometry and a control
119
+ # This method will search for the files geometry.#{geometry}.in
120
+ # and control.#{control}.in in the project directory, then
121
+ # create a calculation directory that is the merger of those two
122
+ # filenames, and finally copy the geometry and control files into
123
+ # the calculation directory and rename them geometry.in and control.in
124
+ # @param [String] geometry The filename of the geometry file to use to initialize the calculation
125
+ # @param [String] control The filename of the control file to use to initialize the calculation
126
+ # @param [Hash<Symbol, Object>] user_vars A symbol=>Object hash of variables that will be available when
127
+ # evaluating the geometry and control files using embedded ruby
128
+ # This hash is also used to generate a calculation subdirectory
129
+ def Calculation.create(project, geometry, control, user_vars = {})
130
+
131
+ calc = Calculation.new(geometry, control)
132
+ calc.created_at = Time.new
133
+
134
+ control_in = calc.control_file
135
+ geometry_in = calc.geometry_file
136
+
137
+ # Define the calculation sub-directory if user_vars exists
138
+ unless user_vars.empty?
139
+ calc.calc_subdir = user_vars.keys.collect{|k| (k.to_s + "=" + user_vars[k].to_s).gsub('@', '').gsub(' ','_') }.join("..")
140
+ end
141
+
142
+ # Add configuration variables to the calculation binding
143
+ uvars_file = File.join(AimsProject::CONFIG_DIR, "user_variables.rb")
144
+ calc.get_binding.eval(File.read(uvars_file)) if File.exists?(uvars_file)
145
+
146
+ # Merge project variables into calcuation binding
147
+ if project
148
+ project.instance_variables.each{|v|
149
+ if v == :@name # Ignore the project name
150
+ calc.instance_variable_set(:@project_name, project.instance_variable_get(v))
151
+ else
152
+ calc.instance_variable_set(v, project.instance_variable_get(v))
153
+ end
154
+ }
155
+ end
156
+
157
+ # Merge user-vars to the calculation binding
158
+ user_vars.each_pair{|sym, val|
159
+ calc.instance_variable_set(sym, val)
160
+ }
161
+
162
+
163
+ # Check file existence
164
+ raise "Unable to locate #{control_in}" unless File.exists?(control_in)
165
+ raise "Unable to locate #{geometry_in}" unless File.exists?(geometry_in)
166
+
167
+ # Validate the files
168
+ raise "#{geometry_in} has changed since last use" unless check_version(geometry_in)
169
+ raise "#{control_in} has changed since last use" unless check_version(control_in)
170
+
171
+ # Validate that the directory doesn't already exist
172
+ if Dir.exists? calc.calculation_directory
173
+ raise "Could not create calculation.\n #{calc.calculation_directory} already exists. \n\n If you really want to re-create this calculation, then manually delete it and try again. \n"
174
+ end
175
+ FileUtils.mkdir_p calc.calculation_directory
176
+
177
+ erb = ERB.new(File.read(control_in))
178
+ File.open File.join(calc.calculation_directory, "control.in"), "w" do |f|
179
+ f.puts erb.result(calc.get_binding)
180
+ end
181
+
182
+ erb = ERB.new(File.read(geometry_in))
183
+ File.open File.join(calc.calculation_directory, "geometry.in"), "w" do |f|
184
+ f.puts erb.result(calc.get_binding)
185
+ end
186
+
187
+
188
+ calc.status = AimsProject::STAGED
189
+ calc.save
190
+
191
+ return calc
192
+ end
193
+
194
+ # Get the binding for this calculation
195
+ def get_binding
196
+ binding()
197
+ end
198
+
199
+ # The name of this calculation
200
+ def name
201
+ "#{geometry}.#{control}"
202
+ end
203
+
204
+ # Intended to replace @geometry, but needs lots of regression testing
205
+ # for now, will just generate this on the fly
206
+ def input_geometry
207
+ unless @actual_geometry
208
+ @input_geometry = Aims::GeometryParser.parse(File.join(self.calculation_directory, "geometry.in"))
209
+ end
210
+ @input_geometry
211
+ end
212
+
213
+ # Set the status to HOLD.
214
+ # Only possible if status is currently STAGED
215
+ def hold
216
+ if STAGED == status
217
+ self.status = HOLD
218
+ save
219
+ return true
220
+ else
221
+ return false
222
+ end
223
+ end
224
+
225
+ # Set the status to STAGED if current status is HOLD
226
+ def release
227
+ if HOLD == status
228
+ self.status = STAGED
229
+ save
230
+ return true
231
+ else
232
+ return false
233
+ end
234
+ end
235
+
236
+ # Create a new calculation that will restart a geometry relaxation
237
+ # calculation using the last available geometry.
238
+ # This method will generate a new file in
239
+ # the geometry directory with the extension +restartN+, where
240
+ # N will be incremented if the filename already exists.
241
+ # A new calculation will be created with the new geometry and
242
+ # the original control.
243
+ def restart_relaxation
244
+
245
+ # Create a new geometry file
246
+ geometry_orig = self.geometry_file
247
+
248
+ # Append the subdirectory if a subdir
249
+ if @calc_subdir
250
+ geometry_orig = geometry_orig + ".#{calc_subdir}"
251
+ end
252
+
253
+ # If restarting a restart, then increment the digit
254
+ if (geometry_orig.split(".").last =~ /restart(\d+)/)
255
+ n = $1.to_i
256
+ geometry_orig = geometry_orig.split(".")[0...-1].join(".")
257
+ else
258
+ n = 0
259
+ end
260
+
261
+ begin
262
+ n += 1
263
+ geometry_new = geometry_orig + ".restart#{n}"
264
+ end while File.exist?(geometry_new)
265
+
266
+ File.open(geometry_new, 'w') do |f|
267
+ f.puts "# Final geometry from #{calculation_directory}"
268
+ f.puts self.geometry_next_step.format_geometry_in
269
+ end
270
+
271
+ Calculation.create(nil, geometry_new, self.control)
272
+
273
+ end
274
+
275
+ # Initialize a new calculation. Consider using Calculation#create
276
+ # to generate the directory structure as well.
277
+ # @param [String] geometry the filename of the input geometry
278
+ # @param [String] control the filename of the input control
279
+ def initialize(geometry, control)
280
+ self.geometry = File.basename(geometry)
281
+ self.control = File.basename(control)
282
+ self.history = Array.new
283
+ end
284
+
285
+ # Serialize this calculation as a yaml file
286
+ def save(dir = nil)
287
+ self.updated_at = Time.new
288
+ dir = calculation_directory unless dir
289
+ File.open(File.join(dir, AimsProject::CALC_STATUS_FILENAME), 'w') do |f|
290
+ f.puts YAML.dump(self)
291
+ end
292
+ end
293
+
294
+ # Reload this calculation from the serialized YAML file
295
+ def reload
296
+ c = Calculation.load(self.calculation_directory)
297
+ self.geometry = c.geometry
298
+ @input_geometry = c.input_geometry
299
+ self.control = c.control
300
+ self.status = c.status
301
+ return self
302
+ end
303
+
304
+ # Determine the name of the control.in file from the
305
+ # control variable.
306
+ def control_file
307
+ File.join(AimsProject::CONTROL_DIR, self.control)
308
+ end
309
+
310
+ # Determine the name of the geometr.in file from the
311
+ # geometry variable
312
+ def geometry_file
313
+ File.join(AimsProject::GEOMETRY_DIR, self.geometry)
314
+ end
315
+
316
+ #
317
+ # Check for the existence of a cached version of the input file
318
+ # If it exists, check if the cached version is the same
319
+ # as the working version, and return true if they are, false if they are not.
320
+ # If the cached version does not exist, then cache the working version and return true.
321
+ def Calculation.check_version(file)
322
+ cache_dir = ".input_cache"
323
+ unless File.exists? cache_dir
324
+ Dir.mkdir cache_dir
325
+ Dir.mkdir File.join(cache_dir, AimsProject::GEOMETRY_DIR)
326
+ Dir.mkdir File.join(cache_dir, AimsProject::CONTROL_DIR)
327
+ end
328
+
329
+ return false unless File.exists?(file)
330
+ cache_version = File.join(cache_dir, file)
331
+ if File.exists?(cache_version)
332
+ return FileUtils.compare_file(file, cache_version)
333
+ else
334
+ FileUtils.cp_r file, cache_version
335
+ return true
336
+ end
337
+
338
+ end
339
+
340
+ # The path of this calculation relative to the project
341
+ def relative_path
342
+ calculation_directory
343
+ end
344
+
345
+ #
346
+ # Return the directory for this calculation
347
+ #
348
+ def calculation_directory
349
+ File.join AimsProject::CALCULATION_DIR, self.name, (@calc_subdir || "")
350
+ end
351
+
352
+ def load_output(output_pattern = "*output*")
353
+ output_files = Dir.glob(File.join(calculation_directory, output_pattern))
354
+ if output_files.empty?
355
+ @output = nil
356
+ else
357
+ @output = Aims::OutputParser.parse(output_files.last)
358
+ end
359
+ end
360
+
361
+ # Search the calculation directory for the calculation output.
362
+ # If found, parse it and return the Aims::AimsOutput object, otherwise
363
+ # return nil.
364
+ # If multiple output files are found, use the last one in the list
365
+ # when sorted alpha-numerically. (This is assumed to be the most recent calculation)
366
+ def output(output_pattern = "*output*")
367
+ unless @output
368
+ load_output(output_pattern)
369
+ end
370
+ @output
371
+ end
372
+
373
+ # Parse the calculation output and return the final geometry
374
+ # of this calculation. Return nil if no output is found.
375
+ def final_geometry
376
+ # ouput is not cached, so we only retrieve it once
377
+ o = self.output
378
+ if o
379
+ o.final_geometry
380
+ else
381
+ nil
382
+ end
383
+ end
384
+
385
+ # Parse the geometry.in.next_step file in the calculation directory
386
+ # if it exists and return the Aims::Geometry object or nil
387
+ def geometry_next_step
388
+ g_file = File.join(calculation_directory, "geometry.in.next_step")
389
+ if File.exists?(g_file)
390
+ Aims::GeometryParser.parse(g_file)
391
+ else
392
+ nil
393
+ end
394
+ end
395
+
396
+ # Return whether this calculation is converged or not
397
+ def converged?
398
+ if output.nil?
399
+ false
400
+ else
401
+ output.geometry_converged
402
+ end
403
+ end
404
+
405
+ end
406
+ end