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,152 @@
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
+ module URBANopt
43
+ module RNM
44
+ # class created to convert the extended catalog into the OpenDSS catalog to be used by the OpenDSS Gem
45
+ class ConversionToOpendssCatalog
46
+ attr_accessor :hash
47
+
48
+ def initialize(extended_catalog_path)
49
+ @extended_catalog_path = extended_catalog_path
50
+ @hash = hash
51
+ end
52
+
53
+ # method to convert initial SI units in the ext catalog into Imperial units used by the Carson equation
54
+ def convert_to_imperial_units(hash)
55
+ hash_new = {}
56
+ hash.each do |k, v|
57
+ if k.include? '(mm)'
58
+ mm_ft = 0.00328
59
+ hash_new[k] = (v * mm_ft).round(5)
60
+ elsif k.include? '(ohm/km)'
61
+ km_miles = 0.6214
62
+ hash_new[k] = (v / km_miles).round(2)
63
+ elsif k.include? '(m)'
64
+ m_ft = 3.281
65
+ hash_new[k] = (v * m_ft).round(2)
66
+ else
67
+ hash_new[k] = hash[k]
68
+ end
69
+ end
70
+ return hash_new
71
+ end
72
+
73
+ def create_catalog(save_path)
74
+ @hash = {}
75
+ component = []
76
+ catalog = JSON.parse(File.read(@extended_catalog_path))
77
+ i = 0
78
+ z = 0
79
+
80
+ catalog.each do |key, value|
81
+ case key
82
+ when 'SUBSTATIONS AND DISTRIBUTION TRANSFORMERS'
83
+ transformers = URBANopt::RNM::ProcessorOpendss.new
84
+ component = []
85
+ for ii in 0..catalog[key].length - 1 # assessing each type of transformer (Urban, Inter)
86
+ catalog[key][ii].each do |k, v|
87
+ # calling a method to verify that the same transformers are not repeated in the OpenDSS catalog
88
+ transformers.process_data(catalog[key][ii][k])
89
+ end
90
+ end
91
+ for i in 0..transformers.list.length - 1
92
+ trafo = URBANopt::RNM::Transformers.new
93
+ component.push(trafo.create(transformers.list[i]))
94
+ end
95
+ @hash['transformers_properties'] = component
96
+ when 'CAPACITORS'
97
+ capacitors = URBANopt::RNM::ProcessorOpendss.new
98
+ component = []
99
+ catalog[key].each do |k, v|
100
+ # calling a method to verify that the same capacitors are not repeated in the OpenDSS catalog
101
+ capacitors.process_data(catalog[key][k])
102
+ for i in 0..capacitors.list.length - 1
103
+ capacitor = URBANopt::RNM::Capacitor.new
104
+ component.push(capacitor.create(capacitors.list[i]))
105
+ end
106
+ end
107
+ @hash['capacitor_properties'] = component
108
+ when 'LINES'
109
+ @conductors = URBANopt::RNM::ProcessorOpendss.new
110
+ component = []
111
+ for ii in 1..catalog[key].length - 1
112
+ catalog[key][ii].each do |k, v| # assessing if interurban section, urban section, etc.
113
+ for jj in 0..catalog[key][ii][k].length - 1 # assessing each power line
114
+ catalog[key][ii][k][jj].each do |attribute, values|
115
+ if attribute == 'Line geometry'
116
+ # calling a method to verify that the same lines are not repeated in the OpenDSS catalog
117
+ @conductors.process_data(catalog[key][ii][k][jj][attribute])
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+
126
+ key = 'WIRES'
127
+ component = []
128
+ updated_value = 0
129
+ i = 0 # counter for all the lines in the OpenDSS catalog
130
+ catalog[key].each do |k, v|
131
+ for jj in 0..@conductors.cont - 1
132
+ for ii in 0..catalog[key][k].length - 1
133
+ if @conductors.list[jj]['wire'] == catalog[key][k][ii]['nameclass']
134
+ @conductors.list[jj] = convert_to_imperial_units(@conductors.list[jj])
135
+ updated_value = convert_to_imperial_units(catalog[key][k][ii])
136
+ wire = URBANopt::RNM::WiresOpendss.new
137
+ component.push(wire.create(@conductors.list[jj], updated_value))
138
+ i += 1
139
+ end
140
+ end
141
+ end
142
+ end
143
+ @hash['wires'] = component
144
+
145
+ # save to save_path
146
+ File.open(save_path, 'w') do |f|
147
+ f.write(JSON.pretty_generate(@hash))
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,261 @@
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 'geoutm'
42
+ require 'json'
43
+
44
+ module URBANopt
45
+ module RNM
46
+ # creating a class to process parse and process the geographic information for buildings, streets and substations
47
+ class GeojsonInput
48
+ # Set constants
49
+ UG_RATIO_DEFAULT = 0.9
50
+ ONLY_LV_CONSUMERS_DEFAULT = true
51
+ MAX_LV_NODES_DEFAULT = 1
52
+
53
+ # defining a method to set each street nodes to a uniform distance among eachothers, valid for both streets and buildings
54
+ # the method is passing as arguments the hash with each feature info from the geojson file, the latitude and longitude to be converted to UTM,
55
+ # the array containing the already processed nodes, the index defining the position of the lat and lon passed in this method
56
+ # and the index defining the reached position in the array with the processed nodes
57
+ # this method returns the array with the processed nodes and its index
58
+ def coordinates(hash, lat, lon, coordinates, k, i)
59
+ lat_lon = GeoUtm::LatLon.new(lat, lon)
60
+ z = 0 # default value for surface elevation
61
+ uniform_distance = 15 # values set as uniform distance among nodes
62
+ utm = lat_lon.to_utm # converting latitude and longitude to UTM
63
+ x_utm = utm.e.round(2) # UTM x-distance from the origin
64
+ y_utm = utm.n.round(2) # UTM y-distance from the origin
65
+ identifier = hash['properties']['id']
66
+ # creating streetmap nodes every 10 m for each road, considering the angle of each road
67
+ if k != 0
68
+ distance_y = y_utm - coordinates[i - 1][1]
69
+ distance_x = x_utm - coordinates[i - 1][0]
70
+ distance = (distance_x**2 + distance_y**2)**0.5
71
+ intervals = (distance / uniform_distance).to_i
72
+ # creating variables for x, y for each node, with the right street inclination
73
+ x_uniform = uniform_distance * (distance_x / distance)
74
+ y_uniform = uniform_distance * (distance_y / distance)
75
+ n = 1 # counter to keep track when the number of intervals for each "distnce" is reached
76
+ # creating nodes in the coordinates array with the right street inclination and uniform distance among each others
77
+ while n <= intervals
78
+ id = identifier.to_s + "_#{i}"
79
+ coordinates[i] = (coordinates[i - 1][0] + x_uniform).round(2), (coordinates[i - 1][1] + y_uniform).round(2), z, id
80
+ i += 1
81
+ n += 1
82
+ end
83
+ # when the last interval of each road is reached, the last node values are given as the streets coordinates
84
+ # imported from the street.json file
85
+ id = identifier.to_s + "_#{i}"
86
+ coordinates[i] = x_utm.round(2), y_utm.round(2), z, id
87
+ i += 1
88
+ else
89
+ # in the 1st node of each road, the coordinates are tacken directly from the streetmap.json file
90
+ id = identifier.to_s + "_#{i}"
91
+ coordinates[i] = x_utm, y_utm, z, id
92
+ i += 1
93
+ end
94
+ return coordinates, i
95
+ end
96
+
97
+ # defining a method to find the coordinates of the closest node of each building building to the closest street, to be used for the customers_ext.txt file
98
+ # the method receives as arguments every nodes of 1 building and the array containing all the street nodes computed before
99
+ # and it returns the coordinates and id of the closest node of the building to the street
100
+ ## The new algorithm developed calculates an approximate distance: (x+y)/2, of each building-node with each street-node and compares it with the "minimum_distance"
101
+ # this approximate distance has been defined in order to be able to disregard all the distances which are greater than the "minimum distance" computed until that moment, without being required to compute the Pithagorean Theorem, which requires a long computational time.
102
+ # Therefore (x+y)/2 has been computed knowing that: if the minimum length of the hypothenuse of a right triangle is when the triangle is isosceles so when the hyphothenuse (d) is equal to d = sqrt(2)*x (where x is the distance among the nodes on the x-axis),
103
+ # so we can assume that x = (x+y)/2, than if d = sqrt(2)*((x+y)/2) > (x+y)/2 > minimum_distance
104
+ # than it confirmes that x and y can be disregarded and there is no need to compute the real distance for that node since the approximate distance value (which represents the minimum possible distance for the sum of those catheti)
105
+ # is greater than the minimum_distance that it is been compared with.
106
+ # This process it is iterated for all the distances among the building-nodes with the street-nodes until an approximate distance (x+y)/2 is lower than the minimum distance computed until that moment
107
+ # and in that case the real distance with the Pythagorean Theorem is computed and compared with the minimum distance.
108
+ def consumer_coordinates(building, street)
109
+ dist_min = 5000 # assuming a first fitticious minimum distance that will be replaced later on by the real minimum distance
110
+ # iterating the distance among each node of each street and each node of each building until the minimum distance is found
111
+ for j in 0..building.length - 1 # assessing each building node of the considered building
112
+ for i in 0..street.length - 1 # assessing each street node
113
+ y = building[j][1] - street[i][1] # calculating the distance on the y-axis
114
+ x = building[j][0] - street[i][0] # calculating the distance on the x-axis
115
+ distance_approx = (x + y) / 2 # finding the "approximate" distance of each building node with each street node, in order to reduce computational time (considering that if the sum of the 2 cathets divided by 2 is lower than the minimum distance, than the real distance of this building node to the closest street-node will be further processed to see if it can be a "candidate" for the minimum distance)
116
+ if distance_approx < dist_min # if the the new distance found is lower than the minimum one than the real distance considering this building-node and this street-node will be computed
117
+ distance = (x**2 + y**2)**0.5 # the real distance between the building node and the street node is computed
118
+ if distance < dist_min # if the new distance is lower than the minimum distance found until that moment, than this new "distance" value will be set as the minimum distance between the building node and the street node
119
+ dist_min = distance
120
+ chosen_coord = building[j] # assigning the node coordinates values and id of the building with the minimium distance to the street to chose_coord variable
121
+ end
122
+ end
123
+ end
124
+ end
125
+ return chosen_coord
126
+ end
127
+
128
+ # defining a method for parsing the coordinates of the substations to be passed in the RNM-US model
129
+ # choose the closes coord to the street or the one in the midle of the polygon since the sub is far away from district and streets
130
+ def sub_coordinates(points_coord, id)
131
+ x_utm = []
132
+ y_utm = []
133
+ for i in 0..points_coord.length - 1
134
+ lat_lon = GeoUtm::LatLon.new(points_coord[i][1], points_coord[i][0])
135
+ utm = lat_lon.to_utm # converting latitude and longitude to UTM
136
+ x_utm[i] = utm.e.round(2) # UTM x-distance from the origin
137
+ y_utm[i] = utm.n.round(2) # UTM y-distance from the origin
138
+ end
139
+ coord_sub = [(x_utm[0] + x_utm[2]) / 2, (y_utm[0] + y_utm[2]) / 2, 0, "sub_#{id}"]
140
+ return coord_sub
141
+ end
142
+
143
+ # creating a method passing the GeoJSON file from URBANopt as the argument to define options that can be modified by the user
144
+ # streets and building and primary substation coordinates
145
+ # and returning the street coordinates array, the building coordinates array and the tot number of buildings in the project
146
+ def coordinates_feature_hash(geojson_hash,scenario_features=[])
147
+ i = 0 # index representing the number of street_nodes
148
+ building_number = 0 # variable created to keep track the number of buildings in the project
149
+ street_number = 0 # variable created to keep track the number of streets in the project
150
+ substation_number = 0 # variable created to keep track the number of substations in the project
151
+ customers_coordinates = [] # array containing the coordinates and id of the closest node of each building to the street
152
+ street_coordinates = [] # array containing every street node coordinates and id
153
+ coordinates_buildings = [] # array containing every building node coordinates and id
154
+ building_ids = [] # array containing building_ids to retrieve urbanopt results later
155
+ building_floors = [] # array containing numb of floors for each building
156
+ substation_location = []
157
+ utm_zone = 0
158
+ streets = geojson_hash
159
+ puts "SCENARIO FEATURES: #{scenario_features}"
160
+ # parsing the options defined by the user to run the RNM-US with a certain % of UG cables and designing the network with only LV nodes
161
+ # to be consistent in case several case-studies are run to have an homogenous parameter on how to compare the same buildings with different energy consumption
162
+ # Use defaults and warn user if these fields are unset
163
+ if streets.key?('project') && streets['project'].key?('underground_cables_ratio')
164
+ ug_ratio = streets['project']['underground_cables_ratio'].to_f
165
+ puts "RNM-US gem INFO: using underground_cables_ratio of #{ug_ratio}"
166
+ else
167
+ ug_ratio = UG_RATIO_DEFAULT
168
+ puts "RNM-US gem WARNING: field ['project']['underground_cables_ratio'] not specified in Feature File...using default value of #{UG_RATIO_DEFAULT}"
169
+ end
170
+ if streets.key?('project') && streets['project'].key?('only_lv_consumers')
171
+ only_lv_consumers = streets['project']['only_lv_consumers']
172
+ puts "RNM-US gem INFO: using only_lv_consumers ratio of #{only_lv_consumers}"
173
+ else
174
+ only_lv_consumers = ONLY_LV_CONSUMERS_DEFAULT
175
+ puts "RNM-US gem WARNING: field ['project']['only_lv_consumers'] not specified in Feature File...using default value of #{ONLY_LV_CONSUMERS_DEFAULT}"
176
+ end
177
+ if streets.key?('project') && streets['project'].key?('max_number_of_lv_nodes_per_building')
178
+ max_num_lv_nodes = streets['project']['max_number_of_lv_nodes_per_building']
179
+ puts "RNM-US gem INFO: using at max #{max_num_lv_nodes} lv nodes per building"
180
+ else
181
+ max_num_lv_nodes = MAX_LV_NODES_DEFAULT
182
+ puts "RNM-US gem WARNING: field ['project']['max_number_of_lv_nodes_per_building'] not specified in Feature File...using default value of #{MAX_LV_NODES_DEFAULT}"
183
+ end
184
+ # each features (linestring, multilinestring and polygon) are processed in an external method, to create intermediate nodes
185
+ # for a better graphical representation of the district
186
+ # "Point" geometry is ignored (site origin feature)
187
+ # put error if there is not this info, and use default values
188
+ streets['features'].each do |street|
189
+ # the geojson file is read and according to the "type" of feature (linestring, multilinestring, polygon)
190
+ # a different loop is executed to fill every node coordinates in a specific array
191
+ if street['properties']['type'] == 'District System' && street['properties']['district_system_type'] == 'Electrical Substation'
192
+ substation_location[substation_number] = sub_coordinates(street['geometry']['coordinates'][0], street['properties']['id'])
193
+ substation_number += 1
194
+ elsif street['geometry']['type'] == 'LineString' && street['properties']['type'] == 'Road'
195
+ each_street = [] # defining an array for each street, that includes all the nodes coordinates for each street
196
+ i = 0 # index representing the number of street_nodes
197
+ for k in 0..street['geometry']['coordinates'].length - 1
198
+ each_street, i = coordinates(street, street['geometry']['coordinates'][k][1], street['geometry']['coordinates'][k][0], each_street, k, i)
199
+ end
200
+ street_coordinates[street_number] = each_street
201
+ street_number += 1
202
+ elsif street['geometry']['type'] == 'MultiLineString' && street['properties']['type'] == 'Road'
203
+ each_street = []
204
+ i = 0 # index representing the number of street_nodes
205
+ for k in 0..street['geometry']['coordinates'].length - 1
206
+ for j in 0..street['geometry']['coordinates'][k].length - 1
207
+ each_street, i = coordinates(street, street['geometry']['coordinates'][k][j][1], street['geometry']['coordinates'][k][j][0], each_street, j, i)
208
+ end
209
+ end
210
+ street_coordinates[street_number] = each_street
211
+ street_number += 1
212
+ elsif street['geometry']['type'] == 'Polygon' && street['properties']['type'] == 'Building' and scenario_features.include? street['properties']['id']
213
+ for k in 0..street['geometry']['coordinates'].length - 1
214
+ h = 0 # index representing number of nodes for each single building
215
+ building = [] # array containing every building node coordinates and id of 1 building
216
+ for j in 0..street['geometry']['coordinates'][k].length - 1
217
+ building, h = URBANopt::RNM::GeojsonInput.new.coordinates(street, street['geometry']['coordinates'][k][j][1], street['geometry']['coordinates'][k][j][0], building, j, h)
218
+ end
219
+ coordinates_buildings[building_number] = building # inserting in each index the nodes coordinates and id of each building
220
+ building_ids[building_number] = street['properties']['id']
221
+ building_floors[building_number] = street['properties']['number_of_stories']
222
+ if building_number == 0
223
+ utm = GeoUtm::LatLon.new(street['geometry']['coordinates'][k][j][1], street['geometry']['coordinates'][k][j][0]).to_utm
224
+ utm_zone = utm.zone
225
+ end
226
+ building_number += 1
227
+ end
228
+ end
229
+ end
230
+
231
+ # raise error if no streets were found
232
+ if street_number == 0
233
+ raise 'ERROR: No roads were found in the Feature File. Road locations must be in the Feature File for RNM analysis.'
234
+ end
235
+
236
+ street_type = []
237
+ for i in 0..street_number - 1
238
+ # creating a class to define when the lines in each street have to be considered OH or UG
239
+ street_type[i] = URBANopt::RNM::OhUgRate.new
240
+ # obtaining the average height for each street and numb of buildings in each street
241
+ street_type[i].height_building(coordinates_buildings, street_coordinates[i], building_floors)
242
+ end
243
+ street = []
244
+ jj = 0
245
+ # defining each street as "OH" or "UG" according to the threshold value obtained from the user's input
246
+ for i in 0..street_number - 1
247
+ street_type[i].classify_street_type(street_type, ug_ratio)
248
+ for j in 0..street_coordinates[i].length - 1
249
+ street[jj] = street_coordinates[i][j][0], street_coordinates[i][j][1], street_coordinates[i][j][2], street_coordinates[i][j][3], street_type[i].type
250
+ jj += 1
251
+ end
252
+ end
253
+ # an external method is called to find the coordinates of the closest node of each building to the street
254
+ for i in 0..building_number - 1
255
+ customers_coordinates[i] = consumer_coordinates(coordinates_buildings[i], street)
256
+ end
257
+ return street, customers_coordinates, coordinates_buildings, building_number, building_ids, substation_location, only_lv_consumers, max_num_lv_nodes, utm_zone # considering creating an hash as attribute
258
+ end
259
+ end
260
+ end
261
+ end
@@ -0,0 +1,335 @@
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 'csv'
42
+ require 'json'
43
+ require 'urbanopt/rnm/logger'
44
+ require 'urbanopt/geojson'
45
+
46
+ module URBANopt
47
+ module RNM
48
+ class InputFiles
49
+ ##
50
+ # Initialize InputFiles attributes: +run_dir+, +feature_file_path+, +reopt+, +extended_catalog_path+, +average_building_peak_catalog_path+, +rnm_dirname+, +opendss_catalog+
51
+ ##
52
+ # [parameters:]
53
+ # * +run_dir+ - _String_ - Full path to directory for simulation of this scenario
54
+ # * +feature_file_path+ - _String_ - Full path to GeoJSON feature file containing features and streets for simulation.
55
+ # * +extended_catalog_path+ - _String_ - Full path to the extended_catalog which include all the info about electric equipment and RNM-US parameters
56
+ # * +average_building_peak_catalog_path+ - _String_ - Full path to the catalog providing average peak building consumption per floor area and average floor area per building type
57
+ # * +reopt+ - _Boolean_ - Input command from the user to either include or not DG capabilities in planning the network, if REopt was ran before
58
+ # * +opendss_catalog+ - _Boolean_ - Input command from the user to either run or not the opendss_conversion_script to convert the extended_catalog in OpenDSS catalog
59
+ # * +rnm_dirname+ - _String_ - name of RNM-US directory that will contain the input files (within the scenario directory)
60
+ ##
61
+ def initialize(run_dir, scenario_features, feature_file, extended_catalog_path, average_building_peak_catalog_path, reopt: false, opendss_catalog: true, rnm_dirname: 'rnm-us')
62
+ @run_dir = run_dir
63
+ @feature_file = feature_file
64
+ @scenario_features = scenario_features
65
+ @rnm_dirname = rnm_dirname
66
+ @extended_catalog_path = extended_catalog_path
67
+ @average_building_peak_catalog_path = average_building_peak_catalog_path
68
+ @reopt = reopt
69
+ @opendss_catalog = opendss_catalog
70
+ # initialize @@logger
71
+ @@logger ||= URBANopt::RNM.logger
72
+
73
+ # initialize RNM directory
74
+ if !Dir.exist?(File.join(@run_dir, @rnm_dirname))
75
+ FileUtils.mkdir_p(File.join(@run_dir, @rnm_dirname))
76
+ @@logger.info("Created directory: #{File.join(@run_dir, @rnm_dirname)}")
77
+ end
78
+ end
79
+
80
+ # finding the limits on LV defined by the equipments in the catalog
81
+ def catalog_limits
82
+ catalog = JSON.parse(File.read(@extended_catalog_path))
83
+ limit = Hash.new(0)
84
+ limit_lines = Hash.new(0)
85
+ limit_trafo = Hash.new(0)
86
+ # evaluating first all the LV power lines included in the extended catalog and finding the LV 3-phase and single-phase
87
+ # lines with the highest capacity
88
+ catalog['LINES'][1].each do |key, v|
89
+ (0..catalog['LINES'][1][key].length - 1).each do |ii|
90
+ if catalog['LINES'][1][key][ii]['Voltage(kV)'] == '0.416'
91
+ if catalog['LINES'][1][key][ii]['Line geometry'][0]['phase'] != 'N'
92
+ wire = catalog['LINES'][1][key][ii]['Line geometry'][0]['wire']
93
+ else
94
+ wire = catalog['LINES'][1][key][ii]['Line geometry'][1]['wire']
95
+ end
96
+ jj = 0
97
+ jj += 1 while catalog['WIRES']['WIRES CATALOG'][jj]['nameclass'] != wire
98
+ current = catalog['WIRES']['WIRES CATALOG'][jj]['ampacity (A)'].to_i
99
+ if catalog['LINES'][1][key][ii]['Nphases'].to_i == 3
100
+ if ((current * (catalog['LINES'][1][key][ii]['Voltage(kV)']).to_f) * (3**0.5)) > limit_lines[:three_phase]
101
+ limit_lines[:three_phase] = ((current * (catalog['LINES'][1][key][ii]['Voltage(kV)']).to_f) * (3**0.5)).round(2)
102
+ end
103
+ else
104
+ if (current * (catalog['LINES'][1][key][ii]['Voltage(kV)']).to_f) > limit_lines[:single_phase]
105
+ limit_lines[:single_phase] = (current * (catalog['LINES'][1][key][ii]['Voltage(kV)']).to_f).round(2)
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ # evaluating all the distribution transformers included in the extended catalog and finding 3-phase and single-phase
112
+ # distr. transformers with the highest capacity
113
+ (0..catalog['SUBSTATIONS AND DISTRIBUTION TRANSFORMERS'].length - 1).each do |k, v|
114
+ catalog['SUBSTATIONS AND DISTRIBUTION TRANSFORMERS'][k].each do |key, value|
115
+ if catalog['SUBSTATIONS AND DISTRIBUTION TRANSFORMERS'][k][key][0]['Voltage level'] == 'MV-LV'
116
+ (0..catalog['SUBSTATIONS AND DISTRIBUTION TRANSFORMERS'][k][key].length - 1).each do |i|
117
+ if catalog['SUBSTATIONS AND DISTRIBUTION TRANSFORMERS'][k][key][i]['Nphases'] == '3'
118
+ if (catalog['SUBSTATIONS AND DISTRIBUTION TRANSFORMERS'][k][key][i]['Guaranteed Power(kVA)'].to_i) > limit_trafo[:three_phase]
119
+ limit_trafo[:three_phase] = catalog['SUBSTATIONS AND DISTRIBUTION TRANSFORMERS'][k][key][i]['Guaranteed Power(kVA)'].to_i
120
+ end
121
+ else
122
+ if (catalog['SUBSTATIONS AND DISTRIBUTION TRANSFORMERS'][k][key][i]['Guaranteed Power(kVA)'].to_i) > limit_trafo[:single_phase]
123
+ limit_trafo[:single_phase] = catalog['SUBSTATIONS AND DISTRIBUTION TRANSFORMERS'][k][key][i]['Guaranteed Power(kVA)'].to_i
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
130
+ trafo_margin = catalog['OTHERS']['Margin of design of new facilities LV (100.0 = designs with double so that 50% is left over)'].to_i
131
+ limit_trafo[:single_phase] = limit_trafo[:single_phase] / (1 + (trafo_margin / 100))
132
+ limit_trafo[:three_phase] = limit_trafo[:three_phase] / (1 + (trafo_margin / 100))
133
+ # setting as the limit for single-phase and 3-phase the component with the lowest capacity
134
+ if limit_trafo[:three_phase] < limit_lines[:three_phase]
135
+ limit[:three_phase] = limit_trafo[:three_phase]
136
+ else
137
+ limit[:three_phase] = limit_lines[:three_phase]
138
+ end
139
+ if limit_trafo[:single_phase] < limit_lines[:single_phase]
140
+ limit[:single_phase] = limit_trafo[:single_phase]
141
+ else
142
+ limit[:single_phase] = limit_lines[:single_phase]
143
+ end
144
+ return limit
145
+ end
146
+
147
+ ##
148
+ # Create the files that are required as input in RNM-US.
149
+ # (e.g. streetmapAS.txt, customers.txt, customers_ext.txt, customers_profiles_p.txt, customers_profiles_q.txt,
150
+ # customers_profiles_p_ext.txt, customers_profiles_q_ext.txt,substation_location.txt, generators.txt,
151
+ # generator_profiles_p.txt, generator_profiles_q.txt, generator_profiles_p_ext.txt, generator_profiles_q_ext.txt,
152
+ # ficheros_entrada.txt, ficheros_entrada_inc.txt, udcons.csv)
153
+ ##
154
+ def create
155
+ # the GEOjson file is loaded and a method is called to extract the required information regarding the street, building and substation location
156
+ street_coordinates, customers_coordinates, coordinates_buildings, tot_buildings, building_ids, substation_location, only_lv_consumers, max_num_lv_nodes, utm_zone = URBANopt::RNM::GeojsonInput.new.coordinates_feature_hash(@feature_file, @scenario_features)
157
+ # puts("BUILDING IDS: #{building_ids}")
158
+ # define the LV/MV limit imposed by the components of the catalog: distr.transformers and power lines and exporting the utm_zone to the catalog
159
+ lv_limit = catalog_limits
160
+ # verifying if running RNM-US with REopt option
161
+
162
+ if @reopt
163
+ if !File.join(@run_dir, 'feature_optimization').nil?
164
+ scenario_report_path = File.join(@run_dir, 'feature_optimization')
165
+ # creating a class prosumers with all the info for all the DER and consumption for each building
166
+ prosumers = URBANopt::RNM::Prosumers.new(@reopt, only_lv_consumers, max_num_lv_nodes, @average_building_peak_catalog_path, lv_limit) # passing these 2 conditions to see what option did the user
167
+ else
168
+ raise 'scenario report is not found'
169
+ end
170
+
171
+ else
172
+ if !File.join(@run_dir, 'scenario_report').nil?
173
+ scenario_report_path = File.join(@run_dir, 'default_scenario_report')
174
+ # creating a class consumers with all the info about the consumption for each building
175
+ consumers = URBANopt::RNM::Consumers.new(@reopt, only_lv_consumers, max_num_lv_nodes, @average_building_peak_catalog_path, lv_limit) # passing these 2 conditions to see what option did the user applied
176
+ else
177
+ raise 'scenario_report is not found'
178
+ end
179
+ end
180
+ file_csv = []
181
+ file_json = []
182
+ # finding the 2 most extreme hours of the year (maximum net demand and maximum net generation) the distribution network is planned
183
+ hours = URBANopt::RNM::ReportScenario.new(@reopt)
184
+ # hours_commercial = URBANopt::RNM::ReportScenario.new(@reopt)
185
+ (0..tot_buildings - 1).each do |j|
186
+ if @reopt
187
+ file_csv[j] = File.join(@run_dir, (building_ids[j]).to_s, 'feature_reports', 'feature_optimization.csv')
188
+
189
+ # check that reopt json file exists (feature optimization only)
190
+ if !File.exist?(File.join(@run_dir, (building_ids[j]).to_s, 'feature_reports', 'feature_optimization.json'))
191
+ msg = 'REopt feature_optimization.json file not found. To use REopt results in the RNM analysis,' \
192
+ 'first post-process the project with the --reopt-feature flag.'
193
+ raise msg
194
+ end
195
+
196
+ file_json[j] = JSON.parse(File.read(File.join(@run_dir, (building_ids[j]).to_s, 'feature_reports', 'feature_optimization.json')))
197
+ hours.aggregate_consumption(file_csv[j], file_json[j], j)
198
+ else
199
+
200
+ file_csv[j] = File.join(@run_dir, (building_ids[j]).to_s, 'feature_reports', 'default_feature_report.csv')
201
+ file_json[j] = JSON.parse(File.read(File.join(@run_dir, (building_ids[j]).to_s, 'feature_reports', 'default_feature_report.json')))
202
+
203
+ hours.aggregate_consumption(file_csv[j], file_json[j], j)
204
+ end
205
+ end
206
+ hours.scenario_report_results
207
+
208
+ # iterating over each building to define each consumer/prosumer
209
+ (0..tot_buildings - 1).each do |j| # (0..20).each do |j|
210
+ # use building_ids lookup to get name of results directory
211
+ # reports will be in 'feature_reports' directory
212
+ if @reopt
213
+ # file_path = File.join(@run_dir, "#{building_ids[j]}", 'feature_reports', 'feature_optimization')
214
+ # prosumers.prosumer_files_load(file_path[j] + ".csv", File.read(file_path + ".json"), customers_coordinates[j], coordinates_buildings[j], hours)
215
+ prosumers.prosumer_files_load(file_csv[j], file_json[j], customers_coordinates[j], coordinates_buildings[j], hours)
216
+ else
217
+ # file_path = File.join(@run_dir, "#{building_ids[j]}", '014_default_feature_reports', 'default_feature_reports')
218
+ consumers.customer_files_load(file_csv[j], file_json[j], customers_coordinates[j], coordinates_buildings[j], hours)
219
+ end
220
+ end
221
+ rnm_us_catalog = URBANopt::RNM::RnmUsCatalogConversion.new(@extended_catalog_path, @run_dir, @rnm_dirname)
222
+ rnm_us_catalog.processing_data(utm_zone)
223
+ # call and create the opendss_catalog class if the user wants to convert the extended catalog into OpenDSS catalog
224
+ if @opendss_catalog
225
+ @opendss_catalog = URBANopt::RNM::ConversionToOpendssCatalog.new(@extended_catalog_path)
226
+ # create catalog and save to specified path
227
+ @opendss_catalog.create_catalog(File.join(@run_dir, 'opendss_catalog.json'))
228
+ end
229
+ # creating all the inputs files required by the RNM-US model in the folder Inputs in the RNM folder
230
+ File.open(File.join(@run_dir, @rnm_dirname, 'streetmapAS.txt'), 'w+') do |f|
231
+ f.puts(street_coordinates.map { |x| x.join(';') })
232
+ end
233
+ ficheros_entrada = []
234
+ if substation_location != 'nil'
235
+ File.open(File.join(@run_dir, @rnm_dirname, 'primary_substations.txt'), 'w+') do |f|
236
+ f.puts(substation_location.map { |x| x.join(';') })
237
+ end
238
+ ficheros_entrada = []
239
+ ficheros_entrada.push('CSubestacionDistribucionGreenfield;primary_substations.txt')
240
+ ficheros_entrada.push('CPuntoCallejero;streetmapAS.txt')
241
+ ficheros_entrada.push('END')
242
+ File.open(File.join(@run_dir, @rnm_dirname, 'ficheros_entrada.txt'), 'w+') do |f|
243
+ f.puts(ficheros_entrada)
244
+ end
245
+ else
246
+ puts('substation location automatically chosen by RNM-US model')
247
+ ficheros_entrada.push('CPuntoCallejero;streetmapAS.txt')
248
+ ficheros_entrada.push('END')
249
+ File.open(File.join(@run_dir, @rnm_dirname, 'ficheros_entrada.txt'), 'w+') do |f|
250
+ f.puts(ficheros_entrada)
251
+ end
252
+ end
253
+ ficheros_entrada_inc = []
254
+ if @reopt
255
+ File.open(File.join(@run_dir, @rnm_dirname, 'customers.txt'), 'w+') do |f|
256
+ f.puts(prosumers.customers.map { |x| x.join(';') })
257
+ end
258
+ File.open(File.join(@run_dir, @rnm_dirname, 'customers_ext.txt'), 'w+') do |g|
259
+ g.puts(prosumers.customers_ext.map { |w| w.join(';') })
260
+ end
261
+ File.open(File.join(@run_dir, @rnm_dirname, 'cust_profile_p.txt'), 'w+') do |g|
262
+ g.puts(prosumers.profile_customer_p.map { |w| w.join(';') })
263
+ end
264
+ File.open(File.join(@run_dir, @rnm_dirname, 'cust_profile_q.txt'), 'w+') do |g|
265
+ g.puts(prosumers.profile_customer_q.map { |w| w.join(';') })
266
+ end
267
+ # CSV.open(File.join(@run_dir, @rnm_dirname, "cust_profile_q_extendido.csv"), "w") do |csv|
268
+ # csv << [prosumers.profile_customer_q_ext]
269
+ # end
270
+ File.open(File.join(@run_dir, @rnm_dirname, 'cust_profile_q_extendido.txt'), 'w+') do |g|
271
+ g.puts(prosumers.profile_customer_q_ext.map { |w| w.join(';') })
272
+ end
273
+ File.open(File.join(@run_dir, @rnm_dirname, 'cust_profile_p_extendido.txt'), 'w+') do |g|
274
+ g.puts(prosumers.profile_customer_p_ext.map { |w| w.join(';') })
275
+ end
276
+ File.open(File.join(@run_dir, @rnm_dirname, 'generators.txt'), 'w+') do |g|
277
+ g.puts(prosumers.dg.map { |w| w.join(';') })
278
+ end
279
+ # creating profiles txt files
280
+ File.open(File.join(@run_dir, @rnm_dirname, 'gen_profile_p.txt'), 'w+') do |g|
281
+ g.puts(prosumers.dg_profile_p.map { |w| w.join(';') })
282
+ end
283
+ File.open(File.join(@run_dir, @rnm_dirname, 'gen_profile_q.txt'), 'w+') do |g|
284
+ g.puts(prosumers.dg_profile_q.map { |w| w.join(';') })
285
+ end
286
+ File.open(File.join(@run_dir, @rnm_dirname, 'gen_profile_q_extendido.txt'), 'w+') do |g|
287
+ g.puts(prosumers.profile_dg_q_extended.map { |w| w.join(';') })
288
+ end
289
+ File.open(File.join(@run_dir, @rnm_dirname, 'gen_profile_p_extendido.txt'), 'w+') do |g|
290
+ g.puts(prosumers.profile_dg_p_extended.map { |w| w.join(';') })
291
+ end
292
+ ficheros_entrada_inc.push('CClienteGreenfield;customers_ext.txt;cust_profile_p.txt;cust_profile_q.txt;cust_profile_p_extendido.txt;cust_profile_q_extendido.txt')
293
+ ficheros_entrada_inc.push('CGeneradorGreenfield;generators.txt;gen_profile_p.txt;gen_profile_q.txt;gen_profile_p_extendido.txt;gen_profile_q_extendido.txt')
294
+ ficheros_entrada_inc.push('END')
295
+ File.open(File.join(@run_dir, @rnm_dirname, 'ficheros_entrada_inc.txt'), 'w+') do |g|
296
+ g.puts(ficheros_entrada_inc)
297
+ end
298
+ else
299
+ File.open(File.join(@run_dir, @rnm_dirname, 'customers.txt'), 'w+') do |f|
300
+ f.puts(consumers.customers.map { |x| x.join(';') })
301
+ end
302
+ File.open(File.join(@run_dir, @rnm_dirname, 'customers_ext.txt'), 'w+') do |g|
303
+ g.puts(consumers.customers_ext.map { |w| w.join(';') })
304
+ end
305
+ File.open(File.join(@run_dir, @rnm_dirname, 'cust_profile_p.txt'), 'w+') do |g|
306
+ g.puts(consumers.profile_customer_p.map { |w| w.join(';') })
307
+ end
308
+ File.open(File.join(@run_dir, @rnm_dirname, 'cust_profile_q.txt'), 'w+') do |g|
309
+ g.puts(consumers.profile_customer_q.map { |w| w.join(';') })
310
+ end
311
+ File.open(File.join(@run_dir, @rnm_dirname, 'cust_profile_q_extendido.txt'), 'w+') do |g|
312
+ g.puts(consumers.profile_customer_q_ext.map { |w| w.join(';') })
313
+ end
314
+ File.open(File.join(@run_dir, @rnm_dirname, 'cust_profile_p_extendido.txt'), 'w+') do |g|
315
+ g.puts(consumers.profile_customer_p_ext.map { |w| w.join(';') })
316
+ end
317
+ ficheros_entrada_inc.push('CClienteGreenfield;customers_ext.txt;cust_profile_p.txt;cust_profile_q.txt;cust_profile_p_extendido.txt;cust_profile_q_extendido.txt')
318
+ ficheros_entrada_inc.push('END')
319
+ File.open(File.join(@run_dir, @rnm_dirname, 'ficheros_entrada_inc.txt'), 'w+') do |g|
320
+ g.puts(ficheros_entrada_inc)
321
+ end
322
+ end
323
+ end
324
+
325
+ ##
326
+ # Delete the RNM-US input files directory
327
+ ##
328
+ def delete
329
+ if Dir.exist?(File.join(@run_dir, @rnm_dirname))
330
+ FileUtils.rm_rf(File.join(@run_dir, @rnm_dirname))
331
+ end
332
+ end
333
+ end
334
+ end
335
+ end