urbanopt-reopt 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +27 -0
  3. data/.rakeTasks +7 -0
  4. data/.rdoc_options +37 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +9 -0
  7. data/.travis.yml +22 -0
  8. data/CHANGELOG.md +5 -0
  9. data/Gemfile +67 -0
  10. data/Jenkinsfile +10 -0
  11. data/LICENSE.md +27 -0
  12. data/RDOC_MAIN.md +176 -0
  13. data/README.md +177 -0
  14. data/Rakefile +30 -0
  15. data/deploy_docs.sh +5 -0
  16. data/developer_nrel_key.rb +31 -0
  17. data/doc_templates/LICENSE.md +27 -0
  18. data/doc_templates/README.md.erb +42 -0
  19. data/doc_templates/copyright_erb.txt +31 -0
  20. data/doc_templates/copyright_js.txt +4 -0
  21. data/doc_templates/copyright_ruby.txt +29 -0
  22. data/docs/.gitignore +3 -0
  23. data/docs/.vuepress/components/InnerJsonSchema.vue +78 -0
  24. data/docs/.vuepress/components/JsonSchema.vue +12 -0
  25. data/docs/.vuepress/components/ReoptInputSchema.vue +12 -0
  26. data/docs/.vuepress/components/ReoptOutputSchema.vue +12 -0
  27. data/docs/.vuepress/components/StaticLink.vue +8 -0
  28. data/docs/.vuepress/config.js +16 -0
  29. data/docs/.vuepress/highlight.js +8 -0
  30. data/docs/.vuepress/public/custom_rdoc_styles.css +58 -0
  31. data/docs/.vuepress/utils.js +17 -0
  32. data/docs/README.md +196 -0
  33. data/docs/package-lock.json +254 -0
  34. data/docs/package.json +22 -0
  35. data/docs/schemas/reopt-input-schema.md +57 -0
  36. data/docs/schemas/reopt-output-schema.md +66 -0
  37. data/index.html +1 -0
  38. data/index.md +176 -0
  39. data/lib/files/.gitkeep +0 -0
  40. data/lib/urbanopt/reopt/extension.rb +44 -0
  41. data/lib/urbanopt/reopt/feature_report_adapter.rb +364 -0
  42. data/lib/urbanopt/reopt/reopt_lite_api.rb +230 -0
  43. data/lib/urbanopt/reopt/reopt_logger.rb +42 -0
  44. data/lib/urbanopt/reopt/reopt_post_processor.rb +245 -0
  45. data/lib/urbanopt/reopt/reopt_schema/reopt_input_schema.json +1111 -0
  46. data/lib/urbanopt/reopt/reopt_schema/reopt_output_schema.json +538 -0
  47. data/lib/urbanopt/reopt/scenario/reopt_scenario_csv.rb +115 -0
  48. data/lib/urbanopt/reopt/scenario_report_adapter.rb +404 -0
  49. data/lib/urbanopt/reopt/version.rb +35 -0
  50. data/lib/urbanopt/reopt.rb +36 -0
  51. data/lib/urbanopt/reopt_scenario.rb +31 -0
  52. data/lib/urbanopt-reopt.rb +31 -0
  53. data/urbanopt-reopt.gemspec +33 -0
  54. 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