openstudio-workflow 1.3.3 → 1.3.4

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 +77 -72
  3. data/README.md +93 -93
  4. data/Rakefile +36 -36
  5. data/lib/openstudio-workflow.rb +65 -49
  6. data/lib/openstudio/workflow/adapters/input/local.rb +324 -301
  7. data/lib/openstudio/workflow/adapters/output/local.rb +161 -97
  8. data/lib/openstudio/workflow/adapters/output/socket.rb +107 -91
  9. data/lib/openstudio/workflow/adapters/output/web.rb +82 -66
  10. data/lib/openstudio/workflow/adapters/output_adapter.rb +163 -147
  11. data/lib/openstudio/workflow/job.rb +57 -22
  12. data/lib/openstudio/workflow/jobs/resources/monthly_report.idf +222 -222
  13. data/lib/openstudio/workflow/jobs/run_energyplus.rb +70 -54
  14. data/lib/openstudio/workflow/jobs/run_ep_measures.rb +73 -57
  15. data/lib/openstudio/workflow/jobs/run_initialization.rb +203 -171
  16. data/lib/openstudio/workflow/jobs/run_os_measures.rb +89 -73
  17. data/lib/openstudio/workflow/jobs/run_postprocess.rb +73 -57
  18. data/lib/openstudio/workflow/jobs/run_preprocess.rb +104 -80
  19. data/lib/openstudio/workflow/jobs/run_reporting_measures.rb +118 -102
  20. data/lib/openstudio/workflow/jobs/run_translation.rb +84 -68
  21. data/lib/openstudio/workflow/multi_delegator.rb +62 -46
  22. data/lib/openstudio/workflow/registry.rb +172 -137
  23. data/lib/openstudio/workflow/run.rb +328 -312
  24. data/lib/openstudio/workflow/time_logger.rb +96 -53
  25. data/lib/openstudio/workflow/util.rb +49 -14
  26. data/lib/openstudio/workflow/util/energyplus.rb +605 -570
  27. data/lib/openstudio/workflow/util/io.rb +68 -33
  28. data/lib/openstudio/workflow/util/measure.rb +650 -615
  29. data/lib/openstudio/workflow/util/model.rb +151 -100
  30. data/lib/openstudio/workflow/util/post_process.rb +238 -187
  31. data/lib/openstudio/workflow/util/weather_file.rb +143 -108
  32. data/lib/openstudio/workflow/version.rb +40 -24
  33. data/lib/openstudio/workflow_json.rb +476 -443
  34. data/lib/openstudio/workflow_runner.rb +268 -252
  35. metadata +23 -23
@@ -1,100 +1,151 @@
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
+ # *******************************************************************************
2
+ # OpenStudio(R), Copyright (c) 2008-2018, Alliance for Sustainable Energy, LLC.
3
+ # All rights reserved.
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are met:
6
+ #
7
+ # (1) Redistributions of source code must retain the above copyright notice,
8
+ # this list of conditions and the following disclaimer.
9
+ #
10
+ # (2) Redistributions in binary form must reproduce the above copyright notice,
11
+ # this list of conditions and the following disclaimer in the documentation
12
+ # and/or other materials provided with the distribution.
13
+ #
14
+ # (3) Neither the name of the copyright holder nor the names of any contributors
15
+ # may be used to endorse or promote products derived from this software without
16
+ # specific prior written permission from the respective party.
17
+ #
18
+ # (4) Other than as required in clauses (1) and (2), distributions in any form
19
+ # of modifications or other derivative works may not use the "OpenStudio"
20
+ # trademark, "OS", "os", or any other confusingly similar designation without
21
+ # specific prior written permission from Alliance for Sustainable Energy, LLC.
22
+ #
23
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
24
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
25
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
26
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER, THE UNITED STATES
27
+ # GOVERNMENT, OR ANY CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
28
+ # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
29
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
30
+ # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
31
+ # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
32
+ # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
33
+ # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34
+ # *******************************************************************************
35
+
36
+ module OpenStudio
37
+ module Workflow
38
+ module Util
39
+ # Manages routine tasks involving OpenStudio::Model or OpenStudio::Workflow objects, such as loading, saving, and
40
+ # translating them.
41
+ #
42
+ module Model
43
+ # Method to create / load an OSM file
44
+ #
45
+ # @param [String] osm_path The full path to an OSM file to load
46
+ # @param [Object] logger An optional logger to use for finding the OSM model
47
+ # @return [Object] The return from this method is a loaded OSM or a failure.
48
+ #
49
+ def load_osm(osm_path, logger)
50
+ logger.info 'Loading OSM model'
51
+
52
+ # Load the model and return it
53
+ logger.info "Reading in OSM model #{osm_path}"
54
+
55
+ loaded_model = nil
56
+ begin
57
+ translator = OpenStudio::OSVersion::VersionTranslator.new
58
+ loaded_model = translator.loadModel(osm_path)
59
+ rescue
60
+ # TODO: get translator working in embedded.
61
+ # Need to embed idd files
62
+ logger.warn 'OpenStudio VersionTranslator could not be loaded'
63
+ loaded_model = OpenStudio::Model::Model.load(osm_path)
64
+ end
65
+ raise "Failed to load OSM file #{osm_path}" if loaded_model.empty?
66
+ loaded_model.get
67
+ end
68
+
69
+ # Method to create / load an IDF file
70
+ #
71
+ # @param [String] idf_path Full path to the IDF
72
+ # @param [Object] logger An optional logger to use for finding the idf model
73
+ # @return [Object] The return from this method is a loaded IDF or a failure.
74
+ #
75
+ def load_idf(idf_path, logger)
76
+ logger.info 'Loading IDF model'
77
+
78
+ # Load the IDF into a workspace object and return it
79
+ logger.info "Reading in IDF model #{idf_path}"
80
+
81
+ idf = OpenStudio::Workspace.load(idf_path)
82
+ raise "Failed to load IDF file #{idf_path}" if idf.empty?
83
+ idf.get
84
+ end
85
+
86
+ # Translates a OpenStudio model object into an OpenStudio IDF object
87
+ #
88
+ # @param [Object] model the OpenStudio::Model instance to translate into an OpenStudio::Workspace object -- see
89
+ # the OpenStudio SDK for details on the process
90
+ # @return [Object] Returns and OpenStudio::Workspace object
91
+ # @todo (rhorsey) rescue errors here
92
+ #
93
+ def translate_to_energyplus(model, logger = nil)
94
+ logger = ::Logger.new(STDOUT) unless logger
95
+ logger.info 'Translate object to EnergyPlus IDF in preparation for EnergyPlus'
96
+ a = ::Time.now
97
+ # ensure objects exist for reporting purposes
98
+ model.getFacility
99
+ model.getBuilding
100
+ forward_translator = OpenStudio::EnergyPlus::ForwardTranslator.new
101
+ model_idf = forward_translator.translateModel(model)
102
+ b = ::Time.now
103
+ logger.info "Translate object to EnergyPlus IDF took #{b.to_f - a.to_f}"
104
+ model_idf
105
+ end
106
+
107
+ # Saves an OpenStudio model object to file
108
+ #
109
+ # @param [Object] model The OpenStudio::Model instance to save to file
110
+ # @param [String] save_directory Folder to save the model in
111
+ # @param [String] name ('in.osm') Option to define a non-standard name
112
+ # @return [String] OSM file name
113
+ #
114
+ def save_osm(model, save_directory, name = 'in.osm')
115
+ osm_filename = File.join(save_directory.to_s, name.to_s)
116
+ File.open(osm_filename, 'w') do |f|
117
+ f << model.to_s
118
+ # make sure data is written to the disk one way or the other
119
+ begin
120
+ f.fsync
121
+ rescue
122
+ f.flush
123
+ end
124
+ end
125
+ osm_filename
126
+ end
127
+
128
+ # Saves an OpenStudio IDF model object to file
129
+ #
130
+ # @param [Object] model The OpenStudio::Workspace instance to save to file
131
+ # @param [String] save_directory Folder to save the model in
132
+ # @param [String] name ('in.osm') Option to define a non-standard name
133
+ # @return [String] IDF file name
134
+ #
135
+ def save_idf(model_idf, save_directory, name = 'in.idf')
136
+ idf_filename = File.join(save_directory.to_s, name.to_s)
137
+ File.open(idf_filename, 'w') do |f|
138
+ f << model_idf.to_s
139
+ # make sure data is written to the disk one way or the other
140
+ begin
141
+ f.fsync
142
+ rescue
143
+ f.flush
144
+ end
145
+ end
146
+ idf_filename
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
@@ -1,187 +1,238 @@
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
+ # *******************************************************************************
2
+ # OpenStudio(R), Copyright (c) 2008-2018, Alliance for Sustainable Energy, LLC.
3
+ # All rights reserved.
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are met:
6
+ #
7
+ # (1) Redistributions of source code must retain the above copyright notice,
8
+ # this list of conditions and the following disclaimer.
9
+ #
10
+ # (2) Redistributions in binary form must reproduce the above copyright notice,
11
+ # this list of conditions and the following disclaimer in the documentation
12
+ # and/or other materials provided with the distribution.
13
+ #
14
+ # (3) Neither the name of the copyright holder nor the names of any contributors
15
+ # may be used to endorse or promote products derived from this software without
16
+ # specific prior written permission from the respective party.
17
+ #
18
+ # (4) Other than as required in clauses (1) and (2), distributions in any form
19
+ # of modifications or other derivative works may not use the "OpenStudio"
20
+ # trademark, "OS", "os", or any other confusingly similar designation without
21
+ # specific prior written permission from Alliance for Sustainable Energy, LLC.
22
+ #
23
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
24
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
25
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
26
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER, THE UNITED STATES
27
+ # GOVERNMENT, OR ANY CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
28
+ # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
29
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
30
+ # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
31
+ # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
32
+ # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
33
+ # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34
+ # *******************************************************************************
35
+
36
+ module OpenStudio
37
+ module Workflow
38
+ module Util
39
+ require 'openstudio/workflow/util/measure'
40
+ require 'csv'
41
+ require 'rexml/document'
42
+
43
+ # This module serves as a wrapper around various post-processing tasks used to manage outputs
44
+ # @todo (rhorsey) ummmm. So some of this is pretty ugly. Since @dmacumber had ideas about this maybe he can figure
45
+ # out what to do about it all
46
+ # @todo (nlong) the output adapter restructure will frack up the extraction method royally
47
+ #
48
+ module PostProcess
49
+ # This method loads a sql file into OpenStudio and returns it
50
+ #
51
+ # @param [String] sql_file Absolute path to the sql file to be loaded
52
+ # @return [Object, nil] The OpenStudio::SqlFile object, or nil if it could not be found
53
+ #
54
+ def load_sql_file(sql_file)
55
+ return nil unless File.exist? sql_file
56
+ OpenStudio::SqlFile.new(@sql_filename)
57
+ end
58
+
59
+ # This method parses all sorts of stuff which something needs
60
+ #
61
+ # @param [String] run_dir The directory that the simulation was run in
62
+ # @return [Hash, Hash] results and objective_function (which may be empty) are returned
63
+ # @todo (rhorsey) fix the description
64
+ #
65
+ def run_extract_inputs_and_outputs(run_dir, logger)
66
+ # For xml, the measure attributes are in the measure_attributes_xml.json file
67
+ # TODO: somehow pass the metadata around on which JSONs to suck into the database
68
+ results = {}
69
+ # Inputs are in the measure_attributes.json file
70
+ if File.exist? "#{run_dir}/measure_attributes.json"
71
+ h = JSON.parse(File.read("#{run_dir}/measure_attributes.json"), symbolize_names: true)
72
+ h = rename_hash_keys(h, logger)
73
+ results.merge! h
74
+ end
75
+
76
+ logger.info 'Saving the result hash to file'
77
+ File.open("#{run_dir}/results.json", 'w') do |f|
78
+ f << JSON.pretty_generate(results)
79
+ # make sure data is written to the disk one way or the other
80
+ begin
81
+ f.fsync
82
+ rescue
83
+ f.flush
84
+ end
85
+ end
86
+
87
+ objective_functions = {}
88
+ if @registry[:analysis]
89
+ logger.info 'Iterating over Analysis JSON Output Variables'
90
+ # Save the objective functions to the object for sending back to the simulation executive
91
+ analysis_json = @registry[:analysis]
92
+ if analysis_json[:analysis] && analysis_json[:analysis][:output_variables]
93
+ analysis_json[:analysis][:output_variables].each do |variable|
94
+ # determine which ones are the objective functions (code smell: todo: use enumerator)
95
+ if variable[:objective_function]
96
+ logger.info "Looking for objective function #{variable[:name]}"
97
+ # TODO: move this to cleaner logic. Use ostruct?
98
+ k, v = variable[:name].split('.')
99
+
100
+ # look for the objective function key and make sure that it is not nil. False is an okay obj function.
101
+ if results.key?(k.to_sym) && !results[k.to_sym][v.to_sym].nil?
102
+ objective_functions["objective_function_#{variable[:objective_function_index] + 1}"] = results[k.to_sym][v.to_sym]
103
+ if variable[:objective_function_target]
104
+ logger.info "Found objective function target for #{variable[:name]}"
105
+ objective_functions["objective_function_target_#{variable[:objective_function_index] + 1}"] = variable[:objective_function_target].to_f
106
+ end
107
+ if variable[:scaling_factor]
108
+ logger.info "Found scaling factor for #{variable[:name]}"
109
+ objective_functions["scaling_factor_#{variable[:objective_function_index] + 1}"] = variable[:scaling_factor].to_f
110
+ end
111
+ if variable[:objective_function_group]
112
+ logger.info "Found objective function group for #{variable[:name]}"
113
+ objective_functions["objective_function_group_#{variable[:objective_function_index] + 1}"] = variable[:objective_function_group].to_f
114
+ end
115
+ else
116
+ logger.warn "No results for objective function #{variable[:name]}"
117
+ objective_functions["objective_function_#{variable[:objective_function_index] + 1}"] = Float::MAX
118
+ objective_functions["objective_function_target_#{variable[:objective_function_index] + 1}"] = nil
119
+ objective_functions["scaling_factor_#{variable[:objective_function_index] + 1}"] = nil
120
+ objective_functions["objective_function_group_#{variable[:objective_function_index] + 1}"] = nil
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
126
+
127
+ return results, objective_functions
128
+ end
129
+
130
+ # Remove any invalid characters in the measure attribute keys. Periods and Pipes are the most problematic
131
+ # because mongo does not allow hash keys with periods, and the pipes are used in the map/reduce method that
132
+ # was written to speed up the data write in openstudio-server. Also remove any trailing underscores and spaces
133
+ #
134
+ # @param [Hash] hash Any hash with potentially problematic characters
135
+ # @param [Logger] logger Logger to write to
136
+ #
137
+ def rename_hash_keys(hash, logger)
138
+ # @todo should we log the name changes?
139
+ regex = /[|!@#\$%^&\*\(\)\{\}\\\[\];:'",<.>\/?\+=]+/
140
+
141
+ rename_keys = lambda do |h|
142
+ if Hash === h
143
+ h.each_key do |key|
144
+ if key.to_s =~ regex
145
+ logger.warn "Renaming result key '#{key}' to remove invalid characters"
146
+ end
147
+ end
148
+ Hash[h.map { |k, v| [k.to_s.gsub(regex, '_').squeeze('_').gsub(/[_\s]+$/, '').chomp.to_sym, rename_keys[v]] }]
149
+ else
150
+ h
151
+ end
152
+ end
153
+
154
+ rename_keys[hash]
155
+ end
156
+
157
+
158
+ # Save reports to a common directory
159
+ #
160
+ # @param [String] run_dir
161
+ # @param [String] directory
162
+ # @param [String] logger
163
+ #
164
+ def gather_reports(run_dir, directory, workflow_json, logger)
165
+ logger.info run_dir
166
+ logger.info directory
167
+
168
+ FileUtils.mkdir_p "#{directory}/reports"
169
+
170
+ # try to find the energyplus result file
171
+ eplus_html = "#{run_dir}/eplustbl.htm"
172
+ if File.exist? eplus_html
173
+ # do some encoding on the html if possible
174
+ html = File.read(eplus_html)
175
+ html = html.force_encoding('ISO-8859-1').encode('utf-8', replace: nil)
176
+ logger.info "Saving EnergyPlus HTML report to #{directory}/reports/eplustbl.html"
177
+ File.open("#{directory}/reports/eplustbl.html", 'w') do |f|
178
+ f << html
179
+ # make sure data is written to the disk one way or the other
180
+ begin
181
+ f.fsync
182
+ rescue
183
+ f.flush
184
+ end
185
+ end
186
+ end
187
+
188
+ # Also, find any "report*.*" files
189
+ Dir["#{run_dir}/*/report*.*"].each do |report|
190
+ # HRH: This is a temporary work-around to support PAT 2.1 pretty names AND the CLI while we roll a WorkflowJSON solution
191
+ measure_dir_name = File.dirname(report).split(File::SEPARATOR).last.gsub(/[0-9][0-9][0-9]_/, '')
192
+ measure_xml_path = File.absolute_path(File.join(File.dirname(report), '../../..', 'measures',
193
+ measure_dir_name, 'measure.xml'))
194
+ logger.info "measure_xml_path: #{measure_xml_path}"
195
+ if File.exists? measure_xml_path
196
+ measure_xml = REXML::Document.new File.read(measure_xml_path)
197
+ measure_class_name = OpenStudio.toUnderscoreCase(measure_xml.root.elements['class_name'].text)
198
+ else
199
+ measure_class_name = OpenStudio.toUnderscoreCase(measure_dir_name)
200
+ end
201
+ file_ext = File.extname(report)
202
+ append_str = File.basename(report, '.*')
203
+ new_file_name = "#{directory}/reports/#{measure_class_name}_#{append_str}#{file_ext}"
204
+ logger.info "Saving report #{report} to #{new_file_name}"
205
+ FileUtils.copy report, new_file_name
206
+ end
207
+
208
+ # Remove empty directories in run folder
209
+ Dir["#{run_dir}/*"].select { |d| File.directory? d }.select { |d| (Dir.entries(d) - %w(. ..)).empty? }.each do |d|
210
+ logger.info "Removing empty directory #{d}"
211
+ Dir.rmdir d
212
+ end
213
+ end
214
+
215
+
216
+ # A general post-processing step which could be made significantly more modular
217
+ #
218
+ # @param [String] run_dir
219
+ #
220
+ def cleanup(run_dir, directory, logger)
221
+
222
+
223
+ paths_to_rm = []
224
+ # paths_to_rm << Pathname.glob("#{run_dir}/*.osm")
225
+ # paths_to_rm << Pathname.glob("#{run_dir}/*.idf") # keep the idfs
226
+ # paths_to_rm << Pathname.glob("*.audit")
227
+ # paths_to_rm << Pathname.glob("*.bnd")
228
+ # paths_to_rm << Pathname.glob("#{run_dir}/*.eso")
229
+ paths_to_rm << Pathname.glob("#{run_dir}/*.mtr")
230
+ paths_to_rm << Pathname.glob("#{run_dir}/*.epw")
231
+ #paths_to_rm << Pathname.glob("#{run_dir}/*.mtd")
232
+ #paths_to_rm << Pathname.glob("#{run_dir}/*.rdd")
233
+ paths_to_rm.each { |p| FileUtils.rm_rf(p) }
234
+ end
235
+ end
236
+ end
237
+ end
238
+ end