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