Class: AimsProject::Calculation

Inherits:
Object
  • Object
show all
Defined in:
lib/aims_project/calculation.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(geometry, control) ⇒ Calculation

Initialize a new calculation. Consider using Calculation#create to generate the directory structure as well.

Parameters:

  • geometry (String)

    the filename of the input geometry

  • control (String)

    the filename of the input control



279
280
281
282
283
# File 'lib/aims_project/calculation.rb', line 279

def initialize(geometry, control)
  self.geometry = File.basename(geometry)
  self.control = File.basename(control)
  self.history = Array.new
end

Instance Attribute Details

#calc_subdirObject

The calculation subdirectory, (can be nil)



45
46
47
# File 'lib/aims_project/calculation.rb', line 45

def calc_subdir
  @calc_subdir
end

#controlObject

The name of the control file



39
40
41
# File 'lib/aims_project/calculation.rb', line 39

def control
  @control
end

#created_atObject



52
53
54
# File 'lib/aims_project/calculation.rb', line 52

def created_at
  @created_at = cast_as_date(@created_at)
end

#geometryObject

The name of the geometry file



36
37
38
# File 'lib/aims_project/calculation.rb', line 36

def geometry
  @geometry
end

#historyObject

An array of (#to_s) items that are the calculation history



48
49
50
# File 'lib/aims_project/calculation.rb', line 48

def history
  @history
end

#statusObject

The current calculation status



42
43
44
# File 'lib/aims_project/calculation.rb', line 42

def status
  @status
end

#updated_atObject



59
60
61
62
63
# File 'lib/aims_project/calculation.rb', line 59

def updated_at
  # Cast value to Date
  # Do this in the accessor because loading from YAML bypasses the setter method
  @updated_at = cast_as_date(@updated_at)
end

Class Method Details

.check_version(file) ⇒ Object

Check for the existence of a cached version of the input file If it exists, check if the cached version is the same as the working version, and return true if they are, false if they are not. If the cached version does not exist, then cache the working version and return true.



321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# File 'lib/aims_project/calculation.rb', line 321

def Calculation.check_version(file)
  cache_dir = ".input_cache"
  unless File.exists? cache_dir
    Dir.mkdir cache_dir
    Dir.mkdir File.join(cache_dir, AimsProject::GEOMETRY_DIR)
    Dir.mkdir File.join(cache_dir, AimsProject::CONTROL_DIR)
  end
  
  return false unless File.exists?(file)
  cache_version = File.join(cache_dir, file)
  if File.exists?(cache_version)
    return FileUtils.compare_file(file, cache_version)
  else
    FileUtils.cp_r file, cache_version
    return true
  end

end

.create(project, geometry, control, user_vars = {}) ⇒ Object

Create a calculation and the corresponding directory structure given a geometry and a control This method will search for the files geometry.##geometry.in and control.##control.in in the project directory, then create a calculation directory that is the merger of those two filenames, and finally copy the geometry and control files into the calculation directory and rename them geometry.in and control.in

Parameters:

  • geometry (String)

    The filename of the geometry file to use to initialize the calculation

  • control (String)

    The filename of the control file to use to initialize the calculation

  • user_vars (Hash<Symbol, Object>) (defaults to: {})

    A symbol=>Object hash of variables that will be available when evaluating the geometry and control files using embedded ruby

    This hash is also used to generate a calculation subdirectory



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/aims_project/calculation.rb', line 129

def Calculation.create(project, geometry, control, user_vars = {})

  calc = Calculation.new(geometry, control)
  calc.created_at = Time.new

  control_in = calc.control_file
  geometry_in = calc.geometry_file

  # Define the calculation sub-directory if user_vars exists
  unless user_vars.empty?
    calc.calc_subdir = user_vars.keys.collect{|k| (k.to_s + "=" + user_vars[k].to_s).gsub('@', '').gsub(' ','_') }.join("..")
  end
  
  # Add configuration variables to the calculation binding
  uvars_file = File.join(AimsProject::CONFIG_DIR, "user_variables.rb")
  calc.get_binding.eval(File.read(uvars_file)) if File.exists?(uvars_file)

  # Merge project variables into calcuation binding
  if project
    project.instance_variables.each{|v|
      if v == :@name # Ignore the project name
        calc.instance_variable_set(:@project_name, project.instance_variable_get(v))
      else
        calc.instance_variable_set(v, project.instance_variable_get(v))
      end
    }
  end

  # Merge user-vars to the calculation binding
  user_vars.each_pair{|sym, val|
    calc.instance_variable_set(sym, val)
  }
  

  # Check file existence
  raise "Unable to locate #{control_in}" unless File.exists?(control_in) 
  raise "Unable to locate #{geometry_in}" unless File.exists?(geometry_in)

  # Validate the files
  raise "#{geometry_in} has changed since last use" unless check_version(geometry_in)
  raise "#{control_in} has changed since last use" unless check_version(control_in)

  # Validate that the directory doesn't already exist
  if Dir.exists? calc.calculation_directory
    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"
  end
  FileUtils.mkdir_p calc.calculation_directory
  
  erb = ERB.new(File.read(control_in))
  File.open File.join(calc.calculation_directory, "control.in"), "w" do |f|
    f.puts erb.result(calc.get_binding)
  end
  
  erb = ERB.new(File.read(geometry_in))
  File.open File.join(calc.calculation_directory, "geometry.in"), "w" do |f|
    f.puts erb.result(calc.get_binding)
  end


  calc.status = AimsProject::STAGED
  calc.save
  
  return calc
end

.find_all(status) ⇒ Object

Find all calculations in the current directory with a given status



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/aims_project/calculation.rb', line 84

def Calculation.find_all(status)
  # Catch all root calculations
  calculations = Dir.glob File.join(AimsProject::CALCULATION_DIR, "*", AimsProject::CALC_STATUS_FILENAME)
  # Catch all sub calculations
  calculations << Dir.glob(File.join(AimsProject::CALCULATION_DIR, "*", "*",  AimsProject::CALC_STATUS_FILENAME))
  calculations.collect{|calc_status_file|
    calc_dir = File.dirname(calc_status_file)
    calc = Calculation.load(calc_dir)
    if (status == calc.status)
      calc
    else
      nil
    end
  }.compact
end

.load(dir) ⇒ Object

Load a calculation from the serialized yaml file in the given directory raises an ObjectFileNotFoundException if the specified directory does not contain a yaml serialization of the calculation. raises a CorruptObjectFileException if the yaml file cannot be de-serialized



104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/aims_project/calculation.rb', line 104

def Calculation.load(dir)
  
  calc_file  = File.join(dir, AimsProject::CALC_STATUS_FILENAME)
  raise ObjectFileNotFoundException.new(calc_file) unless File.exists?(calc_file)
  
  f = File.open(calc_file, 'r')
  calc_obj = YAML.load(f)
  f.close
  
  raise CorruptObjectFileException.new(calc_file) unless calc_obj
  return calc_obj
end

Instance Method Details

#calculation_directoryObject

Return the directory for this calculation



348
349
350
# File 'lib/aims_project/calculation.rb', line 348

def calculation_directory
  File.join AimsProject::CALCULATION_DIR, self.name, (@calc_subdir || "")
end

#cast_as_date(obj) ⇒ Object



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/aims_project/calculation.rb', line 65

def cast_as_date(obj)
  if obj.is_a? Date or obj.is_a? Time or obj.is_a? DateTime
    obj.to_datetime
  elsif obj.is_a? String
    DateTime.parse(obj)
    # unless s
    #   s = DateTime.strptime(obj, "%F %T %z")
    # end
    # unless s
    #   s = DateTime.strptime(obj, "%FT%s%z")
    # end
    # s
  else
    DateTime.new(0)
  end      
end

#control_fileObject

Determine the name of the control.in file from the control variable.



306
307
308
# File 'lib/aims_project/calculation.rb', line 306

def control_file
  File.join(AimsProject::CONTROL_DIR, self.control)
end

#converged?Boolean

Return whether this calculation is converged or not

Returns:

  • (Boolean)


397
398
399
400
401
402
403
# File 'lib/aims_project/calculation.rb', line 397

def converged?
  if output.nil?
    false
  else
    output.geometry_converged
  end
end

#final_geometryObject

Parse the calculation output and return the final geometry of this calculation. Return nil if no output is found.



375
376
377
378
379
380
381
382
383
# File 'lib/aims_project/calculation.rb', line 375

def final_geometry
  # ouput is not cached, so we only retrieve it once
  o = self.output
  if o
    o.final_geometry
  else
    nil
  end
end

#geometry_fileObject

Determine the name of the geometr.in file from the geometry variable



312
313
314
# File 'lib/aims_project/calculation.rb', line 312

def geometry_file
  File.join(AimsProject::GEOMETRY_DIR, self.geometry)
end

#geometry_next_stepObject

Parse the geometry.in.next_step file in the calculation directory if it exists and return the Aims::Geometry object or nil



387
388
389
390
391
392
393
394
# File 'lib/aims_project/calculation.rb', line 387

def geometry_next_step
  g_file = File.join(calculation_directory, "geometry.in.next_step")
  if File.exists?(g_file)
    Aims::GeometryParser.parse(g_file)
  else
    nil
  end
end

#get_bindingObject

Get the binding for this calculation



195
196
197
# File 'lib/aims_project/calculation.rb', line 195

def get_binding
  binding()
end

#holdObject

Set the status to HOLD. Only possible if status is currently STAGED



215
216
217
218
219
220
221
222
223
# File 'lib/aims_project/calculation.rb', line 215

def hold
  if STAGED == status
    self.status = HOLD
    save
    return true
  else
    return false
  end
end

#input_geometryObject

Intended to replace @geometry, but needs lots of regression testing for now, will just generate this on the fly



206
207
208
209
210
211
# File 'lib/aims_project/calculation.rb', line 206

def input_geometry
  unless @actual_geometry
    @input_geometry = Aims::GeometryParser.parse(File.join(self.calculation_directory, "geometry.in"))
  end
  @input_geometry
end

#load_output(output_pattern = "*output*") ⇒ Object



352
353
354
355
356
357
358
359
# File 'lib/aims_project/calculation.rb', line 352

def load_output(output_pattern = "*output*")
  output_files = Dir.glob(File.join(calculation_directory, output_pattern))
  if output_files.empty?
    @output = nil
  else
    @output = Aims::OutputParser.parse(output_files.last)
  end      
end

#nameObject

The name of this calculation



200
201
202
# File 'lib/aims_project/calculation.rb', line 200

def name
  "#{geometry}.#{control}"
end

#output(output_pattern = "*output*") ⇒ Object

Search the calculation directory for the calculation output. If found, parse it and return the Aims::AimsOutput object, otherwise return nil. If multiple output files are found, use the last one in the list when sorted alpha-numerically. (This is assumed to be the most recent calculation)



366
367
368
369
370
371
# File 'lib/aims_project/calculation.rb', line 366

def output(output_pattern = "*output*")
  unless @output
    load_output(output_pattern)
  end
  @output
end

#relative_pathObject

The path of this calculation relative to the project



341
342
343
# File 'lib/aims_project/calculation.rb', line 341

def relative_path
  calculation_directory
end

#releaseObject

Set the status to STAGED if current status is HOLD



226
227
228
229
230
231
232
233
234
# File 'lib/aims_project/calculation.rb', line 226

def release
  if HOLD == status
    self.status = STAGED
    save
    return true
  else
    return false
  end
end

#reloadObject

Reload this calculation from the serialized YAML file



295
296
297
298
299
300
301
302
# File 'lib/aims_project/calculation.rb', line 295

def reload
  c = Calculation.load(self.calculation_directory)
  self.geometry = c.geometry
  @input_geometry = c.input_geometry
  self.control = c.control
  self.status = c.status
  return self
end

#restart_relaxationObject

Create a new calculation that will restart a geometry relaxation calculation using the last available geometry.

This method will generate a new file in the geometry directory with the extension restartN, where N will be incremented if the filename already exists. A new calculation will be created with the new geometry and the original control.



243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
# File 'lib/aims_project/calculation.rb', line 243

def restart_relaxation
  
  # Create a new geometry file
  geometry_orig = self.geometry_file
  
  # Append the subdirectory if a subdir
  if @calc_subdir
    geometry_orig = geometry_orig + ".#{calc_subdir}"
  end
  
  # If restarting a restart, then increment the digit
  if (geometry_orig.split(".").last =~ /restart(\d+)/)
    n = $1.to_i
    geometry_orig = geometry_orig.split(".")[0...-1].join(".")
  else
    n = 0
  end

  begin 
    n += 1
    geometry_new = geometry_orig + ".restart#{n}"
  end while File.exist?(geometry_new)
  
  File.open(geometry_new, 'w') do |f|
    f.puts "# Final geometry from #{calculation_directory}"
    f.puts self.geometry_next_step.format_geometry_in
  end
  
  Calculation.create(nil, geometry_new, self.control)
  
end

#save(dir = nil) ⇒ Object

Serialize this calculation as a yaml file



286
287
288
289
290
291
292
# File 'lib/aims_project/calculation.rb', line 286

def save(dir = nil)
  self.updated_at = Time.new
  dir = calculation_directory unless dir
  File.open(File.join(dir, AimsProject::CALC_STATUS_FILENAME), 'w') do |f|
    f.puts YAML.dump(self)
  end
end