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.
@@ -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