urbanopt-rnm-us 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +32 -0
- data/.rubocop.yml +9 -0
- data/CHANGELOG.md +5 -0
- data/CONTRIBUTING.md +58 -0
- data/Gemfile +10 -0
- data/LICENSE.md +38 -0
- data/README.md +49 -0
- data/Rakefile +166 -0
- data/catalogs/average_peak_per_building_type.json +94 -0
- data/catalogs/extended_catalog.json +15109 -0
- data/lib/urbanopt-rnm.rb +41 -0
- data/lib/urbanopt/rnm.rb +59 -0
- data/lib/urbanopt/rnm/api_client.rb +296 -0
- data/lib/urbanopt/rnm/capacitor_opendss.rb +57 -0
- data/lib/urbanopt/rnm/carson_eq.rb +389 -0
- data/lib/urbanopt/rnm/consumers.rb +255 -0
- data/lib/urbanopt/rnm/conversion_to_opendss.rb +152 -0
- data/lib/urbanopt/rnm/geojson_input.rb +261 -0
- data/lib/urbanopt/rnm/input_files.rb +335 -0
- data/lib/urbanopt/rnm/logger.rb +52 -0
- data/lib/urbanopt/rnm/oh_ug_rate.rb +129 -0
- data/lib/urbanopt/rnm/post_processor.rb +220 -0
- data/lib/urbanopt/rnm/processor_opendss_catalog.rb +73 -0
- data/lib/urbanopt/rnm/prosumers.rb +372 -0
- data/lib/urbanopt/rnm/rnm_us_catalog_conversion.rb +140 -0
- data/lib/urbanopt/rnm/runner.rb +177 -0
- data/lib/urbanopt/rnm/scenario_report.rb +145 -0
- data/lib/urbanopt/rnm/substation_location.rb +63 -0
- data/lib/urbanopt/rnm/transformer_opendss.rb +67 -0
- data/lib/urbanopt/rnm/version.rb +45 -0
- data/lib/urbanopt/rnm/wires_class.rb +70 -0
- data/lib/urbanopt/rnm/wires_opendss.rb +93 -0
- data/urbanopt-rnm-us-gem.gemspec +37 -0
- metadata +218 -0
@@ -0,0 +1,372 @@
|
|
1
|
+
# *********************************************************************************
|
2
|
+
# URBANopt (tm), Copyright (c) 2019-2021, 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
|
+
# Redistribution of this software, without modification, must refer to the software
|
20
|
+
# by the same designation. Redistribution of a modified version of this software
|
21
|
+
# (i) may not refer to the modified version by the same designation, or by any
|
22
|
+
# confusingly similar designation, and (ii) must refer to the underlying software
|
23
|
+
# originally provided by Alliance as "URBANopt". Except to comply with the foregoing,
|
24
|
+
# the term "URBANopt", or any confusingly similar designation may not be used to
|
25
|
+
# refer to any modified version of this software or any modified version of the
|
26
|
+
# underlying software originally provided by Alliance without the prior written
|
27
|
+
# consent of Alliance.
|
28
|
+
|
29
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
30
|
+
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
31
|
+
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
32
|
+
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
|
33
|
+
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
34
|
+
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
35
|
+
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
36
|
+
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
37
|
+
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
|
38
|
+
# OF THE POSSIBILITY OF SUCH DAMAGE.
|
39
|
+
# *********************************************************************************
|
40
|
+
|
41
|
+
require 'json'
|
42
|
+
require 'csv'
|
43
|
+
module URBANopt
|
44
|
+
module RNM
|
45
|
+
# creating a class that creates the consumers input required by the RNM-US model,
|
46
|
+
# according to their geographic location, energy consumption and peak demand, and power consumption profiles
|
47
|
+
class Prosumers
|
48
|
+
attr_accessor :customers, :customers_ext, :profile_customer_p, :profile_customer_q, :profile_customer_p_ext, :profile_customer_q_ext, :dg, :dg_profile_p, :dg_profile_q, :profile_dg_p_extended, :profile_dg_q_extended, :power_factor
|
49
|
+
|
50
|
+
# initializing all the attributes to build the inputs files required by the RNM-US model
|
51
|
+
def initialize(reopt, only_lv_consumers = false, max_num_lv_nodes, average_building_peak_catalog_path, lv_limit)
|
52
|
+
@reopt = reopt
|
53
|
+
@average_building_peak_catalog_path = average_building_peak_catalog_path
|
54
|
+
@only_lv_consumers = only_lv_consumers
|
55
|
+
@max_num_lv_nodes = max_num_lv_nodes
|
56
|
+
@customers = []
|
57
|
+
@customers_ext = []
|
58
|
+
@profile_customer_p = []
|
59
|
+
@profile_customer_q = []
|
60
|
+
@profile_customer_p_ext = []
|
61
|
+
@profile_customer_q_ext = []
|
62
|
+
@dg = []
|
63
|
+
@dg_profile_p = []
|
64
|
+
@dg_profile_q = []
|
65
|
+
@profile_dg_p_extended = []
|
66
|
+
@profile_dg_q_extended = []
|
67
|
+
@power_factor = power_factor
|
68
|
+
@lv_limit = lv_limit
|
69
|
+
end
|
70
|
+
# creating a method to process each building electricity consumption
|
71
|
+
# the method receives as argument the required data obtined from each feature csv and json urbanopt output files
|
72
|
+
# and returns the customer_ext array for each feature, with the required customer data needed for RNM-US
|
73
|
+
# and the profiles consumers files
|
74
|
+
|
75
|
+
# method defined for the case of a single node where both battery, DG and consumers are placed
|
76
|
+
# evaluation of the peak power in each node to define the type of connection (e.g. voltage level and n phases)
|
77
|
+
def construct_prosumer_general(profiles, single_values, building_map, area, height, users, der_capacity)
|
78
|
+
id = building_map[3]
|
79
|
+
id_dg = "#{building_map[3]}_DG"
|
80
|
+
id_batt = "#{building_map[3]}_battery"
|
81
|
+
building_map.pop # deleting the last_element of the list which represents the id
|
82
|
+
peak_app_power_node = 0
|
83
|
+
# defining the max peak in the hour with consumption max peak
|
84
|
+
# in the hour with generation max peak
|
85
|
+
# in the hour with storage max peak
|
86
|
+
|
87
|
+
for i in 0..profiles[:planning_profile_cust_active].length - 1
|
88
|
+
hourly_app_power = ((profiles[:planning_profile_cust_active][i] + profiles[:planning_profile_storage_active][i] - profiles[:planning_profile_dg_active][i]) / @power_factor).abs
|
89
|
+
if hourly_app_power > peak_app_power_node
|
90
|
+
peak_app_power_node = hourly_app_power
|
91
|
+
end
|
92
|
+
end
|
93
|
+
# creating the customer text files (treating also the battery as a consumer) & the DG text file
|
94
|
+
if @medium_voltage
|
95
|
+
voltage_default = 12.47
|
96
|
+
phases = 3
|
97
|
+
else
|
98
|
+
voltage_default, phases = voltage_values(peak_app_power_node / 0.9) # margin to consider 0.9 for safety reasons
|
99
|
+
end
|
100
|
+
@customers.push([building_map, id, voltage_default, single_values[:peak_active_power_cons], single_values[:peak_reactive_power_cons], phases])
|
101
|
+
@customers_ext.push([building_map, id, voltage_default, single_values[:peak_active_power_cons], single_values[:peak_reactive_power_cons], phases, area, height, (single_values[:energy]).round(2), single_values[:peak_active_power_cons], single_values[:peak_reactive_power_cons], users])
|
102
|
+
@profile_customer_q.push([id, 48, profiles[:planning_profile_cust_reactive]])
|
103
|
+
@profile_customer_p.push([id, 48, profiles[:planning_profile_cust_active]])
|
104
|
+
@profile_customer_p_ext.push([id, 8760, profiles[:yearly_profile_cust_active]])
|
105
|
+
@profile_customer_q_ext.push([id, 8760, profiles[:yearly_profile_cust_reactive]])
|
106
|
+
|
107
|
+
if !der_capacity[:storage].nil? && der_capacity[:storage] > 0
|
108
|
+
@customers.push([building_map, id_batt, voltage_default, single_values[:peak_active_power_storage], single_values[:peak_reactive_power_storage], phases])
|
109
|
+
@customers_ext.push([building_map, id_batt, voltage_default, single_values[:peak_active_power_storage], single_values[:peak_reactive_power_storage], phases, area, height, (single_values[:energy]).round(2), single_values[:peak_active_power_storage], single_values[:peak_reactive_power_storage], users])
|
110
|
+
@profile_customer_q.push([id_batt, 48, profiles[:planning_profile_storage_reactive]])
|
111
|
+
@profile_customer_p.push([id_batt, 48, profiles[:planning_profile_storage_active]])
|
112
|
+
@profile_customer_p_ext.push([id_batt, 8760, profiles[:yearly_profile_storage_active]])
|
113
|
+
@profile_customer_q_ext.push([id_batt, 8760, profiles[:yearly_profile_storage_reactive]])
|
114
|
+
end
|
115
|
+
@dg.push([building_map, id_dg, voltage_default, der_capacity[:dg], single_values[:peak_active_power_dg].round(2), single_values[:peak_reactive_power_dg].round(2), phases])
|
116
|
+
@dg_profile_p.push([id_dg, 48, profiles[:planning_profile_dg_active]])
|
117
|
+
@dg_profile_q.push([id_dg, 48, profiles[:planning_profile_dg_reactive]])
|
118
|
+
@profile_dg_p_extended.push([id_dg, 8760, profiles[:yearly_profile_dg_active]])
|
119
|
+
@profile_dg_q_extended.push([id_dg, 8760, profiles[:yearly_profile_dg_reactive]])
|
120
|
+
end
|
121
|
+
|
122
|
+
# creating a method to process each building electricity consumption
|
123
|
+
# the method receives as argument the required data obtined from each feature csv and json urbanopt output files
|
124
|
+
# and returns the customer_ext array for each feature, with the required customer data needed for RNM-US
|
125
|
+
# and the profiles consumers files
|
126
|
+
|
127
|
+
# this method is called only if the user sets the option of "only LV nodes" to true
|
128
|
+
# defining a certain numb of nodes for each building and distributing the peak power values equally
|
129
|
+
# among the nodes of each building
|
130
|
+
def construct_prosumer_lv(nodes_per_bldg = 0, profiles, single_values, building_map, building_nodes, area, height, users, der_capacity)
|
131
|
+
# the default variables are defined (i.e. type and rurality type)
|
132
|
+
planning_profile_node_active = []
|
133
|
+
planning_profile_node_reactive = []
|
134
|
+
yearly_profile_node_active = []
|
135
|
+
yearly_profile_node_reactive = []
|
136
|
+
closest_node = building_map[3].split('_')[1].to_i # refers to the closest node of the building in consideration to the street
|
137
|
+
node = closest_node
|
138
|
+
cont = 1
|
139
|
+
cont_reverse = 1
|
140
|
+
nodes_consumers = nodes_per_bldg - 1
|
141
|
+
for i in 1..nodes_per_bldg
|
142
|
+
coordinates = building_map
|
143
|
+
node = closest_node + cont # to set the new nodes with enough distance among each others
|
144
|
+
node_reverse = closest_node - cont_reverse
|
145
|
+
if i > 1 && node <= building_nodes.length - 2
|
146
|
+
coordinates = building_nodes[node] # take the closest building node index to the street and pass the nodes after it
|
147
|
+
cont += 1
|
148
|
+
elsif i > 1
|
149
|
+
coordinates = building_nodes[node_reverse]
|
150
|
+
cont_reverse += 1
|
151
|
+
end
|
152
|
+
# this condition is used to firstly place the building consumption nodes and then the last node
|
153
|
+
# to be placed is the one referred to DG and battery for the building
|
154
|
+
if i < nodes_per_bldg # considering the consumers nodes
|
155
|
+
id = coordinates[3]
|
156
|
+
coordinates.pop
|
157
|
+
peak_active_power_cons = (single_values[:peak_active_power_cons] / nodes_consumers).round(2)
|
158
|
+
peak_reactive_power_cons = (single_values[:peak_reactive_power_cons] / nodes_consumers).round(2)
|
159
|
+
voltage_default, phases = voltage_values(peak_active_power_cons / @power_factor)
|
160
|
+
for k in 0..profiles[:planning_profile_cust_active].length - 1
|
161
|
+
planning_profile_node_active[k] = (profiles[:planning_profile_cust_active][k] / nodes_consumers).round(2)
|
162
|
+
planning_profile_node_reactive[k] = (profiles[:planning_profile_cust_reactive][k] / nodes_consumers).round(2)
|
163
|
+
end
|
164
|
+
for k in 0..profiles[:yearly_profile_cust_active].length - 1
|
165
|
+
yearly_profile_node_active[k] = (profiles[:yearly_profile_cust_active][k] / nodes_consumers).round(2)
|
166
|
+
yearly_profile_node_reactive[k] = (profiles[:yearly_profile_cust_reactive][k] / nodes_consumers).round(2)
|
167
|
+
end
|
168
|
+
@customers.push([coordinates, id, voltage_default, peak_active_power_cons, peak_reactive_power_cons, phases])
|
169
|
+
@customers_ext.push([coordinates, id, voltage_default, peak_active_power_cons, peak_reactive_power_cons, phases, area, height, (single_values[:energy] / nodes_consumers).round(2), peak_active_power_cons, peak_reactive_power_cons, users])
|
170
|
+
@profile_customer_q.push([id, 48, planning_profile_node_reactive])
|
171
|
+
@profile_customer_p.push([id, 48, planning_profile_node_active])
|
172
|
+
@profile_customer_p_ext.push([id, 8760, yearly_profile_node_active])
|
173
|
+
@profile_customer_q_ext.push([id, 8760, yearly_profile_node_reactive])
|
174
|
+
else
|
175
|
+
# considering the DG and battery
|
176
|
+
voltage_default, phases = voltage_values(der_capacity[:dg]) # assuming that the pv capacity is always higher than battery capacity
|
177
|
+
id_dg = "#{coordinates[3]}_DG"
|
178
|
+
id_batt = "#{coordinates[3]}_battery"
|
179
|
+
coordinates.pop
|
180
|
+
@dg.push([coordinates, id_dg, voltage_default, der_capacity[:dg], single_values[:peak_active_power_dg].round(2), single_values[:peak_reactive_power_dg].round(2), phases])
|
181
|
+
@dg_profile_p.push([id_dg, 48, profiles[:planning_profile_dg_active]])
|
182
|
+
@dg_profile_q.push([id_dg, 48, profiles[:planning_profile_dg_reactive]])
|
183
|
+
@profile_dg_p_extended.push([id_dg, 8760, profiles[:yearly_profile_dg_active]])
|
184
|
+
@profile_dg_q_extended.push([id_dg, 8760, profiles[:yearly_profile_dg_reactive]])
|
185
|
+
if !der_capacity[:storage].nil? && der_capacity[:storage] > 0
|
186
|
+
@customers.push([coordinates, id_batt, voltage_default, single_values[:peak_active_power_storage], single_values[:peak_reactive_power_storage], phases])
|
187
|
+
@customers_ext.push([coordinates, id_batt, voltage_default, single_values[:peak_active_power_storage], single_values[:peak_reactive_power_storage], phases, area, height, (single_values[:energy]).round(2), single_values[:peak_active_power_storage], single_values[:peak_reactive_power_storage], users])
|
188
|
+
@profile_customer_q.push([id_batt, 48, profiles[:planning_profile_storage_reactive]])
|
189
|
+
@profile_customer_p.push([id_batt, 48, profiles[:planning_profile_storage_active]])
|
190
|
+
@profile_customer_p_ext.push([id_batt, 8760, profiles[:yearly_profile_storage_active]])
|
191
|
+
@profile_customer_q_ext.push([id_batt, 8760, profiles[:yearly_profile_storage_reactive]])
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
# creating a function that for each node defines the connection (e.g LV, MV, single-phase, 3-phase)
|
198
|
+
# according to the catalog limits previously calculated
|
199
|
+
def voltage_values(peak_apparent_power)
|
200
|
+
case peak_apparent_power
|
201
|
+
when -10000..@lv_limit[:single_phase] # set by the catalog limits
|
202
|
+
phases = 1
|
203
|
+
voltage_default = 0.416
|
204
|
+
when @lv_limit[:single_phase]..@lv_limit[:three_phase] # defined from the catalog (from the wires)
|
205
|
+
phases = 3
|
206
|
+
voltage_default = 0.416
|
207
|
+
# MV and 3 phases untill 16 MVA defined by SMART-DS project
|
208
|
+
when @lv_limit[:three_phase]..16000
|
209
|
+
phases = 3
|
210
|
+
voltage_default = 12.47
|
211
|
+
else
|
212
|
+
# HV and 3 phases for over 16 MVA
|
213
|
+
phases = 3
|
214
|
+
voltage_default = 69
|
215
|
+
end
|
216
|
+
return voltage_default, phases
|
217
|
+
end
|
218
|
+
|
219
|
+
# defining a method to calculate the total sum of DG and battery capacity for each building in the district
|
220
|
+
def sum_dg(dg)
|
221
|
+
capacity = Hash.new(0)
|
222
|
+
for i in 0..dg['solar_pv'].length - 1
|
223
|
+
capacity[:dg] += dg['solar_pv'][i]['size_kw'].to_f.round(2)
|
224
|
+
end
|
225
|
+
for i in 0..dg['wind'].length - 1
|
226
|
+
capacity[:dg] += dg['wind'][i]['size_kw'].to_f.round(2)
|
227
|
+
end
|
228
|
+
for i in 0..dg['generator'].length - 1
|
229
|
+
capacity[:dg] += dg['generator'][i]['size_kw'].to_f.round(2)
|
230
|
+
end
|
231
|
+
capacity[:storage] = dg['total_storage_kw']
|
232
|
+
return capacity
|
233
|
+
end
|
234
|
+
|
235
|
+
# creating a method to define the number of nodes for each building in case the user set the option "only LV" to true.
|
236
|
+
# this method calculates the number of nodes for each building in the project and in case the numb of nodes is higher than 4
|
237
|
+
# than the building is considered as a single node connected in MV
|
238
|
+
# the numb of nodes is calculated based on the average_peak_catalog which is obtained from DOE open_source data per location and per building type
|
239
|
+
def av_peak_cons_per_building_type(feature_file)
|
240
|
+
average_peak_by_size = []
|
241
|
+
floor_area = []
|
242
|
+
average_peak = 5 # defining a random value first, since now the residential buildings are not considered in the catalog
|
243
|
+
mixed_use_av_peak = 0
|
244
|
+
area_mixed_use = 0
|
245
|
+
# defining a conservative factor which creates some margin with the number of nodes found using the av_peak catalog, with the
|
246
|
+
# actual nodes that could be found with the current buildings peak consumptions in the project
|
247
|
+
conservative_factor = 0.8 # considered as a reasonable assumption, but this value could be changed
|
248
|
+
average_peak_folder = JSON.parse(File.read(@average_building_peak_catalog_path))
|
249
|
+
for i in 0..feature_file.length - 1
|
250
|
+
area = (feature_file[i]['floor_area']).round(2)
|
251
|
+
building_type = feature_file[i]['building_type'] # it specifies the type of building, sometimes it is directly the sub-type
|
252
|
+
counter = 0 # counter to find number of buildings type belonging to same "category"
|
253
|
+
average_peak_folder.each do |building_class|
|
254
|
+
if building_type == building_class['building type'] || building_type == building_class['sub-type']
|
255
|
+
average_peak = (building_class['average peak demand (kW/ft2)'].to_f * area).to_f.round(4) # finding the average peak considering the floor area of the bilding under consideration
|
256
|
+
average_peak_by_size[counter] = average_peak
|
257
|
+
floor_area[counter] = (building_class['floor_area (ft2)'] - area).abs # minimum difference among area and area from the prototypes defined by DOE
|
258
|
+
counter += 1
|
259
|
+
# in this way I don t consider residential and I assume it s average_peak = 0, it is ok because we assume always 1 node per RES consumers, single-detached family houses
|
260
|
+
end
|
261
|
+
end
|
262
|
+
if counter > 1
|
263
|
+
index = floor_area.index(floor_area.min)
|
264
|
+
average_peak = average_peak_by_size[index]
|
265
|
+
end
|
266
|
+
if feature_file.length > 1 # defined for Mixed_use buildings, which include more building types
|
267
|
+
mixed_use_av_peak += average_peak
|
268
|
+
area_mixed_use += area
|
269
|
+
end
|
270
|
+
end
|
271
|
+
if feature_file.length > 1
|
272
|
+
average_peak = mixed_use_av_peak # average peak per mixed use considering the building types which are in this building
|
273
|
+
area = area_mixed_use
|
274
|
+
end
|
275
|
+
nodes_per_bldg = (average_peak / (@lv_limit[:three_phase] * @power_factor * conservative_factor)).to_f.ceil # computing number of nodes per building
|
276
|
+
if nodes_per_bldg > @max_num_lv_nodes # defined as reasonable maximum
|
277
|
+
nodes_per_bldg = 1
|
278
|
+
@medium_voltage = true
|
279
|
+
end
|
280
|
+
|
281
|
+
nodes_per_bldg += 1 # tacking into account the extra node for distributed generation and the battery
|
282
|
+
|
283
|
+
return nodes_per_bldg, area
|
284
|
+
end
|
285
|
+
|
286
|
+
# defining a method for the customers and generators files creation:
|
287
|
+
# obtaining all the needed input from each feature_report.csv file (active & apparent power and tot energy consumed and produced)
|
288
|
+
# and from each feature_report.json file (area, height, number of users, DG capacity)
|
289
|
+
# the method passes as arguments the urbanopt json and csv output file for each feature and the building coordinates previously calculated
|
290
|
+
# and the "extreme" hours used to plan the network
|
291
|
+
def prosumer_files_load(csv_feature_report, json_feature_report, building_map, building_nodes, hour)
|
292
|
+
profiles = Hash.new { |h, k| h[k] = [] }
|
293
|
+
single_values = Hash.new(0)
|
294
|
+
@medium_voltage = false
|
295
|
+
hours = 23
|
296
|
+
feature_type = json_feature_report['program']['building_types'][0]['building_type']
|
297
|
+
residential_building_types = 'Single-Family Detached' # add the other types
|
298
|
+
# finding the index where to start computing and saving the info, from the value of the "worst-case hour" for the max peak consumption of the district
|
299
|
+
if residential_building_types.include? feature_type
|
300
|
+
profile_start_max = hour.hour_index_max_res - hour.peak_hour_max_res
|
301
|
+
profile_start_min = hour.hour_index_min_res - hour.peak_hour_min_res
|
302
|
+
else
|
303
|
+
profile_start_max = hour.hour_index_max_comm - hour.peak_hour_max_comm
|
304
|
+
profile_start_min = hour.hour_index_min_comm - hour.peak_hour_min_comm
|
305
|
+
end
|
306
|
+
# finding the index where to start computing and saving the info, from the value of the "most extreme hours" for the max peak consumption of the district
|
307
|
+
# profile_start_max = hour.hour_index_max - hour.peak_hour_max
|
308
|
+
# profile_start_min = hour.hour_index_min - hour.peak_hour_min
|
309
|
+
k = 0 # index for each hour of the year represented in the csv file
|
310
|
+
i = 0 # to represent the 24 hours
|
311
|
+
h_cons_batt = 0
|
312
|
+
h_dg_max = 0 # hour with max DG generation
|
313
|
+
h_stor_max = 0 # hour with max storage absorption
|
314
|
+
max_peak = 0
|
315
|
+
# content = CSV.foreach(csv_feature_report, headers: true) do |power|
|
316
|
+
CSV.foreach(csv_feature_report, headers: true) do |power|
|
317
|
+
@power_factor = power['Electricity:Facility Power(kW)'].to_f / power['Electricity:Facility Apparent Power(kVA)'].to_f
|
318
|
+
profiles[:yearly_profile_cust_active].push(power['REopt:Electricity:Load:Total(kw)'].to_f)
|
319
|
+
profiles[:yearly_profile_cust_reactive].push(profiles[:yearly_profile_cust_active][k] * Math.tan(Math.acos(@power_factor)))
|
320
|
+
profiles[:yearly_profile_dg_active].push(power['REopt:ElectricityProduced:Total(kw)'].to_f)
|
321
|
+
profiles[:yearly_profile_dg_reactive].push(profiles[:yearly_profile_dg_active][k] * Math.tan(Math.acos(@power_factor)))
|
322
|
+
profiles[:yearly_profile_storage_active].push(power['REopt:Electricity:Grid:ToBattery(kw)'].to_f + power['REopt:ElectricityProduced:Generator:ToBattery(kw)'].to_f + power['REopt:ElectricityProduced:PV:ToBattery(kw)'].to_f + power['REopt:ElectricityProduced:Wind:ToBattery(kw)'].to_f - power['REopt:Electricity:Storage:ToLoad(kw)'].to_f - power['REopt:Electricity:Storage:ToGrid(kw)'].to_f)
|
323
|
+
profiles[:yearly_profile_storage_reactive].push(profiles[:yearly_profile_storage_active][k] * Math.tan(Math.acos(@power_factor)))
|
324
|
+
single_values[:energy] += power['REopt:Electricity:Load:Total(kw)'].to_f # calculating the yearly energy consumed by each feature
|
325
|
+
single_values[:energy_dg] += power['REopt:ElectricityProduced:Total(kw)'].to_f
|
326
|
+
single_values[:energy_storage] += power['REopt:Electricity:Grid:ToBattery(kw)'].to_f + power['REopt:ElectricityProduced:Generator:ToBattery(kw)'].to_f + power['REopt:ElectricityProduced:PV:ToBattery(kw)'].to_f + power['REopt:ElectricityProduced:Wind:ToBattery(kw)'].to_f - power['REopt:Electricity:Storage:ToLoad(kw)'].to_f - power['REopt:Electricity:Storage:ToGrid(kw)'].to_f
|
327
|
+
if k >= profile_start_max && k <= profile_start_max + hours || k >= profile_start_min && k <= profile_start_min + hours
|
328
|
+
profiles[:planning_profile_cust_active].push(power['REopt:Electricity:Load:Total(kw)'].to_f)
|
329
|
+
if profiles[:planning_profile_cust_active][i] > single_values[:peak_active_power_cons]
|
330
|
+
single_values[:peak_active_power_cons] = profiles[:planning_profile_cust_active][i]
|
331
|
+
single_values[:peak_reactive_power_cons] = single_values[:peak_active_power_cons] * Math.tan(Math.acos(power_factor))
|
332
|
+
single_values[:h_cons_max] = i
|
333
|
+
end
|
334
|
+
profiles[:planning_profile_storage_active].push((power['REopt:Electricity:Grid:ToBattery(kw)'].to_f + power['REopt:ElectricityProduced:Generator:ToBattery(kw)'].to_f + power['REopt:ElectricityProduced:PV:ToBattery(kw)'].to_f + power['REopt:ElectricityProduced:Wind:ToBattery(kw)'].to_f - power['REopt:Electricity:Storage:ToLoad(kw)'].to_f - power['REopt:Electricity:Storage:ToGrid(kw)'].to_f))
|
335
|
+
if profiles[:planning_profile_storage_active][i] > single_values[:peak_active_power_storage]
|
336
|
+
single_values[:peak_active_power_storage] = profiles[:planning_profile_storage_active][i]
|
337
|
+
single_values[:peak_reactive_power_storage] = single_values[:peak_active_power_storage] * Math.tan(Math.acos(power_factor))
|
338
|
+
single_values[:h_stor_max] = i
|
339
|
+
end
|
340
|
+
profiles[:planning_profile_dg_active].push(power['REopt:ElectricityProduced:Total(kw)'].to_f)
|
341
|
+
if profiles[:planning_profile_dg_active][i] > single_values[:peak_active_power_dg]
|
342
|
+
single_values[:peak_active_power_dg] = profiles[:planning_profile_dg_active][i]
|
343
|
+
single_values[:peak_reactive_power_dg] = single_values[:peak_active_power_dg] * Math.tan(Math.acos(power_factor))
|
344
|
+
single_values[:h_dg_max] = i
|
345
|
+
end
|
346
|
+
profiles[:planning_profile_cust_reactive].push(profiles[:planning_profile_cust_active][i] * Math.tan(Math.acos(power_factor)))
|
347
|
+
profiles[:planning_profile_storage_reactive].push(profiles[:planning_profile_storage_active][i] * Math.tan(Math.acos(power_factor)))
|
348
|
+
profiles[:planning_profile_dg_reactive].push(profiles[:planning_profile_dg_active][i] * Math.tan(Math.acos(power_factor)))
|
349
|
+
i += 1
|
350
|
+
end
|
351
|
+
k += 1
|
352
|
+
end
|
353
|
+
height = (json_feature_report['program']['maximum_roof_height_ft']).round(2)
|
354
|
+
users = json_feature_report['program']['number_of_residential_units']
|
355
|
+
der_capacity = sum_dg(json_feature_report['distributed_generation'])
|
356
|
+
if @only_lv_consumers
|
357
|
+
nodes_per_bldg, area = av_peak_cons_per_building_type(json_feature_report['program']['building_types'])
|
358
|
+
if @max_num_lv_nodes == 1
|
359
|
+
construct_prosumer_general(profiles, single_values, building_map, area, height, users, der_capacity)
|
360
|
+
else
|
361
|
+
construct_prosumer_lv(nodes_per_bldg, profiles, single_values, building_map, building_nodes, area, height, users, der_capacity)
|
362
|
+
end
|
363
|
+
else
|
364
|
+
# this key seems to change between floor_area or floor_area_ft
|
365
|
+
area = json_feature_report['program'].key?('floor_area') ? (json_feature_report['program']['floor_area']).round(2) : (json_feature_report['program']['floor_area_sqft']).round(2)
|
366
|
+
# associating 2 nodes (consumers & DG and battery in the same node) per building considering the consumer, the battery and DG
|
367
|
+
construct_prosumer_general(profiles, single_values, building_map, area, height, users, der_capacity)
|
368
|
+
end
|
369
|
+
end
|
370
|
+
end
|
371
|
+
end
|
372
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# *********************************************************************************
|
2
|
+
# URBANopt (tm), Copyright (c) 2019-2021, 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
|
+
# Redistribution of this software, without modification, must refer to the software
|
20
|
+
# by the same designation. Redistribution of a modified version of this software
|
21
|
+
# (i) may not refer to the modified version by the same designation, or by any
|
22
|
+
# confusingly similar designation, and (ii) must refer to the underlying software
|
23
|
+
# originally provided by Alliance as "URBANopt". Except to comply with the foregoing,
|
24
|
+
# the term "URBANopt", or any confusingly similar designation may not be used to
|
25
|
+
# refer to any modified version of this software or any modified version of the
|
26
|
+
# underlying software originally provided by Alliance without the prior written
|
27
|
+
# consent of Alliance.
|
28
|
+
|
29
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
30
|
+
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
31
|
+
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
32
|
+
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
|
33
|
+
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
34
|
+
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
35
|
+
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
36
|
+
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
37
|
+
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
|
38
|
+
# OF THE POSSIBILITY OF SUCH DAMAGE.
|
39
|
+
# *********************************************************************************
|
40
|
+
|
41
|
+
require 'json'
|
42
|
+
require 'urbanopt/rnm/logger'
|
43
|
+
|
44
|
+
module URBANopt
|
45
|
+
module RNM
|
46
|
+
# creating a class to convert the extended catalog into the RNM-US catalog which is needed by the model
|
47
|
+
class RnmUsCatalogConversion
|
48
|
+
def initialize(extended_catalog_path, run_dir, rnm_dirname)
|
49
|
+
@extended_catalog_path = extended_catalog_path
|
50
|
+
@run_dir = run_dir
|
51
|
+
@rnm_dirname = rnm_dirname
|
52
|
+
end
|
53
|
+
|
54
|
+
def matrix_processing(csv, matrix, v, row, section, wires, k)
|
55
|
+
if matrix.is_a?(Array)
|
56
|
+
csv << row
|
57
|
+
headings = []
|
58
|
+
for j in 0..matrix.length - 1
|
59
|
+
if section == 'LINES'
|
60
|
+
if matrix[j]['Line geometry'].is_a? Array
|
61
|
+
line_creation = URBANopt::RNM::CarsonEq.new(matrix[j])
|
62
|
+
matrix[j] = line_creation.creation(wires) # passing the info about each power line
|
63
|
+
else
|
64
|
+
matrix[j].delete('Line geometry')
|
65
|
+
end
|
66
|
+
end
|
67
|
+
ii = 0
|
68
|
+
matrix[j].each do |keys, values|
|
69
|
+
matrix[j].delete('connection')
|
70
|
+
matrix[j].delete('resistance(Ohm)')
|
71
|
+
matrix[j].delete('control_type')
|
72
|
+
if ii == 0
|
73
|
+
headings[ii] = "##{keys}" # to ensure it works with the catalog
|
74
|
+
else
|
75
|
+
headings[ii] = keys
|
76
|
+
end
|
77
|
+
row[ii] = values
|
78
|
+
ii += 1
|
79
|
+
end
|
80
|
+
if j == 0
|
81
|
+
csv << headings
|
82
|
+
end
|
83
|
+
csv << row
|
84
|
+
|
85
|
+
end
|
86
|
+
else
|
87
|
+
if k == 'Mismatch voltages S (kV):'.to_s || k == 'Mismatches convergence S (kVA):' || k == 'Minimum allowable voltages (pu):' || k == 'Maximum allowable voltages (pu):'
|
88
|
+
for i in 0..v.split(',').length - 1
|
89
|
+
row.push(v.split(',')[i])
|
90
|
+
end
|
91
|
+
else
|
92
|
+
row.push(v)
|
93
|
+
end
|
94
|
+
csv << row
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def processing_data(utm_zone)
|
99
|
+
# parsing lines and wires info:
|
100
|
+
row = Array.new(25)
|
101
|
+
ext_catalog = JSON.parse(File.read(@extended_catalog_path))
|
102
|
+
CSV.open(File.join(@run_dir, @rnm_dirname, 'udcons.csv'), 'w') do |csv|
|
103
|
+
ext_catalog.each do |key, v|
|
104
|
+
if key != 'WIRES'
|
105
|
+
csv << ["<#{key}>"]
|
106
|
+
if ext_catalog[key].is_a?(Hash) # defining the section under consideration is an Hash or an Array
|
107
|
+
if key == 'OTHERS'
|
108
|
+
ext_catalog[key]["UTM Zone"] = utm_zone.to_s
|
109
|
+
end
|
110
|
+
ext_catalog[key].each do |k, v|
|
111
|
+
row = []
|
112
|
+
row.push(k) # title of the array
|
113
|
+
matrix_processing(csv, ext_catalog[key][k], v, row, key, ext_catalog['WIRES'], k)
|
114
|
+
end
|
115
|
+
else
|
116
|
+
if ext_catalog[key].empty?
|
117
|
+
csv << ['END']
|
118
|
+
else
|
119
|
+
for i in 0..ext_catalog[key].length - 1
|
120
|
+
row = []
|
121
|
+
if ext_catalog[key][i].is_a?(Hash)
|
122
|
+
ext_catalog[key][i].each do |k, v|
|
123
|
+
row.push(k) # title of the array
|
124
|
+
matrix_processing(csv, ext_catalog[key][i][k], v, row, key, ext_catalog['WIRES'], k)
|
125
|
+
if key == 'LINES' || key == 'SUBSTATIONS AND DISTRIBUTION TRANSFORMERS' || key == 'CAPACITORS'
|
126
|
+
csv << ['END']
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
csv << ["</#{key}>"]
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|