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