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