urbanopt-reopt 1.0.0 → 1.2.0
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/.github/workflows/nightly_ci_build.yml +1 -1
- data/CHANGELOG.md +11 -0
- data/CONTRIBUTING.md +3 -3
- data/Gemfile +24 -19
- data/LICENSE.md +1 -1
- data/RDOC_MAIN.md +2 -2
- data/README.md +2 -2
- data/Rakefile +1 -1
- data/developer_nrel_key.rb +1 -1
- data/doc_templates/LICENSE.md +1 -1
- data/doc_templates/copyright_erb.txt +1 -1
- data/doc_templates/copyright_js.txt +1 -1
- data/doc_templates/copyright_ruby.txt +1 -1
- data/docs/README.md +2 -2
- data/docs/package.json +1 -1
- data/index.md +2 -2
- data/lib/urbanopt/reopt/extension.rb +1 -1
- data/lib/urbanopt/reopt/feature_report_adapter.rb +10 -11
- data/lib/urbanopt/reopt/{reopt_ghp_adapter.rb → reopt_ghp_adapter_ghp.rb} +238 -48
- data/lib/urbanopt/reopt/reopt_ghp_api.rb +3 -3
- data/lib/urbanopt/reopt/reopt_ghp_files/reopt_ghp_assumption.json +3 -3
- data/lib/urbanopt/reopt/reopt_ghp_post_processor.rb +13 -4
- data/lib/urbanopt/reopt/reopt_ghp_result.rb +145 -0
- data/lib/urbanopt/reopt/reopt_lite_api.rb +54 -13
- data/lib/urbanopt/reopt/reopt_logger.rb +1 -1
- data/lib/urbanopt/reopt/reopt_post_processor.rb +54 -18
- data/lib/urbanopt/reopt/reopt_schema/REopt-BAU-input.json +174 -0
- data/lib/urbanopt/reopt/reopt_schema/REopt-GHP-input.json +263 -95
- data/lib/urbanopt/reopt/scenario/reopt_scenario_csv.rb +1 -1
- data/lib/urbanopt/reopt/scenario_report_adapter.rb +8 -9
- data/lib/urbanopt/reopt/utilities.rb +1 -1
- data/lib/urbanopt/reopt/version.rb +2 -2
- data/lib/urbanopt/reopt.rb +3 -2
- data/lib/urbanopt/reopt_scenario.rb +1 -1
- data/lib/urbanopt-reopt.rb +1 -1
- data/urbanopt-reopt.gemspec +6 -3
- metadata +53 -9
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'fileutils'
|
|
3
|
+
|
|
4
|
+
module URBANopt # :nodoc:
|
|
5
|
+
module REopt # :nodoc:
|
|
6
|
+
class REoptGHPResult
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@@logger ||= URBANopt::REopt.reopt_logger
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def result_calculate(reopt_ghp_dir)
|
|
13
|
+
reopt_output = File.join(reopt_ghp_dir, 'reopt_ghp_outputs')
|
|
14
|
+
bau_outputs = []
|
|
15
|
+
ghp_outputs = []
|
|
16
|
+
|
|
17
|
+
# Collect all valid output files with building ID
|
|
18
|
+
Dir.glob(File.join(reopt_output, '*_output.json')) do |file|
|
|
19
|
+
filename = File.basename(file)
|
|
20
|
+
parts = filename.split('_') # ["BAU", "building", "1", "output.json"]
|
|
21
|
+
next unless parts.length >= 4
|
|
22
|
+
|
|
23
|
+
scenario = parts[0] # "BAU", "GHP", etc.
|
|
24
|
+
building_id = parts[1..2].join('_') # "building_1"
|
|
25
|
+
|
|
26
|
+
if scenario == 'BAU'
|
|
27
|
+
bau_outputs << [file, building_id]
|
|
28
|
+
elsif ['GHP', 'GHX'].include?(scenario)
|
|
29
|
+
ghp_outputs << [file, building_id]
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Initialize grouped results
|
|
34
|
+
lcc = {}
|
|
35
|
+
lifecycle_capital_costs = {}
|
|
36
|
+
initial_capital_costs = {}
|
|
37
|
+
initial_capital_costs_after_incentives = {}
|
|
38
|
+
lifecycle_elecbill_after_tax = {}
|
|
39
|
+
npv = {}
|
|
40
|
+
|
|
41
|
+
# Totals
|
|
42
|
+
lcc_total_bau = 0
|
|
43
|
+
lcc_total_ghp = 0
|
|
44
|
+
lifecycle_capital_costs_total_bau = 0
|
|
45
|
+
lifecycle_capital_costs_total_ghp = 0
|
|
46
|
+
initial_capital_costs_total_bau = 0
|
|
47
|
+
initial_capital_costs_total_ghp = 0
|
|
48
|
+
initial_capital_costs_after_incentives_total_bau = 0
|
|
49
|
+
initial_capital_costs_after_incentives_total_ghp = 0
|
|
50
|
+
lifecycle_elecbill_after_tax_total_bau = 0
|
|
51
|
+
lifecycle_elecbill_after_tax_total_ghp = 0
|
|
52
|
+
npv_total_bau = 0
|
|
53
|
+
npv_total_ghp = 0
|
|
54
|
+
|
|
55
|
+
# Process BAU files
|
|
56
|
+
bau_outputs.each do |file, building_id|
|
|
57
|
+
data = JSON.parse(File.read(file), symbolize_names: true)
|
|
58
|
+
financial = data.dig(:outputs, :Financial) || {}
|
|
59
|
+
|
|
60
|
+
lcc["lcc_bau_#{building_id}"] = financial[:lcc] || 0
|
|
61
|
+
lcc_total_bau += lcc["lcc_bau_#{building_id}"]
|
|
62
|
+
|
|
63
|
+
lifecycle_capital_costs["bau_#{building_id}"] = financial[:lifecycle_capital_costs] || 0
|
|
64
|
+
lifecycle_capital_costs_total_bau += lifecycle_capital_costs["bau_#{building_id}"]
|
|
65
|
+
|
|
66
|
+
initial_capital_costs["bau_#{building_id}"] = financial[:initial_capital_costs] || 0
|
|
67
|
+
initial_capital_costs_total_bau += initial_capital_costs["bau_#{building_id}"]
|
|
68
|
+
|
|
69
|
+
initial_capital_costs_after_incentives["bau_#{building_id}"] = financial[:initial_capital_costs_after_incentives] || 0
|
|
70
|
+
initial_capital_costs_after_incentives_total_bau += initial_capital_costs_after_incentives["bau_#{building_id}"]
|
|
71
|
+
|
|
72
|
+
lifecycle_elecbill_after_tax["bau_#{building_id}"] = financial[:lifecycle_elecbill_after_tax_bau] || 0
|
|
73
|
+
lifecycle_elecbill_after_tax_total_bau += lifecycle_elecbill_after_tax["bau_#{building_id}"]
|
|
74
|
+
|
|
75
|
+
npv["bau_#{building_id}"] = financial[:npv] || 0
|
|
76
|
+
npv_total_bau += npv["bau_#{building_id}"]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Process GHP files
|
|
80
|
+
ghp_outputs.each do |file, building_id|
|
|
81
|
+
data = JSON.parse(File.read(file), symbolize_names: true)
|
|
82
|
+
financial = data.dig(:outputs, :Financial) || {}
|
|
83
|
+
|
|
84
|
+
lcc["lcc_ghp_#{building_id}"] = financial[:lcc] || 0
|
|
85
|
+
lcc_total_ghp += lcc["lcc_ghp_#{building_id}"]
|
|
86
|
+
|
|
87
|
+
lifecycle_capital_costs["ghp_#{building_id}"] = financial[:lifecycle_capital_costs] || 0
|
|
88
|
+
lifecycle_capital_costs_total_ghp += lifecycle_capital_costs["ghp_#{building_id}"]
|
|
89
|
+
|
|
90
|
+
initial_capital_costs["ghp_#{building_id}"] = financial[:initial_capital_costs] || 0
|
|
91
|
+
initial_capital_costs_total_ghp += initial_capital_costs["ghp_#{building_id}"]
|
|
92
|
+
|
|
93
|
+
initial_capital_costs_after_incentives["ghp_#{building_id}"] = financial[:initial_capital_costs_after_incentives] || 0
|
|
94
|
+
initial_capital_costs_after_incentives_total_ghp += initial_capital_costs_after_incentives["ghp_#{building_id}"]
|
|
95
|
+
|
|
96
|
+
lifecycle_elecbill_after_tax["ghp_#{building_id}"] = financial[:lifecycle_elecbill_after_tax] || 0
|
|
97
|
+
lifecycle_elecbill_after_tax_total_ghp += lifecycle_elecbill_after_tax["ghp_#{building_id}"]
|
|
98
|
+
|
|
99
|
+
npv["ghp_#{building_id}"] = financial[:npv] || 0
|
|
100
|
+
npv_total_ghp += npv["ghp_#{building_id}"]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Add totals and net values
|
|
104
|
+
lcc["lcc_bau_total"] = lcc_total_bau
|
|
105
|
+
lcc["lcc_ghp_total"] = lcc_total_ghp
|
|
106
|
+
lcc["lcc_net"] = lcc_total_ghp - lcc_total_bau
|
|
107
|
+
|
|
108
|
+
lifecycle_capital_costs["bau_total"] = lifecycle_capital_costs_total_bau
|
|
109
|
+
lifecycle_capital_costs["ghp_total"] = lifecycle_capital_costs_total_ghp
|
|
110
|
+
lifecycle_capital_costs["net"] = lifecycle_capital_costs_total_ghp - lifecycle_capital_costs_total_bau
|
|
111
|
+
|
|
112
|
+
initial_capital_costs["bau_total"] = initial_capital_costs_total_bau
|
|
113
|
+
initial_capital_costs["ghp_total"] = initial_capital_costs_total_ghp
|
|
114
|
+
initial_capital_costs["net"] = initial_capital_costs_total_ghp - initial_capital_costs_total_bau
|
|
115
|
+
|
|
116
|
+
initial_capital_costs_after_incentives["bau_total"] = initial_capital_costs_after_incentives_total_bau
|
|
117
|
+
initial_capital_costs_after_incentives["ghp_total"] = initial_capital_costs_after_incentives_total_ghp
|
|
118
|
+
initial_capital_costs_after_incentives["net"] = initial_capital_costs_after_incentives_total_ghp - initial_capital_costs_after_incentives_total_bau
|
|
119
|
+
|
|
120
|
+
lifecycle_elecbill_after_tax["bau_total"] = lifecycle_elecbill_after_tax_total_bau
|
|
121
|
+
lifecycle_elecbill_after_tax["ghp_total"] = lifecycle_elecbill_after_tax_total_ghp
|
|
122
|
+
lifecycle_elecbill_after_tax["net"] = lifecycle_elecbill_after_tax_total_ghp - lifecycle_elecbill_after_tax_total_bau
|
|
123
|
+
|
|
124
|
+
npv["bau_total"] = npv_total_bau
|
|
125
|
+
npv["ghp_total"] = npv_total_ghp
|
|
126
|
+
npv["net"] = npv_total_ghp - npv_total_bau
|
|
127
|
+
|
|
128
|
+
# Final result structure
|
|
129
|
+
result_data = {
|
|
130
|
+
lcc: lcc,
|
|
131
|
+
lifecycle_capital_cost: lifecycle_capital_costs,
|
|
132
|
+
initial_capital_costs: initial_capital_costs,
|
|
133
|
+
initial_capital_costs_after_incentives: initial_capital_costs_after_incentives,
|
|
134
|
+
lifecycle_elecbill_after_tax: lifecycle_elecbill_after_tax,
|
|
135
|
+
npv: npv
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
output_path = File.join(reopt_ghp_dir, "reopt_ghp_result_summary.json")
|
|
139
|
+
File.open(output_path, "w") { |file| file.write(JSON.pretty_generate(result_data)) }
|
|
140
|
+
puts "Wrote summary to #{output_path}"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# *********************************************************************************
|
|
2
|
-
# URBANopt (tm), Copyright (c) Alliance for
|
|
2
|
+
# URBANopt (tm), Copyright (c) Alliance for Energy Innovation, LLC.
|
|
3
3
|
# See also https://github.com/urbanopt/urbanopt-reopt-gem/blob/develop/LICENSE.md
|
|
4
4
|
# *********************************************************************************
|
|
5
5
|
|
|
@@ -29,7 +29,8 @@ module URBANopt # :nodoc:
|
|
|
29
29
|
@use_localhost = use_localhost
|
|
30
30
|
if @use_localhost
|
|
31
31
|
@uri_submit = URI.parse('http//:127.0.0.1:8000/v3/job/')
|
|
32
|
-
@uri_submit_outagesimjob = URI.parse('http
|
|
32
|
+
@uri_submit_outagesimjob = URI.parse('http://127.0.0.1:8000/v3/erp/')
|
|
33
|
+
|
|
33
34
|
else
|
|
34
35
|
if [nil, '', '<insert your key here>'].include? nrel_developer_key
|
|
35
36
|
if [nil, '', '<insert your key here>'].include? DEVELOPER_NREL_KEY
|
|
@@ -40,7 +41,7 @@ module URBANopt # :nodoc:
|
|
|
40
41
|
end
|
|
41
42
|
@nrel_developer_key = nrel_developer_key
|
|
42
43
|
@uri_submit = URI.parse("https://developer.nrel.gov/api/reopt/v3/job?api_key=#{@nrel_developer_key}")
|
|
43
|
-
@uri_submit_outagesimjob = URI.parse("https://developer.nrel.gov/api/reopt/v3/
|
|
44
|
+
@uri_submit_outagesimjob = URI.parse("https://developer.nrel.gov/api/reopt/v3/erp?api_key=#{@nrel_developer_key}")
|
|
44
45
|
# initialize @@logger
|
|
45
46
|
@@logger ||= URBANopt::REopt.reopt_logger
|
|
46
47
|
end
|
|
@@ -76,10 +77,10 @@ module URBANopt # :nodoc:
|
|
|
76
77
|
##
|
|
77
78
|
def uri_resilience(run_uuid) # :nodoc:
|
|
78
79
|
if @use_localhost
|
|
79
|
-
return URI.parse("http://127.0.0.1:8000/v3/
|
|
80
|
+
return URI.parse("http://127.0.0.1:8000/v3/erp/#{run_uuid}/results")
|
|
80
81
|
end
|
|
81
82
|
|
|
82
|
-
return URI.parse("https://developer.nrel.gov/api/reopt/v3/
|
|
83
|
+
return URI.parse("https://developer.nrel.gov/api/reopt/v3/erp/#{run_uuid}/results?api_key=#{@nrel_developer_key}")
|
|
83
84
|
end
|
|
84
85
|
|
|
85
86
|
def make_request(http, req, max_tries = 3)
|
|
@@ -167,7 +168,7 @@ module URBANopt # :nodoc:
|
|
|
167
168
|
#
|
|
168
169
|
# [*return:*] _Bool_ - Returns true if the post succeeds. Otherwise returns false.
|
|
169
170
|
##
|
|
170
|
-
def resilience_request(run_uuid, filename)
|
|
171
|
+
def resilience_request(run_uuid, filename, reopt_input, erp_assumptions_file)
|
|
171
172
|
if File.directory? filename
|
|
172
173
|
if run_uuid.nil?
|
|
173
174
|
run_uuid = 'error'
|
|
@@ -176,23 +177,60 @@ module URBANopt # :nodoc:
|
|
|
176
177
|
run_uuid = "error#{SecureRandom.uuid}"
|
|
177
178
|
end
|
|
178
179
|
filename = File.join(filename, "#{run_uuid}_resilience.json")
|
|
180
|
+
# Save the REopt resilience results to a file
|
|
179
181
|
@@logger.info("REopt results saved to #{filename}")
|
|
180
182
|
end
|
|
181
183
|
|
|
182
|
-
#
|
|
183
|
-
@@logger.info("Submitting Resilience Statistics job for #{run_uuid}")
|
|
184
|
+
# Add info message to logger
|
|
185
|
+
@@logger.info("Submitting Resilience Statistics job for #{run_uuid}")
|
|
186
|
+
|
|
187
|
+
# Format HTTP request
|
|
184
188
|
header = { 'Content-Type' => 'application/json' }
|
|
185
189
|
http = Net::HTTP.new(@uri_submit_outagesimjob.host, @uri_submit_outagesimjob.port)
|
|
186
190
|
if !@use_localhost
|
|
187
191
|
http.use_ssl = true
|
|
188
192
|
end
|
|
193
|
+
|
|
194
|
+
# POST to erp endpoint
|
|
189
195
|
post_request = Net::HTTP::Post.new(@uri_submit_outagesimjob, header)
|
|
190
|
-
|
|
196
|
+
post = erp_assumptions_file
|
|
197
|
+
post["reopt_run_uuid"] = run_uuid
|
|
198
|
+
|
|
199
|
+
post_request.body = ::JSON.generate(post, allow_nan: true)
|
|
200
|
+
|
|
201
|
+
# Send the request
|
|
191
202
|
submit_response = make_request(http, post_request)
|
|
203
|
+
if !submit_response.is_a?(Net::HTTPSuccess)
|
|
204
|
+
@@logger.error('make_request Failed')
|
|
205
|
+
raise 'REopt connection Failed'
|
|
206
|
+
end
|
|
192
207
|
@@logger.debug(submit_response.body)
|
|
193
208
|
|
|
194
|
-
#
|
|
195
|
-
|
|
209
|
+
# Get <erp_run_uuid>
|
|
210
|
+
erp_run_uuid = JSON.parse(submit_response.body, allow_nan: true)['run_uuid']
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
if File.directory? filename
|
|
214
|
+
if erp_run_uuid.nil?
|
|
215
|
+
erp_run_uuid = 'error'
|
|
216
|
+
end
|
|
217
|
+
if erp_run_uuid.downcase.include? 'error'
|
|
218
|
+
erp_run_uuid = "error#{SecureRandom.uuid}"
|
|
219
|
+
end
|
|
220
|
+
filename = File.join(filename, "#{description}_#{erp_run_uuid}.json")
|
|
221
|
+
@@logger.info("REopt ERP results saved to #{filename}")
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
text = JSON.parse(submit_response.body, allow_nan: true)
|
|
225
|
+
if submit_response.code != '201'
|
|
226
|
+
File.open(filename, 'w+') do |f|
|
|
227
|
+
f.puts(JSON.pretty_generate(text))
|
|
228
|
+
end
|
|
229
|
+
raise "Error in REopt optimization post - see #{filename}"
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Fetch Results, pass on <erp_run_uuid>
|
|
233
|
+
uri = uri_resilience(erp_run_uuid)
|
|
196
234
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
197
235
|
if !@use_localhost
|
|
198
236
|
http.use_ssl = true
|
|
@@ -305,7 +343,7 @@ module URBANopt # :nodoc:
|
|
|
305
343
|
response = make_request(http, get_request)
|
|
306
344
|
|
|
307
345
|
data = JSON.parse(response.body, allow_nan: true)
|
|
308
|
-
|
|
346
|
+
|
|
309
347
|
if !data['outputs']['PV']
|
|
310
348
|
pv_sizes = 0
|
|
311
349
|
sizes = 0
|
|
@@ -389,7 +427,10 @@ module URBANopt # :nodoc:
|
|
|
389
427
|
end
|
|
390
428
|
|
|
391
429
|
if status == 'optimal'
|
|
392
|
-
return
|
|
430
|
+
return {
|
|
431
|
+
'data' => data,
|
|
432
|
+
'run_uuid' => run_uuid
|
|
433
|
+
}
|
|
393
434
|
end
|
|
394
435
|
|
|
395
436
|
error_message = data['messages']['error']
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# *********************************************************************************
|
|
2
|
-
# URBANopt (tm), Copyright (c) Alliance for
|
|
2
|
+
# URBANopt (tm), Copyright (c) Alliance for Energy Innovation, LLC.
|
|
3
3
|
# See also https://github.com/urbanopt/urbanopt-reopt-gem/blob/develop/LICENSE.md
|
|
4
4
|
# *********************************************************************************
|
|
5
5
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# *********************************************************************************
|
|
2
|
-
# URBANopt (tm), Copyright (c) Alliance for
|
|
2
|
+
# URBANopt (tm), Copyright (c) Alliance for Energy Innovation, LLC.
|
|
3
3
|
# See also https://github.com/urbanopt/urbanopt-reopt-gem/blob/develop/LICENSE.md
|
|
4
4
|
# *********************************************************************************
|
|
5
5
|
|
|
@@ -24,7 +24,7 @@ module URBANopt # :nodoc:
|
|
|
24
24
|
# * +use_localhost+ - _Bool_ - If this is true, requests will be sent to a version of the \REopt API running on localhost. Default is false, such that the production version of \REopt is accessed.
|
|
25
25
|
# * +nrel_developer_key+ - _String_ - API used to access the \REopt APi. Required only if +localhost+ is false. Obtain from https://developer.nrel.gov/signup/
|
|
26
26
|
##
|
|
27
|
-
def initialize(scenario_report, scenario_reopt_assumptions_file = nil, reopt_feature_assumptions = [], nrel_developer_key = nil, localhost = false)
|
|
27
|
+
def initialize(scenario_report, scenario_reopt_assumptions_file = nil, reopt_feature_assumptions = [], nrel_developer_key = nil, localhost = false, erp_assumptions_file = nil)
|
|
28
28
|
# initialize @@logger
|
|
29
29
|
@@logger ||= URBANopt::REopt.reopt_logger
|
|
30
30
|
|
|
@@ -75,9 +75,17 @@ module URBANopt # :nodoc:
|
|
|
75
75
|
@feature_reports_reopt_default_assumption_hashes << JSON.parse(File.open(file, 'r').read, symbolize_names: true)
|
|
76
76
|
end
|
|
77
77
|
end
|
|
78
|
+
|
|
79
|
+
if !erp_assumptions_file.nil?
|
|
80
|
+
@erp_assumptions_file = erp_assumptions_file
|
|
81
|
+
File.open(erp_assumptions_file, 'r') do |file|
|
|
82
|
+
@scenario_erp_default_assumptions_hash = JSON.parse(file.read, symbolize_names: true)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
78
86
|
end
|
|
79
87
|
|
|
80
|
-
attr_accessor :scenario_reopt_default_assumptions_hash, :scenario_reopt_default_output_file, :scenario_timeseries_default_output_file, :feature_reports_reopt_default_assumption_hashes, :feature_reports_reopt_default_output_files, :feature_reports_timeseries_default_output_files
|
|
88
|
+
attr_accessor :scenario_reopt_default_assumptions_hash, :scenario_reopt_default_output_file, :scenario_timeseries_default_output_file, :feature_reports_reopt_default_assumption_hashes, :feature_reports_reopt_default_output_files, :feature_reports_timeseries_default_output_files, :erp_assumptions_file
|
|
81
89
|
|
|
82
90
|
##
|
|
83
91
|
# Updates a FeatureReport based on an optional set of \REopt optimization assumptions.
|
|
@@ -101,8 +109,9 @@ module URBANopt # :nodoc:
|
|
|
101
109
|
reopt_output_file = File.join(feature_report.directory_name, 'reopt')
|
|
102
110
|
end
|
|
103
111
|
reopt_output = api.reopt_request(reopt_input, reopt_output_file)
|
|
104
|
-
|
|
112
|
+
|
|
105
113
|
if run_resilience
|
|
114
|
+
# get run UUID from the reopt output
|
|
106
115
|
run_uuid = reopt_output['outputs']['run_uuid']
|
|
107
116
|
if File.directory? reopt_output_file
|
|
108
117
|
resilience_stats = api.resilience_request(run_uuid, reopt_output_file)
|
|
@@ -128,11 +137,11 @@ module URBANopt # :nodoc:
|
|
|
128
137
|
#
|
|
129
138
|
# * +feature_report+ - _URBANopt::Reporting::DefaultReports::ScenarioReport_ - ScenarioReport which will be used in creating and then updated by a \REopt opimization response.
|
|
130
139
|
# * +reopt_assumptions_hash+ - _Hash_ - Optional. A \REopt formatted hash containing default parameters (i.e. utility rate, escalation rate) which will be updated by the ScenarioReport (i.e. location, roof availability)
|
|
131
|
-
# * +reopt_output_file+ - _String_ - Optional. Path to a file at which
|
|
140
|
+
# * +reopt_output_file+ - _String_ - Optional. Path to a file at which REopt responses will be saved.
|
|
132
141
|
# * +timeseries_csv_path+ - _String_ - Optional. Path to a file at which the new timeseries CSV for the ScenarioReport will be saved.
|
|
133
142
|
#
|
|
134
143
|
# [*return:*] _URBANopt::Scenario::DefaultReports::ScenarioReport_ Returns an updated ScenarioReport
|
|
135
|
-
def run_scenario_report(scenario_report:, reopt_assumptions_hash: nil, reopt_output_file: nil, timeseries_csv_path: nil, save_name: nil, run_resilience: false, community_photovoltaic: nil)
|
|
144
|
+
def run_scenario_report(scenario_report:, reopt_assumptions_hash: nil, reopt_output_file: nil, timeseries_csv_path: nil, save_name: nil, run_resilience: false, community_photovoltaic: nil, erp_assumptions_file: nil)
|
|
136
145
|
@save_assumptions_filepath = false
|
|
137
146
|
if !reopt_assumptions_hash.nil?
|
|
138
147
|
@scenario_reopt_default_assumptions_hash = reopt_assumptions_hash
|
|
@@ -145,26 +154,44 @@ module URBANopt # :nodoc:
|
|
|
145
154
|
if !timeseries_csv_path.nil?
|
|
146
155
|
@scenario_timeseries_default_output_file = timeseries_csv_path
|
|
147
156
|
end
|
|
148
|
-
|
|
157
|
+
if !erp_assumptions_file.nil?
|
|
158
|
+
@erp_assumptions_file = erp_assumptions_file
|
|
159
|
+
File.open(erp_assumptions_file, 'r') do |file|
|
|
160
|
+
@scenario_erp_default_assumptions_hash = JSON.parse(file.read, symbolize_names: true)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
149
163
|
api = URBANopt::REopt::REoptLiteAPI.new(@nrel_developer_key, @localhost)
|
|
150
164
|
adapter = URBANopt::REopt::ScenarioReportAdapter.new
|
|
151
165
|
|
|
152
166
|
reopt_input = adapter.reopt_json_from_scenario_report(scenario_report, @scenario_reopt_default_assumptions_hash, community_photovoltaic)
|
|
167
|
+
|
|
168
|
+
# Save inputs file (just in case)
|
|
169
|
+
# Get the directory of the output file
|
|
170
|
+
input_save_dir = File.dirname(@scenario_reopt_default_output_file)
|
|
171
|
+
input_save_path = File.join(input_save_dir, "reopt_input.json")
|
|
172
|
+
File.open(input_save_path, 'w') do |f|
|
|
173
|
+
f.write(JSON.pretty_generate(reopt_input))
|
|
174
|
+
end
|
|
175
|
+
@@logger.info("Saving REopt inputs to for inspection: #{input_save_path}.")
|
|
176
|
+
|
|
153
177
|
reopt_output = api.reopt_request(reopt_input, @scenario_reopt_default_output_file)
|
|
178
|
+
reopt_output = api.reopt_request(reopt_input, @scenario_reopt_default_output_file)['data']
|
|
179
|
+
|
|
180
|
+
run_uuid = reopt_output['run_uuid']
|
|
181
|
+
# if run resilience is set to true by user, then we will run the resilience request
|
|
154
182
|
|
|
155
183
|
if run_resilience
|
|
156
|
-
|
|
184
|
+
|
|
157
185
|
if File.directory? @scenario_reopt_default_output_file
|
|
158
|
-
resilience_stats = api.resilience_request(run_uuid, @scenario_reopt_default_output_file)
|
|
186
|
+
resilience_stats = api.resilience_request(run_uuid, @scenario_reopt_default_output_file, reopt_input, @scenario_erp_default_assumptions_hash)
|
|
159
187
|
else
|
|
160
|
-
resilience_stats = api.resilience_request(run_uuid, @scenario_reopt_default_output_file.sub('.json', '_resilience.json'))
|
|
188
|
+
resilience_stats = api.resilience_request(run_uuid, @scenario_reopt_default_output_file.sub('.json', '_resilience.json'), reopt_input, @scenario_erp_default_assumptions_hash)
|
|
161
189
|
end
|
|
162
190
|
else
|
|
163
191
|
resilience_stats = nil
|
|
164
192
|
end
|
|
165
|
-
|
|
193
|
+
|
|
166
194
|
result = adapter.update_scenario_report(scenario_report, reopt_output, @scenario_timeseries_default_output_file, resilience_stats)
|
|
167
|
-
# can you save the assumptions file path that was used?
|
|
168
195
|
if @save_assumptions_filepath && @scenario_reopt_assumptions_file
|
|
169
196
|
result.distributed_generation.reopt_assumptions_file_path = @scenario_reopt_assumptions_file
|
|
170
197
|
end
|
|
@@ -187,7 +214,7 @@ module URBANopt # :nodoc:
|
|
|
187
214
|
# * +timeseries_csv_path+ - _Array_ - Optional. A array of paths to files at which the new timeseries CSV for the FeatureReports will be saved. The number and order of the paths should match the feature_reports array.
|
|
188
215
|
#
|
|
189
216
|
# [*return:*] _Array_ Returns an array of updated _URBANopt::Scenario::DefaultReports::FeatureReport_ objects
|
|
190
|
-
def run_feature_reports(feature_reports:, reopt_assumptions_hashes: [], reopt_output_files: [], timeseries_csv_paths: [], save_names: nil, run_resilience: false, keep_existing_output: false, groundmount_photovoltaic: nil)
|
|
217
|
+
def run_feature_reports(feature_reports:, reopt_assumptions_hashes: [], reopt_output_files: [], timeseries_csv_paths: [], save_names: nil, run_resilience: false, keep_existing_output: false, groundmount_photovoltaic: nil, erp_assumptions_file: nil)
|
|
191
218
|
if !reopt_assumptions_hashes.empty?
|
|
192
219
|
@feature_reports_reopt_default_assumption_hashes = reopt_assumptions_hashes
|
|
193
220
|
end
|
|
@@ -211,6 +238,12 @@ module URBANopt # :nodoc:
|
|
|
211
238
|
@feature_reports_timeseries_default_output_files << File.join(fr.directory_name, "feature_report_#{fr.id}_timeseries.csv")
|
|
212
239
|
end
|
|
213
240
|
end
|
|
241
|
+
if erp_assumptions_file && !erp_assumptions_file.empty?
|
|
242
|
+
@erp_assumptions_file = erp_assumptions_file
|
|
243
|
+
File.open(erp_assumptions_file, 'r') do |file|
|
|
244
|
+
@feature_erp_default_assumptions_hash = JSON.parse(file.read, symbolize_names: true)
|
|
245
|
+
end
|
|
246
|
+
end
|
|
214
247
|
|
|
215
248
|
api = URBANopt::REopt::REoptLiteAPI.new(@nrel_developer_key, @localhost)
|
|
216
249
|
feature_adapter = URBANopt::REopt::FeatureReportAdapter.new
|
|
@@ -220,13 +253,16 @@ module URBANopt # :nodoc:
|
|
|
220
253
|
if !(keep_existing_output && output_exists(@feature_reports_reopt_default_output_files[idx]))
|
|
221
254
|
begin
|
|
222
255
|
reopt_input = feature_adapter.reopt_json_from_feature_report(feature_report, @feature_reports_reopt_default_assumption_hashes[idx], groundmount_photovoltaic)
|
|
256
|
+
|
|
223
257
|
reopt_output = api.reopt_request(reopt_input, @feature_reports_reopt_default_output_files[idx])
|
|
258
|
+
reopt_output = reopt_output['data'] || reopt_output
|
|
259
|
+
|
|
224
260
|
if run_resilience
|
|
225
|
-
run_uuid = reopt_output['
|
|
261
|
+
run_uuid = reopt_output['run_uuid']
|
|
226
262
|
if File.directory? @feature_reports_reopt_default_output_files[idx]
|
|
227
|
-
resilience_stats = api.resilience_request(run_uuid, @feature_reports_reopt_default_output_files[idx])
|
|
263
|
+
resilience_stats = api.resilience_request(run_uuid, @feature_reports_reopt_default_output_files[idx], reopt_input, @feature_erp_default_assumptions_hash)
|
|
228
264
|
else
|
|
229
|
-
resilience_stats = api.resilience_request(run_uuid, @feature_reports_reopt_default_output_files[idx].sub('.json', '_resilience.json'))
|
|
265
|
+
resilience_stats = api.resilience_request(run_uuid, @feature_reports_reopt_default_output_files[idx].sub('.json', '_resilience.json'), reopt_input, @feature_erp_default_assumptions_hash)
|
|
230
266
|
end
|
|
231
267
|
else
|
|
232
268
|
resilience_stats = nil
|
|
@@ -281,8 +317,8 @@ module URBANopt # :nodoc:
|
|
|
281
317
|
# * +feature_report_timeseries_csv_paths+ - _Array_ - Optional. An array of paths to files at which the new timeseries CSV for the FeatureReports will be saved. The number and order of the paths should match the array in ScenarioReport.feature_reports.
|
|
282
318
|
#
|
|
283
319
|
# [*return:*] _URBANopt::Scenario::DefaultReports::ScenarioReport_ - Returns an updated ScenarioReport
|
|
284
|
-
def run_scenario_report_features(scenario_report:, reopt_assumptions_hashes: [], reopt_output_files: [], feature_report_timeseries_csv_paths: [], save_names_feature_reports: nil, save_name_scenario_report: nil, run_resilience: false, keep_existing_output: false, groundmount_photovoltaic: nil)
|
|
285
|
-
new_feature_reports = run_feature_reports(feature_reports: scenario_report.feature_reports, reopt_assumptions_hashes: reopt_assumptions_hashes, reopt_output_files: reopt_output_files, timeseries_csv_paths: feature_report_timeseries_csv_paths, save_names: save_names_feature_reports, run_resilience: run_resilience, keep_existing_output: keep_existing_output, groundmount_photovoltaic: groundmount_photovoltaic)
|
|
320
|
+
def run_scenario_report_features(scenario_report:, reopt_assumptions_hashes: [], reopt_output_files: [], feature_report_timeseries_csv_paths: [], save_names_feature_reports: nil, save_name_scenario_report: nil, run_resilience: false, keep_existing_output: false, groundmount_photovoltaic: nil, erp_assumptions_file: nil)
|
|
321
|
+
new_feature_reports = run_feature_reports(feature_reports: scenario_report.feature_reports, reopt_assumptions_hashes: reopt_assumptions_hashes, reopt_output_files: reopt_output_files, timeseries_csv_paths: feature_report_timeseries_csv_paths, save_names: save_names_feature_reports, run_resilience: run_resilience, keep_existing_output: keep_existing_output, groundmount_photovoltaic: groundmount_photovoltaic, erp_assumptions_file: erp_assumptions_file)
|
|
286
322
|
|
|
287
323
|
# only do this if you have run feature reports
|
|
288
324
|
new_scenario_report = URBANopt::Reporting::DefaultReports::ScenarioReport.new
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "REopt GHP BAU Inputs (Minimal Valid Schema Wrapper)",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"properties": {
|
|
5
|
+
"Site": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"latitude": {
|
|
9
|
+
"type": "number",
|
|
10
|
+
"minimum": -90,
|
|
11
|
+
"maximum": 90,
|
|
12
|
+
"description": "The approximate latitude of the site in decimal degrees."
|
|
13
|
+
},
|
|
14
|
+
"longitude": {
|
|
15
|
+
"type": "number",
|
|
16
|
+
"minimum": -180,
|
|
17
|
+
"maximum": 180,
|
|
18
|
+
"description": "The approximate longitude of the site in decimal degrees."
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"required": ["latitude", "longitude"]
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
"SpaceHeatingLoad": {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"properties": {
|
|
27
|
+
"fuel_loads_mmbtu_per_hour": {
|
|
28
|
+
"type": "array",
|
|
29
|
+
"description": "This is the fuel energy consumption for space heating at the individual building level. This is used in the business as usual (BAU) LCCA analysis in the GHP module to calculate BAU bills. These are 8760 hourly timeseries values.",
|
|
30
|
+
"note": "This parameter is required to run REopt's BAU for GHP module. If there is no fuel based heating in BAU, add values close to 0."
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"required": ["fuel_loads_mmbtu_per_hour"]
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
"DomesticHotWaterLoad": {
|
|
37
|
+
"type": "object",
|
|
38
|
+
"properties": {
|
|
39
|
+
"fuel_loads_mmbtu_per_hour": {
|
|
40
|
+
"type": "array",
|
|
41
|
+
"description": "This is the fuel enegry consumption for providing domestic hot water at the individual building level. This is used in the business as usual (BAU) LCCA analysis in the GHP module. These are 8760 hourly timeseries values.",
|
|
42
|
+
"note": "Only required if BAU system has DHW load and uses fuel for this load. If BAU system has no DHW load, or does not use fuel for DHW heating, set to values close to zeros. If BAU system uses electricity for DHW, please add this electric DHW load to ElectricLoad"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
"CoolingLoad": {
|
|
48
|
+
"type": "object",
|
|
49
|
+
"properties": {
|
|
50
|
+
"fuel_loads_mmbtu_per_hour": {
|
|
51
|
+
"type": "array",
|
|
52
|
+
"description": "This is the fuel energy consumption for space cooling at the individual building level. This is used in the business as usual (BAU) LCCA analysis in the GHP module to calculate BAU bills. These are 8760 hourly timeseries values.",
|
|
53
|
+
"note": "Only required if BAU system uses fuel for cooling. If BAU system uses electricity for cooling (AC), please add this load to ElectricLoad defined below. If BAU system has no cooling load, set to values close to zeros."
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"required": ["fuel_loads_mmbtu_per_hour"]
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
"ElectricLoad": {
|
|
60
|
+
"type": "object",
|
|
61
|
+
"properties": {
|
|
62
|
+
"loads_kw": {
|
|
63
|
+
"type": "array",
|
|
64
|
+
"description": "8760 timeseries",
|
|
65
|
+
"note": "This is the electric load profile for heating and cooling in the BAU scenario (kW). It is an hourly timeseries load profile with 8760 values. If there is no electricity based heating or cooling in BAU, set these values close to 0."
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"required": ["loads_kw"]
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
"ElectricTarriff": {
|
|
72
|
+
"type": "object",
|
|
73
|
+
"description": "Label attribute of utility rate structure from https://openei.org/services/doc/rest/util_rates/?version=3.",
|
|
74
|
+
"properties": {
|
|
75
|
+
"urdb_label": { "type": "string" }
|
|
76
|
+
},
|
|
77
|
+
"required": ["urdb_label"]
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
"ExistingBoiler": {
|
|
81
|
+
"type": "object",
|
|
82
|
+
"properties": {
|
|
83
|
+
"fuel_cost_per_mmbtu": {
|
|
84
|
+
"type": "number",
|
|
85
|
+
"description": "Only required if BAU system use fuel for heating. In this case, fuel cost needs to be specified"
|
|
86
|
+
},
|
|
87
|
+
"installed_cost_per_mmbtu_per_hour": {
|
|
88
|
+
"type": "number",
|
|
89
|
+
"description": "Capital cost of ExistingBoiler. Include if counting the capital cost of upgrading ExistingBoiler in the BAU system in NPV calculation. Default is $56,000/MMBtu (EIA). The peak heating load is considered to be the size of the system and is multiplied by this field to get total capital cost. Note: this option can only be called from the API after July 2, 2025"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
"ElectricHeater": {
|
|
95
|
+
"type": "object",
|
|
96
|
+
"properties": {
|
|
97
|
+
"installed_cost_per_mmbtu_per_hour": {
|
|
98
|
+
"type": "number",
|
|
99
|
+
"description": "Capital cost of ElectricHeater/Electric furnace. Include if BAU system use electric heater/electric furnace for heating and if counting capital cost of upgrading electric heater/furnace in the BAU system in NPV calculation. Default value is $59,412/MMBtu (EIA)"
|
|
100
|
+
},
|
|
101
|
+
"min_mmbtu_per_hour": {
|
|
102
|
+
"type": "number",
|
|
103
|
+
"description": "User defined input for the size of electric heater system. This value should be the same in min_mmbtu_per_hour and max_mmbtu_per_hour."
|
|
104
|
+
},
|
|
105
|
+
"max_mmbtu_per_hour": {
|
|
106
|
+
"type": "number",
|
|
107
|
+
"description": "User defined input for the size of electric heater system. This value should be the same in min_mmbtu_per_hour and max_mmbtu_per_hour."
|
|
108
|
+
},
|
|
109
|
+
"can_serve_space_heating": {
|
|
110
|
+
"type": "boolean",
|
|
111
|
+
"description": "true if electric heater can serve space heating load in the BAU system"
|
|
112
|
+
},
|
|
113
|
+
"can_serve_dhw": {
|
|
114
|
+
"type": "boolean",
|
|
115
|
+
"description": "true if electric heater can serve water heating load in the BAU system"
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
"Financial": {
|
|
121
|
+
"type": "object",
|
|
122
|
+
"properties": {
|
|
123
|
+
"elec_cost_escalation_rate_fraction": {
|
|
124
|
+
"default": 0.026,
|
|
125
|
+
"minimum": -1,
|
|
126
|
+
"maximum": 1,
|
|
127
|
+
"type": "number",
|
|
128
|
+
"description": "Annual nominal utility electricity cost escalation rate",
|
|
129
|
+
"required": false
|
|
130
|
+
},
|
|
131
|
+
"owner_discount_rate_fraction": {
|
|
132
|
+
"default": 0.081,
|
|
133
|
+
"minimum": 0,
|
|
134
|
+
"maximum": 1,
|
|
135
|
+
"type": "number",
|
|
136
|
+
"description": "Nominal host discount rate. This is used when host site owns the investment",
|
|
137
|
+
"required": false
|
|
138
|
+
},
|
|
139
|
+
"offtaker_discount_rate_fraction": {
|
|
140
|
+
"default": 0.081,
|
|
141
|
+
"minimum": 0,
|
|
142
|
+
"maximum": 1,
|
|
143
|
+
"type": "number",
|
|
144
|
+
"description": "Nominal discount rate. This is used when site finances the investment through a third party",
|
|
145
|
+
"required": false
|
|
146
|
+
},
|
|
147
|
+
"owner_tax_rate_fraction": {
|
|
148
|
+
"default": 0.26,
|
|
149
|
+
"minimum": 0,
|
|
150
|
+
"maximum": 1,
|
|
151
|
+
"type": "number",
|
|
152
|
+
"description": "Tax rate site when host site owns the investment",
|
|
153
|
+
"required": false
|
|
154
|
+
},
|
|
155
|
+
"offtaker_tax_rate_fraction": {
|
|
156
|
+
"default": 0.26,
|
|
157
|
+
"minimum": 0,
|
|
158
|
+
"maximum": 1,
|
|
159
|
+
"type": "number",
|
|
160
|
+
"description": "Tax rate when site finances the investment through a third party",
|
|
161
|
+
"required": false
|
|
162
|
+
},
|
|
163
|
+
"om_cost_escalation_rate_fraction": {
|
|
164
|
+
"default": 0.025,
|
|
165
|
+
"minimum": -1,
|
|
166
|
+
"maximum": 1,
|
|
167
|
+
"type": "number",
|
|
168
|
+
"description": "Annual nominal O&M cost escalation rate",
|
|
169
|
+
"required": false
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|