File indexing completed on 2024-09-28 07:02:38
0001
0002
0003
0004
0005
0006
0007 require 'awesome_print'
0008 require 'nokogiri'
0009 require 'fileutils'
0010 require 'open3'
0011 require 'timeout'
0012 require 'pry'
0013
0014
0015 if ENV['DETECTOR'].nil? or ENV['DETECTOR_PATH'].nil?
0016 $stderr.puts "ERROR: source environ.sh"
0017 exit 1
0018 end
0019
0020
0021 Detector = ENV['DETECTOR']
0022 DetectorPath = ENV['DETECTOR_PATH']
0023 CompactFile = "#{DetectorPath}/compact/drich.xml"
0024
0025 Cleanup = true
0026 MultiThreaded = true
0027 PoolSize = [`nproc`.to_i-2,1].max
0028 TimeLimit = 300
0029
0030
0031
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
0057
0058
0059 def print_status(message)
0060 puts "[***] #{message}"
0061 end
0062 print_status 'preparation'
0063
0064
0065 def get_units(str)
0066 if str.include?('*')
0067 '*' + str.split('*').last
0068 else
0069 ''
0070 end
0071 end
0072
0073
0074 if variator_code.include? '/'
0075 unless variator_code.match? /^(\/|\.\/)/
0076 variator_code = "./#{variator_code}"
0077 end
0078 elsif File.file?(variator_code) or File.file?(variator_code+'.rb')
0079 variator_code = "./#{variator_code}"
0080 else
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
0093 errors = Array.new
0094 error = Proc.new do |message|
0095 errors << message
0096 $stderr.puts message
0097 end
0098
0099
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
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
0116
0117
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
0124
0125
0126
0127
0128
0129
0130
0131 variator.varied_settings.each do |var|
0132
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]}'"
0139 end
0140
0141 val_str = nodes.first.attr var[:attribute]
0142 units = get_units val_str
0143
0144 variant_values = var[:function].call *var[:args], var[:count]
0145
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
0157
0158
0159 variant_arrays = variator.varied_settings.map{ |var| var[:variants] }
0160 variant_settings_list = variant_arrays.first.product *variant_arrays[1..]
0161
0162
0163
0164 variant_settings_list.each do |variant_settings|
0165 variator.derived_settings.each do |derived_setting|
0166
0167
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
0173 nodes = xml.xpath derived_setting[:xpath]
0174 val_str = nodes.first.attr derived_setting[:attribute]
0175 units = get_units val_str
0176
0177 derived_value = derived_setting[:derivation].call(valHash)
0178
0179 variant_settings << { :value => "#{derived_value}#{units}" }.merge(derived_setting)
0180 end
0181 end
0182
0183
0184
0185
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
0192 xml_clone = xml.dup
0193
0194
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
0204
0205
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
0215
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:
0223 EOF
0224 end
0225
0226
0227
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
0241
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
0252 simulations << {
0253 :pipelines => variator.simulation_pipelines.call(simulation_settings)
0254 }.merge(simulation_settings)
0255
0256 end
0257
0258
0259
0260
0261
0262 def execute_thread(sim)
0263
0264 print_thread_status = Proc.new do |message|
0265 puts "-> variant #{sim[:id]} -> #{message}"
0266 end
0267 print_thread_status.call "BEGIN"
0268
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
0277 timed_out = false
0278 sim[:pipelines].each do |simulation_pipeline|
0279
0280 print_thread_status.call simulation_pipeline.map(&:first).join(' | ')
0281 pipeline_waiters = []
0282 begin
0283 Timeout::timeout(TimeLimit) do
0284
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
0291 end
0292 rescue Timeout::Error
0293 timed_out = true
0294
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
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
0312 end
0313 print_thread_status.call "END"
0314 end
0315
0316
0317
0318
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
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
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