urbanopt-reopt 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +27 -0
- data/.rakeTasks +7 -0
- data/.rdoc_options +37 -0
- data/.rspec +3 -0
- data/.rubocop.yml +9 -0
- data/.travis.yml +22 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +67 -0
- data/Jenkinsfile +10 -0
- data/LICENSE.md +27 -0
- data/RDOC_MAIN.md +176 -0
- data/README.md +177 -0
- data/Rakefile +30 -0
- data/deploy_docs.sh +5 -0
- data/developer_nrel_key.rb +31 -0
- data/doc_templates/LICENSE.md +27 -0
- data/doc_templates/README.md.erb +42 -0
- data/doc_templates/copyright_erb.txt +31 -0
- data/doc_templates/copyright_js.txt +4 -0
- data/doc_templates/copyright_ruby.txt +29 -0
- data/docs/.gitignore +3 -0
- data/docs/.vuepress/components/InnerJsonSchema.vue +78 -0
- data/docs/.vuepress/components/JsonSchema.vue +12 -0
- data/docs/.vuepress/components/ReoptInputSchema.vue +12 -0
- data/docs/.vuepress/components/ReoptOutputSchema.vue +12 -0
- data/docs/.vuepress/components/StaticLink.vue +8 -0
- data/docs/.vuepress/config.js +16 -0
- data/docs/.vuepress/highlight.js +8 -0
- data/docs/.vuepress/public/custom_rdoc_styles.css +58 -0
- data/docs/.vuepress/utils.js +17 -0
- data/docs/README.md +196 -0
- data/docs/package-lock.json +254 -0
- data/docs/package.json +22 -0
- data/docs/schemas/reopt-input-schema.md +57 -0
- data/docs/schemas/reopt-output-schema.md +66 -0
- data/index.html +1 -0
- data/index.md +176 -0
- data/lib/files/.gitkeep +0 -0
- data/lib/urbanopt/reopt/extension.rb +44 -0
- data/lib/urbanopt/reopt/feature_report_adapter.rb +364 -0
- data/lib/urbanopt/reopt/reopt_lite_api.rb +230 -0
- data/lib/urbanopt/reopt/reopt_logger.rb +42 -0
- data/lib/urbanopt/reopt/reopt_post_processor.rb +245 -0
- data/lib/urbanopt/reopt/reopt_schema/reopt_input_schema.json +1111 -0
- data/lib/urbanopt/reopt/reopt_schema/reopt_output_schema.json +538 -0
- data/lib/urbanopt/reopt/scenario/reopt_scenario_csv.rb +115 -0
- data/lib/urbanopt/reopt/scenario_report_adapter.rb +404 -0
- data/lib/urbanopt/reopt/version.rb +35 -0
- data/lib/urbanopt/reopt.rb +36 -0
- data/lib/urbanopt/reopt_scenario.rb +31 -0
- data/lib/urbanopt-reopt.rb +31 -0
- data/urbanopt-reopt.gemspec +33 -0
- metadata +194 -0
@@ -0,0 +1,364 @@
|
|
1
|
+
# *********************************************************************************
|
2
|
+
# URBANopt, Copyright (c) 2019-2020, Alliance for Sustainable Energy, LLC, and other
|
3
|
+
# contributors. All rights reserved.
|
4
|
+
#
|
5
|
+
# Redistribution and use in source and binary forms, with or without modification,
|
6
|
+
# are permitted provided that the following conditions are met:
|
7
|
+
#
|
8
|
+
# Redistributions of source code must retain the above copyright notice, this list
|
9
|
+
# of conditions and the following disclaimer.
|
10
|
+
#
|
11
|
+
# Redistributions in binary form must reproduce the above copyright notice, this
|
12
|
+
# list of conditions and the following disclaimer in the documentation and/or other
|
13
|
+
# materials provided with the distribution.
|
14
|
+
#
|
15
|
+
# Neither the name of the copyright holder nor the names of its contributors may be
|
16
|
+
# used to endorse or promote products derived from this software without specific
|
17
|
+
# prior written permission.
|
18
|
+
#
|
19
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
20
|
+
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
21
|
+
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
22
|
+
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
|
23
|
+
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
24
|
+
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
25
|
+
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
26
|
+
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
27
|
+
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
|
28
|
+
# OF THE POSSIBILITY OF SUCH DAMAGE.
|
29
|
+
# *********************************************************************************
|
30
|
+
|
31
|
+
require 'urbanopt/scenario/default_reports'
|
32
|
+
require 'urbanopt/reopt/reopt_logger'
|
33
|
+
require 'csv'
|
34
|
+
require 'matrix'
|
35
|
+
|
36
|
+
module URBANopt # :nodoc:
|
37
|
+
module REopt # :nodoc:
|
38
|
+
class FeatureReportAdapter
|
39
|
+
##
|
40
|
+
# FeatureReportAdapter can convert a URBANopt::Scenario::DefaultReports::FeatureReport into a \REopt Lite posts or update a URBANopt::Scenario::DefaultReports::FeatureReport from a \REopt Lite response.
|
41
|
+
##
|
42
|
+
# [*parameters:*]
|
43
|
+
##
|
44
|
+
def initialize
|
45
|
+
# initialize @@logger
|
46
|
+
@@logger ||= URBANopt::REopt.reopt_logger
|
47
|
+
end
|
48
|
+
|
49
|
+
##
|
50
|
+
# Convert a FeatureReport into a \REopt Lite post
|
51
|
+
#
|
52
|
+
# [*parameters:*]
|
53
|
+
#
|
54
|
+
# * +feature_report+ - _URBANopt::Scenario::DefaultReports::FeatureReport_ - FeatureReport to use in converting the optional +reopt_assumptions_hash+ to a \REopt Lite post. If a +reopt_assumptions_hash+ is not provided, a default post will be updated from this FeatureReport and submitted to the \REopt Lite API.
|
55
|
+
# * +reopt_assumptions_hash+ - _Hash_ - Optional. A hash formatted for submittal to the \REopt Lite API containing default values. Values will be overwritten from the FeatureReport where available (i.e. latitude, roof_squarefeet). Missing optional parameters will be filled in with default values by the API.
|
56
|
+
#
|
57
|
+
# [*return:*] _Hash_ - Returns hash formatted for submittal to the \REopt Lite API
|
58
|
+
##
|
59
|
+
def reopt_json_from_feature_report(feature_report, reopt_assumptions_hash = nil)
|
60
|
+
name = feature_report.name.delete ' '
|
61
|
+
description = "feature_report_#{name}_#{feature_report.id}"
|
62
|
+
reopt_inputs = { Scenario: { Site: { ElectricTariff: { blended_monthly_demand_charges_us_dollars_per_kw: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], blended_monthly_rates_us_dollars_per_kwh: [0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13] }, LoadProfile: {}, Wind: { max_kw: 0 } } } }
|
63
|
+
if !reopt_assumptions_hash.nil?
|
64
|
+
reopt_inputs = reopt_assumptions_hash
|
65
|
+
else
|
66
|
+
@@logger.info('Using default REopt Lite assumptions')
|
67
|
+
end
|
68
|
+
|
69
|
+
# Check FeatureReport has required data
|
70
|
+
requireds_names = ['latitude', 'longitude']
|
71
|
+
requireds = [feature_report.location.latitude, feature_report.location.longitude]
|
72
|
+
|
73
|
+
if requireds.include?(nil) || requireds.include?(0)
|
74
|
+
requireds.each_with_index do |i, x|
|
75
|
+
if [nil, 0].include? x
|
76
|
+
n = requireds_names[i]
|
77
|
+
p 'a' # @@logger.error("Missing value for #{n} - this is a required input")
|
78
|
+
raise "Missing value for #{n} - this is a required input"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
reopt_inputs[:Scenario][:description] = description
|
84
|
+
|
85
|
+
# Parse Location
|
86
|
+
reopt_inputs[:Scenario][:Site][:latitude] = feature_report.location.latitude
|
87
|
+
reopt_inputs[:Scenario][:Site][:longitude] = feature_report.location.longitude
|
88
|
+
|
89
|
+
# Parse Optional FeatureReport metrics
|
90
|
+
unless feature_report.program.roof_area.nil?
|
91
|
+
reopt_inputs[:Scenario][:Site][:roof_squarefeet] = feature_report.program.roof_area[:available_roof_area]
|
92
|
+
end
|
93
|
+
|
94
|
+
unless feature_report.program.site_area.nil?
|
95
|
+
reopt_inputs[:Scenario][:Site][:land_acres] = feature_report.program.site_area * 1.0 / 43560 # acres/sqft
|
96
|
+
end
|
97
|
+
|
98
|
+
# Parse Load Profile
|
99
|
+
begin
|
100
|
+
col_num = feature_report.timeseries_csv.column_names.index('Electricity:Facility')
|
101
|
+
t = CSV.read(feature_report.timeseries_csv.path, headers: true, converters: :numeric)
|
102
|
+
energy_timeseries_kwh = t.by_col[col_num].map { |e| ((e || 0) * 0.293071) } # convert kBTU to KWH
|
103
|
+
if (feature_report.timesteps_per_hour || 1) > 1
|
104
|
+
energy_timeseries_kwh = energy_timeseries_kwh.each_slice(feature_report.timesteps_per_hour).to_a.map { |x| x.inject(0, :+) / x.length.to_f }
|
105
|
+
end
|
106
|
+
|
107
|
+
if energy_timeseries_kwh.length < feature_report.timesteps_per_hour * 8760
|
108
|
+
energy_timeseries_kwh += [0] * ((feature_report.timesteps_per_hour * 8760) - energy_timeseries_kwh.length)
|
109
|
+
@@logger.info("Assuming load profile for Feature Report #{feature_report.name} #{feature_report.id} starts January 1 - filling in rest with zeros")
|
110
|
+
end
|
111
|
+
reopt_inputs[:Scenario][:Site][:LoadProfile][:loads_kw] = energy_timeseries_kwh.map { |e| e ? e : 0 }
|
112
|
+
rescue StandardError
|
113
|
+
@@logger.error("Could not parse the annual electric load from the timeseries csv - #{feature_report.timeseries_csv.path}")
|
114
|
+
raise "Could not parse the annual electric load from the timeseries csv - #{feature_report.timeseries_csv.path}"
|
115
|
+
end
|
116
|
+
return reopt_inputs
|
117
|
+
end
|
118
|
+
|
119
|
+
##
|
120
|
+
# Update a FeatureReport from a \REopt Lite response
|
121
|
+
#
|
122
|
+
# [*parameters:*]
|
123
|
+
#
|
124
|
+
# * +feature_report+ - _URBANopt::Scenario::DefaultReports::FeatureReport_ - FeatureReport to update from a \REopt Lite reponse hash.
|
125
|
+
# * +reopt_output+ - _Hash_ - A reponse hash from the \REopt Lite API to use in overwriting FeatureReport technology sizes, costs and dispatch strategies.
|
126
|
+
# * +timeseries_csv_path+ - _String_ - Optional. The path to a file at which a new timeseries CSV will be written. If not provided a file is created based on the run_uuid of the \REopt Lite optimization task.
|
127
|
+
#
|
128
|
+
# [*return:*] _URBANopt::Scenario::DefaultReports::FeatureReport_ - Returns an updated FeatureReport.
|
129
|
+
##
|
130
|
+
def update_feature_report(feature_report, reopt_output, timeseries_csv_path = nil)
|
131
|
+
# Check if the \REopt Lite response is valid
|
132
|
+
if reopt_output['outputs']['Scenario']['status'] != 'optimal'
|
133
|
+
@@logger.info("Warning cannot Feature Report #{feature_report.name} #{feature_report.id} - REopt optimization was non-optimal")
|
134
|
+
return feature_report
|
135
|
+
end
|
136
|
+
|
137
|
+
# Update location
|
138
|
+
feature_report.location.latitude = reopt_output['inputs']['Scenario']['Site']['latitude']
|
139
|
+
feature_report.location.longitude = reopt_output['inputs']['Scenario']['Site']['longitude']
|
140
|
+
|
141
|
+
# Update timeseries csv from \REopt Lite dispatch data
|
142
|
+
feature_report.timesteps_per_hour = reopt_output['inputs']['Scenario']['time_steps_per_hour']
|
143
|
+
|
144
|
+
# Update distributed generation sizing and financials
|
145
|
+
(feature_report.distributed_generation.lcc_us_dollars = reopt_output['outputs']['Scenario']['Site']['Financial']['lcc_us_dollars']) || 0
|
146
|
+
(feature_report.distributed_generation.npv_us_dollars = reopt_output['outputs']['Scenario']['Site']['Financial']['npv_us_dollars']) || 0
|
147
|
+
(feature_report.distributed_generation.year_one_energy_cost_us_dollars = reopt_output['outputs']['Scenario']['Site']['ElectricTariff']['year_one_energy_cost_us_dollars']) || 0
|
148
|
+
(feature_report.distributed_generation.year_one_demand_cost_us_dollars = reopt_output['outputs']['Scenario']['Site']['ElectricTariff']['year_one_demand_cost_us_dollars']) || 0
|
149
|
+
(feature_report.distributed_generation.year_one_bill_us_dollars = reopt_output['outputs']['Scenario']['Site']['ElectricTariff']['year_one_bill_us_dollars']) || 0
|
150
|
+
(feature_report.distributed_generation.total_energy_cost_us_dollars = reopt_output['outputs']['Scenario']['Site']['ElectricTariff']['total_energy_cost_us_dollars']) || 0
|
151
|
+
|
152
|
+
(feature_report.distributed_generation.solar_pv.size_kw = reopt_output['outputs']['Scenario']['Site']['PV']['size_kw']) || 0
|
153
|
+
(feature_report.distributed_generation.wind.size_kw = reopt_output['outputs']['Scenario']['Site']['Wind']['size_kw']) || 0
|
154
|
+
(feature_report.distributed_generation.generator.size_kw = reopt_output['outputs']['Scenario']['Site']['Generator']['size_kw']) || 0
|
155
|
+
(feature_report.distributed_generation.storage.size_kw = reopt_output['outputs']['Scenario']['Site']['Storage']['size_kw']) || 0
|
156
|
+
(feature_report.distributed_generation.storage.size_kwh = reopt_output['outputs']['Scenario']['Site']['Storage']['size_kwh']) || 0
|
157
|
+
|
158
|
+
generation_timeseries_kwh = Matrix[[0] * 8760]
|
159
|
+
|
160
|
+
unless reopt_output['outputs']['Scenario']['Site']['PV'].nil?
|
161
|
+
if (reopt_output['outputs']['Scenario']['Site']['PV']['size_kw'] || 0) > 0
|
162
|
+
if !reopt_output['outputs']['Scenario']['Site']['PV']['year_one_power_production_series_kw'].nil?
|
163
|
+
generation_timeseries_kwh += Matrix[reopt_output['outputs']['Scenario']['Site']['PV']['year_one_power_production_series_kw']]
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# unless reopt_output['outputs']['Scenario']['Site']['Storage'].nil?
|
169
|
+
# if (reopt_output['outputs']['Scenario']['Site']['Storage']['size_kw'] or 0) > 0
|
170
|
+
# if !reopt_output['outputs']['Scenario']['Site']['Storage']['year_one_to_grid_series_kw'].nil?
|
171
|
+
# generation_timeseries_kwh = generation_timeseries_kwh + Matrix[reopt_output['outputs']['Scenario']['Site']['Storage']['year_one_to_grid_series_kw']]
|
172
|
+
# end
|
173
|
+
# end
|
174
|
+
# end
|
175
|
+
|
176
|
+
unless reopt_output['outputs']['Scenario']['Site']['Wind'].nil?
|
177
|
+
if (reopt_output['outputs']['Scenario']['Site']['Wind']['size_kw'] || 0) > 0
|
178
|
+
if !reopt_output['outputs']['Scenario']['Site']['Wind']['year_one_power_production_series_kw'].nil?
|
179
|
+
generation_timeseries_kwh += Matrix[reopt_output['outputs']['Scenario']['Site']['Wind']['year_one_power_production_series_kw']]
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
unless reopt_output['outputs']['Scenario']['Site']['Generator'].nil?
|
185
|
+
if (reopt_output['outputs']['Scenario']['Site']['Generator']['size_kw'] || 0) > 0
|
186
|
+
if !reopt_output['outputs']['Scenario']['Site']['Generator']['year_one_power_production_series_kw'].nil?
|
187
|
+
generation_timeseries_kwh += Matrix[reopt_output['outputs']['Scenario']['Site']['Generator']['year_one_power_production_series_kw']]
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
$generation_timeseries_kwh = generation_timeseries_kwh.to_a[0] || [0] * 8760
|
193
|
+
$generation_timeseries_kwh_col = feature_report.timeseries_csv.column_names.index('ElectricityProduced:Total')
|
194
|
+
if $generation_timeseries_kwh_col.nil?
|
195
|
+
$generation_timeseries_kwh_col = feature_report.timeseries_csv.column_names.length
|
196
|
+
feature_report.timeseries_csv.column_names.push('ElectricityProduced:Total')
|
197
|
+
end
|
198
|
+
|
199
|
+
$load = reopt_output['outputs']['Scenario']['Site']['LoadProfile']['year_one_electric_load_series_kw'] || [0] * 8760
|
200
|
+
$load_col = feature_report.timeseries_csv.column_names.index('Electricity:Load:Total')
|
201
|
+
if $load_col.nil?
|
202
|
+
$load_col = feature_report.timeseries_csv.column_names.length
|
203
|
+
feature_report.timeseries_csv.column_names.push('Electricity:Load:Total')
|
204
|
+
end
|
205
|
+
|
206
|
+
$utility_to_load = reopt_output['outputs']['Scenario']['Site']['ElectricTariff']['year_one_to_load_series_kw'] || [0] * 8760
|
207
|
+
$utility_to_load_col = feature_report.timeseries_csv.column_names.index('Electricity:Grid:ToLoad')
|
208
|
+
if $utility_to_load_col.nil?
|
209
|
+
$utility_to_load_col = feature_report.timeseries_csv.column_names.length
|
210
|
+
feature_report.timeseries_csv.column_names.push('Electricity:Grid:ToLoad')
|
211
|
+
end
|
212
|
+
|
213
|
+
$utility_to_battery = reopt_output['outputs']['Scenario']['Site']['ElectricTariff']['year_one_to_battery_series_kw'] || [0] * 8760
|
214
|
+
$utility_to_battery_col = feature_report.timeseries_csv.column_names.index('Electricity:Grid:ToBattery')
|
215
|
+
if $utility_to_battery_col.nil?
|
216
|
+
$utility_to_battery_col = feature_report.timeseries_csv.column_names.length
|
217
|
+
feature_report.timeseries_csv.column_names.push('Electricity:Grid:ToBattery')
|
218
|
+
end
|
219
|
+
|
220
|
+
$storage_to_load = reopt_output['outputs']['Scenario']['Site']['Storage']['year_one_to_load_series_kw'] || [0] * 8760
|
221
|
+
$storage_to_load_col = feature_report.timeseries_csv.column_names.index('Electricity:Storage:ToLoad')
|
222
|
+
if $storage_to_load_col.nil?
|
223
|
+
$storage_to_load_col = feature_report.timeseries_csv.column_names.length
|
224
|
+
feature_report.timeseries_csv.column_names.push('Electricity:Storage:ToLoad')
|
225
|
+
end
|
226
|
+
|
227
|
+
$storage_to_grid = reopt_output['outputs']['Scenario']['Site']['Storage']['year_one_to_grid_series_kw'] || [0] * 8760
|
228
|
+
$storage_to_grid_col = feature_report.timeseries_csv.column_names.index('Electricity:Storage:ToGrid')
|
229
|
+
if $storage_to_grid_col.nil?
|
230
|
+
$storage_to_grid_col = feature_report.timeseries_csv.column_names.length
|
231
|
+
feature_report.timeseries_csv.column_names.push('Electricity:Storage:ToGrid')
|
232
|
+
end
|
233
|
+
|
234
|
+
$storage_soc = reopt_output['outputs']['Scenario']['Site']['Storage']['year_one_soc_series_pct'] || [0] * 8760
|
235
|
+
$storage_soc_col = feature_report.timeseries_csv.column_names.index('Electricity:Storage:StateOfCharge')
|
236
|
+
if $storage_soc_col.nil?
|
237
|
+
$storage_soc_col = feature_report.timeseries_csv.column_names.length
|
238
|
+
feature_report.timeseries_csv.column_names.push('Electricity:Storage:StateOfCharge')
|
239
|
+
end
|
240
|
+
|
241
|
+
$generator_total = reopt_output['outputs']['Scenario']['Site']['Generator']['year_one_power_production_series_kw'] || [0] * 8760
|
242
|
+
$generator_total_col = feature_report.timeseries_csv.column_names.index('ElectricityProduced:Generator:Total')
|
243
|
+
if $generator_total_col.nil?
|
244
|
+
$generator_total_col = feature_report.timeseries_csv.column_names.length
|
245
|
+
feature_report.timeseries_csv.column_names.push('ElectricityProduced:Generator:Total')
|
246
|
+
end
|
247
|
+
|
248
|
+
$generator_to_battery = reopt_output['outputs']['Scenario']['Site']['Generator']['year_one_to_battery_series_kw'] || [0] * 8760
|
249
|
+
$generator_to_battery_col = feature_report.timeseries_csv.column_names.index('ElectricityProduced:Generator:ToBattery')
|
250
|
+
if $generator_to_battery_col.nil?
|
251
|
+
$generator_to_battery_col = feature_report.timeseries_csv.column_names.length
|
252
|
+
feature_report.timeseries_csv.column_names.push('ElectricityProduced:Generator:ToBattery')
|
253
|
+
end
|
254
|
+
|
255
|
+
$generator_to_load = reopt_output['outputs']['Scenario']['Site']['Generator']['year_one_to_load_series_kw'] || [0] * 8760
|
256
|
+
$generator_to_load_col = feature_report.timeseries_csv.column_names.index('ElectricityProduced:Generator:ToLoad')
|
257
|
+
if $generator_to_load_col.nil?
|
258
|
+
$generator_to_load_col = feature_report.timeseries_csv.column_names.length
|
259
|
+
feature_report.timeseries_csv.column_names.push('ElectricityProduced:Generator:ToLoad')
|
260
|
+
end
|
261
|
+
|
262
|
+
$generator_to_grid = reopt_output['outputs']['Scenario']['Site']['Generator']['year_one_to_grid_series_kw'] || [0] * 8760
|
263
|
+
$generator_to_grid_col = feature_report.timeseries_csv.column_names.index('ElectricityProduced:Generator:ToGrid')
|
264
|
+
if $generator_to_grid_col.nil?
|
265
|
+
$generator_to_grid_col = feature_report.timeseries_csv.column_names.length
|
266
|
+
feature_report.timeseries_csv.column_names.push('ElectricityProduced:Generator:ToGrid')
|
267
|
+
end
|
268
|
+
|
269
|
+
$pv_total = reopt_output['outputs']['Scenario']['Site']['PV']['year_one_power_production_series_kw'] || [0] * 8760
|
270
|
+
$pv_total_col = feature_report.timeseries_csv.column_names.index('ElectricityProduced:PV:Total')
|
271
|
+
if $pv_total_col.nil?
|
272
|
+
$pv_total_col = feature_report.timeseries_csv.column_names.length
|
273
|
+
feature_report.timeseries_csv.column_names.push('ElectricityProduced:PV:Total')
|
274
|
+
end
|
275
|
+
|
276
|
+
$pv_to_battery = reopt_output['outputs']['Scenario']['Site']['PV']['year_one_to_battery_series_kw'] || [0] * 8760
|
277
|
+
$pv_to_battery_col = feature_report.timeseries_csv.column_names.index('ElectricityProduced:PV:ToBattery')
|
278
|
+
if $pv_to_battery_col.nil?
|
279
|
+
$pv_to_battery_col = feature_report.timeseries_csv.column_names.length
|
280
|
+
feature_report.timeseries_csv.column_names.push('ElectricityProduced:PV:ToBattery')
|
281
|
+
end
|
282
|
+
|
283
|
+
$pv_to_load = reopt_output['outputs']['Scenario']['Site']['PV']['year_one_to_load_series_kw'] || [0] * 8760
|
284
|
+
$pv_to_load_col = feature_report.timeseries_csv.column_names.index('ElectricityProduced:PV:ToLoad')
|
285
|
+
if $pv_to_load_col.nil?
|
286
|
+
$pv_to_load_col = feature_report.timeseries_csv.column_names.length
|
287
|
+
feature_report.timeseries_csv.column_names.push('ElectricityProduced:PV:ToLoad')
|
288
|
+
end
|
289
|
+
|
290
|
+
$pv_to_grid = reopt_output['outputs']['Scenario']['Site']['PV']['year_one_to_grid_series_kw'] || [0] * 8760
|
291
|
+
$pv_to_grid_col = feature_report.timeseries_csv.column_names.index('ElectricityProduced:PV:ToGrid')
|
292
|
+
if $pv_to_grid_col.nil?
|
293
|
+
$pv_to_grid_col = feature_report.timeseries_csv.column_names.length
|
294
|
+
feature_report.timeseries_csv.column_names.push('ElectricityProduced:PV:ToGrid')
|
295
|
+
end
|
296
|
+
|
297
|
+
$wind_total = reopt_output['outputs']['Scenario']['Site']['Wind']['year_one_power_production_series_kw'] || [0] * 8760
|
298
|
+
$wind_total_col = feature_report.timeseries_csv.column_names.index('ElectricityProduced:Wind:Total')
|
299
|
+
if $wind_total_col.nil?
|
300
|
+
$wind_total_col = feature_report.timeseries_csv.column_names.length
|
301
|
+
feature_report.timeseries_csv.column_names.push('ElectricityProduced:Wind:Total')
|
302
|
+
end
|
303
|
+
|
304
|
+
$wind_to_battery = reopt_output['outputs']['Scenario']['Site']['Wind']['year_one_to_battery_series_kw'] || [0] * 8760
|
305
|
+
$wind_to_battery_col = feature_report.timeseries_csv.column_names.index('ElectricityProduced:Wind:ToBattery')
|
306
|
+
if $wind_to_battery_col.nil?
|
307
|
+
$wind_to_battery_col = feature_report.timeseries_csv.column_names.length
|
308
|
+
feature_report.timeseries_csv.column_names.push('ElectricityProduced:Wind:ToBattery')
|
309
|
+
end
|
310
|
+
|
311
|
+
$wind_to_load = reopt_output['outputs']['Scenario']['Site']['Wind']['year_one_to_load_series_kw'] || [0] * 8760
|
312
|
+
$wind_to_load_col = feature_report.timeseries_csv.column_names.index('ElectricityProduced:Wind:ToLoad')
|
313
|
+
if $wind_to_load_col.nil?
|
314
|
+
$wind_to_load_col = feature_report.timeseries_csv.column_names.length
|
315
|
+
feature_report.timeseries_csv.column_names.push('ElectricityProduced:Wind:ToLoad')
|
316
|
+
end
|
317
|
+
|
318
|
+
$wind_to_grid = reopt_output['outputs']['Scenario']['Site']['Wind']['year_one_to_grid_series_kw'] || [0] * 8760
|
319
|
+
$wind_to_grid_col = feature_report.timeseries_csv.column_names.index('ElectricityProduced:Wind:ToGrid')
|
320
|
+
if $wind_to_grid_col.nil?
|
321
|
+
$wind_to_grid_col = feature_report.timeseries_csv.column_names.length
|
322
|
+
feature_report.timeseries_csv.column_names.push('ElectricityProduced:Wind:ToGrid')
|
323
|
+
end
|
324
|
+
|
325
|
+
def modrow(x, i) # :nodoc:
|
326
|
+
x[$generation_timeseries_kwh_col] = $generation_timeseries_kwh[i] || 0
|
327
|
+
x[$load_col] = $load[i] || 0
|
328
|
+
x[$utility_to_load_col] = $utility_to_load[i] || 0
|
329
|
+
x[$utility_to_battery_col] = $utility_to_battery[i] || 0
|
330
|
+
x[$storage_to_load_col] = $storage_to_load[i] || 0
|
331
|
+
x[$storage_to_grid_col] = $storage_to_grid[i] || 0
|
332
|
+
x[$storage_soc_col] = $storage_soc[i] || 0
|
333
|
+
x[$generator_total_col] = $generator_total[i] || 0
|
334
|
+
x[$generator_to_battery_col] = $generator_to_battery[i] || 0
|
335
|
+
x[$generator_to_load_col] = $generator_to_load[i] || 0
|
336
|
+
x[$generator_to_grid_col] = $generator_to_grid[i] || 0
|
337
|
+
x[$pv_total_col] = $pv_total[i] || 0
|
338
|
+
x[$pv_to_battery_col] = $pv_to_battery[i] || 0
|
339
|
+
x[$pv_to_load_col] = $pv_to_load[i] || 0
|
340
|
+
x[$pv_to_grid_col] = $pv_to_grid[i] || 0
|
341
|
+
x[$wind_total_col] = $wind_total[i] || 0
|
342
|
+
x[$wind_to_battery_col] = $wind_to_battery[i] || 0
|
343
|
+
x[$wind_to_load_col] = $wind_to_load[i] || 0
|
344
|
+
x[$wind_to_grid_col] = $wind_to_grid[i] || 0
|
345
|
+
return x
|
346
|
+
end
|
347
|
+
|
348
|
+
old_data = CSV.open(feature_report.timeseries_csv.path).read
|
349
|
+
mod_data = old_data.map.with_index do |x, i|
|
350
|
+
if i > 0
|
351
|
+
modrow(x, i)
|
352
|
+
else
|
353
|
+
x
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
mod_data[0] = feature_report.timeseries_csv.column_names
|
358
|
+
|
359
|
+
feature_report.timeseries_csv.reload_data(mod_data)
|
360
|
+
return feature_report
|
361
|
+
end
|
362
|
+
end
|
363
|
+
end
|
364
|
+
end
|
@@ -0,0 +1,230 @@
|
|
1
|
+
# *********************************************************************************
|
2
|
+
# URBANopt, Copyright (c) 2019-2020, Alliance for Sustainable Energy, LLC, and other
|
3
|
+
# contributors. All rights reserved.
|
4
|
+
#
|
5
|
+
# Redistribution and use in source and binary forms, with or without modification,
|
6
|
+
# are permitted provided that the following conditions are met:
|
7
|
+
#
|
8
|
+
# Redistributions of source code must retain the above copyright notice, this list
|
9
|
+
# of conditions and the following disclaimer.
|
10
|
+
#
|
11
|
+
# Redistributions in binary form must reproduce the above copyright notice, this
|
12
|
+
# list of conditions and the following disclaimer in the documentation and/or other
|
13
|
+
# materials provided with the distribution.
|
14
|
+
#
|
15
|
+
# Neither the name of the copyright holder nor the names of its contributors may be
|
16
|
+
# used to endorse or promote products derived from this software without specific
|
17
|
+
# prior written permission.
|
18
|
+
#
|
19
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
20
|
+
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
21
|
+
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
22
|
+
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
|
23
|
+
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
24
|
+
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
25
|
+
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
26
|
+
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
27
|
+
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
|
28
|
+
# OF THE POSSIBILITY OF SUCH DAMAGE.
|
29
|
+
# *********************************************************************************
|
30
|
+
|
31
|
+
require 'net/https'
|
32
|
+
require 'openssl'
|
33
|
+
require 'uri'
|
34
|
+
require 'uri'
|
35
|
+
require 'json'
|
36
|
+
require 'securerandom'
|
37
|
+
require 'certified'
|
38
|
+
require_relative '../../../developer_nrel_key'
|
39
|
+
require 'urbanopt/reopt/reopt_logger'
|
40
|
+
|
41
|
+
module URBANopt # :nodoc:
|
42
|
+
module REopt # :nodoc:
|
43
|
+
class REoptLiteAPI
|
44
|
+
##
|
45
|
+
# \REoptLiteAPI manages submitting optimization tasks to the \REopt Lite API and recieving results.
|
46
|
+
# Results can either be sourced from the production \REopt Lite API with an API key from developer.nrel.gov, or from
|
47
|
+
# a version running at localhost.
|
48
|
+
##
|
49
|
+
#
|
50
|
+
# [*parameters:*]
|
51
|
+
#
|
52
|
+
# * +use_localhost+ - _Bool_ - If this is true, requests will be sent to a version of the \REopt Lite API running on localhost. Default is false, such that the production version of \REopt Lite is accessed.
|
53
|
+
# * +nrel_developer_key+ - _String_ - API key used to access the \REopt Lite APi. Required only if localhost is false. Obtain from https://developer.nrel.gov/signup/
|
54
|
+
##
|
55
|
+
def initialize(nrel_developer_key = nil, use_localhost = false)
|
56
|
+
@use_localhost = use_localhost
|
57
|
+
if @use_localhost
|
58
|
+
@uri_submit = URI.parse('http//:127.0.0.1:8000/v1/job/')
|
59
|
+
else
|
60
|
+
if [nil, '', '<insert your key here>'].include? nrel_developer_key
|
61
|
+
if [nil, '', '<insert your key here>'].include? DEVELOPER_NREL_KEY
|
62
|
+
raise 'A developer.nrel.gov API key is required. Please see https://developer.nrel.gov/signup/ then update the file urbanopt-reopt-gem/developer_nrel_key.rb'
|
63
|
+
else
|
64
|
+
nrel_developer_key = DEVELOPER_NREL_KEY
|
65
|
+
end
|
66
|
+
end
|
67
|
+
@nrel_developer_key = nrel_developer_key
|
68
|
+
@uri_submit = URI.parse("https://developer.nrel.gov/api/reopt/v1/job/?api_key=#{@nrel_developer_key}")
|
69
|
+
# initialize @@logger
|
70
|
+
@@logger ||= URBANopt::REopt.reopt_logger
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
##
|
75
|
+
# URL of the results end point for a specific optimization task
|
76
|
+
##
|
77
|
+
#
|
78
|
+
# [*parameters:*]
|
79
|
+
#
|
80
|
+
# * +run_uuid+ - _String_ - Unique run_uuid obtained from the \REopt Lite job submittal URL for a specific optimization task.
|
81
|
+
#
|
82
|
+
# [*return:*] _URI_ - Returns URI object for use in calling the \REopt Lite results endpoint for a specifc optimization task.
|
83
|
+
##
|
84
|
+
def uri_results(run_uuid) # :nodoc:
|
85
|
+
if @use_localhost
|
86
|
+
return URI.parse("http://127.0.0.1:8000/v1/job/#{run_uuid}/results")
|
87
|
+
end
|
88
|
+
return URI.parse("https://developer.nrel.gov/api/reopt/v1/job/#{run_uuid}/results?api_key=#{@nrel_developer_key}")
|
89
|
+
end
|
90
|
+
|
91
|
+
def make_request(http, r, max_tries = 3)
|
92
|
+
result = nil
|
93
|
+
tries = 0
|
94
|
+
while tries < max_tries
|
95
|
+
begin
|
96
|
+
result = http.request(r)
|
97
|
+
tries = 4
|
98
|
+
rescue StandardError
|
99
|
+
tries += 1
|
100
|
+
end
|
101
|
+
end
|
102
|
+
return result
|
103
|
+
end
|
104
|
+
|
105
|
+
##
|
106
|
+
# Checks if a optimization task can be submitted to the \REopt Lite API
|
107
|
+
##
|
108
|
+
#
|
109
|
+
# [*parameters:*]
|
110
|
+
#
|
111
|
+
# * +data+ - _Hash_ - Default \REopt Lite formatted post containing at least all the required parameters.
|
112
|
+
#
|
113
|
+
# [*return:*] _Bool_ - Returns true if the post succeeeds. Otherwise returns false.
|
114
|
+
##
|
115
|
+
def check_connection(data)
|
116
|
+
header = { 'Content-Type' => 'application/json' }
|
117
|
+
http = Net::HTTP.new(@uri_submit.host, @uri_submit.port)
|
118
|
+
if !@use_localhost
|
119
|
+
http.use_ssl = true
|
120
|
+
end
|
121
|
+
|
122
|
+
request = Net::HTTP::Post.new(@uri_submit, header)
|
123
|
+
request.body = data.to_json
|
124
|
+
|
125
|
+
# Send the request
|
126
|
+
response = make_request(http, request)
|
127
|
+
|
128
|
+
if !response.is_a?(Net::HTTPSuccess)
|
129
|
+
@@logger.error('Check_connection Failed')
|
130
|
+
raise 'Check_connection Failed'
|
131
|
+
end
|
132
|
+
return true
|
133
|
+
end
|
134
|
+
|
135
|
+
##
|
136
|
+
# Completes a \REopt Lite optimization. From a formatted hash, an optimization task is submitted to the API.
|
137
|
+
# Results are polled at 5 second interval until they are ready or an error is returned from the API. Results
|
138
|
+
# are written to disk.
|
139
|
+
##
|
140
|
+
#
|
141
|
+
# [*parameters:*]
|
142
|
+
#
|
143
|
+
# * +reopt_input+ - _Hash_ - \REopt Lite formatted post containing at least required parameters.
|
144
|
+
# * +filename+ - _String_ - Path to file that will be created containing the full \REopt Lite response.
|
145
|
+
#
|
146
|
+
# [*return:*] _Bool_ - Returns true if the post succeeeds. Otherwise returns false.
|
147
|
+
##
|
148
|
+
def reopt_request(reopt_input, filename)
|
149
|
+
description = reopt_input[:Scenario][:description]
|
150
|
+
|
151
|
+
@@logger.info("Submitting #{description} to REopt Lite API")
|
152
|
+
|
153
|
+
# Format the request
|
154
|
+
header = { 'Content-Type' => 'application/json' }
|
155
|
+
http = Net::HTTP.new(@uri_submit.host, @uri_submit.port)
|
156
|
+
if !@use_localhost
|
157
|
+
http.use_ssl = true
|
158
|
+
end
|
159
|
+
request = Net::HTTP::Post.new(@uri_submit, header)
|
160
|
+
request.body = reopt_input.to_json
|
161
|
+
|
162
|
+
# Send the request
|
163
|
+
response = make_request(http, request)
|
164
|
+
|
165
|
+
# Get UUID
|
166
|
+
run_uuid = JSON.parse(response.body)['run_uuid']
|
167
|
+
|
168
|
+
if File.directory? filename
|
169
|
+
if run_uuid.nil?
|
170
|
+
run_uuid = 'error'
|
171
|
+
end
|
172
|
+
if run_uuid.downcase.include? 'error'
|
173
|
+
run_uuid = "error#{SecureRandom.uuid}"
|
174
|
+
end
|
175
|
+
filename = File.join(filename, "#{description}_#{run_uuid}.json")
|
176
|
+
@@logger.info("REopt results saved to #{filename}")
|
177
|
+
end
|
178
|
+
|
179
|
+
if response.code != '201'
|
180
|
+
File.open(filename, 'w') do |f|
|
181
|
+
f.write(response.body)
|
182
|
+
end
|
183
|
+
raise "Error in REopt optimization post - see #{filename}"
|
184
|
+
end
|
185
|
+
|
186
|
+
# Poll results until ready or error occurs
|
187
|
+
status = 'Optimizing...'
|
188
|
+
uri = uri_results(run_uuid)
|
189
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
190
|
+
if !@use_localhost
|
191
|
+
http.use_ssl = true
|
192
|
+
end
|
193
|
+
|
194
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
195
|
+
|
196
|
+
while status == 'Optimizing...'
|
197
|
+
response = make_request(http, request)
|
198
|
+
data = JSON.parse(response.body)
|
199
|
+
sizes = (data['outputs']['Scenario']['Site']['PV']['size_kw'] || 0) + (data['outputs']['Scenario']['Site']['Storage']['size_kw'] || 0) + (data['outputs']['Scenario']['Site']['Wind']['size_kw'] || 0) + (data['outputs']['Scenario']['Site']['Generator']['size_kw'] || 0)
|
200
|
+
status = data['outputs']['Scenario']['status']
|
201
|
+
|
202
|
+
sleep 5
|
203
|
+
end
|
204
|
+
|
205
|
+
_max_retry = 5
|
206
|
+
_tries = 0
|
207
|
+
(check_complete = sizes == 0) && ((data['outputs']['Scenario']['Site']['Financial']['npv_us_dollars'] || 0) > 0)
|
208
|
+
while (_tries < _max_retry) && check_complete
|
209
|
+
sleep 1
|
210
|
+
response = make_request(http, request)
|
211
|
+
data = JSON.parse(response.body)
|
212
|
+
sizes = (data['outputs']['Scenario']['Site']['PV']['size_kw'] || 0) + (data['outputs']['Scenario']['Site']['Storage']['size_kw'] || 0) + (data['outputs']['Scenario']['Site']['Wind']['size_kw'] || 0) + (data['outputs']['Scenario']['Site']['Generator']['size_kw'] || 0)
|
213
|
+
(check_complete = sizes == 0) && ((data['outputs']['Scenario']['Site']['Financial']['npv_us_dollars'] || 0) > 0)
|
214
|
+
_tries += 1
|
215
|
+
end
|
216
|
+
|
217
|
+
File.open(filename, 'w') do |f|
|
218
|
+
f.write(data.to_json)
|
219
|
+
end
|
220
|
+
|
221
|
+
if status == 'optimal'
|
222
|
+
return data
|
223
|
+
end
|
224
|
+
|
225
|
+
error_message = data['messages']['error']
|
226
|
+
raise "Error from REopt API - #{error_message}"
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# *********************************************************************************
|
2
|
+
# URBANopt, Copyright (c) 2019-2020, Alliance for Sustainable Energy, LLC, and other
|
3
|
+
# contributors. All rights reserved.
|
4
|
+
#
|
5
|
+
# Redistribution and use in source and binary forms, with or without modification,
|
6
|
+
# are permitted provided that the following conditions are met:
|
7
|
+
#
|
8
|
+
# Redistributions of source code must retain the above copyright notice, this list
|
9
|
+
# of conditions and the following disclaimer.
|
10
|
+
#
|
11
|
+
# Redistributions in binary form must reproduce the above copyright notice, this
|
12
|
+
# list of conditions and the following disclaimer in the documentation and/or other
|
13
|
+
# materials provided with the distribution.
|
14
|
+
#
|
15
|
+
# Neither the name of the copyright holder nor the names of its contributors may be
|
16
|
+
# used to endorse or promote products derived from this software without specific
|
17
|
+
# prior written permission.
|
18
|
+
#
|
19
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
20
|
+
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
21
|
+
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
22
|
+
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
|
23
|
+
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
24
|
+
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
25
|
+
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
26
|
+
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
27
|
+
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
|
28
|
+
# OF THE POSSIBILITY OF SUCH DAMAGE.
|
29
|
+
# *********************************************************************************
|
30
|
+
|
31
|
+
require 'logger'
|
32
|
+
|
33
|
+
module URBANopt
|
34
|
+
module REopt
|
35
|
+
@@reopt_logger = Logger.new(STDOUT)
|
36
|
+
##
|
37
|
+
# Definining class variable "@@logger" to log errors, info and warning messages.
|
38
|
+
def self.reopt_logger
|
39
|
+
@@reopt_logger
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|