openstudio-analysis 1.3.5 → 1.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/openstudio-analysis.yml +40 -40
- data/.gitignore +21 -21
- data/.rubocop.yml +9 -9
- data/CHANGELOG.md +269 -265
- data/Gemfile +14 -14
- data/README.md +102 -102
- data/Rakefile +40 -40
- data/lib/openstudio/analysis/algorithm_attributes.rb +47 -47
- data/lib/openstudio/analysis/formulation.rb +857 -857
- data/lib/openstudio/analysis/server_api.rb +862 -862
- data/lib/openstudio/analysis/server_scripts.rb +108 -108
- data/lib/openstudio/analysis/support_files.rb +104 -104
- data/lib/openstudio/analysis/translator/datapoints.rb +454 -454
- data/lib/openstudio/analysis/translator/excel.rb +893 -893
- data/lib/openstudio/analysis/translator/workflow.rb +143 -143
- data/lib/openstudio/analysis/version.rb +12 -12
- data/lib/openstudio/analysis/workflow.rb +279 -279
- data/lib/openstudio/analysis/workflow_step.rb +523 -523
- data/lib/openstudio/analysis.rb +144 -144
- data/lib/openstudio/helpers/hash.rb +10 -10
- data/lib/openstudio/helpers/string.rb +36 -36
- data/lib/openstudio/helpers/utils.rb +36 -36
- data/lib/openstudio/weather/epw.rb +178 -178
- data/lib/openstudio-analysis.rb +47 -47
- data/openstudio-analysis.gemspec +38 -38
- data/update_license.rb +60 -60
- metadata +9 -9
@@ -1,857 +1,857 @@
|
|
1
|
-
# *******************************************************************************
|
2
|
-
# OpenStudio(R), Copyright (c) Alliance for Sustainable Energy, LLC.
|
3
|
-
# See also https://openstudio.net/license
|
4
|
-
# *******************************************************************************
|
5
|
-
|
6
|
-
# OpenStudio formulation class handles the generation of the OpenStudio Analysis format.
|
7
|
-
module OpenStudio
|
8
|
-
module Analysis
|
9
|
-
SeedModel = Struct.new(:file)
|
10
|
-
WeatherFile = Struct.new(:file)
|
11
|
-
|
12
|
-
@@measure_paths = ['./measures']
|
13
|
-
# List of paths to look for measures when adding them. This currently only is used when loading an
|
14
|
-
# analysis hash file. It looks in the order of the measure_paths. As soon as it finds one, it stops.
|
15
|
-
def self.measure_paths
|
16
|
-
@@measure_paths
|
17
|
-
end
|
18
|
-
|
19
|
-
def self.measure_paths=(new_array)
|
20
|
-
@@measure_paths = new_array
|
21
|
-
end
|
22
|
-
|
23
|
-
class Formulation
|
24
|
-
attr_reader :seed_model
|
25
|
-
attr_reader :weather_file
|
26
|
-
attr_reader :analysis_type
|
27
|
-
attr_reader :outputs
|
28
|
-
attr_accessor :display_name
|
29
|
-
attr_accessor :workflow
|
30
|
-
attr_accessor :algorithm
|
31
|
-
attr_accessor :osw_path
|
32
|
-
attr_accessor :download_zip
|
33
|
-
attr_accessor :download_reports
|
34
|
-
attr_accessor :download_osw
|
35
|
-
attr_accessor :download_osm
|
36
|
-
attr_accessor :cli_debug
|
37
|
-
attr_accessor :cli_verbose
|
38
|
-
attr_accessor :initialize_worker_timeout
|
39
|
-
attr_accessor :run_workflow_timeout
|
40
|
-
attr_accessor :upload_results_timeout
|
41
|
-
|
42
|
-
# the attributes below are used for packaging data into the analysis zip file
|
43
|
-
attr_reader :weather_files
|
44
|
-
attr_reader :seed_models
|
45
|
-
attr_reader :worker_inits
|
46
|
-
attr_reader :worker_finalizes
|
47
|
-
attr_reader :libraries
|
48
|
-
attr_reader :server_scripts
|
49
|
-
|
50
|
-
# Create an instance of the OpenStudio::Analysis::Formulation
|
51
|
-
#
|
52
|
-
# @param display_name [String] Display name of the project.
|
53
|
-
# @return [Object] An OpenStudio::Analysis::Formulation object
|
54
|
-
def initialize(display_name)
|
55
|
-
@display_name = display_name
|
56
|
-
@analysis_type = nil
|
57
|
-
@outputs = []
|
58
|
-
@workflow = OpenStudio::Analysis::Workflow.new
|
59
|
-
# Initialize child objects (expect workflow)
|
60
|
-
@weather_file = WeatherFile.new
|
61
|
-
@seed_model = SeedModel.new
|
62
|
-
@algorithm = OpenStudio::Analysis::AlgorithmAttributes.new
|
63
|
-
@download_zip = true
|
64
|
-
@download_reports = true
|
65
|
-
@download_osw = true
|
66
|
-
@download_osm = true
|
67
|
-
@cli_debug = "--debug"
|
68
|
-
@cli_verbose = "--verbose"
|
69
|
-
@initialize_worker_timeout = 28800
|
70
|
-
@run_workflow_timeout = 28800
|
71
|
-
@upload_results_timeout = 28800
|
72
|
-
|
73
|
-
# Analysis Zip attributes
|
74
|
-
@weather_files = SupportFiles.new
|
75
|
-
@seed_models = SupportFiles.new
|
76
|
-
@worker_inits = SupportFiles.new
|
77
|
-
@worker_finalizes = SupportFiles.new
|
78
|
-
@libraries = SupportFiles.new
|
79
|
-
@server_scripts = ServerScripts.new
|
80
|
-
end
|
81
|
-
|
82
|
-
# Define the type of analysis which is going to be running
|
83
|
-
#
|
84
|
-
# @param name [String] Name of the algorithm/analysis. (e.g. rgenoud, lhs, single_run)
|
85
|
-
# allowed values are ANALYSIS_TYPES = ['spea_nrel', 'rgenoud', 'nsga_nrel', 'lhs', 'preflight',
|
86
|
-
# 'morris', 'sobol', 'doe', 'fast99', 'ga', 'gaisl',
|
87
|
-
# 'single_run', 'repeat_run', 'batch_run']
|
88
|
-
def analysis_type=(value)
|
89
|
-
if OpenStudio::Analysis::AlgorithmAttributes::ANALYSIS_TYPES.include?(value)
|
90
|
-
@analysis_type = value
|
91
|
-
else
|
92
|
-
raise "Invalid analysis type. Allowed types: #{OpenStudio::Analysis::AlgorithmAttributes::ANALYSIS_TYPES}"
|
93
|
-
end
|
94
|
-
end
|
95
|
-
|
96
|
-
# Path to the seed model
|
97
|
-
#
|
98
|
-
# @param path [String] Path to the seed model. This should be relative.
|
99
|
-
def seed_model=(file)
|
100
|
-
@seed_model[:file] = file
|
101
|
-
end
|
102
|
-
|
103
|
-
# Path to the weather file (or folder). If it is a folder, then the measures will look for the weather file
|
104
|
-
# by name in that folder.
|
105
|
-
#
|
106
|
-
# @param path [String] Path to the weather file or folder.
|
107
|
-
def weather_file=(file)
|
108
|
-
@weather_file[:file] = file
|
109
|
-
end
|
110
|
-
|
111
|
-
# Set the value for 'download_zip'
|
112
|
-
#
|
113
|
-
# @param value [Boolean] The value for 'download_zip'
|
114
|
-
def download_zip=(value)
|
115
|
-
if [true, false].include?(value)
|
116
|
-
@download_zip = value
|
117
|
-
else
|
118
|
-
raise ArgumentError, "Invalid value for 'download_zip'. Only true or false allowed."
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
# Set the value for 'download_reports'
|
123
|
-
#
|
124
|
-
# @param value [Boolean] The value for 'download_reports'
|
125
|
-
def download_reports=(value)
|
126
|
-
if [true, false].include?(value)
|
127
|
-
@download_reports = value
|
128
|
-
else
|
129
|
-
raise ArgumentError, "Invalid value for 'download_reports'. Only true or false allowed."
|
130
|
-
end
|
131
|
-
end
|
132
|
-
|
133
|
-
# Set the value for 'download_osw'
|
134
|
-
#
|
135
|
-
# @param value [Boolean] The value for 'download_osw'
|
136
|
-
def download_osw=(value)
|
137
|
-
if [true, false].include?(value)
|
138
|
-
@download_osw = value
|
139
|
-
else
|
140
|
-
raise ArgumentError, "Invalid value for 'download_osw'. Only true or false allowed."
|
141
|
-
end
|
142
|
-
end
|
143
|
-
|
144
|
-
# Set the value for 'download_osm'
|
145
|
-
#
|
146
|
-
# @param value [Boolean] The value for 'download_osm'
|
147
|
-
def download_osm=(value)
|
148
|
-
if [true, false].include?(value)
|
149
|
-
@download_osm = value
|
150
|
-
else
|
151
|
-
raise ArgumentError, "Invalid value for 'download_osm'. Only true or false allowed."
|
152
|
-
end
|
153
|
-
end
|
154
|
-
|
155
|
-
# Set the value for 'cli_debug'
|
156
|
-
#
|
157
|
-
# @param value [Boolean] The value for 'cli_debug'
|
158
|
-
def cli_debug=(value)
|
159
|
-
@cli_debug = value
|
160
|
-
end
|
161
|
-
|
162
|
-
# Set the value for 'cli_verbose'
|
163
|
-
#
|
164
|
-
# @param value [Boolean] The value for 'cli_verbose'
|
165
|
-
def cli_verbose=(value)
|
166
|
-
@cli_verbose = value
|
167
|
-
end
|
168
|
-
|
169
|
-
# Set the value for 'run_workflow_timeout'
|
170
|
-
#
|
171
|
-
# @param value [Integer] The value for 'run_workflow_timeout'
|
172
|
-
def run_workflow_timeout=(value)
|
173
|
-
if value.is_a?(Integer)
|
174
|
-
@run_workflow_timeout = value
|
175
|
-
else
|
176
|
-
raise ArgumentError, "Invalid value for 'run_workflow_timeout'. Only integer values allowed."
|
177
|
-
end
|
178
|
-
end
|
179
|
-
|
180
|
-
# Set the value for 'initialize_worker_timeout'
|
181
|
-
#
|
182
|
-
# @param value [Integer] The value for 'initialize_worker_timeout'
|
183
|
-
def initialize_worker_timeout=(value)
|
184
|
-
if value.is_a?(Integer)
|
185
|
-
@initialize_worker_timeout = value
|
186
|
-
else
|
187
|
-
raise ArgumentError, "Invalid value for 'initialize_worker_timeout'. Only integer values allowed."
|
188
|
-
end
|
189
|
-
end
|
190
|
-
|
191
|
-
# Set the value for 'upload_results_timeout'
|
192
|
-
#
|
193
|
-
# @param value [Integer] The value for 'upload_results_timeout'
|
194
|
-
def upload_results_timeout=(value)
|
195
|
-
if value.is_a?(Integer)
|
196
|
-
@upload_results_timeout = value
|
197
|
-
else
|
198
|
-
raise ArgumentError, "Invalid value for 'upload_results_timeout'. Only integer values allowed."
|
199
|
-
end
|
200
|
-
end
|
201
|
-
|
202
|
-
# Add an output of interest to the problem formulation
|
203
|
-
#
|
204
|
-
# @param output_hash [Hash] Hash of the output variable in the legacy format
|
205
|
-
# @option output_hash [String] :display_name Name to display
|
206
|
-
# @option output_hash [String] :display_name_short A shorter display name
|
207
|
-
# @option output_hash [String] :metadata_id Link to DEnCity ID in which this output corresponds
|
208
|
-
# @option output_hash [String] :name Unique machine name of the variable. Typically this is measure.attribute
|
209
|
-
# @option output_hash [String] :export Export the variable to CSV and dataframes from OpenStudio-server
|
210
|
-
# @option output_hash [String] :visualize Visualize the variable in the plots on OpenStudio-server
|
211
|
-
# @option output_hash [String] :units Units of the variable as a string
|
212
|
-
# @option output_hash [String] :variable_type Data type of the variable
|
213
|
-
# @option output_hash [Boolean] :objective_function Whether or not this output is an objective function. Default: false
|
214
|
-
# @option output_hash [Integer] :objective_function_index Index of the objective function. Default: nil
|
215
|
-
# @option output_hash [Float] :objective_function_target Target for the objective function to reach (if defined). Default: nil
|
216
|
-
# @option output_hash [Float] :scaling_factor How to scale the objective function(s). Default: nil
|
217
|
-
# @option output_hash [Integer] :objective_function_group If grouping objective functions, then group ID. Default: nil
|
218
|
-
def add_output(output_hash)
|
219
|
-
# Check if the name is already been added.
|
220
|
-
exist = @outputs.find_index { |o| o[:name] == output_hash[:name] }
|
221
|
-
# if so, update the fields but keep objective_function_index the same
|
222
|
-
if exist
|
223
|
-
original = @outputs[exist]
|
224
|
-
if original[:objective_function] && !output_hash[:objective_function]
|
225
|
-
return @outputs
|
226
|
-
end
|
227
|
-
output = original.merge(output_hash)
|
228
|
-
output[:objective_function_index] = original[:objective_function_index]
|
229
|
-
@outputs[exist] = output
|
230
|
-
else
|
231
|
-
output = {
|
232
|
-
units: '',
|
233
|
-
objective_function: false,
|
234
|
-
objective_function_index: nil,
|
235
|
-
objective_function_target: nil,
|
236
|
-
#set default to nil or 1 if objective_function is true and this is not set
|
237
|
-
objective_function_group: (output_hash[:objective_function] ? 1 : nil),
|
238
|
-
scaling_factor: nil,
|
239
|
-
#set default to false or true if objective_function is true and this is not set
|
240
|
-
visualize: (output_hash[:objective_function] ? true : false),
|
241
|
-
metadata_id: nil,
|
242
|
-
export: true,
|
243
|
-
}.merge(output_hash)
|
244
|
-
#set display_name default to be name if its not set
|
245
|
-
output[:display_name] = output_hash[:display_name] ? output_hash[:display_name] : output_hash[:name]
|
246
|
-
#set display_name_short default to be display_name if its not set, this can be null if :display_name not set
|
247
|
-
output[:display_name_short] = output_hash[:display_name_short] ? output_hash[:display_name_short] : output_hash[:display_name]
|
248
|
-
# if the variable is an objective_function, then increment and
|
249
|
-
# assign and objective function index
|
250
|
-
if output[:objective_function]
|
251
|
-
values = @outputs.select { |o| o[:objective_function] }
|
252
|
-
output[:objective_function_index] = values.size
|
253
|
-
end
|
254
|
-
|
255
|
-
@outputs << output
|
256
|
-
end
|
257
|
-
|
258
|
-
@outputs
|
259
|
-
end
|
260
|
-
|
261
|
-
# return the machine name of the analysis
|
262
|
-
def name
|
263
|
-
@display_name.to_underscore
|
264
|
-
end
|
265
|
-
|
266
|
-
# return a hash.
|
267
|
-
#
|
268
|
-
# @param version [Integer] Version of the format to return
|
269
|
-
# @return [Hash]
|
270
|
-
def to_hash(version = 1)
|
271
|
-
# fail 'Must define an analysis type' unless @analysis_type
|
272
|
-
if version == 1
|
273
|
-
h = {
|
274
|
-
analysis: {
|
275
|
-
display_name: @display_name,
|
276
|
-
name: name,
|
277
|
-
output_variables: @outputs,
|
278
|
-
problem: {
|
279
|
-
analysis_type: @analysis_type,
|
280
|
-
algorithm: algorithm.to_hash(version),
|
281
|
-
workflow: workflow.to_hash(version)
|
282
|
-
}
|
283
|
-
}
|
284
|
-
}
|
285
|
-
|
286
|
-
if @seed_model[:file]
|
287
|
-
h[:analysis][:seed] = {
|
288
|
-
file_type: File.extname(@seed_model[:file]).delete('.').upcase,
|
289
|
-
path: "./seed/#{File.basename(@seed_model[:file])}"
|
290
|
-
}
|
291
|
-
else
|
292
|
-
h[:analysis][:seed] = nil
|
293
|
-
end
|
294
|
-
|
295
|
-
# silly catch for if weather_file is not set
|
296
|
-
wf = nil
|
297
|
-
if @weather_file[:file]
|
298
|
-
wf = @weather_file
|
299
|
-
elsif !@weather_files.empty?
|
300
|
-
# get the first EPW file (not the first file)
|
301
|
-
wf = @weather_files.find { |w| File.extname(w[:file]).casecmp('.epw').zero? }
|
302
|
-
end
|
303
|
-
|
304
|
-
if wf
|
305
|
-
h[:analysis][:weather_file] = {
|
306
|
-
file_type: File.extname(wf[:file]).delete('.').upcase,
|
307
|
-
path: "./weather/#{File.basename(wf[:file])}"
|
308
|
-
}
|
309
|
-
else
|
310
|
-
# log: could not find weather file
|
311
|
-
warn 'Could not resolve a valid weather file. Check paths to weather files'
|
312
|
-
end
|
313
|
-
|
314
|
-
h[:analysis][:file_format_version] = version
|
315
|
-
h[:analysis][:cli_debug] = @cli_debug
|
316
|
-
h[:analysis][:cli_verbose] = @cli_verbose
|
317
|
-
h[:analysis][:run_workflow_timeout] = @run_workflow_timeout
|
318
|
-
h[:analysis][:upload_results_timeout] = @upload_results_timeout
|
319
|
-
h[:analysis][:initialize_worker_timeout] = @initialize_worker_timeout
|
320
|
-
h[:analysis][:download_zip] = @download_zip
|
321
|
-
h[:analysis][:download_reports] = @download_reports
|
322
|
-
h[:analysis][:download_osw] = @download_osw
|
323
|
-
h[:analysis][:download_osm] = @download_osm
|
324
|
-
|
325
|
-
#-BLB I dont think this does anything. server_scripts are run if they are in
|
326
|
-
#the /scripts/analysis or /scripts/data_point directories
|
327
|
-
#but nothing is ever checked in the OSA.
|
328
|
-
#
|
329
|
-
h[:analysis][:server_scripts] = {}
|
330
|
-
|
331
|
-
# This is a hack right now, but after the initial hash is created go back and add in the objective functions
|
332
|
-
# to the the algorithm as defined in the output_variables list
|
333
|
-
ofs = @outputs.map { |i| i[:name] if i[:objective_function] }.compact
|
334
|
-
if h[:analysis][:problem][:algorithm]
|
335
|
-
h[:analysis][:problem][:algorithm][:objective_functions] = ofs
|
336
|
-
end
|
337
|
-
|
338
|
-
h
|
339
|
-
else
|
340
|
-
raise "Version #{version} not defined for #{self.class} and #{__method__}"
|
341
|
-
end
|
342
|
-
end
|
343
|
-
|
344
|
-
# Load the analysis JSON from a hash (with symbolized keys)
|
345
|
-
def self.from_hash(h, seed_dir = nil, weather_dir = nil)
|
346
|
-
o = OpenStudio::Analysis::Formulation.new(h[:analysis][:display_name])
|
347
|
-
|
348
|
-
version = 1
|
349
|
-
if version == 1
|
350
|
-
h[:analysis][:output_variables].each do |ov|
|
351
|
-
o.add_output(ov)
|
352
|
-
end
|
353
|
-
|
354
|
-
o.workflow = OpenStudio::Analysis::Workflow.load(workflow: h[:analysis][:problem][:workflow])
|
355
|
-
|
356
|
-
if weather_dir
|
357
|
-
o.weather_file "#{weather_path}/#{File.basename(h[:analysis][:weather_file][:path])}"
|
358
|
-
else
|
359
|
-
o.weather_file = h[:analysis][:weather_file][:path]
|
360
|
-
end
|
361
|
-
|
362
|
-
if seed_dir
|
363
|
-
o.seed_model "#{weather_path}/#{File.basename(h[:analysis][:seed][:path])}"
|
364
|
-
else
|
365
|
-
o.seed_model = h[:analysis][:seed][:path]
|
366
|
-
end
|
367
|
-
else
|
368
|
-
raise "Version #{version} not defined for #{self.class} and #{__method__}"
|
369
|
-
end
|
370
|
-
|
371
|
-
o
|
372
|
-
end
|
373
|
-
|
374
|
-
# return a hash of the data point with the static variables set
|
375
|
-
#
|
376
|
-
# @param version [Integer] Version of the format to return
|
377
|
-
# @return [Hash]
|
378
|
-
def to_static_data_point_hash(version = 1)
|
379
|
-
if version == 1
|
380
|
-
static_hash = {}
|
381
|
-
# TODO: this method should be on the workflow step and bubbled up to this interface
|
382
|
-
@workflow.items.map do |item|
|
383
|
-
item.variables.map { |v| static_hash[v[:uuid]] = v[:static_value] }
|
384
|
-
end
|
385
|
-
|
386
|
-
h = {
|
387
|
-
data_point: {
|
388
|
-
set_variable_values: static_hash,
|
389
|
-
status: 'na',
|
390
|
-
uuid: SecureRandom.uuid
|
391
|
-
}
|
392
|
-
}
|
393
|
-
h
|
394
|
-
end
|
395
|
-
end
|
396
|
-
|
397
|
-
# save the file to JSON. Will overwrite the file if it already exists
|
398
|
-
#
|
399
|
-
# @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.
|
400
|
-
# @param version [Integer] Version of the format to return
|
401
|
-
# @return [Boolean]
|
402
|
-
def save(filename, version = 1)
|
403
|
-
filename += '.json' if File.extname(filename) == ''
|
404
|
-
|
405
|
-
FileUtils.mkdir_p File.dirname(filename) unless Dir.exist? File.dirname(filename)
|
406
|
-
File.open(filename, 'w') { |f| f << JSON.pretty_generate(to_hash(version)) }
|
407
|
-
|
408
|
-
true
|
409
|
-
end
|
410
|
-
|
411
|
-
# save the data point JSON with the variables set to the static values. Will overwrite the file if it already exists
|
412
|
-
#
|
413
|
-
# @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.
|
414
|
-
# @param version [Integer] Version of the format to return
|
415
|
-
# @return [Boolean]
|
416
|
-
def save_static_data_point(filename, version = 1)
|
417
|
-
filename += '.json' if File.extname(filename) == ''
|
418
|
-
|
419
|
-
FileUtils.mkdir_p File.dirname(filename) unless Dir.exist? File.dirname(filename)
|
420
|
-
File.open(filename, 'w') { |f| f << JSON.pretty_generate(to_static_data_point_hash(version)) }
|
421
|
-
|
422
|
-
true
|
423
|
-
end
|
424
|
-
|
425
|
-
# save the analysis zip file which contains the measures, seed model, weather file, and init/final scripts
|
426
|
-
#
|
427
|
-
# @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.
|
428
|
-
# @return [Boolean]
|
429
|
-
def save_zip(filename)
|
430
|
-
filename += '.zip' if File.extname(filename) == ''
|
431
|
-
|
432
|
-
FileUtils.mkdir_p File.dirname(filename) unless Dir.exist? File.dirname(filename)
|
433
|
-
|
434
|
-
save_analysis_zip(filename)
|
435
|
-
end
|
436
|
-
|
437
|
-
|
438
|
-
def save_osa_zip(filename, all_weather_files = false, all_seed_files = false)
|
439
|
-
filename += '.zip' if File.extname(filename) == ''
|
440
|
-
|
441
|
-
FileUtils.mkdir_p File.dirname(filename) unless Dir.exist? File.dirname(filename)
|
442
|
-
|
443
|
-
save_analysis_zip_osa(filename, all_weather_files, all_seed_files)
|
444
|
-
end
|
445
|
-
|
446
|
-
# convert an OSW to an OSA
|
447
|
-
# osw_filename is the full path to the OSW file
|
448
|
-
# assumes the associated files and directories are in the same location
|
449
|
-
# /example.osw
|
450
|
-
# /measures
|
451
|
-
# /seeds
|
452
|
-
# /weather
|
453
|
-
#
|
454
|
-
def convert_osw(osw_filename, *measure_paths)
|
455
|
-
# load OSW so we can loop over [:steps]
|
456
|
-
if File.exist? osw_filename #will this work for both rel and abs paths?
|
457
|
-
osw = JSON.parse(File.read(osw_filename), symbolize_names: true)
|
458
|
-
@osw_path = File.expand_path(osw_filename)
|
459
|
-
else
|
460
|
-
raise "Could not find workflow file #{osw_filename}"
|
461
|
-
end
|
462
|
-
|
463
|
-
# set the weather and seed files if set in OSW
|
464
|
-
# use :file_paths and look for files to set
|
465
|
-
if osw[:file_paths]
|
466
|
-
# seed_model, check if in OSW and not found in path search already
|
467
|
-
if osw[:seed_file]
|
468
|
-
osw[:file_paths].each do |path|
|
469
|
-
puts "searching for seed at: #{File.join(File.expand_path(path), osw[:seed_file])}"
|
470
|
-
if File.exist?(File.join(File.expand_path(path), osw[:seed_file]))
|
471
|
-
puts "found seed_file: #{osw[:seed_file]}"
|
472
|
-
self.seed_model = File.join(File.expand_path(path), osw[:seed_file])
|
473
|
-
break
|
474
|
-
end
|
475
|
-
end
|
476
|
-
else
|
477
|
-
warn "osw[:seed_file] is not defined"
|
478
|
-
end
|
479
|
-
|
480
|
-
# weather_file, check if in OSW and not found in path search already
|
481
|
-
if osw[:weather_file]
|
482
|
-
osw[:file_paths].each do |path|
|
483
|
-
puts "searching for weather at: #{File.join(File.expand_path(path), osw[:weather_file])}"
|
484
|
-
if File.exist?(File.join(File.expand_path(path), osw[:weather_file]))
|
485
|
-
puts "found weather_file: #{osw[:weather_file]}"
|
486
|
-
self.weather_file = File.join(File.expand_path(path), osw[:weather_file])
|
487
|
-
break
|
488
|
-
end
|
489
|
-
end
|
490
|
-
else
|
491
|
-
warn "osw[:weather_file] is not defined"
|
492
|
-
end
|
493
|
-
|
494
|
-
# file_paths is not defined in OSW, so warn and try to set
|
495
|
-
else
|
496
|
-
warn ":file_paths is not defined in the OSW."
|
497
|
-
self.weather_file = osw[:weather_file] ? osw[:weather_file] : nil
|
498
|
-
self.seed_model = osw[:seed_file] ? osw[:seed_file] : nil
|
499
|
-
end
|
500
|
-
|
501
|
-
#set analysis_type default to Single_Run
|
502
|
-
self.analysis_type = 'single_run'
|
503
|
-
|
504
|
-
#loop over OSW 'steps' and map over measures
|
505
|
-
#there is no name/display name in the OSW. Just measure directory name
|
506
|
-
#read measure.XML from directory to get name / display name
|
507
|
-
#increment name by +_1 if there are duplicates
|
508
|
-
#add measure
|
509
|
-
#change default args to osw arg values
|
510
|
-
|
511
|
-
osw[:steps].each do |step|
|
512
|
-
#get measure directory
|
513
|
-
measure_dir = step[:measure_dir_name]
|
514
|
-
measure_name = measure_dir.split("measures/").last
|
515
|
-
puts "measure_dir_name: #{measure_name}"
|
516
|
-
#get XML
|
517
|
-
# Loop over possible user defined *measure_paths, including the dir of the osw_filename path and :measure_paths, to find the measure,
|
518
|
-
# then set measure_dir_abs_path to that path
|
519
|
-
measure_dir_abs_path = ''
|
520
|
-
paths_to_parse = [File.dirname(osw_filename), osw[:measure_paths], *measure_paths].flatten.compact.map { |path| File.join(File.expand_path(path), measure_dir, 'measure.xml') }
|
521
|
-
puts "searching for xml's in: #{paths_to_parse}"
|
522
|
-
xml = {}
|
523
|
-
paths_to_parse.each do |path|
|
524
|
-
if File.exist?(path)
|
525
|
-
puts "found xml: #{path}"
|
526
|
-
xml = parse_measure_xml(path)
|
527
|
-
if !xml.empty?
|
528
|
-
measure_dir_abs_path = path
|
529
|
-
break
|
530
|
-
end
|
531
|
-
end
|
532
|
-
end
|
533
|
-
raise "measure #{measure_name} not found" if xml.empty?
|
534
|
-
puts ""
|
535
|
-
#add check for previous names _+1
|
536
|
-
count = 1
|
537
|
-
name = xml[:name]
|
538
|
-
display_name = xml[:display_name]
|
539
|
-
loop do
|
540
|
-
measure = @workflow.find_measure(name)
|
541
|
-
break if measure.nil?
|
542
|
-
|
543
|
-
count += 1
|
544
|
-
name = "#{xml[:name]}_#{count}"
|
545
|
-
display_name = "#{xml[:display_name]} #{count}"
|
546
|
-
end
|
547
|
-
#Add Measure to workflow
|
548
|
-
@workflow.add_measure_from_path(name, display_name, measure_dir_abs_path) #this forces to an absolute path which seems constent with PAT
|
549
|
-
#@workflow.add_measure_from_path(name, display_name, measure_dir) #this uses the path in the OSW which could be relative
|
550
|
-
|
551
|
-
#Change the default argument values to the osw values
|
552
|
-
#1. find measure in @workflow
|
553
|
-
m = @workflow.find_measure(name)
|
554
|
-
#2. loop thru osw args
|
555
|
-
#check if the :argument is missing from the measure step, it shouldnt be but just in case give a clean message
|
556
|
-
if step[:arguments].nil?
|
557
|
-
raise "measure #{name} step has no arguments: #{step}"
|
558
|
-
else
|
559
|
-
step[:arguments].each do |k,v|
|
560
|
-
#check if argument is in measure, otherwise setting argument_value will crash
|
561
|
-
raise "OSW arg: #{k} is not in Measure: #{name}" if m.arguments.find_all { |a| a[:name] == k.to_s }.empty?
|
562
|
-
#set measure arg to match osw arg
|
563
|
-
m.argument_value(k.to_s, v)
|
564
|
-
end
|
565
|
-
end
|
566
|
-
end
|
567
|
-
end
|
568
|
-
|
569
|
-
private
|
570
|
-
|
571
|
-
# New format for OSAs. Package up the seed, weather files, and measures
|
572
|
-
# filename is the name of the file to be saved. ex: analysis.zip
|
573
|
-
# it will parse the OSA and zip up all the files defined in the workflow
|
574
|
-
def save_analysis_zip_osa(filename, all_weather_files = false, all_seed_files = false)
|
575
|
-
def add_directory_to_zip_osa(zipfile, local_directory, relative_zip_directory)
|
576
|
-
puts "Add Directory #{local_directory}"
|
577
|
-
Dir[File.join(local_directory.to_s, '**', '**')].each do |file|
|
578
|
-
puts "Adding File #{file}"
|
579
|
-
zipfile.add(file.sub(local_directory, relative_zip_directory), file)
|
580
|
-
end
|
581
|
-
zipfile
|
582
|
-
end
|
583
|
-
#delete file if exists
|
584
|
-
FileUtils.rm_f(filename) if File.exist?(filename)
|
585
|
-
#get the full path to the OSW, since all Files/Dirs should be in same directory as the OSW
|
586
|
-
puts "osw_path: #{@osw_path}"
|
587
|
-
osw_full_path = File.dirname(File.expand_path(@osw_path))
|
588
|
-
puts "osw_full_path: #{osw_full_path}"
|
589
|
-
|
590
|
-
Zip::File.open(filename, create: true) do |zf|
|
591
|
-
## Weather files
|
592
|
-
puts 'Adding Support Files: Weather'
|
593
|
-
# check if weather file exists. use abs path. remove leading ./ from @weather_file path if there.
|
594
|
-
# check if path is already absolute
|
595
|
-
if @weather_file[:file]
|
596
|
-
if File.exists?(@weather_file[:file])
|
597
|
-
puts " Adding #{@weather_file[:file]}"
|
598
|
-
#zf.add("weather/#{File.basename(@weather_file[:file])}", @weather_file[:file])
|
599
|
-
base_name = File.basename(@weather_file[:file], ".*")
|
600
|
-
puts "base_name: #{base_name}"
|
601
|
-
# convert backslash on windows to forward slash so Dir.glob will work (in case user uses \)
|
602
|
-
weather_dirname = File.dirname(@weather_file[:file]).gsub("\\", "/")
|
603
|
-
puts "weather_dirname: #{weather_dirname}"
|
604
|
-
# If all_weather_files is true, add all files in the directory to the zip.
|
605
|
-
# Otherwise, add only files that match the base name.
|
606
|
-
file_pattern = all_weather_files ? "*" : "#{base_name}.*"
|
607
|
-
Dir.glob(File.join(weather_dirname, file_pattern)) do |file_path|
|
608
|
-
puts "file_path: #{file_path}"
|
609
|
-
puts "zip path: weather/#{File.basename(file_path)}"
|
610
|
-
zf.add("weather/#{File.basename(file_path)}", file_path)
|
611
|
-
end
|
612
|
-
# make absolute path and check for file
|
613
|
-
elsif File.exists?(File.join(osw_full_path,@weather_file[:file].sub(/^\.\//, '')))
|
614
|
-
puts " Adding: #{File.join(osw_full_path,@weather_file[:file].sub(/^\.\//, ''))}"
|
615
|
-
#zf.add("weather/#{File.basename(@weather_file[:file])}", File.join(osw_full_path,@weather_file[:file].sub(/^\.\//, '')))
|
616
|
-
base_name = File.basename(@weather_file[:file].sub(/^\.\//, ''), ".*")
|
617
|
-
puts "base_name2: #{base_name}"
|
618
|
-
weather_dirname = File.dirname(File.join(osw_full_path,@weather_file[:file].sub(/^\.\//, ''))).gsub("\\", "/")
|
619
|
-
puts "weather_dirname: #{weather_dirname}"
|
620
|
-
file_pattern = all_weather_files ? "*" : "#{base_name}.*"
|
621
|
-
Dir.glob(File.join(weather_dirname, file_pattern)) do |file_path|
|
622
|
-
puts "file_path2: #{file_path}"
|
623
|
-
puts "zip path2: weather/#{File.basename(file_path)}"
|
624
|
-
zf.add("weather/#{File.basename(file_path)}", file_path)
|
625
|
-
end
|
626
|
-
else
|
627
|
-
raise "weather_file[:file] does not exist at: #{File.join(osw_full_path,@weather_file[:file].sub(/^\.\//, ''))}"
|
628
|
-
end
|
629
|
-
else
|
630
|
-
warn "weather_file[:file] is not defined"
|
631
|
-
end
|
632
|
-
|
633
|
-
## Seed files
|
634
|
-
puts 'Adding Support Files: Seed Models'
|
635
|
-
#check if seed file exists. use abs path. remove leading ./ from @seed_model path if there.
|
636
|
-
#check if path is already absolute
|
637
|
-
if @seed_model[:file]
|
638
|
-
if File.exists?(@seed_model[:file])
|
639
|
-
puts " Adding #{@seed_model[:file]}"
|
640
|
-
zf.add("seeds/#{File.basename(@seed_model[:file])}", @seed_model[:file])
|
641
|
-
if all_seed_files
|
642
|
-
seed_dirname = File.dirname(@seed_model[:file]).gsub("\\", "/")
|
643
|
-
puts "seed_dirname: #{seed_dirname}"
|
644
|
-
Dir.glob(File.join(seed_dirname, '*')) do |file_path|
|
645
|
-
next if file_path == @seed_model[:file] # Skip if the file is the same as @seed_model[:file] so not added twice
|
646
|
-
puts "file_path: #{file_path}"
|
647
|
-
puts "zip path: seeds/#{File.basename(file_path)}"
|
648
|
-
zf.add("seeds/#{File.basename(file_path)}", file_path)
|
649
|
-
end
|
650
|
-
end
|
651
|
-
#make absolute path and check for file
|
652
|
-
elsif File.exists?(File.join(osw_full_path,@seed_model[:file].sub(/^\.\//, '')))
|
653
|
-
puts " Adding #{File.join(osw_full_path,@seed_model[:file].sub(/^\.\//, ''))}"
|
654
|
-
zf.add("seeds/#{File.basename(@seed_model[:file])}", File.join(osw_full_path,@seed_model[:file].sub(/^\.\//, '')))
|
655
|
-
if all_seed_files
|
656
|
-
seed_dirname = File.dirname(File.join(osw_full_path,@seed_model[:file].sub(/^\.\//, ''))).gsub("\\", "/")
|
657
|
-
puts "seed_dirname: #{seed_dirname}"
|
658
|
-
Dir.glob(File.join(seed_dirname, '*')) do |file_path|
|
659
|
-
next if file_path == File.join(osw_full_path,@seed_model[:file].sub(/^\.\//, '')) # Skip if the file is the same as @seed_model[:file] so not added twice
|
660
|
-
puts "file_path: #{file_path}"
|
661
|
-
puts "zip path: seeds/#{File.basename(file_path)}"
|
662
|
-
zf.add("seeds/#{File.basename(file_path)}", file_path)
|
663
|
-
end
|
664
|
-
end
|
665
|
-
else
|
666
|
-
raise "seed_file[:file] does not exist at: #{File.join(osw_full_path,@seed_model[:file].sub(/^\.\//, ''))}"
|
667
|
-
end
|
668
|
-
else
|
669
|
-
warn "seed_file[:file] is not defined"
|
670
|
-
end
|
671
|
-
|
672
|
-
puts 'Adding Support Files: Libraries'
|
673
|
-
@libraries.each do |lib|
|
674
|
-
raise "Libraries must specify their 'library_name' as metadata which becomes the directory upon zip" unless lib[:metadata][:library_name]
|
675
|
-
|
676
|
-
if File.directory? lib[:file]
|
677
|
-
Dir[File.join(lib[:file], '**', '**')].each do |file|
|
678
|
-
puts " Adding #{file}"
|
679
|
-
zf.add(file.sub(lib[:file], "lib/#{lib[:metadata][:library_name]}"), file)
|
680
|
-
end
|
681
|
-
else
|
682
|
-
# just add the file to the zip
|
683
|
-
puts " Adding #{lib[:file]}"
|
684
|
-
zf.add(lib[:file], "lib/#{File.basename(lib[:file])}", lib[:file])
|
685
|
-
end
|
686
|
-
end
|
687
|
-
|
688
|
-
puts 'Adding Support Files: Server Scripts'
|
689
|
-
@server_scripts.each_with_index do |f, index|
|
690
|
-
if f[:init_or_final] == 'finalization'
|
691
|
-
file_name = 'finalization.sh'
|
692
|
-
else
|
693
|
-
file_name = 'initialization.sh'
|
694
|
-
end
|
695
|
-
if f[:server_or_data_point] == 'analysis'
|
696
|
-
new_name = "scripts/analysis/#{file_name}"
|
697
|
-
else
|
698
|
-
new_name = "scripts/data_point/#{file_name}"
|
699
|
-
end
|
700
|
-
puts " Adding #{f[:file]} as #{new_name}"
|
701
|
-
zf.add(new_name, f[:file])
|
702
|
-
|
703
|
-
if f[:arguments]
|
704
|
-
arg_file = "#{(new_name.sub(/\.sh\z/, ''))}.args"
|
705
|
-
puts " Adding arguments as #{arg_file}"
|
706
|
-
file = Tempfile.new('arg')
|
707
|
-
file.write(f[:arguments])
|
708
|
-
zf.add(arg_file, file)
|
709
|
-
file.close
|
710
|
-
end
|
711
|
-
end
|
712
|
-
|
713
|
-
## Measures
|
714
|
-
puts 'Adding Measures'
|
715
|
-
added_measures = []
|
716
|
-
# The list of the measures should always be there, but make sure they are uniq
|
717
|
-
@workflow.each do |measure|
|
718
|
-
measure_dir_to_add = measure.measure_definition_directory_local
|
719
|
-
|
720
|
-
next if added_measures.include? measure_dir_to_add
|
721
|
-
|
722
|
-
puts " Adding #{File.basename(measure_dir_to_add)}"
|
723
|
-
Dir[File.join(measure_dir_to_add, '**')].each do |file|
|
724
|
-
if File.directory?(file)
|
725
|
-
if File.basename(file) == 'resources' || File.basename(file) == 'lib'
|
726
|
-
#remove leading ./ from measure_definition_directory path if there.
|
727
|
-
add_directory_to_zip_osa(zf, file, "#{measure.measure_definition_directory.sub(/^\.\//, '')}/#{File.basename(file)}")
|
728
|
-
end
|
729
|
-
else
|
730
|
-
puts " Adding File #{file}"
|
731
|
-
#remove leading ./ from measure.measure_definition_directory string with regex .sub(/^\.\//, '')
|
732
|
-
zip_path_for_measures = file.sub(measure_dir_to_add, measure.measure_definition_directory.sub(/^\.\//, ''))
|
733
|
-
#puts " zip_path_for_measures: #{zip_path_for_measures}"
|
734
|
-
zf.add(zip_path_for_measures, file)
|
735
|
-
end
|
736
|
-
end
|
737
|
-
|
738
|
-
added_measures << measure_dir_to_add
|
739
|
-
end
|
740
|
-
end
|
741
|
-
end
|
742
|
-
|
743
|
-
#keep legacy function
|
744
|
-
# Package up the seed, weather files, and measures
|
745
|
-
def save_analysis_zip(filename)
|
746
|
-
def add_directory_to_zip(zipfile, local_directory, relative_zip_directory)
|
747
|
-
# puts "Add Directory #{local_directory}"
|
748
|
-
Dir[File.join(local_directory.to_s, '**', '**')].each do |file|
|
749
|
-
# puts "Adding File #{file}"
|
750
|
-
zipfile.add(file.sub(local_directory, relative_zip_directory), file)
|
751
|
-
end
|
752
|
-
zipfile
|
753
|
-
end
|
754
|
-
|
755
|
-
FileUtils.rm_f(filename) if File.exist?(filename)
|
756
|
-
|
757
|
-
Zip::File.open(filename, Zip::File::CREATE) do |zf|
|
758
|
-
## Weather files
|
759
|
-
# TODO: eventually remove the @weather_file attribute and grab the weather file out
|
760
|
-
# of the @weather_files
|
761
|
-
puts 'Adding Support Files: Weather'
|
762
|
-
if @weather_file[:file] && !@weather_files.files.find { |f| @weather_file[:file] == f[:file] }
|
763
|
-
# manually add the weather file
|
764
|
-
puts " Adding #{@weather_file[:file]}"
|
765
|
-
zf.add("./weather/#{File.basename(@weather_file[:file])}", @weather_file[:file])
|
766
|
-
end
|
767
|
-
@weather_files.each do |f|
|
768
|
-
puts " Adding #{f[:file]}"
|
769
|
-
zf.add("./weather/#{File.basename(f[:file])}", f[:file])
|
770
|
-
end
|
771
|
-
|
772
|
-
## Seed files
|
773
|
-
puts 'Adding Support Files: Seed Models'
|
774
|
-
if @seed_model[:file] && !@seed_models.files.find { |f| @seed_model[:file] == f[:file] }
|
775
|
-
# manually add the weather file
|
776
|
-
puts " Adding #{@seed_model[:file]}"
|
777
|
-
zf.add("./seed/#{File.basename(@seed_model[:file])}", @seed_model[:file])
|
778
|
-
end
|
779
|
-
@seed_models.each do |f|
|
780
|
-
puts " Adding #{f[:file]}"
|
781
|
-
zf.add("./seed/#{File.basename(f[:file])}", f[:file])
|
782
|
-
end
|
783
|
-
|
784
|
-
puts 'Adding Support Files: Libraries'
|
785
|
-
@libraries.each do |lib|
|
786
|
-
raise "Libraries must specify their 'library_name' as metadata which becomes the directory upon zip" unless lib[:metadata][:library_name]
|
787
|
-
|
788
|
-
if File.directory? lib[:file]
|
789
|
-
Dir[File.join(lib[:file], '**', '**')].each do |file|
|
790
|
-
puts " Adding #{file}"
|
791
|
-
zf.add(file.sub(lib[:file], "./lib/#{lib[:metadata][:library_name]}/"), file)
|
792
|
-
end
|
793
|
-
else
|
794
|
-
# just add the file to the zip
|
795
|
-
puts " Adding #{lib[:file]}"
|
796
|
-
zf.add(lib[:file], "./lib/#{File.basename(lib[:file])}", lib[:file])
|
797
|
-
end
|
798
|
-
end
|
799
|
-
|
800
|
-
puts 'Adding Support Files: Worker Initialization Scripts'
|
801
|
-
@worker_inits.each_with_index do |f, index|
|
802
|
-
ordered_file_name = "#{index.to_s.rjust(2, '0')}_#{File.basename(f[:file])}"
|
803
|
-
puts " Adding #{f[:file]} as #{ordered_file_name}"
|
804
|
-
zf.add(f[:file].sub(f[:file], "./scripts/worker_initialization//#{ordered_file_name}"), f[:file])
|
805
|
-
|
806
|
-
if f[:metadata][:args]
|
807
|
-
arg_file = "#{File.basename(ordered_file_name, '.*')}.args"
|
808
|
-
file = Tempfile.new('arg')
|
809
|
-
file.write(f[:metadata][:args])
|
810
|
-
zf.add("./scripts/worker_initialization/#{arg_file}", file)
|
811
|
-
file.close
|
812
|
-
end
|
813
|
-
end
|
814
|
-
|
815
|
-
puts 'Adding Support Files: Worker Finalization Scripts'
|
816
|
-
@worker_finalizes.each_with_index do |f, index|
|
817
|
-
ordered_file_name = "#{index.to_s.rjust(2, '0')}_#{File.basename(f[:file])}"
|
818
|
-
puts " Adding #{f[:file]} as #{ordered_file_name}"
|
819
|
-
zf.add(f[:file].sub(f[:file], "scripts/worker_finalization/#{ordered_file_name}"), f[:file])
|
820
|
-
|
821
|
-
if f[:metadata][:args]
|
822
|
-
arg_file = "#{File.basename(ordered_file_name, '.*')}.args"
|
823
|
-
file = Tempfile.new('arg')
|
824
|
-
file.write(f[:metadata][:args])
|
825
|
-
zf.add("scripts/worker_finalization/#{arg_file}", file)
|
826
|
-
file.close
|
827
|
-
end
|
828
|
-
end
|
829
|
-
|
830
|
-
## Measures
|
831
|
-
puts 'Adding Measures'
|
832
|
-
added_measures = []
|
833
|
-
# The list of the measures should always be there, but make sure they are uniq
|
834
|
-
@workflow.each do |measure|
|
835
|
-
measure_dir_to_add = measure.measure_definition_directory_local
|
836
|
-
|
837
|
-
next if added_measures.include? measure_dir_to_add
|
838
|
-
|
839
|
-
puts " Adding #{File.basename(measure_dir_to_add)}"
|
840
|
-
Dir[File.join(measure_dir_to_add, '**')].each do |file|
|
841
|
-
if File.directory?(file)
|
842
|
-
if File.basename(file) == 'resources' || File.basename(file) == 'lib'
|
843
|
-
add_directory_to_zip(zf, file, "#{measure.measure_definition_directory}/#{File.basename(file)}")
|
844
|
-
end
|
845
|
-
else
|
846
|
-
# puts "Adding File #{file}"
|
847
|
-
zf.add(file.sub(measure_dir_to_add, "#{measure.measure_definition_directory}/"), file)
|
848
|
-
end
|
849
|
-
end
|
850
|
-
|
851
|
-
added_measures << measure_dir_to_add
|
852
|
-
end
|
853
|
-
end
|
854
|
-
end
|
855
|
-
end
|
856
|
-
end
|
857
|
-
end
|
1
|
+
# *******************************************************************************
|
2
|
+
# OpenStudio(R), Copyright (c) Alliance for Sustainable Energy, LLC.
|
3
|
+
# See also https://openstudio.net/license
|
4
|
+
# *******************************************************************************
|
5
|
+
|
6
|
+
# OpenStudio formulation class handles the generation of the OpenStudio Analysis format.
|
7
|
+
module OpenStudio
|
8
|
+
module Analysis
|
9
|
+
SeedModel = Struct.new(:file)
|
10
|
+
WeatherFile = Struct.new(:file)
|
11
|
+
|
12
|
+
@@measure_paths = ['./measures']
|
13
|
+
# List of paths to look for measures when adding them. This currently only is used when loading an
|
14
|
+
# analysis hash file. It looks in the order of the measure_paths. As soon as it finds one, it stops.
|
15
|
+
def self.measure_paths
|
16
|
+
@@measure_paths
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.measure_paths=(new_array)
|
20
|
+
@@measure_paths = new_array
|
21
|
+
end
|
22
|
+
|
23
|
+
class Formulation
|
24
|
+
attr_reader :seed_model
|
25
|
+
attr_reader :weather_file
|
26
|
+
attr_reader :analysis_type
|
27
|
+
attr_reader :outputs
|
28
|
+
attr_accessor :display_name
|
29
|
+
attr_accessor :workflow
|
30
|
+
attr_accessor :algorithm
|
31
|
+
attr_accessor :osw_path
|
32
|
+
attr_accessor :download_zip
|
33
|
+
attr_accessor :download_reports
|
34
|
+
attr_accessor :download_osw
|
35
|
+
attr_accessor :download_osm
|
36
|
+
attr_accessor :cli_debug
|
37
|
+
attr_accessor :cli_verbose
|
38
|
+
attr_accessor :initialize_worker_timeout
|
39
|
+
attr_accessor :run_workflow_timeout
|
40
|
+
attr_accessor :upload_results_timeout
|
41
|
+
|
42
|
+
# the attributes below are used for packaging data into the analysis zip file
|
43
|
+
attr_reader :weather_files
|
44
|
+
attr_reader :seed_models
|
45
|
+
attr_reader :worker_inits
|
46
|
+
attr_reader :worker_finalizes
|
47
|
+
attr_reader :libraries
|
48
|
+
attr_reader :server_scripts
|
49
|
+
|
50
|
+
# Create an instance of the OpenStudio::Analysis::Formulation
|
51
|
+
#
|
52
|
+
# @param display_name [String] Display name of the project.
|
53
|
+
# @return [Object] An OpenStudio::Analysis::Formulation object
|
54
|
+
def initialize(display_name)
|
55
|
+
@display_name = display_name
|
56
|
+
@analysis_type = nil
|
57
|
+
@outputs = []
|
58
|
+
@workflow = OpenStudio::Analysis::Workflow.new
|
59
|
+
# Initialize child objects (expect workflow)
|
60
|
+
@weather_file = WeatherFile.new
|
61
|
+
@seed_model = SeedModel.new
|
62
|
+
@algorithm = OpenStudio::Analysis::AlgorithmAttributes.new
|
63
|
+
@download_zip = true
|
64
|
+
@download_reports = true
|
65
|
+
@download_osw = true
|
66
|
+
@download_osm = true
|
67
|
+
@cli_debug = "--debug"
|
68
|
+
@cli_verbose = "--verbose"
|
69
|
+
@initialize_worker_timeout = 28800
|
70
|
+
@run_workflow_timeout = 28800
|
71
|
+
@upload_results_timeout = 28800
|
72
|
+
|
73
|
+
# Analysis Zip attributes
|
74
|
+
@weather_files = SupportFiles.new
|
75
|
+
@seed_models = SupportFiles.new
|
76
|
+
@worker_inits = SupportFiles.new
|
77
|
+
@worker_finalizes = SupportFiles.new
|
78
|
+
@libraries = SupportFiles.new
|
79
|
+
@server_scripts = ServerScripts.new
|
80
|
+
end
|
81
|
+
|
82
|
+
# Define the type of analysis which is going to be running
|
83
|
+
#
|
84
|
+
# @param name [String] Name of the algorithm/analysis. (e.g. rgenoud, lhs, single_run)
|
85
|
+
# allowed values are ANALYSIS_TYPES = ['spea_nrel', 'rgenoud', 'nsga_nrel', 'lhs', 'preflight',
|
86
|
+
# 'morris', 'sobol', 'doe', 'fast99', 'ga', 'gaisl',
|
87
|
+
# 'single_run', 'repeat_run', 'batch_run']
|
88
|
+
def analysis_type=(value)
|
89
|
+
if OpenStudio::Analysis::AlgorithmAttributes::ANALYSIS_TYPES.include?(value)
|
90
|
+
@analysis_type = value
|
91
|
+
else
|
92
|
+
raise "Invalid analysis type. Allowed types: #{OpenStudio::Analysis::AlgorithmAttributes::ANALYSIS_TYPES}"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Path to the seed model
|
97
|
+
#
|
98
|
+
# @param path [String] Path to the seed model. This should be relative.
|
99
|
+
def seed_model=(file)
|
100
|
+
@seed_model[:file] = file
|
101
|
+
end
|
102
|
+
|
103
|
+
# Path to the weather file (or folder). If it is a folder, then the measures will look for the weather file
|
104
|
+
# by name in that folder.
|
105
|
+
#
|
106
|
+
# @param path [String] Path to the weather file or folder.
|
107
|
+
def weather_file=(file)
|
108
|
+
@weather_file[:file] = file
|
109
|
+
end
|
110
|
+
|
111
|
+
# Set the value for 'download_zip'
|
112
|
+
#
|
113
|
+
# @param value [Boolean] The value for 'download_zip'
|
114
|
+
def download_zip=(value)
|
115
|
+
if [true, false].include?(value)
|
116
|
+
@download_zip = value
|
117
|
+
else
|
118
|
+
raise ArgumentError, "Invalid value for 'download_zip'. Only true or false allowed."
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Set the value for 'download_reports'
|
123
|
+
#
|
124
|
+
# @param value [Boolean] The value for 'download_reports'
|
125
|
+
def download_reports=(value)
|
126
|
+
if [true, false].include?(value)
|
127
|
+
@download_reports = value
|
128
|
+
else
|
129
|
+
raise ArgumentError, "Invalid value for 'download_reports'. Only true or false allowed."
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# Set the value for 'download_osw'
|
134
|
+
#
|
135
|
+
# @param value [Boolean] The value for 'download_osw'
|
136
|
+
def download_osw=(value)
|
137
|
+
if [true, false].include?(value)
|
138
|
+
@download_osw = value
|
139
|
+
else
|
140
|
+
raise ArgumentError, "Invalid value for 'download_osw'. Only true or false allowed."
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# Set the value for 'download_osm'
|
145
|
+
#
|
146
|
+
# @param value [Boolean] The value for 'download_osm'
|
147
|
+
def download_osm=(value)
|
148
|
+
if [true, false].include?(value)
|
149
|
+
@download_osm = value
|
150
|
+
else
|
151
|
+
raise ArgumentError, "Invalid value for 'download_osm'. Only true or false allowed."
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Set the value for 'cli_debug'
|
156
|
+
#
|
157
|
+
# @param value [Boolean] The value for 'cli_debug'
|
158
|
+
def cli_debug=(value)
|
159
|
+
@cli_debug = value
|
160
|
+
end
|
161
|
+
|
162
|
+
# Set the value for 'cli_verbose'
|
163
|
+
#
|
164
|
+
# @param value [Boolean] The value for 'cli_verbose'
|
165
|
+
def cli_verbose=(value)
|
166
|
+
@cli_verbose = value
|
167
|
+
end
|
168
|
+
|
169
|
+
# Set the value for 'run_workflow_timeout'
|
170
|
+
#
|
171
|
+
# @param value [Integer] The value for 'run_workflow_timeout'
|
172
|
+
def run_workflow_timeout=(value)
|
173
|
+
if value.is_a?(Integer)
|
174
|
+
@run_workflow_timeout = value
|
175
|
+
else
|
176
|
+
raise ArgumentError, "Invalid value for 'run_workflow_timeout'. Only integer values allowed."
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# Set the value for 'initialize_worker_timeout'
|
181
|
+
#
|
182
|
+
# @param value [Integer] The value for 'initialize_worker_timeout'
|
183
|
+
def initialize_worker_timeout=(value)
|
184
|
+
if value.is_a?(Integer)
|
185
|
+
@initialize_worker_timeout = value
|
186
|
+
else
|
187
|
+
raise ArgumentError, "Invalid value for 'initialize_worker_timeout'. Only integer values allowed."
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# Set the value for 'upload_results_timeout'
|
192
|
+
#
|
193
|
+
# @param value [Integer] The value for 'upload_results_timeout'
|
194
|
+
def upload_results_timeout=(value)
|
195
|
+
if value.is_a?(Integer)
|
196
|
+
@upload_results_timeout = value
|
197
|
+
else
|
198
|
+
raise ArgumentError, "Invalid value for 'upload_results_timeout'. Only integer values allowed."
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
# Add an output of interest to the problem formulation
|
203
|
+
#
|
204
|
+
# @param output_hash [Hash] Hash of the output variable in the legacy format
|
205
|
+
# @option output_hash [String] :display_name Name to display
|
206
|
+
# @option output_hash [String] :display_name_short A shorter display name
|
207
|
+
# @option output_hash [String] :metadata_id Link to DEnCity ID in which this output corresponds
|
208
|
+
# @option output_hash [String] :name Unique machine name of the variable. Typically this is measure.attribute
|
209
|
+
# @option output_hash [String] :export Export the variable to CSV and dataframes from OpenStudio-server
|
210
|
+
# @option output_hash [String] :visualize Visualize the variable in the plots on OpenStudio-server
|
211
|
+
# @option output_hash [String] :units Units of the variable as a string
|
212
|
+
# @option output_hash [String] :variable_type Data type of the variable
|
213
|
+
# @option output_hash [Boolean] :objective_function Whether or not this output is an objective function. Default: false
|
214
|
+
# @option output_hash [Integer] :objective_function_index Index of the objective function. Default: nil
|
215
|
+
# @option output_hash [Float] :objective_function_target Target for the objective function to reach (if defined). Default: nil
|
216
|
+
# @option output_hash [Float] :scaling_factor How to scale the objective function(s). Default: nil
|
217
|
+
# @option output_hash [Integer] :objective_function_group If grouping objective functions, then group ID. Default: nil
|
218
|
+
def add_output(output_hash)
|
219
|
+
# Check if the name is already been added.
|
220
|
+
exist = @outputs.find_index { |o| o[:name] == output_hash[:name] }
|
221
|
+
# if so, update the fields but keep objective_function_index the same
|
222
|
+
if exist
|
223
|
+
original = @outputs[exist]
|
224
|
+
if original[:objective_function] && !output_hash[:objective_function]
|
225
|
+
return @outputs
|
226
|
+
end
|
227
|
+
output = original.merge(output_hash)
|
228
|
+
output[:objective_function_index] = original[:objective_function_index]
|
229
|
+
@outputs[exist] = output
|
230
|
+
else
|
231
|
+
output = {
|
232
|
+
units: '',
|
233
|
+
objective_function: false,
|
234
|
+
objective_function_index: nil,
|
235
|
+
objective_function_target: nil,
|
236
|
+
#set default to nil or 1 if objective_function is true and this is not set
|
237
|
+
objective_function_group: (output_hash[:objective_function] ? 1 : nil),
|
238
|
+
scaling_factor: nil,
|
239
|
+
#set default to false or true if objective_function is true and this is not set
|
240
|
+
visualize: (output_hash[:objective_function] ? true : false),
|
241
|
+
metadata_id: nil,
|
242
|
+
export: true,
|
243
|
+
}.merge(output_hash)
|
244
|
+
#set display_name default to be name if its not set
|
245
|
+
output[:display_name] = output_hash[:display_name] ? output_hash[:display_name] : output_hash[:name]
|
246
|
+
#set display_name_short default to be display_name if its not set, this can be null if :display_name not set
|
247
|
+
output[:display_name_short] = output_hash[:display_name_short] ? output_hash[:display_name_short] : output_hash[:display_name]
|
248
|
+
# if the variable is an objective_function, then increment and
|
249
|
+
# assign and objective function index
|
250
|
+
if output[:objective_function]
|
251
|
+
values = @outputs.select { |o| o[:objective_function] }
|
252
|
+
output[:objective_function_index] = values.size
|
253
|
+
end
|
254
|
+
|
255
|
+
@outputs << output
|
256
|
+
end
|
257
|
+
|
258
|
+
@outputs
|
259
|
+
end
|
260
|
+
|
261
|
+
# return the machine name of the analysis
|
262
|
+
def name
|
263
|
+
@display_name.to_underscore
|
264
|
+
end
|
265
|
+
|
266
|
+
# return a hash.
|
267
|
+
#
|
268
|
+
# @param version [Integer] Version of the format to return
|
269
|
+
# @return [Hash]
|
270
|
+
def to_hash(version = 1)
|
271
|
+
# fail 'Must define an analysis type' unless @analysis_type
|
272
|
+
if version == 1
|
273
|
+
h = {
|
274
|
+
analysis: {
|
275
|
+
display_name: @display_name,
|
276
|
+
name: name,
|
277
|
+
output_variables: @outputs,
|
278
|
+
problem: {
|
279
|
+
analysis_type: @analysis_type,
|
280
|
+
algorithm: algorithm.to_hash(version),
|
281
|
+
workflow: workflow.to_hash(version)
|
282
|
+
}
|
283
|
+
}
|
284
|
+
}
|
285
|
+
|
286
|
+
if @seed_model[:file]
|
287
|
+
h[:analysis][:seed] = {
|
288
|
+
file_type: File.extname(@seed_model[:file]).delete('.').upcase,
|
289
|
+
path: "./seed/#{File.basename(@seed_model[:file])}"
|
290
|
+
}
|
291
|
+
else
|
292
|
+
h[:analysis][:seed] = nil
|
293
|
+
end
|
294
|
+
|
295
|
+
# silly catch for if weather_file is not set
|
296
|
+
wf = nil
|
297
|
+
if @weather_file[:file]
|
298
|
+
wf = @weather_file
|
299
|
+
elsif !@weather_files.empty?
|
300
|
+
# get the first EPW file (not the first file)
|
301
|
+
wf = @weather_files.find { |w| File.extname(w[:file]).casecmp('.epw').zero? }
|
302
|
+
end
|
303
|
+
|
304
|
+
if wf
|
305
|
+
h[:analysis][:weather_file] = {
|
306
|
+
file_type: File.extname(wf[:file]).delete('.').upcase,
|
307
|
+
path: "./weather/#{File.basename(wf[:file])}"
|
308
|
+
}
|
309
|
+
else
|
310
|
+
# log: could not find weather file
|
311
|
+
warn 'Could not resolve a valid weather file. Check paths to weather files'
|
312
|
+
end
|
313
|
+
|
314
|
+
h[:analysis][:file_format_version] = version
|
315
|
+
h[:analysis][:cli_debug] = @cli_debug
|
316
|
+
h[:analysis][:cli_verbose] = @cli_verbose
|
317
|
+
h[:analysis][:run_workflow_timeout] = @run_workflow_timeout
|
318
|
+
h[:analysis][:upload_results_timeout] = @upload_results_timeout
|
319
|
+
h[:analysis][:initialize_worker_timeout] = @initialize_worker_timeout
|
320
|
+
h[:analysis][:download_zip] = @download_zip
|
321
|
+
h[:analysis][:download_reports] = @download_reports
|
322
|
+
h[:analysis][:download_osw] = @download_osw
|
323
|
+
h[:analysis][:download_osm] = @download_osm
|
324
|
+
|
325
|
+
#-BLB I dont think this does anything. server_scripts are run if they are in
|
326
|
+
#the /scripts/analysis or /scripts/data_point directories
|
327
|
+
#but nothing is ever checked in the OSA.
|
328
|
+
#
|
329
|
+
h[:analysis][:server_scripts] = {}
|
330
|
+
|
331
|
+
# This is a hack right now, but after the initial hash is created go back and add in the objective functions
|
332
|
+
# to the the algorithm as defined in the output_variables list
|
333
|
+
ofs = @outputs.map { |i| i[:name] if i[:objective_function] }.compact
|
334
|
+
if h[:analysis][:problem][:algorithm]
|
335
|
+
h[:analysis][:problem][:algorithm][:objective_functions] = ofs
|
336
|
+
end
|
337
|
+
|
338
|
+
h
|
339
|
+
else
|
340
|
+
raise "Version #{version} not defined for #{self.class} and #{__method__}"
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
# Load the analysis JSON from a hash (with symbolized keys)
|
345
|
+
def self.from_hash(h, seed_dir = nil, weather_dir = nil)
|
346
|
+
o = OpenStudio::Analysis::Formulation.new(h[:analysis][:display_name])
|
347
|
+
|
348
|
+
version = 1
|
349
|
+
if version == 1
|
350
|
+
h[:analysis][:output_variables].each do |ov|
|
351
|
+
o.add_output(ov)
|
352
|
+
end
|
353
|
+
|
354
|
+
o.workflow = OpenStudio::Analysis::Workflow.load(workflow: h[:analysis][:problem][:workflow])
|
355
|
+
|
356
|
+
if weather_dir
|
357
|
+
o.weather_file "#{weather_path}/#{File.basename(h[:analysis][:weather_file][:path])}"
|
358
|
+
else
|
359
|
+
o.weather_file = h[:analysis][:weather_file][:path]
|
360
|
+
end
|
361
|
+
|
362
|
+
if seed_dir
|
363
|
+
o.seed_model "#{weather_path}/#{File.basename(h[:analysis][:seed][:path])}"
|
364
|
+
else
|
365
|
+
o.seed_model = h[:analysis][:seed][:path]
|
366
|
+
end
|
367
|
+
else
|
368
|
+
raise "Version #{version} not defined for #{self.class} and #{__method__}"
|
369
|
+
end
|
370
|
+
|
371
|
+
o
|
372
|
+
end
|
373
|
+
|
374
|
+
# return a hash of the data point with the static variables set
|
375
|
+
#
|
376
|
+
# @param version [Integer] Version of the format to return
|
377
|
+
# @return [Hash]
|
378
|
+
def to_static_data_point_hash(version = 1)
|
379
|
+
if version == 1
|
380
|
+
static_hash = {}
|
381
|
+
# TODO: this method should be on the workflow step and bubbled up to this interface
|
382
|
+
@workflow.items.map do |item|
|
383
|
+
item.variables.map { |v| static_hash[v[:uuid]] = v[:static_value] }
|
384
|
+
end
|
385
|
+
|
386
|
+
h = {
|
387
|
+
data_point: {
|
388
|
+
set_variable_values: static_hash,
|
389
|
+
status: 'na',
|
390
|
+
uuid: SecureRandom.uuid
|
391
|
+
}
|
392
|
+
}
|
393
|
+
h
|
394
|
+
end
|
395
|
+
end
|
396
|
+
|
397
|
+
# save the file to JSON. Will overwrite the file if it already exists
|
398
|
+
#
|
399
|
+
# @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.
|
400
|
+
# @param version [Integer] Version of the format to return
|
401
|
+
# @return [Boolean]
|
402
|
+
def save(filename, version = 1)
|
403
|
+
filename += '.json' if File.extname(filename) == ''
|
404
|
+
|
405
|
+
FileUtils.mkdir_p File.dirname(filename) unless Dir.exist? File.dirname(filename)
|
406
|
+
File.open(filename, 'w') { |f| f << JSON.pretty_generate(to_hash(version)) }
|
407
|
+
|
408
|
+
true
|
409
|
+
end
|
410
|
+
|
411
|
+
# save the data point JSON with the variables set to the static values. Will overwrite the file if it already exists
|
412
|
+
#
|
413
|
+
# @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.
|
414
|
+
# @param version [Integer] Version of the format to return
|
415
|
+
# @return [Boolean]
|
416
|
+
def save_static_data_point(filename, version = 1)
|
417
|
+
filename += '.json' if File.extname(filename) == ''
|
418
|
+
|
419
|
+
FileUtils.mkdir_p File.dirname(filename) unless Dir.exist? File.dirname(filename)
|
420
|
+
File.open(filename, 'w') { |f| f << JSON.pretty_generate(to_static_data_point_hash(version)) }
|
421
|
+
|
422
|
+
true
|
423
|
+
end
|
424
|
+
|
425
|
+
# save the analysis zip file which contains the measures, seed model, weather file, and init/final scripts
|
426
|
+
#
|
427
|
+
# @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.
|
428
|
+
# @return [Boolean]
|
429
|
+
def save_zip(filename)
|
430
|
+
filename += '.zip' if File.extname(filename) == ''
|
431
|
+
|
432
|
+
FileUtils.mkdir_p File.dirname(filename) unless Dir.exist? File.dirname(filename)
|
433
|
+
|
434
|
+
save_analysis_zip(filename)
|
435
|
+
end
|
436
|
+
|
437
|
+
|
438
|
+
def save_osa_zip(filename, all_weather_files = false, all_seed_files = false)
|
439
|
+
filename += '.zip' if File.extname(filename) == ''
|
440
|
+
|
441
|
+
FileUtils.mkdir_p File.dirname(filename) unless Dir.exist? File.dirname(filename)
|
442
|
+
|
443
|
+
save_analysis_zip_osa(filename, all_weather_files, all_seed_files)
|
444
|
+
end
|
445
|
+
|
446
|
+
# convert an OSW to an OSA
|
447
|
+
# osw_filename is the full path to the OSW file
|
448
|
+
# assumes the associated files and directories are in the same location
|
449
|
+
# /example.osw
|
450
|
+
# /measures
|
451
|
+
# /seeds
|
452
|
+
# /weather
|
453
|
+
#
|
454
|
+
def convert_osw(osw_filename, *measure_paths)
|
455
|
+
# load OSW so we can loop over [:steps]
|
456
|
+
if File.exist? osw_filename #will this work for both rel and abs paths?
|
457
|
+
osw = JSON.parse(File.read(osw_filename), symbolize_names: true)
|
458
|
+
@osw_path = File.expand_path(osw_filename)
|
459
|
+
else
|
460
|
+
raise "Could not find workflow file #{osw_filename}"
|
461
|
+
end
|
462
|
+
|
463
|
+
# set the weather and seed files if set in OSW
|
464
|
+
# use :file_paths and look for files to set
|
465
|
+
if osw[:file_paths]
|
466
|
+
# seed_model, check if in OSW and not found in path search already
|
467
|
+
if osw[:seed_file]
|
468
|
+
osw[:file_paths].each do |path|
|
469
|
+
puts "searching for seed at: #{File.join(File.expand_path(path), osw[:seed_file])}"
|
470
|
+
if File.exist?(File.join(File.expand_path(path), osw[:seed_file]))
|
471
|
+
puts "found seed_file: #{osw[:seed_file]}"
|
472
|
+
self.seed_model = File.join(File.expand_path(path), osw[:seed_file])
|
473
|
+
break
|
474
|
+
end
|
475
|
+
end
|
476
|
+
else
|
477
|
+
warn "osw[:seed_file] is not defined"
|
478
|
+
end
|
479
|
+
|
480
|
+
# weather_file, check if in OSW and not found in path search already
|
481
|
+
if osw[:weather_file]
|
482
|
+
osw[:file_paths].each do |path|
|
483
|
+
puts "searching for weather at: #{File.join(File.expand_path(path), osw[:weather_file])}"
|
484
|
+
if File.exist?(File.join(File.expand_path(path), osw[:weather_file]))
|
485
|
+
puts "found weather_file: #{osw[:weather_file]}"
|
486
|
+
self.weather_file = File.join(File.expand_path(path), osw[:weather_file])
|
487
|
+
break
|
488
|
+
end
|
489
|
+
end
|
490
|
+
else
|
491
|
+
warn "osw[:weather_file] is not defined"
|
492
|
+
end
|
493
|
+
|
494
|
+
# file_paths is not defined in OSW, so warn and try to set
|
495
|
+
else
|
496
|
+
warn ":file_paths is not defined in the OSW."
|
497
|
+
self.weather_file = osw[:weather_file] ? osw[:weather_file] : nil
|
498
|
+
self.seed_model = osw[:seed_file] ? osw[:seed_file] : nil
|
499
|
+
end
|
500
|
+
|
501
|
+
#set analysis_type default to Single_Run
|
502
|
+
self.analysis_type = 'single_run'
|
503
|
+
|
504
|
+
#loop over OSW 'steps' and map over measures
|
505
|
+
#there is no name/display name in the OSW. Just measure directory name
|
506
|
+
#read measure.XML from directory to get name / display name
|
507
|
+
#increment name by +_1 if there are duplicates
|
508
|
+
#add measure
|
509
|
+
#change default args to osw arg values
|
510
|
+
|
511
|
+
osw[:steps].each do |step|
|
512
|
+
#get measure directory
|
513
|
+
measure_dir = step[:measure_dir_name]
|
514
|
+
measure_name = measure_dir.split("measures/").last
|
515
|
+
puts "measure_dir_name: #{measure_name}"
|
516
|
+
#get XML
|
517
|
+
# Loop over possible user defined *measure_paths, including the dir of the osw_filename path and :measure_paths, to find the measure,
|
518
|
+
# then set measure_dir_abs_path to that path
|
519
|
+
measure_dir_abs_path = ''
|
520
|
+
paths_to_parse = [File.dirname(osw_filename), osw[:measure_paths], *measure_paths].flatten.compact.map { |path| File.join(File.expand_path(path), measure_dir, 'measure.xml') }
|
521
|
+
puts "searching for xml's in: #{paths_to_parse}"
|
522
|
+
xml = {}
|
523
|
+
paths_to_parse.each do |path|
|
524
|
+
if File.exist?(path)
|
525
|
+
puts "found xml: #{path}"
|
526
|
+
xml = parse_measure_xml(path)
|
527
|
+
if !xml.empty?
|
528
|
+
measure_dir_abs_path = path
|
529
|
+
break
|
530
|
+
end
|
531
|
+
end
|
532
|
+
end
|
533
|
+
raise "measure #{measure_name} not found" if xml.empty?
|
534
|
+
puts ""
|
535
|
+
#add check for previous names _+1
|
536
|
+
count = 1
|
537
|
+
name = xml[:name]
|
538
|
+
display_name = xml[:display_name]
|
539
|
+
loop do
|
540
|
+
measure = @workflow.find_measure(name)
|
541
|
+
break if measure.nil?
|
542
|
+
|
543
|
+
count += 1
|
544
|
+
name = "#{xml[:name]}_#{count}"
|
545
|
+
display_name = "#{xml[:display_name]} #{count}"
|
546
|
+
end
|
547
|
+
#Add Measure to workflow
|
548
|
+
@workflow.add_measure_from_path(name, display_name, measure_dir_abs_path) #this forces to an absolute path which seems constent with PAT
|
549
|
+
#@workflow.add_measure_from_path(name, display_name, measure_dir) #this uses the path in the OSW which could be relative
|
550
|
+
|
551
|
+
#Change the default argument values to the osw values
|
552
|
+
#1. find measure in @workflow
|
553
|
+
m = @workflow.find_measure(name)
|
554
|
+
#2. loop thru osw args
|
555
|
+
#check if the :argument is missing from the measure step, it shouldnt be but just in case give a clean message
|
556
|
+
if step[:arguments].nil?
|
557
|
+
raise "measure #{name} step has no arguments: #{step}"
|
558
|
+
else
|
559
|
+
step[:arguments].each do |k,v|
|
560
|
+
#check if argument is in measure, otherwise setting argument_value will crash
|
561
|
+
raise "OSW arg: #{k} is not in Measure: #{name}" if m.arguments.find_all { |a| a[:name] == k.to_s }.empty?
|
562
|
+
#set measure arg to match osw arg
|
563
|
+
m.argument_value(k.to_s, v)
|
564
|
+
end
|
565
|
+
end
|
566
|
+
end
|
567
|
+
end
|
568
|
+
|
569
|
+
private
|
570
|
+
|
571
|
+
# New format for OSAs. Package up the seed, weather files, and measures
|
572
|
+
# filename is the name of the file to be saved. ex: analysis.zip
|
573
|
+
# it will parse the OSA and zip up all the files defined in the workflow
|
574
|
+
def save_analysis_zip_osa(filename, all_weather_files = false, all_seed_files = false)
|
575
|
+
def add_directory_to_zip_osa(zipfile, local_directory, relative_zip_directory)
|
576
|
+
puts "Add Directory #{local_directory}"
|
577
|
+
Dir[File.join(local_directory.to_s, '**', '**')].each do |file|
|
578
|
+
puts "Adding File #{file}"
|
579
|
+
zipfile.add(file.sub(local_directory, relative_zip_directory), file)
|
580
|
+
end
|
581
|
+
zipfile
|
582
|
+
end
|
583
|
+
#delete file if exists
|
584
|
+
FileUtils.rm_f(filename) if File.exist?(filename)
|
585
|
+
#get the full path to the OSW, since all Files/Dirs should be in same directory as the OSW
|
586
|
+
puts "osw_path: #{@osw_path}"
|
587
|
+
osw_full_path = File.dirname(File.expand_path(@osw_path))
|
588
|
+
puts "osw_full_path: #{osw_full_path}"
|
589
|
+
|
590
|
+
Zip::File.open(filename, create: true) do |zf|
|
591
|
+
## Weather files
|
592
|
+
puts 'Adding Support Files: Weather'
|
593
|
+
# check if weather file exists. use abs path. remove leading ./ from @weather_file path if there.
|
594
|
+
# check if path is already absolute
|
595
|
+
if @weather_file[:file]
|
596
|
+
if File.exists?(@weather_file[:file])
|
597
|
+
puts " Adding #{@weather_file[:file]}"
|
598
|
+
#zf.add("weather/#{File.basename(@weather_file[:file])}", @weather_file[:file])
|
599
|
+
base_name = File.basename(@weather_file[:file], ".*")
|
600
|
+
puts "base_name: #{base_name}"
|
601
|
+
# convert backslash on windows to forward slash so Dir.glob will work (in case user uses \)
|
602
|
+
weather_dirname = File.dirname(@weather_file[:file]).gsub("\\", "/")
|
603
|
+
puts "weather_dirname: #{weather_dirname}"
|
604
|
+
# If all_weather_files is true, add all files in the directory to the zip.
|
605
|
+
# Otherwise, add only files that match the base name.
|
606
|
+
file_pattern = all_weather_files ? "*" : "#{base_name}.*"
|
607
|
+
Dir.glob(File.join(weather_dirname, file_pattern)) do |file_path|
|
608
|
+
puts "file_path: #{file_path}"
|
609
|
+
puts "zip path: weather/#{File.basename(file_path)}"
|
610
|
+
zf.add("weather/#{File.basename(file_path)}", file_path)
|
611
|
+
end
|
612
|
+
# make absolute path and check for file
|
613
|
+
elsif File.exists?(File.join(osw_full_path,@weather_file[:file].sub(/^\.\//, '')))
|
614
|
+
puts " Adding: #{File.join(osw_full_path,@weather_file[:file].sub(/^\.\//, ''))}"
|
615
|
+
#zf.add("weather/#{File.basename(@weather_file[:file])}", File.join(osw_full_path,@weather_file[:file].sub(/^\.\//, '')))
|
616
|
+
base_name = File.basename(@weather_file[:file].sub(/^\.\//, ''), ".*")
|
617
|
+
puts "base_name2: #{base_name}"
|
618
|
+
weather_dirname = File.dirname(File.join(osw_full_path,@weather_file[:file].sub(/^\.\//, ''))).gsub("\\", "/")
|
619
|
+
puts "weather_dirname: #{weather_dirname}"
|
620
|
+
file_pattern = all_weather_files ? "*" : "#{base_name}.*"
|
621
|
+
Dir.glob(File.join(weather_dirname, file_pattern)) do |file_path|
|
622
|
+
puts "file_path2: #{file_path}"
|
623
|
+
puts "zip path2: weather/#{File.basename(file_path)}"
|
624
|
+
zf.add("weather/#{File.basename(file_path)}", file_path)
|
625
|
+
end
|
626
|
+
else
|
627
|
+
raise "weather_file[:file] does not exist at: #{File.join(osw_full_path,@weather_file[:file].sub(/^\.\//, ''))}"
|
628
|
+
end
|
629
|
+
else
|
630
|
+
warn "weather_file[:file] is not defined"
|
631
|
+
end
|
632
|
+
|
633
|
+
## Seed files
|
634
|
+
puts 'Adding Support Files: Seed Models'
|
635
|
+
#check if seed file exists. use abs path. remove leading ./ from @seed_model path if there.
|
636
|
+
#check if path is already absolute
|
637
|
+
if @seed_model[:file]
|
638
|
+
if File.exists?(@seed_model[:file])
|
639
|
+
puts " Adding #{@seed_model[:file]}"
|
640
|
+
zf.add("seeds/#{File.basename(@seed_model[:file])}", @seed_model[:file])
|
641
|
+
if all_seed_files
|
642
|
+
seed_dirname = File.dirname(@seed_model[:file]).gsub("\\", "/")
|
643
|
+
puts "seed_dirname: #{seed_dirname}"
|
644
|
+
Dir.glob(File.join(seed_dirname, '*')) do |file_path|
|
645
|
+
next if file_path == @seed_model[:file] # Skip if the file is the same as @seed_model[:file] so not added twice
|
646
|
+
puts "file_path: #{file_path}"
|
647
|
+
puts "zip path: seeds/#{File.basename(file_path)}"
|
648
|
+
zf.add("seeds/#{File.basename(file_path)}", file_path)
|
649
|
+
end
|
650
|
+
end
|
651
|
+
#make absolute path and check for file
|
652
|
+
elsif File.exists?(File.join(osw_full_path,@seed_model[:file].sub(/^\.\//, '')))
|
653
|
+
puts " Adding #{File.join(osw_full_path,@seed_model[:file].sub(/^\.\//, ''))}"
|
654
|
+
zf.add("seeds/#{File.basename(@seed_model[:file])}", File.join(osw_full_path,@seed_model[:file].sub(/^\.\//, '')))
|
655
|
+
if all_seed_files
|
656
|
+
seed_dirname = File.dirname(File.join(osw_full_path,@seed_model[:file].sub(/^\.\//, ''))).gsub("\\", "/")
|
657
|
+
puts "seed_dirname: #{seed_dirname}"
|
658
|
+
Dir.glob(File.join(seed_dirname, '*')) do |file_path|
|
659
|
+
next if file_path == File.join(osw_full_path,@seed_model[:file].sub(/^\.\//, '')) # Skip if the file is the same as @seed_model[:file] so not added twice
|
660
|
+
puts "file_path: #{file_path}"
|
661
|
+
puts "zip path: seeds/#{File.basename(file_path)}"
|
662
|
+
zf.add("seeds/#{File.basename(file_path)}", file_path)
|
663
|
+
end
|
664
|
+
end
|
665
|
+
else
|
666
|
+
raise "seed_file[:file] does not exist at: #{File.join(osw_full_path,@seed_model[:file].sub(/^\.\//, ''))}"
|
667
|
+
end
|
668
|
+
else
|
669
|
+
warn "seed_file[:file] is not defined"
|
670
|
+
end
|
671
|
+
|
672
|
+
puts 'Adding Support Files: Libraries'
|
673
|
+
@libraries.each do |lib|
|
674
|
+
raise "Libraries must specify their 'library_name' as metadata which becomes the directory upon zip" unless lib[:metadata][:library_name]
|
675
|
+
|
676
|
+
if File.directory? lib[:file]
|
677
|
+
Dir[File.join(lib[:file], '**', '**')].each do |file|
|
678
|
+
puts " Adding #{file}"
|
679
|
+
zf.add(file.sub(lib[:file], "lib/#{lib[:metadata][:library_name]}"), file)
|
680
|
+
end
|
681
|
+
else
|
682
|
+
# just add the file to the zip
|
683
|
+
puts " Adding #{lib[:file]}"
|
684
|
+
zf.add(lib[:file], "lib/#{File.basename(lib[:file])}", lib[:file])
|
685
|
+
end
|
686
|
+
end
|
687
|
+
|
688
|
+
puts 'Adding Support Files: Server Scripts'
|
689
|
+
@server_scripts.each_with_index do |f, index|
|
690
|
+
if f[:init_or_final] == 'finalization'
|
691
|
+
file_name = 'finalization.sh'
|
692
|
+
else
|
693
|
+
file_name = 'initialization.sh'
|
694
|
+
end
|
695
|
+
if f[:server_or_data_point] == 'analysis'
|
696
|
+
new_name = "scripts/analysis/#{file_name}"
|
697
|
+
else
|
698
|
+
new_name = "scripts/data_point/#{file_name}"
|
699
|
+
end
|
700
|
+
puts " Adding #{f[:file]} as #{new_name}"
|
701
|
+
zf.add(new_name, f[:file])
|
702
|
+
|
703
|
+
if f[:arguments]
|
704
|
+
arg_file = "#{(new_name.sub(/\.sh\z/, ''))}.args"
|
705
|
+
puts " Adding arguments as #{arg_file}"
|
706
|
+
file = Tempfile.new('arg')
|
707
|
+
file.write(f[:arguments])
|
708
|
+
zf.add(arg_file, file)
|
709
|
+
file.close
|
710
|
+
end
|
711
|
+
end
|
712
|
+
|
713
|
+
## Measures
|
714
|
+
puts 'Adding Measures'
|
715
|
+
added_measures = []
|
716
|
+
# The list of the measures should always be there, but make sure they are uniq
|
717
|
+
@workflow.each do |measure|
|
718
|
+
measure_dir_to_add = measure.measure_definition_directory_local
|
719
|
+
|
720
|
+
next if added_measures.include? measure_dir_to_add
|
721
|
+
|
722
|
+
puts " Adding #{File.basename(measure_dir_to_add)}"
|
723
|
+
Dir[File.join(measure_dir_to_add, '**')].each do |file|
|
724
|
+
if File.directory?(file)
|
725
|
+
if File.basename(file) == 'resources' || File.basename(file) == 'lib'
|
726
|
+
#remove leading ./ from measure_definition_directory path if there.
|
727
|
+
add_directory_to_zip_osa(zf, file, "#{measure.measure_definition_directory.sub(/^\.\//, '')}/#{File.basename(file)}")
|
728
|
+
end
|
729
|
+
else
|
730
|
+
puts " Adding File #{file}"
|
731
|
+
#remove leading ./ from measure.measure_definition_directory string with regex .sub(/^\.\//, '')
|
732
|
+
zip_path_for_measures = file.sub(measure_dir_to_add, measure.measure_definition_directory.sub(/^\.\//, ''))
|
733
|
+
#puts " zip_path_for_measures: #{zip_path_for_measures}"
|
734
|
+
zf.add(zip_path_for_measures, file)
|
735
|
+
end
|
736
|
+
end
|
737
|
+
|
738
|
+
added_measures << measure_dir_to_add
|
739
|
+
end
|
740
|
+
end
|
741
|
+
end
|
742
|
+
|
743
|
+
#keep legacy function
|
744
|
+
# Package up the seed, weather files, and measures
|
745
|
+
def save_analysis_zip(filename)
|
746
|
+
def add_directory_to_zip(zipfile, local_directory, relative_zip_directory)
|
747
|
+
# puts "Add Directory #{local_directory}"
|
748
|
+
Dir[File.join(local_directory.to_s, '**', '**')].each do |file|
|
749
|
+
# puts "Adding File #{file}"
|
750
|
+
zipfile.add(file.sub(local_directory, relative_zip_directory), file)
|
751
|
+
end
|
752
|
+
zipfile
|
753
|
+
end
|
754
|
+
|
755
|
+
FileUtils.rm_f(filename) if File.exist?(filename)
|
756
|
+
|
757
|
+
Zip::File.open(filename, Zip::File::CREATE) do |zf|
|
758
|
+
## Weather files
|
759
|
+
# TODO: eventually remove the @weather_file attribute and grab the weather file out
|
760
|
+
# of the @weather_files
|
761
|
+
puts 'Adding Support Files: Weather'
|
762
|
+
if @weather_file[:file] && !@weather_files.files.find { |f| @weather_file[:file] == f[:file] }
|
763
|
+
# manually add the weather file
|
764
|
+
puts " Adding #{@weather_file[:file]}"
|
765
|
+
zf.add("./weather/#{File.basename(@weather_file[:file])}", @weather_file[:file])
|
766
|
+
end
|
767
|
+
@weather_files.each do |f|
|
768
|
+
puts " Adding #{f[:file]}"
|
769
|
+
zf.add("./weather/#{File.basename(f[:file])}", f[:file])
|
770
|
+
end
|
771
|
+
|
772
|
+
## Seed files
|
773
|
+
puts 'Adding Support Files: Seed Models'
|
774
|
+
if @seed_model[:file] && !@seed_models.files.find { |f| @seed_model[:file] == f[:file] }
|
775
|
+
# manually add the weather file
|
776
|
+
puts " Adding #{@seed_model[:file]}"
|
777
|
+
zf.add("./seed/#{File.basename(@seed_model[:file])}", @seed_model[:file])
|
778
|
+
end
|
779
|
+
@seed_models.each do |f|
|
780
|
+
puts " Adding #{f[:file]}"
|
781
|
+
zf.add("./seed/#{File.basename(f[:file])}", f[:file])
|
782
|
+
end
|
783
|
+
|
784
|
+
puts 'Adding Support Files: Libraries'
|
785
|
+
@libraries.each do |lib|
|
786
|
+
raise "Libraries must specify their 'library_name' as metadata which becomes the directory upon zip" unless lib[:metadata][:library_name]
|
787
|
+
|
788
|
+
if File.directory? lib[:file]
|
789
|
+
Dir[File.join(lib[:file], '**', '**')].each do |file|
|
790
|
+
puts " Adding #{file}"
|
791
|
+
zf.add(file.sub(lib[:file], "./lib/#{lib[:metadata][:library_name]}/"), file)
|
792
|
+
end
|
793
|
+
else
|
794
|
+
# just add the file to the zip
|
795
|
+
puts " Adding #{lib[:file]}"
|
796
|
+
zf.add(lib[:file], "./lib/#{File.basename(lib[:file])}", lib[:file])
|
797
|
+
end
|
798
|
+
end
|
799
|
+
|
800
|
+
puts 'Adding Support Files: Worker Initialization Scripts'
|
801
|
+
@worker_inits.each_with_index do |f, index|
|
802
|
+
ordered_file_name = "#{index.to_s.rjust(2, '0')}_#{File.basename(f[:file])}"
|
803
|
+
puts " Adding #{f[:file]} as #{ordered_file_name}"
|
804
|
+
zf.add(f[:file].sub(f[:file], "./scripts/worker_initialization//#{ordered_file_name}"), f[:file])
|
805
|
+
|
806
|
+
if f[:metadata][:args]
|
807
|
+
arg_file = "#{File.basename(ordered_file_name, '.*')}.args"
|
808
|
+
file = Tempfile.new('arg')
|
809
|
+
file.write(f[:metadata][:args])
|
810
|
+
zf.add("./scripts/worker_initialization/#{arg_file}", file)
|
811
|
+
file.close
|
812
|
+
end
|
813
|
+
end
|
814
|
+
|
815
|
+
puts 'Adding Support Files: Worker Finalization Scripts'
|
816
|
+
@worker_finalizes.each_with_index do |f, index|
|
817
|
+
ordered_file_name = "#{index.to_s.rjust(2, '0')}_#{File.basename(f[:file])}"
|
818
|
+
puts " Adding #{f[:file]} as #{ordered_file_name}"
|
819
|
+
zf.add(f[:file].sub(f[:file], "scripts/worker_finalization/#{ordered_file_name}"), f[:file])
|
820
|
+
|
821
|
+
if f[:metadata][:args]
|
822
|
+
arg_file = "#{File.basename(ordered_file_name, '.*')}.args"
|
823
|
+
file = Tempfile.new('arg')
|
824
|
+
file.write(f[:metadata][:args])
|
825
|
+
zf.add("scripts/worker_finalization/#{arg_file}", file)
|
826
|
+
file.close
|
827
|
+
end
|
828
|
+
end
|
829
|
+
|
830
|
+
## Measures
|
831
|
+
puts 'Adding Measures'
|
832
|
+
added_measures = []
|
833
|
+
# The list of the measures should always be there, but make sure they are uniq
|
834
|
+
@workflow.each do |measure|
|
835
|
+
measure_dir_to_add = measure.measure_definition_directory_local
|
836
|
+
|
837
|
+
next if added_measures.include? measure_dir_to_add
|
838
|
+
|
839
|
+
puts " Adding #{File.basename(measure_dir_to_add)}"
|
840
|
+
Dir[File.join(measure_dir_to_add, '**')].each do |file|
|
841
|
+
if File.directory?(file)
|
842
|
+
if File.basename(file) == 'resources' || File.basename(file) == 'lib'
|
843
|
+
add_directory_to_zip(zf, file, "#{measure.measure_definition_directory}/#{File.basename(file)}")
|
844
|
+
end
|
845
|
+
else
|
846
|
+
# puts "Adding File #{file}"
|
847
|
+
zf.add(file.sub(measure_dir_to_add, "#{measure.measure_definition_directory}/"), file)
|
848
|
+
end
|
849
|
+
end
|
850
|
+
|
851
|
+
added_measures << measure_dir_to_add
|
852
|
+
end
|
853
|
+
end
|
854
|
+
end
|
855
|
+
end
|
856
|
+
end
|
857
|
+
end
|