aims_project_windows 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,65 @@
1
+ module AimsProject
2
+
3
+ class CalculationTree < Wx::ScrolledWindow
4
+
5
+ include Wx
6
+
7
+ attr_accessor :app, :treeControl
8
+
9
+ def initialize(app, window)
10
+ super(window)
11
+ self.app = app
12
+
13
+ init_tree
14
+
15
+ sizer = BoxSizer.new(VERTICAL)
16
+ sizer.add(self.treeControl, 1, EXPAND | ALL, 5)
17
+
18
+ set_auto_layout(true)
19
+ set_sizer(sizer)
20
+ end
21
+
22
+ def init_tree
23
+ @treeControl = Wx::TreeCtrl.new(self)
24
+ root = self.treeControl.add_root("-")
25
+ end
26
+
27
+ def show_calculation(calc)
28
+ @tree_map = {}
29
+
30
+ @treeControl.delete_all_items
31
+ root = self.treeControl.add_root(calc.name)
32
+ input_geom = @treeControl.append_item(root, calc.geometry)
33
+ @tree_map[input_geom] = calc.input_geometry
34
+ @treeControl.append_item(root, calc.control)
35
+ @treeControl.append_item(root, calc.status)
36
+ @treeControl.append_item(root, "CONVERGED: #{calc.converged?}")
37
+ # @treeControl.append_item(root, calc.output.total_wall_time)
38
+ if calc.output
39
+ calc.output.geometry_steps.each{|step|
40
+ step_id = @treeControl.append_item(root, "Step %i" % step.step_num)
41
+ @tree_map[step_id] = step
42
+ @treeControl.append_item(step_id, "Total Energy: %f" % step.total_energy)
43
+ @treeControl.append_item(step_id, "SC Iters: %i" % step.sc_iterations.size)
44
+ @treeControl.append_item(step_id, "Wall Time: %f" % step.total_wall_time.to_s)
45
+ }
46
+ end
47
+ @treeControl.expand(root)
48
+ # self.app.project.calculations.each{|calc|
49
+ # calcid = self.treeControl.append_item(root, calc.name)
50
+ # @tree_map[calcid] = calc
51
+ # }
52
+
53
+ evt_tree_sel_changed(self.treeControl) {|evt|
54
+ item = @tree_map[evt.get_item]
55
+ if item.is_a? Aims::GeometryStep
56
+ self.app.show_geometry(item.geometry)
57
+ end
58
+ if item.is_a? Aims::Geometry
59
+ self.app.show_geometry(item)
60
+ end
61
+ }
62
+ end
63
+
64
+ end
65
+ end
@@ -0,0 +1,141 @@
1
+ module AimsProject
2
+
3
+ class CalculationWindow < Wx::Panel
4
+
5
+ include Wx
6
+
7
+ CALC_TABLE_COLS=4
8
+
9
+ def initialize(app, parent)
10
+
11
+ super(parent)
12
+ @app = app
13
+
14
+ # Initialize the selection
15
+ @selection = {}
16
+
17
+ # The inspector window
18
+ @inspector_window = @app.inspector.add_inspector_window
19
+
20
+ # Initialize the options for the crystal viewer
21
+ @options = CrystalViewerOptions.new(@inspector_window)
22
+
23
+ # Top level is a splitter
24
+ topSplitterWindow = SplitterWindow.new(self)
25
+ sizer = VBoxSizer.new
26
+ sizer.add_item(topSplitterWindow, :proportion => 1, :flag => EXPAND)
27
+
28
+ set_sizer(sizer)
29
+
30
+ # The top is a list control
31
+ @calcTable = Grid.new(topSplitterWindow, -1)
32
+ init_table
33
+
34
+ # Populate the calculations list
35
+ @calcs = @app.project.calculations.sort{|a,b| a.name <=> b.name}
36
+ @calcs.each_with_index{|calc, i|
37
+ add_calc_at_row(calc, i)
38
+ }
39
+ @calcTable.auto_size
40
+
41
+ # The bottom is a vertical splitter
42
+ calcWindowSplitter = SplitterWindow.new(topSplitterWindow)
43
+
44
+ # with a tree and a viewer
45
+ @calcTree = CalculationTree.new(self, calcWindowSplitter)
46
+ @calcViewer = CrystalViewer.new(self, calcWindowSplitter, @options)
47
+ calcWindowSplitter.split_vertically(@calcTree, @calcViewer)
48
+
49
+
50
+ # Split the top and bottom
51
+ topSplitterWindow.split_horizontally(@calcTable, calcWindowSplitter, 100)
52
+
53
+ # Setup the events
54
+ evt_grid_cmd_range_select(@calcTable) {|evt|
55
+ if evt.selecting
56
+ row = evt.get_top_row
57
+ puts "CalculationWindow.show_calculation #{@calcs[row].calculation_directory}"
58
+ show_calculation(@calcs[row])
59
+ end
60
+ }
61
+
62
+ evt_thread_callback {|evt|
63
+ @calcTree.show_calculation(@calculation)
64
+ if @calculation.final_geometry
65
+ show_geometry(@calculation.final_geometry)
66
+ else
67
+ show_geometry(@calculation.input_geometry)
68
+ end
69
+ }
70
+
71
+ end
72
+
73
+ def init_table
74
+ @calcTable.create_grid(@app.project.calculations.size, CALC_TABLE_COLS, Grid::GridSelectRows)
75
+ @calcTable.set_col_label_value(0, "Geometry")
76
+ @calcTable.set_col_label_value(1, "Subdirectory")
77
+ @calcTable.set_col_label_value(2, "Control")
78
+ @calcTable.set_col_label_value(3, "Status")
79
+
80
+ end
81
+
82
+ # Insert a calculation in the table at the specified row
83
+ def add_calc_at_row(calc, row)
84
+ @calcTable.set_cell_value(row, 0, calc.geometry)
85
+ @calcTable.set_cell_value(row, 1, calc.calc_subdir.to_s)
86
+ @calcTable.set_cell_value(row, 2, calc.control)
87
+ @calcTable.set_cell_value(row, 3, calc.status)
88
+ end
89
+
90
+ def show_inspector
91
+ @app.inspector.show_inspector_window(@inspector_window)
92
+ end
93
+
94
+ def show_calculation(calc)
95
+ begin
96
+ @calculation = calc
97
+ @err = nil
98
+ t = Thread.new(self) { |evtHandler|
99
+ begin
100
+ @app.set_status("Loading #{@calculation.name}")
101
+ @calculation.load_output
102
+ evt = ThreadCallbackEvent.new
103
+ evtHandler.add_pending_event(evt)
104
+ @app.set_status("")
105
+ rescue $! => e
106
+ @app.set_status(e.message)
107
+ end
108
+ }
109
+ t.priority = t.priority + 100
110
+ rescue $! => e
111
+ puts e.message
112
+ puts e.backtrace
113
+ @app.error_dialog(e)
114
+ end
115
+ end
116
+
117
+ # Get an Image
118
+ def image
119
+ @calcViewer.image
120
+ end
121
+
122
+ # get the currently displayed geometry
123
+ def geometry
124
+ @calcViewer.unit_cell
125
+ end
126
+
127
+ # Display the given geometry
128
+ def show_geometry(geometry)
129
+ @calcViewer.unit_cell = GeometryFile.new(geometry)
130
+ end
131
+
132
+ def select_atom(atom)
133
+ @app.set_status(atom.format_geometry_in)
134
+ end
135
+
136
+ def nudge_selected_atoms(x,y,z)
137
+ @app.error_dialog("Sorry, 'nudge' doesn't work on calculation outputs.")
138
+ end
139
+
140
+ end
141
+ end