openstudio-analysis 1.3.5 → 1.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,857 +1,857 @@
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
+ # 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