openstudio-workflow 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|