openstudio-analysis 1.3.4 → 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,893 +1,893 @@
1
- # *******************************************************************************
2
- # OpenStudio(R), Copyright (c) Alliance for Sustainable Energy, LLC.
3
- # See also https://openstudio.net/license
4
- # *******************************************************************************
5
-
6
- module OpenStudio
7
- module Analysis
8
- module Translator
9
- class Excel
10
- attr_reader :version
11
- attr_reader :settings
12
- attr_reader :variables
13
- attr_reader :outputs
14
- attr_reader :models
15
- attr_reader :weather_files
16
- attr_reader :measure_paths
17
- attr_reader :weather_paths
18
- attr_reader :worker_inits
19
- attr_reader :worker_finals
20
- attr_reader :export_path
21
- attr_reader :cluster_name
22
- attr_reader :variables
23
- attr_reader :algorithm
24
- attr_reader :problem
25
- attr_reader :run_setup
26
- attr_reader :aws_tags
27
-
28
- # remove these once we have classes to construct the JSON file
29
- attr_accessor :name
30
- attr_accessor :cluster_name
31
- attr_reader :analysis_name
32
-
33
- # methods to override instance variables
34
-
35
- # pass in the filename to read
36
- def initialize(xls_filename)
37
- @xls_filename = xls_filename
38
- @root_path = File.expand_path(File.dirname(@xls_filename))
39
-
40
- @xls = nil
41
- # try to read the spreadsheet as a roo object
42
- if File.exist?(@xls_filename)
43
- @xls = Roo::Spreadsheet.open(@xls_filename)
44
- else
45
- raise "File #{@xls_filename} does not exist"
46
- end
47
-
48
- # Initialize some other instance variables
49
- @version = '0.0.1'
50
- @analyses = [] # Array o OpenStudio::Analysis. Use method to access
51
- @name = nil
52
- @analysis_name = nil
53
- @settings = {}
54
- @weather_files = []
55
- @weather_paths = []
56
- @models = []
57
- @other_files = []
58
- @worker_inits = []
59
- @worker_finals = []
60
- @export_path = './export'
61
- @measure_paths = []
62
- @number_of_samples = 0 # TODO: remove this
63
- @problem = {}
64
- @algorithm = {}
65
- @outputs = {}
66
- @run_setup = {}
67
- @aws_tags = []
68
- end
69
-
70
- def process
71
- @setup = parse_setup
72
-
73
- @version = Semantic::Version.new @version
74
- raise "Spreadsheet version #{@version} is no longer supported. Please upgrade your spreadsheet to at least 0.1.9" if @version < '0.1.9'
75
-
76
- @variables = parse_variables
77
-
78
- @outputs = parse_outputs
79
-
80
- # call validate to make sure everything that is needed exists (i.e. directories)
81
- validate_analysis
82
- end
83
-
84
- # Helper methods to remove models and add new ones programatically. Note that these should
85
- # be moved into a general analysis class
86
- def delete_models
87
- @models = []
88
- end
89
-
90
- def add_model(name, display_name, type, path)
91
- @models << {
92
- name: name,
93
- display_name: display_name,
94
- type: type,
95
- path: path
96
- }
97
- end
98
-
99
- def validate_analysis
100
- # Setup the paths and do some error checking
101
- @measure_paths.each do |mp|
102
- raise "Measures directory '#{mp}' does not exist" unless Dir.exist?(mp)
103
- end
104
-
105
- @models.uniq!
106
- raise 'No seed models defined in spreadsheet' if @models.empty?
107
-
108
- @models.each do |model|
109
- raise "Seed model does not exist: #{model[:path]}" unless File.exist?(model[:path])
110
- end
111
-
112
- @weather_files.uniq!
113
- raise 'No weather files found based on what is in the spreadsheet' if @weather_files.empty?
114
-
115
- @weather_files.each do |wf|
116
- raise "Weather file does not exist: #{wf}" unless File.exist?(wf)
117
- end
118
-
119
- # This can be a directory as well
120
- @other_files.each do |f|
121
- raise "Other files do not exist for: #{f[:path]}" unless File.exist?(f[:path])
122
- end
123
-
124
- @worker_inits.each do |f|
125
- raise "Worker initialization file does not exist for: #{f[:path]}" unless File.exist?(f[:path])
126
- end
127
-
128
- @worker_finals.each do |f|
129
- raise "Worker finalization file does not exist for: #{f[:path]}" unless File.exist?(f[:path])
130
- end
131
-
132
- FileUtils.mkdir_p(@export_path)
133
-
134
- # verify that the measure display names are unique
135
- # puts @variables.inspect
136
- measure_display_names = @variables['data'].map { |m| m['enabled'] ? m['display_name'] : nil }.compact
137
- measure_display_names_mult = measure_display_names.select { |m| measure_display_names.count(m) > 1 }.uniq
138
- if measure_display_names_mult && !measure_display_names_mult.empty?
139
- raise "Measure Display Names are not unique for '#{measure_display_names_mult.join('\', \'')}'"
140
- end
141
-
142
- # verify that all continuous variables have all the data needed and create a name map
143
- variable_names = []
144
- @variables['data'].each do |measure|
145
- if measure['enabled']
146
- measure['variables'].each do |variable|
147
- # Determine if row is suppose to be an argument or a variable to be perturbed.
148
- if variable['variable_type'] == 'variable'
149
- variable_names << variable['display_name']
150
-
151
- # make sure that variables have static values
152
- if variable['distribution']['static_value'].nil? || variable['distribution']['static_value'] == ''
153
- raise "Variable #{measure['name']}:#{variable['name']} needs a static value"
154
- end
155
-
156
- if variable['type'] == 'enum' || variable['type'] == 'Choice'
157
- # check something
158
- else # must be an integer or double
159
- if variable['distribution']['type'] == 'discrete_uncertain'
160
- if variable['distribution']['discrete_values'].nil? || variable['distribution']['discrete_values'] == ''
161
- raise "Variable #{measure['name']}:#{variable['name']} needs discrete values"
162
- end
163
- elsif variable['distribution']['type'] == 'integer_sequence'
164
- if variable['distribution']['mean'].nil? || variable['distribution']['mean'] == ''
165
- raise "Variable #{measure['name']}:#{variable['name']} must have a mean/mode"
166
- end
167
- if variable['distribution']['min'].nil? || variable['distribution']['min'] == ''
168
- raise "Variable #{measure['name']}:#{variable['name']} must have a minimum"
169
- end
170
- if variable['distribution']['max'].nil? || variable['distribution']['max'] == ''
171
- raise "Variable #{measure['name']}:#{variable['name']} must have a maximum"
172
- end
173
- else
174
- if variable['distribution']['mean'].nil? || variable['distribution']['mean'] == ''
175
- raise "Variable #{measure['name']}:#{variable['name']} must have a mean"
176
- end
177
- if variable['distribution']['stddev'].nil? || variable['distribution']['stddev'] == ''
178
- raise "Variable #{measure['name']}:#{variable['name']} must have a stddev"
179
- end
180
- end
181
-
182
- if variable['distribution']['mean'].nil? || variable['distribution']['mean'] == ''
183
- raise "Variable #{measure['name']}:#{variable['name']} must have a mean/mode"
184
- end
185
- if variable['distribution']['min'].nil? || variable['distribution']['min'] == ''
186
- raise "Variable #{measure['name']}:#{variable['name']} must have a minimum"
187
- end
188
- if variable['distribution']['max'].nil? || variable['distribution']['max'] == ''
189
- raise "Variable #{measure['name']}:#{variable['name']} must have a maximum"
190
- end
191
- unless variable['type'] == 'string' || variable['type'] =~ /bool/
192
- if variable['distribution']['min'] > variable['distribution']['max']
193
- raise "Variable min is greater than variable max for #{measure['name']}:#{variable['name']}"
194
- end
195
- end
196
-
197
- end
198
- end
199
- end
200
- end
201
- end
202
-
203
- dupes = variable_names.select { |e| variable_names.count(e) > 1 }.uniq
204
- if dupes.count > 0
205
- raise "duplicate variable names found in list #{dupes.inspect}"
206
- end
207
-
208
- # most of the checks will raise a runtime exception, so this true will never be called
209
- true
210
- end
211
-
212
- # convert the data in excel's parsed data into an OpenStudio Analysis Object
213
- #
214
- # @seed_model [Hash] Seed model to set the new analysis to
215
- # @append_model_name [Boolean] Append the name of the seed model to the display name
216
- # @return [Object] An OpenStudio::Analysis
217
- def analysis(seed_model = nil, append_model_name = false)
218
- raise 'There are no seed models defined in the excel file. Please add one.' if @models.empty?
219
- raise "There are more than one seed models defined in the excel file. Call 'analyses' to return the array" if @models.size > 1 && seed_model.nil?
220
-
221
- seed_model = @models.first if seed_model.nil?
222
-
223
- # Use the programmatic interface to make the analysis
224
- # append the model name to the analysis name if requested (normally if there are more than 1 models in the spreadsheet)
225
- display_name = append_model_name ? @name + ' ' + seed_model[:display_name] : @name
226
-
227
- a = OpenStudio::Analysis.create(display_name)
228
-
229
- @variables['data'].each do |measure|
230
- next unless measure['enabled']
231
-
232
- @measure_paths.each do |measure_path|
233
- measure_dir_to_add = "#{measure_path}/#{measure['measure_file_name_directory']}"
234
- if Dir.exist? measure_dir_to_add
235
- if File.exist? "#{measure_dir_to_add}/measure.rb"
236
- measure['local_path_to_measure'] = "#{measure_dir_to_add}/measure.rb"
237
- break
238
- else
239
- raise "Measure in directory '#{measure_dir_to_add}' did not contain a measure.rb file"
240
- end
241
- end
242
- end
243
-
244
- raise "Could not find measure '#{measure['name']}' in directory named '#{measure['measure_file_name_directory']}' in the measure paths '#{@measure_paths.join(', ')}'" unless measure['local_path_to_measure']
245
-
246
- a.workflow.add_measure_from_excel(measure)
247
- end
248
-
249
- @other_files.each do |library|
250
- a.libraries.add(library[:path], library_name: library[:lib_zip_name])
251
- end
252
-
253
- @worker_inits.each do |w|
254
- a.worker_inits.add(w[:path], args: w[:args])
255
- end
256
-
257
- @worker_finals.each do |w|
258
- a.worker_finalizes.add(w[:path], args: w[:args])
259
- end
260
-
261
- # Add in the outputs
262
- @outputs['output_variables'].each do |o|
263
- o = Hash[o.map { |k, v| [k.to_sym, v] }]
264
- a.add_output(o)
265
- end
266
-
267
- a.analysis_type = @problem['analysis_type']
268
- @algorithm.each do |k, v|
269
- a.algorithm.set_attribute(k, v)
270
- end
271
-
272
- # clear out the seed files before adding new ones
273
- a.seed_model = seed_model[:path]
274
-
275
- # clear out the weather files before adding new ones
276
- a.weather_files.clear
277
- @weather_paths.each do |wp|
278
- a.weather_files.add_files(wp)
279
- end
280
-
281
- a
282
- end
283
-
284
- # Return an array of analyses objects of OpenStudio::Analysis::Formulation
285
- def analyses
286
- as = []
287
- @models.map do |model|
288
- as << analysis(model, @models.count > 1)
289
- end
290
-
291
- as
292
- end
293
-
294
- # Method to return the cluster name for backwards compatibility
295
- def cluster_name
296
- @settings['cluster_name']
297
- end
298
-
299
- # save_analysis will iterate over each model that is defined in the spreadsheet and save the
300
- # zip and json file.
301
- def save_analysis
302
- analyses.each do |a|
303
- puts "Saving JSON and ZIP file for #{@name}:#{a.display_name}"
304
- json_file_name = "#{@export_path}/#{a.name}.json"
305
- FileUtils.rm_f(json_file_name) if File.exist?(json_file_name)
306
- # File.open(json_file_name, 'w') { |f| f << JSON.pretty_generate(new_analysis_json) }
307
-
308
- a.save json_file_name
309
- a.save_zip "#{File.dirname(json_file_name)}/#{File.basename(json_file_name, '.*')}.zip"
310
- end
311
- end
312
-
313
- protected
314
-
315
- # parse_setup will pull out the data on the "setup" tab and store it in memory for later use
316
- def parse_setup
317
- rows = @xls.sheet('Setup').parse
318
- b_settings = false
319
- b_run_setup = false
320
- b_problem_setup = false
321
- b_algorithm_setup = false
322
- b_weather_files = false
323
- b_models = false
324
- b_other_libs = false
325
- b_worker_init = false
326
- b_worker_final = false
327
-
328
- rows.each do |row|
329
- if row[0] == 'Settings'
330
- b_settings = true
331
- b_run_setup = false
332
- b_problem_setup = false
333
- b_algorithm_setup = false
334
- b_weather_files = false
335
- b_models = false
336
- b_other_libs = false
337
- b_worker_init = false
338
- b_worker_final = false
339
- next
340
- elsif row[0] == 'Running Setup'
341
- b_settings = false
342
- b_run_setup = true
343
- b_problem_setup = false
344
- b_algorithm_setup = false
345
- b_weather_files = false
346
- b_models = false
347
- b_other_libs = false
348
- b_worker_init = false
349
- b_worker_final = false
350
- next
351
- elsif row[0] == 'Problem Definition'
352
- b_settings = false
353
- b_run_setup = false
354
- b_problem_setup = true
355
- b_algorithm_setup = false
356
- b_weather_files = false
357
- b_models = false
358
- b_other_libs = false
359
- b_worker_init = false
360
- b_worker_final = false
361
- next
362
- elsif row[0] == 'Algorithm Setup'
363
- b_settings = false
364
- b_run_setup = false
365
- b_problem_setup = false
366
- b_algorithm_setup = true
367
- b_weather_files = false
368
- b_models = false
369
- b_other_libs = false
370
- b_worker_init = false
371
- b_worker_final = false
372
- next
373
- elsif row[0] == 'Weather Files'
374
- b_settings = false
375
- b_run_setup = false
376
- b_problem_setup = false
377
- b_algorithm_setup = false
378
- b_weather_files = true
379
- b_models = false
380
- b_other_libs = false
381
- b_worker_init = false
382
- b_worker_final = false
383
- next
384
- elsif row[0] == 'Models'
385
- b_settings = false
386
- b_run_setup = false
387
- b_problem_setup = false
388
- b_algorithm_setup = false
389
- b_weather_files = false
390
- b_models = true
391
- b_other_libs = false
392
- b_worker_init = false
393
- b_worker_final = false
394
- next
395
- elsif row[0] == 'Other Library Files'
396
- b_settings = false
397
- b_run_setup = false
398
- b_problem_setup = false
399
- b_algorithm_setup = false
400
- b_weather_files = false
401
- b_models = false
402
- b_other_libs = true
403
- b_worker_init = false
404
- b_worker_final = false
405
- next
406
- elsif row[0] =~ /Worker Initialization Scripts/
407
- b_settings = false
408
- b_run_setup = false
409
- b_problem_setup = false
410
- b_algorithm_setup = false
411
- b_weather_files = false
412
- b_models = false
413
- b_other_libs = false
414
- b_worker_init = true
415
- b_worker_final = false
416
- next
417
- elsif row[0] =~ /Worker Finalization Scripts/
418
- b_settings = false
419
- b_run_setup = false
420
- b_problem_setup = false
421
- b_algorithm_setup = false
422
- b_weather_files = false
423
- b_models = false
424
- b_other_libs = false
425
- b_worker_init = false
426
- b_worker_final = true
427
- next
428
- end
429
-
430
- next if row[0].nil?
431
-
432
- if b_settings
433
- @version = row[1].chomp if row[0] == 'Spreadsheet Version'
434
- @settings[row[0].to_underscore.to_s] = row[1] if row[0]
435
- if @settings['cluster_name']
436
- @settings['cluster_name'] = @settings['cluster_name'].to_underscore
437
- end
438
-
439
- if row[0] == 'AWS Tag'
440
- @aws_tags << row[1].strip
441
- end
442
-
443
- # type some of the values that we know
444
- @settings['proxy_port'] = @settings['proxy_port'].to_i if @settings['proxy_port']
445
-
446
- elsif b_run_setup
447
- if row[0] == 'Analysis Name'
448
- if row[1]
449
- @name = row[1]
450
- else
451
- @name = SecureRandom.uuid
452
- end
453
- @analysis_name = @name.to_underscore
454
- end
455
- if row[0] == 'Export Directory'
456
- tmp_filepath = row[1]
457
- if (Pathname.new tmp_filepath).absolute?
458
- @export_path = tmp_filepath
459
- else
460
- @export_path = File.expand_path(File.join(@root_path, tmp_filepath))
461
- end
462
- end
463
- if row[0] == 'Measure Directory'
464
- tmp_filepath = row[1]
465
- if (Pathname.new tmp_filepath).absolute?
466
- @measure_paths << tmp_filepath
467
- else
468
- @measure_paths << File.expand_path(File.join(@root_path, tmp_filepath))
469
- end
470
- end
471
- @run_setup[row[0].to_underscore.to_s] = row[1] if row[0]
472
-
473
- # type cast
474
- if @run_setup['allow_multiple_jobs']
475
- raise 'allow_multiple_jobs is no longer a valid option in the Excel file, please delete the row and rerun'
476
- end
477
- if @run_setup['use_server_as_worker']
478
- raise 'use_server_as_worker is no longer a valid option in the Excel file, please delete the row and rerun'
479
- end
480
- elsif b_problem_setup
481
- if row[0]
482
- v = row[1]
483
- v.to_i if v % 1 == 0
484
- @problem[row[0].to_underscore.to_s] = v
485
- end
486
-
487
- elsif b_algorithm_setup
488
- if row[0] && !row[0].empty?
489
- v = row[1]
490
- v = v.to_i if v % 1 == 0
491
- @algorithm[row[0].to_underscore.to_s] = v
492
- end
493
- elsif b_weather_files
494
- if row[0] == 'Weather File'
495
- weather_path = row[1]
496
- unless (Pathname.new weather_path).absolute?
497
- weather_path = File.expand_path(File.join(@root_path, weather_path))
498
- end
499
- @weather_paths << weather_path
500
- @weather_files += Dir.glob(weather_path)
501
- end
502
- elsif b_models
503
- if row[1]
504
- tmp_m_name = row[1]
505
- else
506
- tmp_m_name = SecureRandom.uuid
507
- end
508
- # Only add models if the row is flagged
509
- if row[0]&.casecmp('model')&.zero?
510
- model_path = row[3]
511
- unless (Pathname.new model_path).absolute?
512
- model_path = File.expand_path(File.join(@root_path, model_path))
513
- end
514
- @models << { name: tmp_m_name.to_underscore, display_name: tmp_m_name, type: row[2], path: model_path }
515
- end
516
- elsif b_other_libs
517
- # determine if the path is relative
518
- other_path = row[2]
519
- unless (Pathname.new other_path).absolute?
520
- other_path = File.expand_path(File.join(@root_path, other_path))
521
- end
522
-
523
- @other_files << { lib_zip_name: row[1], path: other_path }
524
- elsif b_worker_init
525
- worker_init_path = row[1]
526
- unless (Pathname.new worker_init_path).absolute?
527
- worker_init_path = File.expand_path(File.join(@root_path, worker_init_path))
528
- end
529
-
530
- @worker_inits << { name: row[0], path: worker_init_path, args: row[2] }
531
- elsif b_worker_final
532
- worker_final_path = row[1]
533
- unless (Pathname.new worker_final_path).absolute?
534
- worker_final_path = File.expand_path(File.join(@root_path, worker_final_path))
535
- end
536
-
537
- @worker_finals << { name: row[0], path: worker_final_path, args: row[2] }
538
- end
539
-
540
- next
541
- end
542
-
543
- # do some last checks
544
- @measure_paths = ['./measures'] if @measure_paths.empty?
545
- end
546
-
547
- # parse_variables will parse the XLS spreadsheet and save the data into
548
- # a higher level JSON file. The JSON file is historic and it should really
549
- # be omitted as an intermediate step
550
- def parse_variables
551
- # clean remove whitespace and unicode chars
552
- # The parse is a unique format (https://github.com/Empact/roo/blob/master/lib/roo/base.rb#L444)
553
- # If you add a new column and you want that variable in the hash, then you must add it here.
554
- # rows = @xls.sheet('Variables').parse(:enabled => "# variable")
555
- # puts rows.inspect
556
-
557
- rows = nil
558
- begin
559
- if @version >= '0.3.3'.to_version
560
- rows = @xls.sheet('Variables').parse(enabled: /# variable/i,
561
- measure_name_or_var_type: /type/i,
562
- measure_file_name_or_var_display_name: /parameter\sdisplay\sname.*/i,
563
- measure_file_name_directory: /measure\sdirectory/i,
564
- measure_type_or_parameter_name_in_measure: /parameter\sname\sin\smeasure/i,
565
- display_name_short: /parameter\sshort\sdisplay\sname/i,
566
- # sampling_method: /sampling\smethod/i,
567
- variable_type: /variable\stype/i,
568
- units: /units/i,
569
- default_value: /static.default\svalue/i,
570
- enums: /enumerations/i,
571
- min: /min/i,
572
- max: /max/i,
573
- mode: /mean|mode/i,
574
- stddev: /std\sdev/i,
575
- delta_x: /delta.x/i,
576
- discrete_values: /discrete\svalues/i,
577
- discrete_weights: /discrete\sweights/i,
578
- distribution: /distribution/i,
579
- source: /data\ssource/i,
580
- notes: /notes/i,
581
- relation_to_eui: /typical\svar\sto\seui\srelationship/i,
582
- clean: true)
583
- elsif @version >= '0.3.0'.to_version
584
- rows = @xls.sheet('Variables').parse(enabled: /# variable/i,
585
- measure_name_or_var_type: /type/i,
586
- measure_file_name_or_var_display_name: /parameter\sdisplay\sname.*/i,
587
- measure_file_name_directory: /measure\sdirectory/i,
588
- measure_type_or_parameter_name_in_measure: /parameter\sname\sin\smeasure/i,
589
- # sampling_method: /sampling\smethod/i,
590
- variable_type: /variable\stype/i,
591
- units: /units/i,
592
- default_value: /static.default\svalue/i,
593
- enums: /enumerations/i,
594
- min: /min/i,
595
- max: /max/i,
596
- mode: /mean|mode/i,
597
- stddev: /std\sdev/i,
598
- delta_x: /delta.x/i,
599
- discrete_values: /discrete\svalues/i,
600
- discrete_weights: /discrete\sweights/i,
601
- distribution: /distribution/i,
602
- source: /data\ssource/i,
603
- notes: /notes/i,
604
- relation_to_eui: /typical\svar\sto\seui\srelationship/i,
605
- clean: true)
606
- elsif @version >= '0.2.0'.to_version
607
- rows = @xls.sheet('Variables').parse(enabled: /# variable/i,
608
- measure_name_or_var_type: /type/i,
609
- measure_file_name_or_var_display_name: /parameter\sdisplay\sname.*/i,
610
- measure_file_name_directory: /measure\sdirectory/i,
611
- measure_type_or_parameter_name_in_measure: /parameter\sname\sin\smeasure/i,
612
- sampling_method: /sampling\smethod/i,
613
- variable_type: /variable\stype/i,
614
- units: /units/i,
615
- default_value: /static.default\svalue/i,
616
- enums: /enumerations/i,
617
- min: /min/i,
618
- max: /max/i,
619
- mode: /mean|mode/i,
620
- stddev: /std\sdev/i,
621
- delta_x: /delta.x/i,
622
- discrete_values: /discrete\svalues/i,
623
- discrete_weights: /discrete\sweights/i,
624
- distribution: /distribution/i,
625
- source: /data\ssource/i,
626
- notes: /notes/i,
627
- relation_to_eui: /typical\svar\sto\seui\srelationship/i,
628
- clean: true)
629
- elsif @version >= '0.1.12'.to_version
630
- rows = @xls.sheet('Variables').parse(enabled: /# variable/i,
631
- measure_name_or_var_type: /type/i,
632
- measure_file_name_or_var_display_name: /parameter\sdisplay\sname.*/i,
633
- measure_type_or_parameter_name_in_measure: /parameter\sname\sin\smeasure/i,
634
- sampling_method: /sampling\smethod/i,
635
- variable_type: /variable\stype/i,
636
- units: /units/i,
637
- default_value: /static.default\svalue/i,
638
- enums: /enumerations/i,
639
- min: /min/i,
640
- max: /max/i,
641
- mode: /mean|mode/i,
642
- stddev: /std\sdev/i,
643
- delta_x: /delta.x/i,
644
- discrete_values: /discrete\svalues/i,
645
- discrete_weights: /discrete\sweights/i,
646
- distribution: /distribution/i,
647
- source: /data\ssource/i,
648
- notes: /notes/i,
649
- relation_to_eui: /typical\svar\sto\seui\srelationship/i,
650
- clean: true)
651
- elsif @version >= '0.1.11'.to_version
652
- rows = @xls.sheet('Variables').parse(enabled: /# variable/i,
653
- measure_name_or_var_type: /type/i,
654
- measure_file_name_or_var_display_name: /parameter\sdisplay\sname.*/i,
655
- measure_type_or_parameter_name_in_measure: /parameter\sname\sin\smeasure/i,
656
- sampling_method: /sampling\smethod/i,
657
- variable_type: /variable\stype/i,
658
- units: /units/i,
659
- default_value: /static.default\svalue/i,
660
- enums: /enumerations/i,
661
- min: /min/i,
662
- max: /max/i,
663
- mode: /mean|mode/i,
664
- stddev: /std\sdev/i,
665
- # delta_x: /delta.x/i,
666
- discrete_values: /discrete\svalues/i,
667
- discrete_weights: /discrete\sweights/i,
668
- distribution: /distribution/i,
669
- source: /data\ssource/i,
670
- notes: /notes/i,
671
- relation_to_eui: /typical\svar\sto\seui\srelationship/i,
672
- clean: true)
673
- else
674
- rows = @xls.sheet('Variables').parse(enabled: /# variable/i,
675
- measure_name_or_var_type: /type/i,
676
- measure_file_name_or_var_display_name: /parameter\sdisplay\sname.*/i,
677
- measure_type_or_parameter_name_in_measure: /parameter\sname\sin\smeasure/i,
678
- sampling_method: /sampling\smethod/i,
679
- variable_type: /variable\stype/i,
680
- units: /units/i,
681
- default_value: /static.default\svalue/i,
682
- enums: /enumerations/i,
683
- min: /min/i,
684
- max: /max/i,
685
- mode: /mean|mode/i,
686
- stddev: /std\sdev/i,
687
- # delta_x: /delta.x/i,
688
- # discrete_values: /discrete\svalues/i,
689
- # discrete_weights: /discrete\sweights/i,
690
- distribution: /distribution/i,
691
- source: /data\ssource/i,
692
- notes: /notes/i,
693
- relation_to_eui: /typical\svar\sto\seui\srelationship/i,
694
- clean: true)
695
- end
696
- rescue StandardError => e
697
- raise "Unable to parse spreadsheet #{@xls_filename} with version #{@version} due to error: #{e.message}"
698
- end
699
-
700
- raise "Could not find the sheet name 'Variables' in excel file #{@root_path}" unless rows
701
-
702
- # map the data to another hash that is more easily processed
703
- data = {}
704
- data['data'] = []
705
-
706
- measure_index = -1
707
- variable_index = -1
708
- measure_name = nil
709
- rows.each_with_index do |row, icnt|
710
- # puts "Parsing line: #{icnt}:#{row}"
711
-
712
- # check if we are a measure - nil means that the cell was blank
713
- if row[:enabled].nil?
714
- if measure_name && data['data'][measure_index]['enabled']
715
- variable_index += 1
716
-
717
- var = {}
718
- var['variable_type'] = row[:measure_name_or_var_type]
719
- var['display_name'] = row[:measure_file_name_or_var_display_name]
720
- var['display_name_short'] = row[:display_name_short] ? row[:display_name_short] : var['display_name']
721
- var['name'] = row[:measure_type_or_parameter_name_in_measure]
722
- var['index'] = variable_index # order of the variable (not sure of its need)
723
- var['type'] = row[:variable_type].downcase
724
- var['units'] = row[:units]
725
- var['distribution'] = {}
726
-
727
- # parse the choices/enums
728
- if var['type'] == 'enum' || var['type'] == 'choice' # this is now a choice
729
- if row[:enums]
730
- var['distribution']['enumerations'] = row[:enums].delete('|').split(',').map(&:strip)
731
- end
732
- elsif var['type'] == 'bool'
733
- var['distribution']['enumerations'] = []
734
- var['distribution']['enumerations'] << 'true' # TODO: should this be a real bool?
735
- var['distribution']['enumerations'] << 'false'
736
- end
737
-
738
- var['distribution']['min'] = row[:min]
739
- var['distribution']['max'] = row[:max]
740
- var['distribution']['mean'] = row[:mode]
741
- var['distribution']['stddev'] = row[:stddev]
742
- var['distribution']['discrete_values'] = row[:discrete_values]
743
- var['distribution']['discrete_weights'] = row[:discrete_weights]
744
- var['distribution']['type'] = row[:distribution]
745
- var['distribution']['static_value'] = row[:default_value]
746
- var['distribution']['delta_x'] = row[:delta_x]
747
-
748
- # type various values correctly
749
- var['distribution']['min'] = typecast_value(var['type'], var['distribution']['min'])
750
- var['distribution']['max'] = typecast_value(var['type'], var['distribution']['max'])
751
- var['distribution']['mean'] = typecast_value(var['type'], var['distribution']['mean'])
752
- var['distribution']['stddev'] = typecast_value(var['type'], var['distribution']['stddev'])
753
- var['distribution']['static_value'] = typecast_value(var['type'], var['distribution']['static_value'])
754
-
755
- # eval the discrete value and weight arrays
756
- case var['type']
757
- when 'bool', 'boolean'
758
- if var['distribution']['discrete_values']
759
- var['distribution']['discrete_values'] = eval(var['distribution']['discrete_values']).map { |v| v.to_s == 'true' }
760
- end
761
- if var['distribution']['discrete_weights'] && var['distribution']['discrete_weights'] != ''
762
- var['distribution']['discrete_weights'] = eval(var['distribution']['discrete_weights'])
763
- end
764
- else
765
- if var['distribution']['discrete_values']
766
- var['distribution']['discrete_values'] = eval(var['distribution']['discrete_values'])
767
- end
768
- if var['distribution']['discrete_weights'] && var['distribution']['discrete_weights'] != ''
769
- var['distribution']['discrete_weights'] = eval(var['distribution']['discrete_weights'])
770
- end
771
- end
772
-
773
- var['distribution']['source'] = row[:source]
774
- var['notes'] = row[:notes]
775
- var['relation_to_eui'] = row[:relation_to_eui]
776
-
777
- data['data'][measure_index]['variables'] << var
778
- end
779
- else
780
- measure_index += 1
781
- variable_index = 0
782
- data['data'][measure_index] = {}
783
-
784
- # generate name id
785
- # TODO: put this into a logger. puts "Parsing measure #{row[1]}"
786
- display_name = row[:measure_name_or_var_type]
787
- measure_name = display_name.downcase.strip.tr('-', '_').tr(' ', '_').gsub('__', '_')
788
- data['data'][measure_index]['display_name'] = display_name
789
- data['data'][measure_index]['name'] = measure_name
790
- data['data'][measure_index]['enabled'] = row[:enabled]
791
- data['data'][measure_index]['measure_file_name'] = row[:measure_file_name_or_var_display_name]
792
- if row[:measure_file_name_directory]
793
- data['data'][measure_index]['measure_file_name_directory'] = row[:measure_file_name_directory]
794
- else
795
- data['data'][measure_index]['measure_file_name_directory'] = row[:measure_file_name_or_var_display_name].to_underscore
796
- end
797
- data['data'][measure_index]['measure_type'] = row[:measure_type_or_parameter_name_in_measure]
798
- data['data'][measure_index]['version'] = @version_id
799
-
800
- data['data'][measure_index]['variables'] = []
801
- end
802
- end
803
-
804
- data
805
- end
806
-
807
- def parse_outputs
808
- rows = nil
809
- if @version >= '0.3.3'.to_version
810
- rows = @xls.sheet('Outputs').parse(display_name: /variable\sdisplay\sname/i,
811
- display_name_short: /short\sdisplay\sname/i,
812
- metadata_id: /taxonomy\sidentifier/i,
813
- name: /^name$/i,
814
- units: /units/i,
815
- visualize: /visualize/i,
816
- export: /export/i,
817
- variable_type: /variable\stype/i,
818
- objective_function: /objective\sfunction/i,
819
- objective_function_target: /objective\sfunction\starget/i,
820
- scaling_factor: /scale/i,
821
- objective_function_group: /objective\sfunction\sgroup/i)
822
- elsif @version >= '0.3.0'.to_version
823
- rows = @xls.sheet('Outputs').parse(display_name: /variable\sdisplay\sname/i,
824
- # display_name_short: /short\sdisplay\sname/i,
825
- metadata_id: /taxonomy\sidentifier/i,
826
- name: /^name$/i,
827
- units: /units/i,
828
- visualize: /visualize/i,
829
- export: /export/i,
830
- variable_type: /variable\stype/i,
831
- objective_function: /objective\sfunction/i,
832
- objective_function_target: /objective\sfunction\starget/i,
833
- scaling_factor: /scale/i,
834
- objective_function_group: /objective\sfunction\sgroup/i)
835
- else
836
- rows = @xls.sheet('Outputs').parse(display_name: /variable\sdisplay\sname/i,
837
- # display_name_short: /short\sdisplay\sname/i,
838
- # metadata_id: /taxonomy\sidentifier/i,
839
- name: /^name$/i,
840
- units: /units/i,
841
- # visualize: /visualize/i,
842
- # export: /export/i,
843
- # variable_type: /variable\stype/i,
844
- objective_function: /objective\sfunction/i,
845
- objective_function_target: /objective\sfunction\starget/i,
846
- scaling_factor: /scale/i,
847
- objective_function_group: /objective/i)
848
-
849
- end
850
-
851
- unless rows
852
- raise "Could not find the sheet name 'Outputs' in excel file #{@root_path}"
853
- end
854
-
855
- data = {}
856
- data['output_variables'] = []
857
-
858
- variable_index = -1
859
- group_index = 1
860
-
861
- rows.each_with_index do |row, icnt|
862
- next if icnt < 1 # skip the first 3 lines of the file
863
-
864
- var = {}
865
- var['display_name'] = row[:display_name]
866
- var['display_name_short'] = row[:display_name_short] ? row[:display_name_short] : row[:display_name]
867
- var['metadata_id'] = row[:metadata_id]
868
- var['name'] = row[:name]
869
- var['units'] = row[:units]
870
- var['visualize'] = row[:visualize]
871
- var['export'] = row[:export]
872
- var['variable_type'] = row[:variable_type].downcase if row[:variable_type]
873
- var['objective_function'] = row[:objective_function]
874
- var['objective_function_target'] = row[:objective_function_target]
875
- var['scaling_factor'] = row[:scaling_factor]
876
-
877
- if var['objective_function']
878
- if row[:objective_function_group].nil?
879
- var['objective_function_group'] = group_index
880
- group_index += 1
881
- else
882
- var['objective_function_group'] = row[:objective_function_group]
883
- end
884
- end
885
- data['output_variables'] << var
886
- end
887
-
888
- data
889
- end
890
- end
891
- end
892
- end
893
- end
1
+ # *******************************************************************************
2
+ # OpenStudio(R), Copyright (c) Alliance for Sustainable Energy, LLC.
3
+ # See also https://openstudio.net/license
4
+ # *******************************************************************************
5
+
6
+ module OpenStudio
7
+ module Analysis
8
+ module Translator
9
+ class Excel
10
+ attr_reader :version
11
+ attr_reader :settings
12
+ attr_reader :variables
13
+ attr_reader :outputs
14
+ attr_reader :models
15
+ attr_reader :weather_files
16
+ attr_reader :measure_paths
17
+ attr_reader :weather_paths
18
+ attr_reader :worker_inits
19
+ attr_reader :worker_finals
20
+ attr_reader :export_path
21
+ attr_reader :cluster_name
22
+ attr_reader :variables
23
+ attr_reader :algorithm
24
+ attr_reader :problem
25
+ attr_reader :run_setup
26
+ attr_reader :aws_tags
27
+
28
+ # remove these once we have classes to construct the JSON file
29
+ attr_accessor :name
30
+ attr_accessor :cluster_name
31
+ attr_reader :analysis_name
32
+
33
+ # methods to override instance variables
34
+
35
+ # pass in the filename to read
36
+ def initialize(xls_filename)
37
+ @xls_filename = xls_filename
38
+ @root_path = File.expand_path(File.dirname(@xls_filename))
39
+
40
+ @xls = nil
41
+ # try to read the spreadsheet as a roo object
42
+ if File.exist?(@xls_filename)
43
+ @xls = Roo::Spreadsheet.open(@xls_filename)
44
+ else
45
+ raise "File #{@xls_filename} does not exist"
46
+ end
47
+
48
+ # Initialize some other instance variables
49
+ @version = '0.0.1'
50
+ @analyses = [] # Array o OpenStudio::Analysis. Use method to access
51
+ @name = nil
52
+ @analysis_name = nil
53
+ @settings = {}
54
+ @weather_files = []
55
+ @weather_paths = []
56
+ @models = []
57
+ @other_files = []
58
+ @worker_inits = []
59
+ @worker_finals = []
60
+ @export_path = './export'
61
+ @measure_paths = []
62
+ @number_of_samples = 0 # TODO: remove this
63
+ @problem = {}
64
+ @algorithm = {}
65
+ @outputs = {}
66
+ @run_setup = {}
67
+ @aws_tags = []
68
+ end
69
+
70
+ def process
71
+ @setup = parse_setup
72
+
73
+ @version = Semantic::Version.new @version
74
+ raise "Spreadsheet version #{@version} is no longer supported. Please upgrade your spreadsheet to at least 0.1.9" if @version < '0.1.9'
75
+
76
+ @variables = parse_variables
77
+
78
+ @outputs = parse_outputs
79
+
80
+ # call validate to make sure everything that is needed exists (i.e. directories)
81
+ validate_analysis
82
+ end
83
+
84
+ # Helper methods to remove models and add new ones programatically. Note that these should
85
+ # be moved into a general analysis class
86
+ def delete_models
87
+ @models = []
88
+ end
89
+
90
+ def add_model(name, display_name, type, path)
91
+ @models << {
92
+ name: name,
93
+ display_name: display_name,
94
+ type: type,
95
+ path: path
96
+ }
97
+ end
98
+
99
+ def validate_analysis
100
+ # Setup the paths and do some error checking
101
+ @measure_paths.each do |mp|
102
+ raise "Measures directory '#{mp}' does not exist" unless Dir.exist?(mp)
103
+ end
104
+
105
+ @models.uniq!
106
+ raise 'No seed models defined in spreadsheet' if @models.empty?
107
+
108
+ @models.each do |model|
109
+ raise "Seed model does not exist: #{model[:path]}" unless File.exist?(model[:path])
110
+ end
111
+
112
+ @weather_files.uniq!
113
+ raise 'No weather files found based on what is in the spreadsheet' if @weather_files.empty?
114
+
115
+ @weather_files.each do |wf|
116
+ raise "Weather file does not exist: #{wf}" unless File.exist?(wf)
117
+ end
118
+
119
+ # This can be a directory as well
120
+ @other_files.each do |f|
121
+ raise "Other files do not exist for: #{f[:path]}" unless File.exist?(f[:path])
122
+ end
123
+
124
+ @worker_inits.each do |f|
125
+ raise "Worker initialization file does not exist for: #{f[:path]}" unless File.exist?(f[:path])
126
+ end
127
+
128
+ @worker_finals.each do |f|
129
+ raise "Worker finalization file does not exist for: #{f[:path]}" unless File.exist?(f[:path])
130
+ end
131
+
132
+ FileUtils.mkdir_p(@export_path)
133
+
134
+ # verify that the measure display names are unique
135
+ # puts @variables.inspect
136
+ measure_display_names = @variables['data'].map { |m| m['enabled'] ? m['display_name'] : nil }.compact
137
+ measure_display_names_mult = measure_display_names.select { |m| measure_display_names.count(m) > 1 }.uniq
138
+ if measure_display_names_mult && !measure_display_names_mult.empty?
139
+ raise "Measure Display Names are not unique for '#{measure_display_names_mult.join('\', \'')}'"
140
+ end
141
+
142
+ # verify that all continuous variables have all the data needed and create a name map
143
+ variable_names = []
144
+ @variables['data'].each do |measure|
145
+ if measure['enabled']
146
+ measure['variables'].each do |variable|
147
+ # Determine if row is suppose to be an argument or a variable to be perturbed.
148
+ if variable['variable_type'] == 'variable'
149
+ variable_names << variable['display_name']
150
+
151
+ # make sure that variables have static values
152
+ if variable['distribution']['static_value'].nil? || variable['distribution']['static_value'] == ''
153
+ raise "Variable #{measure['name']}:#{variable['name']} needs a static value"
154
+ end
155
+
156
+ if variable['type'] == 'enum' || variable['type'] == 'Choice'
157
+ # check something
158
+ else # must be an integer or double
159
+ if variable['distribution']['type'] == 'discrete_uncertain'
160
+ if variable['distribution']['discrete_values'].nil? || variable['distribution']['discrete_values'] == ''
161
+ raise "Variable #{measure['name']}:#{variable['name']} needs discrete values"
162
+ end
163
+ elsif variable['distribution']['type'] == 'integer_sequence'
164
+ if variable['distribution']['mean'].nil? || variable['distribution']['mean'] == ''
165
+ raise "Variable #{measure['name']}:#{variable['name']} must have a mean/mode"
166
+ end
167
+ if variable['distribution']['min'].nil? || variable['distribution']['min'] == ''
168
+ raise "Variable #{measure['name']}:#{variable['name']} must have a minimum"
169
+ end
170
+ if variable['distribution']['max'].nil? || variable['distribution']['max'] == ''
171
+ raise "Variable #{measure['name']}:#{variable['name']} must have a maximum"
172
+ end
173
+ else
174
+ if variable['distribution']['mean'].nil? || variable['distribution']['mean'] == ''
175
+ raise "Variable #{measure['name']}:#{variable['name']} must have a mean"
176
+ end
177
+ if variable['distribution']['stddev'].nil? || variable['distribution']['stddev'] == ''
178
+ raise "Variable #{measure['name']}:#{variable['name']} must have a stddev"
179
+ end
180
+ end
181
+
182
+ if variable['distribution']['mean'].nil? || variable['distribution']['mean'] == ''
183
+ raise "Variable #{measure['name']}:#{variable['name']} must have a mean/mode"
184
+ end
185
+ if variable['distribution']['min'].nil? || variable['distribution']['min'] == ''
186
+ raise "Variable #{measure['name']}:#{variable['name']} must have a minimum"
187
+ end
188
+ if variable['distribution']['max'].nil? || variable['distribution']['max'] == ''
189
+ raise "Variable #{measure['name']}:#{variable['name']} must have a maximum"
190
+ end
191
+ unless variable['type'] == 'string' || variable['type'] =~ /bool/
192
+ if variable['distribution']['min'] > variable['distribution']['max']
193
+ raise "Variable min is greater than variable max for #{measure['name']}:#{variable['name']}"
194
+ end
195
+ end
196
+
197
+ end
198
+ end
199
+ end
200
+ end
201
+ end
202
+
203
+ dupes = variable_names.select { |e| variable_names.count(e) > 1 }.uniq
204
+ if dupes.count > 0
205
+ raise "duplicate variable names found in list #{dupes.inspect}"
206
+ end
207
+
208
+ # most of the checks will raise a runtime exception, so this true will never be called
209
+ true
210
+ end
211
+
212
+ # convert the data in excel's parsed data into an OpenStudio Analysis Object
213
+ #
214
+ # @seed_model [Hash] Seed model to set the new analysis to
215
+ # @append_model_name [Boolean] Append the name of the seed model to the display name
216
+ # @return [Object] An OpenStudio::Analysis
217
+ def analysis(seed_model = nil, append_model_name = false)
218
+ raise 'There are no seed models defined in the excel file. Please add one.' if @models.empty?
219
+ raise "There are more than one seed models defined in the excel file. Call 'analyses' to return the array" if @models.size > 1 && seed_model.nil?
220
+
221
+ seed_model = @models.first if seed_model.nil?
222
+
223
+ # Use the programmatic interface to make the analysis
224
+ # append the model name to the analysis name if requested (normally if there are more than 1 models in the spreadsheet)
225
+ display_name = append_model_name ? @name + ' ' + seed_model[:display_name] : @name
226
+
227
+ a = OpenStudio::Analysis.create(display_name)
228
+
229
+ @variables['data'].each do |measure|
230
+ next unless measure['enabled']
231
+
232
+ @measure_paths.each do |measure_path|
233
+ measure_dir_to_add = "#{measure_path}/#{measure['measure_file_name_directory']}"
234
+ if Dir.exist? measure_dir_to_add
235
+ if File.exist? "#{measure_dir_to_add}/measure.rb"
236
+ measure['local_path_to_measure'] = "#{measure_dir_to_add}/measure.rb"
237
+ break
238
+ else
239
+ raise "Measure in directory '#{measure_dir_to_add}' did not contain a measure.rb file"
240
+ end
241
+ end
242
+ end
243
+
244
+ raise "Could not find measure '#{measure['name']}' in directory named '#{measure['measure_file_name_directory']}' in the measure paths '#{@measure_paths.join(', ')}'" unless measure['local_path_to_measure']
245
+
246
+ a.workflow.add_measure_from_excel(measure)
247
+ end
248
+
249
+ @other_files.each do |library|
250
+ a.libraries.add(library[:path], library_name: library[:lib_zip_name])
251
+ end
252
+
253
+ @worker_inits.each do |w|
254
+ a.worker_inits.add(w[:path], args: w[:args])
255
+ end
256
+
257
+ @worker_finals.each do |w|
258
+ a.worker_finalizes.add(w[:path], args: w[:args])
259
+ end
260
+
261
+ # Add in the outputs
262
+ @outputs['output_variables'].each do |o|
263
+ o = Hash[o.map { |k, v| [k.to_sym, v] }]
264
+ a.add_output(o)
265
+ end
266
+
267
+ a.analysis_type = @problem['analysis_type']
268
+ @algorithm.each do |k, v|
269
+ a.algorithm.set_attribute(k, v)
270
+ end
271
+
272
+ # clear out the seed files before adding new ones
273
+ a.seed_model = seed_model[:path]
274
+
275
+ # clear out the weather files before adding new ones
276
+ a.weather_files.clear
277
+ @weather_paths.each do |wp|
278
+ a.weather_files.add_files(wp)
279
+ end
280
+
281
+ a
282
+ end
283
+
284
+ # Return an array of analyses objects of OpenStudio::Analysis::Formulation
285
+ def analyses
286
+ as = []
287
+ @models.map do |model|
288
+ as << analysis(model, @models.count > 1)
289
+ end
290
+
291
+ as
292
+ end
293
+
294
+ # Method to return the cluster name for backwards compatibility
295
+ def cluster_name
296
+ @settings['cluster_name']
297
+ end
298
+
299
+ # save_analysis will iterate over each model that is defined in the spreadsheet and save the
300
+ # zip and json file.
301
+ def save_analysis
302
+ analyses.each do |a|
303
+ puts "Saving JSON and ZIP file for #{@name}:#{a.display_name}"
304
+ json_file_name = "#{@export_path}/#{a.name}.json"
305
+ FileUtils.rm_f(json_file_name) if File.exist?(json_file_name)
306
+ # File.open(json_file_name, 'w') { |f| f << JSON.pretty_generate(new_analysis_json) }
307
+
308
+ a.save json_file_name
309
+ a.save_zip "#{File.dirname(json_file_name)}/#{File.basename(json_file_name, '.*')}.zip"
310
+ end
311
+ end
312
+
313
+ protected
314
+
315
+ # parse_setup will pull out the data on the "setup" tab and store it in memory for later use
316
+ def parse_setup
317
+ rows = @xls.sheet('Setup').parse
318
+ b_settings = false
319
+ b_run_setup = false
320
+ b_problem_setup = false
321
+ b_algorithm_setup = false
322
+ b_weather_files = false
323
+ b_models = false
324
+ b_other_libs = false
325
+ b_worker_init = false
326
+ b_worker_final = false
327
+
328
+ rows.each do |row|
329
+ if row[0] == 'Settings'
330
+ b_settings = true
331
+ b_run_setup = false
332
+ b_problem_setup = false
333
+ b_algorithm_setup = false
334
+ b_weather_files = false
335
+ b_models = false
336
+ b_other_libs = false
337
+ b_worker_init = false
338
+ b_worker_final = false
339
+ next
340
+ elsif row[0] == 'Running Setup'
341
+ b_settings = false
342
+ b_run_setup = true
343
+ b_problem_setup = false
344
+ b_algorithm_setup = false
345
+ b_weather_files = false
346
+ b_models = false
347
+ b_other_libs = false
348
+ b_worker_init = false
349
+ b_worker_final = false
350
+ next
351
+ elsif row[0] == 'Problem Definition'
352
+ b_settings = false
353
+ b_run_setup = false
354
+ b_problem_setup = true
355
+ b_algorithm_setup = false
356
+ b_weather_files = false
357
+ b_models = false
358
+ b_other_libs = false
359
+ b_worker_init = false
360
+ b_worker_final = false
361
+ next
362
+ elsif row[0] == 'Algorithm Setup'
363
+ b_settings = false
364
+ b_run_setup = false
365
+ b_problem_setup = false
366
+ b_algorithm_setup = true
367
+ b_weather_files = false
368
+ b_models = false
369
+ b_other_libs = false
370
+ b_worker_init = false
371
+ b_worker_final = false
372
+ next
373
+ elsif row[0] == 'Weather Files'
374
+ b_settings = false
375
+ b_run_setup = false
376
+ b_problem_setup = false
377
+ b_algorithm_setup = false
378
+ b_weather_files = true
379
+ b_models = false
380
+ b_other_libs = false
381
+ b_worker_init = false
382
+ b_worker_final = false
383
+ next
384
+ elsif row[0] == 'Models'
385
+ b_settings = false
386
+ b_run_setup = false
387
+ b_problem_setup = false
388
+ b_algorithm_setup = false
389
+ b_weather_files = false
390
+ b_models = true
391
+ b_other_libs = false
392
+ b_worker_init = false
393
+ b_worker_final = false
394
+ next
395
+ elsif row[0] == 'Other Library Files'
396
+ b_settings = false
397
+ b_run_setup = false
398
+ b_problem_setup = false
399
+ b_algorithm_setup = false
400
+ b_weather_files = false
401
+ b_models = false
402
+ b_other_libs = true
403
+ b_worker_init = false
404
+ b_worker_final = false
405
+ next
406
+ elsif row[0] =~ /Worker Initialization Scripts/
407
+ b_settings = false
408
+ b_run_setup = false
409
+ b_problem_setup = false
410
+ b_algorithm_setup = false
411
+ b_weather_files = false
412
+ b_models = false
413
+ b_other_libs = false
414
+ b_worker_init = true
415
+ b_worker_final = false
416
+ next
417
+ elsif row[0] =~ /Worker Finalization Scripts/
418
+ b_settings = false
419
+ b_run_setup = false
420
+ b_problem_setup = false
421
+ b_algorithm_setup = false
422
+ b_weather_files = false
423
+ b_models = false
424
+ b_other_libs = false
425
+ b_worker_init = false
426
+ b_worker_final = true
427
+ next
428
+ end
429
+
430
+ next if row[0].nil?
431
+
432
+ if b_settings
433
+ @version = row[1].chomp if row[0] == 'Spreadsheet Version'
434
+ @settings[row[0].to_underscore.to_s] = row[1] if row[0]
435
+ if @settings['cluster_name']
436
+ @settings['cluster_name'] = @settings['cluster_name'].to_underscore
437
+ end
438
+
439
+ if row[0] == 'AWS Tag'
440
+ @aws_tags << row[1].strip
441
+ end
442
+
443
+ # type some of the values that we know
444
+ @settings['proxy_port'] = @settings['proxy_port'].to_i if @settings['proxy_port']
445
+
446
+ elsif b_run_setup
447
+ if row[0] == 'Analysis Name'
448
+ if row[1]
449
+ @name = row[1]
450
+ else
451
+ @name = SecureRandom.uuid
452
+ end
453
+ @analysis_name = @name.to_underscore
454
+ end
455
+ if row[0] == 'Export Directory'
456
+ tmp_filepath = row[1]
457
+ if (Pathname.new tmp_filepath).absolute?
458
+ @export_path = tmp_filepath
459
+ else
460
+ @export_path = File.expand_path(File.join(@root_path, tmp_filepath))
461
+ end
462
+ end
463
+ if row[0] == 'Measure Directory'
464
+ tmp_filepath = row[1]
465
+ if (Pathname.new tmp_filepath).absolute?
466
+ @measure_paths << tmp_filepath
467
+ else
468
+ @measure_paths << File.expand_path(File.join(@root_path, tmp_filepath))
469
+ end
470
+ end
471
+ @run_setup[row[0].to_underscore.to_s] = row[1] if row[0]
472
+
473
+ # type cast
474
+ if @run_setup['allow_multiple_jobs']
475
+ raise 'allow_multiple_jobs is no longer a valid option in the Excel file, please delete the row and rerun'
476
+ end
477
+ if @run_setup['use_server_as_worker']
478
+ raise 'use_server_as_worker is no longer a valid option in the Excel file, please delete the row and rerun'
479
+ end
480
+ elsif b_problem_setup
481
+ if row[0]
482
+ v = row[1]
483
+ v.to_i if v % 1 == 0
484
+ @problem[row[0].to_underscore.to_s] = v
485
+ end
486
+
487
+ elsif b_algorithm_setup
488
+ if row[0] && !row[0].empty?
489
+ v = row[1]
490
+ v = v.to_i if v % 1 == 0
491
+ @algorithm[row[0].to_underscore.to_s] = v
492
+ end
493
+ elsif b_weather_files
494
+ if row[0] == 'Weather File'
495
+ weather_path = row[1]
496
+ unless (Pathname.new weather_path).absolute?
497
+ weather_path = File.expand_path(File.join(@root_path, weather_path))
498
+ end
499
+ @weather_paths << weather_path
500
+ @weather_files += Dir.glob(weather_path)
501
+ end
502
+ elsif b_models
503
+ if row[1]
504
+ tmp_m_name = row[1]
505
+ else
506
+ tmp_m_name = SecureRandom.uuid
507
+ end
508
+ # Only add models if the row is flagged
509
+ if row[0]&.casecmp('model')&.zero?
510
+ model_path = row[3]
511
+ unless (Pathname.new model_path).absolute?
512
+ model_path = File.expand_path(File.join(@root_path, model_path))
513
+ end
514
+ @models << { name: tmp_m_name.to_underscore, display_name: tmp_m_name, type: row[2], path: model_path }
515
+ end
516
+ elsif b_other_libs
517
+ # determine if the path is relative
518
+ other_path = row[2]
519
+ unless (Pathname.new other_path).absolute?
520
+ other_path = File.expand_path(File.join(@root_path, other_path))
521
+ end
522
+
523
+ @other_files << { lib_zip_name: row[1], path: other_path }
524
+ elsif b_worker_init
525
+ worker_init_path = row[1]
526
+ unless (Pathname.new worker_init_path).absolute?
527
+ worker_init_path = File.expand_path(File.join(@root_path, worker_init_path))
528
+ end
529
+
530
+ @worker_inits << { name: row[0], path: worker_init_path, args: row[2] }
531
+ elsif b_worker_final
532
+ worker_final_path = row[1]
533
+ unless (Pathname.new worker_final_path).absolute?
534
+ worker_final_path = File.expand_path(File.join(@root_path, worker_final_path))
535
+ end
536
+
537
+ @worker_finals << { name: row[0], path: worker_final_path, args: row[2] }
538
+ end
539
+
540
+ next
541
+ end
542
+
543
+ # do some last checks
544
+ @measure_paths = ['./measures'] if @measure_paths.empty?
545
+ end
546
+
547
+ # parse_variables will parse the XLS spreadsheet and save the data into
548
+ # a higher level JSON file. The JSON file is historic and it should really
549
+ # be omitted as an intermediate step
550
+ def parse_variables
551
+ # clean remove whitespace and unicode chars
552
+ # The parse is a unique format (https://github.com/Empact/roo/blob/master/lib/roo/base.rb#L444)
553
+ # If you add a new column and you want that variable in the hash, then you must add it here.
554
+ # rows = @xls.sheet('Variables').parse(:enabled => "# variable")
555
+ # puts rows.inspect
556
+
557
+ rows = nil
558
+ begin
559
+ if @version >= '0.3.3'.to_version
560
+ rows = @xls.sheet('Variables').parse(enabled: /# variable/i,
561
+ measure_name_or_var_type: /type/i,
562
+ measure_file_name_or_var_display_name: /parameter\sdisplay\sname.*/i,
563
+ measure_file_name_directory: /measure\sdirectory/i,
564
+ measure_type_or_parameter_name_in_measure: /parameter\sname\sin\smeasure/i,
565
+ display_name_short: /parameter\sshort\sdisplay\sname/i,
566
+ # sampling_method: /sampling\smethod/i,
567
+ variable_type: /variable\stype/i,
568
+ units: /units/i,
569
+ default_value: /static.default\svalue/i,
570
+ enums: /enumerations/i,
571
+ min: /min/i,
572
+ max: /max/i,
573
+ mode: /mean|mode/i,
574
+ stddev: /std\sdev/i,
575
+ delta_x: /delta.x/i,
576
+ discrete_values: /discrete\svalues/i,
577
+ discrete_weights: /discrete\sweights/i,
578
+ distribution: /distribution/i,
579
+ source: /data\ssource/i,
580
+ notes: /notes/i,
581
+ relation_to_eui: /typical\svar\sto\seui\srelationship/i,
582
+ clean: true)
583
+ elsif @version >= '0.3.0'.to_version
584
+ rows = @xls.sheet('Variables').parse(enabled: /# variable/i,
585
+ measure_name_or_var_type: /type/i,
586
+ measure_file_name_or_var_display_name: /parameter\sdisplay\sname.*/i,
587
+ measure_file_name_directory: /measure\sdirectory/i,
588
+ measure_type_or_parameter_name_in_measure: /parameter\sname\sin\smeasure/i,
589
+ # sampling_method: /sampling\smethod/i,
590
+ variable_type: /variable\stype/i,
591
+ units: /units/i,
592
+ default_value: /static.default\svalue/i,
593
+ enums: /enumerations/i,
594
+ min: /min/i,
595
+ max: /max/i,
596
+ mode: /mean|mode/i,
597
+ stddev: /std\sdev/i,
598
+ delta_x: /delta.x/i,
599
+ discrete_values: /discrete\svalues/i,
600
+ discrete_weights: /discrete\sweights/i,
601
+ distribution: /distribution/i,
602
+ source: /data\ssource/i,
603
+ notes: /notes/i,
604
+ relation_to_eui: /typical\svar\sto\seui\srelationship/i,
605
+ clean: true)
606
+ elsif @version >= '0.2.0'.to_version
607
+ rows = @xls.sheet('Variables').parse(enabled: /# variable/i,
608
+ measure_name_or_var_type: /type/i,
609
+ measure_file_name_or_var_display_name: /parameter\sdisplay\sname.*/i,
610
+ measure_file_name_directory: /measure\sdirectory/i,
611
+ measure_type_or_parameter_name_in_measure: /parameter\sname\sin\smeasure/i,
612
+ sampling_method: /sampling\smethod/i,
613
+ variable_type: /variable\stype/i,
614
+ units: /units/i,
615
+ default_value: /static.default\svalue/i,
616
+ enums: /enumerations/i,
617
+ min: /min/i,
618
+ max: /max/i,
619
+ mode: /mean|mode/i,
620
+ stddev: /std\sdev/i,
621
+ delta_x: /delta.x/i,
622
+ discrete_values: /discrete\svalues/i,
623
+ discrete_weights: /discrete\sweights/i,
624
+ distribution: /distribution/i,
625
+ source: /data\ssource/i,
626
+ notes: /notes/i,
627
+ relation_to_eui: /typical\svar\sto\seui\srelationship/i,
628
+ clean: true)
629
+ elsif @version >= '0.1.12'.to_version
630
+ rows = @xls.sheet('Variables').parse(enabled: /# variable/i,
631
+ measure_name_or_var_type: /type/i,
632
+ measure_file_name_or_var_display_name: /parameter\sdisplay\sname.*/i,
633
+ measure_type_or_parameter_name_in_measure: /parameter\sname\sin\smeasure/i,
634
+ sampling_method: /sampling\smethod/i,
635
+ variable_type: /variable\stype/i,
636
+ units: /units/i,
637
+ default_value: /static.default\svalue/i,
638
+ enums: /enumerations/i,
639
+ min: /min/i,
640
+ max: /max/i,
641
+ mode: /mean|mode/i,
642
+ stddev: /std\sdev/i,
643
+ delta_x: /delta.x/i,
644
+ discrete_values: /discrete\svalues/i,
645
+ discrete_weights: /discrete\sweights/i,
646
+ distribution: /distribution/i,
647
+ source: /data\ssource/i,
648
+ notes: /notes/i,
649
+ relation_to_eui: /typical\svar\sto\seui\srelationship/i,
650
+ clean: true)
651
+ elsif @version >= '0.1.11'.to_version
652
+ rows = @xls.sheet('Variables').parse(enabled: /# variable/i,
653
+ measure_name_or_var_type: /type/i,
654
+ measure_file_name_or_var_display_name: /parameter\sdisplay\sname.*/i,
655
+ measure_type_or_parameter_name_in_measure: /parameter\sname\sin\smeasure/i,
656
+ sampling_method: /sampling\smethod/i,
657
+ variable_type: /variable\stype/i,
658
+ units: /units/i,
659
+ default_value: /static.default\svalue/i,
660
+ enums: /enumerations/i,
661
+ min: /min/i,
662
+ max: /max/i,
663
+ mode: /mean|mode/i,
664
+ stddev: /std\sdev/i,
665
+ # delta_x: /delta.x/i,
666
+ discrete_values: /discrete\svalues/i,
667
+ discrete_weights: /discrete\sweights/i,
668
+ distribution: /distribution/i,
669
+ source: /data\ssource/i,
670
+ notes: /notes/i,
671
+ relation_to_eui: /typical\svar\sto\seui\srelationship/i,
672
+ clean: true)
673
+ else
674
+ rows = @xls.sheet('Variables').parse(enabled: /# variable/i,
675
+ measure_name_or_var_type: /type/i,
676
+ measure_file_name_or_var_display_name: /parameter\sdisplay\sname.*/i,
677
+ measure_type_or_parameter_name_in_measure: /parameter\sname\sin\smeasure/i,
678
+ sampling_method: /sampling\smethod/i,
679
+ variable_type: /variable\stype/i,
680
+ units: /units/i,
681
+ default_value: /static.default\svalue/i,
682
+ enums: /enumerations/i,
683
+ min: /min/i,
684
+ max: /max/i,
685
+ mode: /mean|mode/i,
686
+ stddev: /std\sdev/i,
687
+ # delta_x: /delta.x/i,
688
+ # discrete_values: /discrete\svalues/i,
689
+ # discrete_weights: /discrete\sweights/i,
690
+ distribution: /distribution/i,
691
+ source: /data\ssource/i,
692
+ notes: /notes/i,
693
+ relation_to_eui: /typical\svar\sto\seui\srelationship/i,
694
+ clean: true)
695
+ end
696
+ rescue StandardError => e
697
+ raise "Unable to parse spreadsheet #{@xls_filename} with version #{@version} due to error: #{e.message}"
698
+ end
699
+
700
+ raise "Could not find the sheet name 'Variables' in excel file #{@root_path}" unless rows
701
+
702
+ # map the data to another hash that is more easily processed
703
+ data = {}
704
+ data['data'] = []
705
+
706
+ measure_index = -1
707
+ variable_index = -1
708
+ measure_name = nil
709
+ rows.each_with_index do |row, icnt|
710
+ # puts "Parsing line: #{icnt}:#{row}"
711
+
712
+ # check if we are a measure - nil means that the cell was blank
713
+ if row[:enabled].nil?
714
+ if measure_name && data['data'][measure_index]['enabled']
715
+ variable_index += 1
716
+
717
+ var = {}
718
+ var['variable_type'] = row[:measure_name_or_var_type]
719
+ var['display_name'] = row[:measure_file_name_or_var_display_name]
720
+ var['display_name_short'] = row[:display_name_short] ? row[:display_name_short] : var['display_name']
721
+ var['name'] = row[:measure_type_or_parameter_name_in_measure]
722
+ var['index'] = variable_index # order of the variable (not sure of its need)
723
+ var['type'] = row[:variable_type].downcase
724
+ var['units'] = row[:units]
725
+ var['distribution'] = {}
726
+
727
+ # parse the choices/enums
728
+ if var['type'] == 'enum' || var['type'] == 'choice' # this is now a choice
729
+ if row[:enums]
730
+ var['distribution']['enumerations'] = row[:enums].delete('|').split(',').map(&:strip)
731
+ end
732
+ elsif var['type'] == 'bool'
733
+ var['distribution']['enumerations'] = []
734
+ var['distribution']['enumerations'] << 'true' # TODO: should this be a real bool?
735
+ var['distribution']['enumerations'] << 'false'
736
+ end
737
+
738
+ var['distribution']['min'] = row[:min]
739
+ var['distribution']['max'] = row[:max]
740
+ var['distribution']['mean'] = row[:mode]
741
+ var['distribution']['stddev'] = row[:stddev]
742
+ var['distribution']['discrete_values'] = row[:discrete_values]
743
+ var['distribution']['discrete_weights'] = row[:discrete_weights]
744
+ var['distribution']['type'] = row[:distribution]
745
+ var['distribution']['static_value'] = row[:default_value]
746
+ var['distribution']['delta_x'] = row[:delta_x]
747
+
748
+ # type various values correctly
749
+ var['distribution']['min'] = typecast_value(var['type'], var['distribution']['min'])
750
+ var['distribution']['max'] = typecast_value(var['type'], var['distribution']['max'])
751
+ var['distribution']['mean'] = typecast_value(var['type'], var['distribution']['mean'])
752
+ var['distribution']['stddev'] = typecast_value(var['type'], var['distribution']['stddev'])
753
+ var['distribution']['static_value'] = typecast_value(var['type'], var['distribution']['static_value'])
754
+
755
+ # eval the discrete value and weight arrays
756
+ case var['type']
757
+ when 'bool', 'boolean'
758
+ if var['distribution']['discrete_values']
759
+ var['distribution']['discrete_values'] = eval(var['distribution']['discrete_values']).map { |v| v.to_s == 'true' }
760
+ end
761
+ if var['distribution']['discrete_weights'] && var['distribution']['discrete_weights'] != ''
762
+ var['distribution']['discrete_weights'] = eval(var['distribution']['discrete_weights'])
763
+ end
764
+ else
765
+ if var['distribution']['discrete_values']
766
+ var['distribution']['discrete_values'] = eval(var['distribution']['discrete_values'])
767
+ end
768
+ if var['distribution']['discrete_weights'] && var['distribution']['discrete_weights'] != ''
769
+ var['distribution']['discrete_weights'] = eval(var['distribution']['discrete_weights'])
770
+ end
771
+ end
772
+
773
+ var['distribution']['source'] = row[:source]
774
+ var['notes'] = row[:notes]
775
+ var['relation_to_eui'] = row[:relation_to_eui]
776
+
777
+ data['data'][measure_index]['variables'] << var
778
+ end
779
+ else
780
+ measure_index += 1
781
+ variable_index = 0
782
+ data['data'][measure_index] = {}
783
+
784
+ # generate name id
785
+ # TODO: put this into a logger. puts "Parsing measure #{row[1]}"
786
+ display_name = row[:measure_name_or_var_type]
787
+ measure_name = display_name.downcase.strip.tr('-', '_').tr(' ', '_').gsub('__', '_')
788
+ data['data'][measure_index]['display_name'] = display_name
789
+ data['data'][measure_index]['name'] = measure_name
790
+ data['data'][measure_index]['enabled'] = row[:enabled]
791
+ data['data'][measure_index]['measure_file_name'] = row[:measure_file_name_or_var_display_name]
792
+ if row[:measure_file_name_directory]
793
+ data['data'][measure_index]['measure_file_name_directory'] = row[:measure_file_name_directory]
794
+ else
795
+ data['data'][measure_index]['measure_file_name_directory'] = row[:measure_file_name_or_var_display_name].to_underscore
796
+ end
797
+ data['data'][measure_index]['measure_type'] = row[:measure_type_or_parameter_name_in_measure]
798
+ data['data'][measure_index]['version'] = @version_id
799
+
800
+ data['data'][measure_index]['variables'] = []
801
+ end
802
+ end
803
+
804
+ data
805
+ end
806
+
807
+ def parse_outputs
808
+ rows = nil
809
+ if @version >= '0.3.3'.to_version
810
+ rows = @xls.sheet('Outputs').parse(display_name: /variable\sdisplay\sname/i,
811
+ display_name_short: /short\sdisplay\sname/i,
812
+ metadata_id: /taxonomy\sidentifier/i,
813
+ name: /^name$/i,
814
+ units: /units/i,
815
+ visualize: /visualize/i,
816
+ export: /export/i,
817
+ variable_type: /variable\stype/i,
818
+ objective_function: /objective\sfunction/i,
819
+ objective_function_target: /objective\sfunction\starget/i,
820
+ scaling_factor: /scale/i,
821
+ objective_function_group: /objective\sfunction\sgroup/i)
822
+ elsif @version >= '0.3.0'.to_version
823
+ rows = @xls.sheet('Outputs').parse(display_name: /variable\sdisplay\sname/i,
824
+ # display_name_short: /short\sdisplay\sname/i,
825
+ metadata_id: /taxonomy\sidentifier/i,
826
+ name: /^name$/i,
827
+ units: /units/i,
828
+ visualize: /visualize/i,
829
+ export: /export/i,
830
+ variable_type: /variable\stype/i,
831
+ objective_function: /objective\sfunction/i,
832
+ objective_function_target: /objective\sfunction\starget/i,
833
+ scaling_factor: /scale/i,
834
+ objective_function_group: /objective\sfunction\sgroup/i)
835
+ else
836
+ rows = @xls.sheet('Outputs').parse(display_name: /variable\sdisplay\sname/i,
837
+ # display_name_short: /short\sdisplay\sname/i,
838
+ # metadata_id: /taxonomy\sidentifier/i,
839
+ name: /^name$/i,
840
+ units: /units/i,
841
+ # visualize: /visualize/i,
842
+ # export: /export/i,
843
+ # variable_type: /variable\stype/i,
844
+ objective_function: /objective\sfunction/i,
845
+ objective_function_target: /objective\sfunction\starget/i,
846
+ scaling_factor: /scale/i,
847
+ objective_function_group: /objective/i)
848
+
849
+ end
850
+
851
+ unless rows
852
+ raise "Could not find the sheet name 'Outputs' in excel file #{@root_path}"
853
+ end
854
+
855
+ data = {}
856
+ data['output_variables'] = []
857
+
858
+ variable_index = -1
859
+ group_index = 1
860
+
861
+ rows.each_with_index do |row, icnt|
862
+ next if icnt < 1 # skip the first 3 lines of the file
863
+
864
+ var = {}
865
+ var['display_name'] = row[:display_name]
866
+ var['display_name_short'] = row[:display_name_short] ? row[:display_name_short] : row[:display_name]
867
+ var['metadata_id'] = row[:metadata_id]
868
+ var['name'] = row[:name]
869
+ var['units'] = row[:units]
870
+ var['visualize'] = row[:visualize]
871
+ var['export'] = row[:export]
872
+ var['variable_type'] = row[:variable_type].downcase if row[:variable_type]
873
+ var['objective_function'] = row[:objective_function]
874
+ var['objective_function_target'] = row[:objective_function_target]
875
+ var['scaling_factor'] = row[:scaling_factor]
876
+
877
+ if var['objective_function']
878
+ if row[:objective_function_group].nil?
879
+ var['objective_function_group'] = group_index
880
+ group_index += 1
881
+ else
882
+ var['objective_function_group'] = row[:objective_function_group]
883
+ end
884
+ end
885
+ data['output_variables'] << var
886
+ end
887
+
888
+ data
889
+ end
890
+ end
891
+ end
892
+ end
893
+ end