openstudio-workflow 2.2.0 → 2.2.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 +4 -4
- data/CHANGELOG.md +7 -0
- data/lib/openstudio/workflow/adapters/input/local.rb +18 -0
- data/lib/openstudio/workflow/adapters/output/local.rb +4 -2
- data/lib/openstudio/workflow/adapters/output/socket.rb +1 -5
- data/lib/openstudio/workflow/adapters/output/web.rb +0 -16
- data/lib/openstudio/workflow/adapters/output_adapter.rb +5 -6
- data/lib/openstudio/workflow/jobs/run_initialization.rb +1 -1
- data/lib/openstudio/workflow/jobs/run_reporting_measures.rb +2 -4
- data/lib/openstudio/workflow/run.rb +12 -17
- data/lib/openstudio/workflow/util/energyplus.rb +12 -16
- data/lib/openstudio/workflow/util/measure.rb +2 -0
- data/lib/openstudio/workflow/util/model.rb +1 -1
- data/lib/openstudio/workflow/util/post_process.rb +2 -2
- data/lib/openstudio/workflow/util/weather_file.rb +1 -1
- data/lib/openstudio/workflow/version.rb +1 -1
- data/lib/openstudio/workflow_json.rb +2 -4
- data/lib/openstudio/workflow_runner.rb +1 -3
- metadata +20 -34
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 03716fad02e514aa60fbb2e3a14dd2c50286fe2621219f63137f4e1bb77afcb9
|
4
|
+
data.tar.gz: 525a28b94195445ccf890f784898f9868226aea11d0137d112223c2ea0abd099
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bc22ddb2820ea5155ce5308a49cfc405f5bdfa43a6b54fbac46ad8486deadb0f8bd866305566dc6fc70d65dbe71feb2c249fd6c0fa94623427f27b630b4c6fc7
|
7
|
+
data.tar.gz: de613152849f97dd568c2f91a3411cb0c3a0f37d48f3e86a3fd37e7922d074e058eab6515e82f4359edb3e175baa718a91c8cbf6cbe1cc414afacee0d9c3253a
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,13 @@
|
|
1
1
|
OpenStudio::Workflow Change Log
|
2
2
|
==================================
|
3
3
|
|
4
|
+
Version 2.2.1
|
5
|
+
-------------
|
6
|
+
* Fixes [#4150](https://github.com/NREL/OpenStudio/issues/4150) LoadError changes current working directory in CLI
|
7
|
+
* Add skip option to not zip up datapoint results
|
8
|
+
* Update measure tester gem which upgrades Rubocop to 1.15
|
9
|
+
* Update styles to v4 based on new version of Rubocop
|
10
|
+
|
4
11
|
Version 2.2.0
|
5
12
|
-------------
|
6
13
|
* Minimum Ruby version upgraded to 2.7.0
|
@@ -192,6 +192,24 @@ module OpenStudio
|
|
192
192
|
return default
|
193
193
|
end
|
194
194
|
|
195
|
+
def skip_zip_results(user_options, default)
|
196
|
+
# user option trumps all others
|
197
|
+
return user_options[:skip_zip_results] if user_options[:skip_zip_results]
|
198
|
+
|
199
|
+
# try to read from OSW
|
200
|
+
if @run_options && !@run_options.empty?
|
201
|
+
if @run_options.get.respond_to?(:skipZipResults)
|
202
|
+
return @run_options.get.skipZipResults
|
203
|
+
else
|
204
|
+
if @workflow[:run_options]
|
205
|
+
return @workflow[:run_options][:skip_zip_results]
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
return default
|
211
|
+
end
|
212
|
+
|
195
213
|
def preserve_run_dir(user_options, default)
|
196
214
|
# user option trumps all others
|
197
215
|
return user_options[:preserve_run_dir] if user_options[:preserve_run_dir]
|
@@ -136,8 +136,10 @@ module OpenStudio
|
|
136
136
|
|
137
137
|
# Write the results of the workflow to the filesystem
|
138
138
|
#
|
139
|
-
def communicate_results(directory, results)
|
140
|
-
|
139
|
+
def communicate_results(directory, results, skip_zip_results)
|
140
|
+
if !skip_zip_results
|
141
|
+
zip_results(directory)
|
142
|
+
end
|
141
143
|
|
142
144
|
if results.is_a? Hash
|
143
145
|
# DLM: don't we want this in the results zip?
|
@@ -55,10 +55,6 @@ module OpenStudio
|
|
55
55
|
@socket.write("Started\n")
|
56
56
|
end
|
57
57
|
|
58
|
-
def communicate_results(directory, results)
|
59
|
-
super
|
60
|
-
end
|
61
|
-
|
62
58
|
def communicate_complete
|
63
59
|
super
|
64
60
|
@socket.write("Complete\n")
|
@@ -75,7 +71,7 @@ module OpenStudio
|
|
75
71
|
|
76
72
|
def communicate_transition(message, type, options = {})
|
77
73
|
super
|
78
|
-
@socket.write(message
|
74
|
+
@socket.write("#{message}\n")
|
79
75
|
end
|
80
76
|
|
81
77
|
def communicate_energyplus_stdout(line, options = {})
|
@@ -47,22 +47,6 @@ module OpenStudio
|
|
47
47
|
raise 'The required :url option was not passed to the web output adapter' unless options[:url]
|
48
48
|
end
|
49
49
|
|
50
|
-
def communicate_started
|
51
|
-
super
|
52
|
-
end
|
53
|
-
|
54
|
-
def communicate_results(directory, results)
|
55
|
-
super
|
56
|
-
end
|
57
|
-
|
58
|
-
def communicate_complete
|
59
|
-
super
|
60
|
-
end
|
61
|
-
|
62
|
-
def communicate_failure
|
63
|
-
super
|
64
|
-
end
|
65
|
-
|
66
50
|
def communicate_objective_function(objectives, options = {})
|
67
51
|
super
|
68
52
|
end
|
@@ -113,19 +113,18 @@ module OpenStudio
|
|
113
113
|
end
|
114
114
|
|
115
115
|
# skip x-large directory
|
116
|
-
if File.size?(file)
|
117
|
-
next
|
116
|
+
if File.size?(file) && (File.size?(file) >= 15000000)
|
117
|
+
next
|
118
118
|
end
|
119
|
+
|
119
120
|
add_directory_to_zip(zf, file, directory)
|
120
121
|
else
|
121
122
|
next if File.extname(file) =~ /\.rb.*/
|
122
123
|
next if File.extname(file) =~ /\.zip.*/
|
123
124
|
|
124
125
|
# skip large non-osm/idf files
|
125
|
-
if File.size(file)
|
126
|
-
|
127
|
-
next unless File.extname(file) == '.osm' || File.extname(file) == '.idf'
|
128
|
-
end
|
126
|
+
if File.size(file) && (File.size(file) >= 100000000) && !(File.extname(file) == '.osm' || File.extname(file) == '.idf')
|
127
|
+
next
|
129
128
|
end
|
130
129
|
|
131
130
|
zip_file_to_add = file.gsub("#{directory}/", '')
|
@@ -181,7 +181,7 @@ class RunInitialization < OpenStudio::Workflow::Job
|
|
181
181
|
|
182
182
|
unless weather_path.empty?
|
183
183
|
weather_path = weather_path.get
|
184
|
-
@logger.debug
|
184
|
+
@logger.debug "Searching for weather file #{weather_path}"
|
185
185
|
|
186
186
|
weather_full_path = workflow_json.findFile(weather_path)
|
187
187
|
if weather_full_path.empty?
|
@@ -69,10 +69,8 @@ class RunReportingMeasures < OpenStudio::Workflow::Job
|
|
69
69
|
workflow = nil
|
70
70
|
if File.exist? @registry[:osw_path]
|
71
71
|
workflow = ::JSON.parse(File.read(@registry[:osw_path]), symbolize_names: true)
|
72
|
-
if !workflow.nil?
|
73
|
-
|
74
|
-
@registry.register(:urbanopt) { workflow[:urbanopt] }
|
75
|
-
end
|
72
|
+
if !workflow.nil? && !workflow[:urbanopt].nil?
|
73
|
+
@registry.register(:urbanopt) { workflow[:urbanopt] }
|
76
74
|
end
|
77
75
|
end
|
78
76
|
end
|
@@ -48,12 +48,7 @@ module OpenStudio
|
|
48
48
|
class Run
|
49
49
|
attr_accessor :registry
|
50
50
|
|
51
|
-
attr_reader :options
|
52
|
-
attr_reader :input_adapter
|
53
|
-
attr_reader :output_adapter
|
54
|
-
attr_reader :final_message
|
55
|
-
attr_reader :job_results
|
56
|
-
attr_reader :current_state
|
51
|
+
attr_reader :options, :input_adapter, :output_adapter, :final_message, :job_results, :current_state
|
57
52
|
|
58
53
|
# Define the default set of jobs. Note that the states of :queued of :finished need to exist for all job arrays.
|
59
54
|
#
|
@@ -90,6 +85,7 @@ module OpenStudio
|
|
90
85
|
# @option user_options [Hash] :debug Print debugging messages, overrides OSW option if set, defaults to false
|
91
86
|
# @option user_options [Hash] :energyplus_path Specifies path to energyplus executable, defaults to empty
|
92
87
|
# @option user_options [Hash] :fast Speeds up workflow by skipping steps not needed for running simulations, defaults to false
|
88
|
+
# @option user_options [Hash] :skip_zip_results Skips creating the data_point.zip file. Setting to `true` can cause issues with workflows expecting .zip files to signal completion (e.g., OpenStudio Analysis Framework), defaults to false
|
93
89
|
# @option user_options [Hash] :jobs Simulation workflow, overrides jobs in OSW if set, defaults to default_jobs
|
94
90
|
# @option user_options [Hash] :output_adapter Output adapter to use, overrides output adapter in OSW if set, defaults to local adapter
|
95
91
|
# @option user_options [Hash] :preserve_run_dir Prevents run directory from being cleaned prior to run, overrides OSW option if set, defaults to false - DLM, Deprecate
|
@@ -158,15 +154,13 @@ module OpenStudio
|
|
158
154
|
end
|
159
155
|
|
160
156
|
# By default blow away the entire run directory every time and recreate it
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
FileUtils.rm_rf(@registry[:run_dir])
|
169
|
-
end
|
157
|
+
if !@options[:preserve_run_dir] && File.exist?(@registry[:run_dir])
|
158
|
+
# logger is not initialized yet (it needs run dir to exist for log)
|
159
|
+
puts "Removing existing run directory #{@registry[:run_dir]}" if @options[:debug]
|
160
|
+
|
161
|
+
# DLM: this is dangerous, we are calling rm_rf on a user entered directory, need to check this first
|
162
|
+
# TODO: Echoing Dan's comment
|
163
|
+
FileUtils.rm_rf(@registry[:run_dir])
|
170
164
|
end
|
171
165
|
FileUtils.mkdir_p(@registry[:run_dir])
|
172
166
|
|
@@ -176,7 +170,7 @@ module OpenStudio
|
|
176
170
|
else
|
177
171
|
# don't create these files unless we want to use them
|
178
172
|
# DLM: TODO, make sure that run.log will be closed later
|
179
|
-
@options[:targets] = [
|
173
|
+
@options[:targets] = [$stdout, File.open(File.join(@registry[:run_dir], 'run.log'), 'a')]
|
180
174
|
end
|
181
175
|
|
182
176
|
@registry.register(:log_targets) { @options[:targets] }
|
@@ -204,6 +198,7 @@ module OpenStudio
|
|
204
198
|
@options[:verify_osw] = @input_adapter.verify_osw(user_options, true)
|
205
199
|
@options[:weather_file] = @input_adapter.weather_file(user_options, nil)
|
206
200
|
@options[:fast] = @input_adapter.fast(user_options, false)
|
201
|
+
@options[:skip_zip_results] = @input_adapter.skip_zip_results(user_options, false)
|
207
202
|
|
208
203
|
openstudio_dir = 'unknown'
|
209
204
|
begin
|
@@ -236,7 +231,7 @@ module OpenStudio
|
|
236
231
|
|
237
232
|
if !@options[:fast]
|
238
233
|
@logger.info 'Finished workflow - communicating results and zipping files'
|
239
|
-
@output_adapter.communicate_results(@registry[:run_dir], @registry[:results])
|
234
|
+
@output_adapter.communicate_results(@registry[:run_dir], @registry[:results], @options[:skip_zip_results])
|
240
235
|
end
|
241
236
|
rescue StandardError => e
|
242
237
|
@logger.info "Error occurred during running with #{e.message}"
|
@@ -125,7 +125,7 @@ module OpenStudio
|
|
125
125
|
# @return [Void]
|
126
126
|
#
|
127
127
|
def call_energyplus(run_directory, energyplus_path = nil, output_adapter = nil, logger = nil, workflow_json = nil)
|
128
|
-
logger ||= ::Logger.new(
|
128
|
+
logger ||= ::Logger.new($stdout) unless logger
|
129
129
|
|
130
130
|
current_dir = Dir.pwd
|
131
131
|
energyplus_path ||= find_energyplus
|
@@ -231,7 +231,7 @@ module OpenStudio
|
|
231
231
|
end
|
232
232
|
|
233
233
|
# merge in monthly reports
|
234
|
-
EnergyPlus.monthly_report_idf_text.split(
|
234
|
+
EnergyPlus.monthly_report_idf_text.split(/^\s*$/).each do |object|
|
235
235
|
object = object.strip
|
236
236
|
next if object.empty?
|
237
237
|
|
@@ -278,11 +278,9 @@ module OpenStudio
|
|
278
278
|
allowed_objects << 'Meter:CustomDecrement'
|
279
279
|
allowed_objects << 'EnergyManagementSystem:OutputVariable'
|
280
280
|
|
281
|
-
if allowed_objects.include?(idd_object.name)
|
282
|
-
|
283
|
-
|
284
|
-
num_added += 1
|
285
|
-
end
|
281
|
+
if allowed_objects.include?(idd_object.name) && !check_for_object(workspace, idf_object, idd_object.type)
|
282
|
+
workspace.addObject(idf_object)
|
283
|
+
num_added += 1
|
286
284
|
end
|
287
285
|
|
288
286
|
allowed_unique_objects = []
|
@@ -293,15 +291,13 @@ module OpenStudio
|
|
293
291
|
# OutputControl:ReportingTolerances # not allowed
|
294
292
|
# Output:SQLite # not allowed
|
295
293
|
|
296
|
-
if allowed_unique_objects.include?(idf_object.iddObject.name)
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
merge_output_table_summary_reports(summary_reports[0], idf_object)
|
304
|
-
end
|
294
|
+
if allowed_unique_objects.include?(idf_object.iddObject.name) && (idf_object.iddObject.name == 'Output:Table:SummaryReports')
|
295
|
+
summary_reports = workspace.getObjectsByType(idf_object.iddObject.type)
|
296
|
+
if summary_reports.empty?
|
297
|
+
workspace.addObject(idf_object)
|
298
|
+
num_added += 1
|
299
|
+
else
|
300
|
+
merge_output_table_summary_reports(summary_reports[0], idf_object)
|
305
301
|
end
|
306
302
|
end
|
307
303
|
|
@@ -342,6 +342,8 @@ module OpenStudio
|
|
342
342
|
result = nil
|
343
343
|
begin
|
344
344
|
load measure_path.to_s
|
345
|
+
# load.c in ruby can result in changing dir to root / so preserve cwd here. happens in openstudio cli
|
346
|
+
Dir.chdir measure_run_dir
|
345
347
|
measure_object = Object.const_get(class_name).new
|
346
348
|
rescue => e
|
347
349
|
|
@@ -95,7 +95,7 @@ module OpenStudio
|
|
95
95
|
# @todo (rhorsey) rescue errors here
|
96
96
|
#
|
97
97
|
def translate_to_energyplus(model, logger = nil)
|
98
|
-
logger ||= ::Logger.new(
|
98
|
+
logger ||= ::Logger.new($stdout)
|
99
99
|
logger.info 'Translate object to EnergyPlus IDF in preparation for EnergyPlus'
|
100
100
|
a = ::Time.now
|
101
101
|
# ensure objects exist for reporting purposes
|
@@ -138,10 +138,10 @@ module OpenStudio
|
|
138
138
|
#
|
139
139
|
def rename_hash_keys(hash, logger)
|
140
140
|
# @todo should we log the name changes?
|
141
|
-
regex = %r{[
|
141
|
+
regex = %r{[|!@#$%^&*()\{\}\\\[\];:'",<.>/?+=]+}
|
142
142
|
|
143
143
|
rename_keys = lambda do |h|
|
144
|
-
if Hash
|
144
|
+
if h.is_a?(Hash)
|
145
145
|
h.each_key do |key|
|
146
146
|
if key.to_s =~ regex
|
147
147
|
logger.warn "Renaming result key '#{key}' to remove invalid characters"
|
@@ -55,7 +55,7 @@ module OpenStudio
|
|
55
55
|
#
|
56
56
|
def get_weather_file(directory, wf, wf_search_array, model, logger = nil)
|
57
57
|
# TODO: this logic needs some updating, weather file should come from current model, found using search paths
|
58
|
-
logger ||= ::Logger.new(
|
58
|
+
logger ||= ::Logger.new($stdout) unless logger
|
59
59
|
if wf
|
60
60
|
weather_file = get_weather_file_from_fs(directory, wf, wf_search_array, logger)
|
61
61
|
raise 'Could not locate the weather file in the filesystem. Please see the log' if weather_file == false
|
@@ -89,9 +89,7 @@ class WorkflowStepResultValue_Shim
|
|
89
89
|
@type = type
|
90
90
|
end
|
91
91
|
|
92
|
-
attr_reader :name
|
93
|
-
|
94
|
-
attr_reader :value
|
92
|
+
attr_reader :name, :value
|
95
93
|
|
96
94
|
def variantType
|
97
95
|
@type
|
@@ -162,7 +160,7 @@ class WorkflowStepResult_Shim
|
|
162
160
|
|
163
161
|
def setStepResult(step_result)
|
164
162
|
@result[:step_result] = step_result
|
165
|
-
|
163
|
+
end
|
166
164
|
end
|
167
165
|
|
168
166
|
# WorkflowStep_Shim provides a shim interface to the WorkflowStep class in OpenStudio 2.X when running in OpenStudio 1.X
|
@@ -65,14 +65,12 @@ class WorkflowRunner < OpenStudio::Ruleset::OSRunner
|
|
65
65
|
::Time.now.utc.strftime('%Y%m%dT%H%M%SZ')
|
66
66
|
end
|
67
67
|
|
68
|
-
attr_reader :datapoint
|
68
|
+
attr_reader :datapoint, :analysis
|
69
69
|
|
70
70
|
def setDatapoint(datapoint)
|
71
71
|
@datapoint = datapoint
|
72
72
|
end
|
73
73
|
|
74
|
-
attr_reader :analysis
|
75
|
-
|
76
74
|
def setAnalysis(analysis)
|
77
75
|
@analysis = analysis
|
78
76
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: openstudio-workflow
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.2.
|
4
|
+
version: 2.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nicholas Long
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2021-02
|
12
|
+
date: 2021-06-02 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: builder
|
@@ -95,20 +95,34 @@ dependencies:
|
|
95
95
|
- - "~>"
|
96
96
|
- !ruby/object:Gem::Version
|
97
97
|
version: 2.8.0
|
98
|
+
- !ruby/object:Gem::Dependency
|
99
|
+
name: openstudio_measure_tester
|
100
|
+
requirement: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - "~>"
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: 0.3.1
|
105
|
+
type: :development
|
106
|
+
prerelease: false
|
107
|
+
version_requirements: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - "~>"
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: 0.3.1
|
98
112
|
- !ruby/object:Gem::Dependency
|
99
113
|
name: openstudio-standards
|
100
114
|
requirement: !ruby/object:Gem::Requirement
|
101
115
|
requirements:
|
102
116
|
- - "~>"
|
103
117
|
- !ruby/object:Gem::Version
|
104
|
-
version: 0.2.
|
118
|
+
version: 0.2.14
|
105
119
|
type: :development
|
106
120
|
prerelease: false
|
107
121
|
version_requirements: !ruby/object:Gem::Requirement
|
108
122
|
requirements:
|
109
123
|
- - "~>"
|
110
124
|
- !ruby/object:Gem::Version
|
111
|
-
version: 0.2.
|
125
|
+
version: 0.2.14
|
112
126
|
- !ruby/object:Gem::Dependency
|
113
127
|
name: parallel
|
114
128
|
requirement: !ruby/object:Gem::Requirement
|
@@ -157,14 +171,14 @@ dependencies:
|
|
157
171
|
requirements:
|
158
172
|
- - "~>"
|
159
173
|
- !ruby/object:Gem::Version
|
160
|
-
version: '
|
174
|
+
version: '13.0'
|
161
175
|
type: :development
|
162
176
|
prerelease: false
|
163
177
|
version_requirements: !ruby/object:Gem::Requirement
|
164
178
|
requirements:
|
165
179
|
- - "~>"
|
166
180
|
- !ruby/object:Gem::Version
|
167
|
-
version: '
|
181
|
+
version: '13.0'
|
168
182
|
- !ruby/object:Gem::Dependency
|
169
183
|
name: rspec
|
170
184
|
requirement: !ruby/object:Gem::Requirement
|
@@ -179,34 +193,6 @@ dependencies:
|
|
179
193
|
- - "~>"
|
180
194
|
- !ruby/object:Gem::Version
|
181
195
|
version: '3.9'
|
182
|
-
- !ruby/object:Gem::Dependency
|
183
|
-
name: rubocop
|
184
|
-
requirement: !ruby/object:Gem::Requirement
|
185
|
-
requirements:
|
186
|
-
- - "~>"
|
187
|
-
- !ruby/object:Gem::Version
|
188
|
-
version: 0.54.0
|
189
|
-
type: :development
|
190
|
-
prerelease: false
|
191
|
-
version_requirements: !ruby/object:Gem::Requirement
|
192
|
-
requirements:
|
193
|
-
- - "~>"
|
194
|
-
- !ruby/object:Gem::Version
|
195
|
-
version: 0.54.0
|
196
|
-
- !ruby/object:Gem::Dependency
|
197
|
-
name: rubocop-checkstyle_formatter
|
198
|
-
requirement: !ruby/object:Gem::Requirement
|
199
|
-
requirements:
|
200
|
-
- - "~>"
|
201
|
-
- !ruby/object:Gem::Version
|
202
|
-
version: 0.4.0
|
203
|
-
type: :development
|
204
|
-
prerelease: false
|
205
|
-
version_requirements: !ruby/object:Gem::Requirement
|
206
|
-
requirements:
|
207
|
-
- - "~>"
|
208
|
-
- !ruby/object:Gem::Version
|
209
|
-
version: 0.4.0
|
210
196
|
description: Run OpenStudio based measures and simulations using EnergyPlus
|
211
197
|
email:
|
212
198
|
- nicholas.long@nrel.gov
|