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,389 @@
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 'urbanopt/rnm/logger'
42
+ require 'json'
43
+ require 'csv'
44
+ require 'set'
45
+ require 'matrix'
46
+
47
+ module URBANopt
48
+ module RNM
49
+ # creating a class which is able to convert the line-geometry information for each power line and the
50
+ # conductors information, into power lines information, obtaining their impedances and capacitance
51
+ # applying carsons equations
52
+ class CarsonEq
53
+ def initialize(hash)
54
+ @power_line = hash
55
+ end
56
+
57
+ # method to place the new parameters created in the right position in the final RNM-US catalog
58
+ def insert_field(key, fields, proximity = :before)
59
+ @power_line = @power_line.to_a.insert(@power_line.keys.index(key) + (proximity == :after ? 1 : 0), fields.first).to_h
60
+ end
61
+
62
+ # method to convert initial SI units in the ext catalog into Imperial units used by the Carson equation
63
+ # create the one from ft to m
64
+ def si_to_imperial_units(quantity, unit_input, unit_output)
65
+ if unit_output == 'ft' && unit_input == 'mm'
66
+ return quantity * 0.003281
67
+ elsif unit_output == 'ft' && unit_input == 'm'
68
+ quantity / 0.3048
69
+ return quantity / 0.3048
70
+ elsif unit_output == 'mi' && unit_input == 'km'
71
+ return quantity / 1.6093
72
+ elsif unit_output == '1/mi' && unit_input == '1/km'
73
+ return quantity * 1.6093
74
+ elsif unit_output == '1/ft' && unit_input == '1/m'
75
+ return quantity * 0.3048
76
+ end
77
+ end
78
+
79
+ # method to convert the results in Imperial units into SI units used by the RNM-US model
80
+ # create the one from ft to m
81
+ def imperial_to_si_units(quantity, unit_input, unit_output)
82
+ if unit_output == 'ft'
83
+ return quantity * 0.3048
84
+ elsif unit_output == 'mi' && unit_input == 'km'
85
+ return quantity * 1.60934
86
+ elsif unit_output == '1/km' && unit_input == '1/mi'
87
+ return (quantity / 1.60934)
88
+ end
89
+ end
90
+
91
+ def get_sequence_impedance_matrix(phase_impedance_matrix)
92
+ a = Complex(Math.cos(Math::PI * 2 / 3), Math.sin(Math::PI * 2 / 3))
93
+ a_matrix = Matrix[[1, 1, 1], [1, a**2, a], [1, a, a**2]]
94
+ a_matrix_inv = a_matrix.inverse
95
+ half = phase_impedance_matrix * a_matrix
96
+ final_matrix = a_matrix_inv * half
97
+ return final_matrix
98
+ end
99
+
100
+ # method which applies the kron reduction to reduce the primitive impedance matrix by one dimension
101
+ def kron_reduction(primitive_impedance_matrix, n_concentric_neutrals)
102
+ if n_concentric_neutrals == 0
103
+ neutrals = primitive_impedance_matrix.length - 1 # position of the neutral in primitive_imp_matrix
104
+ dim_neutrals = primitive_impedance_matrix.length - neutrals
105
+ else
106
+ neutrals = primitive_impedance_matrix.length - n_concentric_neutrals # position of the neutral in primitive_imp_matrix
107
+ dim_neutrals = n_concentric_neutrals
108
+ end
109
+ dim_phase = primitive_impedance_matrix.length - dim_neutrals
110
+ zij = Array.new(dim_phase) { Array.new(dim_phase) }
111
+ for i in 0..dim_phase - 1
112
+ for j in 0..dim_phase - 1
113
+ j < dim_phase && i < dim_phase
114
+ zij[i][j] = primitive_impedance_matrix[i][j]
115
+ end
116
+ end
117
+ znn = Array.new(dim_neutrals) { Array.new(dim_neutrals) }
118
+ for i in 0..znn.length - 1
119
+ for j in 0..znn.length - 1
120
+ znn[i][j] = primitive_impedance_matrix[neutrals + i][neutrals + j] # neutrals e l indice del neutro
121
+ end
122
+ end
123
+ zin = Array.new(dim_phase) { Array.new(dim_neutrals) } # z[i][n]
124
+ for i in 0..dim_phase - 1
125
+ for j in 0..dim_neutrals - 1
126
+ i < dim_phase
127
+ zin[i][j] = primitive_impedance_matrix[i][neutrals + j]
128
+ end
129
+ end
130
+ znj = Array.new(dim_neutrals) { Array.new(dim_phase) } # z[n][j]
131
+ for i in 0..dim_neutrals - 1
132
+ for j in 0..dim_phase - 1
133
+ j < dim_phase
134
+ znj[i][j] = primitive_impedance_matrix[i + neutrals][j]
135
+ end
136
+ end
137
+
138
+ half = Matrix[*znn].inverse * Matrix[*znj] # first step to obtain the first matrix
139
+ seq = Matrix[*zij] - (Matrix[*zin] * half)
140
+ return seq
141
+ end
142
+
143
+ # methods to apply the modified Carson equation to either the diagonal components of the primitive impedance matrix or to the other components
144
+ def carson_equation_self(ri, gmri)
145
+ # Carson's equation for self impedance
146
+ return Complex(ri + 0.0953, 0.12134 * (Math.log(1.0 / gmri) + 7.93402)) # GMR e il geometric mean radius del conduttore
147
+ end
148
+
149
+ def carson_equation(dij)
150
+ # """Carson's equation for mutual impedance
151
+ if dij != 0
152
+ return Complex(0.09530, 0.12134 * (Math.log(1.0 / dij) + 7.93402))
153
+ end
154
+ end
155
+
156
+ # method applying a similar concept of the Carson equation, but for obtaining the primitive potential coeff matrix
157
+ # for obtaining the lines capacitance
158
+ def get_primitive_potential_coeff_matrix(diamaters, images_matrix, dist_matrix)
159
+ n_rows = images_matrix.length
160
+ n_cols = images_matrix.length
161
+ primitive_potential_coeff_matrix = Array.new(n_rows) { Array.new(n_rows) }
162
+ for i in 0..n_rows - 1
163
+ for j in 0..n_cols - 1
164
+ if i == j
165
+ primitive_potential_coeff_matrix[i][j] = 11.17689 * Math.log(images_matrix[i][j] / (diamaters[i] / 2)) # assuming relative permittivity of air of 1.4240 x 10 mF/mile
166
+ else
167
+ primitive_potential_coeff_matrix[i][j] = 11.17689 * Math.log(images_matrix[i][j] / dist_matrix[i][j]) # assuming relative permittivity of air of 1.4240 x 10 mF/mile
168
+ end
169
+ end
170
+ end
171
+ return primitive_potential_coeff_matrix
172
+ end
173
+
174
+ # method for obtaining the primitive impedance matrix
175
+ def get_primitive_impedance_matrix(dist_matrix, gmr_list, r_list)
176
+ '''Get primitive impedance matrix from distance matrix between the wires, GMR list, and resistance list.'''
177
+ n_rows = dist_matrix.length
178
+ n_cols = dist_matrix.length
179
+ primitive_impedance_matrix = Array.new(n_rows) { Array.new(n_rows) }
180
+ for i in 0..n_rows - 1
181
+ for j in 0..n_cols - 1
182
+ if i == j
183
+ primitive_impedance_matrix[i][j] = carson_equation_self(r_list[i], gmr_list[i])
184
+ else
185
+ primitive_impedance_matrix[i][j] = carson_equation(dist_matrix[i][j])
186
+ end
187
+ end
188
+ end
189
+ return primitive_impedance_matrix
190
+ end
191
+
192
+ # method to obtain the lines capacitance
193
+ def get_capacitance(wire_list)
194
+ nphases = wire_list.height.length
195
+ conc_neutrals = wire_list.r_neutral.length
196
+ distance_matrix = Array.new(nphases) { Array.new(nphases) }
197
+ distance_matrix_feet = Array.new(nphases) { Array.new(nphases) }
198
+ image_matrix = Array.new(nphases) { Array.new(nphases) }
199
+ image_matrix_ft = Array.new(nphases) { Array.new(nphases) }
200
+ diameters_conductors_ft = []
201
+ w = 2 * Math::PI * 60
202
+ for i in 0..nphases - 1
203
+ diameters_conductors_ft[i] = si_to_imperial_units(wire_list.diameter[i], 'mm', 'ft')
204
+ for j in 0..nphases - 1
205
+ distance_matrix[i][j] = (((wire_list.x[i] - wire_list.x[j])**2 + (wire_list.height[i] - wire_list.height[j])**2)**0.5).to_f.round(5) # ((i - j).abs() * 0.6) #60cm apart one above the other or side by side
206
+ image_matrix[i][j] = (((wire_list.x[i] - wire_list.x[j])**2 + (wire_list.height[i] - -wire_list.height[j])**2)**0.5).to_f.round(5) # computing matrix with distances among images of the cables referred to the ground
207
+ distance_matrix_feet[i][j] = si_to_imperial_units(distance_matrix[i][j], 'm', 'ft')
208
+ image_matrix_ft[i][j] = si_to_imperial_units(image_matrix[i][j], 'm', 'ft')
209
+ end
210
+ end
211
+ if conc_neutrals == 0 # meaning that we are NOT considering concentric neutrals
212
+ primitive_potential_coeff = get_primitive_potential_coeff_matrix(diameters_conductors_ft, image_matrix_ft, distance_matrix_feet)
213
+ if primitive_potential_coeff.length != 3
214
+ reduced_potential_coeff = kron_reduction(primitive_potential_coeff, conc_neutrals)
215
+ else
216
+ reduced_potential_coeff = Matrix[*primitive_potential_coeff]
217
+ end
218
+ capacitance_matrix = reduced_potential_coeff.inverse # obtaining the capacitance matrix in micro Farad
219
+ for i in 0..capacitance_matrix.column_size - 1
220
+ for j in 0..capacitance_matrix.column_size - 1
221
+ capacitance_matrix[i, j] = Complex(0, capacitance_matrix[i, j] * w)
222
+ end
223
+ end
224
+ if capacitance_matrix.column_size > 1
225
+ seq_capacitance = Array.new(3) { Array.new(3) }
226
+ seq_admittance_ft = get_sequence_impedance_matrix(capacitance_matrix)
227
+ for i in 0..seq_admittance_ft.column_size - 1
228
+ for j in 0..seq_admittance_ft.column_size - 1
229
+ seq_capacitance[i][j] = imperial_to_si_units((seq_admittance_ft[i, j] / w) * 1000, '1/mi', '1/km') # to be provided in nF
230
+ end
231
+ end
232
+ capacitance = { 'Capacitance(nF/km)' => seq_capacitance[1][1].imag, 'C0 (nF/km)' => seq_capacitance[0][0].imag }
233
+ else
234
+ seq_capacitance = imperial_to_si_units((capacitance_matrix[0, 0] / w) * 1000, '1/mi', '1/km')
235
+ capacitance = { 'Capacitance(nF/km)' => seq_capacitance.imag, 'C0 (nF/km)' => seq_capacitance.imag }
236
+ end
237
+ else # now computing the capacitance for UG concentric neutral power lines
238
+ material_permettivity = 2.3 # assuming using the minimum permittivity value for "cross-linked polyethlyene", as the insulation material
239
+ free_space_permittivity = 0.0142 # in microfaraday/mile
240
+ radius = (wire_list.outside_diamater_neutral[0] - wire_list.diameter_n_strand[0]) / 2 # in mm
241
+ radius_ft = si_to_imperial_units(radius, 'mm', 'ft')
242
+ radius_neutral_ft = si_to_imperial_units(wire_list.diameter_n_strand[0] / 2, 'mm', 'ft')
243
+ radius_conductor_ft = diameters_conductors_ft[0] / 2
244
+ numerator = 2 * Math::PI * material_permettivity * free_space_permittivity
245
+ denominator = (Math.log(radius_ft / radius_conductor_ft) - ((1 / wire_list.neutral_strands[0]) * Math.log((wire_list.neutral_strands[0] * radius_neutral_ft) / radius_ft)))
246
+ (numerator / denominator) * 1000
247
+ seq_capacitance = imperial_to_si_units((numerator / denominator) * 1000, '1/mi', '1/km')
248
+ capacitance = { 'Capacitance(nF/km)' => seq_capacitance, 'C0 (nF/km)' => seq_capacitance }
249
+ end
250
+ return capacitance
251
+ end
252
+
253
+ # method to obtain the line sequence impedances
254
+ def get_sequence_impedances(wire_list)
255
+ '''Get sequence impedances Z0, Z+, Z- from distance matrix between the wires, GMR list, and resistance list.'''
256
+ nphases = wire_list.height.length
257
+ conc_neutrals = wire_list.r_neutral.length
258
+ distance_matrix = Array.new(nphases + conc_neutrals) { Array.new(nphases + conc_neutrals) }
259
+ distance_matrix_feet = Array.new(nphases + conc_neutrals) { Array.new(nphases + conc_neutrals) }
260
+ gmr_ft = []
261
+ resistance_mi = []
262
+ gmr_neutral = []
263
+ for i in 0..nphases - 1
264
+ for j in 0..nphases - 1
265
+ distance_matrix[i][j] = (((wire_list.x[i] - wire_list.x[j])**2 + (wire_list.height[i] - wire_list.height[j])**2)**0.5).to_f.round(5) # ((i - j).abs() * 0.6) #60cm apart one above the other or side by side
266
+ end
267
+ end
268
+ if !wire_list.r_neutral[0].nil? # computing parameters for concentric neutrals wires
269
+ for j in 0..conc_neutrals - 1
270
+ radius = (wire_list.outside_diamater_neutral[j] - wire_list.diameter_n_strand[j]) / 2
271
+ wire_list.gmr.push(wire_list.gmr_neutral[j] * wire_list.neutral_strands[j] * (radius**(wire_list.neutral_strands[j] - 1))**(1 / wire_list.neutral_strands[j]))
272
+ wire_list.r.push(wire_list.r_neutral[j] / wire_list.neutral_strands[j])
273
+ for k in 0..conc_neutrals - 1
274
+ distance_matrix[conc_neutrals + j][conc_neutrals + k] = distance_matrix[j][k]
275
+ if j == k
276
+ distance_matrix[j + conc_neutrals][k] = radius / 1000 # converting from mm to m
277
+ distance_matrix[j][k + conc_neutrals] = radius / 1000
278
+ # As per Example 4.2 of Kersting. phase-neutral = phase-phase distance for different cables
279
+ else
280
+ distance_matrix[j + conc_neutrals][k] = distance_matrix[j][k]
281
+ distance_matrix[j][k + conc_neutrals] = distance_matrix[j][k]
282
+ end
283
+ end
284
+ end
285
+ end
286
+ for i in 0..distance_matrix.length - 1
287
+ gmr_ft[i] = si_to_imperial_units(wire_list.gmr[i], 'mm', 'ft') # in ft
288
+ resistance_mi[i] = si_to_imperial_units(wire_list.r[i], '1/km', '1/mi') # in miles verify if it was provided in miles
289
+ for j in 0..distance_matrix.length - 1
290
+ distance_matrix_feet[i][j] = si_to_imperial_units(distance_matrix[i][j], 'm', 'ft')
291
+ end
292
+ end
293
+ primitive = get_primitive_impedance_matrix(distance_matrix_feet, gmr_ft, resistance_mi)
294
+ if primitive.length != 3 # not for V-phase lines, I keep these lines as a 3x3 Matrix, without executing Kron Reduction
295
+ phase = kron_reduction(primitive, conc_neutrals) # passing number of concentric neutrals if any
296
+ else
297
+ phase = Matrix[*primitive] # still treated as a 3x3 Matrix
298
+ end
299
+ if phase.column_size != 1 # if single-phase lines the sequence impedance value is already obtained
300
+ seq_new = Array.new(3) { Array.new(3) }
301
+ seq = get_sequence_impedance_matrix(phase)
302
+ for i in 0..seq.column_size - 1
303
+ for j in 0..seq.column_size - 1
304
+ seq_new[i][j] = imperial_to_si_units(seq[i, j], '1/mi', '1/km')
305
+ end
306
+ end
307
+ impedances = { 'Resistance(ohms/km)' => seq_new[1][1].real, 'Ind. Reactance(ohms/km)' => seq_new[1][1].imag, 'R0 (ohms/km)' => seq_new[0][0].real, 'X0 (ohms/km)' => seq_new[0][0].imag }
308
+ else
309
+ seq_new = imperial_to_si_units(phase[0, 0], '1/mi', '1/km')
310
+ impedances = { 'Resistance(ohms/km)' => seq_new.real, 'Ind. Reactance(ohms/km)' => seq_new.imag, 'R0 (ohms/km)' => seq_new.real, 'X0 (ohms/km)' => seq_new.imag }
311
+ end
312
+ return impedances
313
+ end
314
+
315
+ # method that starting from each line geometry finds the parameters of each wire forming that power line
316
+ def creation(wires)
317
+ seq_impedances = []
318
+ wire_list = []
319
+ hash = {}
320
+ jj = 0
321
+ wire_list = URBANopt::RNM::WiresExtendedCatalog.new
322
+ # puts wire_list
323
+ for j in 0..@power_line['Line geometry'].length - 1
324
+ for k in 0..wires['WIRES CATALOG'].length - 1
325
+ if @power_line['Line geometry'][j]['wire'] == wires['WIRES CATALOG'][k]['nameclass']
326
+ wire_list.name.push(wires['WIRES CATALOG'][k]['nameclass'])
327
+ wire_list.diameter.push(wires['WIRES CATALOG'][k]['diameter (mm)'])
328
+ wire_list.r.push(wires['WIRES CATALOG'][k]['resistance (ohm/km)'])
329
+ wire_list.gmr.push(wires['WIRES CATALOG'][k]['gmr (mm)'])
330
+ wire_list.ampacity.push(wires['WIRES CATALOG'][k]['ampacity (A)'])
331
+ wire_list.type.push(wires['WIRES CATALOG'][k]['type'])
332
+ if wires['WIRES CATALOG'][k].include? 'resistance neutral (ohm/km)'
333
+ wire_list.r_neutral.push(wires['WIRES CATALOG'][k]['resistance neutral (ohm/km)'])
334
+ wire_list.gmr_neutral.push(wires['WIRES CATALOG'][k]['gmr neutral (mm)'])
335
+ wire_list.neutral_strands.push(wires['WIRES CATALOG'][k]['# concentric neutral strands'])
336
+ wire_list.diameter_n_strand.push(wires['WIRES CATALOG'][k]['concentric diameter neutral strand (mm)'])
337
+ wire_list.outside_diamater_neutral.push(wires['WIRES CATALOG'][k]['concentric neutral outside diameter (mm)'])
338
+ end
339
+ end
340
+ end
341
+ if @power_line['Line geometry'][j]['phase'] != 'N' # && @power_line["Current(A) "] != nil
342
+ line_current = wire_list.ampacity[j]
343
+ end
344
+ wire_list.x.push(@power_line['Line geometry'][j]['x (m)'])
345
+ wire_list.height.push(@power_line['Line geometry'][j]['height (m)'])
346
+ end
347
+ capacitances = get_capacitance(wire_list)
348
+ impedances = get_sequence_impedances(wire_list)
349
+ # electric_parameters = impedances.merge(capacitances)
350
+ @power_line.delete('Line geometry')
351
+ cont = 0
352
+ pair = []
353
+ key = 0
354
+ # organizing the impedances and capacitance values found to be placed in the right order in the RNM-US catalog
355
+ impedances.each do |k, v| # place the new fields in the right positions
356
+ pair[cont] = { k => v }
357
+ if cont < 2
358
+ field =
359
+ if cont == 0
360
+ insert_field('Nphases', pair[cont], :after)
361
+ else
362
+ insert_field(key, pair[cont], :after)
363
+ end
364
+ key = k
365
+ else
366
+ if cont == 2
367
+ insert_field(key, { 'Current(A)' => line_current }, :after)
368
+ insert_field('Repair time maximum (hours)', pair[cont], :after)
369
+ else
370
+ insert_field(key, pair[cont], :after)
371
+ end
372
+ key = k
373
+ end
374
+ cont += 1
375
+ end
376
+ cont = 0
377
+ capacitances.each do |k, v|
378
+ if cont == 0
379
+ insert_field('Ind. Reactance(ohms/km)', { k => v }, :after)
380
+ else
381
+ insert_field('X0 (ohms/km)', { k => v }, :after)
382
+ end
383
+ cont += 1
384
+ end
385
+ return @power_line
386
+ end
387
+ end
388
+ end
389
+ end
@@ -0,0 +1,255 @@
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
+ # creating a class that creates the consumers input required by the RNM-US model,
42
+ # according to their geographic location, energy consumption and peak demand, and power consumption profiles
43
+ require 'json'
44
+ require 'csv'
45
+ module URBANopt
46
+ module RNM
47
+ class Consumers
48
+ attr_accessor :customers, :customers_ext, :profile_customer_p, :profile_customer_q, :profile_customer_p_ext, :profile_customer_q_ext, :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
+ @power_factor = power_factor
63
+ @lv_limit = lv_limit
64
+ end
65
+
66
+ # creating a method to process each building electricity consumption
67
+ # the method receives as argument the required data obtained from each feature csv and json urbanopt output files
68
+ # and returns the customer_ext array for each feature, with the required customer data needed for RNM-US
69
+ # and the profiles consumers files
70
+
71
+ # the method is divided in 2 part, the first is run in case the user uses the "only LV" option to run the network,
72
+ # defining a certain numb of nodes for each building
73
+ # while the 2nd option is run in case "only LV" set to false and the consumption for each building will be placed in a single node
74
+ def construct_consumer(profiles, single_values, building_map, building_nodes, height, users, folder)
75
+ if @only_lv_consumers
76
+ planning_profile_node_active = []
77
+ planning_profile_node_reactive = []
78
+ yearly_profile_node_active = []
79
+ yearly_profile_node_reactive = []
80
+ nodes_per_bldg, area, medium_voltage = av_peak_cons_per_building_type(folder['building_types'])
81
+ # the default variables are defined (i.e. type and rurality type)
82
+ closest_node = building_map[3].split('_')[1].to_i # refers to the node, found in the class above
83
+ node = closest_node
84
+ cont = 1
85
+ cont_reverse = 1
86
+ for i in 1..nodes_per_bldg
87
+ coordinates = building_map
88
+ node = closest_node + cont # to set the new nodes with enough distance among each others
89
+ node_reverse = closest_node - cont_reverse
90
+ if i > 1 && node <= building_nodes.length - 1
91
+ coordinates = building_nodes[node] # take the closest building node index to the street and pass the nodes after it
92
+ cont += 1
93
+ elsif i > 1
94
+ coordinates = building_nodes[node_reverse]
95
+ cont_reverse += 1
96
+ end
97
+ # creating the lists for the customers text files required by the model
98
+ id = coordinates[3]
99
+ peak_active_power_cons = ((single_values[:peak_active_power_cons]) / nodes_per_bldg).round(2)
100
+ peak_reactive_power_cons = ((single_values[:peak_reactive_power_cons]) / nodes_per_bldg).round(2)
101
+ # introducing this for consistency
102
+ if medium_voltage
103
+ voltage_default = 12.47
104
+ phases = 3
105
+ else
106
+ voltage_default, phases = voltage_values(peak_active_power_cons / @power_factor)
107
+ end
108
+
109
+ for k in 0..profiles[:planning_profile_cust_active].length - 1
110
+ planning_profile_node_active[k] = ((profiles[:planning_profile_cust_active][k]) / nodes_per_bldg).round(2)
111
+ planning_profile_node_reactive[k] = ((profiles[:planning_profile_cust_reactive][k]) / nodes_per_bldg).round(2)
112
+ end
113
+ for k in 0..profiles[:yearly_profile_cust_active].length - 1
114
+ yearly_profile_node_active[k] = ((profiles[:yearly_profile_cust_active][k]) / nodes_per_bldg).round(2)
115
+ yearly_profile_node_reactive[k] = ((profiles[:yearly_profile_cust_reactive][k]) / nodes_per_bldg).round(2)
116
+ end
117
+ @customers.push([coordinates, voltage_default, peak_active_power_cons, peak_reactive_power_cons, phases])
118
+ @customers_ext.push([coordinates, voltage_default, peak_active_power_cons, peak_reactive_power_cons, phases, area, height, (single_values[:energy] / nodes_per_bldg).round(2), peak_active_power_cons, peak_reactive_power_cons, users])
119
+ @profile_customer_q.push([id, 24, planning_profile_node_reactive])
120
+ @profile_customer_p.push([id, 24, planning_profile_node_active])
121
+ @profile_customer_p_ext.push([id, 8760, yearly_profile_node_active])
122
+ @profile_customer_q_ext.push([id, 8760, yearly_profile_node_reactive])
123
+
124
+ end
125
+ # 2nd option run in case the building consumption is represented by a single node
126
+ else
127
+ id = building_map[3]
128
+ # this key seems to change between floor_area or floor_area_ft
129
+ area = folder.key?('floor_area') ? (folder['floor_area']).round(2) : (folder['floor_area_sqft']).round(2)
130
+ voltage_default, phases = voltage_values(single_values[:peak_active_power_cons] / @power_factor * 0.9) # applying safety factor
131
+ @customers.push([building_map, voltage_default, single_values[:peak_active_power_cons], single_values[:peak_reactive_power_cons], phases])
132
+ @customers_ext.push([building_map, 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])
133
+ @profile_customer_q.push([id, 24, profiles[:planning_profile_cust_reactive]])
134
+ @profile_customer_p.push([id, 24, profiles[:planning_profile_cust_active]])
135
+ @profile_customer_p_ext.push([id, 8760, profiles[:yearly_profile_cust_active]])
136
+ @profile_customer_q_ext.push([id, 8760, profiles[:yearly_profile_cust_reactive]])
137
+ end
138
+ end
139
+
140
+ # creating a function that for each node defines the connection (e.g LV, MV, single-phase, 3-phase)
141
+ # according to the catalog limits previously calculated
142
+ def voltage_values(peak_apparent_power)
143
+ case peak_apparent_power
144
+ when 0..@lv_limit[:single_phase] # set by the catalog limits
145
+ phases = 1
146
+ voltage_default = 0.416
147
+ when @lv_limit[:single_phase]..@lv_limit[:three_phase] # defined from the catalog (from the wires)
148
+ phases = 3
149
+ voltage_default = 0.416
150
+ # MV and 3 phases untill 16 MVA, defined by the SMART-DS project
151
+ when @lv_limit[:three_phase]..16000
152
+ phases = 3
153
+ voltage_default = 12.47
154
+ else
155
+ # HV and 3 phases for over 16 MVA
156
+ phases = 3
157
+ voltage_default = 69
158
+ end
159
+ return voltage_default, phases
160
+ end
161
+
162
+ # creating a method to define the number of nodes for each building in case the user set the option "only LV" to true.
163
+ # this method calculates the number of nodes for each building in the project and in case the numb of nodes is higher than 4
164
+ # than the building is considered as a single node connected in MV
165
+ # 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
166
+ def av_peak_cons_per_building_type(feature_file)
167
+ average_peak_by_size = []
168
+ floor_area = []
169
+ average_peak = 5 # defining a random value first, since now the residential buildings are not considered in the catalog
170
+ mixed_use_av_peak = 0
171
+ area_mixed_use = 0
172
+ medium_voltage = false
173
+ # defining a conservative factor which creates some margin with the number of nodes found using the av_peak catalog, with the
174
+ # actual nodes that could be found with the current buildings peak consumptions in the project
175
+ conservative_factor = 0.8 # considered as a reasonable assumption, but this value could be changed
176
+ average_peak_folder = JSON.parse(File.read(@average_building_peak_catalog_path))
177
+ for i in 0..feature_file.length - 1
178
+ area = (feature_file[i]['floor_area']).round(2)
179
+ building_type = feature_file[i]['building_type'] # it specifies the type of building, sometimes it is directly the sub-type
180
+ counter = 0 # counter to find number of buildings type belonging to same "category"
181
+ average_peak_folder.each do |building_class|
182
+ if building_type == building_class['building type'] || building_type == building_class['sub-type']
183
+ 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
184
+ average_peak_by_size[counter] = average_peak
185
+ floor_area[counter] = (building_class['floor_area (ft2)'] - area).abs # minimum difference among area and area from the prototypes defined by DOE
186
+ counter += 1
187
+ # in this way I don t consider residential and I assume it's average_peak = 0, it is reasonable because we assume always 1 node per RES consumers single-detached family houses
188
+ end
189
+ end
190
+ if counter > 1
191
+ index = floor_area.index(floor_area.min)
192
+ average_peak = average_peak_by_size[index]
193
+ end
194
+ if feature_file.length > 1 # defined for Mixed_use buildings, which include more building types
195
+ mixed_use_av_peak += average_peak
196
+ area_mixed_use += area
197
+ end
198
+ end
199
+ if feature_file.length > 1
200
+ average_peak = mixed_use_av_peak # average peak per mixed use considering the building types which are in this building
201
+ area = area_mixed_use
202
+ end
203
+ nodes_per_bldg = (average_peak / (@lv_limit[:three_phase] * @power_factor * conservative_factor)).to_f.ceil # computing number of nodes per building
204
+ if nodes_per_bldg > @max_num_lv_nodes # to define this as an input in the geojson file
205
+ nodes_per_bldg = 1
206
+ medium_voltage = true
207
+ end
208
+ return nodes_per_bldg, area, medium_voltage
209
+ end
210
+
211
+ # defining a method for the customers files creation:
212
+ # obtaining all the needed input from each feature_report.csv file (active & apparent power and tot energy consumed)
213
+ # and from each feature_report.json file (area, height, number of users)
214
+ # the method passes as arguments the urbanopt json and csv output file for each feature and the building coordinates previously calculated
215
+ # and the "extreme" hour used to plan the network
216
+ def customer_files_load(csv_feature_report, json_feature_report, building_map, building_nodes, hour)
217
+ profiles = Hash.new { |h, k| h[k] = [] }
218
+ single_values = Hash.new(0)
219
+ hours = 23
220
+ feature_type = json_feature_report['program']['building_types'][0]['building_type']
221
+ residential_building_types = 'Single-Family Detached' # add the other types
222
+ # 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
223
+ if residential_building_types.include? feature_type
224
+ profile_start_max = hour.hour_index_max_res - hour.peak_hour_max_res
225
+ else
226
+ profile_start_max = hour.hour_index_max_comm - hour.peak_hour_max_comm
227
+ end
228
+ k = 0 # index for each hour of the year represented in the csv file
229
+ i = 0 # to represent the 24 hours of a day
230
+ # content = CSV.foreach(csv_feature_report, headers: true) do |power|
231
+ CSV.foreach(csv_feature_report, headers: true) do |power|
232
+ @power_factor = power['Electricity:Facility Power(kW)'].to_f / power['Electricity:Facility Apparent Power(kVA)'].to_f
233
+ profiles[:yearly_profile_cust_active].push(power['Electricity:Facility Power(kW)'].to_f)
234
+ profiles[:yearly_profile_cust_reactive].push(profiles[:yearly_profile_cust_active][k] * Math.tan(Math.acos(@power_factor)))
235
+ single_values[:energy] += power['REopt:Electricity:Load:Total(kw)'].to_f # calculating the yearly energy consumed by each feature
236
+ if k >= profile_start_max && k <= profile_start_max + hours
237
+ profiles[:planning_profile_cust_active].push(power['Electricity:Facility Power(kW)'].to_f)
238
+ if power['Electricity:Facility Power(kW)'].to_f > single_values[:peak_active_power_cons]
239
+ single_values[:peak_active_power_cons] = power['Electricity:Facility Power(kW)'].to_f
240
+ single_values[:peak_reactive_power_cons] = single_values[:peak_active_power_cons] * Math.tan(Math.acos(@power_factor))
241
+ end
242
+ profiles[:planning_profile_cust_reactive][i] = profiles[:planning_profile_cust_active][i] * Math.tan(Math.acos(@power_factor))
243
+ i += 1
244
+ end
245
+ k += 1
246
+ end
247
+ # parsing the required information from feature.json file
248
+ # folder = JSON.parse(json_feature_report)
249
+ height = (json_feature_report['program']['maximum_roof_height_ft']).round(2) # here depends on the feature version
250
+ users = json_feature_report['program']['number_of_residential_units']
251
+ construct_consumer(profiles, single_values, building_map, building_nodes, height, users, json_feature_report['program'])
252
+ end
253
+ end
254
+ end
255
+ end