openstudio-workflow 1.0.0.pat1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +16 -68
- data/Rakefile +9 -9
- data/bin/openstudio_cli +786 -0
- data/lib/openstudio/workflow/adapters/input/local.rb +97 -0
- data/lib/openstudio/workflow/adapters/output/local.rb +90 -0
- data/lib/openstudio/workflow/adapters/output/socket.rb +70 -0
- data/lib/openstudio/workflow/{jobs/run_preflight/run_preflight.rb → adapters/output/web.rb} +37 -19
- data/lib/openstudio/workflow/{adapter.rb → adapters/output_adapter.rb} +53 -51
- data/lib/openstudio/workflow/job.rb +22 -0
- data/lib/openstudio/workflow/jobs/{run_energyplus → resources}/monthly_report.idf +0 -0
- data/lib/openstudio/workflow/jobs/run_energyplus.rb +49 -0
- data/lib/openstudio/workflow/jobs/run_ep_measures.rb +55 -0
- data/lib/openstudio/workflow/jobs/run_initialization.rb +136 -0
- data/lib/openstudio/workflow/jobs/run_os_measures.rb +59 -0
- data/lib/openstudio/workflow/jobs/run_postprocess.rb +53 -0
- data/lib/openstudio/workflow/jobs/run_preprocess.rb +81 -0
- data/lib/openstudio/workflow/jobs/run_reporting_measures.rb +86 -0
- data/lib/openstudio/workflow/jobs/run_translation.rb +49 -0
- data/lib/openstudio/workflow/multi_delegator.rb +1 -3
- data/lib/openstudio/workflow/registry.rb +137 -0
- data/lib/openstudio/workflow/run.rb +182 -221
- data/lib/openstudio/workflow/time_logger.rb +1 -1
- data/lib/openstudio/workflow/util/energyplus.rb +564 -0
- data/lib/openstudio/workflow/util/io.rb +33 -0
- data/lib/openstudio/workflow/util/measure.rb +520 -0
- data/lib/openstudio/workflow/util/model.rb +100 -0
- data/lib/openstudio/workflow/util/post_process.rb +177 -0
- data/lib/openstudio/workflow/util/weather_file.rb +108 -0
- data/lib/openstudio/workflow/util.rb +14 -0
- data/lib/openstudio/workflow/version.rb +1 -1
- data/lib/openstudio/workflow_json.rb +399 -0
- data/lib/openstudio/workflow_runner.rb +213 -0
- data/lib/openstudio-workflow.rb +13 -118
- metadata +45 -85
- data/lib/openstudio/extended_runner.rb +0 -105
- data/lib/openstudio/workflow/adapters/local.rb +0 -101
- data/lib/openstudio/workflow/adapters/mongo.rb +0 -227
- data/lib/openstudio/workflow/jobs/lib/apply_measures.rb +0 -253
- data/lib/openstudio/workflow/jobs/run_energyplus/run_energyplus.rb +0 -314
- data/lib/openstudio/workflow/jobs/run_openstudio/run_openstudio.rb +0 -230
- data/lib/openstudio/workflow/jobs/run_postprocess/run_postprocess.rb +0 -110
- data/lib/openstudio/workflow/jobs/run_reporting_measures/run_reporting_measures.rb +0 -471
- data/lib/openstudio/workflow/jobs/run_runmanager/run_runmanager.rb +0 -247
- data/lib/openstudio/workflow/jobs/run_xml/run_xml.rb +0 -279
@@ -0,0 +1,177 @@
|
|
1
|
+
module OpenStudio
|
2
|
+
module Workflow
|
3
|
+
module Util
|
4
|
+
require 'openstudio/workflow/util/measure'
|
5
|
+
require 'csv'
|
6
|
+
|
7
|
+
# This module serves as a wrapper around various post-processing tasks used to manage outputs
|
8
|
+
# @todo (rhorsey) ummmm. So some of this is pretty ugly. Since @dmacumber had ideas about this maybe he can figure
|
9
|
+
# out what to do about it all
|
10
|
+
# @todo (nlong) the output adapter restructure will frack up the extraction method royally
|
11
|
+
#
|
12
|
+
module PostProcess
|
13
|
+
# This method loads a sql file into OpenStudio and returns it
|
14
|
+
#
|
15
|
+
# @param [String] sql_file Absolute path to the sql file to be loaded
|
16
|
+
# @return [Object, nil] The OpenStudio::SqlFile object, or nil if it could not be found
|
17
|
+
#
|
18
|
+
def load_sql_file(sql_file)
|
19
|
+
return nil unless File.exist? sql_file
|
20
|
+
OpenStudio::SqlFile.new(@sql_filename)
|
21
|
+
end
|
22
|
+
|
23
|
+
# This method parses all sorts of stuff which something needs
|
24
|
+
#
|
25
|
+
# @param [String] run_dir The directory that the simulation was run in
|
26
|
+
# @return [Hash, Hash] results and objective_function (which may be empty) are returned
|
27
|
+
# @todo (rhorsey) fix the description
|
28
|
+
#
|
29
|
+
def run_extract_inputs_and_outputs(run_dir, logger)
|
30
|
+
# For xml, the measure attributes are in the measure_attributes_xml.json file
|
31
|
+
# TODO: somehow pass the metadata around on which JSONs to suck into the database
|
32
|
+
results = {}
|
33
|
+
# Inputs are in the measure_attributes.json file
|
34
|
+
if File.exist? "#{run_dir}/measure_attributes.json"
|
35
|
+
h = JSON.parse(File.read("#{run_dir}/measure_attributes.json"), symbolize_names: true)
|
36
|
+
h = rename_hash_keys(h, logger)
|
37
|
+
results.merge! h
|
38
|
+
end
|
39
|
+
|
40
|
+
logger.info 'Saving the result hash to file'
|
41
|
+
File.open("#{run_dir}/results.json", 'w') { |f| f << JSON.pretty_generate(results) }
|
42
|
+
|
43
|
+
objective_functions = {}
|
44
|
+
if @registry[:analysis]
|
45
|
+
logger.info 'Iterating over Analysis JSON Output Variables'
|
46
|
+
# Save the objective functions to the object for sending back to the simulation executive
|
47
|
+
|
48
|
+
if @analysis_json[:analysis] && @analysis_json[:analysis][:output_variables]
|
49
|
+
@analysis_json[:analysis][:output_variables].each do |variable|
|
50
|
+
# determine which ones are the objective functions (code smell: todo: use enumerator)
|
51
|
+
if variable[:objective_function]
|
52
|
+
logger.info "Looking for objective function #{variable[:name]}"
|
53
|
+
# TODO: move this to cleaner logic. Use ostruct?
|
54
|
+
k, v = variable[:name].split('.')
|
55
|
+
|
56
|
+
# look for the objective function key and make sure that it is not nil. False is an okay obj function.
|
57
|
+
if results.key?(k.to_sym) && !results[k.to_sym][v.to_sym].nil?
|
58
|
+
objective_functions["objective_function_#{variable[:objective_function_index] + 1}"] = results[k.to_sym][v.to_sym]
|
59
|
+
if variable[:objective_function_target]
|
60
|
+
logger.info "Found objective function target for #{variable[:name]}"
|
61
|
+
objective_functions["objective_function_target_#{variable[:objective_function_index] + 1}"] = variable[:objective_function_target].to_f
|
62
|
+
end
|
63
|
+
if variable[:scaling_factor]
|
64
|
+
logger.info "Found scaling factor for #{variable[:name]}"
|
65
|
+
objective_functions["scaling_factor_#{variable[:objective_function_index] + 1}"] = variable[:scaling_factor].to_f
|
66
|
+
end
|
67
|
+
if variable[:objective_function_group]
|
68
|
+
logger.info "Found objective function group for #{variable[:name]}"
|
69
|
+
objective_functions["objective_function_group_#{variable[:objective_function_index] + 1}"] = variable[:objective_function_group].to_f
|
70
|
+
end
|
71
|
+
else
|
72
|
+
logger.warn "No results for objective function #{variable[:name]}"
|
73
|
+
objective_functions["objective_function_#{variable[:objective_function_index] + 1}"] = Float::MAX
|
74
|
+
objective_functions["objective_function_target_#{variable[:objective_function_index] + 1}"] = nil
|
75
|
+
objective_functions["scaling_factor_#{variable[:objective_function_index] + 1}"] = nil
|
76
|
+
objective_functions["objective_function_group_#{variable[:objective_function_index] + 1}"] = nil
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
return results, objective_functions
|
84
|
+
end
|
85
|
+
|
86
|
+
# Remove any invalid characters in the measure attribute keys. Periods and Pipes are the most problematic
|
87
|
+
# because mongo does not allow hash keys with periods, and the pipes are used in the map/reduce method that
|
88
|
+
# was written to speed up the data write in openstudio-server. Also remove any trailing underscores and spaces
|
89
|
+
#
|
90
|
+
# @param [Hash] hash Any hash with potentially problematic characters
|
91
|
+
# @param [Logger] logger Logger to write to
|
92
|
+
#
|
93
|
+
def rename_hash_keys(hash, logger)
|
94
|
+
# @todo should we log the name changes?
|
95
|
+
regex = /[|!@#\$%^&\*\(\)\{\}\\\[\];:'",<.>\/?\+=]+/
|
96
|
+
|
97
|
+
rename_keys = lambda do |h|
|
98
|
+
if Hash === h
|
99
|
+
h.each_key do |key|
|
100
|
+
if key.to_s =~ regex
|
101
|
+
logger.warn "Renaming result key '#{key}' to remove invalid characters"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
Hash[h.map { |k, v| [k.to_s.gsub(regex, '_').squeeze('_').gsub(/[_\s]+$/, '').chomp.to_sym, rename_keys[v]] }]
|
105
|
+
else
|
106
|
+
h
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
rename_keys[hash]
|
111
|
+
end
|
112
|
+
|
113
|
+
|
114
|
+
# Save reports to a common directory
|
115
|
+
#
|
116
|
+
# @param [String] run_dir
|
117
|
+
# @param [String] directory
|
118
|
+
# @param [String] logger
|
119
|
+
#
|
120
|
+
def gather_reports(run_dir, directory, logger)
|
121
|
+
logger.info run_dir
|
122
|
+
logger.info directory
|
123
|
+
|
124
|
+
FileUtils.mkdir_p "#{directory}/reports"
|
125
|
+
|
126
|
+
# try to find the energyplus result file
|
127
|
+
eplus_html = "#{run_dir}/eplustbl.htm"
|
128
|
+
if File.exist? eplus_html
|
129
|
+
# do some encoding on the html if possible
|
130
|
+
html = File.read(eplus_html)
|
131
|
+
html = html.force_encoding('ISO-8859-1').encode('utf-8', replace: nil)
|
132
|
+
logger.info "Saving EnergyPlus HTML report to #{directory}/reports/eplustbl.html"
|
133
|
+
File.open("#{directory}/reports/eplustbl.html", 'w') { |f| f << html }
|
134
|
+
end
|
135
|
+
|
136
|
+
# Also, find any "report*.*" files
|
137
|
+
Dir["#{run_dir}/*/report*.*"].each do |report|
|
138
|
+
# get the parent directory of the file and snake case it
|
139
|
+
measure_class_name = File.basename(File.dirname(report))
|
140
|
+
file_ext = File.extname(report)
|
141
|
+
append_str = File.basename(report, '.*')
|
142
|
+
new_file_name = "#{directory}/reports/#{measure_class_name}_#{append_str}#{file_ext}"
|
143
|
+
logger.info "Saving report #{report} to #{new_file_name}"
|
144
|
+
FileUtils.copy report, new_file_name
|
145
|
+
end
|
146
|
+
|
147
|
+
# Remove empty directories in run folder
|
148
|
+
Dir["#{run_dir}/*"].select { |d| File.directory? d }.select { |d| (Dir.entries(d) - %w(. ..)).empty? }.each do |d|
|
149
|
+
logger.info "Removing empty directory #{d}"
|
150
|
+
Dir.rmdir d
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
|
155
|
+
# A general post-processing step which could be made significantly more modular
|
156
|
+
#
|
157
|
+
# @param [String] run_dir
|
158
|
+
#
|
159
|
+
def cleanup(run_dir, directory, logger)
|
160
|
+
|
161
|
+
|
162
|
+
paths_to_rm = []
|
163
|
+
# paths_to_rm << Pathname.glob("#{run_dir}/*.osm")
|
164
|
+
# paths_to_rm << Pathname.glob("#{run_dir}/*.idf") # keep the idfs
|
165
|
+
# paths_to_rm << Pathname.glob("*.audit")
|
166
|
+
# paths_to_rm << Pathname.glob("*.bnd")
|
167
|
+
# paths_to_rm << Pathname.glob("#{run_dir}/*.eso")
|
168
|
+
paths_to_rm << Pathname.glob("#{run_dir}/*.mtr")
|
169
|
+
paths_to_rm << Pathname.glob("#{run_dir}/*.epw")
|
170
|
+
paths_to_rm << Pathname.glob("#{run_dir}/*.mtd")
|
171
|
+
paths_to_rm << Pathname.glob("#{run_dir}/*.rdd")
|
172
|
+
paths_to_rm.each { |p| FileUtils.rm_rf(p) }
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
module OpenStudio
|
2
|
+
module Workflow
|
3
|
+
module Util
|
4
|
+
# The current precedence rules for weather files are defined in this module. Best practice is to only use the
|
5
|
+
# #get_weather_file method, as it will be forward compatible
|
6
|
+
#
|
7
|
+
module WeatherFile
|
8
|
+
# Returns the weather file with precedence
|
9
|
+
#
|
10
|
+
# @param [String] directory The directory to append all relative directories to, see #get_weather_file_from_fs
|
11
|
+
# @param [String] wf The weather file being searched for. If not the name of the file this parameter should be
|
12
|
+
# the absolute path specifying it's location
|
13
|
+
# @param [Array] wf_search_array The set of precedence ordered relative directories to search for the wf in. A
|
14
|
+
# typical entry might look like `['files', '../../files', '../../weather']`
|
15
|
+
# @param [Object] model The OpenStudio::Model object to parse, see #get_weather_file_from_osm
|
16
|
+
# @return [String, nil] The weather file with precedence if defined, nil if not, and a failure if the wf is
|
17
|
+
# defined but not in the filesystem
|
18
|
+
#
|
19
|
+
def get_weather_file(directory, wf, wf_search_array, model, logger = nil)
|
20
|
+
# TODO: this logic needs some updating, weather file should come from current model, found using search paths
|
21
|
+
logger ||= ::Logger.new(STDOUT) unless logger
|
22
|
+
if wf
|
23
|
+
weather_file = get_weather_file_from_fs(directory, wf, wf_search_array, logger)
|
24
|
+
raise 'Could not locate the weather file in the filesystem. Please see the log' if weather_file == false
|
25
|
+
end
|
26
|
+
weather_file = get_weather_file_from_osm(model, logger) if weather_file.nil?
|
27
|
+
raise 'Could not locate the weather file in the filesystem. Please see the log' if weather_file == false
|
28
|
+
logger.warn 'The weather file could not be determined. Please see the log for details' unless weather_file
|
29
|
+
weather_file
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
# Returns the weather file from the model. If the weather file is defined in the model, then
|
35
|
+
# it checks the file paths to check if the model exists. This allows for a user to upload a
|
36
|
+
# weather file in a measure and then have the measure's path be used for the weather file.
|
37
|
+
#
|
38
|
+
# @todo (rhorsey) verify the description of this method, as it seems suspect
|
39
|
+
# @param [Object] model The OpenStudio::Model object to retrieve the weather file from
|
40
|
+
# @return [nil,false, String] If the result is nil the weather file was not defined in the model, if the result
|
41
|
+
# is false the weather file was set but could not be found on the filesystem, if a string the weather file was
|
42
|
+
# defined and it's existence verified
|
43
|
+
#
|
44
|
+
def get_weather_file_from_osm(model, logger)
|
45
|
+
wf = nil
|
46
|
+
# grab the weather file out of the OSM if it exists
|
47
|
+
if model.weatherFile.empty?
|
48
|
+
logger.warn 'No weather file defined in the model'
|
49
|
+
else
|
50
|
+
p = model.weatherFile.get.path.get.to_s.gsub('file://', '')
|
51
|
+
wf = if File.exist? p
|
52
|
+
File.absolute_path(p)
|
53
|
+
else
|
54
|
+
# this is the weather file from the OSM model
|
55
|
+
File.absolute_path(@model.weatherFile.get.path.get.to_s)
|
56
|
+
end
|
57
|
+
logger.info "The weather file path found in the model object: #{wf}"
|
58
|
+
unless File.exist? wf
|
59
|
+
logger.warn 'The weather file could not be found on the filesystem.'
|
60
|
+
wf = false
|
61
|
+
end
|
62
|
+
end
|
63
|
+
wf
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns the weather file defined in the OSW
|
67
|
+
#
|
68
|
+
# @param [String] directory The base directory to append all relative directories to
|
69
|
+
# @param [String] wf The weather file being searched for. If not the name of the file this parameter should be
|
70
|
+
# the absolute path specifying it's location
|
71
|
+
# @param [Array] wf_search_array The set of precedence ordered relative directories to search for the wf in. A
|
72
|
+
# typical entry might look like `['files', '../../files', '../../weather']`
|
73
|
+
# @return [nil, false, String] If the result is nil the weather file was not defined in the workflow, if the
|
74
|
+
# result is false the weather file was set but could not be found on the filesystem, if a string the weather
|
75
|
+
# file was defined and it's existence verified. The order of precedence for paths is as follows: 1 - an
|
76
|
+
# absolute path defined in wf, 2 - the wf_search_array, should it be defined, joined with the weather file and
|
77
|
+
# appended to the directory, with each entry in the array searched until the wf is found
|
78
|
+
#
|
79
|
+
def get_weather_file_from_fs(directory, wf, wf_search_array, logger)
|
80
|
+
raise "wf was defined as #{wf}. Please correct" unless wf
|
81
|
+
weather_file = nil
|
82
|
+
if Pathname.new(wf).absolute?
|
83
|
+
weather_file = wf
|
84
|
+
else
|
85
|
+
wf_search_array.each do |wf_dir|
|
86
|
+
logger.warn "The path #{wf_dir} does not exist" unless File.exist? File.join(directory, wf_dir)
|
87
|
+
next unless File.exist? File.join(directory, wf_dir)
|
88
|
+
if Dir.entries(File.join(directory, wf_dir)).include? File.basename(wf)
|
89
|
+
weather_file = File.absolute_path(File.join(directory, wf_dir, wf))
|
90
|
+
break
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
unless weather_file
|
95
|
+
logger.warn 'The weather file was not found on the filesystem'
|
96
|
+
return nil
|
97
|
+
end
|
98
|
+
logger.info "Weather file with precedence in the file system is #{weather_file}"
|
99
|
+
unless File.exist? weather_file
|
100
|
+
logger.warn 'The weather file could not be found on the filesystem'
|
101
|
+
weather_file = false
|
102
|
+
end
|
103
|
+
weather_file
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module OpenStudio
|
2
|
+
module Workflow
|
3
|
+
# Hard load utils for the moment
|
4
|
+
#
|
5
|
+
module Util
|
6
|
+
require 'openstudio/workflow/util/io'
|
7
|
+
require 'openstudio/workflow/util/measure'
|
8
|
+
require 'openstudio/workflow/util/weather_file'
|
9
|
+
require 'openstudio/workflow/util/model'
|
10
|
+
require 'openstudio/workflow/util/energyplus'
|
11
|
+
require 'openstudio/workflow/util/post_process'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|