urbanopt-reopt 0.11.0 → 1.0.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 +4 -4
- data/.github/workflows/nightly_ci_build.yml +5 -7
- data/.gitignore +1 -1
- data/CHANGELOG.md +24 -0
- data/Gemfile +16 -11
- data/LICENSE.md +1 -1
- data/RDOC_MAIN.md +6 -6
- data/README.md +6 -6
- data/doc_templates/LICENSE.md +1 -1
- data/doc_templates/copyright_erb.txt +1 -1
- data/docs/README.md +6 -6
- data/docs/schemas/reopt-input-schema.md +25 -23
- data/docs/schemas/reopt-output-schema.md +7 -7
- data/index.md +9 -9
- data/lib/urbanopt/reopt/feature_report_adapter.rb +230 -198
- data/lib/urbanopt/reopt/reopt_ghp_adapter.rb +337 -0
- data/lib/urbanopt/reopt/reopt_ghp_api.rb +156 -0
- data/lib/urbanopt/reopt/reopt_ghp_files/reopt_ghp_assumption.json +27 -0
- data/lib/urbanopt/reopt/reopt_ghp_post_processor.rb +149 -0
- data/lib/urbanopt/reopt/reopt_lite_api.rb +84 -46
- data/lib/urbanopt/reopt/reopt_logger.rb +1 -1
- data/lib/urbanopt/reopt/reopt_post_processor.rb +12 -12
- data/lib/urbanopt/reopt/reopt_schema/REopt-GHP-input.json +148 -0
- data/lib/urbanopt/reopt/reopt_schema/reopt_input_schema.json +1125 -1105
- data/lib/urbanopt/reopt/reopt_schema/reopt_output_schema.json +488 -523
- data/lib/urbanopt/reopt/scenario_report_adapter.rb +260 -229
- data/lib/urbanopt/reopt/utilities.rb +2 -2
- data/lib/urbanopt/reopt/version.rb +1 -1
- data/lib/urbanopt/reopt.rb +3 -0
- data/urbanopt-reopt.gemspec +9 -10
- metadata +27 -64
@@ -0,0 +1,337 @@
|
|
1
|
+
# *********************************************************************************
|
2
|
+
# URBANopt (tm), Copyright (c) Alliance for Sustainable Energy, LLC.
|
3
|
+
# See also https://github.com/urbanopt/urbanopt-reopt-gem/blob/develop/LICENSE.md
|
4
|
+
# *********************************************************************************
|
5
|
+
|
6
|
+
module URBANopt # :nodoc:
|
7
|
+
module REopt # :nodoc:
|
8
|
+
class REoptGHPAdapter
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
# initialize @@logger
|
12
|
+
@@logger ||= URBANopt::REopt.reopt_logger
|
13
|
+
# Define class variable
|
14
|
+
@@hours_in_year = 8760
|
15
|
+
end
|
16
|
+
|
17
|
+
def create_reopt_input_building(run_dir, system_parameter_hash, reopt_ghp_assumptions_hash, building_id, modelica_result)
|
18
|
+
|
19
|
+
# Define variables
|
20
|
+
reopt_inputs_building = {}
|
21
|
+
if !reopt_ghp_assumptions_hash.nil?
|
22
|
+
reopt_inputs_building = reopt_ghp_assumptions_hash
|
23
|
+
else
|
24
|
+
@@logger.info('Using default REopt assumptions')
|
25
|
+
# create a dictionary for REopt Inputs
|
26
|
+
reopt_inputs_building = {
|
27
|
+
Site: {},
|
28
|
+
SpaceHeatingLoad: {},
|
29
|
+
DomesticHotWaterLoad: {},
|
30
|
+
ElectricLoad: {},
|
31
|
+
ElectricTariff: {
|
32
|
+
urdb_label: ""
|
33
|
+
},
|
34
|
+
GHP: {},
|
35
|
+
ExistingBoiler: {}
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
# The URDB label is required to be specified in the input assumption file
|
40
|
+
if reopt_inputs_building[:ElectricTariff][:urdb_label].nil? || reopt_inputs_building[:ElectricTariff][:urdb_label].empty?
|
41
|
+
raise "Missing value for urdb_label - this is a required input"
|
42
|
+
end
|
43
|
+
|
44
|
+
scenario_json_path = File.join(run_dir, "default_scenario_report.json")
|
45
|
+
if File.exist?(scenario_json_path)
|
46
|
+
File.open(scenario_json_path, 'r') do |file|
|
47
|
+
scenario_json_data = JSON.parse(file.read, symbolize_names: true)
|
48
|
+
# update site location
|
49
|
+
@latitude = scenario_json_data[:scenario_report][:location][:latitude_deg]
|
50
|
+
@longitude = scenario_json_data[:scenario_report][:location][:longitude_deg]
|
51
|
+
reopt_inputs_building[:Site][:latitude] = @latitude
|
52
|
+
reopt_inputs_building[:Site][:longitude] = @longitude
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
reopt_inputs_building[:SpaceHeatingLoad][:fuel_loads_mmbtu_per_hour] = []
|
58
|
+
# Read the default csv report
|
59
|
+
default_feature_report_path = File.join(run_dir, building_id.to_s, "feature_reports", "default_feature_report.csv")
|
60
|
+
if File.exist?(default_feature_report_path)
|
61
|
+
timeseries_data = CSV.read(default_feature_report_path, headers: true)
|
62
|
+
|
63
|
+
# Initialize the total kBtu sum
|
64
|
+
total_kbtu = 0.0
|
65
|
+
|
66
|
+
# Convert each value in "Heating:NaturalGas(kBtu)" to MMBtu and store in the array
|
67
|
+
timeseries_data.each do |row|
|
68
|
+
if row['Heating:NaturalGas(kBtu)'] # Ensure the value exists
|
69
|
+
kBtu_value = row['Heating:NaturalGas(kBtu)'].to_f # Convert to float
|
70
|
+
total_kbtu += kBtu_value # Sum kBtu values
|
71
|
+
end
|
72
|
+
end
|
73
|
+
# Check if the total kBtu is zero
|
74
|
+
if total_kbtu.zero?
|
75
|
+
# If zero, populate with hourly values meet reopts formatting requirements
|
76
|
+
reopt_inputs_building[:SpaceHeatingLoad][:fuel_loads_mmbtu_per_hour] = [0.000001] * @@hours_in_year
|
77
|
+
else
|
78
|
+
# If not zero, convert and append to the array
|
79
|
+
timeseries_data.each do |row|
|
80
|
+
if row['Heating:NaturalGas(kBtu)'] # Ensure the value exists
|
81
|
+
kBtu_value = row['Heating:NaturalGas(kBtu)'].to_f # Convert to float
|
82
|
+
mMBtu_value = kBtu_value / 1000 # Convert kBtu to MMBtu
|
83
|
+
reopt_inputs_building[:SpaceHeatingLoad][:fuel_loads_mmbtu_per_hour] << mMBtu_value # Append to the array
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
else
|
89
|
+
# Calculate space heating load
|
90
|
+
reopt_inputs_building[:SpaceHeatingLoad][:fuel_loads_mmbtu_per_hour] = [0.000001] * @@hours_in_year
|
91
|
+
puts "Existing heating fuel cost was not taken into consideration in result calculations."
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
# read_modelica_result
|
96
|
+
modelica_project = File.expand_path(modelica_result)
|
97
|
+
project_name = File.basename(modelica_project)
|
98
|
+
@modelica_csv = File.join(
|
99
|
+
modelica_project,
|
100
|
+
"#{project_name}.Districts.DistrictEnergySystem_results",
|
101
|
+
"#{project_name}.Districts.DistrictEnergySystem_result.csv"
|
102
|
+
)
|
103
|
+
|
104
|
+
if File.exist?(@modelica_csv)
|
105
|
+
modelica_data = CSV.read(@modelica_csv, headers: true)
|
106
|
+
heating_power = "heating_electric_power_#{building_id}"
|
107
|
+
cooling_power = "cooling_electric_power_#{building_id}"
|
108
|
+
pump_power = "pump_power_#{building_id}"
|
109
|
+
ets_pump_power = "ets_pump_power_#{building_id}"
|
110
|
+
heating_system_capacity = "heating_system_capacity_#{building_id}"
|
111
|
+
cooling_system_capacity = "cooling_system_capacity_#{building_id}"
|
112
|
+
|
113
|
+
heating_power_values = cooling_power_values = pump_power_values = ets_pump_power_values = []
|
114
|
+
total_electric_load_building = []
|
115
|
+
# Ensure the column exists
|
116
|
+
if modelica_data.headers.include?(heating_power)
|
117
|
+
heating_power_values = modelica_data[heating_power]
|
118
|
+
end
|
119
|
+
if modelica_data.headers.include?(cooling_power)
|
120
|
+
cooling_power_values = modelica_data[cooling_power]
|
121
|
+
end
|
122
|
+
if modelica_data.headers.include?(pump_power)
|
123
|
+
pump_power_values = modelica_data[pump_power]
|
124
|
+
end
|
125
|
+
if modelica_data.headers.include?(ets_pump_power)
|
126
|
+
ets_pump_power_values = modelica_data[ets_pump_power]
|
127
|
+
end
|
128
|
+
|
129
|
+
total_electric_load_building = heating_power_values.zip(cooling_power_values, pump_power_values, ets_pump_power_values).map do |elements|
|
130
|
+
# Convert watts to kilowatts
|
131
|
+
elements.map { |e| e.to_f / 1000 }.sum
|
132
|
+
end
|
133
|
+
|
134
|
+
peak_combined_heatpump_thermal_ton = 0
|
135
|
+
|
136
|
+
if modelica_data.headers.include?(heating_system_capacity)
|
137
|
+
heating_system_capacity_value = modelica_data[heating_system_capacity][0]
|
138
|
+
end
|
139
|
+
if modelica_data.headers.include?(cooling_system_capacity)
|
140
|
+
cooling_system_capacity_value = modelica_data[cooling_system_capacity][0]
|
141
|
+
end
|
142
|
+
|
143
|
+
watts_per_ton_cooling_capacity = 3517
|
144
|
+
peak_combined_heatpump_thermal_ton = ([heating_system_capacity_value.to_f.abs, cooling_system_capacity_value.to_f.abs].max) / watts_per_ton_cooling_capacity
|
145
|
+
|
146
|
+
# Store the result in reopt_inputs_building ElectricLoad
|
147
|
+
reopt_inputs_building[:ElectricLoad][:loads_kw] = total_electric_load_building
|
148
|
+
|
149
|
+
|
150
|
+
domestic_hot_water = total_electric_load_building.map do |load|
|
151
|
+
load * 0
|
152
|
+
end
|
153
|
+
|
154
|
+
# This is not used in REopt calculation but required for formatting.
|
155
|
+
reopt_inputs_building[:DomesticHotWaterLoad][:fuel_loads_mmbtu_per_hour] = domestic_hot_water
|
156
|
+
|
157
|
+
# Add GHP Fields
|
158
|
+
reopt_inputs_building[:GHP] = {}
|
159
|
+
# REopt default
|
160
|
+
reopt_inputs_building[:GHP][:require_ghp_purchase] = 1
|
161
|
+
reopt_inputs_building[:GHP][:om_cost_per_sqft_year] = 0
|
162
|
+
reopt_inputs_building[:GHP][:heatpump_capacity_sizing_factor_on_peak_load] = 1.0
|
163
|
+
# Add the floor area
|
164
|
+
building_json_path = File.join(run_dir, building_id.to_s, "feature_reports", "default_feature_report.json")
|
165
|
+
if File.exist?(building_json_path)
|
166
|
+
File.open(building_json_path, 'r') do |file|
|
167
|
+
building_json_data = JSON.parse(file.read, symbolize_names: true)
|
168
|
+
reopt_inputs_building[:GHP][:building_sqft] = building_json_data[:program][:floor_area_sqft]
|
169
|
+
end
|
170
|
+
else
|
171
|
+
puts "File not found: #{building_json_path}"
|
172
|
+
end
|
173
|
+
|
174
|
+
# Add existing boiler fuel cost
|
175
|
+
# TODO : Add this as optional user input
|
176
|
+
nat_gas_dollars_per_mmbtu = 13.5
|
177
|
+
reopt_inputs_building[:ExistingBoiler][:fuel_cost_per_mmbtu] = nat_gas_dollars_per_mmbtu
|
178
|
+
|
179
|
+
# Add ghpghx_responses
|
180
|
+
ghpghx_output = {}
|
181
|
+
ghpghx_output[:outputs] = {}
|
182
|
+
ghpghx_output[:inputs] = {}
|
183
|
+
ghpghx_output[:outputs][:heat_pump_configuration] = "WSHP"
|
184
|
+
# This is not used in REopt calculation but required for formatting.
|
185
|
+
ghpghx_output[:outputs][:yearly_ghx_pump_electric_consumption_series_kw] = [0] * @@hours_in_year
|
186
|
+
ghpghx_output[:outputs][:number_of_boreholes] = 0
|
187
|
+
ghpghx_output[:outputs][:length_boreholes_ft] = 0
|
188
|
+
|
189
|
+
ghpghx_output[:outputs][:peak_combined_heatpump_thermal_ton] = peak_combined_heatpump_thermal_ton
|
190
|
+
ghpghx_output[:outputs][:yearly_total_electric_consumption_kwh] = total_electric_load_building.sum
|
191
|
+
ghpghx_output[:outputs][:yearly_total_electric_consumption_series_kw] = total_electric_load_building
|
192
|
+
ghpghx_output[:outputs][:yearly_heating_heatpump_electric_consumption_series_kw] = total_electric_load_building
|
193
|
+
ghpghx_output[:outputs][:yearly_cooling_heatpump_electric_consumption_series_kw] = [0] * @@hours_in_year
|
194
|
+
# This is not used in REopt calculation but required for formatting.
|
195
|
+
ghpghx_output[:inputs][:heating_thermal_load_mmbtu_per_hr] = [0.0001] * @@hours_in_year
|
196
|
+
# This is not used in REopt calculation but required for formatting.
|
197
|
+
ghpghx_output[:inputs][:cooling_thermal_load_ton] = [0] * @@hours_in_year
|
198
|
+
|
199
|
+
ghpghx_output_all = [ghpghx_output]
|
200
|
+
reopt_inputs_building[:GHP][:ghpghx_responses] = {}
|
201
|
+
reopt_inputs_building[:GHP][:ghpghx_responses] = ghpghx_output_all
|
202
|
+
|
203
|
+
end
|
204
|
+
|
205
|
+
#save output report in reopt_ghp directory
|
206
|
+
reopt_ghp_dir = File.join(run_dir, "reopt_ghp", "reopt_ghp_inputs")
|
207
|
+
json_file_path = File.join(reopt_ghp_dir, "GHP_building_#{building_id}.json")
|
208
|
+
pretty_json = JSON.pretty_generate(reopt_inputs_building)
|
209
|
+
File.write(json_file_path, pretty_json)
|
210
|
+
|
211
|
+
end
|
212
|
+
|
213
|
+
def create_reopt_input_district(run_dir, system_parameter_hash, reopt_ghp_assumptions_hash, ghp_id, modelica_result)
|
214
|
+
|
215
|
+
reopt_inputs_district = {}
|
216
|
+
|
217
|
+
if !reopt_ghp_assumptions_hash.nil?
|
218
|
+
reopt_inputs_district = reopt_ghp_assumptions_hash
|
219
|
+
else
|
220
|
+
@@logger.info('Using default REopt assumptions')
|
221
|
+
# create a dictionary for REopt Inputs
|
222
|
+
reopt_inputs_district = {
|
223
|
+
Site: {},
|
224
|
+
SpaceHeatingLoad: {},
|
225
|
+
DomesticHotWaterLoad: {},
|
226
|
+
ElectricLoad: {},
|
227
|
+
ElectricTariff: {
|
228
|
+
"urdb_label": ""
|
229
|
+
},
|
230
|
+
GHP: {},
|
231
|
+
ExistingBoiler: {}
|
232
|
+
}
|
233
|
+
end
|
234
|
+
|
235
|
+
reopt_inputs_district[:Site] = {}
|
236
|
+
reopt_inputs_district[:Site][:latitude] = @latitude
|
237
|
+
reopt_inputs_district[:Site][:longitude] = @longitude
|
238
|
+
# The URDB label is required to be specified in the input assumption file
|
239
|
+
if reopt_inputs_district[:ElectricTariff][:urdb_label].nil? || reopt_inputs_district[:ElectricTariff][:urdb_label].empty?
|
240
|
+
|
241
|
+
raise "Missing value for urdb_label - this is a required input"
|
242
|
+
|
243
|
+
end
|
244
|
+
|
245
|
+
# This is not used in REopt calculation but required for formatting.
|
246
|
+
reopt_inputs_district[:SpaceHeatingLoad][:fuel_loads_mmbtu_per_hour] = [0.000001]*@@hours_in_year
|
247
|
+
# This is not used in REopt calculation but required for formatting.
|
248
|
+
reopt_inputs_district[:DomesticHotWaterLoad][:fuel_loads_mmbtu_per_hour] = [0.0000001]*@@hours_in_year
|
249
|
+
|
250
|
+
# Adding year for ElectricLoad so district electric load can be calculated with REopt API v3.11
|
251
|
+
reopt_inputs_district[:ElectricLoad] = {:year => 2017}
|
252
|
+
#required for reopt formatting
|
253
|
+
reopt_inputs_district[:ElectricLoad][:loads_kw] = [0.00001]*@@hours_in_year
|
254
|
+
|
255
|
+
reopt_inputs_district[:ExistingBoiler] = {}
|
256
|
+
reopt_inputs_district[:ExistingBoiler][:fuel_cost_per_mmbtu] = 13.5
|
257
|
+
|
258
|
+
# GHP inputs
|
259
|
+
reopt_inputs_district[:GHP] = {}
|
260
|
+
reopt_inputs_district[:GHP][:require_ghp_purchase] = 1
|
261
|
+
reopt_inputs_district[:GHP][:building_sqft] = 0.00001
|
262
|
+
reopt_inputs_district[:GHP][:om_cost_per_sqft_year] = 0
|
263
|
+
reopt_inputs_district[:GHP][:heatpump_capacity_sizing_factor_on_peak_load] = 1.0
|
264
|
+
|
265
|
+
# Add ghpghx outputs
|
266
|
+
ghpghx_output = {}
|
267
|
+
ghpghx_output[:outputs] = {}
|
268
|
+
ghpghx_output[:inputs] = {}
|
269
|
+
|
270
|
+
ghpghx_output[:inputs][:heating_thermal_load_mmbtu_per_hr] = [0]*@@hours_in_year
|
271
|
+
ghpghx_output[:inputs][:cooling_thermal_load_ton] = [0] * @@hours_in_year
|
272
|
+
|
273
|
+
|
274
|
+
# Read GHX sizes from system parameter hash
|
275
|
+
ghe_specific_params = system_parameter_hash[:district_system][:fifth_generation][:ghe_parameters][:ghe_specific_params]
|
276
|
+
ghe_specific_params.each do |ghe_specific_param|
|
277
|
+
if ghe_specific_param[:ghe_id] = ghp_id
|
278
|
+
number_of_boreholes = ghe_specific_param[:borehole][:number_of_boreholes]
|
279
|
+
length_of_boreholes = ghe_specific_param[:borehole][:length_of_boreholes]
|
280
|
+
ghpghx_output[:outputs][:number_of_boreholes] = number_of_boreholes
|
281
|
+
# convert meters to feet
|
282
|
+
ghpghx_output[:outputs][:length_boreholes_ft] = (length_of_boreholes)*3.28084
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
if File.exist?(@modelica_csv)
|
287
|
+
|
288
|
+
modelica_data = CSV.read(@modelica_csv, headers: true)
|
289
|
+
|
290
|
+
electrical_power_consumed = modelica_data["electrical_power_consumed"]
|
291
|
+
# Convert watts to kilowatts
|
292
|
+
electrical_power_consumed_kw = electrical_power_consumed.map { |e| e.to_f / 1000 }
|
293
|
+
# if ghp_id.include?('-')
|
294
|
+
# # Note: For some reason when reading columns, '-' from the column headers are removed, whereas ghp_id has -
|
295
|
+
# ghp_id_formatted = ghp_id.delete('-')
|
296
|
+
# ghp_column = "electrical_power_consumed_#{ghp_id_formatted}".to_sym
|
297
|
+
|
298
|
+
# else
|
299
|
+
# # Note: For some reason when reading columns, '-' from the column headers are removed, whereas ghp_id has -
|
300
|
+
# ghp_column = "electrical_power_consumed_#{ghp_id}".to_sym
|
301
|
+
# end
|
302
|
+
|
303
|
+
# # Ensure the column exists
|
304
|
+
# unless modelica_data.headers.include?(ghp_column)
|
305
|
+
# puts "Column #{ghp_column} does not exist in the CSV file."
|
306
|
+
# end
|
307
|
+
|
308
|
+
# # Access values from the column
|
309
|
+
# column_values = modelica_data.by_col[ghp_column]
|
310
|
+
|
311
|
+
ghpghx_output[:outputs][:yearly_ghx_pump_electric_consumption_series_kw] = electrical_power_consumed_kw
|
312
|
+
else
|
313
|
+
ghpghx_output[:outputs][:yearly_ghx_pump_electric_consumption_series_kw] = [0.000000001]*@@hours_in_year
|
314
|
+
end
|
315
|
+
|
316
|
+
# This is not used in REopt calculation but required for formatting.
|
317
|
+
ghpghx_output[:outputs][:peak_combined_heatpump_thermal_ton] = 0.000000001
|
318
|
+
|
319
|
+
ghpghx_output[:outputs][:heat_pump_configuration] = "WSHP"
|
320
|
+
# Required for REpot formatting
|
321
|
+
ghpghx_output[:outputs][:yearly_total_electric_consumption_series_kw] = [0.00001] * @@hours_in_year
|
322
|
+
ghpghx_output[:outputs][:yearly_heating_heatpump_electric_consumption_series_kw] = [0] * @@hours_in_year
|
323
|
+
ghpghx_output[:outputs][:yearly_cooling_heatpump_electric_consumption_series_kw] = [0] * @@hours_in_year
|
324
|
+
|
325
|
+
ghpghx_output_all = [ghpghx_output, ghpghx_output]
|
326
|
+
reopt_inputs_district[:GHP][:ghpghx_responses] = ghpghx_output_all
|
327
|
+
|
328
|
+
#save output report in reopt_ghp directory
|
329
|
+
reopt_ghp_dir = File.join(run_dir, "reopt_ghp", "reopt_ghp_inputs")
|
330
|
+
json_file_path = File.join(reopt_ghp_dir, "GHX_#{ghp_id}.json")
|
331
|
+
pretty_json = JSON.pretty_generate(reopt_inputs_district)
|
332
|
+
File.write(json_file_path, pretty_json)
|
333
|
+
end
|
334
|
+
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
# *********************************************************************************
|
2
|
+
# URBANopt (tm), Copyright (c) Alliance for Sustainable Energy, LLC.
|
3
|
+
# See also https://github.com/urbanopt/urbanopt-reopt-gem/blob/develop/LICENSE.md
|
4
|
+
# *********************************************************************************
|
5
|
+
|
6
|
+
require 'net/https'
|
7
|
+
require 'openssl'
|
8
|
+
require 'uri'
|
9
|
+
require 'json'
|
10
|
+
require 'securerandom'
|
11
|
+
require_relative '../../../developer_nrel_key'
|
12
|
+
require 'urbanopt/reopt/reopt_logger'
|
13
|
+
|
14
|
+
module URBANopt # :nodoc:
|
15
|
+
module REopt # :nodoc:
|
16
|
+
class REoptLiteGHPAPI
|
17
|
+
|
18
|
+
def initialize(reopt_input_file, nrel_developer_key = nil, reopt_output_file, use_localhost)
|
19
|
+
|
20
|
+
# Store developer key
|
21
|
+
if [nil, '', '<insert your key here>'].include? nrel_developer_key
|
22
|
+
if [nil, '', '<insert your key here>'].include? DEVELOPER_NREL_KEY
|
23
|
+
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'
|
24
|
+
else
|
25
|
+
#Store the NREL developer key
|
26
|
+
nrel_developer_key = DEVELOPER_NREL_KEY
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
@use_localhost = use_localhost
|
31
|
+
if @use_localhost
|
32
|
+
@root_url = "http://localhost:8000/v3"
|
33
|
+
else
|
34
|
+
@root_url = "https://developer.nrel.gov/api/reopt/v3"
|
35
|
+
end
|
36
|
+
# add REopt URL
|
37
|
+
@nrel_developer_key = nrel_developer_key
|
38
|
+
@reopt_input_file = reopt_input_file
|
39
|
+
@reopt_output_file = reopt_output_file
|
40
|
+
# initialize @@logger
|
41
|
+
@@logger ||= URBANopt::REopt.reopt_logger
|
42
|
+
@@logger.level = Logger::INFO
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
def get_api_results(run_id=nil)
|
47
|
+
|
48
|
+
reopt_input_file = @reopt_input_file
|
49
|
+
nrel_developer_key = @nrel_developer_key
|
50
|
+
root_url = @root_url
|
51
|
+
reopt_output_file = @reopt_output_file
|
52
|
+
|
53
|
+
if run_id.nil?
|
54
|
+
run_id = get_run_uuid(reopt_input_file, nrel_developer_key, reopt_output_file)
|
55
|
+
end
|
56
|
+
if !run_id.nil?
|
57
|
+
results_url = "#{@root_url}/job/#{run_id}/results/?api_key=#{nrel_developer_key}"
|
58
|
+
puts "This is results URL #{results_url}"
|
59
|
+
results = reopt_request(results_url)
|
60
|
+
|
61
|
+
File.open(reopt_output_file, 'w') do |f|
|
62
|
+
f.write(JSON.pretty_generate(results))
|
63
|
+
@@logger.info("Saved results to #{reopt_output_file}")
|
64
|
+
end
|
65
|
+
else
|
66
|
+
results = nil
|
67
|
+
@@logger.error("Unable to get results: no UUID returned.")
|
68
|
+
end
|
69
|
+
results
|
70
|
+
end
|
71
|
+
|
72
|
+
def get_run_uuid(reopt_input_file, nrel_developer_key, root_url)
|
73
|
+
|
74
|
+
reopt_input_file = @reopt_input_file
|
75
|
+
nrel_developer_key = @nrel_developer_key
|
76
|
+
root_url = @root_url
|
77
|
+
post_url = "#{root_url}/job/?api_key=#{nrel_developer_key}"
|
78
|
+
puts "This is URL: #{post_url}"
|
79
|
+
@@logger.info("Connecting to #{post_url}")
|
80
|
+
|
81
|
+
# Parse the URL and prepare the HTTP request
|
82
|
+
uri = URI.parse(post_url)
|
83
|
+
request = Net::HTTP::Post.new(uri)
|
84
|
+
request.content_type = 'application/json'
|
85
|
+
|
86
|
+
# Add the JSON payload (assuming 'post' is the body data)
|
87
|
+
request.body = reopt_input_file.to_json
|
88
|
+
|
89
|
+
# Send the HTTP request
|
90
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
91
|
+
http.request(request)
|
92
|
+
end
|
93
|
+
|
94
|
+
run_id = nil
|
95
|
+
|
96
|
+
if !response.is_a?(Net::HTTPSuccess)
|
97
|
+
@@logger.error("Status code #{response.code}. #{response.body}")
|
98
|
+
@@logger.error("Status code #{response.code}")
|
99
|
+
else
|
100
|
+
@@logger.info("Response OK from #{post_url}.")
|
101
|
+
run_id_dict = JSON.parse(response.body)
|
102
|
+
|
103
|
+
begin
|
104
|
+
run_id = run_id_dict['run_uuid']
|
105
|
+
rescue KeyError
|
106
|
+
msg = "Response from #{post_url} did not contain run_uuid."
|
107
|
+
@@logger.error(msg)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
# Return run_id
|
111
|
+
run_id
|
112
|
+
end
|
113
|
+
|
114
|
+
def reopt_request(results_url, poll_interval = 5, max_timeout = 300)
|
115
|
+
|
116
|
+
key_error_count = 0
|
117
|
+
key_error_threshold = 3
|
118
|
+
status = "Optimizing..."
|
119
|
+
@@logger.info("Polling #{results_url} for results with interval of #{poll_interval}...")
|
120
|
+
resp_dict = {}
|
121
|
+
start_time = Time.now
|
122
|
+
|
123
|
+
loop do
|
124
|
+
uri = URI.parse(results_url)
|
125
|
+
response = Net::HTTP.get_response(uri)
|
126
|
+
resp_dict = JSON.parse(response.body)
|
127
|
+
|
128
|
+
begin
|
129
|
+
status = resp_dict['status']
|
130
|
+
rescue KeyError
|
131
|
+
key_error_count += 1
|
132
|
+
@@logger.info("KeyError count: #{key_error_count}")
|
133
|
+
if key_error_count > key_error_threshold
|
134
|
+
@@logger.info("Breaking polling loop due to KeyError count threshold of #{key_error_threshold} exceeded.")
|
135
|
+
break
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
if status != "Optimizing..."
|
140
|
+
break
|
141
|
+
end
|
142
|
+
|
143
|
+
if Time.now - start_time > max_timeout
|
144
|
+
@@logger.info("Breaking polling loop due to max timeout of #{max_timeout} seconds exceeded.")
|
145
|
+
break
|
146
|
+
end
|
147
|
+
|
148
|
+
sleep(poll_interval)
|
149
|
+
|
150
|
+
end
|
151
|
+
resp_dict
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
{
|
2
|
+
"Site": {
|
3
|
+
"latitude": 42.81428490645775,
|
4
|
+
"longitude": -78.84701778930912
|
5
|
+
},
|
6
|
+
"SpaceHeatingLoad": {
|
7
|
+
},
|
8
|
+
"DomesticHotWaterLoad": {
|
9
|
+
},
|
10
|
+
"ElectricLoad": {
|
11
|
+
"year": 2017
|
12
|
+
},
|
13
|
+
"ElectricTariff": {
|
14
|
+
"urdb_label": "594976725457a37b1175d089"
|
15
|
+
},
|
16
|
+
"GHP":{
|
17
|
+
"installed_cost_heatpump_per_ton": 1075,
|
18
|
+
"installed_cost_ghx_per_ft": 14,
|
19
|
+
"installed_cost_building_hydronic_loop_per_sqft": 1.7,
|
20
|
+
"om_cost_per_sqft_year": 0,
|
21
|
+
"macrs_bonus_fraction": 0.6,
|
22
|
+
"macrs_itc_reduction": 0.5,
|
23
|
+
"federal_itc_fraction": 0.3
|
24
|
+
},
|
25
|
+
"ExistingBoiler": {
|
26
|
+
}
|
27
|
+
}
|
@@ -0,0 +1,149 @@
|
|
1
|
+
# *********************************************************************************
|
2
|
+
# URBANopt (tm), Copyright (c) Alliance for Sustainable Energy, LLC.
|
3
|
+
# See also https://github.com/urbanopt/urbanopt-reopt-gem/blob/develop/LICENSE.md
|
4
|
+
# *********************************************************************************
|
5
|
+
|
6
|
+
require 'bundler/setup'
|
7
|
+
require 'urbanopt/reopt/reopt_logger'
|
8
|
+
require 'urbanopt/reopt/reopt_ghp_api'
|
9
|
+
require 'csv'
|
10
|
+
require 'json'
|
11
|
+
require 'fileutils'
|
12
|
+
|
13
|
+
module URBANopt # :nodoc:
|
14
|
+
module REopt # :nodoc:
|
15
|
+
class REoptGHPPostProcessor
|
16
|
+
def initialize(run_dir, system_parameter, modelica_result, reopt_ghp_assumptions = nil, nrel_developer_key = nil, localhost)
|
17
|
+
# initialize @@logger
|
18
|
+
@@logger ||= URBANopt::REopt.reopt_logger
|
19
|
+
|
20
|
+
@nrel_developer_key = nrel_developer_key
|
21
|
+
@localhost = localhost
|
22
|
+
@reopt_ghp_output_district = nil
|
23
|
+
@reopt_ghp_output_building = []
|
24
|
+
@reopt_ghp_assumptions_hash = nil
|
25
|
+
@reopt_ghp_assumptions = nil
|
26
|
+
@system_parameter = nil
|
27
|
+
@system_parameter_hash = nil
|
28
|
+
@modelica_result = nil
|
29
|
+
@building_ids = nil
|
30
|
+
@run_dir = run_dir
|
31
|
+
|
32
|
+
if !reopt_ghp_assumptions.nil?
|
33
|
+
@reopt_ghp_assumptions = reopt_ghp_assumptions
|
34
|
+
File.open(reopt_ghp_assumptions, 'r') do |file|
|
35
|
+
@reopt_ghp_assumptions_input_hash = JSON.parse(file.read, symbolize_names: true)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
if !system_parameter.nil?
|
40
|
+
@system_parameter = system_parameter
|
41
|
+
File.open(system_parameter, 'r') do |file|
|
42
|
+
@system_parameter_input_hash = JSON.parse(file.read, symbolize_names: true)
|
43
|
+
end
|
44
|
+
# Determine loop order
|
45
|
+
loop_order = File.join(File.dirname(system_parameter), '_loop_order.json')
|
46
|
+
if File.exist?(loop_order)
|
47
|
+
File.open(loop_order, 'r') do |file|
|
48
|
+
loop_order_input = JSON.parse(file.read, symbolize_names: true)
|
49
|
+
# Check the type of the parsed data
|
50
|
+
if loop_order_input.is_a?(Array)
|
51
|
+
@loop_order_input_hash = loop_order_input
|
52
|
+
@loop_order_input_hash.each do |item|
|
53
|
+
puts "Building IDs in group: #{item[:list_bldg_ids_in_group].inspect}"
|
54
|
+
puts "GHE IDs in group: #{item[:list_ghe_ids_in_group].inspect}"
|
55
|
+
end
|
56
|
+
elsif loop_order_input.is_a?(Hash)
|
57
|
+
@loop_order_input_hash = [loop_order_input] # Wrap in array if a single object
|
58
|
+
@loop_order_input_hash.each do |item|
|
59
|
+
puts "Building IDs in group: #{item[:list_bldg_ids_in_group].inspect}"
|
60
|
+
puts "GHE IDs in group: #{item[:list_ghe_ids_in_group].inspect}"
|
61
|
+
end
|
62
|
+
else
|
63
|
+
puts 'Unexpected JSON structure'
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
if !modelica_result.nil?
|
71
|
+
@modelica_result_input = modelica_result
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
attr_accessor :run_dir, :system_parameter_input_hash, :reopt_ghp_assumptions_input_hash, :loop_order_input_hash, :modelica_result_input
|
76
|
+
|
77
|
+
# # Create REopt input and output building report
|
78
|
+
def run_reopt_lcca(system_parameter_hash: nil, reopt_ghp_assumptions_hash: nil, modelica_result: nil)
|
79
|
+
adapter = URBANopt::REopt::REoptGHPAdapter.new
|
80
|
+
|
81
|
+
# if these arguments are specified, use them
|
82
|
+
if !system_parameter_hash.nil?
|
83
|
+
@system_parameter_input_hash = system_parameter_hash
|
84
|
+
end
|
85
|
+
|
86
|
+
if !reopt_ghp_assumptions_hash.nil?
|
87
|
+
@reopt_ghp_assumptions_input_hash = reopt_ghp_assumptions_hash
|
88
|
+
end
|
89
|
+
|
90
|
+
if !modelica_result.nil?
|
91
|
+
@modelica_result_input = modelica_result
|
92
|
+
end
|
93
|
+
|
94
|
+
# Create folder for REopt input files only if they dont exist
|
95
|
+
reopt_ghp_dir = File.join(@run_dir, 'reopt_ghp')
|
96
|
+
reopt_ghp_input = File.join(reopt_ghp_dir, 'reopt_ghp_inputs')
|
97
|
+
unless Dir.exist?(reopt_ghp_dir)
|
98
|
+
FileUtils.mkdir_p(reopt_ghp_dir)
|
99
|
+
end
|
100
|
+
unless Dir.exist?(reopt_ghp_input)
|
101
|
+
FileUtils.mkdir_p(reopt_ghp_input)
|
102
|
+
end
|
103
|
+
|
104
|
+
reopt_ghp_output = File.join(reopt_ghp_dir, 'reopt_ghp_outputs')
|
105
|
+
unless Dir.exist?(reopt_ghp_output)
|
106
|
+
FileUtils.mkdir_p(reopt_ghp_output)
|
107
|
+
end
|
108
|
+
|
109
|
+
# get building IDs from _loop_order.json
|
110
|
+
building_ids = []
|
111
|
+
ghp_ids = []
|
112
|
+
@loop_order_input_hash.each do |loop|
|
113
|
+
building_ids.concat(loop[:list_bldg_ids_in_group].flatten)
|
114
|
+
ghp_ids.concat(loop[:list_ghe_ids_in_group].flatten)
|
115
|
+
end
|
116
|
+
|
117
|
+
building_ids.each do |building_id|
|
118
|
+
# create REopt building input file for all buildings in loop order list
|
119
|
+
reopt_input_building = adapter.create_reopt_input_building(@run_dir, @system_parameter_input_hash, @reopt_ghp_assumptions_input_hash, building_id, @modelica_result_input)
|
120
|
+
end
|
121
|
+
ghp_ids.each do |ghp_id|
|
122
|
+
# create REopt district input file
|
123
|
+
reopt_input_district = adapter.create_reopt_input_district(@run_dir, @system_parameter_input_hash, @reopt_ghp_assumptions_input_hash, ghp_id, @modelica_result_input)
|
124
|
+
end
|
125
|
+
|
126
|
+
Dir.foreach(reopt_ghp_input) do |input_file|
|
127
|
+
# Skip '.' and '..' (current and parent directory entries)
|
128
|
+
next if input_file == '.' || input_file == '..'
|
129
|
+
|
130
|
+
reopt_ghp_input_file_path = File.join(reopt_ghp_input, input_file)
|
131
|
+
|
132
|
+
reopt_input_data = nil
|
133
|
+
|
134
|
+
File.open(reopt_ghp_input_file_path, 'r') do |f|
|
135
|
+
reopt_input_data = JSON.parse(f.read)
|
136
|
+
end
|
137
|
+
|
138
|
+
base_name = File.basename(input_file, '.json')
|
139
|
+
|
140
|
+
# reopt_ghp_output_file
|
141
|
+
reopt_output_file = File.join(reopt_ghp_output, "#{base_name}_output.json")
|
142
|
+
# call the REopt API
|
143
|
+
api = URBANopt::REopt::REoptLiteGHPAPI.new(reopt_input_data, DEVELOPER_NREL_KEY, reopt_output_file, @localhost)
|
144
|
+
api.get_api_results
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end # REoptGHPPostProcessor
|
148
|
+
end # REopt
|
149
|
+
end # URBANopt
|