aims_project_windows 0.3.1

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,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