-
Notifications
You must be signed in to change notification settings - Fork 0
/
calc_functions.py
332 lines (299 loc) · 16.8 KB
/
calc_functions.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
# -*- coding: utf-8 -*-
"""
Created on Sun Aug 06 07:53:19 2017
@author: AtotheM
"""
import networkx as nx
import math
from checker_functions import loop_check, length_check, voltage_check, neg_vAr_check
from helper_functions import get_node_voltage
from constants import WBASE, VARBASE
#TODO: Currently only supports lagging power factors on loads (positive vAr). Find a way to implement leading power factor. Need to fix the flow direction from source.
#TODO: Currently transformers need to have nomSecondaryV2 and nomSecondaryV1. Fix to only require V1 for transformers
#with single voltage outputs. Can this be accomplished when inputting transformer data?
#TODO: Write a function that checks the capacity of transformers. Needs to account for single phase loads being
#connected to three phase transformers (1/3 capacity)."""
#TODO: Write a function that sizes wire sizes based on permitted ampacity.
#TODO: Write a function that sizes wires based on voltage drop permitted to furthest load. Wire size required for ampacity
#needs to be the lower bound.
#TODO: Need to account for three phase faults and single phase L-G faults to expand SSC calculations.
#TODO: Figure out if voltage drop accross transformer is working when a single phase load is connected to a three phase transformer secondaryVoltage
#TODO: Need to check if three-phase and single phase load mixtures are being properly addressed
def per_unit_conv(graph):
"""Converts a eletrical network to the per unit system.
"""
#Define base power with random assumption
wBase, vArBase = WBASE, VARBASE
sBase = complex(wBase, vArBase)
#Set the voltage bases for each edge. Node bases will come from nominal voltage settings.
for beg,end,data in graph.edges(data=True):
if graph.in_degree(beg) == 0:
data["vBase"] = get_node_voltage(graph, beg)
if graph.node[end]["nodeType"] == "transformer":
data["vBase"] = graph.node[end]["nomPrimaryV"]
else:
data["vBase"] = get_node_voltage(graph, end)
#Set the Zbases for all nodes except transformers (dont need to for transformers)
for i in graph.nodes():
if graph.node[i]["nodeType"] == "transformer":
continue
else:
graph.node[i]["zBase"] = (get_node_voltage(graph, i)**2.0) / sBase
#Set the ZBases for all edges
for beg,end,data in graph.edges(data=True):
try:
data["zBase"] = complex(((data["vBase"].real**2.0) / sBase.real), ((data["vBase"].imag**2.0) / sBase.imag))
except KeyError:
print "Voltage Base not set for edge:", i
#Calculate the per unit impedance of all nodes and egdes.
#TODO: Determine better way to treat source impedance since generators will have non negligable internal impedance. Service nodes dont have a impedance attribute currently.
for i in graph.nodes():
#For loads
if graph.node[i]["nodeType"] == "load":
zPU = ((get_node_voltage(graph, i)**2.0) / complex(graph.node[i]["w"], graph.node[i]["vAr"])) / graph.node[i]["zBase"]
graph.node[i]["zPU"] = zPU
#For transformers
if graph.node[i]["nodeType"] == "transformer":
zPU = (complex(graph.node[i]["pctR"]/100.0, -graph.node[i]["pctX"]/100.0) * (sBase / (graph.node[i]["rating"]*1000.0)) *
((graph.node[i]["nomPrimaryV"] - (graph.node[i]["nomPrimaryV"] * graph.node[i]["tapSetting"]/100.0)) /
graph.node[i]["nomPrimaryV"])**2.0)
graph.node[i]["zPU"] = complex(zPU.real, zPU.imag)
#For edges
for beg,end,data in graph.edges(data=True):
data["zPU"] = (complex(((data["rL"] * data["length"])/((1000.0 * data['numWires']))),
(((data["xL"] * data["length"] * -1.0)/((1000.0 * data['numWires']))))) / data["zBase"])
#Calculate the per unit current requirements of each load and transformer (power consumption)
for i in graph.nodes():
try:
if graph.node[i]["nodeType"] == "load":
graph.node[i]["wPU"] = graph.node[i]["w"] / WBASE
graph.node[i]["vArPU"] = graph.node[i]["vAr"] / VARBASE
else:
pass
except KeyError:
pass
@loop_check
@length_check
@voltage_check
@neg_vAr_check
def calc_flows_PU(graph, debug=False):
"""Calculates the per unit load flow on each edge for the entire graph, then assigns the value to the edges
as the 'w' and 'vArPU' attribuite. Also calculates the total load required at the service point, then assigns these
values to the service node as negative float for outgoing power flow.
"""
#Sum total per unit current of the system
wPUTotal = 0
vArPUTotal = 0
serviceNode = graph.get_service_node()
for i in graph.nodes():
if i == serviceNode:
pass
try:
wPUTotal -= graph.node[i]["wPU"]
vArPUTotal -= graph.node[i]["vArPU"]
except KeyError:
pass
graph.node[serviceNode]["wPU"] = wPUTotal
graph.node[serviceNode]["vArPU"] = vArPUTotal
graph.node[serviceNode]["trueVoltagePU"] = 1.0 #Service PU voltage will always be 1
#Create dict of real flow between network nodes
wFlowsDict = nx.network_simplex(graph, demand="wPU")[1]
#Assign per unit to each edge in the graph
for k,v in wFlowsDict.items():
if len(wFlowsDict[k].values()) == 0:
pass
for k1,v1 in wFlowsDict[k].items():
graph[k][k1]["wPU"] = v1
#Create dict of imag flow between network nodes
vArFlowsDict = nx.network_simplex(graph, demand="vArPU")[1]
#Assign per unit to each edge in the graph
for k,v in vArFlowsDict.items():
if len(vArFlowsDict[k].values()) == 0:
pass
for k1,v1 in vArFlowsDict[k].items():
graph[k][k1]["vArPU"] = v1
if debug:
print wPUTotal
print vArPUTotal
print wFlowsDict
print vArFlowsDict
def actual_conv(graph):
"""Converts a eletrical network from the per unit system to actual values.
"""
#Calculate actual vDrop and current on each edge
serviceNode = graph.get_service_node()
for beg,end,data in graph.edges(data=True):
data["vDrop"] = data["vDropPU"] * data["vBase"]
if graph.node[end]["phase"] == 3:
data["I"] = data["IPU"] * (data["vBase"]/(data["zBase"] * math.sqrt(3)))
else:
data["I"] = data["IPU"] * (data["vBase"]/data["zBase"])
#Calculate actual voltage at each node
for i in graph.nodes():
if i == serviceNode:
pass
if graph.node[i]["nodeType"] == "transformer":
graph.node[i]["primaryVoltage"] = graph.node[i]["primaryVoltagePU"] * graph.node[i]["nomPrimaryV"]
graph.node[i]["secondaryVoltage1"] = graph.node[i]["secondaryVoltagePU"] * graph.node[i]["nomSecondaryV1"] #TODO: Transformers with only one secondary voltage
graph.node[i]["secondaryVoltage2"] = graph.node[i]["secondaryVoltagePU"] * graph.node[i]["nomSecondaryV2"] #TODO: Transformers with only one secondary voltage
else:
try:
graph.node[i]["trueVoltage"] = graph.node[i]["trueVoltagePU"] * graph.node[i]["nomVLL"]
except KeyError:
graph.node[i]["trueVoltage"] = graph.node[i]["trueVoltagePU"] * graph.node[i]["nomVLN"]
def segment_vdrop_PU(graph, sourceNode, endNode):
"""Calculates the per unit voltage drop along an edge, and sets the trueVoltagePU of the
endNode along with the per unit current along the edge. The trueVoltagePU is the sourceNode
trueVoltagePU minus the per unit voltage drop along the interconnecting edge.
"""
edge = graph.get_edge_data(sourceNode, endNode)
wPU = edge["wPU"]
vArPU = edge["vArPU"]
zPU = edge["zPU"]
phaseEnd = graph.node[endNode]["phase"]
if graph.node[sourceNode]["nodeType"] == "transformer" and graph.node[endNode]["nodeType"] == "transformer":
vSPU = graph.node[sourceNode]["nomSecondaryV2"] / graph.node[endNode]["nomPrimaryV"]#TODO: Fix for transformers with one voltage secondary
elif graph.node[sourceNode]["nodeType"] == "transformer":
vSPU = graph.node[sourceNode]["nomSecondaryV2"] / get_node_voltage(graph, endNode) #TODO: Fix for transformers with one voltage secondary
elif graph.node[endNode]["nodeType"] == "transformer":
vSPU = get_node_voltage(graph, sourceNode) / graph.node[endNode]["nomPrimaryV"]
else:
vSPU = 1
IPU = complex(wPU, vArPU) / vSPU
#Set current flowing along each edge
edge["IPU"] = IPU
#Calculate round-trip voltage drop along edge
if phaseEnd == 1:
vDropPU = complex(IPU.real, IPU.imag) * zPU * 2.0
elif phaseEnd == 3:
vDropPU = math.sqrt(3) * complex(IPU.real, IPU.imag) * zPU
else:
raise ValueError("Phase value for edge must be 1 or 3")
assert vDropPU.real < 1.0, ("Segment voltge drop exceeds starting voltage between {0} and {1}."
" Check network configuration and inputs.".format(sourceNode, endNode))
#Check for nodes that are transformers and treat them specially
edge["vDropPU"] = vDropPU
if graph.node[endNode]["nodeType"] == "transformer":
if graph.node[sourceNode]["nodeType"] == "transformer":
graph.node[endNode]["primaryVoltagePU"] = graph.node[sourceNode]["secondaryVoltagePU"] - vDropPU
else:
graph.node[endNode]["primaryVoltagePU"] = graph.node[sourceNode]["trueVoltagePU"] - vDropPU
#Set transformer secondary voltage
successors = [i for i in graph.successors(endNode)]
if len(successors) > 1:
raise ValueError("""Transformer secondaries may only be directly connected to one
successor node.""")
graph.node[endNode]["secondaryVoltagePU"] = ((graph.node[endNode]["primaryVoltagePU"] -
(graph.node[endNode]["primaryVoltagePU"] * graph.node[endNode]["tapSetting"]/100.0)) -
(IPU * graph.node[endNode]["zPU"])) #TODO: Fix for transformers with two voltage secondaries... does this even need to be handled?
return
if graph.node[sourceNode]["nodeType"] == "transformer":
graph.node[endNode]["trueVoltagePU"] = graph.node[sourceNode]["secondaryVoltagePU"] - vDropPU
return
else:
graph.node[endNode]["trueVoltagePU"] = graph.node[sourceNode]["trueVoltagePU"] - vDropPU
return
def calc_voltages_PU(graph):
"""Calculates the trueVoltagePU for each node on the network, starting from the "service" node
then running down the digraph edges out to the load nodes using an oriented depth-first created
tree of the graph structure. The segment_vdrop_PU function is used to calculate the per unit voltage drop along
each edge, set edge per unit currents, and to assign the trueVoltagePU to each node.
"""
serviceNode = graph.get_service_node()
#Oriented tree structure of digraph starting at serviceNode
graphStructure = nx.edge_dfs(graph, source=serviceNode)
#Calculate voltages starting at source and working towards loads
for i in graphStructure:
segment_vdrop_PU(graph, i[0], i[1])
def calc_sym_ssc(graph, xRRatio=10.0):
#NOTE: Have to reverse vArBase for SSC to function correctly. Hence multiplaction by -1.0.
#TODO: Update docstring when functionality is expanded.... L-G faults and three-phase systems
"""Calculates the maximum symmetrical short circuit current at each node on the network using a point-to-point calculation procedure,
starting from the "service" node then running down the digraph edges out to the load nodes. The series impedance from the
"service" node to each other node on the network is used to calculate the short circuit current available. Determines and sets
line to line faults and line to ground faults for single phase nodes. Requires that the short circuit current avaliable is provided
for the secondary terminals of the service transformer and that the service is single phase or split phase center tapped. The utility
X/R can be input (on secondary of service transformer), or a default of 10 is used.
"""
wBase, vArBase = WBASE, VARBASE
sBase = complex(wBase, vArBase * -1.0)
serviceNode = graph.get_service_node()
try:
xRRatio = graph.node[serviceNode]['xRRatio']
except KeyError:
pass #Default X/R assumption unless otherwise set
try:
serviceVoltage = get_node_voltage(graph, serviceNode)
zBase = (serviceVoltage**2.0) / sBase
zPULL = (serviceVoltage / graph.node[serviceNode]['availSSC']) / abs(zBase)
zPULN = (serviceVoltage / graph.node[serviceNode]['availSSC']*1.5) / abs(zBase) #TODO: Find better way... this is rough assumption
sourceXPULL = math.sin(math.atan(xRRatio)) * zPULL
sourceXPULN = math.sin(math.atan(xRRatio)) * zPULN
sourceRPULL = math.cos(math.atan(xRRatio)) * zPULL
sourceRPULN = math.cos(math.atan(xRRatio)) * zPULN
sourceZPULL= complex(sourceRPULL, -1.0 * sourceXPULL) #Lagging source impedance
sourceZPULN= complex(sourceRPULN, -1.0 * sourceXPULN) #Lagging source impedance
except KeyError:
print ("""Missing input data for service node. Unable to perform short circuit current calculations.
Avaliable short circuit current at service node is required.""")
for i in graph.nodes():
#Move on from service node
if i == serviceNode:
continue
length, path = nx.bidirectional_dijkstra(graph, serviceNode, i)
#TODO: Implement way to start with a starting SSC that sets a upstream system impedance...consider source X/R and available energy
zSeriesPULL = sourceZPULL #Start with service impedance from source
zSeriesPULN = sourceZPULN
zEdgeSeriesPU = 0
#Sum series edge impedances between service point and node
for j in range(len(path)-1):
if not graph.node[path[j]]["phase"] == 1: #TODO: Remove once software can calculate three phase fault currents.
raise Exception("calc_sym_ssc() only works for single phase systems currently")
try:
zEdgeSeriesPU += 2.0 * graph[path[j]][path[j+1]]["zPU"] #Mutiply by 2 for return impedance of conductors
except KeyError:
print "zPU not set for edge between {0} and {1}".format(path[j], path[j+1])
#If transformer is on the path add that to the series Impedance
zSeriesPULL += zEdgeSeriesPU
#TODO: Only works for single phase center tapped systems... consider setting ratio from upstream transformer or service whatever is closer
#Convert to base voltage for L-N from L-L base
zSeriesPULN += zEdgeSeriesPU * 1.0 * ((2 / 1)**2.0)
for y in path:
if graph.node[y]["nodeType"] == "transformer":
try:
zSeriesPULL += graph.node[y]["zPU"]
zSeriesPULN += graph.node[y]["zPU"] * 1.5 #TODO: Find better way... this is rough assumption
except KeyError:
print "zPU not set for transformer {0}".format(y)
#If transformer is the node where zSeriesPU is being set, dont include that transformers impedance (ssc at primary)
if graph.node[i]["nodeType"] == "transformer":
graph.node[i]["zSeriesPULL"] = zSeriesPULL - graph.node[i]["zPU"]
graph.node[i]["zSeriesPULN"] = zSeriesPULN - graph.node[y]["zPU"] * 1.5 #TODO: Find better way... this is rough assumption
continue
graph.node[i]["zSeriesPULL"] = zSeriesPULL
graph.node[i]["zSeriesPULN"] = zSeriesPULN
for i in graph.nodes():
if i == serviceNode:
graph.node[i]["SSC_LL"] = graph.node[i]['availSSC']
try:
if graph.node[serviceNode]["phase"] == 1 and graph.node[serviceNode]["nomVLL"]*0.5 == graph.node[serviceNode]["nomVLN"] :
graph.node[i]["SSC_LN"] = graph.node[i]['availSSC'] * 1.5 #TODO: Find better way... this is rough assumption
except KeyError:
pass
continue
if graph.node[i]["phase"] == 1:
try:
if graph.node[i]["nodeType"] == "transformer":
graph.node[i]["SSC_LL"] = (1.0 / graph.node[i]["zSeriesPULL"]) * (sBase / graph.node[i]["nomPrimaryV"])
else:
try:
graph.node[i]["SSC_LL"] = (1.0 / graph.node[i]["zSeriesPULL"]) * (sBase / graph.node[i]["nomVLL"])
except KeyError:
pass
try:
graph.node[i]["SSC_LN"] = (1.0 / graph.node[i]["zSeriesPULN"]) * (sBase / graph.node[i]["nomVLN"])
except KeyError:
pass
except KeyError:
print "Missing series per unit impedance for node {0}".format(i)
#TODO: Implement SSC for three phase systems