openstudio-workflow 1.0.0.pat1 → 1.0.0
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/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
|