urbanopt-reopt 0.1.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 +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
|