openstudio-analysis 1.3.6 → 1.3.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,857 +1,881 @@
1
- # *******************************************************************************
2
- # OpenStudio(R), Copyright (c) Alliance for Sustainable Energy, LLC.
3
- # See also https://openstudio.net/license
4
- # *******************************************************************************
5
-
6
- # OpenStudio formulation class handles the generation of the OpenStudio Analysis format.
7
- module OpenStudio
8
- module Analysis
9
- SeedModel = Struct.new(:file)
10
- WeatherFile = Struct.new(:file)
11
-
12
- @@measure_paths = ['./measures']
13
- # List of paths to look for measures when adding them. This currently only is used when loading an
14
- # analysis hash file. It looks in the order of the measure_paths. As soon as it finds one, it stops.
15
- def self.measure_paths
16
- @@measure_paths
17
- end
18
-
19
- def self.measure_paths=(new_array)
20
- @@measure_paths = new_array
21
- end
22
-
23
- class Formulation
24
- attr_reader :seed_model
25
- attr_reader :weather_file
26
- attr_reader :analysis_type
27
- attr_reader :outputs
28
- attr_accessor :display_name
29
- attr_accessor :workflow
30
- attr_accessor :algorithm
31
- attr_accessor :osw_path
32
- attr_accessor :download_zip
33
- attr_accessor :download_reports
34
- attr_accessor :download_osw
35
- attr_accessor :download_osm
36
- attr_accessor :cli_debug
37
- attr_accessor :cli_verbose
38
- attr_accessor :initialize_worker_timeout
39
- attr_accessor :run_workflow_timeout
40
- attr_accessor :upload_results_timeout
41
-
42
- # the attributes below are used for packaging data into the analysis zip file
43
- attr_reader :weather_files
44
- attr_reader :seed_models
45
- attr_reader :worker_inits
46
- attr_reader :worker_finalizes
47
- attr_reader :libraries
48
- attr_reader :server_scripts
49
-
50
- # Create an instance of the OpenStudio::Analysis::Formulation
51
- #
52
- # @param display_name [String] Display name of the project.
53
- # @return [Object] An OpenStudio::Analysis::Formulation object
54
- def initialize(display_name)
55
- @display_name = display_name
56
- @analysis_type = nil
57
- @outputs = []
58
- @workflow = OpenStudio::Analysis::Workflow.new
59
- # Initialize child objects (expect workflow)
60
- @weather_file = WeatherFile.new
61
- @seed_model = SeedModel.new
62
- @algorithm = OpenStudio::Analysis::AlgorithmAttributes.new
63
- @download_zip = true
64
- @download_reports = true
65
- @download_osw = true
66
- @download_osm = true
67
- @cli_debug = "--debug"
68
- @cli_verbose = "--verbose"
69
- @initialize_worker_timeout = 28800
70
- @run_workflow_timeout = 28800
71
- @upload_results_timeout = 28800
72
-
73
- # Analysis Zip attributes
74
- @weather_files = SupportFiles.new
75
- @seed_models = SupportFiles.new
76
- @worker_inits = SupportFiles.new
77
- @worker_finalizes = SupportFiles.new
78
- @libraries = SupportFiles.new
79
- @server_scripts = ServerScripts.new
80
- end
81
-
82
- # Define the type of analysis which is going to be running
83
- #
84
- # @param name [String] Name of the algorithm/analysis. (e.g. rgenoud, lhs, single_run)
85
- # allowed values are ANALYSIS_TYPES = ['spea_nrel', 'rgenoud', 'nsga_nrel', 'lhs', 'preflight',
86
- # 'morris', 'sobol', 'doe', 'fast99', 'ga', 'gaisl',
87
- # 'single_run', 'repeat_run', 'batch_run']
88
- def analysis_type=(value)
89
- if OpenStudio::Analysis::AlgorithmAttributes::ANALYSIS_TYPES.include?(value)
90
- @analysis_type = value
91
- else
92
- raise "Invalid analysis type. Allowed types: #{OpenStudio::Analysis::AlgorithmAttributes::ANALYSIS_TYPES}"
93
- end
94
- end
95
-
96
- # Path to the seed model
97
- #
98
- # @param path [String] Path to the seed model. This should be relative.
99
- def seed_model=(file)
100
- @seed_model[:file] = file
101
- end
102
-
103
- # Path to the weather file (or folder). If it is a folder, then the measures will look for the weather file
104
- # by name in that folder.
105
- #
106
- # @param path [String] Path to the weather file or folder.
107
- def weather_file=(file)
108
- @weather_file[:file] = file
109
- end
110
-
111
- # Set the value for 'download_zip'
112
- #
113
- # @param value [Boolean] The value for 'download_zip'
114
- def download_zip=(value)
115
- if [true, false].include?(value)
116
- @download_zip = value
117
- else
118
- raise ArgumentError, "Invalid value for 'download_zip'. Only true or false allowed."
119
- end
120
- end
121
-
122
- # Set the value for 'download_reports'
123
- #
124
- # @param value [Boolean] The value for 'download_reports'
125
- def download_reports=(value)
126
- if [true, false].include?(value)
127
- @download_reports = value
128
- else
129
- raise ArgumentError, "Invalid value for 'download_reports'. Only true or false allowed."
130
- end
131
- end
132
-
133
- # Set the value for 'download_osw'
134
- #
135
- # @param value [Boolean] The value for 'download_osw'
136
- def download_osw=(value)
137
- if [true, false].include?(value)
138
- @download_osw = value
139
- else
140
- raise ArgumentError, "Invalid value for 'download_osw'. Only true or false allowed."
141
- end
142
- end
143
-
144
- # Set the value for 'download_osm'
145
- #
146
- # @param value [Boolean] The value for 'download_osm'
147
- def download_osm=(value)
148
- if [true, false].include?(value)
149
- @download_osm = value
150
- else
151
- raise ArgumentError, "Invalid value for 'download_osm'. Only true or false allowed."
152
- end
153
- end
154
-
155
- # Set the value for 'cli_debug'
156
- #
157
- # @param value [Boolean] The value for 'cli_debug'
158
- def cli_debug=(value)
159
- @cli_debug = value
160
- end
161
-
162
- # Set the value for 'cli_verbose'
163
- #
164
- # @param value [Boolean] The value for 'cli_verbose'
165
- def cli_verbose=(value)
166
- @cli_verbose = value
167
- end
168
-
169
- # Set the value for 'run_workflow_timeout'
170
- #
171
- # @param value [Integer] The value for 'run_workflow_timeout'
172
- def run_workflow_timeout=(value)
173
- if value.is_a?(Integer)
174
- @run_workflow_timeout = value
175
- else
176
- raise ArgumentError, "Invalid value for 'run_workflow_timeout'. Only integer values allowed."
177
- end
178
- end
179
-
180
- # Set the value for 'initialize_worker_timeout'
181
- #
182
- # @param value [Integer] The value for 'initialize_worker_timeout'
183
- def initialize_worker_timeout=(value)
184
- if value.is_a?(Integer)
185
- @initialize_worker_timeout = value
186
- else
187
- raise ArgumentError, "Invalid value for 'initialize_worker_timeout'. Only integer values allowed."
188
- end
189
- end
190
-
191
- # Set the value for 'upload_results_timeout'
192
- #
193
- # @param value [Integer] The value for 'upload_results_timeout'
194
- def upload_results_timeout=(value)
195
- if value.is_a?(Integer)
196
- @upload_results_timeout = value
197
- else
198
- raise ArgumentError, "Invalid value for 'upload_results_timeout'. Only integer values allowed."
199
- end
200
- end
201
-
202
- # Add an output of interest to the problem formulation
203
- #
204
- # @param output_hash [Hash] Hash of the output variable in the legacy format
205
- # @option output_hash [String] :display_name Name to display
206
- # @option output_hash [String] :display_name_short A shorter display name
207
- # @option output_hash [String] :metadata_id Link to DEnCity ID in which this output corresponds
208
- # @option output_hash [String] :name Unique machine name of the variable. Typically this is measure.attribute
209
- # @option output_hash [String] :export Export the variable to CSV and dataframes from OpenStudio-server
210
- # @option output_hash [String] :visualize Visualize the variable in the plots on OpenStudio-server
211
- # @option output_hash [String] :units Units of the variable as a string
212
- # @option output_hash [String] :variable_type Data type of the variable
213
- # @option output_hash [Boolean] :objective_function Whether or not this output is an objective function. Default: false
214
- # @option output_hash [Integer] :objective_function_index Index of the objective function. Default: nil
215
- # @option output_hash [Float] :objective_function_target Target for the objective function to reach (if defined). Default: nil
216
- # @option output_hash [Float] :scaling_factor How to scale the objective function(s). Default: nil
217
- # @option output_hash [Integer] :objective_function_group If grouping objective functions, then group ID. Default: nil
218
- def add_output(output_hash)
219
- # Check if the name is already been added.
220
- exist = @outputs.find_index { |o| o[:name] == output_hash[:name] }
221
- # if so, update the fields but keep objective_function_index the same
222
- if exist
223
- original = @outputs[exist]
224
- if original[:objective_function] && !output_hash[:objective_function]
225
- return @outputs
226
- end
227
- output = original.merge(output_hash)
228
- output[:objective_function_index] = original[:objective_function_index]
229
- @outputs[exist] = output
230
- else
231
- output = {
232
- units: '',
233
- objective_function: false,
234
- objective_function_index: nil,
235
- objective_function_target: nil,
236
- #set default to nil or 1 if objective_function is true and this is not set
237
- objective_function_group: (output_hash[:objective_function] ? 1 : nil),
238
- scaling_factor: nil,
239
- #set default to false or true if objective_function is true and this is not set
240
- visualize: (output_hash[:objective_function] ? true : false),
241
- metadata_id: nil,
242
- export: true,
243
- }.merge(output_hash)
244
- #set display_name default to be name if its not set
245
- output[:display_name] = output_hash[:display_name] ? output_hash[:display_name] : output_hash[:name]
246
- #set display_name_short default to be display_name if its not set, this can be null if :display_name not set
247
- output[:display_name_short] = output_hash[:display_name_short] ? output_hash[:display_name_short] : output_hash[:display_name]
248
- # if the variable is an objective_function, then increment and
249
- # assign and objective function index
250
- if output[:objective_function]
251
- values = @outputs.select { |o| o[:objective_function] }
252
- output[:objective_function_index] = values.size
253
- end
254
-
255
- @outputs << output
256
- end
257
-
258
- @outputs
259
- end
260
-
261
- # return the machine name of the analysis
262
- def name
263
- @display_name.to_underscore
264
- end
265
-
266
- # return a hash.
267
- #
268
- # @param version [Integer] Version of the format to return
269
- # @return [Hash]
270
- def to_hash(version = 1)
271
- # fail 'Must define an analysis type' unless @analysis_type
272
- if version == 1
273
- h = {
274
- analysis: {
275
- display_name: @display_name,
276
- name: name,
277
- output_variables: @outputs,
278
- problem: {
279
- analysis_type: @analysis_type,
280
- algorithm: algorithm.to_hash(version),
281
- workflow: workflow.to_hash(version)
282
- }
283
- }
284
- }
285
-
286
- if @seed_model[:file]
287
- h[:analysis][:seed] = {
288
- file_type: File.extname(@seed_model[:file]).delete('.').upcase,
289
- path: "./seed/#{File.basename(@seed_model[:file])}"
290
- }
291
- else
292
- h[:analysis][:seed] = nil
293
- end
294
-
295
- # silly catch for if weather_file is not set
296
- wf = nil
297
- if @weather_file[:file]
298
- wf = @weather_file
299
- elsif !@weather_files.empty?
300
- # get the first EPW file (not the first file)
301
- wf = @weather_files.find { |w| File.extname(w[:file]).casecmp('.epw').zero? }
302
- end
303
-
304
- if wf
305
- h[:analysis][:weather_file] = {
306
- file_type: File.extname(wf[:file]).delete('.').upcase,
307
- path: "./weather/#{File.basename(wf[:file])}"
308
- }
309
- else
310
- # log: could not find weather file
311
- warn 'Could not resolve a valid weather file. Check paths to weather files'
312
- end
313
-
314
- h[:analysis][:file_format_version] = version
315
- h[:analysis][:cli_debug] = @cli_debug
316
- h[:analysis][:cli_verbose] = @cli_verbose
317
- h[:analysis][:run_workflow_timeout] = @run_workflow_timeout
318
- h[:analysis][:upload_results_timeout] = @upload_results_timeout
319
- h[:analysis][:initialize_worker_timeout] = @initialize_worker_timeout
320
- h[:analysis][:download_zip] = @download_zip
321
- h[:analysis][:download_reports] = @download_reports
322
- h[:analysis][:download_osw] = @download_osw
323
- h[:analysis][:download_osm] = @download_osm
324
-
325
- #-BLB I dont think this does anything. server_scripts are run if they are in
326
- #the /scripts/analysis or /scripts/data_point directories
327
- #but nothing is ever checked in the OSA.
328
- #
329
- h[:analysis][:server_scripts] = {}
330
-
331
- # This is a hack right now, but after the initial hash is created go back and add in the objective functions
332
- # to the the algorithm as defined in the output_variables list
333
- ofs = @outputs.map { |i| i[:name] if i[:objective_function] }.compact
334
- if h[:analysis][:problem][:algorithm]
335
- h[:analysis][:problem][:algorithm][:objective_functions] = ofs
336
- end
337
-
338
- h
339
- else
340
- raise "Version #{version} not defined for #{self.class} and #{__method__}"
341
- end
342
- end
343
-
344
- # Load the analysis JSON from a hash (with symbolized keys)
345
- def self.from_hash(h, seed_dir = nil, weather_dir = nil)
346
- o = OpenStudio::Analysis::Formulation.new(h[:analysis][:display_name])
347
-
348
- version = 1
349
- if version == 1
350
- h[:analysis][:output_variables].each do |ov|
351
- o.add_output(ov)
352
- end
353
-
354
- o.workflow = OpenStudio::Analysis::Workflow.load(workflow: h[:analysis][:problem][:workflow])
355
-
356
- if weather_dir
357
- o.weather_file "#{weather_path}/#{File.basename(h[:analysis][:weather_file][:path])}"
358
- else
359
- o.weather_file = h[:analysis][:weather_file][:path]
360
- end
361
-
362
- if seed_dir
363
- o.seed_model "#{weather_path}/#{File.basename(h[:analysis][:seed][:path])}"
364
- else
365
- o.seed_model = h[:analysis][:seed][:path]
366
- end
367
- else
368
- raise "Version #{version} not defined for #{self.class} and #{__method__}"
369
- end
370
-
371
- o
372
- end
373
-
374
- # return a hash of the data point with the static variables set
375
- #
376
- # @param version [Integer] Version of the format to return
377
- # @return [Hash]
378
- def to_static_data_point_hash(version = 1)
379
- if version == 1
380
- static_hash = {}
381
- # TODO: this method should be on the workflow step and bubbled up to this interface
382
- @workflow.items.map do |item|
383
- item.variables.map { |v| static_hash[v[:uuid]] = v[:static_value] }
384
- end
385
-
386
- h = {
387
- data_point: {
388
- set_variable_values: static_hash,
389
- status: 'na',
390
- uuid: SecureRandom.uuid
391
- }
392
- }
393
- h
394
- end
395
- end
396
-
397
- # save the file to JSON. Will overwrite the file if it already exists
398
- #
399
- # @param filename [String] Name of file to create. It will create the directory and override the file if it exists. If no file extension is given, then it will use .json.
400
- # @param version [Integer] Version of the format to return
401
- # @return [Boolean]
402
- def save(filename, version = 1)
403
- filename += '.json' if File.extname(filename) == ''
404
-
405
- FileUtils.mkdir_p File.dirname(filename) unless Dir.exist? File.dirname(filename)
406
- File.open(filename, 'w') { |f| f << JSON.pretty_generate(to_hash(version)) }
407
-
408
- true
409
- end
410
-
411
- # save the data point JSON with the variables set to the static values. Will overwrite the file if it already exists
412
- #
413
- # @param filename [String] Name of file to create. It will create the directory and override the file if it exists. If no file extension is given, then it will use .json.
414
- # @param version [Integer] Version of the format to return
415
- # @return [Boolean]
416
- def save_static_data_point(filename, version = 1)
417
- filename += '.json' if File.extname(filename) == ''
418
-
419
- FileUtils.mkdir_p File.dirname(filename) unless Dir.exist? File.dirname(filename)
420
- File.open(filename, 'w') { |f| f << JSON.pretty_generate(to_static_data_point_hash(version)) }
421
-
422
- true
423
- end
424
-
425
- # save the analysis zip file which contains the measures, seed model, weather file, and init/final scripts
426
- #
427
- # @param filename [String] Name of file to create. It will create the directory and override the file if it exists. If no file extension is given, then it will use .json.
428
- # @return [Boolean]
429
- def save_zip(filename)
430
- filename += '.zip' if File.extname(filename) == ''
431
-
432
- FileUtils.mkdir_p File.dirname(filename) unless Dir.exist? File.dirname(filename)
433
-
434
- save_analysis_zip(filename)
435
- end
436
-
437
-
438
- def save_osa_zip(filename, all_weather_files = false, all_seed_files = false)
439
- filename += '.zip' if File.extname(filename) == ''
440
-
441
- FileUtils.mkdir_p File.dirname(filename) unless Dir.exist? File.dirname(filename)
442
-
443
- save_analysis_zip_osa(filename, all_weather_files, all_seed_files)
444
- end
445
-
446
- # convert an OSW to an OSA
447
- # osw_filename is the full path to the OSW file
448
- # assumes the associated files and directories are in the same location
449
- # /example.osw
450
- # /measures
451
- # /seeds
452
- # /weather
453
- #
454
- def convert_osw(osw_filename, *measure_paths)
455
- # load OSW so we can loop over [:steps]
456
- if File.exist? osw_filename #will this work for both rel and abs paths?
457
- osw = JSON.parse(File.read(osw_filename), symbolize_names: true)
458
- @osw_path = File.expand_path(osw_filename)
459
- else
460
- raise "Could not find workflow file #{osw_filename}"
461
- end
462
-
463
- # set the weather and seed files if set in OSW
464
- # use :file_paths and look for files to set
465
- if osw[:file_paths]
466
- # seed_model, check if in OSW and not found in path search already
467
- if osw[:seed_file]
468
- osw[:file_paths].each do |path|
469
- puts "searching for seed at: #{File.join(File.expand_path(path), osw[:seed_file])}"
470
- if File.exist?(File.join(File.expand_path(path), osw[:seed_file]))
471
- puts "found seed_file: #{osw[:seed_file]}"
472
- self.seed_model = File.join(File.expand_path(path), osw[:seed_file])
473
- break
474
- end
475
- end
476
- else
477
- warn "osw[:seed_file] is not defined"
478
- end
479
-
480
- # weather_file, check if in OSW and not found in path search already
481
- if osw[:weather_file]
482
- osw[:file_paths].each do |path|
483
- puts "searching for weather at: #{File.join(File.expand_path(path), osw[:weather_file])}"
484
- if File.exist?(File.join(File.expand_path(path), osw[:weather_file]))
485
- puts "found weather_file: #{osw[:weather_file]}"
486
- self.weather_file = File.join(File.expand_path(path), osw[:weather_file])
487
- break
488
- end
489
- end
490
- else
491
- warn "osw[:weather_file] is not defined"
492
- end
493
-
494
- # file_paths is not defined in OSW, so warn and try to set
495
- else
496
- warn ":file_paths is not defined in the OSW."
497
- self.weather_file = osw[:weather_file] ? osw[:weather_file] : nil
498
- self.seed_model = osw[:seed_file] ? osw[:seed_file] : nil
499
- end
500
-
501
- #set analysis_type default to Single_Run
502
- self.analysis_type = 'single_run'
503
-
504
- #loop over OSW 'steps' and map over measures
505
- #there is no name/display name in the OSW. Just measure directory name
506
- #read measure.XML from directory to get name / display name
507
- #increment name by +_1 if there are duplicates
508
- #add measure
509
- #change default args to osw arg values
510
-
511
- osw[:steps].each do |step|
512
- #get measure directory
513
- measure_dir = step[:measure_dir_name]
514
- measure_name = measure_dir.split("measures/").last
515
- puts "measure_dir_name: #{measure_name}"
516
- #get XML
517
- # Loop over possible user defined *measure_paths, including the dir of the osw_filename path and :measure_paths, to find the measure,
518
- # then set measure_dir_abs_path to that path
519
- measure_dir_abs_path = ''
520
- paths_to_parse = [File.dirname(osw_filename), osw[:measure_paths], *measure_paths].flatten.compact.map { |path| File.join(File.expand_path(path), measure_dir, 'measure.xml') }
521
- puts "searching for xml's in: #{paths_to_parse}"
522
- xml = {}
523
- paths_to_parse.each do |path|
524
- if File.exist?(path)
525
- puts "found xml: #{path}"
526
- xml = parse_measure_xml(path)
527
- if !xml.empty?
528
- measure_dir_abs_path = path
529
- break
530
- end
531
- end
532
- end
533
- raise "measure #{measure_name} not found" if xml.empty?
534
- puts ""
535
- #add check for previous names _+1
536
- count = 1
537
- name = xml[:name]
538
- display_name = xml[:display_name]
539
- loop do
540
- measure = @workflow.find_measure(name)
541
- break if measure.nil?
542
-
543
- count += 1
544
- name = "#{xml[:name]}_#{count}"
545
- display_name = "#{xml[:display_name]} #{count}"
546
- end
547
- #Add Measure to workflow
548
- @workflow.add_measure_from_path(name, display_name, measure_dir_abs_path) #this forces to an absolute path which seems constent with PAT
549
- #@workflow.add_measure_from_path(name, display_name, measure_dir) #this uses the path in the OSW which could be relative
550
-
551
- #Change the default argument values to the osw values
552
- #1. find measure in @workflow
553
- m = @workflow.find_measure(name)
554
- #2. loop thru osw args
555
- #check if the :argument is missing from the measure step, it shouldnt be but just in case give a clean message
556
- if step[:arguments].nil?
557
- raise "measure #{name} step has no arguments: #{step}"
558
- else
559
- step[:arguments].each do |k,v|
560
- #check if argument is in measure, otherwise setting argument_value will crash
561
- raise "OSW arg: #{k} is not in Measure: #{name}" if m.arguments.find_all { |a| a[:name] == k.to_s }.empty?
562
- #set measure arg to match osw arg
563
- m.argument_value(k.to_s, v)
564
- end
565
- end
566
- end
567
- end
568
-
569
- private
570
-
571
- # New format for OSAs. Package up the seed, weather files, and measures
572
- # filename is the name of the file to be saved. ex: analysis.zip
573
- # it will parse the OSA and zip up all the files defined in the workflow
574
- def save_analysis_zip_osa(filename, all_weather_files = false, all_seed_files = false)
575
- def add_directory_to_zip_osa(zipfile, local_directory, relative_zip_directory)
576
- puts "Add Directory #{local_directory}"
577
- Dir[File.join(local_directory.to_s, '**', '**')].each do |file|
578
- puts "Adding File #{file}"
579
- zipfile.add(file.sub(local_directory, relative_zip_directory), file)
580
- end
581
- zipfile
582
- end
583
- #delete file if exists
584
- FileUtils.rm_f(filename) if File.exist?(filename)
585
- #get the full path to the OSW, since all Files/Dirs should be in same directory as the OSW
586
- puts "osw_path: #{@osw_path}"
587
- osw_full_path = File.dirname(File.expand_path(@osw_path))
588
- puts "osw_full_path: #{osw_full_path}"
589
-
590
- Zip::File.open(filename, create: true) do |zf|
591
- ## Weather files
592
- puts 'Adding Support Files: Weather'
593
- # check if weather file exists. use abs path. remove leading ./ from @weather_file path if there.
594
- # check if path is already absolute
595
- if @weather_file[:file]
596
- if File.exists?(@weather_file[:file])
597
- puts " Adding #{@weather_file[:file]}"
598
- #zf.add("weather/#{File.basename(@weather_file[:file])}", @weather_file[:file])
599
- base_name = File.basename(@weather_file[:file], ".*")
600
- puts "base_name: #{base_name}"
601
- # convert backslash on windows to forward slash so Dir.glob will work (in case user uses \)
602
- weather_dirname = File.dirname(@weather_file[:file]).gsub("\\", "/")
603
- puts "weather_dirname: #{weather_dirname}"
604
- # If all_weather_files is true, add all files in the directory to the zip.
605
- # Otherwise, add only files that match the base name.
606
- file_pattern = all_weather_files ? "*" : "#{base_name}.*"
607
- Dir.glob(File.join(weather_dirname, file_pattern)) do |file_path|
608
- puts "file_path: #{file_path}"
609
- puts "zip path: weather/#{File.basename(file_path)}"
610
- zf.add("weather/#{File.basename(file_path)}", file_path)
611
- end
612
- # make absolute path and check for file
613
- elsif File.exists?(File.join(osw_full_path,@weather_file[:file].sub(/^\.\//, '')))
614
- puts " Adding: #{File.join(osw_full_path,@weather_file[:file].sub(/^\.\//, ''))}"
615
- #zf.add("weather/#{File.basename(@weather_file[:file])}", File.join(osw_full_path,@weather_file[:file].sub(/^\.\//, '')))
616
- base_name = File.basename(@weather_file[:file].sub(/^\.\//, ''), ".*")
617
- puts "base_name2: #{base_name}"
618
- weather_dirname = File.dirname(File.join(osw_full_path,@weather_file[:file].sub(/^\.\//, ''))).gsub("\\", "/")
619
- puts "weather_dirname: #{weather_dirname}"
620
- file_pattern = all_weather_files ? "*" : "#{base_name}.*"
621
- Dir.glob(File.join(weather_dirname, file_pattern)) do |file_path|
622
- puts "file_path2: #{file_path}"
623
- puts "zip path2: weather/#{File.basename(file_path)}"
624
- zf.add("weather/#{File.basename(file_path)}", file_path)
625
- end
626
- else
627
- raise "weather_file[:file] does not exist at: #{File.join(osw_full_path,@weather_file[:file].sub(/^\.\//, ''))}"
628
- end
629
- else
630
- warn "weather_file[:file] is not defined"
631
- end
632
-
633
- ## Seed files
634
- puts 'Adding Support Files: Seed Models'
635
- #check if seed file exists. use abs path. remove leading ./ from @seed_model path if there.
636
- #check if path is already absolute
637
- if @seed_model[:file]
638
- if File.exists?(@seed_model[:file])
639
- puts " Adding #{@seed_model[:file]}"
640
- zf.add("seeds/#{File.basename(@seed_model[:file])}", @seed_model[:file])
641
- if all_seed_files
642
- seed_dirname = File.dirname(@seed_model[:file]).gsub("\\", "/")
643
- puts "seed_dirname: #{seed_dirname}"
644
- Dir.glob(File.join(seed_dirname, '*')) do |file_path|
645
- next if file_path == @seed_model[:file] # Skip if the file is the same as @seed_model[:file] so not added twice
646
- puts "file_path: #{file_path}"
647
- puts "zip path: seeds/#{File.basename(file_path)}"
648
- zf.add("seeds/#{File.basename(file_path)}", file_path)
649
- end
650
- end
651
- #make absolute path and check for file
652
- elsif File.exists?(File.join(osw_full_path,@seed_model[:file].sub(/^\.\//, '')))
653
- puts " Adding #{File.join(osw_full_path,@seed_model[:file].sub(/^\.\//, ''))}"
654
- zf.add("seeds/#{File.basename(@seed_model[:file])}", File.join(osw_full_path,@seed_model[:file].sub(/^\.\//, '')))
655
- if all_seed_files
656
- seed_dirname = File.dirname(File.join(osw_full_path,@seed_model[:file].sub(/^\.\//, ''))).gsub("\\", "/")
657
- puts "seed_dirname: #{seed_dirname}"
658
- Dir.glob(File.join(seed_dirname, '*')) do |file_path|
659
- next if file_path == File.join(osw_full_path,@seed_model[:file].sub(/^\.\//, '')) # Skip if the file is the same as @seed_model[:file] so not added twice
660
- puts "file_path: #{file_path}"
661
- puts "zip path: seeds/#{File.basename(file_path)}"
662
- zf.add("seeds/#{File.basename(file_path)}", file_path)
663
- end
664
- end
665
- else
666
- raise "seed_file[:file] does not exist at: #{File.join(osw_full_path,@seed_model[:file].sub(/^\.\//, ''))}"
667
- end
668
- else
669
- warn "seed_file[:file] is not defined"
670
- end
671
-
672
- puts 'Adding Support Files: Libraries'
673
- @libraries.each do |lib|
674
- raise "Libraries must specify their 'library_name' as metadata which becomes the directory upon zip" unless lib[:metadata][:library_name]
675
-
676
- if File.directory? lib[:file]
677
- Dir[File.join(lib[:file], '**', '**')].each do |file|
678
- puts " Adding #{file}"
679
- zf.add(file.sub(lib[:file], "lib/#{lib[:metadata][:library_name]}"), file)
680
- end
681
- else
682
- # just add the file to the zip
683
- puts " Adding #{lib[:file]}"
684
- zf.add(lib[:file], "lib/#{File.basename(lib[:file])}", lib[:file])
685
- end
686
- end
687
-
688
- puts 'Adding Support Files: Server Scripts'
689
- @server_scripts.each_with_index do |f, index|
690
- if f[:init_or_final] == 'finalization'
691
- file_name = 'finalization.sh'
692
- else
693
- file_name = 'initialization.sh'
694
- end
695
- if f[:server_or_data_point] == 'analysis'
696
- new_name = "scripts/analysis/#{file_name}"
697
- else
698
- new_name = "scripts/data_point/#{file_name}"
699
- end
700
- puts " Adding #{f[:file]} as #{new_name}"
701
- zf.add(new_name, f[:file])
702
-
703
- if f[:arguments]
704
- arg_file = "#{(new_name.sub(/\.sh\z/, ''))}.args"
705
- puts " Adding arguments as #{arg_file}"
706
- file = Tempfile.new('arg')
707
- file.write(f[:arguments])
708
- zf.add(arg_file, file)
709
- file.close
710
- end
711
- end
712
-
713
- ## Measures
714
- puts 'Adding Measures'
715
- added_measures = []
716
- # The list of the measures should always be there, but make sure they are uniq
717
- @workflow.each do |measure|
718
- measure_dir_to_add = measure.measure_definition_directory_local
719
-
720
- next if added_measures.include? measure_dir_to_add
721
-
722
- puts " Adding #{File.basename(measure_dir_to_add)}"
723
- Dir[File.join(measure_dir_to_add, '**')].each do |file|
724
- if File.directory?(file)
725
- if File.basename(file) == 'resources' || File.basename(file) == 'lib'
726
- #remove leading ./ from measure_definition_directory path if there.
727
- add_directory_to_zip_osa(zf, file, "#{measure.measure_definition_directory.sub(/^\.\//, '')}/#{File.basename(file)}")
728
- end
729
- else
730
- puts " Adding File #{file}"
731
- #remove leading ./ from measure.measure_definition_directory string with regex .sub(/^\.\//, '')
732
- zip_path_for_measures = file.sub(measure_dir_to_add, measure.measure_definition_directory.sub(/^\.\//, ''))
733
- #puts " zip_path_for_measures: #{zip_path_for_measures}"
734
- zf.add(zip_path_for_measures, file)
735
- end
736
- end
737
-
738
- added_measures << measure_dir_to_add
739
- end
740
- end
741
- end
742
-
743
- #keep legacy function
744
- # Package up the seed, weather files, and measures
745
- def save_analysis_zip(filename)
746
- def add_directory_to_zip(zipfile, local_directory, relative_zip_directory)
747
- # puts "Add Directory #{local_directory}"
748
- Dir[File.join(local_directory.to_s, '**', '**')].each do |file|
749
- # puts "Adding File #{file}"
750
- zipfile.add(file.sub(local_directory, relative_zip_directory), file)
751
- end
752
- zipfile
753
- end
754
-
755
- FileUtils.rm_f(filename) if File.exist?(filename)
756
-
757
- Zip::File.open(filename, Zip::File::CREATE) do |zf|
758
- ## Weather files
759
- # TODO: eventually remove the @weather_file attribute and grab the weather file out
760
- # of the @weather_files
761
- puts 'Adding Support Files: Weather'
762
- if @weather_file[:file] && !@weather_files.files.find { |f| @weather_file[:file] == f[:file] }
763
- # manually add the weather file
764
- puts " Adding #{@weather_file[:file]}"
765
- zf.add("./weather/#{File.basename(@weather_file[:file])}", @weather_file[:file])
766
- end
767
- @weather_files.each do |f|
768
- puts " Adding #{f[:file]}"
769
- zf.add("./weather/#{File.basename(f[:file])}", f[:file])
770
- end
771
-
772
- ## Seed files
773
- puts 'Adding Support Files: Seed Models'
774
- if @seed_model[:file] && !@seed_models.files.find { |f| @seed_model[:file] == f[:file] }
775
- # manually add the weather file
776
- puts " Adding #{@seed_model[:file]}"
777
- zf.add("./seed/#{File.basename(@seed_model[:file])}", @seed_model[:file])
778
- end
779
- @seed_models.each do |f|
780
- puts " Adding #{f[:file]}"
781
- zf.add("./seed/#{File.basename(f[:file])}", f[:file])
782
- end
783
-
784
- puts 'Adding Support Files: Libraries'
785
- @libraries.each do |lib|
786
- raise "Libraries must specify their 'library_name' as metadata which becomes the directory upon zip" unless lib[:metadata][:library_name]
787
-
788
- if File.directory? lib[:file]
789
- Dir[File.join(lib[:file], '**', '**')].each do |file|
790
- puts " Adding #{file}"
791
- zf.add(file.sub(lib[:file], "./lib/#{lib[:metadata][:library_name]}/"), file)
792
- end
793
- else
794
- # just add the file to the zip
795
- puts " Adding #{lib[:file]}"
796
- zf.add(lib[:file], "./lib/#{File.basename(lib[:file])}", lib[:file])
797
- end
798
- end
799
-
800
- puts 'Adding Support Files: Worker Initialization Scripts'
801
- @worker_inits.each_with_index do |f, index|
802
- ordered_file_name = "#{index.to_s.rjust(2, '0')}_#{File.basename(f[:file])}"
803
- puts " Adding #{f[:file]} as #{ordered_file_name}"
804
- zf.add(f[:file].sub(f[:file], "./scripts/worker_initialization//#{ordered_file_name}"), f[:file])
805
-
806
- if f[:metadata][:args]
807
- arg_file = "#{File.basename(ordered_file_name, '.*')}.args"
808
- file = Tempfile.new('arg')
809
- file.write(f[:metadata][:args])
810
- zf.add("./scripts/worker_initialization/#{arg_file}", file)
811
- file.close
812
- end
813
- end
814
-
815
- puts 'Adding Support Files: Worker Finalization Scripts'
816
- @worker_finalizes.each_with_index do |f, index|
817
- ordered_file_name = "#{index.to_s.rjust(2, '0')}_#{File.basename(f[:file])}"
818
- puts " Adding #{f[:file]} as #{ordered_file_name}"
819
- zf.add(f[:file].sub(f[:file], "scripts/worker_finalization/#{ordered_file_name}"), f[:file])
820
-
821
- if f[:metadata][:args]
822
- arg_file = "#{File.basename(ordered_file_name, '.*')}.args"
823
- file = Tempfile.new('arg')
824
- file.write(f[:metadata][:args])
825
- zf.add("scripts/worker_finalization/#{arg_file}", file)
826
- file.close
827
- end
828
- end
829
-
830
- ## Measures
831
- puts 'Adding Measures'
832
- added_measures = []
833
- # The list of the measures should always be there, but make sure they are uniq
834
- @workflow.each do |measure|
835
- measure_dir_to_add = measure.measure_definition_directory_local
836
-
837
- next if added_measures.include? measure_dir_to_add
838
-
839
- puts " Adding #{File.basename(measure_dir_to_add)}"
840
- Dir[File.join(measure_dir_to_add, '**')].each do |file|
841
- if File.directory?(file)
842
- if File.basename(file) == 'resources' || File.basename(file) == 'lib'
843
- add_directory_to_zip(zf, file, "#{measure.measure_definition_directory}/#{File.basename(file)}")
844
- end
845
- else
846
- # puts "Adding File #{file}"
847
- zf.add(file.sub(measure_dir_to_add, "#{measure.measure_definition_directory}/"), file)
848
- end
849
- end
850
-
851
- added_measures << measure_dir_to_add
852
- end
853
- end
854
- end
855
- end
856
- end
857
- end
1
+ # *******************************************************************************
2
+ # OpenStudio(R), Copyright (c) Alliance for Sustainable Energy, LLC.
3
+ # See also https://openstudio.net/license
4
+ # *******************************************************************************
5
+
6
+ # OpenStudio formulation class handles the generation of the OpenStudio Analysis format.
7
+ module OpenStudio
8
+ module Analysis
9
+ SeedModel = Struct.new(:file)
10
+ WeatherFile = Struct.new(:file)
11
+
12
+ @@measure_paths = ['./measures']
13
+ # List of paths to look for measures when adding them. This currently only is used when loading an
14
+ # analysis hash file. It looks in the order of the measure_paths. As soon as it finds one, it stops.
15
+ def self.measure_paths
16
+ @@measure_paths
17
+ end
18
+
19
+ def self.measure_paths=(new_array)
20
+ @@measure_paths = new_array
21
+ end
22
+
23
+ class Formulation
24
+ attr_reader :seed_model
25
+ attr_reader :weather_file
26
+ attr_reader :analysis_type
27
+ attr_reader :outputs
28
+ attr_accessor :display_name
29
+ attr_accessor :workflow
30
+ attr_accessor :algorithm
31
+ attr_accessor :osw_path
32
+ attr_accessor :download_zip
33
+ attr_accessor :download_reports
34
+ attr_accessor :download_osw
35
+ attr_accessor :download_osm
36
+ attr_accessor :cli_debug
37
+ attr_accessor :cli_verbose
38
+ attr_accessor :initialize_worker_timeout
39
+ attr_accessor :run_workflow_timeout
40
+ attr_accessor :upload_results_timeout
41
+
42
+
43
+ # the attributes below are used for packaging data into the analysis zip file
44
+ attr_reader :weather_files
45
+ attr_reader :seed_models
46
+ attr_reader :worker_inits
47
+ attr_reader :worker_finalizes
48
+ attr_reader :libraries
49
+ attr_reader :server_scripts
50
+ attr_reader :gem_files
51
+
52
+ # Create an instance of the OpenStudio::Analysis::Formulation
53
+ #
54
+ # @param display_name [String] Display name of the project.
55
+ # @return [Object] An OpenStudio::Analysis::Formulation object
56
+ def initialize(display_name)
57
+ @display_name = display_name
58
+ @analysis_type = nil
59
+ @outputs = []
60
+ @workflow = OpenStudio::Analysis::Workflow.new
61
+ # Initialize child objects (expect workflow)
62
+ @weather_file = WeatherFile.new
63
+ @seed_model = SeedModel.new
64
+ @algorithm = OpenStudio::Analysis::AlgorithmAttributes.new
65
+ @download_zip = true
66
+ @download_reports = true
67
+ @download_osw = true
68
+ @download_osm = true
69
+ @cli_debug = "--debug"
70
+ @cli_verbose = "--verbose"
71
+ @initialize_worker_timeout = 28800
72
+ @run_workflow_timeout = 28800
73
+ @upload_results_timeout = 28800
74
+
75
+ # Analysis Zip attributes
76
+ @weather_files = SupportFiles.new
77
+ @gem_files = SupportFiles.new
78
+ @seed_models = SupportFiles.new
79
+ @worker_inits = SupportFiles.new
80
+ @worker_finalizes = SupportFiles.new
81
+ @libraries = SupportFiles.new
82
+ @server_scripts = ServerScripts.new
83
+ end
84
+
85
+ # Define the type of analysis which is going to be running
86
+ #
87
+ # @param name [String] Name of the algorithm/analysis. (e.g. rgenoud, lhs, single_run)
88
+ # allowed values are ANALYSIS_TYPES = ['spea_nrel', 'rgenoud', 'nsga_nrel', 'lhs', 'preflight',
89
+ # 'morris', 'sobol', 'doe', 'fast99', 'ga', 'gaisl',
90
+ # 'single_run', 'repeat_run', 'batch_run']
91
+ def analysis_type=(value)
92
+ if OpenStudio::Analysis::AlgorithmAttributes::ANALYSIS_TYPES.include?(value)
93
+ @analysis_type = value
94
+ else
95
+ raise "Invalid analysis type. Allowed types: #{OpenStudio::Analysis::AlgorithmAttributes::ANALYSIS_TYPES}"
96
+ end
97
+ end
98
+
99
+ # Path to the seed model
100
+ #
101
+ # @param path [String] Path to the seed model. This should be relative.
102
+ def seed_model=(file)
103
+ @seed_model[:file] = file
104
+ end
105
+
106
+ # Path to the weather file (or folder). If it is a folder, then the measures will look for the weather file
107
+ # by name in that folder.
108
+ #
109
+ # @param path [String] Path to the weather file or folder.
110
+ def weather_file=(file)
111
+ @weather_file[:file] = file
112
+ end
113
+
114
+ # Set the value for 'download_zip'
115
+ #
116
+ # @param value [Boolean] The value for 'download_zip'
117
+ def download_zip=(value)
118
+ if [true, false].include?(value)
119
+ @download_zip = value
120
+ else
121
+ raise ArgumentError, "Invalid value for 'download_zip'. Only true or false allowed."
122
+ end
123
+ end
124
+
125
+ # Set the value for 'download_reports'
126
+ #
127
+ # @param value [Boolean] The value for 'download_reports'
128
+ def download_reports=(value)
129
+ if [true, false].include?(value)
130
+ @download_reports = value
131
+ else
132
+ raise ArgumentError, "Invalid value for 'download_reports'. Only true or false allowed."
133
+ end
134
+ end
135
+
136
+ # Set the value for 'download_osw'
137
+ #
138
+ # @param value [Boolean] The value for 'download_osw'
139
+ def download_osw=(value)
140
+ if [true, false].include?(value)
141
+ @download_osw = value
142
+ else
143
+ raise ArgumentError, "Invalid value for 'download_osw'. Only true or false allowed."
144
+ end
145
+ end
146
+
147
+ # Set the value for 'download_osm'
148
+ #
149
+ # @param value [Boolean] The value for 'download_osm'
150
+ def download_osm=(value)
151
+ if [true, false].include?(value)
152
+ @download_osm = value
153
+ else
154
+ raise ArgumentError, "Invalid value for 'download_osm'. Only true or false allowed."
155
+ end
156
+ end
157
+
158
+ # Set the value for 'cli_debug'
159
+ #
160
+ # @param value [Boolean] The value for 'cli_debug'
161
+ def cli_debug=(value)
162
+ @cli_debug = value
163
+ end
164
+
165
+ # Set the value for 'cli_verbose'
166
+ #
167
+ # @param value [Boolean] The value for 'cli_verbose'
168
+ def cli_verbose=(value)
169
+ @cli_verbose = value
170
+ end
171
+
172
+ # Set the value for 'run_workflow_timeout'
173
+ #
174
+ # @param value [Integer] The value for 'run_workflow_timeout'
175
+ def run_workflow_timeout=(value)
176
+ if value.is_a?(Integer)
177
+ @run_workflow_timeout = value
178
+ else
179
+ raise ArgumentError, "Invalid value for 'run_workflow_timeout'. Only integer values allowed."
180
+ end
181
+ end
182
+
183
+ # Set the value for 'initialize_worker_timeout'
184
+ #
185
+ # @param value [Integer] The value for 'initialize_worker_timeout'
186
+ def initialize_worker_timeout=(value)
187
+ if value.is_a?(Integer)
188
+ @initialize_worker_timeout = value
189
+ else
190
+ raise ArgumentError, "Invalid value for 'initialize_worker_timeout'. Only integer values allowed."
191
+ end
192
+ end
193
+
194
+ # Set the value for 'upload_results_timeout'
195
+ #
196
+ # @param value [Integer] The value for 'upload_results_timeout'
197
+ def upload_results_timeout=(value)
198
+ if value.is_a?(Integer)
199
+ @upload_results_timeout = value
200
+ else
201
+ raise ArgumentError, "Invalid value for 'upload_results_timeout'. Only integer values allowed."
202
+ end
203
+ end
204
+
205
+ # Add an output of interest to the problem formulation
206
+ #
207
+ # @param output_hash [Hash] Hash of the output variable in the legacy format
208
+ # @option output_hash [String] :display_name Name to display
209
+ # @option output_hash [String] :display_name_short A shorter display name
210
+ # @option output_hash [String] :metadata_id Link to DEnCity ID in which this output corresponds
211
+ # @option output_hash [String] :name Unique machine name of the variable. Typically this is measure.attribute
212
+ # @option output_hash [String] :export Export the variable to CSV and dataframes from OpenStudio-server
213
+ # @option output_hash [String] :visualize Visualize the variable in the plots on OpenStudio-server
214
+ # @option output_hash [String] :units Units of the variable as a string
215
+ # @option output_hash [String] :variable_type Data type of the variable
216
+ # @option output_hash [Boolean] :objective_function Whether or not this output is an objective function. Default: false
217
+ # @option output_hash [Integer] :objective_function_index Index of the objective function. Default: nil
218
+ # @option output_hash [Float] :objective_function_target Target for the objective function to reach (if defined). Default: nil
219
+ # @option output_hash [Float] :scaling_factor How to scale the objective function(s). Default: nil
220
+ # @option output_hash [Integer] :objective_function_group If grouping objective functions, then group ID. Default: nil
221
+ def add_output(output_hash)
222
+ # Check if the name is already been added.
223
+ exist = @outputs.find_index { |o| o[:name] == output_hash[:name] }
224
+ # if so, update the fields but keep objective_function_index the same
225
+ if exist
226
+ original = @outputs[exist]
227
+ if original[:objective_function] && !output_hash[:objective_function]
228
+ return @outputs
229
+ end
230
+ output = original.merge(output_hash)
231
+ output[:objective_function_index] = original[:objective_function_index]
232
+ @outputs[exist] = output
233
+ else
234
+ output = {
235
+ units: '',
236
+ objective_function: false,
237
+ objective_function_index: nil,
238
+ objective_function_target: nil,
239
+ #set default to nil or 1 if objective_function is true and this is not set
240
+ objective_function_group: (output_hash[:objective_function] ? 1 : nil),
241
+ scaling_factor: nil,
242
+ #set default to false or true if objective_function is true and this is not set
243
+ visualize: (output_hash[:objective_function] ? true : false),
244
+ metadata_id: nil,
245
+ export: true,
246
+ }.merge(output_hash)
247
+ #set display_name default to be name if its not set
248
+ output[:display_name] = output_hash[:display_name] ? output_hash[:display_name] : output_hash[:name]
249
+ #set display_name_short default to be display_name if its not set, this can be null if :display_name not set
250
+ output[:display_name_short] = output_hash[:display_name_short] ? output_hash[:display_name_short] : output_hash[:display_name]
251
+ # if the variable is an objective_function, then increment and
252
+ # assign and objective function index
253
+ if output[:objective_function]
254
+ values = @outputs.select { |o| o[:objective_function] }
255
+ output[:objective_function_index] = values.size
256
+ end
257
+
258
+ @outputs << output
259
+ end
260
+
261
+ @outputs
262
+ end
263
+
264
+ # return the machine name of the analysis
265
+ def name
266
+ @display_name.to_underscore
267
+ end
268
+
269
+ # return a hash.
270
+ #
271
+ # @param version [Integer] Version of the format to return
272
+ # @return [Hash]
273
+ def to_hash(version = 1)
274
+ # fail 'Must define an analysis type' unless @analysis_type
275
+ if version == 1
276
+ h = {
277
+ analysis: {
278
+ display_name: @display_name,
279
+ name: name,
280
+ output_variables: @outputs,
281
+ problem: {
282
+ analysis_type: @analysis_type,
283
+ algorithm: algorithm.to_hash(version),
284
+ workflow: workflow.to_hash(version)
285
+ }
286
+ }
287
+ }
288
+
289
+ if @seed_model[:file]
290
+ h[:analysis][:seed] = {
291
+ file_type: File.extname(@seed_model[:file]).delete('.').upcase,
292
+ path: "./seed/#{File.basename(@seed_model[:file])}"
293
+ }
294
+ else
295
+ h[:analysis][:seed] = nil
296
+ end
297
+
298
+ # silly catch for if weather_file is not set
299
+ wf = nil
300
+ if @weather_file[:file]
301
+ wf = @weather_file
302
+ elsif !@weather_files.empty?
303
+ # get the first EPW file (not the first file)
304
+ wf = @weather_files.find { |w| File.extname(w[:file]).casecmp('.epw').zero? }
305
+ end
306
+
307
+ if wf
308
+ h[:analysis][:weather_file] = {
309
+ file_type: File.extname(wf[:file]).delete('.').upcase,
310
+ path: "./weather/#{File.basename(wf[:file])}"
311
+ }
312
+ else
313
+ # log: could not find weather file
314
+ warn 'Could not resolve a valid weather file. Check paths to weather files'
315
+ end
316
+
317
+ h[:analysis][:file_format_version] = version
318
+ h[:analysis][:cli_debug] = @cli_debug
319
+ h[:analysis][:cli_verbose] = @cli_verbose
320
+ h[:analysis][:run_workflow_timeout] = @run_workflow_timeout
321
+ h[:analysis][:upload_results_timeout] = @upload_results_timeout
322
+ h[:analysis][:initialize_worker_timeout] = @initialize_worker_timeout
323
+ h[:analysis][:download_zip] = @download_zip
324
+ h[:analysis][:download_reports] = @download_reports
325
+ h[:analysis][:download_osw] = @download_osw
326
+ h[:analysis][:download_osm] = @download_osm
327
+
328
+ # If there are Gemfiles, then set the hash to use the :gemfile. The zip file method will
329
+ # add them to the root of the zip file.
330
+ if @gem_files.size.positive?
331
+ h[:analysis][:gemfile] = true
332
+ else
333
+ h[:analysis][:gemfile] = false
334
+ end
335
+
336
+ #-BLB I dont think this does anything. server_scripts are run if they are in
337
+ #the /scripts/analysis or /scripts/data_point directories
338
+ #but nothing is ever checked in the OSA.
339
+ h[:analysis][:server_scripts] = {}
340
+
341
+ # This is a hack right now, but after the initial hash is created go back and add in the objective functions
342
+ # to the the algorithm as defined in the output_variables list
343
+ ofs = @outputs.map { |i| i[:name] if i[:objective_function] }.compact
344
+ if h[:analysis][:problem][:algorithm]
345
+ h[:analysis][:problem][:algorithm][:objective_functions] = ofs
346
+ end
347
+
348
+
349
+
350
+ h
351
+ else
352
+ raise "Version #{version} not defined for #{self.class} and #{__method__}"
353
+ end
354
+ end
355
+
356
+ # Load the analysis JSON from a hash (with symbolized keys)
357
+ def self.from_hash(h, seed_dir = nil, weather_dir = nil)
358
+ o = OpenStudio::Analysis::Formulation.new(h[:analysis][:display_name])
359
+
360
+ version = 1
361
+ if version == 1
362
+ h[:analysis][:output_variables].each do |ov|
363
+ o.add_output(ov)
364
+ end
365
+
366
+ o.workflow = OpenStudio::Analysis::Workflow.load(workflow: h[:analysis][:problem][:workflow])
367
+
368
+ if weather_dir
369
+ o.weather_file "#{weather_path}/#{File.basename(h[:analysis][:weather_file][:path])}"
370
+ else
371
+ o.weather_file = h[:analysis][:weather_file][:path]
372
+ end
373
+
374
+ if seed_dir
375
+ o.seed_model "#{weather_path}/#{File.basename(h[:analysis][:seed][:path])}"
376
+ else
377
+ o.seed_model = h[:analysis][:seed][:path]
378
+ end
379
+ else
380
+ raise "Version #{version} not defined for #{self.class} and #{__method__}"
381
+ end
382
+
383
+ o
384
+ end
385
+
386
+ # return a hash of the data point with the static variables set
387
+ #
388
+ # @param version [Integer] Version of the format to return
389
+ # @return [Hash]
390
+ def to_static_data_point_hash(version = 1)
391
+ if version == 1
392
+ static_hash = {}
393
+ # TODO: this method should be on the workflow step and bubbled up to this interface
394
+ @workflow.items.map do |item|
395
+ item.variables.map { |v| static_hash[v[:uuid]] = v[:static_value] }
396
+ end
397
+
398
+ h = {
399
+ data_point: {
400
+ set_variable_values: static_hash,
401
+ status: 'na',
402
+ uuid: SecureRandom.uuid
403
+ }
404
+ }
405
+ h
406
+ end
407
+ end
408
+
409
+ # save the file to JSON. Will overwrite the file if it already exists
410
+ #
411
+ # @param filename [String] Name of file to create. It will create the directory and override the file if it exists. If no file extension is given, then it will use .json.
412
+ # @param version [Integer] Version of the format to return
413
+ # @return [Boolean]
414
+ def save(filename, version = 1)
415
+ filename += '.json' if File.extname(filename) == ''
416
+
417
+ FileUtils.mkdir_p File.dirname(filename) unless Dir.exist? File.dirname(filename)
418
+ File.open(filename, 'w') { |f| f << JSON.pretty_generate(to_hash(version)) }
419
+
420
+ true
421
+ end
422
+
423
+ # save the data point JSON with the variables set to the static values. Will overwrite the file if it already exists
424
+ #
425
+ # @param filename [String] Name of file to create. It will create the directory and override the file if it exists. If no file extension is given, then it will use .json.
426
+ # @param version [Integer] Version of the format to return
427
+ # @return [Boolean]
428
+ def save_static_data_point(filename, version = 1)
429
+ filename += '.json' if File.extname(filename) == ''
430
+
431
+ FileUtils.mkdir_p File.dirname(filename) unless Dir.exist? File.dirname(filename)
432
+ File.open(filename, 'w') { |f| f << JSON.pretty_generate(to_static_data_point_hash(version)) }
433
+
434
+ true
435
+ end
436
+
437
+ # save the analysis zip file which contains the measures, seed model, weather file, and init/final scripts
438
+ #
439
+ # @param filename [String] Name of file to create. It will create the directory and override the file if it exists. If no file extension is given, then it will use .json.
440
+ # @return [Boolean]
441
+ def save_zip(filename)
442
+ filename += '.zip' if File.extname(filename) == ''
443
+
444
+ FileUtils.mkdir_p File.dirname(filename) unless Dir.exist? File.dirname(filename)
445
+
446
+ save_analysis_zip(filename)
447
+ end
448
+
449
+
450
+ def save_osa_zip(filename, all_weather_files = false, all_seed_files = false)
451
+ filename += '.zip' if File.extname(filename) == ''
452
+
453
+ FileUtils.mkdir_p File.dirname(filename) unless Dir.exist? File.dirname(filename)
454
+
455
+ save_analysis_zip_osa(filename, all_weather_files, all_seed_files)
456
+ end
457
+
458
+ # convert an OSW to an OSA
459
+ # osw_filename is the full path to the OSW file
460
+ # assumes the associated files and directories are in the same location
461
+ # /example.osw
462
+ # /measures
463
+ # /seeds
464
+ # /weather
465
+ #
466
+ def convert_osw(osw_filename, *measure_paths)
467
+ # load OSW so we can loop over [:steps]
468
+ if File.exist? osw_filename #will this work for both rel and abs paths?
469
+ osw = JSON.parse(File.read(osw_filename), symbolize_names: true)
470
+ @osw_path = File.expand_path(osw_filename)
471
+ else
472
+ raise "Could not find workflow file #{osw_filename}"
473
+ end
474
+
475
+ # set the weather and seed files if set in OSW
476
+ # use :file_paths and look for files to set
477
+ if osw[:file_paths]
478
+ # seed_model, check if in OSW and not found in path search already
479
+ if osw[:seed_file]
480
+ osw[:file_paths].each do |path|
481
+ puts "searching for seed at: #{File.join(File.expand_path(path), osw[:seed_file])}"
482
+ if File.exist?(File.join(File.expand_path(path), osw[:seed_file]))
483
+ puts "found seed_file: #{osw[:seed_file]}"
484
+ self.seed_model = File.join(File.expand_path(path), osw[:seed_file])
485
+ break
486
+ end
487
+ end
488
+ else
489
+ warn "osw[:seed_file] is not defined"
490
+ end
491
+
492
+ # weather_file, check if in OSW and not found in path search already
493
+ if osw[:weather_file]
494
+ osw[:file_paths].each do |path|
495
+ puts "searching for weather at: #{File.join(File.expand_path(path), osw[:weather_file])}"
496
+ if File.exist?(File.join(File.expand_path(path), osw[:weather_file]))
497
+ puts "found weather_file: #{osw[:weather_file]}"
498
+ self.weather_file = File.join(File.expand_path(path), osw[:weather_file])
499
+ break
500
+ end
501
+ end
502
+ else
503
+ warn "osw[:weather_file] is not defined"
504
+ end
505
+
506
+ # file_paths is not defined in OSW, so warn and try to set
507
+ else
508
+ warn ":file_paths is not defined in the OSW."
509
+ self.weather_file = osw[:weather_file] ? osw[:weather_file] : nil
510
+ self.seed_model = osw[:seed_file] ? osw[:seed_file] : nil
511
+ end
512
+
513
+ #set analysis_type default to Single_Run
514
+ self.analysis_type = 'single_run'
515
+
516
+ #loop over OSW 'steps' and map over measures
517
+ #there is no name/display name in the OSW. Just measure directory name
518
+ #read measure.XML from directory to get name / display name
519
+ #increment name by +_1 if there are duplicates
520
+ #add measure
521
+ #change default args to osw arg values
522
+
523
+ osw[:steps].each do |step|
524
+ #get measure directory
525
+ measure_dir = step[:measure_dir_name]
526
+ measure_name = measure_dir.split("measures/").last
527
+ puts "measure_dir_name: #{measure_name}"
528
+ #get XML
529
+ # Loop over possible user defined *measure_paths, including the dir of the osw_filename path and :measure_paths, to find the measure,
530
+ # then set measure_dir_abs_path to that path
531
+ measure_dir_abs_path = ''
532
+ paths_to_parse = [File.dirname(osw_filename), osw[:measure_paths], *measure_paths].flatten.compact.map { |path| File.join(File.expand_path(path), measure_dir, 'measure.xml') }
533
+ puts "searching for xml's in: #{paths_to_parse}"
534
+ xml = {}
535
+ paths_to_parse.each do |path|
536
+ if File.exist?(path)
537
+ puts "found xml: #{path}"
538
+ xml = parse_measure_xml(path)
539
+ if !xml.empty?
540
+ measure_dir_abs_path = path
541
+ break
542
+ end
543
+ end
544
+ end
545
+ raise "measure #{measure_name} not found" if xml.empty?
546
+ puts ""
547
+ #add check for previous names _+1
548
+ count = 1
549
+ name = xml[:name]
550
+ display_name = xml[:display_name]
551
+ loop do
552
+ measure = @workflow.find_measure(name)
553
+ break if measure.nil?
554
+
555
+ count += 1
556
+ name = "#{xml[:name]}_#{count}"
557
+ display_name = "#{xml[:display_name]} #{count}"
558
+ end
559
+ #Add Measure to workflow
560
+ @workflow.add_measure_from_path(name, display_name, measure_dir_abs_path) #this forces to an absolute path which seems constent with PAT
561
+ #@workflow.add_measure_from_path(name, display_name, measure_dir) #this uses the path in the OSW which could be relative
562
+
563
+ #Change the default argument values to the osw values
564
+ #1. find measure in @workflow
565
+ m = @workflow.find_measure(name)
566
+ #2. loop thru osw args
567
+ #check if the :argument is missing from the measure step, it shouldnt be but just in case give a clean message
568
+ if step[:arguments].nil?
569
+ raise "measure #{name} step has no arguments: #{step}"
570
+ else
571
+ step[:arguments].each do |k,v|
572
+ #check if argument is in measure, otherwise setting argument_value will crash
573
+ raise "OSW arg: #{k} is not in Measure: #{name}" if m.arguments.find_all { |a| a[:name] == k.to_s }.empty?
574
+ #set measure arg to match osw arg
575
+ m.argument_value(k.to_s, v)
576
+ end
577
+ end
578
+ end
579
+ end
580
+
581
+ private
582
+
583
+ # New format for OSAs. Package up the seed, weather files, and measures
584
+ # filename is the name of the file to be saved. ex: analysis.zip
585
+ # it will parse the OSA and zip up all the files defined in the workflow
586
+ def save_analysis_zip_osa(filename, all_weather_files = false, all_seed_files = false)
587
+ def add_directory_to_zip_osa(zipfile, local_directory, relative_zip_directory)
588
+ puts "Add Directory #{local_directory}"
589
+ Dir[File.join(local_directory.to_s, '**', '**')].each do |file|
590
+ puts "Adding File #{file}"
591
+ zipfile.add(file.sub(local_directory, relative_zip_directory), file)
592
+ end
593
+ zipfile
594
+ end
595
+ #delete file if exists
596
+ FileUtils.rm_f(filename) if File.exist?(filename)
597
+ #get the full path to the OSW, since all Files/Dirs should be in same directory as the OSW
598
+ puts "osw_path: #{@osw_path}"
599
+ osw_full_path = File.dirname(File.expand_path(@osw_path))
600
+ puts "osw_full_path: #{osw_full_path}"
601
+
602
+ Zip::File.open(filename, create: true) do |zf|
603
+ ## Weather files
604
+ puts 'Adding Support Files: Weather'
605
+ # check if weather file exists. use abs path. remove leading ./ from @weather_file path if there.
606
+ # check if path is already absolute
607
+ if @weather_file[:file]
608
+ if File.exists?(@weather_file[:file])
609
+ puts " Adding #{@weather_file[:file]}"
610
+ #zf.add("weather/#{File.basename(@weather_file[:file])}", @weather_file[:file])
611
+ base_name = File.basename(@weather_file[:file], ".*")
612
+ puts "base_name: #{base_name}"
613
+ # convert backslash on windows to forward slash so Dir.glob will work (in case user uses \)
614
+ weather_dirname = File.dirname(@weather_file[:file]).gsub("\\", "/")
615
+ puts "weather_dirname: #{weather_dirname}"
616
+ # If all_weather_files is true, add all files in the directory to the zip.
617
+ # Otherwise, add only files that match the base name.
618
+ file_pattern = all_weather_files ? "*" : "#{base_name}.*"
619
+ Dir.glob(File.join(weather_dirname, file_pattern)) do |file_path|
620
+ puts "file_path: #{file_path}"
621
+ puts "zip path: weather/#{File.basename(file_path)}"
622
+ zf.add("weather/#{File.basename(file_path)}", file_path)
623
+ end
624
+ # make absolute path and check for file
625
+ elsif File.exists?(File.join(osw_full_path,@weather_file[:file].sub(/^\.\//, '')))
626
+ puts " Adding: #{File.join(osw_full_path,@weather_file[:file].sub(/^\.\//, ''))}"
627
+ #zf.add("weather/#{File.basename(@weather_file[:file])}", File.join(osw_full_path,@weather_file[:file].sub(/^\.\//, '')))
628
+ base_name = File.basename(@weather_file[:file].sub(/^\.\//, ''), ".*")
629
+ puts "base_name2: #{base_name}"
630
+ weather_dirname = File.dirname(File.join(osw_full_path,@weather_file[:file].sub(/^\.\//, ''))).gsub("\\", "/")
631
+ puts "weather_dirname: #{weather_dirname}"
632
+ file_pattern = all_weather_files ? "*" : "#{base_name}.*"
633
+ Dir.glob(File.join(weather_dirname, file_pattern)) do |file_path|
634
+ puts "file_path2: #{file_path}"
635
+ puts "zip path2: weather/#{File.basename(file_path)}"
636
+ zf.add("weather/#{File.basename(file_path)}", file_path)
637
+ end
638
+ else
639
+ raise "weather_file[:file] does not exist at: #{File.join(osw_full_path,@weather_file[:file].sub(/^\.\//, ''))}"
640
+ end
641
+ else
642
+ warn "weather_file[:file] is not defined"
643
+ end
644
+
645
+ ## Seed files
646
+ puts 'Adding Support Files: Seed Models'
647
+ #check if seed file exists. use abs path. remove leading ./ from @seed_model path if there.
648
+ #check if path is already absolute
649
+ if @seed_model[:file]
650
+ if File.exists?(@seed_model[:file])
651
+ puts " Adding #{@seed_model[:file]}"
652
+ zf.add("seeds/#{File.basename(@seed_model[:file])}", @seed_model[:file])
653
+ if all_seed_files
654
+ seed_dirname = File.dirname(@seed_model[:file]).gsub("\\", "/")
655
+ puts "seed_dirname: #{seed_dirname}"
656
+ Dir.glob(File.join(seed_dirname, '*')) do |file_path|
657
+ next if file_path == @seed_model[:file] # Skip if the file is the same as @seed_model[:file] so not added twice
658
+ puts "file_path: #{file_path}"
659
+ puts "zip path: seeds/#{File.basename(file_path)}"
660
+ zf.add("seeds/#{File.basename(file_path)}", file_path)
661
+ end
662
+ end
663
+ #make absolute path and check for file
664
+ elsif File.exists?(File.join(osw_full_path,@seed_model[:file].sub(/^\.\//, '')))
665
+ puts " Adding #{File.join(osw_full_path,@seed_model[:file].sub(/^\.\//, ''))}"
666
+ zf.add("seeds/#{File.basename(@seed_model[:file])}", File.join(osw_full_path,@seed_model[:file].sub(/^\.\//, '')))
667
+ if all_seed_files
668
+ seed_dirname = File.dirname(File.join(osw_full_path,@seed_model[:file].sub(/^\.\//, ''))).gsub("\\", "/")
669
+ puts "seed_dirname: #{seed_dirname}"
670
+ Dir.glob(File.join(seed_dirname, '*')) do |file_path|
671
+ next if file_path == File.join(osw_full_path,@seed_model[:file].sub(/^\.\//, '')) # Skip if the file is the same as @seed_model[:file] so not added twice
672
+ puts "file_path: #{file_path}"
673
+ puts "zip path: seeds/#{File.basename(file_path)}"
674
+ zf.add("seeds/#{File.basename(file_path)}", file_path)
675
+ end
676
+ end
677
+ else
678
+ raise "seed_file[:file] does not exist at: #{File.join(osw_full_path,@seed_model[:file].sub(/^\.\//, ''))}"
679
+ end
680
+ else
681
+ warn "seed_file[:file] is not defined"
682
+ end
683
+
684
+ puts 'Adding Support Files: Libraries'
685
+ @libraries.each do |lib|
686
+ raise "Libraries must specify their 'library_name' as metadata which becomes the directory upon zip" unless lib[:metadata][:library_name]
687
+
688
+ if File.directory? lib[:file]
689
+ Dir[File.join(lib[:file], '**', '**')].each do |file|
690
+ puts " Adding #{file}"
691
+ zf.add(file.sub(lib[:file], "lib/#{lib[:metadata][:library_name]}"), file)
692
+ end
693
+ else
694
+ # just add the file to the zip
695
+ puts " Adding #{lib[:file]}"
696
+ zf.add(lib[:file], "lib/#{File.basename(lib[:file])}", lib[:file])
697
+ end
698
+ end
699
+
700
+ puts 'Adding Support Files: Server Scripts'
701
+ @server_scripts.each_with_index do |f, index|
702
+ if f[:init_or_final] == 'finalization'
703
+ file_name = 'finalization.sh'
704
+ else
705
+ file_name = 'initialization.sh'
706
+ end
707
+ if f[:server_or_data_point] == 'analysis'
708
+ new_name = "scripts/analysis/#{file_name}"
709
+ else
710
+ new_name = "scripts/data_point/#{file_name}"
711
+ end
712
+ puts " Adding #{f[:file]} as #{new_name}"
713
+ zf.add(new_name, f[:file])
714
+
715
+ if f[:arguments]
716
+ arg_file = "#{(new_name.sub(/\.sh\z/, ''))}.args"
717
+ puts " Adding arguments as #{arg_file}"
718
+ file = Tempfile.new('arg')
719
+ file.write(f[:arguments])
720
+ zf.add(arg_file, file)
721
+ file.close
722
+ end
723
+ end
724
+
725
+ puts 'Adding Gemfiles'
726
+ @gem_files.each do |f|
727
+ puts " Adding #{f[:file]}"
728
+ zf.add(File.basename(f[:file]), f[:file])
729
+ end
730
+
731
+ ## Measures
732
+ puts 'Adding Measures'
733
+ added_measures = []
734
+ # The list of the measures should always be there, but make sure they are uniq
735
+ @workflow.each do |measure|
736
+ measure_dir_to_add = measure.measure_definition_directory_local
737
+
738
+ next if added_measures.include? measure_dir_to_add
739
+
740
+ puts " Adding #{File.basename(measure_dir_to_add)}"
741
+ Dir[File.join(measure_dir_to_add, '**')].each do |file|
742
+ if File.directory?(file)
743
+ if File.basename(file) == 'resources' || File.basename(file) == 'lib'
744
+ #remove leading ./ from measure_definition_directory path if there.
745
+ add_directory_to_zip_osa(zf, file, "#{measure.measure_definition_directory.sub(/^\.\//, '')}/#{File.basename(file)}")
746
+ end
747
+ else
748
+ puts " Adding File #{file}"
749
+ #remove leading ./ from measure.measure_definition_directory string with regex .sub(/^\.\//, '')
750
+ zip_path_for_measures = file.sub(measure_dir_to_add, measure.measure_definition_directory.sub(/^\.\//, ''))
751
+ #puts " zip_path_for_measures: #{zip_path_for_measures}"
752
+ zf.add(zip_path_for_measures, file)
753
+ end
754
+ end
755
+
756
+ added_measures << measure_dir_to_add
757
+ end
758
+ end
759
+ end
760
+
761
+ #keep legacy function
762
+ # Package up the seed, weather files, and measures
763
+ def save_analysis_zip(filename)
764
+ def add_directory_to_zip(zipfile, local_directory, relative_zip_directory)
765
+ # puts "Add Directory #{local_directory}"
766
+ Dir[File.join(local_directory.to_s, '**', '**')].each do |file|
767
+ # puts "Adding File #{file}"
768
+ zipfile.add(file.sub(local_directory, relative_zip_directory), file)
769
+ end
770
+ zipfile
771
+ end
772
+
773
+ FileUtils.rm_f(filename) if File.exist?(filename)
774
+
775
+ Zip::File.open(filename, Zip::File::CREATE) do |zf|
776
+ ## Weather files
777
+ # TODO: eventually remove the @weather_file attribute and grab the weather file out
778
+ # of the @weather_files
779
+ puts 'Adding Support Files: Weather'
780
+ if @weather_file[:file] && !@weather_files.files.find { |f| @weather_file[:file] == f[:file] }
781
+ # manually add the weather file
782
+ puts " Adding #{@weather_file[:file]}"
783
+ zf.add("./weather/#{File.basename(@weather_file[:file])}", @weather_file[:file])
784
+ end
785
+ @weather_files.each do |f|
786
+ puts " Adding #{f[:file]}"
787
+ zf.add("./weather/#{File.basename(f[:file])}", f[:file])
788
+ end
789
+
790
+ ## Seed files
791
+ puts 'Adding Support Files: Seed Models'
792
+ if @seed_model[:file] && !@seed_models.files.find { |f| @seed_model[:file] == f[:file] }
793
+ # manually add the weather file
794
+ puts " Adding #{@seed_model[:file]}"
795
+ zf.add("./seed/#{File.basename(@seed_model[:file])}", @seed_model[:file])
796
+ end
797
+ @seed_models.each do |f|
798
+ puts " Adding #{f[:file]}"
799
+ zf.add("./seed/#{File.basename(f[:file])}", f[:file])
800
+ end
801
+
802
+ puts 'Adding Support Files: Libraries'
803
+ @libraries.each do |lib|
804
+ raise "Libraries must specify their 'library_name' as metadata which becomes the directory upon zip" unless lib[:metadata][:library_name]
805
+
806
+ if File.directory? lib[:file]
807
+ Dir[File.join(lib[:file], '**', '**')].each do |file|
808
+ puts " Adding #{file}"
809
+ zf.add(file.sub(lib[:file], "./lib/#{lib[:metadata][:library_name]}/"), file)
810
+ end
811
+ else
812
+ # just add the file to the zip
813
+ puts " Adding #{lib[:file]}"
814
+ zf.add(lib[:file], "./lib/#{File.basename(lib[:file])}", lib[:file])
815
+ end
816
+ end
817
+
818
+ puts 'Adding Support Files: Worker Initialization Scripts'
819
+ @worker_inits.each_with_index do |f, index|
820
+ ordered_file_name = "#{index.to_s.rjust(2, '0')}_#{File.basename(f[:file])}"
821
+ puts " Adding #{f[:file]} as #{ordered_file_name}"
822
+ zf.add(f[:file].sub(f[:file], "./scripts/worker_initialization//#{ordered_file_name}"), f[:file])
823
+
824
+ if f[:metadata][:args]
825
+ arg_file = "#{File.basename(ordered_file_name, '.*')}.args"
826
+ file = Tempfile.new('arg')
827
+ file.write(f[:metadata][:args])
828
+ zf.add("./scripts/worker_initialization/#{arg_file}", file)
829
+ file.close
830
+ end
831
+ end
832
+
833
+ puts 'Adding Support Files: Worker Finalization Scripts'
834
+ @worker_finalizes.each_with_index do |f, index|
835
+ ordered_file_name = "#{index.to_s.rjust(2, '0')}_#{File.basename(f[:file])}"
836
+ puts " Adding #{f[:file]} as #{ordered_file_name}"
837
+ zf.add(f[:file].sub(f[:file], "scripts/worker_finalization/#{ordered_file_name}"), f[:file])
838
+
839
+ if f[:metadata][:args]
840
+ arg_file = "#{File.basename(ordered_file_name, '.*')}.args"
841
+ file = Tempfile.new('arg')
842
+ file.write(f[:metadata][:args])
843
+ zf.add("scripts/worker_finalization/#{arg_file}", file)
844
+ file.close
845
+ end
846
+ end
847
+
848
+ puts 'Adding Gemfiles'
849
+ @gem_files.each do |f|
850
+ puts " Adding #{f}"
851
+ zf.add(File.basename(f), f)
852
+ end
853
+
854
+ ## Measures
855
+ puts 'Adding Measures'
856
+ added_measures = []
857
+ # The list of the measures should always be there, but make sure they are uniq
858
+ @workflow.each do |measure|
859
+ measure_dir_to_add = measure.measure_definition_directory_local
860
+
861
+ next if added_measures.include? measure_dir_to_add
862
+
863
+ puts " Adding #{File.basename(measure_dir_to_add)}"
864
+ Dir[File.join(measure_dir_to_add, '**')].each do |file|
865
+ if File.directory?(file)
866
+ if File.basename(file) == 'resources' || File.basename(file) == 'lib'
867
+ add_directory_to_zip(zf, file, "#{measure.measure_definition_directory}/#{File.basename(file)}")
868
+ end
869
+ else
870
+ # puts "Adding File #{file}"
871
+ zf.add(file.sub(measure_dir_to_add, "#{measure.measure_definition_directory}/"), file)
872
+ end
873
+ end
874
+
875
+ added_measures << measure_dir_to_add
876
+ end
877
+ end
878
+ end
879
+ end
880
+ end
881
+ end