urbanopt-rnm-us 0.4.0 → 0.5.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/.gitignore +3 -1
- data/CHANGELOG.md +5 -0
- data/Gemfile +6 -5
- data/README.md +15 -1
- data/Rakefile +31 -6
- data/lib/urbanopt/rnm/api_client.rb +8 -0
- data/lib/urbanopt/rnm/consumers.rb +14 -2
- data/lib/urbanopt/rnm/input_files.rb +18 -0
- data/lib/urbanopt/rnm/prosumers.rb +17 -1
- data/lib/urbanopt/rnm/validation/main_validation.py +135 -33
- data/lib/urbanopt/rnm/validation/opendss_interface.py +458 -79
- data/lib/urbanopt/rnm/validation/plot_lib.py +430 -124
- data/lib/urbanopt/rnm/validation/report.py +370 -0
- data/lib/urbanopt/rnm/validation.rb +8 -6
- data/lib/urbanopt/rnm/version.rb +1 -1
- data/requirements.txt +9 -0
- data/urbanopt-rnm-us-gem.gemspec +1 -1
- metadata +6 -4
@@ -5,67 +5,307 @@ import numpy as np
|
|
5
5
|
import sys as sys
|
6
6
|
import math
|
7
7
|
import networkx as nx
|
8
|
+
from datetime import datetime
|
8
9
|
|
9
10
|
class OpenDSS_Interface:
|
10
|
-
def __init__(self, folder):
|
11
|
-
|
11
|
+
def __init__(self, folder,b_numeric_ids):
|
12
|
+
"""Initialices the folder variables"""
|
13
|
+
self.main_folder = folder
|
14
|
+
self.folder=folder+'/Validation'
|
15
|
+
self.b_numeric_ids=b_numeric_ids
|
12
16
|
|
13
17
|
def remove_terminal(self,bus):
|
18
|
+
"""Removes the terminal from the bus name"""
|
14
19
|
if isinstance(bus,str):
|
15
|
-
return bus.split('.')[0]
|
20
|
+
return bus.split('.')[0] #(everything to the right of point ".")
|
16
21
|
else:
|
17
22
|
return bus
|
18
23
|
|
24
|
+
def is_to_be_analyzed(self,name):
|
25
|
+
"""Determines if an element has to be analyzed"""
|
26
|
+
b_analyzed=False
|
27
|
+
if name.startswith('Line.padswitch'): #RNM specific #We only condider power lines and tarnsformers as branches
|
28
|
+
b_analyzed=False
|
29
|
+
elif name.startswith('Line.breaker'): #RNM specific
|
30
|
+
b_analyzed=False
|
31
|
+
elif name.startswith('Line.fuse'): #RNM specific
|
32
|
+
b_analyzed=False
|
33
|
+
elif name.startswith('Capacitor'):
|
34
|
+
b_analyzed=False
|
35
|
+
elif name.startswith('Line.l'): #RNM specific
|
36
|
+
b_analyzed=True
|
37
|
+
elif name.startswith('Transformer'):
|
38
|
+
b_analyzed=True
|
39
|
+
else:
|
40
|
+
print("Component type was not explicitly consiered in the validation module. It is not analyzed.")
|
41
|
+
print(name)
|
42
|
+
return b_analyzed
|
19
43
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
44
|
+
|
45
|
+
|
46
|
+
def extract_period(self,v_value_i,v_value_period,i,end_index,num_periods,month):
|
47
|
+
"""#Assign v_value_i to the corresponding month"""
|
48
|
+
v_value_period[month-1].extend(v_value_i)
|
25
49
|
return v_value_period
|
26
50
|
|
27
|
-
def add_to_dictionary(self,
|
28
|
-
|
29
|
-
|
30
|
-
|
51
|
+
def add_to_dictionary(self,dict_all,dict_i):
|
52
|
+
"""Adds dict_i to the dictonary dict_all"""
|
53
|
+
for idx,name in enumerate(dict_i):
|
54
|
+
if name in dict_all: #if not empty
|
55
|
+
dict_all[name].append(dict_i[name])
|
31
56
|
else:
|
32
|
-
|
57
|
+
dict_all[name]=[dict_i[name]]
|
33
58
|
|
34
59
|
def dss_run_command(self,command):
|
60
|
+
"""Runs an OpenDSS Direct command"""
|
61
|
+
#Run command
|
35
62
|
output=dss.run_command(command)
|
63
|
+
#If it has any output, print it
|
36
64
|
if (len(output)>0):
|
37
65
|
print(output)
|
38
66
|
|
39
67
|
|
68
|
+
def get_time_stamp(self):
|
69
|
+
"""Read the timestamps and obtain the raw and the formatted version"""
|
70
|
+
#Select file
|
71
|
+
timestamp_file = self.main_folder + '/profiles/' + 'timestamps.csv'
|
72
|
+
#Read file
|
73
|
+
timestamps = []
|
74
|
+
with open(timestamp_file) as csv_data_file:
|
75
|
+
for row in csv_data_file:
|
76
|
+
timestamps.append(row.strip())
|
77
|
+
#remove the header
|
78
|
+
timestamps.pop(0)
|
79
|
+
#Define date time format
|
80
|
+
dt_format = '%Y/%m/%d %H:%M:%S'
|
81
|
+
#Convert to datetime structure
|
82
|
+
timestamps_datetime= [datetime.strptime(i,dt_format) for i in timestamps]
|
83
|
+
return timestamps,timestamps_datetime
|
84
|
+
|
85
|
+
|
40
86
|
def get_all_voltage(self):
|
41
87
|
"""Computes over and under voltages for all buses"""
|
88
|
+
#Get bus names
|
42
89
|
bus_names = dss.Circuit.AllBusNames()
|
90
|
+
#Init variables
|
43
91
|
dict_voltage = {}
|
44
92
|
v_voltage = [0 for _ in range(len(bus_names))]
|
93
|
+
#For each bus
|
45
94
|
for idx,b in enumerate(bus_names):
|
95
|
+
#Set it as active bus
|
46
96
|
dss.Circuit.SetActiveBus(b)
|
97
|
+
#Get voltage and angle
|
47
98
|
vang = dss.Bus.puVmagAngle()
|
99
|
+
#Get voltage magnitude
|
48
100
|
if len(vang[::2]) > 0:
|
49
|
-
|
101
|
+
#Average of the voltages in all the phases, discarding the angles
|
102
|
+
vmag = sum(vang[::2])/(len(vang)/2)
|
50
103
|
else:
|
51
104
|
vmag = 0
|
105
|
+
#Add voltage magnitude to dictionary and to list of voltages
|
52
106
|
dict_voltage[b] = vmag
|
53
107
|
v_voltage[idx]=vmag
|
54
108
|
|
55
109
|
return dict_voltage,v_voltage
|
56
110
|
|
111
|
+
|
112
|
+
def get_all_unbalance(self):
|
113
|
+
"""Computes voltage unbalance for all buses"""
|
114
|
+
# Based on IEEE standard 141-1993 https://www.sciencedirect.com/science/article/pii/S0378779620304594
|
115
|
+
#Get bus names
|
116
|
+
bus_names = dss.Circuit.AllBusNames()
|
117
|
+
#Init variables
|
118
|
+
dict_voltage = {}
|
119
|
+
v_voltage = [0 for _ in range(len(bus_names))]
|
120
|
+
#For each bus
|
121
|
+
for idx,b in enumerate(bus_names):
|
122
|
+
dss.Circuit.SetActiveBus(b)
|
123
|
+
#Set it as active bus
|
124
|
+
#Get voltage and angle
|
125
|
+
vang = dss.Bus.puVmagAngle()
|
126
|
+
#Evaluate the unbalance
|
127
|
+
if len(vang[::2]) ==3: #if three-phase
|
128
|
+
vmedio = sum(vang[::2])/(len(vang)/2) #Average of the voltages in all the phases, discarding the angles
|
129
|
+
va=vang[0] #Phase A
|
130
|
+
vb=vang[2] #Phase B
|
131
|
+
vc=vang[4] #Phase C
|
132
|
+
vmax=max(abs(va-vmedio),abs(vb-vmedio),abs(vc-vmedio)) #Phase Voltage Unbalance Rate (PVUR) (based on IEEE)
|
133
|
+
elif len(vang[::2]) ==2: #If two phase
|
134
|
+
vmedio = sum(vang[::2])/(len(vang)/2) #Average of the voltages in all the phases, discarding the angles
|
135
|
+
va=vang[0] #Phase A
|
136
|
+
vb=vang[2] #Phase B
|
137
|
+
vmax=max(abs(va-vmedio),abs(vb-vmedio)) #Phase Voltage Unbalance Rate (PVUR)
|
138
|
+
elif len(vang[::2]) ==1: #If single-phase
|
139
|
+
vmax = 0 #No unblance
|
140
|
+
else: #Not other cases are considered
|
141
|
+
print("Value: "+str(vang)+"Lend: "+str(len(vang)))
|
142
|
+
raise Exception("Voltage is not single-, two- or three-phase")
|
143
|
+
#Add unbalance to dictionary and to list of unbalances
|
144
|
+
dict_voltage[b] = vmax
|
145
|
+
v_voltage[idx]=vmax
|
146
|
+
return dict_voltage,v_voltage
|
147
|
+
|
148
|
+
|
149
|
+
def get_all_loads(self):
|
150
|
+
"""Get all loads peak kW and kVAr"""
|
151
|
+
#Init variables
|
152
|
+
dict_loads = {}
|
153
|
+
myrange=range(0,dss.Loads.Count())
|
154
|
+
v_loads_kw = [0 for _ in myrange]
|
155
|
+
v_loads_kvar = [0 for _ in myrange]
|
156
|
+
#For each load
|
157
|
+
for idx in myrange:
|
158
|
+
#Set load index
|
159
|
+
dss.Loads.Idx(idx+1)
|
160
|
+
#Get kW of the load
|
161
|
+
kw = dss.Loads.kW()
|
162
|
+
#Get kVAr of the load
|
163
|
+
kvar = dss.Loads.kvar()
|
164
|
+
#Get load name
|
165
|
+
name = 'LOAD.'+dss.Loads.Name()
|
166
|
+
#Add load to dictionary and to list of load kW/kVAr
|
167
|
+
dict_loads[name] = kw
|
168
|
+
v_loads_kw[idx]=kw
|
169
|
+
v_loads_kvar[idx]=kvar
|
170
|
+
return dict_loads,v_loads_kw,v_loads_kvar
|
171
|
+
|
172
|
+
def get_all_loadshapes(self,i):
|
173
|
+
"""Get all loadshapes"""
|
174
|
+
dict_loads = {}
|
175
|
+
#Init variables
|
176
|
+
myrange=range(0,dss.LoadShape.Count())
|
177
|
+
v_loads_kw = [0 for _ in myrange]
|
178
|
+
v_loads_kvar = [0 for _ in myrange]
|
179
|
+
#For each loadshape
|
180
|
+
for idx in myrange:
|
181
|
+
#Set loadshape index
|
182
|
+
dss.LoadShape.Idx(idx+1)
|
183
|
+
#Get hourly kW
|
184
|
+
kw = dss.LoadShape.PMult()
|
185
|
+
#Get hourly kVAr
|
186
|
+
kvar = dss.LoadShape.QMult()
|
187
|
+
#Get load name
|
188
|
+
name = 'LOAD.'+dss.LoadShape.Name()
|
189
|
+
#If not default load (discard it)
|
190
|
+
if not(name=='LOAD.default'):
|
191
|
+
if len(kw)>1:
|
192
|
+
#Add load to dictionary and to list of load kW
|
193
|
+
dict_loads[name] = kw[i]
|
194
|
+
v_loads_kw[idx]=kw[i]
|
195
|
+
if len(kvar)>1:
|
196
|
+
#Add to list of load kW
|
197
|
+
v_loads_kvar[idx]=kvar[i]
|
198
|
+
|
199
|
+
return dict_loads,v_loads_kw,v_loads_kvar
|
200
|
+
|
201
|
+
|
202
|
+
def get_all_buses_loads(self,i):
|
203
|
+
"""Get the load of all the buses"""
|
204
|
+
#Get data from load shapes
|
205
|
+
dict_loads,v_loads_kw,v_loads_kvar=self.get_all_loadshapes(i)
|
206
|
+
#Get all bus names
|
207
|
+
bus_names = dss.Circuit.AllBusNames()
|
208
|
+
#Init dict
|
209
|
+
dict_buses_loads = {}
|
210
|
+
#For each bus
|
211
|
+
for idx,b in enumerate(bus_names):
|
212
|
+
#Set it to the active bus
|
213
|
+
dss.Circuit.SetActiveBus(b)
|
214
|
+
#Get its loads
|
215
|
+
loads = dss.Bus.LoadList()
|
216
|
+
#Init its load to zero
|
217
|
+
load_bus=0
|
218
|
+
#For each load in the bus
|
219
|
+
for l in loads:
|
220
|
+
#Assign it the load in the loadshape
|
221
|
+
load_bus=load_bus+dict_loads[l+"_profile"] #RNM-US specific (the load shapes names are the name of the laods + "_profile")
|
222
|
+
#Add to dictionary
|
223
|
+
dict_buses_loads[b] = load_bus
|
224
|
+
return dict_buses_loads,v_loads_kw,v_loads_kvar
|
225
|
+
|
226
|
+
|
227
|
+
def get_all_buses_ids(self):
|
228
|
+
"""Get the load of all the buses"""
|
229
|
+
#Get all bus names
|
230
|
+
bus_names = dss.Circuit.AllBusNames()
|
231
|
+
#Init dict
|
232
|
+
dict_buses_ids = {}
|
233
|
+
dict_ids_buses = {}
|
234
|
+
#Init numeric identifier
|
235
|
+
num_id=1
|
236
|
+
#For each bus
|
237
|
+
for idx,b in enumerate(bus_names):
|
238
|
+
if b=='st_mat': #RNM-US specific (st_mat is the slack bus)
|
239
|
+
dict_buses_ids[b] = str(0)
|
240
|
+
dict_ids_buses[str(0)] = b
|
241
|
+
else:
|
242
|
+
dict_buses_ids[b] = str(num_id)
|
243
|
+
dict_ids_buses[str(num_id)] = b
|
244
|
+
num_id=num_id+1
|
245
|
+
return dict_buses_ids,dict_ids_buses
|
246
|
+
|
247
|
+
def get_all_lines(self):
|
248
|
+
"""Gets the normal ampacity of power lines"""
|
249
|
+
#Init variables
|
250
|
+
dict_lines = {}
|
251
|
+
myrange=range(0,dss.Lines.Count())
|
252
|
+
v_lines_norm_amps = []
|
253
|
+
#For each power line
|
254
|
+
for idx in myrange:
|
255
|
+
#Set power line index
|
256
|
+
dss.Lines.Idx(idx+1)
|
257
|
+
#Get the normal amapcity
|
258
|
+
normal_amps = dss.Lines.NormAmps()
|
259
|
+
#Get the power line name
|
260
|
+
name = 'Line.'+dss.Lines.Name()
|
261
|
+
#If it is a power line
|
262
|
+
if name.startswith("Line.l("): #RNM-US specific (all power lines start with "Line.l("). This discards for example fuses, switches, ....
|
263
|
+
#Add to dictionary and to list
|
264
|
+
dict_lines[name] = normal_amps
|
265
|
+
v_lines_norm_amps.append(normal_amps)
|
266
|
+
|
267
|
+
return dict_lines,v_lines_norm_amps
|
268
|
+
|
269
|
+
def get_all_transformers(self):
|
270
|
+
"""Gets the size of transformers"""
|
271
|
+
#Init variables
|
272
|
+
dict_transformers = {}
|
273
|
+
myrange=range(0,dss.Transformers.Count())
|
274
|
+
v_transformers_kva = []
|
275
|
+
#For each transformer
|
276
|
+
for idx in myrange:
|
277
|
+
#Set the transformer index
|
278
|
+
dss.Transformers.Idx(idx+1)
|
279
|
+
#Get the transformer size in kVA
|
280
|
+
kva = dss.Transformers.kVA()
|
281
|
+
#Get the transformer size
|
282
|
+
name = 'Transformer.'+dss.Transformers.Name()
|
283
|
+
#If it is a distribution transformer
|
284
|
+
if name.startswith("Transformer.tr("): #Distribution transformer, RNM-US specific (all distribution transformers start with "Transformer.tr("). This discards for example transformers in primary substations
|
285
|
+
#Add to dictionary and to list
|
286
|
+
dict_transformers[name] = kva
|
287
|
+
v_transformers_kva.append(kva)
|
288
|
+
return dict_transformers,v_transformers_kva
|
289
|
+
|
290
|
+
|
57
291
|
def get_all_power(self):
|
58
|
-
"""Computes power in all circuits"""
|
292
|
+
"""Computes power in all circuits (not used, loading is measured instead)"""
|
293
|
+
#Get all element names
|
59
294
|
circuit_names = dss.Circuit.AllElementNames()
|
295
|
+
#Init variables
|
60
296
|
dict_power = {}
|
61
297
|
v_power = [0 for _ in range(len(circuit_names))]
|
298
|
+
#For each circuit
|
62
299
|
for idx,b in enumerate(circuit_names):
|
300
|
+
#Set the active element
|
63
301
|
dss.Circuit.SetActiveElement(b)
|
302
|
+
#Calculates the power through the circuit
|
64
303
|
power = dss.CktElement.Powers()
|
65
304
|
if len(power[::2]) > 0:
|
66
305
|
poweravg = sum(power[::2])/(len(power)/2)
|
67
306
|
else:
|
68
307
|
poweravg = 0
|
308
|
+
#Add to dictionary and to list
|
69
309
|
dict_power[b] = poweravg
|
70
310
|
v_power[idx]=poweravg
|
71
311
|
|
@@ -74,66 +314,77 @@ class OpenDSS_Interface:
|
|
74
314
|
|
75
315
|
def get_all_loading(self):
|
76
316
|
"""Computes loading in all circuits"""
|
317
|
+
#Get all element names
|
77
318
|
circuit_names = dss.Circuit.AllElementNames()
|
319
|
+
#Init variables
|
78
320
|
dict_loading = {}
|
79
321
|
dict_buses_element={} #Associate the element to the buses (this has the inconvenient that only associates one element to each pair of buses)
|
80
322
|
v_loading = [0 for _ in range(len(circuit_names))]
|
323
|
+
#For each circuit
|
81
324
|
for idx,element in enumerate(circuit_names):
|
325
|
+
#Set the active element
|
82
326
|
dss.Circuit.SetActiveElement(element)
|
83
|
-
#
|
327
|
+
#Get the buses in the elment
|
84
328
|
buses = dss.CktElement.BusNames()
|
329
|
+
#Evaluate only if it is a branch (two buses)
|
85
330
|
if (len(buses)>=2):
|
331
|
+
#Obtain the current through the element
|
86
332
|
current = dss.CktElement.CurrentsMagAng()
|
333
|
+
#Obtain the number of terminals
|
87
334
|
num_terminals=dss.CktElement.NumTerminals()
|
88
|
-
|
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
|
335
|
+
#Obtain the current magnitude (first terminal only, because in transformers NormalAmps gives the normal ampacity of the first winding) (to compare the same magnitudes)
|
97
336
|
currentmag = current[0]
|
337
|
+
#Obtain the normal amapcity
|
98
338
|
nominal_current = dss.CktElement.NormalAmps()
|
99
339
|
#Transformers have applied a 1.1 factor in the calculation of NormalAmps
|
100
340
|
#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;
|
341
|
+
#in particular line code: AmpRatings[i] := 1.1 * kVARatings[i] / Fnphases / Vfactor;
|
342
|
+
#Remove (do not use) the 1.1 margin set in the library to the nominal current
|
102
343
|
if (element.startswith("Transformer")):
|
103
|
-
nominal_current=nominal_current/1.1
|
104
|
-
|
344
|
+
nominal_current=nominal_current/1.1
|
345
|
+
#If the element is to be analyzed and has a nominal current (this discards vsources)
|
346
|
+
if (nominal_current>0 and self.is_to_be_analyzed(element)):
|
347
|
+
#Add to dictionaries and to vector
|
105
348
|
dict_loading[element] = currentmag/nominal_current
|
106
349
|
v_loading[idx]=currentmag/nominal_current
|
107
350
|
bus1to2=self.remove_terminal(buses[0])+'-->'+self.remove_terminal(buses[1])
|
108
351
|
dict_buses_element[bus1to2]=element
|
109
|
-
|
110
352
|
return dict_loading,v_loading,dict_buses_element
|
111
353
|
|
112
354
|
def get_all_losses(self):
|
113
355
|
"""Computes losses in all circuits"""
|
356
|
+
#Get all element names
|
114
357
|
circuit_names = dss.Circuit.AllElementNames()
|
358
|
+
#Init variables
|
115
359
|
dict_losses = {}
|
116
360
|
v_losses = [0 for _ in range(len(circuit_names))]
|
117
361
|
total_losses=0
|
362
|
+
#For each element
|
118
363
|
for idx,element in enumerate(circuit_names):
|
364
|
+
#Set it as active element
|
119
365
|
dss.Circuit.SetActiveElement(element)
|
120
|
-
#
|
366
|
+
#Get buses names in element
|
121
367
|
buses = dss.CktElement.BusNames()
|
368
|
+
#only if it is a branch (two buses)
|
122
369
|
if (len(buses)>=2):
|
123
|
-
#
|
370
|
+
#Get nominal current
|
124
371
|
nominal_current = dss.CktElement.NormalAmps()
|
372
|
+
#If it has nominal current (this discards vsources)
|
125
373
|
if (nominal_current>0):
|
374
|
+
#Get hte losses
|
126
375
|
losses = dss.CktElement.Losses()
|
127
376
|
if len(losses) ==2:
|
128
|
-
lossesavg = (losses[0]) #
|
377
|
+
lossesavg = (losses[0]) #[0] to take active losses
|
129
378
|
else:
|
130
|
-
print("
|
379
|
+
print("Error - not correctly reading losses")
|
131
380
|
lossesavg=0
|
132
|
-
#Convert to kW
|
381
|
+
#Convert to kW (becase CktElement.Losses is the exeption that return losses in W)
|
133
382
|
lossesavg=lossesavg/1000
|
134
|
-
|
135
|
-
|
136
|
-
|
383
|
+
#If element is to be analized
|
384
|
+
if self.is_to_be_analyzed(element):
|
385
|
+
#Add to dictionary an to list
|
386
|
+
dict_losses[element] = lossesavg
|
387
|
+
v_losses[idx]=lossesavg
|
137
388
|
return dict_losses,total_losses
|
138
389
|
|
139
390
|
def get_total_subs_losses(self):
|
@@ -141,81 +392,191 @@ class OpenDSS_Interface:
|
|
141
392
|
return dss.Circuit.SubstationLosses()[0] #Real part
|
142
393
|
|
143
394
|
def get_total_line_losses(self):
|
144
|
-
"""Computes total line losses"""
|
395
|
+
"""Computes total power line losses"""
|
145
396
|
return dss.Circuit.LineLosses()[0] #Real part
|
146
397
|
|
147
|
-
def get_edges(self):
|
148
|
-
edges
|
398
|
+
def get_edges(self,v_dict_buses_ids):
|
399
|
+
"""Gets the edges of the distribution system (to obtain the graph of the network)"""
|
400
|
+
#Get all element names
|
149
401
|
circuit_names = dss.Circuit.AllElementNames()
|
402
|
+
#Init variable
|
403
|
+
closed_edges=[]
|
404
|
+
open_edges=[]
|
405
|
+
#For all elements
|
150
406
|
for idx,element in enumerate(circuit_names):
|
407
|
+
#Set it as active element
|
151
408
|
dss.Circuit.SetActiveElement(element)
|
409
|
+
#Get the bus names
|
152
410
|
buses = dss.CktElement.BusNames()
|
153
411
|
#Only if it is a branch
|
154
412
|
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
|
-
#
|
156
|
-
|
157
|
-
|
413
|
+
#Avoid cases bus1=bus2 (after removing terminals)
|
414
|
+
if (self.remove_terminal(buses[0])!=self.remove_terminal(buses[1])):
|
415
|
+
#Identify if enabled #RNM-US specific (open loops are modelled with enabled=n)
|
416
|
+
b_enabled=dss.CktElement.Enabled()
|
417
|
+
#if Enabled (i.e. it it is closed)
|
418
|
+
if (b_enabled):
|
419
|
+
#Add to edges
|
420
|
+
#remove terminal from the bus name (everything to the right of point)
|
421
|
+
#closed_edges.append([(self.remove_terminal(buses[0]),self.remove_terminal(buses[1]))])
|
422
|
+
if (self.b_numeric_ids):
|
423
|
+
closed_edges.append((v_dict_buses_ids[self.remove_terminal(buses[0])],v_dict_buses_ids[self.remove_terminal(buses[1])]))
|
424
|
+
else:
|
425
|
+
closed_edges.append((self.remove_terminal(buses[0]),self.remove_terminal(buses[1])))
|
426
|
+
elif (not b_enabled): #if not Enabled (i.e. it it is open)
|
427
|
+
#open_edges.append([(self.remove_terminal(buses[0]),self.remove_terminal(buses[1]))])
|
428
|
+
if (self.b_numeric_ids):
|
429
|
+
open_edges.append((v_dict_buses_ids[self.remove_terminal(buses[0])],v_dict_buses_ids[self.remove_terminal(buses[1])]))
|
430
|
+
else:
|
431
|
+
open_edges.append((self.remove_terminal(buses[0]),self.remove_terminal(buses[1])))
|
432
|
+
return closed_edges,open_edges
|
433
|
+
|
434
|
+
|
435
|
+
|
436
|
+
def is_violation(self,value,v_range):
|
437
|
+
"""Obtain number of violations (hours) of a bus"""
|
438
|
+
#Init to zero
|
439
|
+
num=0
|
440
|
+
#If out of range
|
441
|
+
if value<v_range['allowed_range'][0] or value>=v_range['allowed_range'][1]:
|
442
|
+
num=1 #Number of violations=1
|
443
|
+
else: #Else
|
444
|
+
num=0 #Number of violations=0
|
445
|
+
return num
|
158
446
|
|
159
|
-
|
160
|
-
|
447
|
+
|
448
|
+
|
449
|
+
def get_num_violations(self,v_value,v_range,name,dict_loads):
|
450
|
+
""""Obtain number of violations (hours) of a bus"""
|
451
|
+
#Init to zero
|
452
|
+
num_violations=0
|
453
|
+
#For each value
|
454
|
+
for idx2,value in enumerate(v_value):
|
455
|
+
#If no dict of loads, add 1 if there is a violation in that value (if it is outside of the allowed range)
|
456
|
+
if (dict_loads is None):
|
457
|
+
num_violations=num_violations+self.is_violation(value,v_range)
|
458
|
+
#If there is a dict of loads
|
459
|
+
elif (name in dict_loads):
|
460
|
+
if (dict_loads[name][idx2]>0): #only compute if there is load
|
461
|
+
#If there is a vioaltion, add the load (to compute the energy delivered with violations)
|
462
|
+
num_violations=num_violations+self.is_violation(value,v_range)*dict_loads[name][idx2]
|
463
|
+
return num_violations
|
464
|
+
|
465
|
+
|
466
|
+
def write_dict(self,subfolder,v_dict,v_range,type,component,v_dict_buses_ids,timestamps,v_hours):
|
467
|
+
"""Writes the dictionary to a file"""
|
468
|
+
#Path and file name
|
469
|
+
output_file_full_path = self.folder + '/' + subfolder + '/' + type + '_' + component + '.csv'
|
161
470
|
# Write directly as a CSV file with headers on first line
|
162
471
|
with open(output_file_full_path, 'w') as fp:
|
163
|
-
#Header: ID, hours
|
472
|
+
#Header: ID, hours
|
164
473
|
for idx,name in enumerate(v_dict):
|
165
|
-
|
474
|
+
if (self.b_numeric_ids and not component=='Branches'):
|
475
|
+
fp.write(',/ '+'Date,'+','.join(str(value) for idx2,value in enumerate(timestamps)) + '\n')
|
476
|
+
fp.write('Num. ID,bus / '+'Hour,'+','.join(str(value) for value in v_hours) + '\n')
|
477
|
+
else:
|
478
|
+
fp.write('Date,'+','.join(str(value) for idx2,value in enumerate(timestamps)) + '\n')
|
479
|
+
fp.write('Hour,'+','.join(str(value) for value in v_hours) + '\n')
|
166
480
|
break
|
167
481
|
#Write matrix
|
168
482
|
for idx,name in enumerate(v_dict):
|
169
|
-
#
|
483
|
+
#Init list
|
170
484
|
truncated_values=[]
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
truncated_values.append("")
|
176
|
-
|
177
|
-
|
178
|
-
#
|
485
|
+
#For each one
|
486
|
+
for idx2,value in enumerate(v_dict[name]):
|
487
|
+
#if it is outsie of the allowed range
|
488
|
+
if not(v_range['allowed_range']) or value<v_range['allowed_range'][0] or value>=v_range['allowed_range'][1]:
|
489
|
+
truncated_values.append("{:.7f}".format(value)) #Add the value
|
490
|
+
else: #else
|
491
|
+
truncated_values.append("") #Fill with an empty variable
|
492
|
+
#Write to file
|
493
|
+
if v_dict_buses_ids is None or not self.b_numeric_ids:
|
494
|
+
fp.write(name+','+','.join(truncated_values)+'\n')
|
495
|
+
else:
|
496
|
+
fp.write(str(v_dict_buses_ids[name])+','+name+','+','.join(truncated_values)+'\n')
|
179
497
|
|
180
498
|
|
181
|
-
def
|
182
|
-
|
499
|
+
def write_id_dict(self,subfolder,type,v_dict_buses_ids):
|
500
|
+
"""Writes the dictionary to a file"""
|
501
|
+
#Path and file name
|
502
|
+
output_file_full_path = self.folder + '/' + subfolder + '/' + type + '.csv'
|
503
|
+
# Write directly as a CSV file with headers on first line
|
504
|
+
with open(output_file_full_path, 'w') as fp:
|
505
|
+
#Header: ID, bus
|
506
|
+
fp.write('Num. ID,bus' + '\n')
|
507
|
+
for idx,name in enumerate(v_dict_buses_ids):
|
508
|
+
fp.write(str(v_dict_buses_ids[name])+','+name+'\n')
|
509
|
+
|
510
|
+
|
511
|
+
def solve_powerflow_iteratively(self,num_periods,start_index,end_index,location,v_range_voltage,v_range_loading,v_range_unbalance):
|
512
|
+
"""Solves the power flow iteratively"""
|
513
|
+
#Get timestamps
|
514
|
+
timestamps,timestamps_datetime=self.get_time_stamp()
|
515
|
+
if start_index is None or start_index<0:
|
516
|
+
start_index=0
|
517
|
+
if end_index is None or end_index>len(timestamps):
|
518
|
+
end_index=len(timestamps)
|
519
|
+
v_hours_sim=range(start_index,end_index,1) #Hours to run OpenDSS
|
520
|
+
v_hours=range(start_index+1,end_index+1,1) #Hours for outputting
|
521
|
+
#Por flow solving mode (hourly)
|
183
522
|
self.dss_run_command("Clear")
|
184
523
|
self.dss_run_command('Redirect '+location)
|
185
524
|
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={}
|
525
|
+
self.dss_run_command("Set mode=yearly stepsize=1h number="+str(start_index+1))
|
199
526
|
#Additional initializations
|
200
|
-
|
527
|
+
#Init vectors
|
528
|
+
v_months=[[] for _ in range(num_periods)] #Months
|
529
|
+
v_voltage_yearly=[] #Yearly votlage
|
530
|
+
v_voltage_period=[[] for _ in range(num_periods)] #Montly voltage
|
531
|
+
v_unbalance_yearly=[] #Yearly unbalance
|
532
|
+
v_unbalance_period=[[] for _ in range(num_periods)] #Monthly unbalance
|
533
|
+
v_power_yearly=[] #Yearly power
|
534
|
+
v_power_period=[[] for _ in range(num_periods)] #Montly pwoer
|
535
|
+
v_loading_yearly=[] #Yearly loading
|
536
|
+
v_loading_period=[[] for _ in range(num_periods)] #Montly loading
|
537
|
+
v_subs_losses_yearly=[] #Yearly substation losses
|
538
|
+
v_line_losses_yearly=[] #Yearly power line losses
|
539
|
+
v_loads_kw_yearly=[] #Yearly kW of loads (for violing plots)
|
540
|
+
v_loads_kw_period=[[] for _ in range(num_periods)] #Montly kW of loads (for violing plots)
|
541
|
+
v_loads_kvar_yearly=[] #Yearly kVAr of loads (for violing plots)
|
542
|
+
v_loads_kvar_period=[[] for _ in range(num_periods)]#Montly kVAr of loads (for violing plots)
|
543
|
+
v_total_load_kw_yearly=[] #Yearly total kW of loads (for duration curve)
|
544
|
+
v_total_load_kvar_yearly=[] #Yearly total kVAr of loads (for duration curve)
|
545
|
+
v_dict_voltage={} #Dict of voltages
|
546
|
+
v_dict_unbalance={} #Dict of unbalances
|
547
|
+
v_dict_loading={} #Dict of loading
|
548
|
+
v_dict_losses={} #Dict of losses
|
549
|
+
v_dict_loads={} #Dict of loads
|
201
550
|
old_percentage_str="" #Variable for tracking progress
|
202
|
-
|
203
|
-
|
551
|
+
#Get buses ids
|
552
|
+
v_dict_buses_ids,v_dict_ids_buses=self.get_all_buses_ids()
|
553
|
+
#For each hour
|
554
|
+
for i in v_hours_sim:
|
555
|
+
#Solve power flow in that hour
|
204
556
|
self.dss_run_command("Solve")
|
557
|
+
#Build month vector
|
558
|
+
month=timestamps_datetime[i].month
|
559
|
+
if not month in v_months:
|
560
|
+
v_months[month-1]=month-1
|
205
561
|
#Get voltages
|
206
562
|
dict_voltage_i, v_voltage_i = self.get_all_voltage()
|
207
563
|
self.add_to_dictionary(v_dict_voltage,dict_voltage_i)
|
208
564
|
v_voltage_yearly.extend(v_voltage_i)
|
209
|
-
self.extract_period(v_voltage_i,v_voltage_period,i,end_index,num_periods)
|
565
|
+
v_voltage_period=self.extract_period(v_voltage_i,v_voltage_period,i,end_index,num_periods,month)
|
566
|
+
#Get voltage unbalance
|
567
|
+
dict_unbalance_i, v_unbalance_i = self.get_all_unbalance()
|
568
|
+
self.add_to_dictionary(v_dict_unbalance,dict_unbalance_i)
|
569
|
+
v_unbalance_yearly.extend(v_unbalance_i)
|
570
|
+
v_unbalance_period=self.extract_period(v_unbalance_i,v_unbalance_period,i,end_index,num_periods,month)
|
210
571
|
#Get power
|
211
572
|
dict_power_i, v_power_i = self.get_all_power()
|
212
573
|
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)
|
574
|
+
v_power_period=self.extract_period(v_power_i,v_power_period,i,end_index,num_periods,month)
|
214
575
|
#Get loading
|
215
576
|
dict_loading_i, v_loading_i,dict_buses_element = self.get_all_loading()
|
216
577
|
self.add_to_dictionary(v_dict_loading,dict_loading_i)
|
217
578
|
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)
|
579
|
+
v_loading_period=self.extract_period(v_loading_i,v_loading_period,i,end_index,num_periods,month)
|
219
580
|
#Get dict losses
|
220
581
|
dict_losses_i, v_losses_i = self.get_all_losses()
|
221
582
|
self.add_to_dictionary(v_dict_losses,dict_losses_i)
|
@@ -224,11 +585,29 @@ class OpenDSS_Interface:
|
|
224
585
|
v_subs_losses_yearly.append(subs_losses_i)
|
225
586
|
line_losses_i = self.get_total_line_losses()
|
226
587
|
v_line_losses_yearly.append(line_losses_i)
|
227
|
-
#
|
588
|
+
#Get loads shapes
|
589
|
+
dict_loads_i, v_loads_kw_i, v_loads_kvar_i= self.get_all_buses_loads(i)
|
590
|
+
self.add_to_dictionary(v_dict_loads,dict_loads_i)
|
591
|
+
v_loads_kw_yearly.extend(v_loads_kw_i)
|
592
|
+
v_loads_kw_period=self.extract_period(v_loads_kw_i,v_loads_kw_period,i,end_index,num_periods,month)
|
593
|
+
v_loads_kvar_yearly.extend(v_loads_kvar_i)
|
594
|
+
v_loads_kvar_period=self.extract_period(v_loads_kvar_i,v_loads_kvar_period,i,end_index,num_periods,month)
|
595
|
+
#Get total peak load
|
596
|
+
v_total_load_kw_yearly.append(sum(v_loads_kw_i))
|
597
|
+
v_total_load_kvar_yearly.append(sum(v_loads_kvar_i))
|
598
|
+
#Print progress (disabled because the Ruby gem does not output the print messages)
|
228
599
|
#percentage_str="{:.0f}".format(100*i/end_index)+"%"
|
229
600
|
#if (percentage_str!=old_percentage_str):
|
230
601
|
# print(percentage_str)
|
231
602
|
#old_percentage_str=percentage_str
|
232
|
-
|
603
|
+
#Get loads shapes
|
604
|
+
dict_loads, v_loads_kw, v_loads_kvar= self.get_all_buses_loads(i)
|
605
|
+
#Get lines normal amps
|
606
|
+
dict_lines,v_lines_norm_amps=self.get_all_lines()
|
607
|
+
#Get transformers size
|
608
|
+
dict_transformers,v_transformers_kva=self.get_all_transformers()
|
609
|
+
#Get only the timestamps simulated
|
610
|
+
timestamps=timestamps[start_index:end_index]
|
611
|
+
return v_dict_buses_ids,v_dict_ids_buses,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,v_dict_loads,v_loads_kw_yearly,v_loads_kw_period,v_loads_kvar_yearly,v_loads_kvar_period,v_total_load_kw_yearly,v_total_load_kvar_yearly, v_loads_kw, v_loads_kvar,v_dict_unbalance,v_unbalance_yearly,v_unbalance_period,dict_lines,v_lines_norm_amps,dict_transformers,v_transformers_kva,timestamps,v_months,v_hours
|
233
612
|
|
234
613
|
|