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.
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