openstudio-analysis 0.4.4 → 0.4.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +2 -4
- data/CHANGELOG.md +7 -0
- data/Gemfile +4 -1
- data/README.md +30 -13
- data/lib/openstudio-analysis.rb +1 -0
- data/lib/openstudio/analysis.rb +34 -0
- data/lib/openstudio/analysis/formulation.rb +12 -4
- data/lib/openstudio/analysis/server_api.rb +79 -1
- data/lib/openstudio/analysis/translator/datapoints.rb +409 -0
- data/lib/openstudio/analysis/translator/excel.rb +3 -3
- data/lib/openstudio/analysis/version.rb +1 -1
- data/lib/openstudio/analysis/workflow.rb +35 -0
- data/lib/openstudio/analysis/workflow_step.rb +2 -3
- data/lib/openstudio/weather/epw.rb +1 -1
- data/openstudio-analysis.gemspec +2 -1
- data/spec/schema/osa.json +550 -0
- data/spec/schema/osa.png +0 -0
- data/spec/schema/osd.json +110 -0
- data/spec/schema/osd.png +0 -0
- metadata +27 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a460438de48ec2ba07da54a58e5c2b4df31495b0
|
4
|
+
data.tar.gz: 710522dfd0cb39a17a6f41dff61a11caf250776e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 97237d9a0fdb49cc9b6e5cb400eaf7dd51b503de37bb457689b38ccbe7e2f977ac9cd8ed969bc8df5ce059bc2ba156ceb3f98b9846ead7002881ba5ab20e0959
|
7
|
+
data.tar.gz: f7a7cc8e3191c816cc41ed3b1fdfd3f8739eef45d4828c6f43b0ba62ea097214da807024bf249b77e48e4825e39b349b7467e1b6855551212d7186f2ab401e6d
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,13 @@
|
|
1
1
|
OpenStudio Analysis Gem Change Log
|
2
2
|
==================================
|
3
3
|
|
4
|
+
Version Unreleased
|
5
|
+
------------------
|
6
|
+
* Add diag analysis type to server_api run method
|
7
|
+
* Remove support for Rubies < 2.0.
|
8
|
+
* Add json extension to formulation name upon save if none exists
|
9
|
+
* Add zip extension to formulation zip upon save if none exists
|
10
|
+
|
4
11
|
Version 0.4.4
|
5
12
|
------------------
|
6
13
|
* Increment objective function count only if they are true
|
data/Gemfile
CHANGED
@@ -2,9 +2,12 @@ source 'http://rubygems.org'
|
|
2
2
|
|
3
3
|
gemspec
|
4
4
|
|
5
|
+
gem 'openstudio-aws', github: 'NREL/OpenStudio-aws-gem', branch: 'develop'
|
6
|
+
gem 'colored', '~> 1.2'
|
7
|
+
|
5
8
|
group :test do
|
6
9
|
gem 'coveralls', require: false
|
7
|
-
gem 'rspec', '~> 3.
|
10
|
+
gem 'rspec', '~> 3.4'
|
8
11
|
gem 'ci_reporter_rspec'
|
9
12
|
gem 'rubocop', '~> 0.31'
|
10
13
|
gem 'rubocop-checkstyle_formatter', '~> 0.2'
|
data/README.md
CHANGED
@@ -19,35 +19,52 @@ There are two ways to create an OpenStudio Analysis description:
|
|
19
19
|
* Programmatically
|
20
20
|
|
21
21
|
```
|
22
|
-
analysis = OpenStudio::Analysis.create
|
23
|
-
analysis.seed_model =
|
24
|
-
analysis.
|
22
|
+
analysis = OpenStudio::Analysis.create('Analysis Name')
|
23
|
+
analysis.seed_model = 'local/dir/seed.osm'
|
24
|
+
analysis.weather_file = 'local/dir/USA_CO_Golden-NREL.724666_TMY3.epw'
|
25
25
|
|
26
26
|
# override existing workflow from a file by
|
27
27
|
analysis.workflow = OpenStudio::Analysis::Workflow.load_from_file(...)
|
28
28
|
|
29
29
|
# add measures to the workflow
|
30
30
|
wf = analysis.workflow
|
31
|
-
|
32
|
-
wf.add_measure_from_path(
|
33
|
-
|
34
|
-
# or allow the system to search for the measure based on default_measure_paths
|
35
|
-
OpenStudio::Analysis.measure_paths = ['measures', '../project_specific_measures']
|
36
|
-
wf.add_measure_by_name('measure_name')
|
31
|
+
def add_measure_from_path(instance_name, instance_display_name, local_path_to_measure)
|
32
|
+
wf.add_measure_from_path('instance_name', 'Display name', 'path_to_measure')
|
33
|
+
wf.add_measure_from_path('instance_name_2', 'Display name two', 'path_to_measure_2')
|
37
34
|
|
38
35
|
# make a measure's argument a variable
|
39
|
-
m = wf.
|
36
|
+
m = wf.add_measure_from_path('instance_name_3', 'Display name three', 'path_to_measure_3')
|
40
37
|
m.make_variable('variable_argument_name', 'discrete')
|
41
38
|
|
42
|
-
m = wf.
|
39
|
+
m = wf.add_measure_from_path('instance_name_4', 'Display name four', 'path_to_measure_4')
|
43
40
|
m.make_variable('variable_argument_name', 'pivot')
|
44
|
-
m.
|
41
|
+
m.argument_value('variable_argument_name', value)
|
45
42
|
|
43
|
+
# Save off the analysis files and a static data point
|
44
|
+
run_dir = 'local/run'
|
45
|
+
analysis.save("#{run_dir}/analysis.json")
|
46
|
+
analysis.save_zip("#{run_dir}/analysis.zip")
|
47
|
+
analysis.save_static_data_point("#{run_dir}/data_point.zip")
|
46
48
|
```
|
47
49
|
|
50
|
+
If you would like to run the data point, then you can use the OpenStudio-workflow gem.
|
48
51
|
|
49
|
-
|
52
|
+
```
|
53
|
+
require 'openstudio-workflow'
|
54
|
+
|
55
|
+
run_dir = 'local/run'
|
56
|
+
OpenStudio::Workflow.extract_archive("#{run_dir}/analysis.zip", run_dir)
|
57
|
+
|
58
|
+
options = {
|
59
|
+
problem_filename: 'analysis.json',
|
60
|
+
datapoint_filename: 'data_point.json',
|
61
|
+
analysis_root_path: run_dir
|
62
|
+
}
|
63
|
+
k = OpenStudio::Workflow.load 'Local', run_dir, options
|
64
|
+
k.run
|
65
|
+
```
|
50
66
|
|
67
|
+
## Testing
|
51
68
|
|
52
69
|
This gem used RSpec for testing. To test simply run `rspec` at the command line.
|
53
70
|
|
data/lib/openstudio-analysis.rb
CHANGED
data/lib/openstudio/analysis.rb
CHANGED
@@ -20,5 +20,39 @@ module OpenStudio
|
|
20
20
|
excel.process
|
21
21
|
excel.analyses
|
22
22
|
end
|
23
|
+
|
24
|
+
# Load an set of batch datapoints from a csv. This will create a analysis
|
25
|
+
# of type 'batch_datapoints' which requires 'batch_run'
|
26
|
+
def self.from_csv(filename)
|
27
|
+
csv = OpenStudio::Analysis::Translator::Datapoints.new(filename)
|
28
|
+
csv.process
|
29
|
+
csv.analysis
|
30
|
+
end
|
31
|
+
|
32
|
+
# Retrieve aws instance options from a project. This will return a hash
|
33
|
+
def self.aws_instance_options(filename)
|
34
|
+
if File.extname(filename) == '.xlsx'
|
35
|
+
excel = OpenStudio::Analysis::Translator::Excel.new(filename)
|
36
|
+
excel.process
|
37
|
+
options = {
|
38
|
+
os_server_version: excel.settings['openstudio_server_version'],
|
39
|
+
server_instance_type: excel.settings['server_instance_type'],
|
40
|
+
worker_instance_type: excel.settings['worker_instance_type'],
|
41
|
+
worker_node_number: excel.settings['worker_nodes'].to_i,
|
42
|
+
user_id: excel.settings['user_id'],
|
43
|
+
aws_tags: excel.aws_tags,
|
44
|
+
analysis_type: excel.analyses.first.analysis_type,
|
45
|
+
cluster_name: excel.cluster_name
|
46
|
+
}
|
47
|
+
elsif File.extname(filename) == '.csv'
|
48
|
+
csv = OpenStudio::Analysis::Translator::Datapoints.new(filename)
|
49
|
+
csv.process
|
50
|
+
options = csv.settings
|
51
|
+
else
|
52
|
+
fail 'Invalid file extension'
|
53
|
+
end
|
54
|
+
|
55
|
+
return options
|
56
|
+
end
|
23
57
|
end
|
24
58
|
end
|
@@ -153,7 +153,7 @@ module OpenStudio
|
|
153
153
|
|
154
154
|
if @seed_model[:file]
|
155
155
|
h[:analysis][:seed] = {
|
156
|
-
file_type: File.extname(@seed_model[:file]).
|
156
|
+
file_type: File.extname(@seed_model[:file]).delete('.').upcase,
|
157
157
|
path: "./seed/#{File.basename(@seed_model[:file])}"
|
158
158
|
}
|
159
159
|
else
|
@@ -171,7 +171,7 @@ module OpenStudio
|
|
171
171
|
|
172
172
|
if wf
|
173
173
|
h[:analysis][:weather_file] = {
|
174
|
-
file_type: File.extname(wf[:file]).
|
174
|
+
file_type: File.extname(wf[:file]).delete('.').upcase,
|
175
175
|
path: "./weather/#{File.basename(wf[:file])}"
|
176
176
|
}
|
177
177
|
else
|
@@ -249,10 +249,12 @@ module OpenStudio
|
|
249
249
|
|
250
250
|
# save the file to JSON. Will overwrite the file if it already exists
|
251
251
|
#
|
252
|
-
# @param filename [String] Name of file to create. It will create the directory and override the file if it exists.
|
252
|
+
# @param filename [String] Name of file to create. It will create the directory and override the file if it exists. If no file extension is given, then it will use .json.
|
253
253
|
# @param version [Integer] Version of the format to return
|
254
254
|
# @return [Boolean]
|
255
255
|
def save(filename, version = 1)
|
256
|
+
filename += '.json' if File.extname(filename) == ''
|
257
|
+
|
256
258
|
FileUtils.mkdir_p File.dirname(filename) unless Dir.exist? File.dirname(filename)
|
257
259
|
File.open(filename, 'w') { |f| f << JSON.pretty_generate(to_hash(version)) }
|
258
260
|
|
@@ -261,9 +263,13 @@ module OpenStudio
|
|
261
263
|
|
262
264
|
# save the data point JSON with the variables set to the static values. Will overwrite the file if it already exists
|
263
265
|
#
|
266
|
+
# @param filename [String] Name of file to create. It will create the directory and override the file if it exists. If no file extension is given, then it will use .json.
|
264
267
|
# @param version [Integer] Version of the format to return
|
265
268
|
# @return [Boolean]
|
266
269
|
def save_static_data_point(filename, version = 1)
|
270
|
+
filename += '.json' if File.extname(filename) == ''
|
271
|
+
|
272
|
+
FileUtils.mkdir_p File.dirname(filename) unless Dir.exist? File.dirname(filename)
|
267
273
|
File.open(filename, 'w') { |f| f << JSON.pretty_generate(to_static_data_point_hash(version)) }
|
268
274
|
|
269
275
|
true
|
@@ -271,9 +277,11 @@ module OpenStudio
|
|
271
277
|
|
272
278
|
# save the analysis zip file which contains the measures, seed model, weather file, and init/final scripts
|
273
279
|
#
|
274
|
-
# @param filename [String] Name of file to create. It will create the directory and override the file if it exists.
|
280
|
+
# @param filename [String] Name of file to create. It will create the directory and override the file if it exists. If no file extension is given, then it will use .json.
|
275
281
|
# @return [Boolean]
|
276
282
|
def save_zip(filename)
|
283
|
+
filename += '.zip' if File.extname(filename) == ''
|
284
|
+
|
277
285
|
FileUtils.mkdir_p File.dirname(filename) unless Dir.exist? File.dirname(filename)
|
278
286
|
|
279
287
|
save_analysis_zip(filename)
|
@@ -5,6 +5,9 @@ module OpenStudio
|
|
5
5
|
class ServerApi
|
6
6
|
attr_reader :hostname
|
7
7
|
|
8
|
+
# Define set of anlaysis methods require batch_run to be queued after them
|
9
|
+
BATCH_RUN_METHODS = %w(lhs preflight single_run repeat_run doe diag baseline_perturbation batch_datapoints)
|
10
|
+
|
8
11
|
def initialize(options = {})
|
9
12
|
defaults = { hostname: 'http://localhost:8080' }
|
10
13
|
options = defaults.merge(options)
|
@@ -762,6 +765,80 @@ module OpenStudio
|
|
762
765
|
analysis_id
|
763
766
|
end
|
764
767
|
|
768
|
+
def run_baseline_perturbation(formulation_filename, analysis_zip_filename)
|
769
|
+
project_options = {}
|
770
|
+
project_id = new_project(project_options)
|
771
|
+
|
772
|
+
analysis_options = {
|
773
|
+
formulation_file: formulation_filename,
|
774
|
+
upload_file: analysis_zip_filename,
|
775
|
+
reset_uuids: true
|
776
|
+
}
|
777
|
+
analysis_id = new_analysis(project_id, analysis_options)
|
778
|
+
|
779
|
+
run_options = {
|
780
|
+
analysis_action: 'start',
|
781
|
+
without_delay: false,
|
782
|
+
analysis_type: 'baseline_perturbation',
|
783
|
+
allow_multiple_jobs: true,
|
784
|
+
use_server_as_worker: true,
|
785
|
+
simulate_data_point_filename: 'simulate_data_point.rb',
|
786
|
+
run_data_point_filename: 'run_openstudio_workflow_monthly.rb'
|
787
|
+
}
|
788
|
+
start_analysis(analysis_id, run_options)
|
789
|
+
|
790
|
+
run_options = {
|
791
|
+
analysis_action: 'start',
|
792
|
+
without_delay: false, # run in background
|
793
|
+
analysis_type: 'batch_run',
|
794
|
+
allow_multiple_jobs: true,
|
795
|
+
use_server_as_worker: true,
|
796
|
+
simulate_data_point_filename: 'simulate_data_point.rb',
|
797
|
+
run_data_point_filename: 'run_openstudio_workflow_monthly.rb'
|
798
|
+
}
|
799
|
+
start_analysis(analysis_id, run_options)
|
800
|
+
|
801
|
+
analysis_id
|
802
|
+
end
|
803
|
+
|
804
|
+
def run_batch_datapoints(formulation_filename, analysis_zip_filename)
|
805
|
+
project_options = {}
|
806
|
+
project_id = new_project(project_options)
|
807
|
+
|
808
|
+
puts 'In run_batch_datapoints'
|
809
|
+
|
810
|
+
analysis_options = {
|
811
|
+
formulation_file: formulation_filename,
|
812
|
+
upload_file: analysis_zip_filename,
|
813
|
+
reset_uuids: true
|
814
|
+
}
|
815
|
+
analysis_id = new_analysis(project_id, analysis_options)
|
816
|
+
|
817
|
+
run_options = {
|
818
|
+
analysis_action: 'start',
|
819
|
+
without_delay: false,
|
820
|
+
analysis_type: 'batch_datapoints',
|
821
|
+
allow_multiple_jobs: true,
|
822
|
+
use_server_as_worker: true,
|
823
|
+
simulate_data_point_filename: 'simulate_data_point.rb',
|
824
|
+
run_data_point_filename: 'run_openstudio_workflow_monthly.rb'
|
825
|
+
}
|
826
|
+
start_analysis(analysis_id, run_options)
|
827
|
+
|
828
|
+
run_options = {
|
829
|
+
analysis_action: 'start',
|
830
|
+
without_delay: false, # run in background
|
831
|
+
analysis_type: 'batch_run',
|
832
|
+
allow_multiple_jobs: true,
|
833
|
+
use_server_as_worker: true,
|
834
|
+
simulate_data_point_filename: 'simulate_data_point.rb',
|
835
|
+
run_data_point_filename: 'run_openstudio_workflow_monthly.rb'
|
836
|
+
}
|
837
|
+
start_analysis(analysis_id, run_options)
|
838
|
+
|
839
|
+
analysis_id
|
840
|
+
end
|
841
|
+
|
765
842
|
def run_analysis_detailed(formulation_filename, analysis_zip_filename, analysis_type,
|
766
843
|
allow_multiple_jobs = true, server_as_worker = true,
|
767
844
|
run_data_point_filename = 'run_openstudio_workflow_monthly.rb')
|
@@ -774,6 +851,7 @@ module OpenStudio
|
|
774
851
|
upload_file: analysis_zip_filename,
|
775
852
|
reset_uuids: true
|
776
853
|
}
|
854
|
+
|
777
855
|
analysis_id = new_analysis(project_id, analysis_options)
|
778
856
|
|
779
857
|
server_as_worker = true if analysis_type == 'optim' || analysis_type == 'rgenoud'
|
@@ -790,7 +868,7 @@ module OpenStudio
|
|
790
868
|
|
791
869
|
# If the analysis is a staged analysis, then go ahead and run batch run because there is
|
792
870
|
# no explicit way to tell the system to do it
|
793
|
-
if
|
871
|
+
if BATCH_RUN_METHODS.include? analysis_type
|
794
872
|
run_options = {
|
795
873
|
analysis_action: 'start',
|
796
874
|
without_delay: false,
|
@@ -0,0 +1,409 @@
|
|
1
|
+
module OpenStudio
|
2
|
+
module Analysis
|
3
|
+
module Translator
|
4
|
+
class Datapoints
|
5
|
+
attr_reader :version
|
6
|
+
attr_reader :settings
|
7
|
+
attr_reader :variables
|
8
|
+
attr_reader :outputs
|
9
|
+
attr_reader :models
|
10
|
+
attr_reader :measure_paths
|
11
|
+
attr_reader :weather_paths
|
12
|
+
attr_reader :worker_inits
|
13
|
+
attr_reader :worker_finals
|
14
|
+
attr_reader :export_path
|
15
|
+
attr_reader :cluster_name
|
16
|
+
attr_reader :variables
|
17
|
+
attr_reader :algorithm
|
18
|
+
attr_reader :problem
|
19
|
+
attr_reader :run_setup
|
20
|
+
attr_reader :aws_tags
|
21
|
+
|
22
|
+
# remove these once we have classes to construct the JSON file
|
23
|
+
attr_accessor :name
|
24
|
+
attr_reader :analysis_name
|
25
|
+
|
26
|
+
# Methods to override instance variables
|
27
|
+
|
28
|
+
# Pass in the filename to read
|
29
|
+
def initialize(csv_filename)
|
30
|
+
@csv_filename = csv_filename
|
31
|
+
@root_path = File.expand_path(File.dirname(@csv_filename))
|
32
|
+
|
33
|
+
@csv = nil
|
34
|
+
# Try to read the spreadsheet as a roo object
|
35
|
+
if File.exist?(@csv_filename)
|
36
|
+
@csv = CSV.read(@csv_filename)
|
37
|
+
else
|
38
|
+
fail "File #{@csv_filename} does not exist"
|
39
|
+
end
|
40
|
+
|
41
|
+
# Remove nil rows and check row length
|
42
|
+
@csv.delete_if { |row| row.uniq.length == 1 && row.uniq[0].nil? }
|
43
|
+
|
44
|
+
# Initialize some other instance variables
|
45
|
+
@version = '0.0.1'
|
46
|
+
@analyses = [] # Array o OpenStudio::Analysis. Use method to access
|
47
|
+
@name = nil
|
48
|
+
@analysis_name = nil
|
49
|
+
@cluster_name = nil
|
50
|
+
@settings = {}
|
51
|
+
@weather_paths = []
|
52
|
+
@models = []
|
53
|
+
@other_files = []
|
54
|
+
@worker_inits = []
|
55
|
+
@worker_finals = []
|
56
|
+
@export_path = './export'
|
57
|
+
@measure_paths = []
|
58
|
+
@problem = {}
|
59
|
+
@algorithm = {}
|
60
|
+
@outputs = {}
|
61
|
+
@run_setup = {}
|
62
|
+
@aws_tags = []
|
63
|
+
end
|
64
|
+
|
65
|
+
def process
|
66
|
+
# Seperate CSV into meta and measure groups
|
67
|
+
measure_tag_index = nil
|
68
|
+
@csv.each_with_index { |row, index| measure_tag_index = index if row[0] == 'BEGIN-MEASURES' }
|
69
|
+
fail "ERROR: No 'BEGIN-MEASURES' tag found in input csv file." unless measure_tag_index
|
70
|
+
meta_rows = []
|
71
|
+
measure_rows = []
|
72
|
+
@csv.each_with_index do |_, index|
|
73
|
+
meta_rows << @csv[index] if index < measure_tag_index
|
74
|
+
measure_rows << @csv[index] if index > measure_tag_index
|
75
|
+
end
|
76
|
+
|
77
|
+
@setup = parse_csv_meta(meta_rows)
|
78
|
+
|
79
|
+
@version = Semantic::Version.new @version
|
80
|
+
fail "CSV interface version #{@version} is no longer supported. Please upgrade your csv interface to at least 0.0.1" if @version < '0.0.0'
|
81
|
+
|
82
|
+
@variables = parse_csv_measures(measure_rows)
|
83
|
+
|
84
|
+
# call validate to make sure everything that is needed exists (i.e. directories)
|
85
|
+
validate_analysis
|
86
|
+
end
|
87
|
+
|
88
|
+
# Helper methods to remove models and add new ones programatically. Note that these should
|
89
|
+
# be moved into a general analysis class
|
90
|
+
def delete_models
|
91
|
+
@models = []
|
92
|
+
end
|
93
|
+
|
94
|
+
def add_model(name, display_name, type, path)
|
95
|
+
@models << {
|
96
|
+
name: name,
|
97
|
+
display_name: display_name,
|
98
|
+
type: type,
|
99
|
+
path: path
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
def validate_analysis
|
104
|
+
# Setup the paths and do some error checking
|
105
|
+
@measure_paths.each do |mp|
|
106
|
+
fail "Measures directory '#{mp}' does not exist" unless Dir.exist?(mp)
|
107
|
+
end
|
108
|
+
|
109
|
+
@models.uniq!
|
110
|
+
fail 'No seed models defined in spreadsheet' if @models.empty?
|
111
|
+
|
112
|
+
@models.each do |model|
|
113
|
+
fail "Seed model does not exist: #{model[:path]}" unless File.exist?(model[:path])
|
114
|
+
end
|
115
|
+
|
116
|
+
@weather_paths.uniq!
|
117
|
+
fail 'No weather files found based on what is in the spreadsheet' if @weather_paths.empty?
|
118
|
+
|
119
|
+
@weather_paths.each do |wf|
|
120
|
+
fail "Weather file does not exist: #{wf}" unless File.exist?(wf)
|
121
|
+
end
|
122
|
+
|
123
|
+
# This can be a directory as well
|
124
|
+
@other_files.each do |f|
|
125
|
+
fail "Other files do not exist for: #{f[:path]}" unless File.exist?(f[:path])
|
126
|
+
end
|
127
|
+
|
128
|
+
@worker_inits.each do |f|
|
129
|
+
fail "Worker initialization file does not exist for: #{f[:path]}" unless File.exist?(f[:path])
|
130
|
+
end
|
131
|
+
|
132
|
+
@worker_finals.each do |f|
|
133
|
+
fail "Worker finalization file does not exist for: #{f[:path]}" unless File.exist?(f[:path])
|
134
|
+
end
|
135
|
+
|
136
|
+
FileUtils.mkdir_p(@export_path)
|
137
|
+
|
138
|
+
# verify that the measure display names are unique
|
139
|
+
# puts @variables.inspect
|
140
|
+
measure_display_names = @variables.map { |m| m[:measure_data][:display_name] }.compact
|
141
|
+
measure_display_names_mult = measure_display_names.select { |m| measure_display_names.count(m) > 1 }.uniq
|
142
|
+
if measure_display_names_mult && !measure_display_names_mult.empty?
|
143
|
+
fail "Measure Display Names are not unique for '#{measure_display_names_mult.join('\', \'')}'"
|
144
|
+
end
|
145
|
+
|
146
|
+
variable_names = @variables.map { |v| v[:vars].map { |hash| hash[:display_name] } }.flatten
|
147
|
+
dupes = variable_names.select { |e| variable_names.count(e) > 1 }.uniq
|
148
|
+
if dupes.count > 0
|
149
|
+
fail "duplicate variable names found in list #{dupes.inspect}"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# convert the data in excel's parsed data into an OpenStudio Analysis Object
|
154
|
+
# @seed_model [Hash] Seed model to set the new analysis to
|
155
|
+
# @append_model_name [Boolean] Append the name of the seed model to the display name
|
156
|
+
# @return [Object] An OpenStudio::Analysis
|
157
|
+
def analysis(seed_model = nil, append_model_name = false)
|
158
|
+
fail 'There are no seed models defined in the excel file. Please add one.' if @models.size == 0
|
159
|
+
fail '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?
|
160
|
+
|
161
|
+
seed_model = @models.first if seed_model.nil?
|
162
|
+
|
163
|
+
# Use the programmatic interface to make the analysis
|
164
|
+
# append the model name to the analysis name if requested (normally if there are more than 1 models in the spreadsheet)
|
165
|
+
display_name = append_model_name ? @name + ' ' + seed_model[:display_name] : @name
|
166
|
+
|
167
|
+
a = OpenStudio::Analysis.create(display_name)
|
168
|
+
|
169
|
+
@variables.each do |measure|
|
170
|
+
@measure_paths.each do |measure_path|
|
171
|
+
measure_dir_to_add = "#{measure_path}/#{measure[:measure_data][:classname]}"
|
172
|
+
if Dir.exist? measure_dir_to_add
|
173
|
+
if File.exist? "#{measure_dir_to_add}/measure.rb"
|
174
|
+
measure[:measure_data][:local_path_to_measure] = "#{measure_dir_to_add}/measure.rb"
|
175
|
+
break
|
176
|
+
else
|
177
|
+
fail "Measure in directory '#{measure_dir_to_add}' did not contain a measure.rb file"
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
fail "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]
|
183
|
+
|
184
|
+
a.workflow.add_measure_from_csv(measure)
|
185
|
+
end
|
186
|
+
|
187
|
+
@other_files.each do |library|
|
188
|
+
a.libraries.add(library[:path], library_name: library[:lib_zip_name])
|
189
|
+
end
|
190
|
+
|
191
|
+
@worker_inits.each do |w|
|
192
|
+
a.worker_inits.add(w[:path], args: w[:args])
|
193
|
+
end
|
194
|
+
|
195
|
+
@worker_finals.each do |w|
|
196
|
+
a.worker_finalizes.add(w[:path], args: w[:args])
|
197
|
+
end
|
198
|
+
|
199
|
+
# Add in the outputs
|
200
|
+
@outputs.each do |o|
|
201
|
+
o = Hash[o.map { |k, v| [k.to_sym, v] }]
|
202
|
+
a.add_output(o)
|
203
|
+
end
|
204
|
+
|
205
|
+
a.analysis_type = @problem['analysis_type']
|
206
|
+
|
207
|
+
# clear out the seed files before adding new ones
|
208
|
+
a.seed_model = seed_model[:path]
|
209
|
+
|
210
|
+
# clear out the weather files before adding new ones
|
211
|
+
a.weather_files.clear
|
212
|
+
@weather_paths.each do |wp|
|
213
|
+
a.weather_files.add_files(wp)
|
214
|
+
end
|
215
|
+
|
216
|
+
a
|
217
|
+
end
|
218
|
+
|
219
|
+
protected
|
220
|
+
|
221
|
+
def parse_csv_meta(meta_rows)
|
222
|
+
# Convert to hash
|
223
|
+
config_hash = {}
|
224
|
+
meta_rows.each do |row|
|
225
|
+
config_hash[row[0].to_sym] = row[1]
|
226
|
+
end
|
227
|
+
|
228
|
+
# Assign required attributes
|
229
|
+
fail 'Require setting not found: version' unless config_hash[:version]
|
230
|
+
@version = config_hash[:version]
|
231
|
+
|
232
|
+
if config_hash[:analysis_name]
|
233
|
+
@name = config_hash[:analysis_name]
|
234
|
+
else
|
235
|
+
@name = SecureRandom.uuid
|
236
|
+
end
|
237
|
+
@analysis_name = @name.snake_case
|
238
|
+
|
239
|
+
fail 'Require setting not found: measure_path' unless config_hash[:measure_paths]
|
240
|
+
config_hash[:measure_paths] = [config_hash[:measure_paths]] unless config_hash[:measure_paths].respond_to?(:each)
|
241
|
+
config_hash[:measure_paths].each do |path|
|
242
|
+
if (Pathname.new path).absolute?
|
243
|
+
@measure_paths << path
|
244
|
+
else
|
245
|
+
@measure_paths << File.expand_path(File.join(@root_path, path))
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
fail 'Required setting not found: weather_paths' unless config_hash[:weather_paths]
|
250
|
+
config_hash[:weather_paths] = eval("#{config_hash[:weather_paths]}")
|
251
|
+
config_hash[:weather_paths].each do |path|
|
252
|
+
if (Pathname.new path).absolute?
|
253
|
+
@weather_paths << path
|
254
|
+
else
|
255
|
+
@weather_paths << File.expand_path(File.join(@root_path, path))
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
fail 'Required setting not found: models' unless config_hash[:models]
|
260
|
+
config_hash[:models] = [config_hash[:models]] unless config_hash[:models].respond_to?(:each)
|
261
|
+
config_hash[:models].each do |path|
|
262
|
+
model_name = File.basename(path).split('.')[0]
|
263
|
+
model_name = SecureRandom.uuid if model_name == ''
|
264
|
+
type = File.basename(path).split('.')[1].upcase
|
265
|
+
unless (Pathname.new path).absolute?
|
266
|
+
path = File.expand_path(File.join(@root_path, path))
|
267
|
+
end
|
268
|
+
@models << { name: model_name.snake_case, display_name: model_name, type: type, path: path }
|
269
|
+
end
|
270
|
+
|
271
|
+
# Assign optional attributes
|
272
|
+
if config_hash[:output]
|
273
|
+
path = File.expand_path(File.join(@root_path, config_hash[:output].to_s))
|
274
|
+
if File.exist? path
|
275
|
+
@outputs = MultiJson.load(File.read(path))
|
276
|
+
else
|
277
|
+
fail "Could not find output json: #{config_hash[:output]}"
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
if config_hash[:export_path]
|
282
|
+
if (Pathname.new config_hash[:export_path]).absolute?
|
283
|
+
@export_path = config_hash[:export_path]
|
284
|
+
else
|
285
|
+
@export_path = File.expand_path(File.join(@root_path, config_hash[:export_path]))
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
if config_hash[:library_path]
|
290
|
+
library_name = File.basename(config_hash[:library_path]).split('.')[0]
|
291
|
+
unless (Pathname.new config_hash[:library_path]).absolute?
|
292
|
+
config_hash[:library_path] = File.expand_path(File.join(@root_path, config_hash[:library_path]))
|
293
|
+
end
|
294
|
+
@other_files << { lib_zip_name: library_name, path: config_hash[:library_path] }
|
295
|
+
end
|
296
|
+
|
297
|
+
@run_setup[:allow_multiple_jobs] = config_hash[:allow_multiple_jobs].to_s.to_bool if config_hash[:allow_multiple_jobs]
|
298
|
+
@run_setup[:use_server_as_worker] = config_hash[:use_server_as_worker].to_s.to_bool if config_hash[:use_server_as_worker]
|
299
|
+
|
300
|
+
# Assign AWS settings
|
301
|
+
@settings[:proxy_port] = config_hash[:proxy_port] if config_hash[:proxy_port]
|
302
|
+
@settings[:cluster_name] = config_hash[:cluster_name] if config_hash[:cluster_name]
|
303
|
+
@settings[:user_id] = config_hash[:user_id] if config_hash[:user_id]
|
304
|
+
@settings[:os_server_version] = config_hash[:os_server_version] if config_hash[:os_server_version]
|
305
|
+
@settings[:server_instance_type] = config_hash[:server_instance_type] if config_hash[:server_instance_type]
|
306
|
+
@settings[:worker_instance_type] = config_hash[:worker_instance_type] if config_hash[:worker_instance_type]
|
307
|
+
@settings[:worker_node_number] = config_hash[:worker_node_number].to_i if config_hash[:worker_node_number]
|
308
|
+
@settings[:aws_tags] = config_hash[:aws_tags] if config_hash[:aws_tags]
|
309
|
+
@settings[:analysis_type] = 'batch_datapoints'
|
310
|
+
end
|
311
|
+
|
312
|
+
def parse_csv_measures(measure_rows)
|
313
|
+
# Build metadata required for parsing
|
314
|
+
measures = measure_rows[0].uniq.select { |measure| !measure.nil? }.map(&:to_sym)
|
315
|
+
measure_map = {}
|
316
|
+
measure_var_list = []
|
317
|
+
measures.each do |measure|
|
318
|
+
measure_map[measure] = {}
|
319
|
+
col_ind = (0..(measure_rows[0].length - 1)).to_a.select { |i| measure_rows[0][i] == measure.to_s }
|
320
|
+
col_ind.each do |var_ind|
|
321
|
+
tuple = measure.to_s + measure_rows[1][var_ind]
|
322
|
+
fail "Multiple measure_variable tuples found for '#{measure}_#{measure_rows[1][var_ind]}'. These tuples must be unique." if measure_var_list.include? tuple
|
323
|
+
measure_var_list << tuple
|
324
|
+
measure_map[measure][measure_rows[1][var_ind].to_sym] = var_ind
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
# For each measure load measure json and parse out critical variable requirements
|
329
|
+
data = []
|
330
|
+
measures.each_with_index do |measure, measure_index|
|
331
|
+
data[measure_index] = {}
|
332
|
+
measure_json = ''
|
333
|
+
for i in 0..(@measure_paths.length - 1)
|
334
|
+
if File.exist? File.join(@measure_paths[i], measure.to_s, 'measure.json')
|
335
|
+
measure_json = MultiJson.load(File.read(File.join(@measure_paths[i], measure.to_s, 'measure.json')))
|
336
|
+
break
|
337
|
+
end
|
338
|
+
end
|
339
|
+
fail "Could not find measure json #{measure}.json in measure_paths: '#{@measure_paths.join("\n")}'" if measure_json == ''
|
340
|
+
measure_data = {}
|
341
|
+
measure_data[:classname] = measure_json['classname']
|
342
|
+
measure_data[:name] = measure_json['name']
|
343
|
+
measure_data[:display_name] = measure_json['display_name']
|
344
|
+
measure_data[:measure_type] = measure_json['measure_type']
|
345
|
+
measure_data[:uid] = measure_json['uid']
|
346
|
+
measure_data[:version_id] = measure_json['version_id']
|
347
|
+
data[measure_index][:measure_data] = measure_data
|
348
|
+
data[measure_index][:vars] = []
|
349
|
+
vars = measure_map[measure]
|
350
|
+
vars.each do |var|
|
351
|
+
var = var[0]
|
352
|
+
var_hash = {}
|
353
|
+
var_json = measure_json['arguments'].select { |hash| hash['local_variable'] == var.to_s }[0]
|
354
|
+
fail "measure.json for measure #{measure} does not have an argument with local_variable == #{var}" if var_json.nil?
|
355
|
+
var_hash[:variable_type] = 'variable'
|
356
|
+
var_hash[:display_name] = measure_rows[2][measure_map[measure][var]]
|
357
|
+
var_hash[:display_name_short] = var_hash[:display_name]
|
358
|
+
var_hash[:name] = var_json['local_variable']
|
359
|
+
var_hash[:type] = var_json['variable_type'].downcase
|
360
|
+
var_hash[:units] = var_json['units']
|
361
|
+
var_hash[:distribution] = {}
|
362
|
+
case var_hash[:type].downcase
|
363
|
+
when 'bool', 'boolean' # is 'boolean' necessary? it's not in the enum catch
|
364
|
+
var_hash[:distribution][:values] = (3..(measure_rows.length - 1)).map { |value| measure_rows[value.to_i][measure_map[measure][var]].to_s == 'true' }
|
365
|
+
var_hash[:distribution][:maximum] = true
|
366
|
+
var_hash[:distribution][:minimum] = false
|
367
|
+
var_hash[:distribution][:mode] = var_hash[:distribution][:values].group_by { |i| i }.max { |x, y| x[1].length <=> y[1].length }[0]
|
368
|
+
when 'choice', 'string'
|
369
|
+
var_hash[:distribution][:values] = (3..(measure_rows.length) - 1).map { |value| measure_rows[value.to_i][measure_map[measure][var]].to_s }
|
370
|
+
var_hash[:distribution][:minimum] = var_hash[:distribution][:values].min
|
371
|
+
var_hash[:distribution][:maximum] = var_hash[:distribution][:values].max
|
372
|
+
var_hash[:distribution][:mode] = var_hash[:distribution][:values].group_by { |i| i }.max { |x, y| x[1].length <=> y[1].length }[0]
|
373
|
+
else
|
374
|
+
var_hash[:distribution][:values] = (3..(measure_rows.length - 1)).map { |value| eval(measure_rows[value.to_i][measure_map[measure][var]]) }
|
375
|
+
var_hash[:distribution][:minimum] = var_hash[:distribution][:values].map(&:to_i).min
|
376
|
+
var_hash[:distribution][:maximum] = var_hash[:distribution][:values].map(&:to_i).max
|
377
|
+
var_hash[:distribution][:mode] = var_hash[:distribution][:values].group_by { |i| i }.max { |x, y| x[1].length <=> y[1].length }[0]
|
378
|
+
end
|
379
|
+
var_hash[:distribution][:weights] = eval('[' + "#{1.0 / (measure_rows.length - 3)}," * (measure_rows.length - 3) + ']')
|
380
|
+
var_hash[:distribution][:type] = 'discrete'
|
381
|
+
var_hash[:distribution][:units] = var_hash[:units]
|
382
|
+
if var_hash[:type] == 'choice'
|
383
|
+
var_hash[:distribution][:enumerations] = var_json['choices']
|
384
|
+
elsif var_hash[:type] == 'bool'
|
385
|
+
var_hash[:distribution][:enumerations] = []
|
386
|
+
var_hash[:distribution][:enumerations] << 'true' # TODO: should this be a real bool?
|
387
|
+
var_hash[:distribution][:enumerations] << 'false'
|
388
|
+
end
|
389
|
+
data[measure_index][:vars] << var_hash
|
390
|
+
end
|
391
|
+
data[measure_index][:args] = []
|
392
|
+
measure_json['arguments'].each do |arg_json|
|
393
|
+
arg = {}
|
394
|
+
arg[:value_type] = arg_json['variable_type'].downcase
|
395
|
+
arg[:name] = arg_json['name']
|
396
|
+
arg[:display_name] = arg_json['display_name']
|
397
|
+
arg[:display_name_short] = arg_json['display_name']
|
398
|
+
arg[:default_value] = arg_json['default_value']
|
399
|
+
arg[:value] = arg_json['default_value']
|
400
|
+
data[measure_index][:args] << arg
|
401
|
+
end
|
402
|
+
end
|
403
|
+
|
404
|
+
data
|
405
|
+
end
|
406
|
+
end
|
407
|
+
end
|
408
|
+
end
|
409
|
+
end
|