urbanopt-rnm-us 0.3.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +3 -1
- data/CHANGELOG.md +11 -0
- data/Gemfile +6 -5
- data/README.md +21 -0
- data/Rakefile +56 -0
- data/catalogs/extended_catalog.json +1928 -1796
- data/lib/urbanopt/rnm/api_client.rb +10 -2
- data/lib/urbanopt/rnm/consumers.rb +17 -4
- data/lib/urbanopt/rnm/input_files.rb +19 -1
- data/lib/urbanopt/rnm/prosumers.rb +19 -3
- data/lib/urbanopt/rnm/runner.rb +10 -0
- data/lib/urbanopt/rnm/transformer_opendss.rb +1 -5
- data/lib/urbanopt/rnm/validation/main_validation.py +153 -0
- data/lib/urbanopt/rnm/validation/opendss_interface.py +613 -0
- data/lib/urbanopt/rnm/validation/plot_lib.py +528 -0
- data/lib/urbanopt/rnm/validation/report.py +370 -0
- data/lib/urbanopt/rnm/validation.rb +77 -0
- data/lib/urbanopt/rnm/version.rb +1 -1
- data/lib/urbanopt/rnm.rb +1 -0
- data/opendss_catalog.json +2216 -0
- data/requirements.txt +9 -0
- data/urbanopt-rnm-us-gem.gemspec +1 -1
- metadata +11 -4
@@ -0,0 +1,613 @@
|
|
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
|
+
from datetime import datetime
|
9
|
+
|
10
|
+
class OpenDSS_Interface:
|
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
|
16
|
+
|
17
|
+
def remove_terminal(self,bus):
|
18
|
+
"""Removes the terminal from the bus name"""
|
19
|
+
if isinstance(bus,str):
|
20
|
+
return bus.split('.')[0] #(everything to the right of point ".")
|
21
|
+
else:
|
22
|
+
return bus
|
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
|
43
|
+
|
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)
|
49
|
+
return v_value_period
|
50
|
+
|
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])
|
56
|
+
else:
|
57
|
+
dict_all[name]=[dict_i[name]]
|
58
|
+
|
59
|
+
def dss_run_command(self,command):
|
60
|
+
"""Runs an OpenDSS Direct command"""
|
61
|
+
#Run command
|
62
|
+
output=dss.run_command(command)
|
63
|
+
#If it has any output, print it
|
64
|
+
if (len(output)>0):
|
65
|
+
print(output)
|
66
|
+
|
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
|
+
|
86
|
+
def get_all_voltage(self):
|
87
|
+
"""Computes over and under voltages for all buses"""
|
88
|
+
#Get bus names
|
89
|
+
bus_names = dss.Circuit.AllBusNames()
|
90
|
+
#Init variables
|
91
|
+
dict_voltage = {}
|
92
|
+
v_voltage = [0 for _ in range(len(bus_names))]
|
93
|
+
#For each bus
|
94
|
+
for idx,b in enumerate(bus_names):
|
95
|
+
#Set it as active bus
|
96
|
+
dss.Circuit.SetActiveBus(b)
|
97
|
+
#Get voltage and angle
|
98
|
+
vang = dss.Bus.puVmagAngle()
|
99
|
+
#Get voltage magnitude
|
100
|
+
if len(vang[::2]) > 0:
|
101
|
+
#Average of the voltages in all the phases, discarding the angles
|
102
|
+
vmag = sum(vang[::2])/(len(vang)/2)
|
103
|
+
else:
|
104
|
+
vmag = 0
|
105
|
+
#Add voltage magnitude to dictionary and to list of voltages
|
106
|
+
dict_voltage[b] = vmag
|
107
|
+
v_voltage[idx]=vmag
|
108
|
+
|
109
|
+
return dict_voltage,v_voltage
|
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
|
+
|
291
|
+
def get_all_power(self):
|
292
|
+
"""Computes power in all circuits (not used, loading is measured instead)"""
|
293
|
+
#Get all element names
|
294
|
+
circuit_names = dss.Circuit.AllElementNames()
|
295
|
+
#Init variables
|
296
|
+
dict_power = {}
|
297
|
+
v_power = [0 for _ in range(len(circuit_names))]
|
298
|
+
#For each circuit
|
299
|
+
for idx,b in enumerate(circuit_names):
|
300
|
+
#Set the active element
|
301
|
+
dss.Circuit.SetActiveElement(b)
|
302
|
+
#Calculates the power through the circuit
|
303
|
+
power = dss.CktElement.Powers()
|
304
|
+
if len(power[::2]) > 0:
|
305
|
+
poweravg = sum(power[::2])/(len(power)/2)
|
306
|
+
else:
|
307
|
+
poweravg = 0
|
308
|
+
#Add to dictionary and to list
|
309
|
+
dict_power[b] = poweravg
|
310
|
+
v_power[idx]=poweravg
|
311
|
+
|
312
|
+
return dict_power,v_power
|
313
|
+
|
314
|
+
|
315
|
+
def get_all_loading(self):
|
316
|
+
"""Computes loading in all circuits"""
|
317
|
+
#Get all element names
|
318
|
+
circuit_names = dss.Circuit.AllElementNames()
|
319
|
+
#Init variables
|
320
|
+
dict_loading = {}
|
321
|
+
dict_buses_element={} #Associate the element to the buses (this has the inconvenient that only associates one element to each pair of buses)
|
322
|
+
v_loading = [0 for _ in range(len(circuit_names))]
|
323
|
+
#For each circuit
|
324
|
+
for idx,element in enumerate(circuit_names):
|
325
|
+
#Set the active element
|
326
|
+
dss.Circuit.SetActiveElement(element)
|
327
|
+
#Get the buses in the elment
|
328
|
+
buses = dss.CktElement.BusNames()
|
329
|
+
#Evaluate only if it is a branch (two buses)
|
330
|
+
if (len(buses)>=2):
|
331
|
+
#Obtain the current through the element
|
332
|
+
current = dss.CktElement.CurrentsMagAng()
|
333
|
+
#Obtain the number of terminals
|
334
|
+
num_terminals=dss.CktElement.NumTerminals()
|
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)
|
336
|
+
currentmag = current[0]
|
337
|
+
#Obtain the normal amapcity
|
338
|
+
nominal_current = dss.CktElement.NormalAmps()
|
339
|
+
#Transformers have applied a 1.1 factor in the calculation of NormalAmps
|
340
|
+
#See library that OpenDSSdirect uses in https://github.com/dss-extensions/dss_capi/blob/master/src/PDElements/Transformer.pas
|
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
|
343
|
+
if (element.startswith("Transformer")):
|
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
|
348
|
+
dict_loading[element] = currentmag/nominal_current
|
349
|
+
v_loading[idx]=currentmag/nominal_current
|
350
|
+
bus1to2=self.remove_terminal(buses[0])+'-->'+self.remove_terminal(buses[1])
|
351
|
+
dict_buses_element[bus1to2]=element
|
352
|
+
return dict_loading,v_loading,dict_buses_element
|
353
|
+
|
354
|
+
def get_all_losses(self):
|
355
|
+
"""Computes losses in all circuits"""
|
356
|
+
#Get all element names
|
357
|
+
circuit_names = dss.Circuit.AllElementNames()
|
358
|
+
#Init variables
|
359
|
+
dict_losses = {}
|
360
|
+
v_losses = [0 for _ in range(len(circuit_names))]
|
361
|
+
total_losses=0
|
362
|
+
#For each element
|
363
|
+
for idx,element in enumerate(circuit_names):
|
364
|
+
#Set it as active element
|
365
|
+
dss.Circuit.SetActiveElement(element)
|
366
|
+
#Get buses names in element
|
367
|
+
buses = dss.CktElement.BusNames()
|
368
|
+
#only if it is a branch (two buses)
|
369
|
+
if (len(buses)>=2):
|
370
|
+
#Get nominal current
|
371
|
+
nominal_current = dss.CktElement.NormalAmps()
|
372
|
+
#If it has nominal current (this discards vsources)
|
373
|
+
if (nominal_current>0):
|
374
|
+
#Get hte losses
|
375
|
+
losses = dss.CktElement.Losses()
|
376
|
+
if len(losses) ==2:
|
377
|
+
lossesavg = (losses[0]) #[0] to take active losses
|
378
|
+
else:
|
379
|
+
print("Error - not correctly reading losses")
|
380
|
+
lossesavg=0
|
381
|
+
#Convert to kW (becase CktElement.Losses is the exeption that return losses in W)
|
382
|
+
lossesavg=lossesavg/1000
|
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
|
388
|
+
return dict_losses,total_losses
|
389
|
+
|
390
|
+
def get_total_subs_losses(self):
|
391
|
+
"""Computes total substation losses"""
|
392
|
+
return dss.Circuit.SubstationLosses()[0] #Real part
|
393
|
+
|
394
|
+
def get_total_line_losses(self):
|
395
|
+
"""Computes total power line losses"""
|
396
|
+
return dss.Circuit.LineLosses()[0] #Real part
|
397
|
+
|
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
|
401
|
+
circuit_names = dss.Circuit.AllElementNames()
|
402
|
+
#Init variable
|
403
|
+
closed_edges=[]
|
404
|
+
open_edges=[]
|
405
|
+
#For all elements
|
406
|
+
for idx,element in enumerate(circuit_names):
|
407
|
+
#Set it as active element
|
408
|
+
dss.Circuit.SetActiveElement(element)
|
409
|
+
#Get the bus names
|
410
|
+
buses = dss.CktElement.BusNames()
|
411
|
+
#Only if it is a branch
|
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
|
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
|
446
|
+
|
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'
|
470
|
+
# Write directly as a CSV file with headers on first line
|
471
|
+
with open(output_file_full_path, 'w') as fp:
|
472
|
+
#Header: ID, hours
|
473
|
+
for idx,name in enumerate(v_dict):
|
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')
|
480
|
+
break
|
481
|
+
#Write matrix
|
482
|
+
for idx,name in enumerate(v_dict):
|
483
|
+
#Init list
|
484
|
+
truncated_values=[]
|
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')
|
497
|
+
|
498
|
+
|
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)
|
522
|
+
self.dss_run_command("Clear")
|
523
|
+
self.dss_run_command('Redirect '+location)
|
524
|
+
self.dss_run_command("solve mode = snap")
|
525
|
+
self.dss_run_command("Set mode=yearly stepsize=1h number="+str(start_index+1))
|
526
|
+
#Additional initializations
|
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
|
550
|
+
old_percentage_str="" #Variable for tracking progress
|
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
|
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
|
561
|
+
#Get voltages
|
562
|
+
dict_voltage_i, v_voltage_i = self.get_all_voltage()
|
563
|
+
self.add_to_dictionary(v_dict_voltage,dict_voltage_i)
|
564
|
+
v_voltage_yearly.extend(v_voltage_i)
|
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)
|
571
|
+
#Get power
|
572
|
+
dict_power_i, v_power_i = self.get_all_power()
|
573
|
+
v_power_yearly.extend(v_power_i)
|
574
|
+
v_power_period=self.extract_period(v_power_i,v_power_period,i,end_index,num_periods,month)
|
575
|
+
#Get loading
|
576
|
+
dict_loading_i, v_loading_i,dict_buses_element = self.get_all_loading()
|
577
|
+
self.add_to_dictionary(v_dict_loading,dict_loading_i)
|
578
|
+
v_loading_yearly.extend(v_loading_i)
|
579
|
+
v_loading_period=self.extract_period(v_loading_i,v_loading_period,i,end_index,num_periods,month)
|
580
|
+
#Get dict losses
|
581
|
+
dict_losses_i, v_losses_i = self.get_all_losses()
|
582
|
+
self.add_to_dictionary(v_dict_losses,dict_losses_i)
|
583
|
+
#Get losses
|
584
|
+
subs_losses_i = self.get_total_subs_losses()
|
585
|
+
v_subs_losses_yearly.append(subs_losses_i)
|
586
|
+
line_losses_i = self.get_total_line_losses()
|
587
|
+
v_line_losses_yearly.append(line_losses_i)
|
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)
|
599
|
+
#percentage_str="{:.0f}".format(100*i/end_index)+"%"
|
600
|
+
#if (percentage_str!=old_percentage_str):
|
601
|
+
# print(percentage_str)
|
602
|
+
#old_percentage_str=percentage_str
|
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
|
612
|
+
|
613
|
+
|