openstudio-workflow 1.2.1 → 1.2.2

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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +72 -72
  3. data/README.md +93 -48
  4. data/Rakefile +36 -36
  5. data/lib/openstudio-workflow.rb +49 -49
  6. data/lib/openstudio/workflow/adapters/input/local.rb +244 -240
  7. data/lib/openstudio/workflow/adapters/output/local.rb +95 -95
  8. data/lib/openstudio/workflow/adapters/output/socket.rb +91 -91
  9. data/lib/openstudio/workflow/adapters/output/web.rb +66 -66
  10. data/lib/openstudio/workflow/adapters/output_adapter.rb +147 -147
  11. data/lib/openstudio/workflow/job.rb +22 -22
  12. data/lib/openstudio/workflow/jobs/resources/monthly_report.idf +222 -222
  13. data/lib/openstudio/workflow/jobs/run_energyplus.rb +49 -49
  14. data/lib/openstudio/workflow/jobs/run_ep_measures.rb +55 -55
  15. data/lib/openstudio/workflow/jobs/run_initialization.rb +169 -167
  16. data/lib/openstudio/workflow/jobs/run_os_measures.rb +69 -69
  17. data/lib/openstudio/workflow/jobs/run_postprocess.rb +53 -53
  18. data/lib/openstudio/workflow/jobs/run_preprocess.rb +69 -69
  19. data/lib/openstudio/workflow/jobs/run_reporting_measures.rb +98 -98
  20. data/lib/openstudio/workflow/jobs/run_translation.rb +61 -61
  21. data/lib/openstudio/workflow/multi_delegator.rb +46 -46
  22. data/lib/openstudio/workflow/registry.rb +137 -137
  23. data/lib/openstudio/workflow/run.rb +299 -299
  24. data/lib/openstudio/workflow/time_logger.rb +53 -53
  25. data/lib/openstudio/workflow/util.rb +14 -14
  26. data/lib/openstudio/workflow/util/energyplus.rb +566 -564
  27. data/lib/openstudio/workflow/util/io.rb +33 -33
  28. data/lib/openstudio/workflow/util/measure.rb +588 -588
  29. data/lib/openstudio/workflow/util/model.rb +100 -100
  30. data/lib/openstudio/workflow/util/post_process.rb +187 -187
  31. data/lib/openstudio/workflow/util/weather_file.rb +108 -108
  32. data/lib/openstudio/workflow/version.rb +24 -24
  33. data/lib/openstudio/workflow_json.rb +426 -426
  34. data/lib/openstudio/workflow_runner.rb +233 -215
  35. metadata +3 -3
@@ -1,100 +1,100 @@
1
- module OpenStudio
2
- module Workflow
3
- module Util
4
- # Manages routine tasks involving OpenStudio::Model or OpenStudio::Workflow objects, such as loading, saving, and
5
- # translating them.
6
- #
7
- module Model
8
- # Method to create / load an OSM file
9
- #
10
- # @param [String] osm_path The full path to an OSM file to load
11
- # @param [Object] logger An optional logger to use for finding the OSM model
12
- # @return [Object] The return from this method is a loaded OSM or a failure.
13
- #
14
- def load_osm(osm_path, logger)
15
- logger.info 'Loading OSM model'
16
-
17
- # Load the model and return it
18
- logger.info "Reading in OSM model #{osm_path}"
19
-
20
- loaded_model = nil
21
- begin
22
- translator = OpenStudio::OSVersion::VersionTranslator.new
23
- loaded_model = translator.loadModel(osm_path)
24
- rescue
25
- # TODO: get translator working in embedded.
26
- # Need to embed idd files
27
- logger.warn 'OpenStudio VersionTranslator could not be loaded'
28
- loaded_model = OpenStudio::Model::Model.load(osm_path)
29
- end
30
- raise "Failed to load OSM file #{osm_path}" if loaded_model.empty?
31
- loaded_model.get
32
- end
33
-
34
- # Method to create / load an IDF file
35
- #
36
- # @param [String] idf_path Full path to the IDF
37
- # @param [Object] logger An optional logger to use for finding the idf model
38
- # @return [Object] The return from this method is a loaded IDF or a failure.
39
- #
40
- def load_idf(idf_path, logger)
41
- logger.info 'Loading IDF model'
42
-
43
- # Load the IDF into a workspace object and return it
44
- logger.info "Reading in IDF model #{idf_path}"
45
-
46
- idf = OpenStudio::Workspace.load(idf_path)
47
- raise "Failed to load IDF file #{idf_path}" if idf.empty?
48
- idf.get
49
- end
50
-
51
- # Translates a OpenStudio model object into an OpenStudio IDF object
52
- #
53
- # @param [Object] model the OpenStudio::Model instance to translate into an OpenStudio::Workspace object -- see
54
- # the OpenStudio SDK for details on the process
55
- # @return [Object] Returns and OpenStudio::Workspace object
56
- # @todo (rhorsey) rescue errors here
57
- #
58
- def translate_to_energyplus(model, logger = nil)
59
- logger = ::Logger.new(STDOUT) unless logger
60
- logger.info 'Translate object to EnergyPlus IDF in preparation for EnergyPlus'
61
- a = ::Time.now
62
- # ensure objects exist for reporting purposes
63
- model.getFacility
64
- model.getBuilding
65
- forward_translator = OpenStudio::EnergyPlus::ForwardTranslator.new
66
- model_idf = forward_translator.translateModel(model)
67
- b = ::Time.now
68
- logger.info "Translate object to EnergyPlus IDF took #{b.to_f - a.to_f}"
69
- model_idf
70
- end
71
-
72
- # Saves an OpenStudio model object to file
73
- #
74
- # @param [Object] model The OpenStudio::Model instance to save to file
75
- # @param [String] save_directory Folder to save the model in
76
- # @param [String] name ('in.osm') Option to define a non-standard name
77
- # @return [String] OSM file name
78
- #
79
- def save_osm(model, save_directory, name = 'in.osm')
80
- osm_filename = File.join(save_directory.to_s, name.to_s)
81
- File.open(osm_filename, 'w') { |f| f << model.to_s }
82
- osm_filename
83
- end
84
-
85
- # Saves an OpenStudio IDF model object to file
86
- #
87
- # @param [Object] model The OpenStudio::Workspace instance to save to file
88
- # @param [String] save_directory Folder to save the model in
89
- # @param [String] name ('in.osm') Option to define a non-standard name
90
- # @return [String] IDF file name
91
- #
92
- def save_idf(model_idf, save_directory, name = 'in.idf')
93
- idf_filename = File.join(save_directory.to_s, name.to_s)
94
- File.open(idf_filename, 'w') { |f| f << model_idf.to_s }
95
- idf_filename
96
- end
97
- end
98
- end
99
- end
100
- end
1
+ module OpenStudio
2
+ module Workflow
3
+ module Util
4
+ # Manages routine tasks involving OpenStudio::Model or OpenStudio::Workflow objects, such as loading, saving, and
5
+ # translating them.
6
+ #
7
+ module Model
8
+ # Method to create / load an OSM file
9
+ #
10
+ # @param [String] osm_path The full path to an OSM file to load
11
+ # @param [Object] logger An optional logger to use for finding the OSM model
12
+ # @return [Object] The return from this method is a loaded OSM or a failure.
13
+ #
14
+ def load_osm(osm_path, logger)
15
+ logger.info 'Loading OSM model'
16
+
17
+ # Load the model and return it
18
+ logger.info "Reading in OSM model #{osm_path}"
19
+
20
+ loaded_model = nil
21
+ begin
22
+ translator = OpenStudio::OSVersion::VersionTranslator.new
23
+ loaded_model = translator.loadModel(osm_path)
24
+ rescue
25
+ # TODO: get translator working in embedded.
26
+ # Need to embed idd files
27
+ logger.warn 'OpenStudio VersionTranslator could not be loaded'
28
+ loaded_model = OpenStudio::Model::Model.load(osm_path)
29
+ end
30
+ raise "Failed to load OSM file #{osm_path}" if loaded_model.empty?
31
+ loaded_model.get
32
+ end
33
+
34
+ # Method to create / load an IDF file
35
+ #
36
+ # @param [String] idf_path Full path to the IDF
37
+ # @param [Object] logger An optional logger to use for finding the idf model
38
+ # @return [Object] The return from this method is a loaded IDF or a failure.
39
+ #
40
+ def load_idf(idf_path, logger)
41
+ logger.info 'Loading IDF model'
42
+
43
+ # Load the IDF into a workspace object and return it
44
+ logger.info "Reading in IDF model #{idf_path}"
45
+
46
+ idf = OpenStudio::Workspace.load(idf_path)
47
+ raise "Failed to load IDF file #{idf_path}" if idf.empty?
48
+ idf.get
49
+ end
50
+
51
+ # Translates a OpenStudio model object into an OpenStudio IDF object
52
+ #
53
+ # @param [Object] model the OpenStudio::Model instance to translate into an OpenStudio::Workspace object -- see
54
+ # the OpenStudio SDK for details on the process
55
+ # @return [Object] Returns and OpenStudio::Workspace object
56
+ # @todo (rhorsey) rescue errors here
57
+ #
58
+ def translate_to_energyplus(model, logger = nil)
59
+ logger = ::Logger.new(STDOUT) unless logger
60
+ logger.info 'Translate object to EnergyPlus IDF in preparation for EnergyPlus'
61
+ a = ::Time.now
62
+ # ensure objects exist for reporting purposes
63
+ model.getFacility
64
+ model.getBuilding
65
+ forward_translator = OpenStudio::EnergyPlus::ForwardTranslator.new
66
+ model_idf = forward_translator.translateModel(model)
67
+ b = ::Time.now
68
+ logger.info "Translate object to EnergyPlus IDF took #{b.to_f - a.to_f}"
69
+ model_idf
70
+ end
71
+
72
+ # Saves an OpenStudio model object to file
73
+ #
74
+ # @param [Object] model The OpenStudio::Model instance to save to file
75
+ # @param [String] save_directory Folder to save the model in
76
+ # @param [String] name ('in.osm') Option to define a non-standard name
77
+ # @return [String] OSM file name
78
+ #
79
+ def save_osm(model, save_directory, name = 'in.osm')
80
+ osm_filename = File.join(save_directory.to_s, name.to_s)
81
+ File.open(osm_filename, 'w') { |f| f << model.to_s }
82
+ osm_filename
83
+ end
84
+
85
+ # Saves an OpenStudio IDF model object to file
86
+ #
87
+ # @param [Object] model The OpenStudio::Workspace instance to save to file
88
+ # @param [String] save_directory Folder to save the model in
89
+ # @param [String] name ('in.osm') Option to define a non-standard name
90
+ # @return [String] IDF file name
91
+ #
92
+ def save_idf(model_idf, save_directory, name = 'in.idf')
93
+ idf_filename = File.join(save_directory.to_s, name.to_s)
94
+ File.open(idf_filename, 'w') { |f| f << model_idf.to_s }
95
+ idf_filename
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -1,187 +1,187 @@
1
- module OpenStudio
2
- module Workflow
3
- module Util
4
- require 'openstudio/workflow/util/measure'
5
- require 'csv'
6
- require 'rexml/document'
7
-
8
- # This module serves as a wrapper around various post-processing tasks used to manage outputs
9
- # @todo (rhorsey) ummmm. So some of this is pretty ugly. Since @dmacumber had ideas about this maybe he can figure
10
- # out what to do about it all
11
- # @todo (nlong) the output adapter restructure will frack up the extraction method royally
12
- #
13
- module PostProcess
14
- # This method loads a sql file into OpenStudio and returns it
15
- #
16
- # @param [String] sql_file Absolute path to the sql file to be loaded
17
- # @return [Object, nil] The OpenStudio::SqlFile object, or nil if it could not be found
18
- #
19
- def load_sql_file(sql_file)
20
- return nil unless File.exist? sql_file
21
- OpenStudio::SqlFile.new(@sql_filename)
22
- end
23
-
24
- # This method parses all sorts of stuff which something needs
25
- #
26
- # @param [String] run_dir The directory that the simulation was run in
27
- # @return [Hash, Hash] results and objective_function (which may be empty) are returned
28
- # @todo (rhorsey) fix the description
29
- #
30
- def run_extract_inputs_and_outputs(run_dir, logger)
31
- # For xml, the measure attributes are in the measure_attributes_xml.json file
32
- # TODO: somehow pass the metadata around on which JSONs to suck into the database
33
- results = {}
34
- # Inputs are in the measure_attributes.json file
35
- if File.exist? "#{run_dir}/measure_attributes.json"
36
- h = JSON.parse(File.read("#{run_dir}/measure_attributes.json"), symbolize_names: true)
37
- h = rename_hash_keys(h, logger)
38
- results.merge! h
39
- end
40
-
41
- logger.info 'Saving the result hash to file'
42
- File.open("#{run_dir}/results.json", 'w') { |f| f << JSON.pretty_generate(results) }
43
-
44
- objective_functions = {}
45
- if @registry[:analysis]
46
- logger.info 'Iterating over Analysis JSON Output Variables'
47
- # Save the objective functions to the object for sending back to the simulation executive
48
- analysis_json = @registry[:analysis]
49
- if analysis_json[:analysis] && analysis_json[:analysis][:output_variables]
50
- analysis_json[:analysis][:output_variables].each do |variable|
51
- # determine which ones are the objective functions (code smell: todo: use enumerator)
52
- if variable[:objective_function]
53
- logger.info "Looking for objective function #{variable[:name]}"
54
- # TODO: move this to cleaner logic. Use ostruct?
55
- k, v = variable[:name].split('.')
56
-
57
- # look for the objective function key and make sure that it is not nil. False is an okay obj function.
58
- if results.key?(k.to_sym) && !results[k.to_sym][v.to_sym].nil?
59
- objective_functions["objective_function_#{variable[:objective_function_index] + 1}"] = results[k.to_sym][v.to_sym]
60
- if variable[:objective_function_target]
61
- logger.info "Found objective function target for #{variable[:name]}"
62
- objective_functions["objective_function_target_#{variable[:objective_function_index] + 1}"] = variable[:objective_function_target].to_f
63
- end
64
- if variable[:scaling_factor]
65
- logger.info "Found scaling factor for #{variable[:name]}"
66
- objective_functions["scaling_factor_#{variable[:objective_function_index] + 1}"] = variable[:scaling_factor].to_f
67
- end
68
- if variable[:objective_function_group]
69
- logger.info "Found objective function group for #{variable[:name]}"
70
- objective_functions["objective_function_group_#{variable[:objective_function_index] + 1}"] = variable[:objective_function_group].to_f
71
- end
72
- else
73
- logger.warn "No results for objective function #{variable[:name]}"
74
- objective_functions["objective_function_#{variable[:objective_function_index] + 1}"] = Float::MAX
75
- objective_functions["objective_function_target_#{variable[:objective_function_index] + 1}"] = nil
76
- objective_functions["scaling_factor_#{variable[:objective_function_index] + 1}"] = nil
77
- objective_functions["objective_function_group_#{variable[:objective_function_index] + 1}"] = nil
78
- end
79
- end
80
- end
81
- end
82
- end
83
-
84
- return results, objective_functions
85
- end
86
-
87
- # Remove any invalid characters in the measure attribute keys. Periods and Pipes are the most problematic
88
- # because mongo does not allow hash keys with periods, and the pipes are used in the map/reduce method that
89
- # was written to speed up the data write in openstudio-server. Also remove any trailing underscores and spaces
90
- #
91
- # @param [Hash] hash Any hash with potentially problematic characters
92
- # @param [Logger] logger Logger to write to
93
- #
94
- def rename_hash_keys(hash, logger)
95
- # @todo should we log the name changes?
96
- regex = /[|!@#\$%^&\*\(\)\{\}\\\[\];:'",<.>\/?\+=]+/
97
-
98
- rename_keys = lambda do |h|
99
- if Hash === h
100
- h.each_key do |key|
101
- if key.to_s =~ regex
102
- logger.warn "Renaming result key '#{key}' to remove invalid characters"
103
- end
104
- end
105
- Hash[h.map { |k, v| [k.to_s.gsub(regex, '_').squeeze('_').gsub(/[_\s]+$/, '').chomp.to_sym, rename_keys[v]] }]
106
- else
107
- h
108
- end
109
- end
110
-
111
- rename_keys[hash]
112
- end
113
-
114
-
115
- # Save reports to a common directory
116
- #
117
- # @param [String] run_dir
118
- # @param [String] directory
119
- # @param [String] logger
120
- #
121
- def gather_reports(run_dir, directory, workflow_json, logger)
122
- logger.info run_dir
123
- logger.info directory
124
-
125
- FileUtils.mkdir_p "#{directory}/reports"
126
-
127
- # try to find the energyplus result file
128
- eplus_html = "#{run_dir}/eplustbl.htm"
129
- if File.exist? eplus_html
130
- # do some encoding on the html if possible
131
- html = File.read(eplus_html)
132
- html = html.force_encoding('ISO-8859-1').encode('utf-8', replace: nil)
133
- logger.info "Saving EnergyPlus HTML report to #{directory}/reports/eplustbl.html"
134
- File.open("#{directory}/reports/eplustbl.html", 'w') { |f| f << html }
135
- end
136
-
137
- # Also, find any "report*.*" files
138
- Dir["#{run_dir}/*/report*.*"].each do |report|
139
- # HRH: This is a temporary work-around to support PAT 2.1 pretty names AND the CLI while we roll a WorkflowJSON solution
140
- measure_dir_name = File.dirname(report).split(File::SEPARATOR).last.gsub(/[0-9][0-9][0-9]_/, '')
141
- measure_xml_path = File.absolute_path(File.join(File.dirname(report), '../../..', 'measures',
142
- measure_dir_name, 'measure.xml'))
143
- logger.info "measure_xml_path: #{measure_xml_path}"
144
- if File.exists? measure_xml_path
145
- measure_xml = REXML::Document.new File.read(measure_xml_path)
146
- measure_class_name = OpenStudio.toUnderscoreCase(measure_xml.root.elements['class_name'].text)
147
- else
148
- measure_class_name = OpenStudio.toUnderscoreCase(measure_dir_name)
149
- end
150
- file_ext = File.extname(report)
151
- append_str = File.basename(report, '.*')
152
- new_file_name = "#{directory}/reports/#{measure_class_name}_#{append_str}#{file_ext}"
153
- logger.info "Saving report #{report} to #{new_file_name}"
154
- FileUtils.copy report, new_file_name
155
- end
156
-
157
- # Remove empty directories in run folder
158
- Dir["#{run_dir}/*"].select { |d| File.directory? d }.select { |d| (Dir.entries(d) - %w(. ..)).empty? }.each do |d|
159
- logger.info "Removing empty directory #{d}"
160
- Dir.rmdir d
161
- end
162
- end
163
-
164
-
165
- # A general post-processing step which could be made significantly more modular
166
- #
167
- # @param [String] run_dir
168
- #
169
- def cleanup(run_dir, directory, logger)
170
-
171
-
172
- paths_to_rm = []
173
- # paths_to_rm << Pathname.glob("#{run_dir}/*.osm")
174
- # paths_to_rm << Pathname.glob("#{run_dir}/*.idf") # keep the idfs
175
- # paths_to_rm << Pathname.glob("*.audit")
176
- # paths_to_rm << Pathname.glob("*.bnd")
177
- # paths_to_rm << Pathname.glob("#{run_dir}/*.eso")
178
- paths_to_rm << Pathname.glob("#{run_dir}/*.mtr")
179
- paths_to_rm << Pathname.glob("#{run_dir}/*.epw")
180
- #paths_to_rm << Pathname.glob("#{run_dir}/*.mtd")
181
- #paths_to_rm << Pathname.glob("#{run_dir}/*.rdd")
182
- paths_to_rm.each { |p| FileUtils.rm_rf(p) }
183
- end
184
- end
185
- end
186
- end
187
- end
1
+ module OpenStudio
2
+ module Workflow
3
+ module Util
4
+ require 'openstudio/workflow/util/measure'
5
+ require 'csv'
6
+ require 'rexml/document'
7
+
8
+ # This module serves as a wrapper around various post-processing tasks used to manage outputs
9
+ # @todo (rhorsey) ummmm. So some of this is pretty ugly. Since @dmacumber had ideas about this maybe he can figure
10
+ # out what to do about it all
11
+ # @todo (nlong) the output adapter restructure will frack up the extraction method royally
12
+ #
13
+ module PostProcess
14
+ # This method loads a sql file into OpenStudio and returns it
15
+ #
16
+ # @param [String] sql_file Absolute path to the sql file to be loaded
17
+ # @return [Object, nil] The OpenStudio::SqlFile object, or nil if it could not be found
18
+ #
19
+ def load_sql_file(sql_file)
20
+ return nil unless File.exist? sql_file
21
+ OpenStudio::SqlFile.new(@sql_filename)
22
+ end
23
+
24
+ # This method parses all sorts of stuff which something needs
25
+ #
26
+ # @param [String] run_dir The directory that the simulation was run in
27
+ # @return [Hash, Hash] results and objective_function (which may be empty) are returned
28
+ # @todo (rhorsey) fix the description
29
+ #
30
+ def run_extract_inputs_and_outputs(run_dir, logger)
31
+ # For xml, the measure attributes are in the measure_attributes_xml.json file
32
+ # TODO: somehow pass the metadata around on which JSONs to suck into the database
33
+ results = {}
34
+ # Inputs are in the measure_attributes.json file
35
+ if File.exist? "#{run_dir}/measure_attributes.json"
36
+ h = JSON.parse(File.read("#{run_dir}/measure_attributes.json"), symbolize_names: true)
37
+ h = rename_hash_keys(h, logger)
38
+ results.merge! h
39
+ end
40
+
41
+ logger.info 'Saving the result hash to file'
42
+ File.open("#{run_dir}/results.json", 'w') { |f| f << JSON.pretty_generate(results) }
43
+
44
+ objective_functions = {}
45
+ if @registry[:analysis]
46
+ logger.info 'Iterating over Analysis JSON Output Variables'
47
+ # Save the objective functions to the object for sending back to the simulation executive
48
+ analysis_json = @registry[:analysis]
49
+ if analysis_json[:analysis] && analysis_json[:analysis][:output_variables]
50
+ analysis_json[:analysis][:output_variables].each do |variable|
51
+ # determine which ones are the objective functions (code smell: todo: use enumerator)
52
+ if variable[:objective_function]
53
+ logger.info "Looking for objective function #{variable[:name]}"
54
+ # TODO: move this to cleaner logic. Use ostruct?
55
+ k, v = variable[:name].split('.')
56
+
57
+ # look for the objective function key and make sure that it is not nil. False is an okay obj function.
58
+ if results.key?(k.to_sym) && !results[k.to_sym][v.to_sym].nil?
59
+ objective_functions["objective_function_#{variable[:objective_function_index] + 1}"] = results[k.to_sym][v.to_sym]
60
+ if variable[:objective_function_target]
61
+ logger.info "Found objective function target for #{variable[:name]}"
62
+ objective_functions["objective_function_target_#{variable[:objective_function_index] + 1}"] = variable[:objective_function_target].to_f
63
+ end
64
+ if variable[:scaling_factor]
65
+ logger.info "Found scaling factor for #{variable[:name]}"
66
+ objective_functions["scaling_factor_#{variable[:objective_function_index] + 1}"] = variable[:scaling_factor].to_f
67
+ end
68
+ if variable[:objective_function_group]
69
+ logger.info "Found objective function group for #{variable[:name]}"
70
+ objective_functions["objective_function_group_#{variable[:objective_function_index] + 1}"] = variable[:objective_function_group].to_f
71
+ end
72
+ else
73
+ logger.warn "No results for objective function #{variable[:name]}"
74
+ objective_functions["objective_function_#{variable[:objective_function_index] + 1}"] = Float::MAX
75
+ objective_functions["objective_function_target_#{variable[:objective_function_index] + 1}"] = nil
76
+ objective_functions["scaling_factor_#{variable[:objective_function_index] + 1}"] = nil
77
+ objective_functions["objective_function_group_#{variable[:objective_function_index] + 1}"] = nil
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ return results, objective_functions
85
+ end
86
+
87
+ # Remove any invalid characters in the measure attribute keys. Periods and Pipes are the most problematic
88
+ # because mongo does not allow hash keys with periods, and the pipes are used in the map/reduce method that
89
+ # was written to speed up the data write in openstudio-server. Also remove any trailing underscores and spaces
90
+ #
91
+ # @param [Hash] hash Any hash with potentially problematic characters
92
+ # @param [Logger] logger Logger to write to
93
+ #
94
+ def rename_hash_keys(hash, logger)
95
+ # @todo should we log the name changes?
96
+ regex = /[|!@#\$%^&\*\(\)\{\}\\\[\];:'",<.>\/?\+=]+/
97
+
98
+ rename_keys = lambda do |h|
99
+ if Hash === h
100
+ h.each_key do |key|
101
+ if key.to_s =~ regex
102
+ logger.warn "Renaming result key '#{key}' to remove invalid characters"
103
+ end
104
+ end
105
+ Hash[h.map { |k, v| [k.to_s.gsub(regex, '_').squeeze('_').gsub(/[_\s]+$/, '').chomp.to_sym, rename_keys[v]] }]
106
+ else
107
+ h
108
+ end
109
+ end
110
+
111
+ rename_keys[hash]
112
+ end
113
+
114
+
115
+ # Save reports to a common directory
116
+ #
117
+ # @param [String] run_dir
118
+ # @param [String] directory
119
+ # @param [String] logger
120
+ #
121
+ def gather_reports(run_dir, directory, workflow_json, logger)
122
+ logger.info run_dir
123
+ logger.info directory
124
+
125
+ FileUtils.mkdir_p "#{directory}/reports"
126
+
127
+ # try to find the energyplus result file
128
+ eplus_html = "#{run_dir}/eplustbl.htm"
129
+ if File.exist? eplus_html
130
+ # do some encoding on the html if possible
131
+ html = File.read(eplus_html)
132
+ html = html.force_encoding('ISO-8859-1').encode('utf-8', replace: nil)
133
+ logger.info "Saving EnergyPlus HTML report to #{directory}/reports/eplustbl.html"
134
+ File.open("#{directory}/reports/eplustbl.html", 'w') { |f| f << html }
135
+ end
136
+
137
+ # Also, find any "report*.*" files
138
+ Dir["#{run_dir}/*/report*.*"].each do |report|
139
+ # HRH: This is a temporary work-around to support PAT 2.1 pretty names AND the CLI while we roll a WorkflowJSON solution
140
+ measure_dir_name = File.dirname(report).split(File::SEPARATOR).last.gsub(/[0-9][0-9][0-9]_/, '')
141
+ measure_xml_path = File.absolute_path(File.join(File.dirname(report), '../../..', 'measures',
142
+ measure_dir_name, 'measure.xml'))
143
+ logger.info "measure_xml_path: #{measure_xml_path}"
144
+ if File.exists? measure_xml_path
145
+ measure_xml = REXML::Document.new File.read(measure_xml_path)
146
+ measure_class_name = OpenStudio.toUnderscoreCase(measure_xml.root.elements['class_name'].text)
147
+ else
148
+ measure_class_name = OpenStudio.toUnderscoreCase(measure_dir_name)
149
+ end
150
+ file_ext = File.extname(report)
151
+ append_str = File.basename(report, '.*')
152
+ new_file_name = "#{directory}/reports/#{measure_class_name}_#{append_str}#{file_ext}"
153
+ logger.info "Saving report #{report} to #{new_file_name}"
154
+ FileUtils.copy report, new_file_name
155
+ end
156
+
157
+ # Remove empty directories in run folder
158
+ Dir["#{run_dir}/*"].select { |d| File.directory? d }.select { |d| (Dir.entries(d) - %w(. ..)).empty? }.each do |d|
159
+ logger.info "Removing empty directory #{d}"
160
+ Dir.rmdir d
161
+ end
162
+ end
163
+
164
+
165
+ # A general post-processing step which could be made significantly more modular
166
+ #
167
+ # @param [String] run_dir
168
+ #
169
+ def cleanup(run_dir, directory, logger)
170
+
171
+
172
+ paths_to_rm = []
173
+ # paths_to_rm << Pathname.glob("#{run_dir}/*.osm")
174
+ # paths_to_rm << Pathname.glob("#{run_dir}/*.idf") # keep the idfs
175
+ # paths_to_rm << Pathname.glob("*.audit")
176
+ # paths_to_rm << Pathname.glob("*.bnd")
177
+ # paths_to_rm << Pathname.glob("#{run_dir}/*.eso")
178
+ paths_to_rm << Pathname.glob("#{run_dir}/*.mtr")
179
+ paths_to_rm << Pathname.glob("#{run_dir}/*.epw")
180
+ #paths_to_rm << Pathname.glob("#{run_dir}/*.mtd")
181
+ #paths_to_rm << Pathname.glob("#{run_dir}/*.rdd")
182
+ paths_to_rm.each { |p| FileUtils.rm_rf(p) }
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end