urbanopt-rnm-us 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- self.folder = folder
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
- 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
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,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])
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
- v_dict_voltage[name]=[dict_voltage_i[name]]
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
- vmag = sum(vang[::2])/(len(vang)/2)
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
- #only if it is a branch (two buses)
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
- 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
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
- if (nominal_current>0):
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
- #only if it is a branch (two buses)
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
- #next if check discards vsources
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]) #Verify this is correct, if not abs() they range from + to -, [0] to take active losses
377
+ lossesavg = (losses[0]) #[0] to take active losses
129
378
  else:
130
- print("error - not correctly reading losses")
379
+ print("Error - not correctly reading losses")
131
380
  lossesavg=0
132
- #Convert to kW. This function is the exeption that return losses in W
381
+ #Convert to kW (becase CktElement.Losses is the exeption that return losses in W)
133
382
  lossesavg=lossesavg/1000
134
- dict_losses[element] = lossesavg
135
- v_losses[idx]=lossesavg
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
- #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
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
- def write_dict(self,v_dict,v_range,type,component):
160
- output_file_full_path = self.folder + '/' + type + '_' + component + '.csv'
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 (consider adding day, month in future)
472
+ #Header: ID, hours
164
473
  for idx,name in enumerate(v_dict):
165
- fp.write('Hour,'+','.join(str(idx2) for idx2,value in enumerate(v_dict[name])) + '\n')
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
- #Truncate list to limits
483
+ #Init list
170
484
  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')
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 solve_powerflow_iteratively(self,num_periods,start_index,end_index,location,v_range_voltage,v_range_loading):
182
- #Por flow solving mode
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
- my_range=range(start_index,end_index,1)
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
- for i in my_range:
203
- #Solve power flow
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
- #Print progress
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
- 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
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