openstudio-analysis 1.3.6 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/openstudio-analysis.yml +65 -40
- data/.gitignore +21 -21
- data/.rubocop.yml +9 -9
- data/CHANGELOG.md +278 -269
- data/Gemfile +14 -14
- data/README.md +102 -102
- data/Rakefile +40 -40
- data/lib/openstudio/analysis/algorithm_attributes.rb +47 -47
- data/lib/openstudio/analysis/formulation.rb +881 -857
- data/lib/openstudio/analysis/server_api.rb +862 -862
- data/lib/openstudio/analysis/server_scripts.rb +108 -108
- data/lib/openstudio/analysis/support_files.rb +104 -104
- data/lib/openstudio/analysis/translator/datapoints.rb +454 -454
- data/lib/openstudio/analysis/translator/excel.rb +893 -893
- data/lib/openstudio/analysis/translator/workflow.rb +143 -143
- data/lib/openstudio/analysis/version.rb +12 -12
- data/lib/openstudio/analysis/workflow.rb +302 -279
- data/lib/openstudio/analysis/workflow_step.rb +523 -523
- data/lib/openstudio/analysis.rb +144 -144
- data/lib/openstudio/helpers/hash.rb +10 -10
- data/lib/openstudio/helpers/string.rb +36 -36
- data/lib/openstudio/helpers/utils.rb +36 -36
- data/lib/openstudio/weather/epw.rb +178 -178
- data/lib/openstudio-analysis.rb +47 -47
- data/openstudio-analysis.gemspec +38 -38
- data/update_license.rb +60 -60
- metadata +18 -18
@@ -1,454 +1,454 @@
|
|
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 Datapoints
|
10
|
-
attr_reader :version
|
11
|
-
attr_reader :settings
|
12
|
-
attr_reader :variables
|
13
|
-
attr_reader :outputs
|
14
|
-
attr_reader :models
|
15
|
-
attr_reader :measure_paths
|
16
|
-
attr_reader :weather_paths
|
17
|
-
attr_reader :worker_inits
|
18
|
-
attr_reader :worker_finals
|
19
|
-
attr_reader :export_path
|
20
|
-
attr_reader :cluster_name
|
21
|
-
attr_reader :variables
|
22
|
-
attr_reader :algorithm
|
23
|
-
attr_reader :problem
|
24
|
-
attr_reader :run_setup
|
25
|
-
attr_reader :aws_tags
|
26
|
-
|
27
|
-
# remove these once we have classes to construct the JSON file
|
28
|
-
attr_accessor :name
|
29
|
-
attr_reader :analysis_name
|
30
|
-
|
31
|
-
require 'rexml/document'
|
32
|
-
|
33
|
-
# Pass in the filename to read
|
34
|
-
def initialize(csv_filename)
|
35
|
-
@csv_filename = csv_filename
|
36
|
-
@root_path = File.expand_path(File.dirname(@csv_filename))
|
37
|
-
|
38
|
-
@csv = nil
|
39
|
-
# Try to read the spreadsheet as a roo object
|
40
|
-
if File.exist?(@csv_filename)
|
41
|
-
@csv = CSV.read(@csv_filename)
|
42
|
-
else
|
43
|
-
raise "File #{@csv_filename} does not exist"
|
44
|
-
end
|
45
|
-
|
46
|
-
# Remove nil rows and check row length
|
47
|
-
@csv.delete_if { |row| row.uniq.length == 1 && row.uniq[0].nil? }
|
48
|
-
|
49
|
-
# Initialize some other instance variables
|
50
|
-
@version = '0.0.1'
|
51
|
-
@analyses = [] # Array o OpenStudio::Analysis. Use method to access
|
52
|
-
@name = nil
|
53
|
-
@analysis_name = nil
|
54
|
-
@cluster_name = nil
|
55
|
-
@settings = {}
|
56
|
-
@weather_paths = []
|
57
|
-
@models = []
|
58
|
-
@other_files = []
|
59
|
-
@worker_inits = []
|
60
|
-
@worker_finals = []
|
61
|
-
@export_path = './export'
|
62
|
-
@measure_paths = []
|
63
|
-
@problem = {}
|
64
|
-
@algorithm = {}
|
65
|
-
@outputs = {}
|
66
|
-
@run_setup = {}
|
67
|
-
@aws_tags = []
|
68
|
-
end
|
69
|
-
|
70
|
-
def process
|
71
|
-
# Seperate CSV into meta and measure groups
|
72
|
-
measure_tag_index = nil
|
73
|
-
@csv.each_with_index { |row, index| measure_tag_index = index if row[0] == 'BEGIN-MEASURES' }
|
74
|
-
raise "ERROR: No 'BEGIN-MEASURES' tag found in input csv file." unless measure_tag_index
|
75
|
-
meta_rows = []
|
76
|
-
measure_rows = []
|
77
|
-
@csv.each_with_index do |_, index|
|
78
|
-
meta_rows << @csv[index] if index < measure_tag_index
|
79
|
-
measure_rows << @csv[index] if index > measure_tag_index
|
80
|
-
end
|
81
|
-
|
82
|
-
@setup = parse_csv_meta(meta_rows)
|
83
|
-
|
84
|
-
@version = Semantic::Version.new @version
|
85
|
-
raise "CSV interface version #{@version} is no longer supported. Please upgrade your csv interface to at least 0.0.1" if @version < '0.0.0'
|
86
|
-
|
87
|
-
@variables = parse_csv_measures(measure_rows)
|
88
|
-
|
89
|
-
# call validate to make sure everything that is needed exists (i.e. directories)
|
90
|
-
validate_analysis
|
91
|
-
end
|
92
|
-
|
93
|
-
# Helper methods to remove models and add new ones programatically. Note that these should
|
94
|
-
# be moved into a general analysis class
|
95
|
-
def delete_models
|
96
|
-
@models = []
|
97
|
-
end
|
98
|
-
|
99
|
-
def add_model(name, display_name, type, path)
|
100
|
-
@models << {
|
101
|
-
name: name,
|
102
|
-
display_name: display_name,
|
103
|
-
type: type,
|
104
|
-
path: path
|
105
|
-
}
|
106
|
-
end
|
107
|
-
|
108
|
-
def validate_analysis
|
109
|
-
# Setup the paths and do some error checking
|
110
|
-
@measure_paths.each do |mp|
|
111
|
-
raise "Measures directory '#{mp}' does not exist" unless Dir.exist?(mp)
|
112
|
-
end
|
113
|
-
|
114
|
-
@models.uniq!
|
115
|
-
raise 'No seed models defined in spreadsheet' if @models.empty?
|
116
|
-
|
117
|
-
@models.each do |model|
|
118
|
-
raise "Seed model does not exist: #{model[:path]}" unless File.exist?(model[:path])
|
119
|
-
end
|
120
|
-
|
121
|
-
@weather_paths.uniq!
|
122
|
-
raise 'No weather files found based on what is in the spreadsheet' if @weather_paths.empty?
|
123
|
-
|
124
|
-
@weather_paths.each do |wf|
|
125
|
-
raise "Weather file does not exist: #{wf}" unless File.exist?(wf)
|
126
|
-
end
|
127
|
-
|
128
|
-
# This can be a directory as well
|
129
|
-
@other_files.each do |f|
|
130
|
-
raise "Other files do not exist for: #{f[:path]}" unless File.exist?(f[:path])
|
131
|
-
end
|
132
|
-
|
133
|
-
@worker_inits.each do |f|
|
134
|
-
raise "Worker initialization file does not exist for: #{f[:path]}" unless File.exist?(f[:path])
|
135
|
-
end
|
136
|
-
|
137
|
-
@worker_finals.each do |f|
|
138
|
-
raise "Worker finalization file does not exist for: #{f[:path]}" unless File.exist?(f[:path])
|
139
|
-
end
|
140
|
-
|
141
|
-
FileUtils.mkdir_p(@export_path)
|
142
|
-
|
143
|
-
# verify that the measure display names are unique
|
144
|
-
# puts @variables.inspect
|
145
|
-
measure_display_names = @variables.map { |m| m[:measure_data][:display_name] }.compact
|
146
|
-
measure_display_names_mult = measure_display_names.select { |m| measure_display_names.count(m) > 1 }.uniq
|
147
|
-
if measure_display_names_mult && !measure_display_names_mult.empty?
|
148
|
-
raise "Measure Display Names are not unique for '#{measure_display_names_mult.join('\', \'')}'"
|
149
|
-
end
|
150
|
-
|
151
|
-
variable_names = @variables.map { |v| v[:vars].map { |hash| hash[:display_name] } }.flatten
|
152
|
-
dupes = variable_names.select { |e| variable_names.count(e) > 1 }.uniq
|
153
|
-
if dupes.count > 0
|
154
|
-
raise "duplicate variable names found in list #{dupes.inspect}"
|
155
|
-
end
|
156
|
-
end
|
157
|
-
|
158
|
-
# convert the data in excel's parsed data into an OpenStudio Analysis Object
|
159
|
-
# @seed_model [Hash] Seed model to set the new analysis to
|
160
|
-
# @append_model_name [Boolean] Append the name of the seed model to the display name
|
161
|
-
# @return [Object] An OpenStudio::Analysis
|
162
|
-
def analysis(seed_model = nil, append_model_name = false)
|
163
|
-
raise 'There are no seed models defined in the excel file. Please add one.' if @models.empty?
|
164
|
-
raise 'There are more than one seed models defined in the excel file. This is not supported by the CSV Translator.' if @models.size > 1 && seed_model.nil?
|
165
|
-
|
166
|
-
seed_model = @models.first if seed_model.nil?
|
167
|
-
|
168
|
-
# Use the programmatic interface to make the analysis
|
169
|
-
# append the model name to the analysis name if requested (normally if there are more than 1 models in the spreadsheet)
|
170
|
-
display_name = append_model_name ? @name + ' ' + seed_model[:display_name] : @name
|
171
|
-
|
172
|
-
a = OpenStudio::Analysis.create(display_name)
|
173
|
-
|
174
|
-
@variables.each do |measure|
|
175
|
-
@measure_paths.each do |measure_path|
|
176
|
-
measure_dir_to_add = "#{measure_path}/#{measure[:measure_data][:classname]}"
|
177
|
-
if Dir.exist? measure_dir_to_add
|
178
|
-
if File.exist? "#{measure_dir_to_add}/measure.rb"
|
179
|
-
measure[:measure_data][:local_path_to_measure] = "#{measure_dir_to_add}/measure.rb"
|
180
|
-
break
|
181
|
-
else
|
182
|
-
raise "Measure in directory '#{measure_dir_to_add}' did not contain a measure.rb file"
|
183
|
-
end
|
184
|
-
end
|
185
|
-
end
|
186
|
-
|
187
|
-
raise "Could not find measure '#{measure['name']}' in directory named '#{measure['measure_file_name_directory']}' in the measure paths '#{@measure_paths.join(', ')}'" unless measure[:measure_data][:local_path_to_measure]
|
188
|
-
|
189
|
-
a.workflow.add_measure_from_csv(measure)
|
190
|
-
end
|
191
|
-
|
192
|
-
@other_files.each do |library|
|
193
|
-
a.libraries.add(library[:path], library_name: library[:lib_zip_name])
|
194
|
-
end
|
195
|
-
|
196
|
-
@worker_inits.each do |w|
|
197
|
-
a.worker_inits.add(w[:path], args: w[:args])
|
198
|
-
end
|
199
|
-
|
200
|
-
@worker_finals.each do |w|
|
201
|
-
a.worker_finalizes.add(w[:path], args: w[:args])
|
202
|
-
end
|
203
|
-
|
204
|
-
# Add in the outputs
|
205
|
-
@outputs.each do |o|
|
206
|
-
o = Hash[o.map { |k, v| [k.to_sym, v] }]
|
207
|
-
a.add_output(o)
|
208
|
-
end
|
209
|
-
|
210
|
-
a.analysis_type = @problem['analysis_type']
|
211
|
-
|
212
|
-
# clear out the seed files before adding new ones
|
213
|
-
a.seed_model = seed_model[:path]
|
214
|
-
|
215
|
-
# clear out the weather files before adding new ones
|
216
|
-
a.weather_files.clear
|
217
|
-
@weather_paths.each do |wp|
|
218
|
-
a.weather_files.add_files(wp)
|
219
|
-
end
|
220
|
-
|
221
|
-
a
|
222
|
-
end
|
223
|
-
|
224
|
-
protected
|
225
|
-
|
226
|
-
def parse_csv_meta(meta_rows)
|
227
|
-
# Convert to hash
|
228
|
-
config_hash = {}
|
229
|
-
meta_rows.each do |row|
|
230
|
-
config_hash[row[0].to_sym] = row[1]
|
231
|
-
end
|
232
|
-
|
233
|
-
# Assign required attributes
|
234
|
-
raise 'Require setting not found: version' unless config_hash[:version]
|
235
|
-
@version = config_hash[:version]
|
236
|
-
|
237
|
-
if config_hash[:analysis_name]
|
238
|
-
@name = config_hash[:analysis_name]
|
239
|
-
else
|
240
|
-
@name = SecureRandom.uuid
|
241
|
-
end
|
242
|
-
@analysis_name = @name.to_underscore
|
243
|
-
|
244
|
-
raise 'Require setting not found: measure_path' unless config_hash[:measure_paths]
|
245
|
-
config_hash[:measure_paths] = [config_hash[:measure_paths]] unless config_hash[:measure_paths].respond_to?(:each)
|
246
|
-
config_hash[:measure_paths].each do |path|
|
247
|
-
if (Pathname.new path).absolute?
|
248
|
-
@measure_paths << path
|
249
|
-
else
|
250
|
-
@measure_paths << File.expand_path(File.join(@root_path, path))
|
251
|
-
end
|
252
|
-
end
|
253
|
-
|
254
|
-
raise 'Required setting not found: weather_paths' unless config_hash[:weather_paths]
|
255
|
-
config_hash[:weather_paths] = config_hash[:weather_paths].split(',')
|
256
|
-
config_hash[:weather_paths].each do |path|
|
257
|
-
if (Pathname.new path).absolute?
|
258
|
-
@weather_paths << path
|
259
|
-
else
|
260
|
-
@weather_paths << File.expand_path(File.join(@root_path, path))
|
261
|
-
end
|
262
|
-
end
|
263
|
-
|
264
|
-
raise 'Required setting not found: models' unless config_hash[:models]
|
265
|
-
config_hash[:models] = [config_hash[:models]] unless config_hash[:models].respond_to?(:each)
|
266
|
-
config_hash[:models].each do |path|
|
267
|
-
model_name = File.basename(path).split('.')[0]
|
268
|
-
model_name = SecureRandom.uuid if model_name == ''
|
269
|
-
type = File.basename(path).split('.')[1].upcase
|
270
|
-
unless (Pathname.new path).absolute?
|
271
|
-
path = File.expand_path(File.join(@root_path, path))
|
272
|
-
end
|
273
|
-
@models << { name: model_name.to_underscore, display_name: model_name, type: type, path: path }
|
274
|
-
end
|
275
|
-
|
276
|
-
# Assign optional attributes
|
277
|
-
if config_hash[:output_json]
|
278
|
-
path = File.expand_path(File.join(@root_path, config_hash[:output_json].to_s))
|
279
|
-
if File.exist? path
|
280
|
-
@outputs = MultiJson.load(File.read(path))
|
281
|
-
else
|
282
|
-
raise "Could not find output json: #{config_hash[:output_json]}"
|
283
|
-
end
|
284
|
-
end
|
285
|
-
|
286
|
-
if config_hash[:export_path]
|
287
|
-
if (Pathname.new config_hash[:export_path]).absolute?
|
288
|
-
@export_path = config_hash[:export_path]
|
289
|
-
else
|
290
|
-
@export_path = File.expand_path(File.join(@root_path, config_hash[:export_path]))
|
291
|
-
end
|
292
|
-
end
|
293
|
-
|
294
|
-
if config_hash[:library_path]
|
295
|
-
library_name = File.basename(config_hash[:library_path]).split('.')[0]
|
296
|
-
unless (Pathname.new config_hash[:library_path]).absolute?
|
297
|
-
config_hash[:library_path] = File.expand_path(File.join(@root_path, config_hash[:library_path]))
|
298
|
-
end
|
299
|
-
@other_files << { lib_zip_name: library_name, path: config_hash[:library_path] }
|
300
|
-
end
|
301
|
-
|
302
|
-
if config_hash[:allow_multiple_jobs]
|
303
|
-
raise 'allow_multiple_jobs is no longer a valid option in the CSV, please delete and rerun'
|
304
|
-
end
|
305
|
-
if config_hash[:use_server_as_worker]
|
306
|
-
raise 'use_server_as_worker is no longer a valid option in the CSV, please delete and rerun'
|
307
|
-
end
|
308
|
-
|
309
|
-
# Assign AWS settings
|
310
|
-
@settings[:proxy_port] = config_hash[:proxy_port] if config_hash[:proxy_port]
|
311
|
-
@settings[:cluster_name] = config_hash[:cluster_name] if config_hash[:cluster_name]
|
312
|
-
@settings[:user_id] = config_hash[:user_id] if config_hash[:user_id]
|
313
|
-
@settings[:os_server_version] = config_hash[:os_server_version] if config_hash[:os_server_version]
|
314
|
-
@settings[:server_instance_type] = config_hash[:server_instance_type] if config_hash[:server_instance_type]
|
315
|
-
@settings[:worker_instance_type] = config_hash[:worker_instance_type] if config_hash[:worker_instance_type]
|
316
|
-
@settings[:worker_node_number] = config_hash[:worker_node_number].to_i if config_hash[:worker_node_number]
|
317
|
-
@settings[:aws_tags] = config_hash[:aws_tags] if config_hash[:aws_tags]
|
318
|
-
@settings[:analysis_type] = 'batch_datapoints'
|
319
|
-
end
|
320
|
-
|
321
|
-
def parse_csv_measures(measure_rows)
|
322
|
-
# Build metadata required for parsing
|
323
|
-
measures = measure_rows[0].uniq.reject(&:nil?).map(&:to_sym)
|
324
|
-
measure_map = {}
|
325
|
-
measure_var_list = []
|
326
|
-
measures.each do |measure|
|
327
|
-
measure_map[measure] = {}
|
328
|
-
col_ind = (0..(measure_rows[0].length - 1)).to_a.select { |i| measure_rows[0][i] == measure.to_s }
|
329
|
-
col_ind.each do |var_ind|
|
330
|
-
tuple = measure.to_s + measure_rows[1][var_ind]
|
331
|
-
raise "Multiple measure_variable tuples found for '#{measure}_#{measure_rows[1][var_ind]}'. These tuples must be unique." if measure_var_list.include? tuple
|
332
|
-
measure_var_list << tuple
|
333
|
-
measure_map[measure][measure_rows[1][var_ind].to_sym] = var_ind
|
334
|
-
end
|
335
|
-
end
|
336
|
-
|
337
|
-
# For each measure load measure json and parse out critical variable requirements
|
338
|
-
data = []
|
339
|
-
measures.each_with_index do |measure, measure_index|
|
340
|
-
data[measure_index] = {}
|
341
|
-
measure_parsed = find_measure(measure.to_s)
|
342
|
-
|
343
|
-
raise "Could not find measure #{measure} xml in measure_paths: '#{@measure_paths.join("\n")}'" unless measure_parsed
|
344
|
-
measure_data = {}
|
345
|
-
measure_data[:classname] = measure_parsed[:classname]
|
346
|
-
measure_data[:name] = measure_parsed[:name]
|
347
|
-
measure_data[:display_name] = measure_parsed[:display_name]
|
348
|
-
measure_data[:measure_type] = measure_parsed[:measure_type]
|
349
|
-
measure_data[:uid] = measure_parsed[:uid]
|
350
|
-
measure_data[:version_id] = measure_parsed[:version_id]
|
351
|
-
data[measure_index][:measure_data] = measure_data
|
352
|
-
data[measure_index][:vars] = []
|
353
|
-
vars = measure_map[measure]
|
354
|
-
|
355
|
-
# construct the list of variables
|
356
|
-
vars.each do |var|
|
357
|
-
# var looks like [:cooling_adjustment, 0]
|
358
|
-
var = var[0]
|
359
|
-
next if var.to_s == 'None'
|
360
|
-
var_hash = {}
|
361
|
-
found_arg = nil
|
362
|
-
measure_parsed[:arguments].each do |arg|
|
363
|
-
if var.to_s == '__SKIP__' || arg[:name] == var.to_s
|
364
|
-
found_arg = arg
|
365
|
-
break
|
366
|
-
end
|
367
|
-
end
|
368
|
-
|
369
|
-
# var_json = measure_json['arguments'].select { |hash| hash['local_variable'] == var.to_s }[0]
|
370
|
-
raise "measure.xml for measure #{measure} does not have an argument with argument == #{var}" unless found_arg
|
371
|
-
var_type = nil
|
372
|
-
var_units = ''
|
373
|
-
if var.to_s == '__SKIP__'
|
374
|
-
var_type = 'boolean'
|
375
|
-
var_units = ''
|
376
|
-
else
|
377
|
-
var_type = found_arg[:variable_type].downcase
|
378
|
-
var_units = found_arg[:units]
|
379
|
-
end
|
380
|
-
|
381
|
-
var_hash[:name] = var.to_s
|
382
|
-
var_hash[:variable_type] = 'variable'
|
383
|
-
var_hash[:display_name] = measure_rows[2][measure_map[measure][var]]
|
384
|
-
var_hash[:display_name_short] = var_hash[:display_name]
|
385
|
-
# var_hash[:name] = var_json['local_variable']
|
386
|
-
var_hash[:type] = var_type
|
387
|
-
var_hash[:units] = var_units
|
388
|
-
var_hash[:distribution] = {}
|
389
|
-
case var_hash[:type].downcase
|
390
|
-
when 'bool', 'boolean'
|
391
|
-
var_hash[:distribution][:values] = (3..(measure_rows.length - 1)).map { |value| measure_rows[value.to_i][measure_map[measure][var]].to_s.casecmp('true').zero? }
|
392
|
-
var_hash[:distribution][:maximum] = true
|
393
|
-
var_hash[:distribution][:minimum] = false
|
394
|
-
var_hash[:distribution][:mode] = var_hash[:distribution][:values].group_by { |i| i }.max { |x, y| x[1].length <=> y[1].length }[0]
|
395
|
-
when 'choice', 'string'
|
396
|
-
var_hash[:distribution][:values] = (3..measure_rows.length - 1).map { |value| measure_rows[value.to_i][measure_map[measure][var]].to_s }
|
397
|
-
var_hash[:distribution][:minimum] = var_hash[:distribution][:values].min
|
398
|
-
var_hash[:distribution][:maximum] = var_hash[:distribution][:values].max
|
399
|
-
var_hash[:distribution][:mode] = var_hash[:distribution][:values].group_by { |i| i }.max { |x, y| x[1].length <=> y[1].length }[0]
|
400
|
-
else
|
401
|
-
var_hash[:distribution][:values] = (3..(measure_rows.length - 1)).map { |value| eval(measure_rows[value.to_i][measure_map[measure][var]]) }
|
402
|
-
var_hash[:distribution][:minimum] = var_hash[:distribution][:values].map(&:to_i).min
|
403
|
-
var_hash[:distribution][:maximum] = var_hash[:distribution][:values].map(&:to_i).max
|
404
|
-
var_hash[:distribution][:mode] = var_hash[:distribution][:values].group_by { |i| i }.max { |x, y| x[1].length <=> y[1].length }[0]
|
405
|
-
end
|
406
|
-
var_hash[:distribution][:weights] = eval('[' + "#{1.0 / (measure_rows.length - 3)}," * (measure_rows.length - 3) + ']')
|
407
|
-
var_hash[:distribution][:type] = 'discrete'
|
408
|
-
var_hash[:distribution][:units] = var_hash[:units]
|
409
|
-
if var_hash[:type] == 'choice'
|
410
|
-
# var_hash[:distribution][:enumerations] = found_arg.xpath('choices/choice').map { |s| s.xpath('value').text }
|
411
|
-
# This would need to be updated if we want to do this again... sorry.
|
412
|
-
elsif var_hash[:type] == 'bool'
|
413
|
-
var_hash[:distribution][:enumerations] = []
|
414
|
-
var_hash[:distribution][:enumerations] << true
|
415
|
-
var_hash[:distribution][:enumerations] << false
|
416
|
-
end
|
417
|
-
data[measure_index][:vars] << var_hash
|
418
|
-
end
|
419
|
-
data[measure_index][:args] = []
|
420
|
-
|
421
|
-
measure_parsed[:arguments].each do |arg_xml|
|
422
|
-
arg = {}
|
423
|
-
arg[:value_type] = arg_xml[:variable_type]
|
424
|
-
arg[:name] = arg_xml[:name]
|
425
|
-
arg[:display_name] = arg_xml[:display_name].downcase
|
426
|
-
arg[:display_name_short] = arg[:display_name]
|
427
|
-
arg[:default_value] = arg_xml[:default_value].downcase
|
428
|
-
arg[:value] = arg[:default_value]
|
429
|
-
data[measure_index][:args] << arg
|
430
|
-
end
|
431
|
-
end
|
432
|
-
|
433
|
-
data
|
434
|
-
end
|
435
|
-
|
436
|
-
private
|
437
|
-
|
438
|
-
# Find the measure in the measure path
|
439
|
-
def find_measure(measure_name)
|
440
|
-
@measure_paths.each do |mp|
|
441
|
-
measure_xml = File.join(mp, measure_name, 'measure.xml')
|
442
|
-
measure_rb = File.join(mp, measure_name, 'measure.rb')
|
443
|
-
if File.exist?(measure_xml) && File.exist?(measure_rb)
|
444
|
-
measure_parsed = parse_measure_xml(measure_xml)
|
445
|
-
return measure_parsed
|
446
|
-
end
|
447
|
-
end
|
448
|
-
|
449
|
-
return nil
|
450
|
-
end
|
451
|
-
end
|
452
|
-
end
|
453
|
-
end
|
454
|
-
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 Datapoints
|
10
|
+
attr_reader :version
|
11
|
+
attr_reader :settings
|
12
|
+
attr_reader :variables
|
13
|
+
attr_reader :outputs
|
14
|
+
attr_reader :models
|
15
|
+
attr_reader :measure_paths
|
16
|
+
attr_reader :weather_paths
|
17
|
+
attr_reader :worker_inits
|
18
|
+
attr_reader :worker_finals
|
19
|
+
attr_reader :export_path
|
20
|
+
attr_reader :cluster_name
|
21
|
+
attr_reader :variables
|
22
|
+
attr_reader :algorithm
|
23
|
+
attr_reader :problem
|
24
|
+
attr_reader :run_setup
|
25
|
+
attr_reader :aws_tags
|
26
|
+
|
27
|
+
# remove these once we have classes to construct the JSON file
|
28
|
+
attr_accessor :name
|
29
|
+
attr_reader :analysis_name
|
30
|
+
|
31
|
+
require 'rexml/document'
|
32
|
+
|
33
|
+
# Pass in the filename to read
|
34
|
+
def initialize(csv_filename)
|
35
|
+
@csv_filename = csv_filename
|
36
|
+
@root_path = File.expand_path(File.dirname(@csv_filename))
|
37
|
+
|
38
|
+
@csv = nil
|
39
|
+
# Try to read the spreadsheet as a roo object
|
40
|
+
if File.exist?(@csv_filename)
|
41
|
+
@csv = CSV.read(@csv_filename)
|
42
|
+
else
|
43
|
+
raise "File #{@csv_filename} does not exist"
|
44
|
+
end
|
45
|
+
|
46
|
+
# Remove nil rows and check row length
|
47
|
+
@csv.delete_if { |row| row.uniq.length == 1 && row.uniq[0].nil? }
|
48
|
+
|
49
|
+
# Initialize some other instance variables
|
50
|
+
@version = '0.0.1'
|
51
|
+
@analyses = [] # Array o OpenStudio::Analysis. Use method to access
|
52
|
+
@name = nil
|
53
|
+
@analysis_name = nil
|
54
|
+
@cluster_name = nil
|
55
|
+
@settings = {}
|
56
|
+
@weather_paths = []
|
57
|
+
@models = []
|
58
|
+
@other_files = []
|
59
|
+
@worker_inits = []
|
60
|
+
@worker_finals = []
|
61
|
+
@export_path = './export'
|
62
|
+
@measure_paths = []
|
63
|
+
@problem = {}
|
64
|
+
@algorithm = {}
|
65
|
+
@outputs = {}
|
66
|
+
@run_setup = {}
|
67
|
+
@aws_tags = []
|
68
|
+
end
|
69
|
+
|
70
|
+
def process
|
71
|
+
# Seperate CSV into meta and measure groups
|
72
|
+
measure_tag_index = nil
|
73
|
+
@csv.each_with_index { |row, index| measure_tag_index = index if row[0] == 'BEGIN-MEASURES' }
|
74
|
+
raise "ERROR: No 'BEGIN-MEASURES' tag found in input csv file." unless measure_tag_index
|
75
|
+
meta_rows = []
|
76
|
+
measure_rows = []
|
77
|
+
@csv.each_with_index do |_, index|
|
78
|
+
meta_rows << @csv[index] if index < measure_tag_index
|
79
|
+
measure_rows << @csv[index] if index > measure_tag_index
|
80
|
+
end
|
81
|
+
|
82
|
+
@setup = parse_csv_meta(meta_rows)
|
83
|
+
|
84
|
+
@version = Semantic::Version.new @version
|
85
|
+
raise "CSV interface version #{@version} is no longer supported. Please upgrade your csv interface to at least 0.0.1" if @version < '0.0.0'
|
86
|
+
|
87
|
+
@variables = parse_csv_measures(measure_rows)
|
88
|
+
|
89
|
+
# call validate to make sure everything that is needed exists (i.e. directories)
|
90
|
+
validate_analysis
|
91
|
+
end
|
92
|
+
|
93
|
+
# Helper methods to remove models and add new ones programatically. Note that these should
|
94
|
+
# be moved into a general analysis class
|
95
|
+
def delete_models
|
96
|
+
@models = []
|
97
|
+
end
|
98
|
+
|
99
|
+
def add_model(name, display_name, type, path)
|
100
|
+
@models << {
|
101
|
+
name: name,
|
102
|
+
display_name: display_name,
|
103
|
+
type: type,
|
104
|
+
path: path
|
105
|
+
}
|
106
|
+
end
|
107
|
+
|
108
|
+
def validate_analysis
|
109
|
+
# Setup the paths and do some error checking
|
110
|
+
@measure_paths.each do |mp|
|
111
|
+
raise "Measures directory '#{mp}' does not exist" unless Dir.exist?(mp)
|
112
|
+
end
|
113
|
+
|
114
|
+
@models.uniq!
|
115
|
+
raise 'No seed models defined in spreadsheet' if @models.empty?
|
116
|
+
|
117
|
+
@models.each do |model|
|
118
|
+
raise "Seed model does not exist: #{model[:path]}" unless File.exist?(model[:path])
|
119
|
+
end
|
120
|
+
|
121
|
+
@weather_paths.uniq!
|
122
|
+
raise 'No weather files found based on what is in the spreadsheet' if @weather_paths.empty?
|
123
|
+
|
124
|
+
@weather_paths.each do |wf|
|
125
|
+
raise "Weather file does not exist: #{wf}" unless File.exist?(wf)
|
126
|
+
end
|
127
|
+
|
128
|
+
# This can be a directory as well
|
129
|
+
@other_files.each do |f|
|
130
|
+
raise "Other files do not exist for: #{f[:path]}" unless File.exist?(f[:path])
|
131
|
+
end
|
132
|
+
|
133
|
+
@worker_inits.each do |f|
|
134
|
+
raise "Worker initialization file does not exist for: #{f[:path]}" unless File.exist?(f[:path])
|
135
|
+
end
|
136
|
+
|
137
|
+
@worker_finals.each do |f|
|
138
|
+
raise "Worker finalization file does not exist for: #{f[:path]}" unless File.exist?(f[:path])
|
139
|
+
end
|
140
|
+
|
141
|
+
FileUtils.mkdir_p(@export_path)
|
142
|
+
|
143
|
+
# verify that the measure display names are unique
|
144
|
+
# puts @variables.inspect
|
145
|
+
measure_display_names = @variables.map { |m| m[:measure_data][:display_name] }.compact
|
146
|
+
measure_display_names_mult = measure_display_names.select { |m| measure_display_names.count(m) > 1 }.uniq
|
147
|
+
if measure_display_names_mult && !measure_display_names_mult.empty?
|
148
|
+
raise "Measure Display Names are not unique for '#{measure_display_names_mult.join('\', \'')}'"
|
149
|
+
end
|
150
|
+
|
151
|
+
variable_names = @variables.map { |v| v[:vars].map { |hash| hash[:display_name] } }.flatten
|
152
|
+
dupes = variable_names.select { |e| variable_names.count(e) > 1 }.uniq
|
153
|
+
if dupes.count > 0
|
154
|
+
raise "duplicate variable names found in list #{dupes.inspect}"
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# convert the data in excel's parsed data into an OpenStudio Analysis Object
|
159
|
+
# @seed_model [Hash] Seed model to set the new analysis to
|
160
|
+
# @append_model_name [Boolean] Append the name of the seed model to the display name
|
161
|
+
# @return [Object] An OpenStudio::Analysis
|
162
|
+
def analysis(seed_model = nil, append_model_name = false)
|
163
|
+
raise 'There are no seed models defined in the excel file. Please add one.' if @models.empty?
|
164
|
+
raise 'There are more than one seed models defined in the excel file. This is not supported by the CSV Translator.' if @models.size > 1 && seed_model.nil?
|
165
|
+
|
166
|
+
seed_model = @models.first if seed_model.nil?
|
167
|
+
|
168
|
+
# Use the programmatic interface to make the analysis
|
169
|
+
# append the model name to the analysis name if requested (normally if there are more than 1 models in the spreadsheet)
|
170
|
+
display_name = append_model_name ? @name + ' ' + seed_model[:display_name] : @name
|
171
|
+
|
172
|
+
a = OpenStudio::Analysis.create(display_name)
|
173
|
+
|
174
|
+
@variables.each do |measure|
|
175
|
+
@measure_paths.each do |measure_path|
|
176
|
+
measure_dir_to_add = "#{measure_path}/#{measure[:measure_data][:classname]}"
|
177
|
+
if Dir.exist? measure_dir_to_add
|
178
|
+
if File.exist? "#{measure_dir_to_add}/measure.rb"
|
179
|
+
measure[:measure_data][:local_path_to_measure] = "#{measure_dir_to_add}/measure.rb"
|
180
|
+
break
|
181
|
+
else
|
182
|
+
raise "Measure in directory '#{measure_dir_to_add}' did not contain a measure.rb file"
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
raise "Could not find measure '#{measure['name']}' in directory named '#{measure['measure_file_name_directory']}' in the measure paths '#{@measure_paths.join(', ')}'" unless measure[:measure_data][:local_path_to_measure]
|
188
|
+
|
189
|
+
a.workflow.add_measure_from_csv(measure)
|
190
|
+
end
|
191
|
+
|
192
|
+
@other_files.each do |library|
|
193
|
+
a.libraries.add(library[:path], library_name: library[:lib_zip_name])
|
194
|
+
end
|
195
|
+
|
196
|
+
@worker_inits.each do |w|
|
197
|
+
a.worker_inits.add(w[:path], args: w[:args])
|
198
|
+
end
|
199
|
+
|
200
|
+
@worker_finals.each do |w|
|
201
|
+
a.worker_finalizes.add(w[:path], args: w[:args])
|
202
|
+
end
|
203
|
+
|
204
|
+
# Add in the outputs
|
205
|
+
@outputs.each do |o|
|
206
|
+
o = Hash[o.map { |k, v| [k.to_sym, v] }]
|
207
|
+
a.add_output(o)
|
208
|
+
end
|
209
|
+
|
210
|
+
a.analysis_type = @problem['analysis_type']
|
211
|
+
|
212
|
+
# clear out the seed files before adding new ones
|
213
|
+
a.seed_model = seed_model[:path]
|
214
|
+
|
215
|
+
# clear out the weather files before adding new ones
|
216
|
+
a.weather_files.clear
|
217
|
+
@weather_paths.each do |wp|
|
218
|
+
a.weather_files.add_files(wp)
|
219
|
+
end
|
220
|
+
|
221
|
+
a
|
222
|
+
end
|
223
|
+
|
224
|
+
protected
|
225
|
+
|
226
|
+
def parse_csv_meta(meta_rows)
|
227
|
+
# Convert to hash
|
228
|
+
config_hash = {}
|
229
|
+
meta_rows.each do |row|
|
230
|
+
config_hash[row[0].to_sym] = row[1]
|
231
|
+
end
|
232
|
+
|
233
|
+
# Assign required attributes
|
234
|
+
raise 'Require setting not found: version' unless config_hash[:version]
|
235
|
+
@version = config_hash[:version]
|
236
|
+
|
237
|
+
if config_hash[:analysis_name]
|
238
|
+
@name = config_hash[:analysis_name]
|
239
|
+
else
|
240
|
+
@name = SecureRandom.uuid
|
241
|
+
end
|
242
|
+
@analysis_name = @name.to_underscore
|
243
|
+
|
244
|
+
raise 'Require setting not found: measure_path' unless config_hash[:measure_paths]
|
245
|
+
config_hash[:measure_paths] = [config_hash[:measure_paths]] unless config_hash[:measure_paths].respond_to?(:each)
|
246
|
+
config_hash[:measure_paths].each do |path|
|
247
|
+
if (Pathname.new path).absolute?
|
248
|
+
@measure_paths << path
|
249
|
+
else
|
250
|
+
@measure_paths << File.expand_path(File.join(@root_path, path))
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
raise 'Required setting not found: weather_paths' unless config_hash[:weather_paths]
|
255
|
+
config_hash[:weather_paths] = config_hash[:weather_paths].split(',')
|
256
|
+
config_hash[:weather_paths].each do |path|
|
257
|
+
if (Pathname.new path).absolute?
|
258
|
+
@weather_paths << path
|
259
|
+
else
|
260
|
+
@weather_paths << File.expand_path(File.join(@root_path, path))
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
raise 'Required setting not found: models' unless config_hash[:models]
|
265
|
+
config_hash[:models] = [config_hash[:models]] unless config_hash[:models].respond_to?(:each)
|
266
|
+
config_hash[:models].each do |path|
|
267
|
+
model_name = File.basename(path).split('.')[0]
|
268
|
+
model_name = SecureRandom.uuid if model_name == ''
|
269
|
+
type = File.basename(path).split('.')[1].upcase
|
270
|
+
unless (Pathname.new path).absolute?
|
271
|
+
path = File.expand_path(File.join(@root_path, path))
|
272
|
+
end
|
273
|
+
@models << { name: model_name.to_underscore, display_name: model_name, type: type, path: path }
|
274
|
+
end
|
275
|
+
|
276
|
+
# Assign optional attributes
|
277
|
+
if config_hash[:output_json]
|
278
|
+
path = File.expand_path(File.join(@root_path, config_hash[:output_json].to_s))
|
279
|
+
if File.exist? path
|
280
|
+
@outputs = MultiJson.load(File.read(path))
|
281
|
+
else
|
282
|
+
raise "Could not find output json: #{config_hash[:output_json]}"
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
if config_hash[:export_path]
|
287
|
+
if (Pathname.new config_hash[:export_path]).absolute?
|
288
|
+
@export_path = config_hash[:export_path]
|
289
|
+
else
|
290
|
+
@export_path = File.expand_path(File.join(@root_path, config_hash[:export_path]))
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
if config_hash[:library_path]
|
295
|
+
library_name = File.basename(config_hash[:library_path]).split('.')[0]
|
296
|
+
unless (Pathname.new config_hash[:library_path]).absolute?
|
297
|
+
config_hash[:library_path] = File.expand_path(File.join(@root_path, config_hash[:library_path]))
|
298
|
+
end
|
299
|
+
@other_files << { lib_zip_name: library_name, path: config_hash[:library_path] }
|
300
|
+
end
|
301
|
+
|
302
|
+
if config_hash[:allow_multiple_jobs]
|
303
|
+
raise 'allow_multiple_jobs is no longer a valid option in the CSV, please delete and rerun'
|
304
|
+
end
|
305
|
+
if config_hash[:use_server_as_worker]
|
306
|
+
raise 'use_server_as_worker is no longer a valid option in the CSV, please delete and rerun'
|
307
|
+
end
|
308
|
+
|
309
|
+
# Assign AWS settings
|
310
|
+
@settings[:proxy_port] = config_hash[:proxy_port] if config_hash[:proxy_port]
|
311
|
+
@settings[:cluster_name] = config_hash[:cluster_name] if config_hash[:cluster_name]
|
312
|
+
@settings[:user_id] = config_hash[:user_id] if config_hash[:user_id]
|
313
|
+
@settings[:os_server_version] = config_hash[:os_server_version] if config_hash[:os_server_version]
|
314
|
+
@settings[:server_instance_type] = config_hash[:server_instance_type] if config_hash[:server_instance_type]
|
315
|
+
@settings[:worker_instance_type] = config_hash[:worker_instance_type] if config_hash[:worker_instance_type]
|
316
|
+
@settings[:worker_node_number] = config_hash[:worker_node_number].to_i if config_hash[:worker_node_number]
|
317
|
+
@settings[:aws_tags] = config_hash[:aws_tags] if config_hash[:aws_tags]
|
318
|
+
@settings[:analysis_type] = 'batch_datapoints'
|
319
|
+
end
|
320
|
+
|
321
|
+
def parse_csv_measures(measure_rows)
|
322
|
+
# Build metadata required for parsing
|
323
|
+
measures = measure_rows[0].uniq.reject(&:nil?).map(&:to_sym)
|
324
|
+
measure_map = {}
|
325
|
+
measure_var_list = []
|
326
|
+
measures.each do |measure|
|
327
|
+
measure_map[measure] = {}
|
328
|
+
col_ind = (0..(measure_rows[0].length - 1)).to_a.select { |i| measure_rows[0][i] == measure.to_s }
|
329
|
+
col_ind.each do |var_ind|
|
330
|
+
tuple = measure.to_s + measure_rows[1][var_ind]
|
331
|
+
raise "Multiple measure_variable tuples found for '#{measure}_#{measure_rows[1][var_ind]}'. These tuples must be unique." if measure_var_list.include? tuple
|
332
|
+
measure_var_list << tuple
|
333
|
+
measure_map[measure][measure_rows[1][var_ind].to_sym] = var_ind
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
# For each measure load measure json and parse out critical variable requirements
|
338
|
+
data = []
|
339
|
+
measures.each_with_index do |measure, measure_index|
|
340
|
+
data[measure_index] = {}
|
341
|
+
measure_parsed = find_measure(measure.to_s)
|
342
|
+
|
343
|
+
raise "Could not find measure #{measure} xml in measure_paths: '#{@measure_paths.join("\n")}'" unless measure_parsed
|
344
|
+
measure_data = {}
|
345
|
+
measure_data[:classname] = measure_parsed[:classname]
|
346
|
+
measure_data[:name] = measure_parsed[:name]
|
347
|
+
measure_data[:display_name] = measure_parsed[:display_name]
|
348
|
+
measure_data[:measure_type] = measure_parsed[:measure_type]
|
349
|
+
measure_data[:uid] = measure_parsed[:uid]
|
350
|
+
measure_data[:version_id] = measure_parsed[:version_id]
|
351
|
+
data[measure_index][:measure_data] = measure_data
|
352
|
+
data[measure_index][:vars] = []
|
353
|
+
vars = measure_map[measure]
|
354
|
+
|
355
|
+
# construct the list of variables
|
356
|
+
vars.each do |var|
|
357
|
+
# var looks like [:cooling_adjustment, 0]
|
358
|
+
var = var[0]
|
359
|
+
next if var.to_s == 'None'
|
360
|
+
var_hash = {}
|
361
|
+
found_arg = nil
|
362
|
+
measure_parsed[:arguments].each do |arg|
|
363
|
+
if var.to_s == '__SKIP__' || arg[:name] == var.to_s
|
364
|
+
found_arg = arg
|
365
|
+
break
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
# var_json = measure_json['arguments'].select { |hash| hash['local_variable'] == var.to_s }[0]
|
370
|
+
raise "measure.xml for measure #{measure} does not have an argument with argument == #{var}" unless found_arg
|
371
|
+
var_type = nil
|
372
|
+
var_units = ''
|
373
|
+
if var.to_s == '__SKIP__'
|
374
|
+
var_type = 'boolean'
|
375
|
+
var_units = ''
|
376
|
+
else
|
377
|
+
var_type = found_arg[:variable_type].downcase
|
378
|
+
var_units = found_arg[:units]
|
379
|
+
end
|
380
|
+
|
381
|
+
var_hash[:name] = var.to_s
|
382
|
+
var_hash[:variable_type] = 'variable'
|
383
|
+
var_hash[:display_name] = measure_rows[2][measure_map[measure][var]]
|
384
|
+
var_hash[:display_name_short] = var_hash[:display_name]
|
385
|
+
# var_hash[:name] = var_json['local_variable']
|
386
|
+
var_hash[:type] = var_type
|
387
|
+
var_hash[:units] = var_units
|
388
|
+
var_hash[:distribution] = {}
|
389
|
+
case var_hash[:type].downcase
|
390
|
+
when 'bool', 'boolean'
|
391
|
+
var_hash[:distribution][:values] = (3..(measure_rows.length - 1)).map { |value| measure_rows[value.to_i][measure_map[measure][var]].to_s.casecmp('true').zero? }
|
392
|
+
var_hash[:distribution][:maximum] = true
|
393
|
+
var_hash[:distribution][:minimum] = false
|
394
|
+
var_hash[:distribution][:mode] = var_hash[:distribution][:values].group_by { |i| i }.max { |x, y| x[1].length <=> y[1].length }[0]
|
395
|
+
when 'choice', 'string'
|
396
|
+
var_hash[:distribution][:values] = (3..measure_rows.length - 1).map { |value| measure_rows[value.to_i][measure_map[measure][var]].to_s }
|
397
|
+
var_hash[:distribution][:minimum] = var_hash[:distribution][:values].min
|
398
|
+
var_hash[:distribution][:maximum] = var_hash[:distribution][:values].max
|
399
|
+
var_hash[:distribution][:mode] = var_hash[:distribution][:values].group_by { |i| i }.max { |x, y| x[1].length <=> y[1].length }[0]
|
400
|
+
else
|
401
|
+
var_hash[:distribution][:values] = (3..(measure_rows.length - 1)).map { |value| eval(measure_rows[value.to_i][measure_map[measure][var]]) }
|
402
|
+
var_hash[:distribution][:minimum] = var_hash[:distribution][:values].map(&:to_i).min
|
403
|
+
var_hash[:distribution][:maximum] = var_hash[:distribution][:values].map(&:to_i).max
|
404
|
+
var_hash[:distribution][:mode] = var_hash[:distribution][:values].group_by { |i| i }.max { |x, y| x[1].length <=> y[1].length }[0]
|
405
|
+
end
|
406
|
+
var_hash[:distribution][:weights] = eval('[' + "#{1.0 / (measure_rows.length - 3)}," * (measure_rows.length - 3) + ']')
|
407
|
+
var_hash[:distribution][:type] = 'discrete'
|
408
|
+
var_hash[:distribution][:units] = var_hash[:units]
|
409
|
+
if var_hash[:type] == 'choice'
|
410
|
+
# var_hash[:distribution][:enumerations] = found_arg.xpath('choices/choice').map { |s| s.xpath('value').text }
|
411
|
+
# This would need to be updated if we want to do this again... sorry.
|
412
|
+
elsif var_hash[:type] == 'bool'
|
413
|
+
var_hash[:distribution][:enumerations] = []
|
414
|
+
var_hash[:distribution][:enumerations] << true
|
415
|
+
var_hash[:distribution][:enumerations] << false
|
416
|
+
end
|
417
|
+
data[measure_index][:vars] << var_hash
|
418
|
+
end
|
419
|
+
data[measure_index][:args] = []
|
420
|
+
|
421
|
+
measure_parsed[:arguments].each do |arg_xml|
|
422
|
+
arg = {}
|
423
|
+
arg[:value_type] = arg_xml[:variable_type]
|
424
|
+
arg[:name] = arg_xml[:name]
|
425
|
+
arg[:display_name] = arg_xml[:display_name].downcase
|
426
|
+
arg[:display_name_short] = arg[:display_name]
|
427
|
+
arg[:default_value] = arg_xml[:default_value].downcase
|
428
|
+
arg[:value] = arg[:default_value]
|
429
|
+
data[measure_index][:args] << arg
|
430
|
+
end
|
431
|
+
end
|
432
|
+
|
433
|
+
data
|
434
|
+
end
|
435
|
+
|
436
|
+
private
|
437
|
+
|
438
|
+
# Find the measure in the measure path
|
439
|
+
def find_measure(measure_name)
|
440
|
+
@measure_paths.each do |mp|
|
441
|
+
measure_xml = File.join(mp, measure_name, 'measure.xml')
|
442
|
+
measure_rb = File.join(mp, measure_name, 'measure.rb')
|
443
|
+
if File.exist?(measure_xml) && File.exist?(measure_rb)
|
444
|
+
measure_parsed = parse_measure_xml(measure_xml)
|
445
|
+
return measure_parsed
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
return nil
|
450
|
+
end
|
451
|
+
end
|
452
|
+
end
|
453
|
+
end
|
454
|
+
end
|