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.
@@ -1,523 +1,523 @@
1
- # *******************************************************************************
2
- # OpenStudio(R), Copyright (c) Alliance for Sustainable Energy, LLC.
3
- # See also https://openstudio.net/license
4
- # *******************************************************************************
5
-
6
- # OpenStudio::Analysis::WorkflowStep is a class container for storing a measure. The generic name of step may be used later
7
- # to include a workflow step on running EnergyPlus, radiance, etc.
8
- module OpenStudio
9
- module Analysis
10
- class WorkflowStep
11
- attr_accessor :type
12
- attr_accessor :name
13
- attr_accessor :display_name
14
-
15
- attr_accessor :measure_definition_class_name
16
- attr_accessor :measure_definition_directory
17
- attr_accessor :measure_definition_directory_local
18
- attr_accessor :measure_definition_display_name
19
- attr_accessor :measure_definition_name
20
- attr_accessor :measure_definition_name_xml
21
- attr_accessor :measure_definition_uuid
22
- attr_accessor :measure_definition_version_uuid
23
- attr_accessor :uuid
24
- attr_accessor :version_uuid
25
- attr_accessor :description
26
- attr_accessor :taxonomy
27
-
28
- attr_reader :arguments
29
- attr_reader :variables
30
-
31
- # Create an instance of the OpenStudio::Analysis::WorkflowStep
32
- #
33
- # @return [Object] An OpenStudio::Analysis::WorkflowStep object
34
- def initialize
35
- @name = ''
36
- @display_name = ''
37
-
38
- # The type of item being added (ModelMeasure, EnergyPlusMeasure, ...)
39
- @type = nil
40
-
41
- @measure_definition_class_name = nil
42
- @measure_definition_directory = nil
43
- @measure_definition_directory_local = nil
44
- @measure_definition_display_name = nil
45
- @measure_definition_name = nil
46
- @measure_definition_name_xml = nil
47
- @measure_definition_uuid = nil
48
- @measure_definition_version_uuid = nil
49
- @uuid = nil
50
- @version_uuid = nil
51
- @description = nil
52
- #@taxonomy = nil #BLB dont do this now
53
- @arguments = []
54
-
55
- @arguments << {
56
- display_name: 'Skip Entire Measure',
57
- display_name_short: 'Skip',
58
- name: '__SKIP__',
59
- value_type: 'boolean',
60
- default_value: false,
61
- value: false
62
- }
63
-
64
- # TODO: eventually the variables should be its own class. This would then be an array of Variable objects.
65
- @variables = []
66
- end
67
-
68
- # Return an array of the argument names
69
- #
70
- # @return [Array] Listing of argument names.
71
- def argument_names
72
- @arguments.map { |a| a[:name] }
73
- end
74
-
75
- # Set the value of an argument to `value`. The user is required to know the data type and pass it in accordingly
76
- #
77
- # @param argument_name [String] The machine name of the argument that you want to set the value to
78
- # @param value [] The value to assign the argument
79
- # @return [Boolean] True/false if it assigned it
80
- def argument_value(argument_name, value)
81
- a = @arguments.find_all { |a| a[:name] == argument_name }
82
- raise "could not find argument_name of #{argument_name} in measure #{name}. Valid argument names are #{argument_names}." if a.empty?
83
- raise "more than one argument with the same name of #{argument_name} in measure #{name}" if a.size > 1
84
-
85
- a = a.first
86
-
87
- a[:value] = value
88
-
89
- a[:value] == value
90
- end
91
-
92
- # Return a variable by its name.
93
- #
94
- # @param name [String] Name of the arugment that makes the variable.
95
- # @return [Object] The variable object
96
- def find_variable_by_name(name)
97
- v = @variables.find { |v| v[:argument][:name] == name }
98
-
99
- v
100
- end
101
-
102
- def remove_variable(variable_name)
103
- v_index = @variables.find_index { |v| v[:argument][:name] == variable_name }
104
- if v_index
105
- @variables.delete_at(v_index)
106
- return true
107
- else
108
- return false
109
- end
110
- end
111
-
112
- # Tag a measure's argument as a variable.
113
- #
114
- # @param argument_name [String] The instance_name of the measure argument that is to be tagged. This is the same name as the argument's variable in the measure.rb file.
115
- # @param variable_display_name [String] What the variable is called. It is best if the display name is self describing (i.e. does not need any other context). It can be the same as the argument display name.
116
- # @param distribution [Hash] Hash describing the distribution of the variable.
117
- # @option distribution [String] :type Type of distribution. `discrete`, `uniform`, `triangle`, `normal`, `lognormal`, `integer_sequence`
118
- # @option distribution [String] :units Units of the variable. This is legacy as previous OpenStudio measures did not specify units separately.
119
- # @option distribution [String] :minimum Minimum value of the distribution, required for all distributions
120
- # @option distribution [String] :maximum Maximum value of the distribution, required for all distributions
121
- # @option distribution [String] :standard_deviation The standard deviation, if the distribution requires it.
122
- # @option distribution [String] :mode The mean/mode of the distribution (if required)
123
- # @option distribution [String] :mean Alias for the mode. If this is used it will override the mode
124
- # @option distribution [String] :relation_to_output How is the variable correlates to the output of interest (for continuous distributions)
125
- # @option distribution [String] :step_size Minimum step size (delta_x) of the variable (for continuous distributions)
126
- # @option distribution [String] :values If discrete, then the values to run
127
- # @option distribution [String] :weights If discrete, then the weights for each of the discrete values, must be the same length as values, and sum to 1. If empty, then it will create this automatically to be uniform.
128
- # @param variable_type [String] What type of variable, variable or pivot. Typically this is variable.
129
- # @param options [Hash] Values that define the variable.
130
- # @option options [String] :variable_type The type of variable, `variable` or `pivot`. By default this is a variable.
131
- # @option options [String] :variable_display_name_short The short display name of the variable. Will be defaulted to the variable_display_name if not passed
132
- # @return [Boolean] True / False if it was able to tag the measure argument
133
- def make_variable(argument_name, variable_display_name, distribution, options = {})
134
- options = { variable_type: 'variable' }.merge(options)
135
- distribution[:mode] = distribution[:mean] if distribution.key? :mean
136
-
137
- raise "Set the static value in the options 'options[:static_value]', not the distribution" if distribution[:static_value]
138
-
139
- a = @arguments.find_all { |a| a[:name] == argument_name }
140
- raise "could not find argument_name of #{argument_name} in measure #{name}. Valid argument names are #{argument_names}." if a.empty?
141
- raise "more than one argument with the same name of #{argument_name} in measure #{name}" if a.size > 1
142
-
143
- if distribution_valid?(distribution)
144
- # grab the argument hash
145
- a = a.first
146
-
147
- # add more information to the argument
148
- v = {}
149
- v[:argument] = a
150
- v[:display_name] = variable_display_name
151
- v[:display_name_short] = options[:variable_display_name_short] ? options[:variable_display_name_short] : variable_display_name
152
- v[:variable_type] = options[:variable_type]
153
-
154
- v[:type] = distribution[:type]
155
- v[:units] = distribution[:units] ? distribution[:units] : nil
156
- v[:minimum] = distribution[:minimum]
157
- v[:maximum] = distribution[:maximum]
158
- v[:relation_to_output] = distribution[:relation_to_output] ? distribution[:relation_to_output] : nil
159
- v[:mode] = distribution[:mode]
160
- v[:static_value] = options[:static_value] if options[:static_value]
161
- # TODO: Static value should be named default value or just value
162
-
163
- # Always look for these attributes even if the distribution does not need them
164
- v[:weights] = distribution[:weights] if distribution[:weights]
165
- v[:values] = distribution[:values] if distribution[:values]
166
- v[:standard_deviation] = distribution[:standard_deviation] if distribution[:standard_deviation]
167
- v[:step_size] = distribution[:step_size] ? distribution[:step_size] : nil
168
-
169
- # assign uuid and version id to the variable
170
- v[:uuid] = SecureRandom.uuid
171
- v[:version_uuid] = SecureRandom.uuid
172
- @variables << v
173
- end
174
-
175
- true
176
- end
177
-
178
- # Convert the class into a hash. TODO: Make this smart based on the :type eventually
179
- #
180
- # @return [Hash] Returns the hash
181
- def to_hash(version = 1, *a)
182
- hash = {}
183
- if version == 1
184
- instance_variables.each do |var|
185
- if var.to_s == '@type'
186
- hash[:measure_type] = instance_variable_get(var)
187
- elsif var.to_s == '@arguments'
188
- hash[:arguments] = []
189
- @arguments.each do |a|
190
- # This will change in version 2 but right now, if the argument is a variable, then the argument will
191
- # be in the variables hash, not the arguments hash.
192
- next unless @variables.find { |v| v[:argument][:name] == a[:name] }.nil?
193
- hash[:arguments] << a
194
- end
195
- elsif var.to_s == '@variables'
196
- # skip until after looping over instance_variables
197
- elsif var.to_s == '@__swigtype__'
198
- # skip the swig variables caused by using the same namespace as OpenStudio
199
- else
200
- hash[var.to_s.delete('@')] = instance_variable_get(var)
201
- end
202
-
203
- # TODO: iterate over the variables and create UUIDs, or not?
204
- end
205
-
206
- # fix everything to support the legacy version
207
- # we need to make a deep copy since multiple calls to .to_hash deletes :type, :mode, etc below
208
- # and we still want those args to be avail for future calls, but not end up in the final OSA hash.
209
- # without this, the v.delete() below (line ~278-281) will remove :type from @variables.
210
- # this would be okay if there was only 1 call to .to_hash. but thats not guaranteed
211
- variables_dup = Marshal.load(Marshal.dump(@variables))
212
- hash[:variables] = variables_dup
213
-
214
- # Clean up the variables to match the legacy format
215
- hash[:variables].each_with_index do |v, index|
216
- v[:variable_type] == 'pivot' ? v[:pivot] = true : v[:variable] = true
217
- v[:static_value] = v[:argument][:default_value] unless v[:static_value]
218
- @variables[index][:static_value] = v[:static_value]
219
-
220
- v[:uncertainty_description] = {}
221
- # In Version 0.5 the _uncertain text will be removed from distribution
222
- if v[:type] =~ /uncertain/
223
- v[:type].delete!('_uncertain')
224
- end
225
- v[:uncertainty_description][:type] = v[:type]
226
-
227
- # This is not neatly coded. This should be a new object that knows how to write itself out.
228
- v[:uncertainty_description][:attributes] = []
229
- if v[:type] =~ /discrete/
230
- new_h = {}
231
- new_h[:name] = 'discrete'
232
-
233
- # check the weights
234
- new_h[:values_and_weights] = v.delete(:values).zip(v.delete(:weights)).map { |w| { value: w[0], weight: w[1] } }
235
- v[:uncertainty_description][:attributes] << new_h
236
- end
237
-
238
- # always write out these attributes
239
- v[:uncertainty_description][:attributes] << { name: 'lower_bounds', value: v[:minimum] }
240
- v[:uncertainty_description][:attributes] << { name: 'upper_bounds', value: v[:maximum] }
241
- v[:uncertainty_description][:attributes] << { name: 'modes', value: v[:mode] }
242
- v[:uncertainty_description][:attributes] << { name: 'delta_x', value: v[:step_size] ? v[:step_size] : nil }
243
- v[:uncertainty_description][:attributes] << { name: 'stddev', value: v[:standard_deviation] ? v[:standard_deviation] : nil }
244
-
245
- v[:workflow_index] = index
246
-
247
- # remove some remaining items
248
- v.delete(:type)
249
- v.delete(:mode) if v.key?(:mode)
250
- v.delete(:step_size) if v.key?(:step_size)
251
- v.delete(:standard_deviation) if v.key?(:standard_deviation)
252
- end
253
-
254
- else
255
- raise "Do not know how to create the Hash for Version #{version}"
256
- end
257
-
258
- hash
259
- end
260
-
261
- # Read the workflow item from a measure hash.
262
- #
263
- # @param instance_name [String] Machine name of the instance
264
- # @param instance_display_name [String] Display name of the instance
265
- # @param path_to_measure [String] This is the local path to the measure directory, relative or absolute. It is used when zipping up all the measures.
266
- # @param hash [Hash] Measure hash in the format of a converted measure.xml hash (from the Analysis Spreadsheet project)
267
- # @param options [Hash] Optional arguments
268
- # @option options [Boolean] :ignore_not_found Do not raise an exception if the measure could not be found on the machine
269
- # @return [Object] Returns the OpenStudio::Analysis::WorkflowStep
270
- def self.from_measure_hash(instance_name, instance_display_name, path_to_measure, hash, options = {})
271
- if File.directory? path_to_measure
272
- path_to_measure = File.join(path_to_measure, 'measure.rb')
273
- end
274
-
275
- # verify that the path to the measure is a path and not a file. If it is make it a path
276
- if File.exist?(path_to_measure) && File.file?(path_to_measure)
277
- path_to_measure = File.dirname(path_to_measure)
278
- else
279
- raise "Could not find measure '#{instance_name}' in '#{path_to_measure}'" unless options[:ignore_not_found]
280
- end
281
-
282
- # Extract the directory
283
- path_to_measure_local = path_to_measure
284
- path_to_measure = "./measures/#{File.basename(path_to_measure)}"
285
-
286
- # map the BCL hash format into the OpenStudio WorkflowStep format
287
- s = OpenStudio::Analysis::WorkflowStep.new
288
-
289
- # add the instance and display name
290
- s.name = instance_name
291
- s.display_name = instance_display_name
292
-
293
- # definition of the measure
294
- s.measure_definition_class_name = hash[:classname]
295
- s.measure_definition_directory = path_to_measure
296
- s.measure_definition_directory_local = path_to_measure_local
297
- s.measure_definition_display_name = hash[:display_name]
298
- s.measure_definition_name = hash[:name]
299
- # name_xml is not used right now but eventually should be used to compare the hash[:name] and the hash[:name_xml]
300
- s.measure_definition_name_xml = hash[:name_xml]
301
- s.measure_definition_uuid = hash[:uid]
302
- s.measure_definition_version_uuid = hash[:version_id]
303
- s.uuid = hash[:uid]
304
- s.version_uuid = hash[:version_id]
305
- s.description = hash[:description]
306
- #s.taxonomy = hash[:taxonomy] #BLB dont do this now
307
-
308
- # do not allow the choice variable_type
309
-
310
- s.type = hash[:measure_type] # this is actually the measure type
311
- hash[:arguments]&.each do |arg|
312
- puts arg
313
- # warn the user to we need to deprecate variable_type and use value_type (which is what os server uses)
314
- var_type = arg[:variable_type] ? arg[:variable_type].downcase : arg[:value_type]
315
-
316
- if var_type == 'choice'
317
- # WARN the user that the measure had a "choice data type"
318
- var_type = 'string'
319
- end
320
-
321
-
322
- if var_type.downcase == 'double'
323
- default_value = arg[:default_value].to_f
324
- elsif var_type.downcase == 'integer'
325
- default_value = arg[:default_value].to_i
326
- elsif var_type.downcase == 'boolean'
327
- # In some cases a nil default is okay. It is seen as "non-existing" and
328
- # needs to be passed through as such.
329
- if arg[:default_value].nil?
330
- default_value = nil
331
- else
332
- default_value = (arg[:default_value].downcase == "true") #convert the string 'true'/'false' to boolean
333
- end
334
- else
335
- default_value = arg[:default_value]
336
- end
337
-
338
- if !arg[:display_name_short].nil?
339
- display_name_short = arg[:display_name_short]
340
- else
341
- display_name_short = arg[:display_name]
342
- end
343
-
344
- s.arguments << {
345
- display_name: arg[:display_name],
346
- display_name_short: display_name_short,
347
- name: arg[:name],
348
- value_type: var_type,
349
- default_value: default_value,
350
- value: default_value
351
- }
352
- end
353
-
354
- # Load the arguments of variables, but do not make them variables. This format is more about arugments, than variables
355
- hash[:variables]&.each do |variable|
356
- # add the arguments first
357
- s.arguments << {
358
- display_name: variable[:argument][:display_name],
359
- display_name_short: variable[:argument][:display_name_short],
360
- name: variable[:argument][:name],
361
- value_type: variable[:argument][:value_type],
362
- default_value: variable[:argument][:default_value],
363
- value: variable[:argument][:default_value]
364
- }
365
- end
366
-
367
- s
368
- end
369
-
370
- # Read the workflow item from a analysis hash. Can we combine measure hash and analysis hash?
371
- #
372
- # @param instance_name [String] Machine name of the instance
373
- # @param instance_display_name [String] Display name of the instance
374
- # @param path_to_measure [String] This is the local path to the measure directroy, relative or absolute. It is used when zipping up all the measures.
375
- # @param hash [Hash] Measure hash in the format of the measure.xml converted to JSON (from the Analysis Spreadsheet project)
376
- # @param options [Hash] Optional arguments
377
- # @option options [Boolean] :ignore_not_found Do not raise an exception if the measure could not be found on the machine
378
- # @return [Object] Returns the OpenStudio::Analysis::WorkflowStep
379
- def self.from_analysis_hash(instance_name, instance_display_name, path_to_measure, hash, options = {})
380
- # TODO: Validate the hash
381
- # TODO: validate that the measure exists?
382
-
383
- if File.directory? path_to_measure
384
- path_to_measure = File.join(path_to_measure, 'measure.rb')
385
- end
386
-
387
- # verify that the path to the measure is a path and not a file. If it is make it a path
388
- if File.exist?(path_to_measure) && File.file?(path_to_measure)
389
- path_to_measure = File.dirname(path_to_measure)
390
- else
391
- raise "Could not find measure '#{instance_name}' in '#{path_to_measure}'" unless options[:ignore_not_found]
392
- end
393
-
394
- # Extract the directo
395
- path_to_measure_local = path_to_measure
396
- path_to_measure = "./measures/#{File.basename(path_to_measure)}"
397
-
398
- # map the BCL hash format into the OpenStudio WorkflowStep format
399
- s = OpenStudio::Analysis::WorkflowStep.new
400
-
401
- # add the instance and display name
402
- s.name = instance_name
403
- s.display_name = instance_display_name
404
-
405
- # definition of the measure
406
- s.measure_definition_class_name = hash[:measure_definition_class_name]
407
- s.measure_definition_directory = path_to_measure
408
- s.measure_definition_directory_local = path_to_measure_local
409
- s.measure_definition_display_name = hash[:measure_definition_display_name]
410
- s.measure_definition_name = hash[:measure_definition_name]
411
- # name_xml is not used right now but eventually should be used to compare the hash[:name] and the hash[:name_xml]
412
- s.measure_definition_name_xml = hash[:measure_definition_name_xml]
413
- s.measure_definition_uuid = hash[:measure_definition_uuid]
414
- s.measure_definition_version_uuid = hash[:measure_definition_version_uuid]
415
- s.uuid = hash[:uuid] if hash[:uuid]
416
- s.version_uuid = hash[:version_uuid] if hash[:version_uuid]
417
- s.description = hash[:description] if hash[:description]
418
- #s.taxonomy = hash[:taxonomy] if hash[:taxonomy] #BLB dont do this, its a Tags array of Tag
419
-
420
- s.type = hash[:measure_type] # this is actually the measure type
421
- hash[:arguments]&.each do |arg|
422
- # warn the user to we need to deprecate variable_type and use value_type (which is what os server uses)
423
- var_type = arg[:value_type]
424
-
425
- if var_type == 'choice'
426
- # WARN the user that the measure had a "choice data type"
427
- var_type = 'string'
428
- end
429
-
430
- if var_type.downcase == 'double'
431
- default_value = arg[:default_value].to_f
432
- value = arg[:value].to_f
433
- elsif var_type.downcase == 'integer'
434
- default_value = arg[:default_value].to_i
435
- value = arg[:value].to_i
436
- elsif var_type.downcase == 'boolean'
437
- default_value = (arg[:default_value].downcase == "true") # convert the string 'true'/'false' to boolean
438
- value = (arg[:value].downcase == "true") # convert the string 'true'/'false' to boolean
439
- else
440
- default_value = arg[:default_value]
441
- value = arg[:value]
442
- end
443
-
444
- if !arg[:display_name_short].nil?
445
- display_name_short = arg[:display_name_short]
446
- else
447
- display_name_short = arg[:display_name]
448
- end
449
-
450
- s.arguments << {
451
- display_name: arg[:display_name],
452
- display_name_short: display_name_short,
453
- name: arg[:name],
454
- value_type: var_type,
455
- default_value: default_value,
456
- value: value
457
- }
458
- end
459
-
460
- hash[:variables]&.each do |variable|
461
- # add the arguments first
462
- s.arguments << {
463
- display_name: variable[:argument][:display_name],
464
- display_name_short: variable[:argument][:display_name_short],
465
- name: variable[:argument][:name],
466
- value_type: variable[:argument][:value_type],
467
- default_value: variable[:argument][:default_value],
468
- value: variable[:argument][:default_value]
469
- }
470
-
471
- var_options = {}
472
- var_options[:variable_type] = variable[:variable_type]
473
- var_options[:variable_display_name_short] = variable[:display_name_short]
474
- var_options[:static_value] = variable[:static_value]
475
- distribution = variable[:uncertainty_description]
476
- distribution[:minimum] = variable[:minimum]
477
- distribution[:mean] = distribution[:attributes].find { |a| a[:name] == 'modes' }[:value]
478
- distribution[:maximum] = variable[:maximum]
479
- distribution[:standard_deviation] = distribution[:attributes].find { |a| a[:name] == 'stddev' }[:value]
480
- distribution[:step_size] = distribution[:attributes].find { |a| a[:name] == 'delta_x' }[:value]
481
- s.make_variable(variable[:argument][:name], variable[:display_name], distribution, var_options)
482
- end
483
-
484
- s
485
- end
486
-
487
- private
488
-
489
- # validate the arguments of the distribution
490
- def distribution_valid?(d)
491
- # regardless of uncertainty description the following must be defined
492
- raise 'No distribution defined for variable' unless d.key? :type
493
- raise 'No minimum defined for variable' unless d.key? :minimum
494
- raise 'No maximum defined for variable' unless d.key? :maximum
495
- raise 'No mean/mode defined for variable' unless d.key? :mode
496
-
497
- if d[:type] =~ /uniform/
498
- # Do we need to tell the user that we don't really need the mean/mode for uniform ?
499
- elsif d[:type] =~ /discrete/
500
- # require min, max, mode
501
- raise 'No values passed for discrete distribution' unless d[:values] || d[:values].empty?
502
- if d[:weights]
503
- raise 'Weights are not the same length as values' unless d[:values].size == d[:weights].size
504
- raise 'Weights do not sum up to one' unless d[:weights].reduce(:+).between?(0.99, 1.01) # allow a small error for now
505
- else
506
- fraction = 1 / d[:values].size.to_f
507
- d[:weights] = [fraction] * d[:values].size
508
- end
509
- elsif d[:type] =~ /integer_sequence/
510
- d[:weights] = 1
511
- d[:values] = 1
512
- elsif d[:type] =~ /triangle/
513
- # requires min, max, mode
514
- elsif d[:type] =~ /normal/ # both normal and lognormal
515
- # require min, max, mode, stddev
516
- raise 'No standard deviation for variable' unless d[:standard_deviation]
517
- end
518
-
519
- true
520
- end
521
- end
522
- end
523
- end
1
+ # *******************************************************************************
2
+ # OpenStudio(R), Copyright (c) Alliance for Sustainable Energy, LLC.
3
+ # See also https://openstudio.net/license
4
+ # *******************************************************************************
5
+
6
+ # OpenStudio::Analysis::WorkflowStep is a class container for storing a measure. The generic name of step may be used later
7
+ # to include a workflow step on running EnergyPlus, radiance, etc.
8
+ module OpenStudio
9
+ module Analysis
10
+ class WorkflowStep
11
+ attr_accessor :type
12
+ attr_accessor :name
13
+ attr_accessor :display_name
14
+
15
+ attr_accessor :measure_definition_class_name
16
+ attr_accessor :measure_definition_directory
17
+ attr_accessor :measure_definition_directory_local
18
+ attr_accessor :measure_definition_display_name
19
+ attr_accessor :measure_definition_name
20
+ attr_accessor :measure_definition_name_xml
21
+ attr_accessor :measure_definition_uuid
22
+ attr_accessor :measure_definition_version_uuid
23
+ attr_accessor :uuid
24
+ attr_accessor :version_uuid
25
+ attr_accessor :description
26
+ attr_accessor :taxonomy
27
+
28
+ attr_reader :arguments
29
+ attr_reader :variables
30
+
31
+ # Create an instance of the OpenStudio::Analysis::WorkflowStep
32
+ #
33
+ # @return [Object] An OpenStudio::Analysis::WorkflowStep object
34
+ def initialize
35
+ @name = ''
36
+ @display_name = ''
37
+
38
+ # The type of item being added (ModelMeasure, EnergyPlusMeasure, ...)
39
+ @type = nil
40
+
41
+ @measure_definition_class_name = nil
42
+ @measure_definition_directory = nil
43
+ @measure_definition_directory_local = nil
44
+ @measure_definition_display_name = nil
45
+ @measure_definition_name = nil
46
+ @measure_definition_name_xml = nil
47
+ @measure_definition_uuid = nil
48
+ @measure_definition_version_uuid = nil
49
+ @uuid = nil
50
+ @version_uuid = nil
51
+ @description = nil
52
+ #@taxonomy = nil #BLB dont do this now
53
+ @arguments = []
54
+
55
+ @arguments << {
56
+ display_name: 'Skip Entire Measure',
57
+ display_name_short: 'Skip',
58
+ name: '__SKIP__',
59
+ value_type: 'boolean',
60
+ default_value: false,
61
+ value: false
62
+ }
63
+
64
+ # TODO: eventually the variables should be its own class. This would then be an array of Variable objects.
65
+ @variables = []
66
+ end
67
+
68
+ # Return an array of the argument names
69
+ #
70
+ # @return [Array] Listing of argument names.
71
+ def argument_names
72
+ @arguments.map { |a| a[:name] }
73
+ end
74
+
75
+ # Set the value of an argument to `value`. The user is required to know the data type and pass it in accordingly
76
+ #
77
+ # @param argument_name [String] The machine name of the argument that you want to set the value to
78
+ # @param value [] The value to assign the argument
79
+ # @return [Boolean] True/false if it assigned it
80
+ def argument_value(argument_name, value)
81
+ a = @arguments.find_all { |a| a[:name] == argument_name }
82
+ raise "could not find argument_name of #{argument_name} in measure #{name}. Valid argument names are #{argument_names}." if a.empty?
83
+ raise "more than one argument with the same name of #{argument_name} in measure #{name}" if a.size > 1
84
+
85
+ a = a.first
86
+
87
+ a[:value] = value
88
+
89
+ a[:value] == value
90
+ end
91
+
92
+ # Return a variable by its name.
93
+ #
94
+ # @param name [String] Name of the arugment that makes the variable.
95
+ # @return [Object] The variable object
96
+ def find_variable_by_name(name)
97
+ v = @variables.find { |v| v[:argument][:name] == name }
98
+
99
+ v
100
+ end
101
+
102
+ def remove_variable(variable_name)
103
+ v_index = @variables.find_index { |v| v[:argument][:name] == variable_name }
104
+ if v_index
105
+ @variables.delete_at(v_index)
106
+ return true
107
+ else
108
+ return false
109
+ end
110
+ end
111
+
112
+ # Tag a measure's argument as a variable.
113
+ #
114
+ # @param argument_name [String] The instance_name of the measure argument that is to be tagged. This is the same name as the argument's variable in the measure.rb file.
115
+ # @param variable_display_name [String] What the variable is called. It is best if the display name is self describing (i.e. does not need any other context). It can be the same as the argument display name.
116
+ # @param distribution [Hash] Hash describing the distribution of the variable.
117
+ # @option distribution [String] :type Type of distribution. `discrete`, `uniform`, `triangle`, `normal`, `lognormal`, `integer_sequence`
118
+ # @option distribution [String] :units Units of the variable. This is legacy as previous OpenStudio measures did not specify units separately.
119
+ # @option distribution [String] :minimum Minimum value of the distribution, required for all distributions
120
+ # @option distribution [String] :maximum Maximum value of the distribution, required for all distributions
121
+ # @option distribution [String] :standard_deviation The standard deviation, if the distribution requires it.
122
+ # @option distribution [String] :mode The mean/mode of the distribution (if required)
123
+ # @option distribution [String] :mean Alias for the mode. If this is used it will override the mode
124
+ # @option distribution [String] :relation_to_output How is the variable correlates to the output of interest (for continuous distributions)
125
+ # @option distribution [String] :step_size Minimum step size (delta_x) of the variable (for continuous distributions)
126
+ # @option distribution [String] :values If discrete, then the values to run
127
+ # @option distribution [String] :weights If discrete, then the weights for each of the discrete values, must be the same length as values, and sum to 1. If empty, then it will create this automatically to be uniform.
128
+ # @param variable_type [String] What type of variable, variable or pivot. Typically this is variable.
129
+ # @param options [Hash] Values that define the variable.
130
+ # @option options [String] :variable_type The type of variable, `variable` or `pivot`. By default this is a variable.
131
+ # @option options [String] :variable_display_name_short The short display name of the variable. Will be defaulted to the variable_display_name if not passed
132
+ # @return [Boolean] True / False if it was able to tag the measure argument
133
+ def make_variable(argument_name, variable_display_name, distribution, options = {})
134
+ options = { variable_type: 'variable' }.merge(options)
135
+ distribution[:mode] = distribution[:mean] if distribution.key? :mean
136
+
137
+ raise "Set the static value in the options 'options[:static_value]', not the distribution" if distribution[:static_value]
138
+
139
+ a = @arguments.find_all { |a| a[:name] == argument_name }
140
+ raise "could not find argument_name of #{argument_name} in measure #{name}. Valid argument names are #{argument_names}." if a.empty?
141
+ raise "more than one argument with the same name of #{argument_name} in measure #{name}" if a.size > 1
142
+
143
+ if distribution_valid?(distribution)
144
+ # grab the argument hash
145
+ a = a.first
146
+
147
+ # add more information to the argument
148
+ v = {}
149
+ v[:argument] = a
150
+ v[:display_name] = variable_display_name
151
+ v[:display_name_short] = options[:variable_display_name_short] ? options[:variable_display_name_short] : variable_display_name
152
+ v[:variable_type] = options[:variable_type]
153
+
154
+ v[:type] = distribution[:type]
155
+ v[:units] = distribution[:units] ? distribution[:units] : nil
156
+ v[:minimum] = distribution[:minimum]
157
+ v[:maximum] = distribution[:maximum]
158
+ v[:relation_to_output] = distribution[:relation_to_output] ? distribution[:relation_to_output] : nil
159
+ v[:mode] = distribution[:mode]
160
+ v[:static_value] = options[:static_value] if options[:static_value]
161
+ # TODO: Static value should be named default value or just value
162
+
163
+ # Always look for these attributes even if the distribution does not need them
164
+ v[:weights] = distribution[:weights] if distribution[:weights]
165
+ v[:values] = distribution[:values] if distribution[:values]
166
+ v[:standard_deviation] = distribution[:standard_deviation] if distribution[:standard_deviation]
167
+ v[:step_size] = distribution[:step_size] ? distribution[:step_size] : nil
168
+
169
+ # assign uuid and version id to the variable
170
+ v[:uuid] = SecureRandom.uuid
171
+ v[:version_uuid] = SecureRandom.uuid
172
+ @variables << v
173
+ end
174
+
175
+ true
176
+ end
177
+
178
+ # Convert the class into a hash. TODO: Make this smart based on the :type eventually
179
+ #
180
+ # @return [Hash] Returns the hash
181
+ def to_hash(version = 1, *a)
182
+ hash = {}
183
+ if version == 1
184
+ instance_variables.each do |var|
185
+ if var.to_s == '@type'
186
+ hash[:measure_type] = instance_variable_get(var)
187
+ elsif var.to_s == '@arguments'
188
+ hash[:arguments] = []
189
+ @arguments.each do |a|
190
+ # This will change in version 2 but right now, if the argument is a variable, then the argument will
191
+ # be in the variables hash, not the arguments hash.
192
+ next unless @variables.find { |v| v[:argument][:name] == a[:name] }.nil?
193
+ hash[:arguments] << a
194
+ end
195
+ elsif var.to_s == '@variables'
196
+ # skip until after looping over instance_variables
197
+ elsif var.to_s == '@__swigtype__'
198
+ # skip the swig variables caused by using the same namespace as OpenStudio
199
+ else
200
+ hash[var.to_s.delete('@')] = instance_variable_get(var)
201
+ end
202
+
203
+ # TODO: iterate over the variables and create UUIDs, or not?
204
+ end
205
+
206
+ # fix everything to support the legacy version
207
+ # we need to make a deep copy since multiple calls to .to_hash deletes :type, :mode, etc below
208
+ # and we still want those args to be avail for future calls, but not end up in the final OSA hash.
209
+ # without this, the v.delete() below (line ~278-281) will remove :type from @variables.
210
+ # this would be okay if there was only 1 call to .to_hash. but thats not guaranteed
211
+ variables_dup = Marshal.load(Marshal.dump(@variables))
212
+ hash[:variables] = variables_dup
213
+
214
+ # Clean up the variables to match the legacy format
215
+ hash[:variables].each_with_index do |v, index|
216
+ v[:variable_type] == 'pivot' ? v[:pivot] = true : v[:variable] = true
217
+ v[:static_value] = v[:argument][:default_value] unless v[:static_value]
218
+ @variables[index][:static_value] = v[:static_value]
219
+
220
+ v[:uncertainty_description] = {}
221
+ # In Version 0.5 the _uncertain text will be removed from distribution
222
+ if v[:type] =~ /uncertain/
223
+ v[:type].delete!('_uncertain')
224
+ end
225
+ v[:uncertainty_description][:type] = v[:type]
226
+
227
+ # This is not neatly coded. This should be a new object that knows how to write itself out.
228
+ v[:uncertainty_description][:attributes] = []
229
+ if v[:type] =~ /discrete/
230
+ new_h = {}
231
+ new_h[:name] = 'discrete'
232
+
233
+ # check the weights
234
+ new_h[:values_and_weights] = v.delete(:values).zip(v.delete(:weights)).map { |w| { value: w[0], weight: w[1] } }
235
+ v[:uncertainty_description][:attributes] << new_h
236
+ end
237
+
238
+ # always write out these attributes
239
+ v[:uncertainty_description][:attributes] << { name: 'lower_bounds', value: v[:minimum] }
240
+ v[:uncertainty_description][:attributes] << { name: 'upper_bounds', value: v[:maximum] }
241
+ v[:uncertainty_description][:attributes] << { name: 'modes', value: v[:mode] }
242
+ v[:uncertainty_description][:attributes] << { name: 'delta_x', value: v[:step_size] ? v[:step_size] : nil }
243
+ v[:uncertainty_description][:attributes] << { name: 'stddev', value: v[:standard_deviation] ? v[:standard_deviation] : nil }
244
+
245
+ v[:workflow_index] = index
246
+
247
+ # remove some remaining items
248
+ v.delete(:type)
249
+ v.delete(:mode) if v.key?(:mode)
250
+ v.delete(:step_size) if v.key?(:step_size)
251
+ v.delete(:standard_deviation) if v.key?(:standard_deviation)
252
+ end
253
+
254
+ else
255
+ raise "Do not know how to create the Hash for Version #{version}"
256
+ end
257
+
258
+ hash
259
+ end
260
+
261
+ # Read the workflow item from a measure hash.
262
+ #
263
+ # @param instance_name [String] Machine name of the instance
264
+ # @param instance_display_name [String] Display name of the instance
265
+ # @param path_to_measure [String] This is the local path to the measure directory, relative or absolute. It is used when zipping up all the measures.
266
+ # @param hash [Hash] Measure hash in the format of a converted measure.xml hash (from the Analysis Spreadsheet project)
267
+ # @param options [Hash] Optional arguments
268
+ # @option options [Boolean] :ignore_not_found Do not raise an exception if the measure could not be found on the machine
269
+ # @return [Object] Returns the OpenStudio::Analysis::WorkflowStep
270
+ def self.from_measure_hash(instance_name, instance_display_name, path_to_measure, hash, options = {})
271
+ if File.directory? path_to_measure
272
+ path_to_measure = File.join(path_to_measure, 'measure.rb')
273
+ end
274
+
275
+ # verify that the path to the measure is a path and not a file. If it is make it a path
276
+ if File.exist?(path_to_measure) && File.file?(path_to_measure)
277
+ path_to_measure = File.dirname(path_to_measure)
278
+ else
279
+ raise "Could not find measure '#{instance_name}' in '#{path_to_measure}'" unless options[:ignore_not_found]
280
+ end
281
+
282
+ # Extract the directory
283
+ path_to_measure_local = path_to_measure
284
+ path_to_measure = "./measures/#{File.basename(path_to_measure)}"
285
+
286
+ # map the BCL hash format into the OpenStudio WorkflowStep format
287
+ s = OpenStudio::Analysis::WorkflowStep.new
288
+
289
+ # add the instance and display name
290
+ s.name = instance_name
291
+ s.display_name = instance_display_name
292
+
293
+ # definition of the measure
294
+ s.measure_definition_class_name = hash[:classname]
295
+ s.measure_definition_directory = path_to_measure
296
+ s.measure_definition_directory_local = path_to_measure_local
297
+ s.measure_definition_display_name = hash[:display_name]
298
+ s.measure_definition_name = hash[:name]
299
+ # name_xml is not used right now but eventually should be used to compare the hash[:name] and the hash[:name_xml]
300
+ s.measure_definition_name_xml = hash[:name_xml]
301
+ s.measure_definition_uuid = hash[:uid]
302
+ s.measure_definition_version_uuid = hash[:version_id]
303
+ s.uuid = hash[:uid]
304
+ s.version_uuid = hash[:version_id]
305
+ s.description = hash[:description]
306
+ #s.taxonomy = hash[:taxonomy] #BLB dont do this now
307
+
308
+ # do not allow the choice variable_type
309
+
310
+ s.type = hash[:measure_type] # this is actually the measure type
311
+ hash[:arguments]&.each do |arg|
312
+ puts arg
313
+ # warn the user to we need to deprecate variable_type and use value_type (which is what os server uses)
314
+ var_type = arg[:variable_type] ? arg[:variable_type].downcase : arg[:value_type]
315
+
316
+ if var_type == 'choice'
317
+ # WARN the user that the measure had a "choice data type"
318
+ var_type = 'string'
319
+ end
320
+
321
+
322
+ if var_type.downcase == 'double'
323
+ default_value = arg[:default_value].to_f
324
+ elsif var_type.downcase == 'integer'
325
+ default_value = arg[:default_value].to_i
326
+ elsif var_type.downcase == 'boolean'
327
+ # In some cases a nil default is okay. It is seen as "non-existing" and
328
+ # needs to be passed through as such.
329
+ if arg[:default_value].nil?
330
+ default_value = nil
331
+ else
332
+ default_value = (arg[:default_value].downcase == "true") #convert the string 'true'/'false' to boolean
333
+ end
334
+ else
335
+ default_value = arg[:default_value]
336
+ end
337
+
338
+ if !arg[:display_name_short].nil?
339
+ display_name_short = arg[:display_name_short]
340
+ else
341
+ display_name_short = arg[:display_name]
342
+ end
343
+
344
+ s.arguments << {
345
+ display_name: arg[:display_name],
346
+ display_name_short: display_name_short,
347
+ name: arg[:name],
348
+ value_type: var_type,
349
+ default_value: default_value,
350
+ value: default_value
351
+ }
352
+ end
353
+
354
+ # Load the arguments of variables, but do not make them variables. This format is more about arugments, than variables
355
+ hash[:variables]&.each do |variable|
356
+ # add the arguments first
357
+ s.arguments << {
358
+ display_name: variable[:argument][:display_name],
359
+ display_name_short: variable[:argument][:display_name_short],
360
+ name: variable[:argument][:name],
361
+ value_type: variable[:argument][:value_type],
362
+ default_value: variable[:argument][:default_value],
363
+ value: variable[:argument][:default_value]
364
+ }
365
+ end
366
+
367
+ s
368
+ end
369
+
370
+ # Read the workflow item from a analysis hash. Can we combine measure hash and analysis hash?
371
+ #
372
+ # @param instance_name [String] Machine name of the instance
373
+ # @param instance_display_name [String] Display name of the instance
374
+ # @param path_to_measure [String] This is the local path to the measure directroy, relative or absolute. It is used when zipping up all the measures.
375
+ # @param hash [Hash] Measure hash in the format of the measure.xml converted to JSON (from the Analysis Spreadsheet project)
376
+ # @param options [Hash] Optional arguments
377
+ # @option options [Boolean] :ignore_not_found Do not raise an exception if the measure could not be found on the machine
378
+ # @return [Object] Returns the OpenStudio::Analysis::WorkflowStep
379
+ def self.from_analysis_hash(instance_name, instance_display_name, path_to_measure, hash, options = {})
380
+ # TODO: Validate the hash
381
+ # TODO: validate that the measure exists?
382
+
383
+ if File.directory? path_to_measure
384
+ path_to_measure = File.join(path_to_measure, 'measure.rb')
385
+ end
386
+
387
+ # verify that the path to the measure is a path and not a file. If it is make it a path
388
+ if File.exist?(path_to_measure) && File.file?(path_to_measure)
389
+ path_to_measure = File.dirname(path_to_measure)
390
+ else
391
+ raise "Could not find measure '#{instance_name}' in '#{path_to_measure}'" unless options[:ignore_not_found]
392
+ end
393
+
394
+ # Extract the directo
395
+ path_to_measure_local = path_to_measure
396
+ path_to_measure = "./measures/#{File.basename(path_to_measure)}"
397
+
398
+ # map the BCL hash format into the OpenStudio WorkflowStep format
399
+ s = OpenStudio::Analysis::WorkflowStep.new
400
+
401
+ # add the instance and display name
402
+ s.name = instance_name
403
+ s.display_name = instance_display_name
404
+
405
+ # definition of the measure
406
+ s.measure_definition_class_name = hash[:measure_definition_class_name]
407
+ s.measure_definition_directory = path_to_measure
408
+ s.measure_definition_directory_local = path_to_measure_local
409
+ s.measure_definition_display_name = hash[:measure_definition_display_name]
410
+ s.measure_definition_name = hash[:measure_definition_name]
411
+ # name_xml is not used right now but eventually should be used to compare the hash[:name] and the hash[:name_xml]
412
+ s.measure_definition_name_xml = hash[:measure_definition_name_xml]
413
+ s.measure_definition_uuid = hash[:measure_definition_uuid]
414
+ s.measure_definition_version_uuid = hash[:measure_definition_version_uuid]
415
+ s.uuid = hash[:uuid] if hash[:uuid]
416
+ s.version_uuid = hash[:version_uuid] if hash[:version_uuid]
417
+ s.description = hash[:description] if hash[:description]
418
+ #s.taxonomy = hash[:taxonomy] if hash[:taxonomy] #BLB dont do this, its a Tags array of Tag
419
+
420
+ s.type = hash[:measure_type] # this is actually the measure type
421
+ hash[:arguments]&.each do |arg|
422
+ # warn the user to we need to deprecate variable_type and use value_type (which is what os server uses)
423
+ var_type = arg[:value_type]
424
+
425
+ if var_type == 'choice'
426
+ # WARN the user that the measure had a "choice data type"
427
+ var_type = 'string'
428
+ end
429
+
430
+ if var_type.downcase == 'double'
431
+ default_value = arg[:default_value].to_f
432
+ value = arg[:value].to_f
433
+ elsif var_type.downcase == 'integer'
434
+ default_value = arg[:default_value].to_i
435
+ value = arg[:value].to_i
436
+ elsif var_type.downcase == 'boolean'
437
+ default_value = (arg[:default_value].downcase == "true") # convert the string 'true'/'false' to boolean
438
+ value = (arg[:value].downcase == "true") # convert the string 'true'/'false' to boolean
439
+ else
440
+ default_value = arg[:default_value]
441
+ value = arg[:value]
442
+ end
443
+
444
+ if !arg[:display_name_short].nil?
445
+ display_name_short = arg[:display_name_short]
446
+ else
447
+ display_name_short = arg[:display_name]
448
+ end
449
+
450
+ s.arguments << {
451
+ display_name: arg[:display_name],
452
+ display_name_short: display_name_short,
453
+ name: arg[:name],
454
+ value_type: var_type,
455
+ default_value: default_value,
456
+ value: value
457
+ }
458
+ end
459
+
460
+ hash[:variables]&.each do |variable|
461
+ # add the arguments first
462
+ s.arguments << {
463
+ display_name: variable[:argument][:display_name],
464
+ display_name_short: variable[:argument][:display_name_short],
465
+ name: variable[:argument][:name],
466
+ value_type: variable[:argument][:value_type],
467
+ default_value: variable[:argument][:default_value],
468
+ value: variable[:argument][:default_value]
469
+ }
470
+
471
+ var_options = {}
472
+ var_options[:variable_type] = variable[:variable_type]
473
+ var_options[:variable_display_name_short] = variable[:display_name_short]
474
+ var_options[:static_value] = variable[:static_value]
475
+ distribution = variable[:uncertainty_description]
476
+ distribution[:minimum] = variable[:minimum]
477
+ distribution[:mean] = distribution[:attributes].find { |a| a[:name] == 'modes' }[:value]
478
+ distribution[:maximum] = variable[:maximum]
479
+ distribution[:standard_deviation] = distribution[:attributes].find { |a| a[:name] == 'stddev' }[:value]
480
+ distribution[:step_size] = distribution[:attributes].find { |a| a[:name] == 'delta_x' }[:value]
481
+ s.make_variable(variable[:argument][:name], variable[:display_name], distribution, var_options)
482
+ end
483
+
484
+ s
485
+ end
486
+
487
+ private
488
+
489
+ # validate the arguments of the distribution
490
+ def distribution_valid?(d)
491
+ # regardless of uncertainty description the following must be defined
492
+ raise 'No distribution defined for variable' unless d.key? :type
493
+ raise 'No minimum defined for variable' unless d.key? :minimum
494
+ raise 'No maximum defined for variable' unless d.key? :maximum
495
+ raise 'No mean/mode defined for variable' unless d.key? :mode
496
+
497
+ if d[:type] =~ /uniform/
498
+ # Do we need to tell the user that we don't really need the mean/mode for uniform ?
499
+ elsif d[:type] =~ /discrete/
500
+ # require min, max, mode
501
+ raise 'No values passed for discrete distribution' unless d[:values] || d[:values].empty?
502
+ if d[:weights]
503
+ raise 'Weights are not the same length as values' unless d[:values].size == d[:weights].size
504
+ raise 'Weights do not sum up to one' unless d[:weights].reduce(:+).between?(0.99, 1.01) # allow a small error for now
505
+ else
506
+ fraction = 1 / d[:values].size.to_f
507
+ d[:weights] = [fraction] * d[:values].size
508
+ end
509
+ elsif d[:type] =~ /integer_sequence/
510
+ d[:weights] = 1
511
+ d[:values] = 1
512
+ elsif d[:type] =~ /triangle/
513
+ # requires min, max, mode
514
+ elsif d[:type] =~ /normal/ # both normal and lognormal
515
+ # require min, max, mode, stddev
516
+ raise 'No standard deviation for variable' unless d[:standard_deviation]
517
+ end
518
+
519
+ true
520
+ end
521
+ end
522
+ end
523
+ end