openstudio-workflow 0.0.1
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 +7 -0
- data/CHANGELOG.md +10 -0
- data/README.md +97 -0
- data/Rakefile +20 -0
- data/lib/openstudio/workflow/adapter.rb +68 -0
- data/lib/openstudio/workflow/adapters/local.rb +110 -0
- data/lib/openstudio/workflow/adapters/mongo.rb +259 -0
- data/lib/openstudio/workflow/jobs/run_energyplus/run_energyplus.rb +128 -0
- data/lib/openstudio/workflow/jobs/run_openstudio/monthly_report.idf +218 -0
- data/lib/openstudio/workflow/jobs/run_openstudio/run_openstudio.rb +344 -0
- data/lib/openstudio/workflow/jobs/run_postprocess/packaged_measures/README.md +5 -0
- data/lib/openstudio/workflow/jobs/run_postprocess/packaged_measures/StandardReports/measure.rb +212 -0
- data/lib/openstudio/workflow/jobs/run_postprocess/packaged_measures/StandardReports/measure.xml +53 -0
- data/lib/openstudio/workflow/jobs/run_postprocess/packaged_measures/StandardReports/resources/report.html.in +298 -0
- data/lib/openstudio/workflow/jobs/run_postprocess/run_postprocess.rb +549 -0
- data/lib/openstudio/workflow/jobs/run_preflight/run_preflight.rb +45 -0
- data/lib/openstudio/workflow/jobs/run_xml/run_xml.rb +279 -0
- data/lib/openstudio/workflow/multi_delegator.rb +48 -0
- data/lib/openstudio/workflow/run.rb +258 -0
- data/lib/openstudio/workflow/version.rb +24 -0
- data/lib/openstudio-workflow.rb +69 -0
- metadata +134 -0
@@ -0,0 +1,45 @@
|
|
1
|
+
######################################################################
|
2
|
+
# Copyright (c) 2008-2014, Alliance for Sustainable Energy.
|
3
|
+
# All rights reserved.
|
4
|
+
#
|
5
|
+
# This library is free software; you can redistribute it and/or
|
6
|
+
# modify it under the terms of the GNU Lesser General Public
|
7
|
+
# License as published by the Free Software Foundation; either
|
8
|
+
# version 2.1 of the License, or (at your option) any later version.
|
9
|
+
#
|
10
|
+
# This library is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
13
|
+
# Lesser General Public License for more details.
|
14
|
+
#
|
15
|
+
# You should have received a copy of the GNU Lesser General Public
|
16
|
+
# License along with this library; if not, write to the Free Software
|
17
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
18
|
+
######################################################################
|
19
|
+
|
20
|
+
|
21
|
+
# Run Prelight job to prepare the directory for simulations.
|
22
|
+
class RunPreflight
|
23
|
+
|
24
|
+
def initialize(directory, logger, adapter, options = {})
|
25
|
+
defaults = {}
|
26
|
+
@options = defaults.merge(options)
|
27
|
+
@directory = directory
|
28
|
+
@adapter = adapter
|
29
|
+
@results = {}
|
30
|
+
@logger = logger
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
def perform
|
35
|
+
@logger.info "Calling #{__method__} in the #{self.class} class"
|
36
|
+
|
37
|
+
|
38
|
+
@adapter.communicate_started @directory, @options
|
39
|
+
|
40
|
+
# Add the moment this does nothing.
|
41
|
+
|
42
|
+
# return the results back to the caller--always
|
43
|
+
@results
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,279 @@
|
|
1
|
+
######################################################################
|
2
|
+
# Copyright (c) 2008-2014, Alliance for Sustainable Energy.
|
3
|
+
# All rights reserved.
|
4
|
+
#
|
5
|
+
# This library is free software; you can redistribute it and/or
|
6
|
+
# modify it under the terms of the GNU Lesser General Public
|
7
|
+
# License as published by the Free Software Foundation; either
|
8
|
+
# version 2.1 of the License, or (at your option) any later version.
|
9
|
+
#
|
10
|
+
# This library is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
13
|
+
# Lesser General Public License for more details.
|
14
|
+
#
|
15
|
+
# You should have received a copy of the GNU Lesser General Public
|
16
|
+
# License along with this library; if not, write to the Free Software
|
17
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
18
|
+
######################################################################
|
19
|
+
|
20
|
+
require 'libxml'
|
21
|
+
|
22
|
+
# This actually belongs as another class that gets added as a state dynamically
|
23
|
+
class RunXml
|
24
|
+
|
25
|
+
CRASH_ON_NO_WORKFLOW_VARIABLE = FALSE
|
26
|
+
# RunXml
|
27
|
+
def initialize(directory, logger, adapter, options = {})
|
28
|
+
defaults = {use_monthly_reports: false, analysis_root_path: '.', xml_library_file: 'xml_runner.rb'}
|
29
|
+
@options = defaults.merge(options)
|
30
|
+
@directory = directory
|
31
|
+
# TODO: there is a base number of arguments that each job will need including @run_directory. abstract it out.
|
32
|
+
@run_directory = "#{@directory}/run"
|
33
|
+
@adapter = adapter
|
34
|
+
@results = {}
|
35
|
+
@logger = logger
|
36
|
+
@logger.info "#{self.class} passed the following options #{@options}"
|
37
|
+
|
38
|
+
# initialize instance variables that are needed in the perform section
|
39
|
+
@weather_filename = nil
|
40
|
+
@weather_directory = File.expand_path(File.join(@options[:analysis_root_path], "weather"))
|
41
|
+
@logger.info "Weather directory is: #{@weather_directory}"
|
42
|
+
@model_xml = nil
|
43
|
+
@model = nil
|
44
|
+
@model_idf = nil
|
45
|
+
@analysis_json = nil
|
46
|
+
# TODO: rename datapoint_json to just datapoint
|
47
|
+
@datapoint_json = nil
|
48
|
+
@output_attributes = {}
|
49
|
+
@report_measures = []
|
50
|
+
@measure_type_lookup = {
|
51
|
+
:openstudio_measure => 'RubyMeasure',
|
52
|
+
:energyplus_measure => 'EnergyPlusMeasure',
|
53
|
+
:reporting_measure => 'ReportingMeasure'
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
def perform
|
59
|
+
@logger.info "Calling #{__method__} in the #{self.class} class"
|
60
|
+
@logger.info "Current directory is #{@directory}"
|
61
|
+
|
62
|
+
@logger.info "Retrieving datapoint and problem"
|
63
|
+
@datapoint_json = @adapter.get_datapoint(@directory, @options)
|
64
|
+
@analysis_json = @adapter.get_problem(@directory, @options)
|
65
|
+
|
66
|
+
if @analysis_json && @analysis_json[:analysis]
|
67
|
+
@model_xml = load_xml_model
|
68
|
+
@weather_filename = load_weather_file
|
69
|
+
|
70
|
+
apply_xml_measures
|
71
|
+
|
72
|
+
@logger.info "XML measure output attributes JSON is #{@output_attributes}"
|
73
|
+
File.open("#{@run_directory}/measure_attributes_xml.json", 'w') {
|
74
|
+
|f| f << JSON.pretty_generate(@output_attributes)
|
75
|
+
}
|
76
|
+
end
|
77
|
+
|
78
|
+
create_osm_from_xml
|
79
|
+
|
80
|
+
@results
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def load_xml_model
|
86
|
+
model = nil
|
87
|
+
@logger.info 'Loading seed model'
|
88
|
+
|
89
|
+
if @analysis_json[:analysis][:seed]
|
90
|
+
@logger.info "Seed model is #{@analysis_json[:analysis][:seed]}"
|
91
|
+
if @analysis_json[:analysis][:seed][:path]
|
92
|
+
|
93
|
+
# assume that the seed model has been placed in the directory
|
94
|
+
baseline_model_path = File.expand_path(
|
95
|
+
File.join(@options[:analysis_root_path], @analysis_json[:analysis][:seed][:path]))
|
96
|
+
|
97
|
+
if File.exist? baseline_model_path
|
98
|
+
@logger.info "Reading in baseline model #{baseline_model_path}"
|
99
|
+
model = LibXML::XML::Document.file(baseline_model_path)
|
100
|
+
fail 'XML model is nil' if model.nil?
|
101
|
+
|
102
|
+
model.save("#{@run_directory}/original.xml")
|
103
|
+
else
|
104
|
+
fail "Seed model '#{baseline_model_path}' did not exist"
|
105
|
+
end
|
106
|
+
else
|
107
|
+
fail 'No seed model path in JSON defined'
|
108
|
+
end
|
109
|
+
else
|
110
|
+
fail 'No seed model block'
|
111
|
+
end
|
112
|
+
|
113
|
+
model
|
114
|
+
end
|
115
|
+
|
116
|
+
# Save the weather file to the instance variable
|
117
|
+
def load_weather_file
|
118
|
+
weather_filename = nil
|
119
|
+
if @analysis_json[:analysis][:weather_file]
|
120
|
+
if @analysis_json[:analysis][:weather_file][:path]
|
121
|
+
# This last(4) needs to be cleaned up. Why don't we know the path of the file?
|
122
|
+
# assume that the seed model has been placed in the directory
|
123
|
+
weather_filename = File.expand_path(
|
124
|
+
File.join(@options[:analysis_root_path], @analysis_json[:analysis][:weather_file][:path]))
|
125
|
+
unless File.exist?(weather_filename)
|
126
|
+
@logger.warn "Could not find weather file for simulation #{weather_filename}. Will continue because may change"
|
127
|
+
end
|
128
|
+
|
129
|
+
|
130
|
+
else
|
131
|
+
fail 'No weather file path defined'
|
132
|
+
end
|
133
|
+
else
|
134
|
+
fail 'No weather file block defined'
|
135
|
+
end
|
136
|
+
|
137
|
+
weather_filename
|
138
|
+
end
|
139
|
+
|
140
|
+
def create_osm_from_xml
|
141
|
+
# Save the final state of the XML file
|
142
|
+
xml_filename = "#{@run_directory}/final.xml"
|
143
|
+
@model_xml.save(xml_filename)
|
144
|
+
|
145
|
+
@logger.info 'Starting XML to OSM translation'
|
146
|
+
|
147
|
+
# set the lib path first -- very specific for this applciation right now
|
148
|
+
@space_lib_path = File.expand_path("#{File.dirname(@options[:xml_library_file])}/space_types")
|
149
|
+
require @options[:xml_library_file]
|
150
|
+
|
151
|
+
@logger.info "The weather file is #{@weather_filename}"
|
152
|
+
begin
|
153
|
+
osxt = Main.new(@weather_directory, @space_lib_path)
|
154
|
+
osm, idf, new_xml, building_name, weather_file = osxt.process(@model_xml.to_s, false, true)
|
155
|
+
rescue Exception => e
|
156
|
+
log_message = "Runner error #{__FILE__} failed with #{e.message}, #{e.backtrace.join("\n")}"
|
157
|
+
fail log_message
|
158
|
+
end
|
159
|
+
|
160
|
+
if osm
|
161
|
+
osm_filename = "#{@run_directory}/xml_out.osm"
|
162
|
+
File.open(osm_filename, 'w') { |f| f << osm }
|
163
|
+
|
164
|
+
@logger.info 'Finished XML to OSM translation'
|
165
|
+
else
|
166
|
+
fail 'No OSM model output from XML translation'
|
167
|
+
end
|
168
|
+
|
169
|
+
@results[:osm_filename] = File.expand_path(osm_filename)
|
170
|
+
@results[:xml_filename] = File.expand_path(xml_filename)
|
171
|
+
@results[:weather_filename] = File.expand_path(File.join(@weather_directory,@weather_filename))
|
172
|
+
end
|
173
|
+
|
174
|
+
def apply_xml_measures
|
175
|
+
# iterate over the workflow and grab the measures
|
176
|
+
if @analysis_json[:analysis][:problem] && @analysis_json[:analysis][:problem][:workflow]
|
177
|
+
@analysis_json[:analysis][:problem][:workflow].each do |wf|
|
178
|
+
if wf[:measure_type] == 'XmlMeasure'
|
179
|
+
# need to map the variables to the XML classes
|
180
|
+
measure_path = wf[:measure_definition_directory]
|
181
|
+
measure_name = wf[:measure_definition_class_name]
|
182
|
+
|
183
|
+
@logger.info "XML Measure path is #{measure_path}"
|
184
|
+
@logger.info "XML Measure name is #{measure_name}"
|
185
|
+
|
186
|
+
@logger.info "Loading measure in relative path #{measure_path}"
|
187
|
+
measure_file_path = File.expand_path(
|
188
|
+
File.join(@options[:analysis_root_path], measure_path, 'measure.rb'))
|
189
|
+
fail "Measure file does not exist #{measure_name} in #{measure_file_path}" unless File.exist? measure_file_path
|
190
|
+
|
191
|
+
require measure_file_path
|
192
|
+
measure = Object.const_get(measure_name).new
|
193
|
+
|
194
|
+
@logger.info "iterate over arguments for workflow item #{wf[:name]}"
|
195
|
+
|
196
|
+
# The Argument hash in the workflow json file looks like the following
|
197
|
+
# {
|
198
|
+
# "display_name": "Set XPath",
|
199
|
+
# "machine_name": "set_xpath",
|
200
|
+
# "name": "xpath",
|
201
|
+
# "value": "/building/blocks/block/envelope/surfaces/window/layout/wwr",
|
202
|
+
# "uuid": "440dcce0-7663-0131-41f1-14109fdf0b37",
|
203
|
+
# "version_uuid": "440e4bd0-7663-0131-41f2-14109fdf0b37"
|
204
|
+
# }
|
205
|
+
args = {}
|
206
|
+
if wf[:arguments]
|
207
|
+
wf[:arguments].each do |wf_arg|
|
208
|
+
if wf_arg[:value]
|
209
|
+
@logger.info "Setting argument value #{wf_arg[:name]} to #{wf_arg[:value]}"
|
210
|
+
# Note that these measures have symbolized hash keys and not strings. I really want indifferential access here!
|
211
|
+
args[wf_arg[:name].to_sym] = wf_arg[:value]
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
@logger.info "iterate over variables for workflow item #{wf[:name]}"
|
217
|
+
if wf[:variables]
|
218
|
+
wf[:variables].each do |wf_var|
|
219
|
+
# Argument hash in workflow looks like the following
|
220
|
+
# "argument": {
|
221
|
+
# "display_name": "Window-To-Wall Ratio",
|
222
|
+
# "machine_name": "window_to_wall_ratio",
|
223
|
+
# "name": "value",
|
224
|
+
# "uuid": "a0618d15-bb0b-4494-a72f-8ad628693a7e",
|
225
|
+
# "version_uuid": "b33cf6b0-f1aa-4706-afab-9470e6bd1912"
|
226
|
+
# },
|
227
|
+
variable_uuid = wf_var[:uuid].to_sym # this is what the variable value is set to
|
228
|
+
if wf_var[:argument]
|
229
|
+
variable_name = wf_var[:argument][:name]
|
230
|
+
|
231
|
+
# Get the value from the data point json that was set via R / Problem Formulation
|
232
|
+
if @datapoint_json[:data_point]
|
233
|
+
if @datapoint_json[:data_point][:set_variable_values]
|
234
|
+
if @datapoint_json[:data_point][:set_variable_values][variable_uuid]
|
235
|
+
@logger.info "Setting variable #{variable_name} to #{@datapoint_json[:data_point][:set_variable_values][variable_uuid]}"
|
236
|
+
|
237
|
+
args[wf_var[:argument][:name].to_sym] = @datapoint_json[:data_point][:set_variable_values][variable_uuid]
|
238
|
+
args["#{wf_var[:argument][:name]}_machine_name".to_sym] = wf_var[:argument][:machine_name]
|
239
|
+
args["#{wf_var[:argument][:name]}_type".to_sym] = wf_var[:value_type] if wf_var[:value_type]
|
240
|
+
@logger.info "Setting the machine name for argument '#{wf_var[:argument][:name]}' to '#{args["#{wf_var[:argument][:name]}_machine_name".to_sym]}'"
|
241
|
+
|
242
|
+
# Catch a very specific case where the weather file has to be changed
|
243
|
+
if wf[:name] == 'location'
|
244
|
+
@logger.warn "VERY SPECIFIC case to change the location to #{@datapoint_json[:data_point][:set_variable_values][variable_uuid]}"
|
245
|
+
@weather_filename = @datapoint_json[:data_point][:set_variable_values][variable_uuid]
|
246
|
+
end
|
247
|
+
else
|
248
|
+
@logger.info "Value for variable '#{variable_name}:#{variable_uuid}' not set in datapoint object"
|
249
|
+
fail "Value for variable '#{variable_name}:#{variable_uuid}' not set in datapoint object" if CRASH_ON_NO_WORKFLOW_VARIABLE
|
250
|
+
break
|
251
|
+
end
|
252
|
+
else
|
253
|
+
fail 'No block for set_variable_values in data point record'
|
254
|
+
end
|
255
|
+
else
|
256
|
+
fail 'No block for data_point in data_point record'
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
# Run the XML Measure
|
263
|
+
xml_changed = measure.run(@model_xml, nil, args)
|
264
|
+
|
265
|
+
# save the JSON with the changed values
|
266
|
+
# the measure has to implement the "results_to_json" method
|
267
|
+
@output_attributes[wf[:name].to_sym] = measure.variable_values
|
268
|
+
measure.results_to_json("#{@run_directory}/#{wf[:name]}_results.json")
|
269
|
+
|
270
|
+
# TODO: do we want to do this?
|
271
|
+
#ros.communicate_intermediate_result(measure.variable_values)
|
272
|
+
|
273
|
+
@logger.info "Finished applying measure workflow #{wf[:name]} with change flag set to '#{xml_changed}'"
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
end
|
279
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
######################################################################
|
2
|
+
# Copyright (c) 2008-2014, Alliance for Sustainable Energy.
|
3
|
+
# All rights reserved.
|
4
|
+
#
|
5
|
+
# This library is free software; you can redistribute it and/or
|
6
|
+
# modify it under the terms of the GNU Lesser General Public
|
7
|
+
# License as published by the Free Software Foundation; either
|
8
|
+
# version 2.1 of the License, or (at your option) any later version.
|
9
|
+
#
|
10
|
+
# This library is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
13
|
+
# Lesser General Public License for more details.
|
14
|
+
#
|
15
|
+
# You should have received a copy of the GNU Lesser General Public
|
16
|
+
# License along with this library; if not, write to the Free Software
|
17
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
18
|
+
######################################################################
|
19
|
+
|
20
|
+
require 'logger'
|
21
|
+
|
22
|
+
class Logger
|
23
|
+
def format_message(severity, datetime, progname, msg)
|
24
|
+
#"#{datetime} (#{$$}) #{msg}\n"
|
25
|
+
#"#{datetime}: #{msg}\n"
|
26
|
+
"[%s %s] %s\n" % [ datetime.strftime("%H:%M:%S.%6N"), severity, msg ]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Class to allow multiple logging paths
|
31
|
+
class MultiDelegator
|
32
|
+
def initialize(*targets)
|
33
|
+
@targets = targets
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.delegate(*methods)
|
37
|
+
methods.each do |m|
|
38
|
+
define_method(m) do |*args|
|
39
|
+
@targets.map { |t| t.send(m, *args) }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
class <<self
|
46
|
+
alias to new
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,258 @@
|
|
1
|
+
######################################################################
|
2
|
+
# Copyright (c) 2008-2014, Alliance for Sustainable Energy.
|
3
|
+
# All rights reserved.
|
4
|
+
#
|
5
|
+
# This library is free software; you can redistribute it and/or
|
6
|
+
# modify it under the terms of the GNU Lesser General Public
|
7
|
+
# License as published by the Free Software Foundation; either
|
8
|
+
# version 2.1 of the License, or (at your option) any later version.
|
9
|
+
#
|
10
|
+
# This library is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
13
|
+
# Lesser General Public License for more details.
|
14
|
+
#
|
15
|
+
# You should have received a copy of the GNU Lesser General Public
|
16
|
+
# License along with this library; if not, write to the Free Software
|
17
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
18
|
+
######################################################################
|
19
|
+
|
20
|
+
# Run Class for OpenStudio workflow. The data are passed in via the adapter
|
21
|
+
module OpenStudio
|
22
|
+
module Workflow
|
23
|
+
class Run
|
24
|
+
include AASM
|
25
|
+
|
26
|
+
attr_accessor :logger
|
27
|
+
|
28
|
+
attr_reader :options
|
29
|
+
attr_reader :adapter
|
30
|
+
attr_reader :directory
|
31
|
+
attr_reader :run_directory
|
32
|
+
attr_reader :final_state
|
33
|
+
attr_reader :job_results
|
34
|
+
|
35
|
+
|
36
|
+
# Create a nice name for the state object instead of aasm
|
37
|
+
alias state aasm
|
38
|
+
|
39
|
+
# load the transitions
|
40
|
+
def self.default_transition
|
41
|
+
# TODO: replace these with dynamic states from a config file of some sort
|
42
|
+
[
|
43
|
+
{from: :queued, to: :preflight},
|
44
|
+
{from: :preflight, to: :openstudio},
|
45
|
+
{from: :openstudio, to: :energyplus},
|
46
|
+
{from: :energyplus, to: :postprocess},
|
47
|
+
{from: :postprocess, to: :finished},
|
48
|
+
]
|
49
|
+
end
|
50
|
+
|
51
|
+
# The default states for the workflow. Note that the states of :queued of :finished need
|
52
|
+
# to exist for all cases.
|
53
|
+
def self.default_states
|
54
|
+
# TODO: replace this with some sort of dynamic store
|
55
|
+
[
|
56
|
+
{state: :queued, :options => {initial: true}},
|
57
|
+
{state: :preflight, :options => {after_enter: :run_preflight}},
|
58
|
+
{state: :openstudio, :options => {after_enter: :run_openstudio}},
|
59
|
+
{state: :energyplus, :options => {after_enter: :run_energyplus}},
|
60
|
+
{state: :postprocess, :options => {after_enter: :run_postprocess}},
|
61
|
+
{state: :finished},
|
62
|
+
{state: :errored}
|
63
|
+
]
|
64
|
+
end
|
65
|
+
|
66
|
+
# initialize a new run class
|
67
|
+
#
|
68
|
+
# @param adapter an instance of the adapter class
|
69
|
+
# @param directory location of the datapoint directory to run. This is needed
|
70
|
+
# independent of the adapter that is being used. Note that the simulation will actually run in 'run'
|
71
|
+
def initialize(adapter, directory, options = {})
|
72
|
+
@adapter = adapter
|
73
|
+
@directory = directory
|
74
|
+
# TODO: run directory is a convention right now. Move to a configuration item
|
75
|
+
@run_directory = "#{@directory}/run"
|
76
|
+
|
77
|
+
defaults = {
|
78
|
+
transitions: OpenStudio::Workflow::Run.default_transition,
|
79
|
+
states: OpenStudio::Workflow::Run.default_states,
|
80
|
+
jobs: {}
|
81
|
+
}
|
82
|
+
@options = defaults.merge(options)
|
83
|
+
|
84
|
+
@error = false
|
85
|
+
|
86
|
+
@job_results = {}
|
87
|
+
|
88
|
+
# By default blow away the entire run directory every time and recreate it
|
89
|
+
FileUtils.rm_rf(@run_directory) if File.exist?(@run_directory)
|
90
|
+
FileUtils.mkdir_p(@run_directory)
|
91
|
+
|
92
|
+
# There is a namespace conflict when OpenStudio is loaded: be careful!
|
93
|
+
log_file = File.open("#{@run_directory}/run.log", "a")
|
94
|
+
|
95
|
+
l = @adapter.get_logger @directory, @options
|
96
|
+
if l
|
97
|
+
@logger = ::Logger.new MultiDelegator.delegate(:write, :close).to(STDOUT, log_file, l)
|
98
|
+
else
|
99
|
+
@logger = ::Logger.new MultiDelegator.delegate(:write, :close).to(STDOUT, log_file)
|
100
|
+
end
|
101
|
+
|
102
|
+
@logger.info "Initializing directory #{@directory} for simulation with options #{@options}"
|
103
|
+
|
104
|
+
super()
|
105
|
+
|
106
|
+
# load the state machine
|
107
|
+
machine
|
108
|
+
end
|
109
|
+
|
110
|
+
# run the simulations.
|
111
|
+
# TODO: add a catch if any job fails; TODO: make a block method to provide feedback
|
112
|
+
def run
|
113
|
+
@logger.info "Starting workflow"
|
114
|
+
begin
|
115
|
+
while self.state.current_state != :finished && !@error
|
116
|
+
self.step
|
117
|
+
end
|
118
|
+
|
119
|
+
@logger.info "Finished workflow"
|
120
|
+
|
121
|
+
if @error
|
122
|
+
# need to tell the system that this failed
|
123
|
+
@adapter.communicate_failure @directory
|
124
|
+
end
|
125
|
+
|
126
|
+
# TODO: this should be a job that handles the use case with a :guard on if @job_results[:run_postprocess]
|
127
|
+
if @job_results[:run_postprocess]
|
128
|
+
# these are the results that need to be sent back to adapter
|
129
|
+
@logger.info "Sending the results back to the adapter"
|
130
|
+
#@logger.info "Sending communicate_results the following options #{@job_results}"
|
131
|
+
@adapter.communicate_results @directory, @job_results[:run_postprocess]
|
132
|
+
end
|
133
|
+
ensure
|
134
|
+
@adapter.communicate_complete @directory
|
135
|
+
@logger.info "Running workflow from #{__FILE__} complete"
|
136
|
+
|
137
|
+
# TODO: define the outputs and figure out how to show it correctory
|
138
|
+
obj_function_array ||= ['NA']
|
139
|
+
|
140
|
+
# Print the objective functions to the screen even though the file is being used right now
|
141
|
+
# Note as well that we can't guarantee that the csv format will be in the right order
|
142
|
+
puts obj_function_array.join(',')
|
143
|
+
end
|
144
|
+
|
145
|
+
state.current_state
|
146
|
+
end
|
147
|
+
|
148
|
+
# call back for when there is an exception running any of the state transitions
|
149
|
+
def step_error(*args)
|
150
|
+
# Make sure to set the instance variable @error to true in order to stop the :step
|
151
|
+
# event from being fired.
|
152
|
+
@error = true
|
153
|
+
@logger.error "Found error in state '#{aasm.current_state}' with message #{args}}"
|
154
|
+
|
155
|
+
# Call the error_out event to transition to the :errored state
|
156
|
+
error_out
|
157
|
+
end
|
158
|
+
|
159
|
+
# TODO: these methods needs to be dynamic or inherited
|
160
|
+
# run energplus
|
161
|
+
def run_energyplus
|
162
|
+
@logger.info "Running #{__method__}"
|
163
|
+
klass = get_run_class(__method__)
|
164
|
+
|
165
|
+
@job_results[__method__.to_sym] = klass.perform
|
166
|
+
end
|
167
|
+
|
168
|
+
# run openstudio to create the model and apply the measures
|
169
|
+
def run_openstudio
|
170
|
+
@logger.info "Running #{__method__}"
|
171
|
+
klass = get_run_class(__method__)
|
172
|
+
|
173
|
+
# TODO: save the resulting filenames to an array
|
174
|
+
@job_results[__method__.to_sym] = klass.perform
|
175
|
+
end
|
176
|
+
|
177
|
+
def run_postprocess
|
178
|
+
@logger.info "Running #{__method__}"
|
179
|
+
klass = get_run_class(__method__)
|
180
|
+
|
181
|
+
@job_results[__method__.to_sym] = klass.perform
|
182
|
+
end
|
183
|
+
|
184
|
+
# preconfigured run method for preflight. This configures the input directories and sets everything
|
185
|
+
# up for running the simulations.
|
186
|
+
def run_preflight
|
187
|
+
@logger.info "Running #{__method__}"
|
188
|
+
klass = get_run_class(__method__)
|
189
|
+
|
190
|
+
@job_results[__method__.to_sym] = klass.perform
|
191
|
+
end
|
192
|
+
|
193
|
+
def run_xml
|
194
|
+
@logger.info "Running #{__method__}"
|
195
|
+
klass = get_run_class(__method__)
|
196
|
+
|
197
|
+
@job_results[__method__.to_sym] = klass.perform
|
198
|
+
@logger.info @job_results
|
199
|
+
end
|
200
|
+
|
201
|
+
def final_state
|
202
|
+
state.current_state
|
203
|
+
end
|
204
|
+
|
205
|
+
private
|
206
|
+
|
207
|
+
# Create a state machine from the predefined transitions methods. This loads in
|
208
|
+
# a single event of :step which steps through the transitions defined in the Hash in default_transitions
|
209
|
+
# and calls the actions defined in the states in the Hash of default_states
|
210
|
+
def machine
|
211
|
+
@logger.info "Initializing state machine"
|
212
|
+
@options[:states].each do |s|
|
213
|
+
s[:options] ? o = s[:options] : o = {}
|
214
|
+
OpenStudio::Workflow::Run.aasm.states << AASM::State.new(s[:state], self.class, o)
|
215
|
+
end
|
216
|
+
OpenStudio::Workflow::Run.aasm.initial_state(:queued)
|
217
|
+
|
218
|
+
# Create a new event and add in the transitions
|
219
|
+
new_event = OpenStudio::Workflow::Run.aasm.event(:step)
|
220
|
+
event = OpenStudio::Workflow::Run.aasm.events[:step]
|
221
|
+
|
222
|
+
# TODO: make a config option to not go to the error state. Useful to not error-state when testing
|
223
|
+
event.options[:error] = 'step_error'
|
224
|
+
@options[:transitions].each do |t|
|
225
|
+
event.transitions(t)
|
226
|
+
end
|
227
|
+
|
228
|
+
# Add in special event to error_out the state machine
|
229
|
+
new_event = OpenStudio::Workflow::Run.aasm.event(:error_out)
|
230
|
+
event = OpenStudio::Workflow::Run.aasm.events[:error_out]
|
231
|
+
event.transitions(:to => :errored)
|
232
|
+
end
|
233
|
+
|
234
|
+
# Get any options that may have been sent into the class defining the workflow step
|
235
|
+
def get_job_options
|
236
|
+
result = {}
|
237
|
+
#if @options[:jobs].has_key?(state.current_state)
|
238
|
+
#logger.info "Retrieving job options from the @options array for #{state.current_state}"
|
239
|
+
# result = @options[:jobs][state.current_state]
|
240
|
+
#end
|
241
|
+
|
242
|
+
#result
|
243
|
+
|
244
|
+
# TODO fix this so that it gets the base config options plus its job options. Need to
|
245
|
+
# also merge in all the former job results.
|
246
|
+
@options.merge(@job_results)
|
247
|
+
end
|
248
|
+
|
249
|
+
def get_run_class(from_method)
|
250
|
+
require_relative "jobs/#{from_method}/#{from_method}"
|
251
|
+
klass_name = from_method.to_s.split('_').map(&:capitalize) * ''
|
252
|
+
@logger.info "Getting method for state transition '#{from_method}'"
|
253
|
+
klass = Object.const_get(klass_name).new(@directory, @logger, @adapter, get_job_options)
|
254
|
+
klass
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|