urbanopt-rnm-us 0.2.0 → 0.4.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 +4 -4
- data/CHANGELOG.md +11 -0
- data/Gemfile +6 -0
- data/LICENSE.md +2 -2
- data/README.md +7 -0
- data/Rakefile +33 -2
- data/catalogs/extended_catalog.json +1928 -1796
- data/lib/urbanopt/rnm/api_client.rb +11 -11
- data/lib/urbanopt/rnm/capacitor_opendss.rb +1 -1
- data/lib/urbanopt/rnm/carson_eq.rb +1 -1
- data/lib/urbanopt/rnm/consumers.rb +8 -7
- data/lib/urbanopt/rnm/conversion_to_opendss.rb +1 -1
- data/lib/urbanopt/rnm/geojson_input.rb +3 -3
- data/lib/urbanopt/rnm/input_files.rb +2 -2
- data/lib/urbanopt/rnm/logger.rb +1 -1
- data/lib/urbanopt/rnm/oh_ug_rate.rb +1 -1
- data/lib/urbanopt/rnm/post_processor.rb +14 -16
- data/lib/urbanopt/rnm/processor_opendss_catalog.rb +1 -1
- data/lib/urbanopt/rnm/prosumers.rb +52 -52
- data/lib/urbanopt/rnm/rnm_us_catalog_conversion.rb +2 -2
- data/lib/urbanopt/rnm/runner.rb +12 -3
- data/lib/urbanopt/rnm/scenario_report.rb +2 -1
- data/lib/urbanopt/rnm/substation_location.rb +1 -1
- data/lib/urbanopt/rnm/transformer_opendss.rb +2 -6
- data/lib/urbanopt/rnm/validation/main_validation.py +51 -0
- data/lib/urbanopt/rnm/validation/opendss_interface.py +234 -0
- data/lib/urbanopt/rnm/validation/plot_lib.py +222 -0
- data/lib/urbanopt/rnm/validation.rb +75 -0
- data/lib/urbanopt/rnm/version.rb +2 -2
- data/lib/urbanopt/rnm/wires_class.rb +1 -1
- data/lib/urbanopt/rnm/wires_opendss.rb +8 -8
- data/lib/urbanopt/rnm.rb +2 -1
- data/lib/urbanopt-rnm.rb +1 -1
- data/opendss_catalog.json +2216 -0
- data/urbanopt-rnm-us-gem.gemspec +1 -1
- metadata +9 -4
@@ -1,5 +1,5 @@
|
|
1
1
|
# *********************************************************************************
|
2
|
-
# URBANopt (tm), Copyright (c) 2019-
|
2
|
+
# URBANopt (tm), Copyright (c) 2019-2022, Alliance for Sustainable Energy, LLC, and other
|
3
3
|
# contributors. All rights reserved.
|
4
4
|
|
5
5
|
# Redistribution and use in source and binary forms, with or without modification,
|
@@ -109,6 +109,7 @@ module URBANopt
|
|
109
109
|
end
|
110
110
|
end
|
111
111
|
end
|
112
|
+
|
112
113
|
def aggregate_consumption(file_csv, file_json, n_feature)
|
113
114
|
feature_type = file_json['program']['building_types'][0]['building_type']
|
114
115
|
# residential_building_types = "Single-Family Detached" #add the other types
|
@@ -1,5 +1,5 @@
|
|
1
1
|
# *********************************************************************************
|
2
|
-
# URBANopt (tm), Copyright (c) 2019-
|
2
|
+
# URBANopt (tm), Copyright (c) 2019-2022, Alliance for Sustainable Energy, LLC, and other
|
3
3
|
# contributors. All rights reserved.
|
4
4
|
|
5
5
|
# Redistribution and use in source and binary forms, with or without modification,
|
@@ -1,5 +1,5 @@
|
|
1
1
|
# *********************************************************************************
|
2
|
-
# URBANopt (tm), Copyright (c) 2019-
|
2
|
+
# URBANopt (tm), Copyright (c) 2019-2022, Alliance for Sustainable Energy, LLC, and other
|
3
3
|
# contributors. All rights reserved.
|
4
4
|
|
5
5
|
# Redistribution and use in source and binary forms, with or without modification,
|
@@ -52,11 +52,7 @@ module URBANopt
|
|
52
52
|
hash[:resistance] = trafo['Low-voltage-side short-circuit resistance (ohms)'].to_f.round(2)
|
53
53
|
hash[:reactance] = trafo['Reactance (p.u. transf)'].to_f.round(2)
|
54
54
|
hash[:phases] = trafo['Nphases']
|
55
|
-
|
56
|
-
hash[:is_center_tap] = false
|
57
|
-
else
|
58
|
-
hash[:is_center_tap] = true
|
59
|
-
end
|
55
|
+
hash[:Centertap] = trafo['Centertap']
|
60
56
|
hash[:high_voltage] = trafo['Primary Voltage (kV)']
|
61
57
|
hash[:low_voltage] = trafo['Secondary Voltage (kV)']
|
62
58
|
hash[:connection] = trafo['connection']
|
@@ -0,0 +1,51 @@
|
|
1
|
+
import opendssdirect as dss
|
2
|
+
import pandas as pd
|
3
|
+
import matplotlib.pyplot as plt
|
4
|
+
import numpy as np
|
5
|
+
import sys as sys
|
6
|
+
import math
|
7
|
+
import networkx as nx
|
8
|
+
import opendss_interface
|
9
|
+
import plot_lib
|
10
|
+
|
11
|
+
class Validation:
|
12
|
+
def __init__(self, folder):
|
13
|
+
self.folder = folder
|
14
|
+
|
15
|
+
def main_validation(self):
|
16
|
+
master_file_full_path = folder + '/dss_files/' + 'Master.dss'
|
17
|
+
start_index = 0
|
18
|
+
num_periods=12
|
19
|
+
end_index = 8760
|
20
|
+
v_range_voltage=(0.9, 1.1)
|
21
|
+
v_limits_voltage=[0.95,1.05]
|
22
|
+
v_range_loading=(0,1.3)
|
23
|
+
v_limits_loading=[1]
|
24
|
+
v_range_show_all=(0,0)
|
25
|
+
|
26
|
+
#For tests
|
27
|
+
#end_index = 24
|
28
|
+
#v_range_voltage=(0.975, 1.025)
|
29
|
+
|
30
|
+
myopendss_io=opendss_interface.OpenDSS_Interface(folder)
|
31
|
+
v_dict_voltage,v_voltage_yearly,v_voltage_period,v_power_yearly,v_power_period,v_dict_loading,v_loading_yearly,v_loading_period,v_dict_losses,v_subs_losses_yearly,v_line_losses_yearly,dict_buses_element=myopendss_io.solve_powerflow_iteratively(num_periods,start_index,end_index,master_file_full_path,v_range_voltage,v_range_loading)
|
32
|
+
myopendss_io.write_dict(v_dict_voltage,v_range_show_all,'Voltages (p.u.)','Buses')
|
33
|
+
myopendss_io.write_dict(v_dict_voltage,v_range_voltage,'Voltage Violations (p.u.)','Buses')
|
34
|
+
myopendss_io.write_dict(v_dict_loading,v_range_show_all,'Loading (p.u.)','Branches')
|
35
|
+
myopendss_io.write_dict(v_dict_loading,v_range_loading,'Loading Violations (p.u.)','Branches')
|
36
|
+
myopendss_io.write_dict(v_dict_losses,v_range_show_all,'Losses','Branches')
|
37
|
+
edges=myopendss_io.get_edges()
|
38
|
+
myplot_lib=plot_lib.Plot_Lib(folder)
|
39
|
+
myplot_lib.plot_hist('Voltage',v_voltage_yearly,v_voltage_period,v_range_voltage,40,num_periods,v_limits_voltage)
|
40
|
+
myplot_lib.plot_hist('Loading',v_loading_yearly,v_loading_period,v_range_loading,80,num_periods,v_limits_loading)
|
41
|
+
myplot_lib.plot_losses(v_subs_losses_yearly,v_line_losses_yearly)
|
42
|
+
myplot_lib.plot_graph(edges,v_dict_voltage,v_range_voltage,v_dict_loading,v_range_loading,dict_buses_element)
|
43
|
+
|
44
|
+
|
45
|
+
|
46
|
+
if __name__ == "__main__":
|
47
|
+
#Example to run it in command window
|
48
|
+
#python main_validation.py files
|
49
|
+
folder = sys.argv[1]
|
50
|
+
valid=Validation(folder)
|
51
|
+
valid.main_validation()
|
@@ -0,0 +1,234 @@
|
|
1
|
+
import opendssdirect as dss
|
2
|
+
import pandas as pd
|
3
|
+
import matplotlib.pyplot as plt
|
4
|
+
import numpy as np
|
5
|
+
import sys as sys
|
6
|
+
import math
|
7
|
+
import networkx as nx
|
8
|
+
|
9
|
+
class OpenDSS_Interface:
|
10
|
+
def __init__(self, folder):
|
11
|
+
self.folder = folder
|
12
|
+
|
13
|
+
def remove_terminal(self,bus):
|
14
|
+
if isinstance(bus,str):
|
15
|
+
return bus.split('.')[0]
|
16
|
+
else:
|
17
|
+
return bus
|
18
|
+
|
19
|
+
|
20
|
+
def extract_period(self,v_value_i,v_value_period,i,end_index,num_periods):
|
21
|
+
for j in range(num_periods):
|
22
|
+
if (i<=end_index*j/num_periods):
|
23
|
+
v_value_period[j].extend(v_value_i)
|
24
|
+
break
|
25
|
+
return v_value_period
|
26
|
+
|
27
|
+
def add_to_dictionary(self,v_dict_voltage,dict_voltage_i):
|
28
|
+
for idx,name in enumerate(dict_voltage_i):
|
29
|
+
if name in v_dict_voltage: #if not empty
|
30
|
+
v_dict_voltage[name].append(dict_voltage_i[name])
|
31
|
+
else:
|
32
|
+
v_dict_voltage[name]=[dict_voltage_i[name]]
|
33
|
+
|
34
|
+
def dss_run_command(self,command):
|
35
|
+
output=dss.run_command(command)
|
36
|
+
if (len(output)>0):
|
37
|
+
print(output)
|
38
|
+
|
39
|
+
|
40
|
+
def get_all_voltage(self):
|
41
|
+
"""Computes over and under voltages for all buses"""
|
42
|
+
bus_names = dss.Circuit.AllBusNames()
|
43
|
+
dict_voltage = {}
|
44
|
+
v_voltage = [0 for _ in range(len(bus_names))]
|
45
|
+
for idx,b in enumerate(bus_names):
|
46
|
+
dss.Circuit.SetActiveBus(b)
|
47
|
+
vang = dss.Bus.puVmagAngle()
|
48
|
+
if len(vang[::2]) > 0:
|
49
|
+
vmag = sum(vang[::2])/(len(vang)/2)
|
50
|
+
else:
|
51
|
+
vmag = 0
|
52
|
+
dict_voltage[b] = vmag
|
53
|
+
v_voltage[idx]=vmag
|
54
|
+
|
55
|
+
return dict_voltage,v_voltage
|
56
|
+
|
57
|
+
def get_all_power(self):
|
58
|
+
"""Computes power in all circuits"""
|
59
|
+
circuit_names = dss.Circuit.AllElementNames()
|
60
|
+
dict_power = {}
|
61
|
+
v_power = [0 for _ in range(len(circuit_names))]
|
62
|
+
for idx,b in enumerate(circuit_names):
|
63
|
+
dss.Circuit.SetActiveElement(b)
|
64
|
+
power = dss.CktElement.Powers()
|
65
|
+
if len(power[::2]) > 0:
|
66
|
+
poweravg = sum(power[::2])/(len(power)/2)
|
67
|
+
else:
|
68
|
+
poweravg = 0
|
69
|
+
dict_power[b] = poweravg
|
70
|
+
v_power[idx]=poweravg
|
71
|
+
|
72
|
+
return dict_power,v_power
|
73
|
+
|
74
|
+
|
75
|
+
def get_all_loading(self):
|
76
|
+
"""Computes loading in all circuits"""
|
77
|
+
circuit_names = dss.Circuit.AllElementNames()
|
78
|
+
dict_loading = {}
|
79
|
+
dict_buses_element={} #Associate the element to the buses (this has the inconvenient that only associates one element to each pair of buses)
|
80
|
+
v_loading = [0 for _ in range(len(circuit_names))]
|
81
|
+
for idx,element in enumerate(circuit_names):
|
82
|
+
dss.Circuit.SetActiveElement(element)
|
83
|
+
#only if it is a branch (two buses)
|
84
|
+
buses = dss.CktElement.BusNames()
|
85
|
+
if (len(buses)>=2):
|
86
|
+
current = dss.CktElement.CurrentsMagAng()
|
87
|
+
num_terminals=dss.CktElement.NumTerminals()
|
88
|
+
if len(current[::2]) > 0:
|
89
|
+
#currentmag = sum(current[::2])/len(current[::2])
|
90
|
+
#Take only the average of 1 terminal (discarding phases so every 2)
|
91
|
+
lenc=len(current[::2])
|
92
|
+
stop=round(2*lenc/num_terminals)
|
93
|
+
#print(current[:stop:2])
|
94
|
+
currentmag = sum(current[:stop:2])/lenc
|
95
|
+
else:
|
96
|
+
currentmag = 0
|
97
|
+
currentmag = current[0]
|
98
|
+
nominal_current = dss.CktElement.NormalAmps()
|
99
|
+
#Transformers have applied a 1.1 factor in the calculation of NormalAmps
|
100
|
+
#See library that OpenDSSdirect uses in https://github.com/dss-extensions/dss_capi/blob/master/src/PDElements/Transformer.pas
|
101
|
+
#in particular line code AmpRatings[i] := 1.1 * kVARatings[i] / Fnphases / Vfactor;
|
102
|
+
if (element.startswith("Transformer")):
|
103
|
+
nominal_current=nominal_current/1.1
|
104
|
+
if (nominal_current>0):
|
105
|
+
dict_loading[element] = currentmag/nominal_current
|
106
|
+
v_loading[idx]=currentmag/nominal_current
|
107
|
+
bus1to2=self.remove_terminal(buses[0])+'-->'+self.remove_terminal(buses[1])
|
108
|
+
dict_buses_element[bus1to2]=element
|
109
|
+
|
110
|
+
return dict_loading,v_loading,dict_buses_element
|
111
|
+
|
112
|
+
def get_all_losses(self):
|
113
|
+
"""Computes losses in all circuits"""
|
114
|
+
circuit_names = dss.Circuit.AllElementNames()
|
115
|
+
dict_losses = {}
|
116
|
+
v_losses = [0 for _ in range(len(circuit_names))]
|
117
|
+
total_losses=0
|
118
|
+
for idx,element in enumerate(circuit_names):
|
119
|
+
dss.Circuit.SetActiveElement(element)
|
120
|
+
#only if it is a branch (two buses)
|
121
|
+
buses = dss.CktElement.BusNames()
|
122
|
+
if (len(buses)>=2):
|
123
|
+
#next if check discards vsources
|
124
|
+
nominal_current = dss.CktElement.NormalAmps()
|
125
|
+
if (nominal_current>0):
|
126
|
+
losses = dss.CktElement.Losses()
|
127
|
+
if len(losses) ==2:
|
128
|
+
lossesavg = (losses[0]) #Verify this is correct, if not abs() they range from + to -, [0] to take active losses
|
129
|
+
else:
|
130
|
+
print("error - not correctly reading losses")
|
131
|
+
lossesavg=0
|
132
|
+
#Convert to kW. This function is the exeption that return losses in W
|
133
|
+
lossesavg=lossesavg/1000
|
134
|
+
dict_losses[element] = lossesavg
|
135
|
+
v_losses[idx]=lossesavg
|
136
|
+
|
137
|
+
return dict_losses,total_losses
|
138
|
+
|
139
|
+
def get_total_subs_losses(self):
|
140
|
+
"""Computes total substation losses"""
|
141
|
+
return dss.Circuit.SubstationLosses()[0] #Real part
|
142
|
+
|
143
|
+
def get_total_line_losses(self):
|
144
|
+
"""Computes total line losses"""
|
145
|
+
return dss.Circuit.LineLosses()[0] #Real part
|
146
|
+
|
147
|
+
def get_edges(self):
|
148
|
+
edges=[]
|
149
|
+
circuit_names = dss.Circuit.AllElementNames()
|
150
|
+
for idx,element in enumerate(circuit_names):
|
151
|
+
dss.Circuit.SetActiveElement(element)
|
152
|
+
buses = dss.CktElement.BusNames()
|
153
|
+
#Only if it is a branch
|
154
|
+
if (len(buses)>=2): #There can be 3 buses in single-phase center-tap transformer, in this case the two last ones are equals (different terminals only) and we can take just the 2 first ones
|
155
|
+
#remove terminal from the bus name (everything to the right of point)
|
156
|
+
edges.append([(self.remove_terminal(buses[0]),self.remove_terminal(buses[1]))])
|
157
|
+
return edges
|
158
|
+
|
159
|
+
def write_dict(self,v_dict,v_range,type,component):
|
160
|
+
output_file_full_path = self.folder + '/' + type + '_' + component + '.csv'
|
161
|
+
# Write directly as a CSV file with headers on first line
|
162
|
+
with open(output_file_full_path, 'w') as fp:
|
163
|
+
#Header: ID, hours (consider adding day, month in future)
|
164
|
+
for idx,name in enumerate(v_dict):
|
165
|
+
fp.write('Hour,'+','.join(str(idx2) for idx2,value in enumerate(v_dict[name])) + '\n')
|
166
|
+
break
|
167
|
+
#Write matrix
|
168
|
+
for idx,name in enumerate(v_dict):
|
169
|
+
#Truncate list to limits
|
170
|
+
truncated_values=[]
|
171
|
+
for idx2,value in enumerate(v_dict[name]):
|
172
|
+
if value<v_range[0] or value>=v_range[1]:
|
173
|
+
truncated_values.append(str(value))
|
174
|
+
else:
|
175
|
+
truncated_values.append("")
|
176
|
+
fp.write(name+','+','.join(truncated_values)+'\n')
|
177
|
+
#truncated_values=[i for i, lower, upper in zip(v_dict_voltage[name], [v_range_voltage[1]]*len(v_dict_voltage[name]), [v_range_voltage[1]]*len(v_dict_voltage[name])) if i <lower or i>upper]
|
178
|
+
#fp.write(name+','+','.join(map(str,v_dict_voltage[name]))+'\n')
|
179
|
+
|
180
|
+
|
181
|
+
def solve_powerflow_iteratively(self,num_periods,start_index,end_index,location,v_range_voltage,v_range_loading):
|
182
|
+
#Por flow solving mode
|
183
|
+
self.dss_run_command("Clear")
|
184
|
+
self.dss_run_command('Redirect '+location)
|
185
|
+
self.dss_run_command("solve mode = snap")
|
186
|
+
self.dss_run_command("Set mode=yearly stepsize=1h number=1")
|
187
|
+
#Init vectors
|
188
|
+
v_voltage_yearly=[]
|
189
|
+
v_voltage_period=[[] for _ in range(num_periods)]
|
190
|
+
v_power_yearly=[]
|
191
|
+
v_power_period=[[] for _ in range(num_periods)]
|
192
|
+
v_loading_yearly=[]
|
193
|
+
v_loading_period=[[] for _ in range(num_periods)]
|
194
|
+
v_subs_losses_yearly=[]
|
195
|
+
v_line_losses_yearly=[]
|
196
|
+
v_dict_voltage={}
|
197
|
+
v_dict_loading={}
|
198
|
+
v_dict_losses={}
|
199
|
+
#Additional initializations
|
200
|
+
my_range=range(start_index,end_index,1)
|
201
|
+
old_percentage_str="" #Variable for tracking progress
|
202
|
+
for i in my_range:
|
203
|
+
#Solve power flow
|
204
|
+
self.dss_run_command("Solve")
|
205
|
+
#Get voltages
|
206
|
+
dict_voltage_i, v_voltage_i = self.get_all_voltage()
|
207
|
+
self.add_to_dictionary(v_dict_voltage,dict_voltage_i)
|
208
|
+
v_voltage_yearly.extend(v_voltage_i)
|
209
|
+
self.extract_period(v_voltage_i,v_voltage_period,i,end_index,num_periods)
|
210
|
+
#Get power
|
211
|
+
dict_power_i, v_power_i = self.get_all_power()
|
212
|
+
v_power_yearly.extend(v_power_i)
|
213
|
+
v_power_period=self.extract_period(v_power_i,v_power_period,i,end_index,num_periods)
|
214
|
+
#Get loading
|
215
|
+
dict_loading_i, v_loading_i,dict_buses_element = self.get_all_loading()
|
216
|
+
self.add_to_dictionary(v_dict_loading,dict_loading_i)
|
217
|
+
v_loading_yearly.extend(v_loading_i)
|
218
|
+
v_loading_period=self.extract_period(v_loading_i,v_loading_period,i,end_index,num_periods)
|
219
|
+
#Get dict losses
|
220
|
+
dict_losses_i, v_losses_i = self.get_all_losses()
|
221
|
+
self.add_to_dictionary(v_dict_losses,dict_losses_i)
|
222
|
+
#Get losses
|
223
|
+
subs_losses_i = self.get_total_subs_losses()
|
224
|
+
v_subs_losses_yearly.append(subs_losses_i)
|
225
|
+
line_losses_i = self.get_total_line_losses()
|
226
|
+
v_line_losses_yearly.append(line_losses_i)
|
227
|
+
#Print progress
|
228
|
+
#percentage_str="{:.0f}".format(100*i/end_index)+"%"
|
229
|
+
#if (percentage_str!=old_percentage_str):
|
230
|
+
# print(percentage_str)
|
231
|
+
#old_percentage_str=percentage_str
|
232
|
+
return v_dict_voltage,v_voltage_yearly,v_voltage_period,v_power_yearly,v_power_period,v_dict_loading,v_loading_yearly,v_loading_period,v_dict_losses,v_subs_losses_yearly,v_line_losses_yearly,dict_buses_element
|
233
|
+
|
234
|
+
|
@@ -0,0 +1,222 @@
|
|
1
|
+
import opendssdirect as dss
|
2
|
+
import pandas as pd
|
3
|
+
import matplotlib.pyplot as plt
|
4
|
+
import numpy as np
|
5
|
+
import sys as sys
|
6
|
+
import math
|
7
|
+
import networkx as nx
|
8
|
+
import opendss_interface
|
9
|
+
|
10
|
+
class Plot_Lib:
|
11
|
+
def __init__(self, folder):
|
12
|
+
self.folder = folder
|
13
|
+
|
14
|
+
def remove_terminal(self,bus):
|
15
|
+
myopendss_io=opendss_interface.OpenDSS_Interface(self.folder)
|
16
|
+
bus=myopendss_io.remove_terminal(bus)
|
17
|
+
return bus
|
18
|
+
|
19
|
+
|
20
|
+
def plot_hist(self,type,v_value,v_value_period,v_range,num_bins,num_periods,v_limits):
|
21
|
+
output_file_full_path_fig = self.folder + '/' + type + ' Histogram (p.u.).png'
|
22
|
+
output_file_full_path_csv = self.folder + '/' + type + ' Histogram (p.u.).csv'
|
23
|
+
plt.figure
|
24
|
+
plt.grid(True)
|
25
|
+
v_legend=["" for _ in range(num_periods+2)]
|
26
|
+
matrix=np.empty((num_bins,num_periods+2)) #Matrix for writting to file (index+periods+yearly)
|
27
|
+
for j in range(num_periods):
|
28
|
+
plt.xlim(v_range)
|
29
|
+
v_weights = np.ones_like(v_value_period[j]) / len(v_value_period[j])
|
30
|
+
counts, bins = np.histogram(v_value_period[j], range=v_range, bins=num_bins, weights=v_weights)
|
31
|
+
if j==0:
|
32
|
+
v_legend[0]=type
|
33
|
+
matrix[:,0]=bins[:num_bins:]
|
34
|
+
matrix[:,j+1]=counts
|
35
|
+
v_legend[j+1]= "M"+str(j+1)
|
36
|
+
plt.plot(bins[:-1]+(bins[1]-bins[0])*0.5, counts)
|
37
|
+
|
38
|
+
v_weights = np.ones_like(v_value) / len(v_value)
|
39
|
+
counts, bins = np.histogram(v_value, range=v_range, bins=num_bins, weights=v_weights)
|
40
|
+
matrix[:,num_periods+1]=counts
|
41
|
+
v_legend[num_periods+1]='Yearly'
|
42
|
+
plt.hist(bins[:-1], bins, weights=counts)
|
43
|
+
plt.legend(v_legend[1:num_periods+2:])
|
44
|
+
#Write line with the limits
|
45
|
+
for j in range(len(v_limits)):
|
46
|
+
h = plt.axvline(v_limits[j], color='r', linestyle='--')
|
47
|
+
#plt.text(0.76, 120000, '1,596 buses out of limits ', fontsize = 10)
|
48
|
+
plt.xlabel(type+' (p.u.)')
|
49
|
+
plt.ylabel('Frequency (p.u.)')
|
50
|
+
plt.savefig(output_file_full_path_fig, dpi=300)
|
51
|
+
plt.show()
|
52
|
+
#Save to file
|
53
|
+
# Write directly as a CSV file with headers on first line
|
54
|
+
with open(output_file_full_path_csv, 'w') as fp:
|
55
|
+
fp.write(','.join(v_legend) + '\n')
|
56
|
+
np.savetxt(fp, matrix, '%s', ',')
|
57
|
+
#Deprecated with dataframes
|
58
|
+
#pd_values = pd.DataFrame(matrix,index=bins)
|
59
|
+
#pd_values.to_csv(output_file_full_path)
|
60
|
+
|
61
|
+
|
62
|
+
def plot_losses(self,v_subs_losses_yearly,v_line_losses_yearly):
|
63
|
+
output_file_full_path_fig = self.folder + '/' + 'Losses' + '.png'
|
64
|
+
plt.figure
|
65
|
+
plt.plot(sorted(v_subs_losses_yearly,reverse=True))
|
66
|
+
plt.plot(sorted(v_line_losses_yearly,reverse=True))
|
67
|
+
plt.plot(sorted(np.add(v_subs_losses_yearly,v_line_losses_yearly),reverse=True))
|
68
|
+
plt.legend(['Substation losses','Line losses','Total losses'])
|
69
|
+
plt.xlabel('Hour (h)')
|
70
|
+
plt.ylabel('Losses (kWh)')
|
71
|
+
plt.savefig(output_file_full_path_fig)
|
72
|
+
plt.show()
|
73
|
+
|
74
|
+
|
75
|
+
def get_graph(self,edges):
|
76
|
+
graph=nx.Graph()
|
77
|
+
for idx,element in enumerate(edges):
|
78
|
+
graph.add_edges_from(element)
|
79
|
+
return graph
|
80
|
+
|
81
|
+
def get_locations(self,graph,bus,locations=[],x_locations_levels={},parent=None,level=0,visited_buses=[]):
|
82
|
+
#Add to the list of visited buses
|
83
|
+
if visited_buses:
|
84
|
+
visited_buses.append(bus)
|
85
|
+
else:
|
86
|
+
visited_buses=[bus]
|
87
|
+
#Obtain the buses connected to this one
|
88
|
+
connected_buses = list(graph.neighbors(bus))
|
89
|
+
#Explore downstream levels
|
90
|
+
x_downstream_locations=[]
|
91
|
+
for downstream_bus in connected_buses:
|
92
|
+
#Remove the terminal from the bus nmae
|
93
|
+
downstream_bus=self.remove_terminal(downstream_bus)
|
94
|
+
#If the bus was already visited, remove from graph (if activated this would remove loops)
|
95
|
+
#if downstream_bus!=parent and downstream_bus in visited_buses and graph.has_edge(bus,downstream_bus) and b_remove_loops:
|
96
|
+
#Remove self loops (possible to happen because of terminals in buses)
|
97
|
+
if downstream_bus==bus:
|
98
|
+
graph.remove_edge(bus,downstream_bus)
|
99
|
+
else:
|
100
|
+
#Explore downstream the graph (recursive search)
|
101
|
+
if downstream_bus!=parent and not downstream_bus in visited_buses:
|
102
|
+
x_loc,locations=self.get_locations(graph,downstream_bus,locations,x_locations_levels,bus,level+1,visited_buses)
|
103
|
+
x_downstream_locations.append(x_loc)
|
104
|
+
#For the upper levels, it takes the average of the downstream buses
|
105
|
+
if x_downstream_locations:
|
106
|
+
loc=(sum(x_downstream_locations)/len(x_downstream_locations),-level);
|
107
|
+
else:
|
108
|
+
if x_locations_levels:
|
109
|
+
#Pick up location from this level or the previous ones
|
110
|
+
#It is neccesary to sort it, to pick in the above for lev loop the x_next_location from the more downstream level
|
111
|
+
for lev in sorted(x_locations_levels):
|
112
|
+
if lev<=level:
|
113
|
+
x_next_location=x_locations_levels[lev]+1
|
114
|
+
#Assign location x, y
|
115
|
+
loc=(x_next_location,-level)
|
116
|
+
else:
|
117
|
+
#Default position for first bus
|
118
|
+
loc=(1,-level)
|
119
|
+
#Assign x location of this level
|
120
|
+
x_locations_levels[level]=loc[0]
|
121
|
+
#update locations
|
122
|
+
if locations:
|
123
|
+
locations[bus]=loc
|
124
|
+
else:
|
125
|
+
locations={bus:loc}
|
126
|
+
#Return x location of this bus (all locations are provided in locations argument)
|
127
|
+
return loc[0],locations
|
128
|
+
|
129
|
+
|
130
|
+
|
131
|
+
|
132
|
+
def plot_graph(self,edges,v_dict_voltage,v_range,v_dict_loading,v_range_loading,dict_buses_element):
|
133
|
+
output_file_full_path_fig = self.folder + '/' + 'Network Violations' + '.png'
|
134
|
+
#output_file_full_path_fig = folder + '/' + type + '.png'
|
135
|
+
#Example
|
136
|
+
graph=nx.Graph()
|
137
|
+
#graph.add_edges_from([(1,2), (1,3), (1,4), (1,5), (1,2), (2,6), (6,7), (7,1)])
|
138
|
+
#discard,locations=self.get_locations(graph,1)
|
139
|
+
graph=self.get_graph(edges)
|
140
|
+
discard,locations=self.get_locations(graph,'st_mat')
|
141
|
+
#Obtain number of violations of each node
|
142
|
+
cmap = plt.cm.get_cmap('jet')
|
143
|
+
dic_num_violations_v={}
|
144
|
+
for node in graph:
|
145
|
+
#Truncate list to limits
|
146
|
+
dic_num_violations_v[node]=0
|
147
|
+
for idx2,value in enumerate(v_dict_voltage[node]):
|
148
|
+
if value<v_range[0] or value>=v_range[1]:
|
149
|
+
dic_num_violations_v[node]=dic_num_violations_v[node]+1
|
150
|
+
#Make colormap
|
151
|
+
color_map_v=[]
|
152
|
+
max_violations_v=max(dic_num_violations_v.values())
|
153
|
+
for node in graph:
|
154
|
+
if max_violations_v==0:
|
155
|
+
intensity=0
|
156
|
+
else:
|
157
|
+
intensity=dic_num_violations_v[node]/max_violations_v
|
158
|
+
#color_map_v.append(cmap_nodes(intensity))
|
159
|
+
color_map_v.append(intensity)
|
160
|
+
#Obtain number of violations of each branch
|
161
|
+
dic_num_violations_l={}
|
162
|
+
for edge in graph.edges():
|
163
|
+
#Truncate list to limits
|
164
|
+
dic_num_violations_l[edge]=0
|
165
|
+
#bus1to2=self.remove_terminal(edge[0])+'-->'+self.remove_terminal(edge[1])
|
166
|
+
#bus2to1=self.remove_terminal(edge[1])+'-->'+self.remove_terminal(edge[0])
|
167
|
+
bus1to2=self.remove_terminal(edge[0])+'-->'+self.remove_terminal(edge[1])
|
168
|
+
bus2to1=self.remove_terminal(edge[1])+'-->'+self.remove_terminal(edge[0])
|
169
|
+
if bus1to2 in dict_buses_element:
|
170
|
+
element=dict_buses_element[bus1to2]
|
171
|
+
else:
|
172
|
+
element=dict_buses_element[bus2to1]
|
173
|
+
for idx2,value in enumerate(v_dict_loading[element]):
|
174
|
+
if value<v_range_loading[0] or value>=v_range_loading[1]:
|
175
|
+
dic_num_violations_l[edge]=dic_num_violations_l[edge]+1
|
176
|
+
#Make colormap
|
177
|
+
color_map_l=[]
|
178
|
+
max_violations_l=max(dic_num_violations_l.values())
|
179
|
+
for edge in graph.edges():
|
180
|
+
if max_violations_l==0:
|
181
|
+
intensity=0
|
182
|
+
else:
|
183
|
+
intensity=dic_num_violations_l[edge]/max_violations_l
|
184
|
+
#color_map_v.append(cmap_nodes(intensity))
|
185
|
+
color_map_l.append(intensity)
|
186
|
+
#Obtain min and max of x locations
|
187
|
+
max_x=0
|
188
|
+
max_y=0
|
189
|
+
for idx,name in enumerate(locations):
|
190
|
+
if (max_x<locations[name][0]):
|
191
|
+
max_x=locations[name][0]
|
192
|
+
if (max_y<-locations[name][1]):
|
193
|
+
max_y=-locations[name][1]
|
194
|
+
#plt.figure(figsize=(max_x,max_y))
|
195
|
+
unitary_size=0.5
|
196
|
+
ratio=16/9
|
197
|
+
maximum=max(max_x,max_y)*unitary_size
|
198
|
+
plt.figure(figsize=(maximum*ratio,maximum))
|
199
|
+
nodes = nx.draw_networkx_nodes(graph, pos=locations, node_color=color_map_v, cmap=cmap)
|
200
|
+
edges=nx.draw_networkx_edges(graph,pos=locations, edge_color=color_map_l,width=4,edge_cmap=cmap)
|
201
|
+
nx.draw_networkx_labels(graph, pos=locations)
|
202
|
+
num_ticks_v=5
|
203
|
+
ticks_v = np.linspace(0, 1, num_ticks_v)
|
204
|
+
labels_v = np.linspace(0, max_violations_v, num_ticks_v)
|
205
|
+
cbar=plt.colorbar(nodes,ticks=ticks_v)
|
206
|
+
cbar.ax.set_yticklabels(["{:4.2f}".format(i) for i in labels_v]) # add the labels
|
207
|
+
cbar.set_label("Voltage violations (h)", fontsize=10, y=0.5, rotation=90)
|
208
|
+
cbar.ax.yaxis.set_label_position('left')
|
209
|
+
num_ticks_l=5
|
210
|
+
ticks_l = np.linspace(0, 1, num_ticks_l)
|
211
|
+
labels_l = np.linspace(0, max_violations_l, num_ticks_l)
|
212
|
+
cbar=plt.colorbar(edges,ticks=ticks_l)
|
213
|
+
cbar.ax.set_yticklabels(["{:4.2f}".format(i) for i in labels_l]) # add the labels
|
214
|
+
cbar.set_label("Thermal limit violations (h)", fontsize=10, y=0.5, rotation=90)
|
215
|
+
cbar.ax.yaxis.set_label_position('left')
|
216
|
+
plt.axis('off')
|
217
|
+
wm = plt.get_current_fig_manager()
|
218
|
+
wm.window.state('zoomed')
|
219
|
+
plt.savefig(output_file_full_path_fig, dpi=300)
|
220
|
+
plt.show()
|
221
|
+
|
222
|
+
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# *********************************************************************************
|
2
|
+
# URBANopt (tm), Copyright (c) 2019-2022, 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
|
+
|
43
|
+
module URBANopt
|
44
|
+
module RNM
|
45
|
+
# Class for OpenDSS validation (runs python script)
|
46
|
+
class Validation
|
47
|
+
##
|
48
|
+
# Initialize attributes: ++run directory+
|
49
|
+
##
|
50
|
+
# [parameters:]
|
51
|
+
# * +rnm_dirname+ - _String_ - name of RNM-US directory that will contain the input files (within the scenario directory)
|
52
|
+
def initialize(rnm_full_path)
|
53
|
+
# absolute path
|
54
|
+
@rnm_full_path = rnm_full_path
|
55
|
+
@opendss_full_path=File.join(@rnm_full_path,'results/OpenDSS')
|
56
|
+
if !Dir.exist?(@opendss_full_path)
|
57
|
+
puts 'Error: folder does not exist'+@rnm_full_path
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
##
|
63
|
+
# Run validation
|
64
|
+
##
|
65
|
+
def run_validation()
|
66
|
+
puts "Initating OpenDSS validation in folder"
|
67
|
+
puts @opendss_full_path
|
68
|
+
puts "This can take some minutes"
|
69
|
+
#puts `python ./lib/urbanopt/rnm/validation/main_validation.py #{@rnm_full_path}`
|
70
|
+
log=`python ./lib/urbanopt/rnm/validation/main_validation.py #{@opendss_full_path}`
|
71
|
+
puts log
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
data/lib/urbanopt/rnm/version.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
# *********************************************************************************
|
2
|
-
# URBANopt (tm), Copyright (c) 2019-
|
2
|
+
# URBANopt (tm), Copyright (c) 2019-2022, Alliance for Sustainable Energy, LLC, and other
|
3
3
|
# contributors. All rights reserved.
|
4
4
|
|
5
5
|
# Redistribution and use in source and binary forms, with or without modification,
|
@@ -40,6 +40,6 @@
|
|
40
40
|
|
41
41
|
module URBANopt
|
42
42
|
module RNM
|
43
|
-
VERSION = '0.
|
43
|
+
VERSION = '0.4.0'.freeze
|
44
44
|
end
|
45
45
|
end
|
@@ -1,5 +1,5 @@
|
|
1
1
|
# *********************************************************************************
|
2
|
-
# URBANopt (tm), Copyright (c) 2019-
|
2
|
+
# URBANopt (tm), Copyright (c) 2019-2022, Alliance for Sustainable Energy, LLC, and other
|
3
3
|
# contributors. All rights reserved.
|
4
4
|
|
5
5
|
# Redistribution and use in source and binary forms, with or without modification,
|