Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2024-09-28 07:02:38

0001 #!/usr/bin/env ruby
0002 #--------------------------------------------------------------------#
0003 # vary parameters of dRICH geometry, and run simulations in parallel #
0004 # Author: C. Dilks                                                   #
0005 #--------------------------------------------------------------------#
0006 
0007 require 'awesome_print'
0008 require 'nokogiri'
0009 require 'fileutils'
0010 require 'open3'
0011 require 'timeout'
0012 require 'pry'
0013 
0014 ### environment check
0015 if ENV['DETECTOR'].nil? or ENV['DETECTOR_PATH'].nil?
0016   $stderr.puts "ERROR: source environ.sh"
0017   exit 1
0018 end
0019 
0020 ### GLOBAL SETTINGS **********************************
0021 Detector     = ENV['DETECTOR']                     # detector name
0022 DetectorPath = ENV['DETECTOR_PATH']                # detector installation prefix path
0023 CompactFile  = "#{DetectorPath}/compact/drich.xml" # dRICH compact file
0024 # ***
0025 Cleanup       = true                   # if true, remove transient files
0026 MultiThreaded = true                   # if true, run one simulation job per thread
0027 PoolSize      = [`nproc`.to_i-2,1].max # number of parallel threads to run (`if MultiThreaded`)
0028 TimeLimit     = 300                    # terminate a pipeline if it takes longer than `TimeLimit` seconds (set to `0` to disable)
0029 
0030 
0031 ### ARGUMENTS ****************************************
0032 OutputDirMain = 'out'
0033 VariatorDir   = 'ruby/variator'
0034 variator_code = 'template'
0035 if ARGV.length<1
0036   $stderr.puts """
0037   USAGE: #{$0} [OUTPUT ID] [VARIATOR CODE (default=#{variator_code})]
0038 
0039     [OUTPUT ID] should be a name for this simulation run
0040     - output files will be written to #{OutputDirMain}/[OUTPUT ID]
0041     - warning: this directory will be *removed* before running jobs
0042 
0043     [VARIATOR CODE] is the file containing the variation code
0044     - default is '#{variator_code}', which is short-hand for '#{VariatorDir}/#{variator_code}.rb'
0045     - two options for specifying the file:
0046       - basename of a file in '#{VariatorDir}', e.g., '#{variator_code}'
0047       - path to a specific file, e.g., './my_personal_variations/var1.rb'
0048     - see examples and template.rb in '#{VariatorDir}' to help define your own
0049   """
0050   exit 2
0051 end
0052 OutputDir = [OutputDirMain,ARGV[0]].join '/'
0053 variator_code = ARGV[1] if ARGV.length>1
0054 
0055 
0056 ### PREPARATION **************************************
0057 
0058 # status printout
0059 def print_status(message)
0060   puts "[***] #{message}"
0061 end
0062 print_status 'preparation'
0063 
0064 # get units, given string with value # todo: might not work for every case; need to generalize
0065 def get_units(str)
0066   if str.include?('*')
0067     '*' + str.split('*').last
0068   else
0069     ''
0070   end
0071 end
0072 
0073 # load variator code
0074 if variator_code.include? '/' # if variator_code is a path to a file
0075   unless variator_code.match? /^(\/|\.\/)/ # unless starts with '/' or './'
0076     variator_code = "./#{variator_code}"
0077   end
0078 elsif File.file?(variator_code) or File.file?(variator_code+'.rb') # elsif local file in pwd
0079   variator_code = "./#{variator_code}"
0080 else # else assume file is in VariatorDir
0081   variator_code = "./#{VariatorDir}/#{variator_code}"
0082 end
0083 print_status "loading variator from #{variator_code}"
0084 unless File.file?(variator_code) or File.file?(variator_code+'.rb')
0085   $stderr.puts "ERROR: cannot find variation code #{variator_code}"
0086   exit 1
0087 end
0088 require variator_code
0089 variator = Variator.new
0090 puts "="*60
0091 
0092 # error collection
0093 errors = Array.new
0094 error = Proc.new do |message|
0095   errors << message
0096   $stderr.puts message
0097 end
0098 
0099 # make output directories
0100 puts "Writing output to #{OutputDir}"
0101 FileUtils.mkdir_p OutputDir
0102 FileUtils.rm_r OutputDir, secure: true, verbose: true
0103 [ 'compact', 'config', 'sim', 'log', ].each do |subdir|
0104   FileUtils.mkdir_p "#{OutputDir}/#{subdir}"
0105 end
0106 
0107 # set xpaths for constant fixed_settings
0108 variator.fixed_settings.each do |setting|
0109   if setting.has_key? :constant
0110     setting[:xpath] = "//constant[@name=\"#{setting[:constant]}\"]"
0111     setting[:attribute] = 'value'
0112   end
0113 end
0114 
0115 # parse compact file
0116 # - write a copy to OutputDir, so it's easier to diff with the variants
0117 #   (xml parsers tend to re-format the syntax)
0118 xml = Nokogiri::XML File.open(CompactFile)
0119 compact_drich_orig = [OutputDir,'compact',File.basename(CompactFile)].join '/'
0120 puts "write parsed XML tree to #{compact_drich_orig}"
0121 File.open(compact_drich_orig,'w') { |out| out.puts xml.to_xml }
0122 
0123 # build array of variants, the results of the variation functions
0124 # - for each variation in `variation`, add key `:variants`, pointing to its variant array
0125 # - each variant array element is the following Hash:
0126 #   {
0127 #     :value     => the variant value together with its units
0128 #     :xpath     => xml node xpath (copied from variation)
0129 #     :attribute => attribute name (copied from variation)
0130 #   }
0131 variator.varied_settings.each do |var|
0132   # get node
0133   nodes = xml.xpath var[:xpath]
0134   if nodes.size == 0
0135     $stderr.puts "ERROR: cannot find node at xpath #{var[:xpath]}"
0136     exit 1
0137   elsif nodes.size > 1
0138     error.call "WARNING: more than one node for xpath '#{var[:xpath]}'" # todo: add support for this case
0139   end
0140   # get units
0141   val_str = nodes.first.attr var[:attribute]
0142   units = get_units val_str
0143   # fill variant_values array by calling the variation function Proc
0144   variant_values = var[:function].call *var[:args], var[:count]
0145   # fill variant array with Hashes
0146   var[:variants] = variant_values.map do |val|
0147     {
0148       :xpath     => var[:xpath],
0149       :attribute => var[:attribute],
0150       :value     => "#{val}#{units}",
0151       :label     => var[:label],
0152     }
0153   end
0154 end
0155 
0156 # take the product of all variant arrays
0157 # - builds a list `variant_settings_list` of all the possible variable settings
0158 # - each element is itself a list of `variant_settings`, for a particular variant
0159 variant_arrays = variator.varied_settings.map{ |var| var[:variants] }
0160 variant_settings_list = variant_arrays.first.product *variant_arrays[1..]
0161 # binding.pry
0162 
0163 # calculate derived settings
0164 variant_settings_list.each do |variant_settings|
0165   variator.derived_settings.each do |derived_setting|
0166     # fill valHash with variant-specific settings (which have a label);
0167     # units are stripped away and values are assumed to be floats
0168     valHash = variant_settings
0169       .find_all{ |h| not h[:label].nil? }
0170       .map{ |h| [ h[:label], h[:value].split('*').first.to_f ] }
0171       .to_h
0172     # get units for the derived setting
0173     nodes   = xml.xpath derived_setting[:xpath]
0174     val_str = nodes.first.attr derived_setting[:attribute]
0175     units   = get_units val_str
0176     # calculate derived settings value, and add the setting to `variant_settings`
0177     derived_value = derived_setting[:derivation].call(valHash)
0178     # add to `variant_settings`, including appended units
0179     variant_settings << { :value => "#{derived_value}#{units}" }.merge(derived_setting)
0180   end
0181 end
0182 # binding.pry
0183 
0184 
0185 ### PRODUCE COMPACT FILES **************************************
0186 print_status 'loop over variants'
0187 cleanup_list = []
0188 simulations  = []
0189 variant_settings_list.each_with_index do |variant_settings,variant_id|
0190 
0191   # clone the xml tree
0192   xml_clone = xml.dup
0193 
0194   # in `xml_clone`, set each attribute of this variant's settings, along with the fixed settings
0195   settings = variant_settings + variator.fixed_settings
0196   print_status "-----> setting variant #{variant_id}:"
0197   ap settings
0198   settings.each do |var|
0199     node = xml_clone.at_xpath var[:xpath]
0200     node.set_attribute var[:attribute], var[:value]
0201   end
0202 
0203   # create drich compact file variant `compact_drich`, by writing `xml_clone`
0204   # - this is a modification of `CompactFile`, with this variant's attributes set
0205   # - `compact_drich` is written to `#{DetectorPath}/compact`, and copied to `OutputDir`
0206   basename      = "#{File.basename(CompactFile,'.xml')}_variant#{variant_id}"
0207   compact_drich = "#{File.dirname(CompactFile)}/#{basename}.xml"
0208   print_status "produce compact file variant #{compact_drich}"
0209   File.open(compact_drich,'w') { |out| out.puts xml_clone.to_xml }
0210   FileUtils.cp compact_drich, "#{OutputDir}/compact"
0211   cleanup_list << compact_drich
0212   cleanup_list << "#{compact_drich}.bak"
0213 
0214   # create detector template config
0215   # - this will be combined with `compact_drich` to render the full detector compact file
0216   config_drich = "#{OutputDir}/config/#{basename}.yml"
0217   print_status "produce jinja2 config #{config_drich}"
0218   File.open(config_drich,'w') do |out|
0219     out.puts <<~EOF
0220       features:
0221         pid:
0222           drich: #{compact_drich}
0223     EOF
0224   end
0225 
0226   # render the full detector compact file, `compact_detector`
0227   # - it will include `compact_drich` instead of the default `CompactFile`
0228   compact_detector = "#{DetectorPath}/#{Detector}_#{basename}.xml"
0229   print_status "jinja2 render template to #{compact_detector}"
0230   render = [
0231     "#{Detector}/bin/make_detector_configuration",
0232     "-d #{Detector}/templates",
0233     "-t #{Detector}.xml.jinja2",
0234     "-o #{compact_detector}",
0235     "-c #{config_drich}",
0236   ]
0237   system render.join(' ')
0238   cleanup_list << compact_detector
0239 
0240   # simulation settings
0241   # NOTE: if you change this, update ruby/variator/template.md
0242   simulation_settings = {
0243     :id               => variant_id,
0244     :variant_info     => settings,
0245     :compact_detector => compact_detector,
0246     :compact_drich    => compact_drich,
0247     :output           => "#{OutputDir}/sim/#{basename}.root",
0248     :log              => "#{OutputDir}/log/#{basename}",
0249   }
0250 
0251   # build simulation pipeline command
0252   simulations << {
0253     :pipelines => variator.simulation_pipelines.call(simulation_settings)
0254   }.merge(simulation_settings)
0255 
0256 end
0257 
0258 
0259 ### EXECUTION **************************************************
0260 
0261 # run the commands listed in `sim[:pipelines]`, and log to `sim[:log]`
0262 def execute_thread(sim)
0263   # status update
0264   print_thread_status = Proc.new do |message|
0265     puts "-> variant #{sim[:id]} -> #{message}"
0266   end
0267   print_thread_status.call "BEGIN"
0268   # print settings for this variant to log file
0269   File.open("#{sim[:log]}.info",'w') do |out|
0270     out.puts "VARIANT #{sim[:id]}:"
0271     out.write sim[:variant_info].ai(plain: true)
0272     out.puts "\n"
0273     out.puts "PIPELINE:"
0274     out.puts sim[:pipelines].map{ |p| p.join(' ') }.ai(plain: true)
0275   end
0276   # loop over pipelines
0277   timed_out = false
0278   sim[:pipelines].each do |simulation_pipeline|
0279     # execute pipeline, with logging, and timeout control
0280     print_thread_status.call simulation_pipeline.map(&:first).join(' | ')
0281     pipeline_waiters = []
0282     begin
0283       Timeout::timeout(TimeLimit) do
0284         # use `pipeline_start`, so calling thread is in control (allows Timeout::timeout to work)
0285         pipeline_waiters = Open3.pipeline_start(
0286           *simulation_pipeline,
0287           :out=>["#{sim[:log]}.out",'a'],
0288           :err=>["#{sim[:log]}.err",'a'],
0289         )
0290         Process.waitall # wait for all pipeline_waiters to finish
0291       end
0292     rescue Timeout::Error
0293       timed_out = true
0294       # print timeout error
0295       print_thread_status.call "TIMEOUT: #{simulation_pipeline.map(&:first).join(' | ')}"
0296       File.open("#{sim[:log]}.err",'a') do |out|
0297         out.puts '='*30
0298         out.puts "TIMEOUT LIMIT REACHED, terminate pipeline:"
0299         out.puts simulation_pipeline.join(' ')
0300         out.puts '='*30
0301       end
0302       # kill the timed-out pipeline
0303       pipeline_waiters.each do |waiter|
0304         print_thread_status.call "KILL #{waiter}"
0305         begin
0306           Process.kill('KILL',waiter.pid)
0307         rescue Errno::ESRCH
0308         end
0309       end
0310     end
0311     return if timed_out # do not run the next pipeline, if timed out
0312   end
0313   print_thread_status.call "END"
0314 end
0315 
0316 # execute the threads, either single- or multi-threaded
0317 # - todo: use concurrency instead of fixed pool sizes (current implementation
0318 #         is only efficient if all threads take the same time to run)
0319 print_status 'SIMULATION COMMAND (for one variant):'
0320 ap simulations.first
0321 print_status 'begin simulation '.upcase + '='*40
0322 if MultiThreaded
0323   print_status "running multi-threaded with PoolSize = #{PoolSize}"
0324   print_status "all pipelines have TimeLimit = #{TimeLimit} seconds"
0325   simulations.each_slice(PoolSize) do |slice|
0326     pool = slice.map do |simulation|
0327       Thread.new{ execute_thread simulation }
0328     end
0329     trap 'INT' do
0330       print_status 'interrupt received; killing threads...'
0331       pool.each &:kill
0332       exit 1
0333     end
0334     pool.each &:join
0335   end
0336 else
0337   simulations.each do |simulation|
0338     execute_thread simulation
0339   end
0340 end
0341 
0342 # cleanup the transient compact files
0343 if Cleanup
0344   print_status "cleanup transient files:"
0345   ap cleanup_list.sort
0346   cleanup_list.each do |file|
0347     FileUtils.rm_f file, verbose: true
0348   end
0349 end
0350 
0351 # collect and print
0352 print_status 'DONE'
0353 simulations.each do |simulation|
0354   err_log = simulation[:log]+".err"
0355   num_errors = `grep -v '^$' #{err_log} | wc -l`.chomp.split.first.to_i
0356   if num_errors>0
0357     errors << "  #{err_log}  => #{num_errors} errors"
0358   end
0359 end
0360 if errors.size>0
0361   print_status 'ERRORS:'
0362   ap errors
0363 else
0364   print_status 'NO ERRORS'
0365 end