urbanopt-reopt 1.1.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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -0
  3. data/CONTRIBUTING.md +3 -3
  4. data/Gemfile +13 -6
  5. data/LICENSE.md +1 -1
  6. data/RDOC_MAIN.md +2 -2
  7. data/README.md +2 -2
  8. data/Rakefile +1 -1
  9. data/developer_nrel_key.rb +1 -1
  10. data/doc_templates/LICENSE.md +1 -1
  11. data/doc_templates/copyright_erb.txt +1 -1
  12. data/doc_templates/copyright_js.txt +1 -1
  13. data/doc_templates/copyright_ruby.txt +1 -1
  14. data/docs/README.md +2 -2
  15. data/docs/package.json +1 -1
  16. data/index.md +2 -2
  17. data/lib/urbanopt/reopt/extension.rb +1 -1
  18. data/lib/urbanopt/reopt/feature_report_adapter.rb +10 -11
  19. data/lib/urbanopt/reopt/{reopt_ghp_adapter.rb → reopt_ghp_adapter_ghp.rb} +238 -48
  20. data/lib/urbanopt/reopt/reopt_ghp_api.rb +3 -3
  21. data/lib/urbanopt/reopt/reopt_ghp_files/reopt_ghp_assumption.json +3 -3
  22. data/lib/urbanopt/reopt/reopt_ghp_post_processor.rb +13 -4
  23. data/lib/urbanopt/reopt/reopt_ghp_result.rb +145 -0
  24. data/lib/urbanopt/reopt/reopt_lite_api.rb +54 -13
  25. data/lib/urbanopt/reopt/reopt_logger.rb +1 -1
  26. data/lib/urbanopt/reopt/reopt_post_processor.rb +54 -18
  27. data/lib/urbanopt/reopt/reopt_schema/REopt-BAU-input.json +174 -0
  28. data/lib/urbanopt/reopt/reopt_schema/REopt-GHP-input.json +263 -95
  29. data/lib/urbanopt/reopt/scenario/reopt_scenario_csv.rb +1 -1
  30. data/lib/urbanopt/reopt/scenario_report_adapter.rb +8 -9
  31. data/lib/urbanopt/reopt/utilities.rb +1 -1
  32. data/lib/urbanopt/reopt/version.rb +2 -2
  33. data/lib/urbanopt/reopt.rb +3 -2
  34. data/lib/urbanopt/reopt_scenario.rb +1 -1
  35. data/lib/urbanopt-reopt.rb +1 -1
  36. data/urbanopt-reopt.gemspec +1 -1
  37. metadata +7 -5
@@ -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 Sustainable Energy, LLC.
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//:127.0.0.1:8000/v3/outagesimjob/')
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/outagesimjob?api_key=#{@nrel_developer_key}")
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/job/#{run_uuid}/resilience_stats")
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/job/#{run_uuid}/resilience_stats?api_key=#{@nrel_developer_key}")
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
- # Submit Job
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
- post_request.body = ::JSON.generate({ 'run_uuid' => run_uuid, 'bau' => false }, allow_nan: true)
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
- # Fetch Results
195
- uri = uri_resilience(run_uuid)
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 data
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 Sustainable Energy, LLC.
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 Sustainable Energy, LLC.
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
- @@logger.debug("REOpt output file: #{reopt_output_file}")
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 REpopt responses will be saved.
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
- run_uuid = reopt_output['outputs']['run_uuid']
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['outputs']['run_uuid']
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
+ }