ltbams 0.9.9__py3-none-any.whl → 1.0.2a1__py3-none-any.whl
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.
- ams/__init__.py +4 -11
- ams/_version.py +3 -3
- ams/cases/5bus/pjm5bus_demo.xlsx +0 -0
- ams/cases/5bus/pjm5bus_jumper.xlsx +0 -0
- ams/cases/5bus/pjm5bus_uced.json +1062 -0
- ams/cases/5bus/pjm5bus_uced.xlsx +0 -0
- ams/cases/5bus/pjm5bus_uced_esd1.xlsx +0 -0
- ams/cases/5bus/pjm5bus_uced_ev.xlsx +0 -0
- ams/cases/ieee123/ieee123.xlsx +0 -0
- ams/cases/ieee123/ieee123_regcv1.xlsx +0 -0
- ams/cases/ieee14/ieee14.json +1166 -0
- ams/cases/ieee14/ieee14.raw +92 -0
- ams/cases/ieee14/ieee14_conn.xlsx +0 -0
- ams/cases/ieee14/ieee14_uced.xlsx +0 -0
- ams/cases/ieee39/ieee39.xlsx +0 -0
- ams/cases/ieee39/ieee39_uced.xlsx +0 -0
- ams/cases/ieee39/ieee39_uced_esd1.xlsx +0 -0
- ams/cases/ieee39/ieee39_uced_pvd1.xlsx +0 -0
- ams/cases/ieee39/ieee39_uced_vis.xlsx +0 -0
- ams/cases/matpower/benchmark.json +1594 -0
- ams/cases/matpower/case118.m +787 -0
- ams/cases/matpower/case14.m +129 -0
- ams/cases/matpower/case300.m +1315 -0
- ams/cases/matpower/case39.m +205 -0
- ams/cases/matpower/case5.m +62 -0
- ams/cases/matpower/case_ACTIVSg2000.m +9460 -0
- ams/cases/npcc/npcc.m +644 -0
- ams/cases/npcc/npcc_uced.xlsx +0 -0
- ams/cases/pglib/pglib_opf_case39_epri__api.m +243 -0
- ams/cases/wecc/wecc.m +714 -0
- ams/cases/wecc/wecc_uced.xlsx +0 -0
- ams/cli.py +6 -0
- ams/core/__init__.py +2 -0
- ams/core/documenter.py +652 -0
- ams/core/matprocessor.py +782 -0
- ams/core/model.py +330 -0
- ams/core/param.py +322 -0
- ams/core/service.py +918 -0
- ams/core/symprocessor.py +224 -0
- ams/core/var.py +59 -0
- ams/extension/__init__.py +5 -0
- ams/extension/eva.py +401 -0
- ams/interface.py +1085 -0
- ams/io/__init__.py +133 -0
- ams/io/json.py +82 -0
- ams/io/matpower.py +406 -0
- ams/io/psse.py +6 -0
- ams/io/pypower.py +103 -0
- ams/io/xlsx.py +80 -0
- ams/main.py +81 -4
- ams/models/__init__.py +24 -0
- ams/models/area.py +40 -0
- ams/models/bus.py +52 -0
- ams/models/cost.py +169 -0
- ams/models/distributed/__init__.py +3 -0
- ams/models/distributed/esd1.py +71 -0
- ams/models/distributed/ev.py +60 -0
- ams/models/distributed/pvd1.py +67 -0
- ams/models/group.py +231 -0
- ams/models/info.py +26 -0
- ams/models/line.py +238 -0
- ams/models/renewable/__init__.py +5 -0
- ams/models/renewable/regc.py +119 -0
- ams/models/reserve.py +94 -0
- ams/models/shunt.py +14 -0
- ams/models/static/__init__.py +2 -0
- ams/models/static/gen.py +165 -0
- ams/models/static/pq.py +61 -0
- ams/models/timeslot.py +69 -0
- ams/models/zone.py +49 -0
- ams/opt/__init__.py +12 -0
- ams/opt/constraint.py +175 -0
- ams/opt/exprcalc.py +127 -0
- ams/opt/expression.py +188 -0
- ams/opt/objective.py +174 -0
- ams/opt/omodel.py +432 -0
- ams/opt/optzbase.py +192 -0
- ams/opt/param.py +156 -0
- ams/opt/var.py +233 -0
- ams/pypower/__init__.py +8 -0
- ams/pypower/_compat.py +9 -0
- ams/pypower/core/__init__.py +8 -0
- ams/pypower/core/pips.py +894 -0
- ams/pypower/core/ppoption.py +244 -0
- ams/pypower/core/ppver.py +18 -0
- ams/pypower/core/solver.py +2451 -0
- ams/pypower/eps.py +6 -0
- ams/pypower/idx.py +174 -0
- ams/pypower/io.py +604 -0
- ams/pypower/make/__init__.py +11 -0
- ams/pypower/make/matrices.py +665 -0
- ams/pypower/make/pdv.py +506 -0
- ams/pypower/routines/__init__.py +7 -0
- ams/pypower/routines/cpf.py +513 -0
- ams/pypower/routines/cpf_callbacks.py +114 -0
- ams/pypower/routines/opf.py +1803 -0
- ams/pypower/routines/opffcns.py +1946 -0
- ams/pypower/routines/pflow.py +852 -0
- ams/pypower/toggle.py +1098 -0
- ams/pypower/utils.py +293 -0
- ams/report.py +212 -50
- ams/routines/__init__.py +23 -0
- ams/routines/acopf.py +117 -0
- ams/routines/cpf.py +65 -0
- ams/routines/dcopf.py +241 -0
- ams/routines/dcpf.py +209 -0
- ams/routines/dcpf0.py +196 -0
- ams/routines/dopf.py +150 -0
- ams/routines/ed.py +312 -0
- ams/routines/pflow.py +255 -0
- ams/routines/pflow0.py +113 -0
- ams/routines/routine.py +1033 -0
- ams/routines/rted.py +519 -0
- ams/routines/type.py +160 -0
- ams/routines/uc.py +376 -0
- ams/shared.py +63 -9
- ams/system.py +61 -22
- ams/utils/__init__.py +3 -0
- ams/utils/misc.py +77 -0
- ams/utils/paths.py +257 -0
- docs/Makefile +21 -0
- docs/make.bat +35 -0
- docs/source/_templates/autosummary/base.rst +5 -0
- docs/source/_templates/autosummary/class.rst +35 -0
- docs/source/_templates/autosummary/module.rst +65 -0
- docs/source/_templates/autosummary/module_toctree.rst +66 -0
- docs/source/api.rst +102 -0
- docs/source/conf.py +203 -0
- docs/source/examples/index.rst +34 -0
- docs/source/genmodelref.py +61 -0
- docs/source/genroutineref.py +47 -0
- docs/source/getting_started/copyright.rst +20 -0
- docs/source/getting_started/formats/index.rst +20 -0
- docs/source/getting_started/formats/matpower.rst +183 -0
- docs/source/getting_started/formats/psse.rst +46 -0
- docs/source/getting_started/formats/pypower.rst +223 -0
- docs/source/getting_started/formats/xlsx.png +0 -0
- docs/source/getting_started/formats/xlsx.rst +23 -0
- docs/source/getting_started/index.rst +76 -0
- docs/source/getting_started/install.rst +234 -0
- docs/source/getting_started/overview.rst +26 -0
- docs/source/getting_started/testcase.rst +45 -0
- docs/source/getting_started/verification.rst +13 -0
- docs/source/images/curent.ico +0 -0
- docs/source/images/dcopf_time.png +0 -0
- docs/source/images/sponsors/CURENT_Logo_NameOnTrans.png +0 -0
- docs/source/images/sponsors/CURENT_Logo_Transparent.png +0 -0
- docs/source/images/sponsors/CURENT_Logo_Transparent_Name.png +0 -0
- docs/source/images/sponsors/doe.png +0 -0
- docs/source/index.rst +108 -0
- docs/source/modeling/example.rst +159 -0
- docs/source/modeling/index.rst +17 -0
- docs/source/modeling/model.rst +210 -0
- docs/source/modeling/routine.rst +122 -0
- docs/source/modeling/system.rst +51 -0
- docs/source/release-notes.rst +398 -0
- ltbams-1.0.2a1.dist-info/METADATA +210 -0
- ltbams-1.0.2a1.dist-info/RECORD +188 -0
- {ltbams-0.9.9.dist-info → ltbams-1.0.2a1.dist-info}/WHEEL +1 -1
- ltbams-1.0.2a1.dist-info/top_level.txt +3 -0
- tests/__init__.py +0 -0
- tests/test_1st_system.py +33 -0
- tests/test_addressing.py +40 -0
- tests/test_andes_mats.py +61 -0
- tests/test_case.py +266 -0
- tests/test_cli.py +34 -0
- tests/test_export_csv.py +89 -0
- tests/test_group.py +83 -0
- tests/test_interface.py +216 -0
- tests/test_io.py +32 -0
- tests/test_jumper.py +27 -0
- tests/test_known_good.py +267 -0
- tests/test_matp.py +437 -0
- tests/test_model.py +54 -0
- tests/test_omodel.py +119 -0
- tests/test_paths.py +22 -0
- tests/test_report.py +251 -0
- tests/test_repr.py +21 -0
- tests/test_routine.py +178 -0
- tests/test_rtn_dcopf.py +101 -0
- tests/test_rtn_dcpf.py +77 -0
- tests/test_rtn_ed.py +279 -0
- tests/test_rtn_pflow.py +219 -0
- tests/test_rtn_rted.py +273 -0
- tests/test_rtn_uc.py +248 -0
- tests/test_service.py +73 -0
- ltbams-0.9.9.dist-info/LICENSE +0 -692
- ltbams-0.9.9.dist-info/METADATA +0 -859
- ltbams-0.9.9.dist-info/RECORD +0 -14
- ltbams-0.9.9.dist-info/top_level.txt +0 -1
- {ltbams-0.9.9.dist-info → ltbams-1.0.2a1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,852 @@
|
|
1
|
+
"""
|
2
|
+
PYPOWER module to solve power flow.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import logging
|
6
|
+
|
7
|
+
from time import time
|
8
|
+
|
9
|
+
import numpy as np
|
10
|
+
|
11
|
+
from numpy import flatnonzero as find
|
12
|
+
from scipy.sparse.linalg import spsolve, splu
|
13
|
+
from scipy.sparse import hstack, vstack
|
14
|
+
from scipy.sparse import csr_matrix as c_sparse
|
15
|
+
|
16
|
+
from andes.shared import deg2rad, rad2deg
|
17
|
+
|
18
|
+
from ams.pypower.core import ppoption
|
19
|
+
import ams.pypower.utils as putils
|
20
|
+
from ams.pypower.idx import IDX
|
21
|
+
from ams.pypower.io import loadcase
|
22
|
+
from ams.pypower.eps import EPS
|
23
|
+
import ams.pypower.routines.opffcns as opfcn
|
24
|
+
|
25
|
+
from ams.pypower.make import (makeB, makeBdc, makeSbus, makeYbus, dSbus_dV)
|
26
|
+
|
27
|
+
from ams.shared import inf
|
28
|
+
|
29
|
+
|
30
|
+
logger = logging.getLogger(__name__)
|
31
|
+
|
32
|
+
|
33
|
+
def runpf(casedata, ppopt):
|
34
|
+
"""
|
35
|
+
Runs a power flow.
|
36
|
+
|
37
|
+
Runs a power flow (full AC Newton's method by default) and optionally
|
38
|
+
returns the solved values in the data matrices, a flag which is True if
|
39
|
+
the algorithm was successful in finding a solution, and the elapsed
|
40
|
+
time in seconds. All input arguments are optional. If casename is
|
41
|
+
provided it specifies the name of the input data file or dict
|
42
|
+
containing the power flow data. The default value is 'case9'.
|
43
|
+
|
44
|
+
Parameters
|
45
|
+
----------
|
46
|
+
casedata : str, dict, or None, optional
|
47
|
+
The name of the input data file or a dict containing the power flow data.
|
48
|
+
Default is None.
|
49
|
+
ppopt : dict, optional
|
50
|
+
PYPOWER options vector. It can be used to specify the solution algorithm
|
51
|
+
and output options among other things. Default is None.
|
52
|
+
|
53
|
+
Returns
|
54
|
+
-------
|
55
|
+
results : dict or None
|
56
|
+
Solved power flow results. None if the power flow did not converge.
|
57
|
+
sstats : dict
|
58
|
+
Solver statistics.
|
59
|
+
|
60
|
+
Notes
|
61
|
+
-----
|
62
|
+
If the ENFORCE_Q_LIMS option is set to True (default is False), then if any
|
63
|
+
generator reactive power limit is violated after running the AC power flow,
|
64
|
+
the corresponding bus is converted to a IDX.bus.PQ bus, with Qg at the limit, and
|
65
|
+
the case is re-run. The voltage magnitude at the bus will deviate from the
|
66
|
+
specified value in order to satisfy the reactive power limit. If the reference
|
67
|
+
bus is converted to IDX.bus.PQ, the first remaining IDX.bus.PV bus will be used as the slack
|
68
|
+
bus for the next iteration. This may result in the real power output at this
|
69
|
+
generator being slightly off from the specified values.
|
70
|
+
|
71
|
+
Enforcing of generator Q limits inspired by contributions from Mu Lin,
|
72
|
+
Lincoln University, New Zealand (1/14/05).
|
73
|
+
|
74
|
+
Author
|
75
|
+
------
|
76
|
+
Ray Zimmerman (PSERC Cornell)
|
77
|
+
"""
|
78
|
+
sstats = dict(solver_name='PYPOWER',
|
79
|
+
num_iters=1) # solver stats
|
80
|
+
ppopt = ppoption(ppopt)
|
81
|
+
|
82
|
+
# read data
|
83
|
+
ppc = loadcase(casedata)
|
84
|
+
|
85
|
+
# add zero columns to branch for flows if needed
|
86
|
+
if ppc["branch"].shape[1] < IDX.branch.QT:
|
87
|
+
ppc["branch"] = np.c_[ppc["branch"],
|
88
|
+
np.zeros((ppc["branch"].shape[0],
|
89
|
+
IDX.branch.QT - ppc["branch"].shape[1] + 1))]
|
90
|
+
|
91
|
+
# convert to internal indexing
|
92
|
+
ppc = opfcn.ext2int(ppc)
|
93
|
+
baseMVA, bus, gen, branch = \
|
94
|
+
ppc["baseMVA"], ppc["bus"], ppc["gen"], ppc["branch"]
|
95
|
+
|
96
|
+
# get bus index lists of each type of bus
|
97
|
+
ref, pv, pq = putils.bustypes(bus, gen)
|
98
|
+
|
99
|
+
# generator info
|
100
|
+
on = find(gen[:, IDX.gen.GEN_STATUS] > 0) # which generators are on?
|
101
|
+
gbus = gen[on, IDX.gen.GEN_BUS].astype(int) # what buses are they at?
|
102
|
+
|
103
|
+
# ----- run the power flow -----
|
104
|
+
t0 = time()
|
105
|
+
|
106
|
+
if ppopt["PF_DC"]: # DC formulation
|
107
|
+
# initial state
|
108
|
+
Va0 = bus[:, IDX.bus.VA] * deg2rad
|
109
|
+
|
110
|
+
# build B matrices and phase shift injections
|
111
|
+
B, Bf, Pbusinj, Pfinj, _ = makeBdc(baseMVA, bus, branch)
|
112
|
+
|
113
|
+
# compute complex bus power injections [generation - load]
|
114
|
+
# adjusted for phase shifters and real shunts
|
115
|
+
Pbus = makeSbus(baseMVA, bus, gen).real - Pbusinj - bus[:, IDX.bus.GS] / baseMVA
|
116
|
+
|
117
|
+
# "run" the power flow
|
118
|
+
Va = dcpf(B, Pbus, Va0, ref, pv, pq)
|
119
|
+
|
120
|
+
# update data matrices with solution
|
121
|
+
branch[:, [IDX.branch.QF, IDX.branch.QT]] = np.zeros((branch.shape[0], 2))
|
122
|
+
branch[:, IDX.branch.PF] = (Bf * Va + Pfinj) * baseMVA
|
123
|
+
branch[:, IDX.branch.PT] = -branch[:, IDX.branch.PF]
|
124
|
+
bus[:, IDX.bus.VM] = np.ones(bus.shape[0])
|
125
|
+
bus[:, IDX.bus.VA] = Va * rad2deg
|
126
|
+
# update Pg for slack generator (1st gen at ref bus)
|
127
|
+
# (note: other gens at ref bus are accounted for in Pbus)
|
128
|
+
# Pg = Pinj + Pload + Gs
|
129
|
+
# newPg = oldPg + newPinj - oldPinj
|
130
|
+
refgen = np.zeros(len(ref), dtype=int)
|
131
|
+
for k in range(len(ref)):
|
132
|
+
temp = find(gbus == ref[k])
|
133
|
+
refgen[k] = on[temp[0]]
|
134
|
+
gen[refgen, IDX.gen.PG] = gen[refgen, IDX.gen.PG] + (B[ref, :] * Va - Pbus[ref]) * baseMVA
|
135
|
+
|
136
|
+
success = 1
|
137
|
+
else: # AC formulation
|
138
|
+
method_map = {1: 'Newton', 2: 'fast-decoupled, XB',
|
139
|
+
3: 'fast-decoupled, BX', 4: 'Gauss-Seidel'}
|
140
|
+
alg = method_map.get(ppopt['PF_ALG'])
|
141
|
+
sstats['solver_name'] = f'PYPOWER-{alg}'
|
142
|
+
logger.debug(f"Solution method: {alg}'s method.")
|
143
|
+
if alg is None:
|
144
|
+
logger.debug('Only Newton\'s method, fast-decoupled, and '
|
145
|
+
'Gauss-Seidel power flow algorithms currently '
|
146
|
+
'implemented.\n')
|
147
|
+
raise ValueError
|
148
|
+
|
149
|
+
# initial state
|
150
|
+
# V0 = np.ones(bus.shape[0]) ## flat start
|
151
|
+
V0 = bus[:, IDX.bus.VM] * np.exp(1j * deg2rad * bus[:, IDX.bus.VA])
|
152
|
+
vcb = np.ones(V0.shape) # create mask of voltage-controlled buses
|
153
|
+
vcb[pq] = 0 # exclude IDX.bus.PQ buses
|
154
|
+
k = find(vcb[gbus]) # in-service gens at v-c buses
|
155
|
+
V0[gbus[k]] = gen[on[k], IDX.gen.VG] / abs(V0[gbus[k]]) * V0[gbus[k]]
|
156
|
+
|
157
|
+
if ppopt["ENFORCE_Q_LIMS"]:
|
158
|
+
ref0 = ref # save index and angle of
|
159
|
+
Varef0 = bus[ref0, IDX.bus.VA] # original reference bus(es)
|
160
|
+
limited = [] # list of indices of gens @ Q lims
|
161
|
+
fixedQg = np.zeros(gen.shape[0]) # Qg of gens at Q limits
|
162
|
+
|
163
|
+
# build admittance matrices
|
164
|
+
Ybus, Yf, Yt = makeYbus(baseMVA, bus, branch)
|
165
|
+
|
166
|
+
repeat = True
|
167
|
+
while repeat:
|
168
|
+
# compute complex bus power injections [generation - load]
|
169
|
+
Sbus = makeSbus(baseMVA, bus, gen)
|
170
|
+
|
171
|
+
# run the power flow
|
172
|
+
if ppopt['PF_ALG'] == 1:
|
173
|
+
V, success, sstats['num_iters'] = newtonpf(Ybus, Sbus, V0, ref, pv, pq, ppopt)
|
174
|
+
elif ppopt['PF_ALG'] in (2, 3):
|
175
|
+
Bp, Bpp = makeB(baseMVA, bus, branch, alg)
|
176
|
+
V, success, sstats['num_iters'] = fdpf(Ybus, Sbus, V0, Bp, Bpp, ref, pv, pq, ppopt)
|
177
|
+
elif ppopt['PF_ALG'] == 4:
|
178
|
+
V, success, sstats['num_iters'] = gausspf(Ybus, Sbus, V0, ref, pv, pq, ppopt)
|
179
|
+
else:
|
180
|
+
pass
|
181
|
+
# update data matrices with solution
|
182
|
+
bus, gen, branch = pfsoln(baseMVA, bus, gen, branch, Ybus, Yf, Yt, V, ref, pv, pq)
|
183
|
+
|
184
|
+
enforce_q_lims = ppopt["ENFORCE_Q_LIMS"]
|
185
|
+
if ppopt["ENFORCE_Q_LIMS"]: # enforce generator Q limits
|
186
|
+
# find gens with violated Q constraints
|
187
|
+
gen_status = gen[:, IDX.gen.GEN_STATUS] > 0
|
188
|
+
qg_max_lim = gen[:, IDX.gen.QG] > gen[:, IDX.gen.QMAX] + ppopt["OPF_VIOLATION"]
|
189
|
+
qg_min_lim = gen[:, IDX.gen.QG] < gen[:, IDX.gen.QMIN] - ppopt["OPF_VIOLATION"]
|
190
|
+
|
191
|
+
mx = find(gen_status & qg_max_lim)
|
192
|
+
mn = find(gen_status & qg_min_lim)
|
193
|
+
|
194
|
+
if len(mx) > 0 or len(mn) > 0: # we have some Q limit violations
|
195
|
+
# first check for INFEASIBILITY (all remaining gens violating)
|
196
|
+
gen_violate = np.union1d(mx, mn)
|
197
|
+
gen_bus = gen[:, IDX.gen.GEN_BUS].astype(int)
|
198
|
+
cond_pv = bus[gen_bus, IDX.bus.BUS_TYPE] == IDX.bus.PV
|
199
|
+
cond_ref = bus[gen_bus, IDX.bus.BUS_TYPE] == IDX.bus.REF
|
200
|
+
gen_remain = find(gen_status & (cond_pv | cond_ref))
|
201
|
+
msg = f'gen_violate = {gen_violate}\nremaining = {gen_remain}'
|
202
|
+
logger.debug(msg)
|
203
|
+
# TODO: condition can be improved, but not sure now
|
204
|
+
if len(gen_remain) <= 0:
|
205
|
+
msg = f'Infeasible: remaining {len(gen_violate)} gens exceed Q limits.'
|
206
|
+
logger.warning(msg)
|
207
|
+
success = 0
|
208
|
+
break
|
209
|
+
|
210
|
+
# one at a time?
|
211
|
+
if ppopt["ENFORCE_Q_LIMS"] == 2: # fix largest violation, ignore the rest
|
212
|
+
k = np.argmax(np.r_[gen[mx, IDX.gen.QG] - gen[mx, IDX.gen.QMAX],
|
213
|
+
gen[mn, IDX.gen.QMIN] - gen[mn, IDX.gen.QG]])
|
214
|
+
if k > len(mx):
|
215
|
+
mn = mn[k - len(mx)]
|
216
|
+
mx = []
|
217
|
+
else:
|
218
|
+
mx = mx[k]
|
219
|
+
mn = []
|
220
|
+
|
221
|
+
if len(mx) > 0:
|
222
|
+
msg = 'Following Gen convert to PQ bus because'
|
223
|
+
msg += ' exceed Q upper limits:\n'
|
224
|
+
msg += ', '.join(str(i + 1) for i in mx)
|
225
|
+
logger.debug(msg)
|
226
|
+
if len(mn) > 0:
|
227
|
+
msg = 'Following Gen convert to PQ bus because'
|
228
|
+
msg += ' exceed Q lower limits:\n'
|
229
|
+
msg += ', '.join(str(i + 1) for i in mn)
|
230
|
+
logger.debug(msg)
|
231
|
+
|
232
|
+
# save corresponding limit values
|
233
|
+
fixedQg[mx] = gen[mx, IDX.gen.QMAX]
|
234
|
+
fixedQg[mn] = gen[mn, IDX.gen.QMIN]
|
235
|
+
mx = np.r_[mx, mn].astype(int)
|
236
|
+
|
237
|
+
# convert to PQ bus
|
238
|
+
# Convert generators PV bus to PQ bus
|
239
|
+
for i in range(len(mx)):
|
240
|
+
idx = mx[i]
|
241
|
+
gen[idx, IDX.gen.QG] = fixedQg[idx] # Set Qg to binding
|
242
|
+
gen[idx, IDX.gen.GEN_STATUS] = 0 # Temporarily turn off generator
|
243
|
+
bi = int(gen[idx, IDX.gen.GEN_BUS]) # Get the bus index
|
244
|
+
bus[bi, [IDX.bus.PD, IDX.bus.QD]] -= gen[idx, [IDX.gen.PG, IDX.gen.QG]] # Adjust load
|
245
|
+
|
246
|
+
if len(ref) > 1 and any(bus[gen[mx, IDX.gen.GEN_BUS], IDX.bus.BUS_TYPE] == IDX.bus.REF):
|
247
|
+
raise ValueError("PYPOWER cannot enforce Q limits for systems with multiple slack buses. "
|
248
|
+
"Please ensure there is only one slack bus in the system.")
|
249
|
+
|
250
|
+
# set bus type to PQ
|
251
|
+
bus[gen[mx, IDX.gen.GEN_BUS].astype(int), IDX.bus.BUS_TYPE] = IDX.bus.PQ
|
252
|
+
|
253
|
+
# update bus index lists of each type of bus
|
254
|
+
ref_temp = ref
|
255
|
+
ref, pv, pq = putils.bustypes(bus, gen)
|
256
|
+
|
257
|
+
# previous line can modify lists to select new REF bus
|
258
|
+
# if there was none, so we should update bus with these
|
259
|
+
# just to keep them consistent
|
260
|
+
if ref != ref_temp:
|
261
|
+
bus[ref, IDX.bus.BUS_TYPE] = IDX.bus.REF
|
262
|
+
bus[pv, IDX.bus.BUS_TYPE] = pv
|
263
|
+
logger.debug(f'Bus<{ref[0]}> is new slack bus')
|
264
|
+
|
265
|
+
limited = np.r_[limited, mx].astype(int)
|
266
|
+
else:
|
267
|
+
repeat = 0 # no more generator Q limits violated
|
268
|
+
else:
|
269
|
+
repeat = 0 # don't enforce generator Q limits, once is enough
|
270
|
+
|
271
|
+
if ppopt["ENFORCE_Q_LIMS"] and len(limited) > 0:
|
272
|
+
# Restore injections from limited gens [those at Q limits]
|
273
|
+
for i in range(len(limited)):
|
274
|
+
idx = limited[i]
|
275
|
+
gen[idx, IDX.gen.QG] = fixedQg[idx] # Restore Qg value
|
276
|
+
bi = int(gen[idx, IDX.gen.GEN_BUS]) # Get the bus index
|
277
|
+
bus[bi, [IDX.bus.PD, IDX.bus.QD]] += gen[idx,
|
278
|
+
[IDX.gen.PG, IDX.gen.QG]] # Re-adjust load
|
279
|
+
gen[idx, IDX.gen.GEN_STATUS] = 1 # Turn generator back on
|
280
|
+
|
281
|
+
if ref != ref0:
|
282
|
+
# adjust voltage angles to make original ref bus correct
|
283
|
+
bus[:, IDX.bus.VA] = bus[:, IDX.bus.VA] - bus[ref0, IDX.bus.VA] + Varef0
|
284
|
+
|
285
|
+
ppc["et"] = time() - t0
|
286
|
+
ppc["success"] = success
|
287
|
+
|
288
|
+
# ----- output results -----
|
289
|
+
# convert back to original bus numbering & print results
|
290
|
+
ppc["bus"], ppc["gen"], ppc["branch"] = bus, gen, branch
|
291
|
+
results = opfcn.int2ext(ppc)
|
292
|
+
|
293
|
+
# zero out result fields of out-of-service gens & branches
|
294
|
+
if len(results["order"]["gen"]["status"]["off"]) > 0:
|
295
|
+
results["gen"][np.ix_(results["order"]["gen"]["status"]["off"], [IDX.gen.PG, IDX.gen.QG])] = 0
|
296
|
+
|
297
|
+
if len(results["order"]["branch"]["status"]["off"]) > 0:
|
298
|
+
results["branch"][
|
299
|
+
np.ix_(
|
300
|
+
results["order"]["branch"]["status"]["off"],
|
301
|
+
[IDX.branch.PF,
|
302
|
+
IDX.branch.QF,
|
303
|
+
IDX.branch.PT,
|
304
|
+
IDX.branch.QT])] = 0
|
305
|
+
|
306
|
+
return results, sstats
|
307
|
+
|
308
|
+
|
309
|
+
def dcpf(B, Pbus, Va0, ref, pv, pq):
|
310
|
+
"""
|
311
|
+
Solves a DC power flow.
|
312
|
+
|
313
|
+
Solves for the bus voltage angles at all but the reference bus, given the
|
314
|
+
full system B matrix and the vector of bus real power injections, the
|
315
|
+
initial vector of bus voltage angles (rad), and column vectors with
|
316
|
+
the lists of bus indices for the swing bus, PV buses, and PQ buses,
|
317
|
+
respectively.
|
318
|
+
|
319
|
+
Parameters
|
320
|
+
----------
|
321
|
+
B : ndarray
|
322
|
+
The system B matrix.
|
323
|
+
Pbus : ndarray
|
324
|
+
Vector of bus real power injections.
|
325
|
+
Va0 : ndarray
|
326
|
+
Initial vector of bus voltage angles (in radians).
|
327
|
+
ref : int
|
328
|
+
Index of the reference bus.
|
329
|
+
pv : ndarray
|
330
|
+
List of bus indices for IDX.bus.PV buses.
|
331
|
+
pq : ndarray
|
332
|
+
List of bus indices for IDX.bus.PQ buses.
|
333
|
+
|
334
|
+
Returns
|
335
|
+
-------
|
336
|
+
Va : ndarray
|
337
|
+
Vector of bus voltage angles in radians.
|
338
|
+
|
339
|
+
Author
|
340
|
+
------
|
341
|
+
Carlos E. Murillo-Sanchez (PSERC Cornell & Universidad Autonoma de Manizales)
|
342
|
+
|
343
|
+
Ray Zimmerman (PSERC Cornell)
|
344
|
+
"""
|
345
|
+
pvpq = np.r_[pv, pq][np.newaxis, :]
|
346
|
+
|
347
|
+
# initialize result vector
|
348
|
+
Va = np.copy(Va0)
|
349
|
+
|
350
|
+
# update angles for non-reference buses
|
351
|
+
Va[pvpq] = spsolve(B[pvpq.T, pvpq], np.transpose(Pbus[pvpq] - B[pvpq.T, ref] * Va0[ref]))
|
352
|
+
|
353
|
+
return Va
|
354
|
+
|
355
|
+
|
356
|
+
def newtonpf(Ybus, Sbus, V0, ref, pv, pq, ppopt):
|
357
|
+
"""
|
358
|
+
Solves the power flow using a full Newton's method.
|
359
|
+
|
360
|
+
Parameters
|
361
|
+
----------
|
362
|
+
Ybus : array-like
|
363
|
+
Full system admittance matrix (for all buses).
|
364
|
+
Sbus : array-like
|
365
|
+
Complex bus power injection vector (for all buses).
|
366
|
+
V0 : array-like
|
367
|
+
Initial vector of complex bus voltages.
|
368
|
+
ref : int
|
369
|
+
Index of the swing bus.
|
370
|
+
pv : list of int
|
371
|
+
List of bus indices for PV buses.
|
372
|
+
pq : list of int
|
373
|
+
List of bus indices for PQ buses.
|
374
|
+
ppopt : dict, optional
|
375
|
+
PYPOWER options vector which can be used to set the termination tolerance,
|
376
|
+
maximum number of iterations, and output options (see ppoption for details).
|
377
|
+
Default is None, which uses default options.
|
378
|
+
|
379
|
+
Returns
|
380
|
+
-------
|
381
|
+
V : array-like
|
382
|
+
Final complex voltages.
|
383
|
+
converged : bool
|
384
|
+
Flag indicating whether the power flow converged or not.
|
385
|
+
i : int
|
386
|
+
Number of iterations performed.
|
387
|
+
|
388
|
+
Notes
|
389
|
+
-----
|
390
|
+
Solves for bus voltages given the full system admittance matrix (for
|
391
|
+
all buses), the complex bus power injection vector (for all buses),
|
392
|
+
the initial vector of complex bus voltages, and column vectors with
|
393
|
+
the lists of bus indices for the swing bus, PV buses, and PQ buses,
|
394
|
+
respectively. The bus voltage vector contains the set point for
|
395
|
+
generator (including the reference bus) buses, and the reference angle
|
396
|
+
of the swing bus, as well as an initial guess for remaining magnitudes
|
397
|
+
and angles. Uses default options if the ppopt parameter is not given.
|
398
|
+
|
399
|
+
Author
|
400
|
+
------
|
401
|
+
Ray Zimmerman (PSERC Cornell)
|
402
|
+
"""
|
403
|
+
ppopt = ppoption(ppopt)
|
404
|
+
# options
|
405
|
+
tol = ppopt['PF_TOL']
|
406
|
+
max_it = ppopt['PF_MAX_IT']
|
407
|
+
verbose = ppopt['VERBOSE']
|
408
|
+
|
409
|
+
# initialize
|
410
|
+
converged = 0
|
411
|
+
i = 0
|
412
|
+
V = V0
|
413
|
+
Va = np.angle(V)
|
414
|
+
Vm = abs(V)
|
415
|
+
|
416
|
+
# set up indexing for updating V
|
417
|
+
pvpq = np.r_[pv, pq]
|
418
|
+
npv = len(pv)
|
419
|
+
npq = len(pq)
|
420
|
+
j1 = 0
|
421
|
+
j2 = npv # j1:j2 - V angle of pv buses
|
422
|
+
j3 = j2
|
423
|
+
j4 = j2 + npq # j3:j4 - V angle of pq buses
|
424
|
+
j5 = j4
|
425
|
+
j6 = j4 + npq # j5:j6 - V mag of pq buses
|
426
|
+
|
427
|
+
# evaluate F(x0)
|
428
|
+
mis = V * np.conj(Ybus * V) - Sbus
|
429
|
+
F = np.r_[mis[pv].real,
|
430
|
+
mis[pq].real,
|
431
|
+
mis[pq].imag]
|
432
|
+
|
433
|
+
# check tolerance
|
434
|
+
normF = np.linalg.norm(F, inf)
|
435
|
+
logger.info('%2d: |F(x)| = %.10g', i, normF)
|
436
|
+
converged = normF < tol
|
437
|
+
|
438
|
+
# do Newton iterations
|
439
|
+
while (not converged and i < max_it):
|
440
|
+
# update iteration counter
|
441
|
+
i = i + 1
|
442
|
+
|
443
|
+
# evaluate Jacobian
|
444
|
+
dS_dVm, dS_dVa = dSbus_dV(Ybus, V)
|
445
|
+
|
446
|
+
J11 = dS_dVa[np.array([pvpq]).T, pvpq].real
|
447
|
+
J12 = dS_dVm[np.array([pvpq]).T, pq].real
|
448
|
+
J21 = dS_dVa[np.array([pq]).T, pvpq].imag
|
449
|
+
J22 = dS_dVm[np.array([pq]).T, pq].imag
|
450
|
+
|
451
|
+
J = vstack([
|
452
|
+
hstack([J11, J12]),
|
453
|
+
hstack([J21, J22])
|
454
|
+
], format="csr")
|
455
|
+
|
456
|
+
# compute update step
|
457
|
+
dx = -1 * spsolve(J, F)
|
458
|
+
|
459
|
+
# update voltage
|
460
|
+
if npv:
|
461
|
+
Va[pv] = Va[pv] + dx[j1:j2]
|
462
|
+
if npq:
|
463
|
+
Va[pq] = Va[pq] + dx[j3:j4]
|
464
|
+
Vm[pq] = Vm[pq] + dx[j5:j6]
|
465
|
+
V = Vm * np.exp(1j * Va)
|
466
|
+
Vm = abs(V) # update Vm and Va again in case
|
467
|
+
Va = np.angle(V) # we wrapped around with a negative Vm
|
468
|
+
|
469
|
+
# evalute F(x)
|
470
|
+
mis = V * np.conj(Ybus * V) - Sbus
|
471
|
+
F = np.r_[mis[pv].real,
|
472
|
+
mis[pq].real,
|
473
|
+
mis[pq].imag]
|
474
|
+
|
475
|
+
# check for convergence
|
476
|
+
normF = np.linalg.norm(F, inf)
|
477
|
+
logger.info('%2d: |F(x)| = %.10g', i, normF)
|
478
|
+
converged = normF < tol
|
479
|
+
|
480
|
+
return V, converged, i
|
481
|
+
|
482
|
+
|
483
|
+
def pfsoln(baseMVA, bus0, gen0, branch0, Ybus, Yf, Yt, V, ref, pv, pq):
|
484
|
+
"""
|
485
|
+
Updates bus, gen, and branch data structures to match power flow solution.
|
486
|
+
|
487
|
+
This function takes the following inputs and updates the input data structures:
|
488
|
+
|
489
|
+
Parameters
|
490
|
+
----------
|
491
|
+
baseMVA : float
|
492
|
+
Base power in MVA.
|
493
|
+
bus0 : ndarray
|
494
|
+
Initial bus data structure.
|
495
|
+
gen0 : ndarray
|
496
|
+
Initial gen data structure.
|
497
|
+
branch0 : ndarray
|
498
|
+
Initial branch data structure.
|
499
|
+
Ybus : sparse matrix
|
500
|
+
Admittance matrix.
|
501
|
+
Yf : sparse matrix
|
502
|
+
Admittance matrix for "from" end of branches.
|
503
|
+
Yt : sparse matrix
|
504
|
+
Admittance matrix for "to" end of branches.
|
505
|
+
V : ndarray
|
506
|
+
Bus voltage magnitude in per unit.
|
507
|
+
ref : int
|
508
|
+
Reference bus index.
|
509
|
+
pv : ndarray
|
510
|
+
PV bus indices.
|
511
|
+
pq : ndarray
|
512
|
+
PQ bus indices.
|
513
|
+
|
514
|
+
Returns
|
515
|
+
-------
|
516
|
+
bus : ndarray
|
517
|
+
Updated bus data structure.
|
518
|
+
gen : ndarray
|
519
|
+
Updated gen data structure.
|
520
|
+
branch : ndarray
|
521
|
+
Updated branch data structure.
|
522
|
+
|
523
|
+
Author
|
524
|
+
------
|
525
|
+
Ray Zimmerman (PSERC Cornell)
|
526
|
+
"""
|
527
|
+
# initialize return values
|
528
|
+
bus = bus0
|
529
|
+
gen = gen0
|
530
|
+
branch = branch0
|
531
|
+
|
532
|
+
# ----- update bus voltages -----
|
533
|
+
bus[:, IDX.bus.VM] = abs(V)
|
534
|
+
bus[:, IDX.bus.VA] = np.angle(V) * rad2deg
|
535
|
+
|
536
|
+
# ----- update Qg for all gens and Pg for slack bus(es) -----
|
537
|
+
# generator info
|
538
|
+
on = find(gen[:, IDX.gen.GEN_STATUS] > 0) # which generators are on?
|
539
|
+
gbus = gen[on, IDX.gen.GEN_BUS].astype(int) # what buses are they at?
|
540
|
+
|
541
|
+
# compute total injected bus powers
|
542
|
+
Sbus = V[gbus] * np.conj(Ybus[gbus, :] * V)
|
543
|
+
|
544
|
+
# update Qg for all generators
|
545
|
+
gen[:, IDX.gen.QG] = np.zeros(gen.shape[0]) # zero out all Qg
|
546
|
+
gen[on, IDX.gen.QG] = Sbus.imag * baseMVA + bus[gbus, IDX.bus.QD] # inj Q + local Qd
|
547
|
+
# ... at this point any buses with more than one generator will have
|
548
|
+
# the total Q dispatch for the bus assigned to each generator. This
|
549
|
+
# must be split between them. We do it first equally, then in proportion
|
550
|
+
# to the reactive range of the generator.
|
551
|
+
|
552
|
+
if len(on) > 1:
|
553
|
+
# build connection matrix, element i, j is 1 if gen on(i) at bus j is ON
|
554
|
+
nb = bus.shape[0]
|
555
|
+
ngon = on.shape[0]
|
556
|
+
Cg = c_sparse((np.ones(ngon), (range(ngon), gbus)), (ngon, nb))
|
557
|
+
|
558
|
+
# divide Qg by number of generators at the bus to distribute equally
|
559
|
+
ngg = Cg * Cg.sum(0).T # ngon x 1, number of gens at this gen's bus
|
560
|
+
ngg = np.asarray(ngg).flatten() # 1D array
|
561
|
+
gen[on, IDX.gen.QG] = gen[on, IDX.gen.QG] / ngg
|
562
|
+
|
563
|
+
# divide proportionally
|
564
|
+
Cmin = c_sparse((gen[on, IDX.gen.QMIN], (range(ngon), gbus)), (ngon, nb))
|
565
|
+
Cmax = c_sparse((gen[on, IDX.gen.QMAX], (range(ngon), gbus)), (ngon, nb))
|
566
|
+
Qg_tot = Cg.T * gen[on, IDX.gen.QG] # nb x 1 vector of total Qg at each bus
|
567
|
+
Qg_min = Cmin.sum(0).T # nb x 1 vector of min total Qg at each bus
|
568
|
+
Qg_max = Cmax.sum(0).T # nb x 1 vector of max total Qg at each bus
|
569
|
+
Qg_min = np.asarray(Qg_min).flatten() # 1D array
|
570
|
+
Qg_max = np.asarray(Qg_max).flatten() # 1D array
|
571
|
+
# gens at buses with Qg range = 0
|
572
|
+
ig = find(Cg * Qg_min == Cg * Qg_max)
|
573
|
+
Qg_save = gen[on[ig], IDX.gen.QG]
|
574
|
+
gen[on, IDX.gen.QG] = gen[on, IDX.gen.QMIN] + \
|
575
|
+
(Cg * ((Qg_tot - Qg_min) / (Qg_max - Qg_min + EPS))) * \
|
576
|
+
(gen[on, IDX.gen.QMAX] - gen[on, IDX.gen.QMIN]) # ^ avoid div by 0
|
577
|
+
gen[on[ig], IDX.gen.QG] = Qg_save # (terms are mult by 0 anyway)
|
578
|
+
|
579
|
+
# update Pg for slack bus(es)
|
580
|
+
# inj P + local Pd
|
581
|
+
for k in range(len(ref)):
|
582
|
+
refgen = find(gbus == ref[k]) # which is(are) the reference gen(s)?
|
583
|
+
gen[on[refgen[0]], IDX.gen.PG] = \
|
584
|
+
Sbus[refgen[0]].real * baseMVA + bus[ref[k], IDX.bus.PD]
|
585
|
+
if len(refgen) > 1: # more than one generator at this ref bus
|
586
|
+
# subtract off what is generated by other gens at this bus
|
587
|
+
gen[on[refgen[0]], IDX.gen.PG] = \
|
588
|
+
gen[on[refgen[0]], IDX.gen.PG] - sum(gen[on[refgen[1:len(refgen)]], IDX.gen.PG])
|
589
|
+
|
590
|
+
# ----- update/compute branch power flows -----
|
591
|
+
out = find(branch[:, IDX.branch.BR_STATUS] == 0) # out-of-service branches
|
592
|
+
br = find(branch[:, IDX.branch.BR_STATUS]).astype(int) # in-service branches
|
593
|
+
|
594
|
+
# complex power at "from" bus
|
595
|
+
Sf = V[branch[br, IDX.branch.F_BUS].astype(int)] * np.conj(Yf[br, :] * V) * baseMVA
|
596
|
+
# complex power injected at "to" bus
|
597
|
+
St = V[branch[br, IDX.branch.T_BUS].astype(int)] * np.conj(Yt[br, :] * V) * baseMVA
|
598
|
+
branch[np.ix_(br, [IDX.branch.PF, IDX.branch.QF, IDX.branch.PT, IDX.branch.QT])
|
599
|
+
] = np.c_[Sf.real, Sf.imag, St.real, St.imag]
|
600
|
+
branch[np.ix_(out, [IDX.branch.PF, IDX.branch.QF, IDX.branch.PT, IDX.branch.QT])
|
601
|
+
] = np.zeros((len(out), 4))
|
602
|
+
|
603
|
+
return bus, gen, branch
|
604
|
+
|
605
|
+
|
606
|
+
def gausspf(Ybus, Sbus, V0, ref, pv, pq, ppopt):
|
607
|
+
"""
|
608
|
+
Solves the power flow using a Gauss-Seidel method.
|
609
|
+
This method seems to be much more slower than Newton's method,
|
610
|
+
not fully checked yet.
|
611
|
+
|
612
|
+
Parameters
|
613
|
+
----------
|
614
|
+
Ybus : array-like
|
615
|
+
Full system admittance matrix (for all buses).
|
616
|
+
Sbus : array-like
|
617
|
+
Complex bus power injection vector (for all buses).
|
618
|
+
V0 : array-like
|
619
|
+
Initial vector of complex bus voltages.
|
620
|
+
ref : int
|
621
|
+
Index of the swing bus.
|
622
|
+
pv : list of int
|
623
|
+
List of bus indices for PV buses.
|
624
|
+
pq : list of int
|
625
|
+
List of bus indices for PQ buses.
|
626
|
+
ppopt : dict
|
627
|
+
PYPOWER options vector which can be used to set the termination tolerance,
|
628
|
+
maximum number of iterations, and output options (see ppoption for details).
|
629
|
+
|
630
|
+
Returns
|
631
|
+
-------
|
632
|
+
V : array-like
|
633
|
+
Final complex voltages.
|
634
|
+
converged : bool
|
635
|
+
Flag indicating whether the power flow converged or not.
|
636
|
+
i : int
|
637
|
+
Number of iterations performed.
|
638
|
+
|
639
|
+
Notes
|
640
|
+
-----
|
641
|
+
Solves for bus voltages given the full system admittance matrix (for
|
642
|
+
all buses), the complex bus power injection vector (for all buses),
|
643
|
+
the initial vector of complex bus voltages, and column vectors with
|
644
|
+
the lists of bus indices for the swing bus, PV buses, and PQ buses,
|
645
|
+
respectively. The bus voltage vector contains the set point for
|
646
|
+
generator (including the reference bus) buses, and the reference angle
|
647
|
+
of the swing bus, as well as an initial guess for remaining magnitudes
|
648
|
+
and angles. Uses default options if the ppopt parameter is not given.
|
649
|
+
|
650
|
+
Author
|
651
|
+
------
|
652
|
+
Ray Zimmerman (PSERC Cornell)
|
653
|
+
|
654
|
+
Alberto Borghetti (University of Bologna, Italy)
|
655
|
+
"""
|
656
|
+
ppopt = ppoption(ppopt)
|
657
|
+
# options
|
658
|
+
tol = ppopt['PF_TOL']
|
659
|
+
max_it = ppopt['PF_MAX_IT_GS']
|
660
|
+
|
661
|
+
# initialize
|
662
|
+
converged = 0
|
663
|
+
i = 0
|
664
|
+
V = V0.copy()
|
665
|
+
# Va = angle(V)
|
666
|
+
Vm = abs(V)
|
667
|
+
|
668
|
+
# set up indexing for updating V
|
669
|
+
npv = len(pv)
|
670
|
+
npq = len(pq)
|
671
|
+
pvpq = np.r_[pv, pq]
|
672
|
+
|
673
|
+
# evaluate F(x0)
|
674
|
+
mis = V * np.conj(Ybus * V) - Sbus
|
675
|
+
F = np.r_[mis[pvpq].real,
|
676
|
+
mis[pq].imag]
|
677
|
+
|
678
|
+
# check tolerance
|
679
|
+
normF = np.linalg.norm(F, inf)
|
680
|
+
logger.info('%d: |F(x)| = %.10g', i, normF)
|
681
|
+
converged = normF < tol
|
682
|
+
|
683
|
+
# do Gauss-Seidel iterations
|
684
|
+
while (not converged and i < max_it):
|
685
|
+
# update iteration counter
|
686
|
+
i = i + 1
|
687
|
+
|
688
|
+
# update voltage
|
689
|
+
# at PQ buses
|
690
|
+
for k in pq[list(range(npq))]:
|
691
|
+
tmp = (np.conj(Sbus[k] / V[k]) - Ybus[k, :] * V) / Ybus[k, k]
|
692
|
+
V[k] = V[k] + tmp.item()
|
693
|
+
|
694
|
+
# at PV buses
|
695
|
+
if npv:
|
696
|
+
for k in pv[list(range(npv))]:
|
697
|
+
tmp = (V[k] * np.conj(Ybus[k, :] * V)).imag
|
698
|
+
Sbus[k] = Sbus[k].real + 1j * tmp.item()
|
699
|
+
tmp = (np.conj(Sbus[k] / V[k]) - Ybus[k, :] * V) / Ybus[k, k]
|
700
|
+
V[k] = V[k] + tmp.item()
|
701
|
+
# V[k] = Vm[k] * V[k] / abs(V[k])
|
702
|
+
V[pv] = Vm[pv] * V[pv] / abs(V[pv])
|
703
|
+
|
704
|
+
# evalute F(x)
|
705
|
+
mis = V * np.conj(Ybus * V) - Sbus
|
706
|
+
F = np.r_[mis[pv].real,
|
707
|
+
mis[pq].real,
|
708
|
+
mis[pq].imag]
|
709
|
+
|
710
|
+
# check for convergence
|
711
|
+
normF = np.linalg.norm(F, inf)
|
712
|
+
logger.info('%d: |F(x)| = %.10g', i, normF)
|
713
|
+
converged = normF < tol
|
714
|
+
|
715
|
+
return V, converged, i
|
716
|
+
|
717
|
+
|
718
|
+
def fdpf(Ybus, Sbus, V0, Bp, Bpp, ref, pv, pq, ppopt):
|
719
|
+
"""
|
720
|
+
Solves the power flow using a fast decoupled method.
|
721
|
+
|
722
|
+
Solves for bus voltages given the full system admittance matrix (for
|
723
|
+
all buses), the complex bus power injection vector (for all buses),
|
724
|
+
the initial vector of complex bus voltages, the FDPF matrices B prime
|
725
|
+
and B double prime, and column vectors with the lists of bus indices
|
726
|
+
for the swing bus, PV buses, and PQ buses, respectively. The bus voltage
|
727
|
+
vector contains the set point for generator (including the reference bus)
|
728
|
+
buses, and the reference angle of the swing bus, as well as an initial
|
729
|
+
guess for remaining magnitudes and angles. `ppopt` is a PYPOWER options
|
730
|
+
vector which can be used to set the termination tolerance, maximum
|
731
|
+
number of iterations, and output options (see `ppoption` for details).
|
732
|
+
Uses default options if this parameter is not given. Returns the
|
733
|
+
final complex voltages, a flag which indicates whether it converged
|
734
|
+
or not, and the number of iterations performed.
|
735
|
+
|
736
|
+
Parameters
|
737
|
+
----------
|
738
|
+
Ybus : ndarray
|
739
|
+
Full system admittance matrix (for all buses).
|
740
|
+
Sbus : ndarray
|
741
|
+
Complex bus power injection vector (for all buses).
|
742
|
+
V0 : ndarray
|
743
|
+
Initial vector of complex bus voltages.
|
744
|
+
Bp : ndarray
|
745
|
+
FDPF matrix B prime.
|
746
|
+
Bpp : ndarray
|
747
|
+
FDPF matrix B double prime.
|
748
|
+
ref : int
|
749
|
+
Index of the reference bus.
|
750
|
+
pv : ndarray
|
751
|
+
List of bus indices for PV buses.
|
752
|
+
pq : ndarray
|
753
|
+
List of bus indices for PQ buses.
|
754
|
+
ppopt : dict
|
755
|
+
PYPOWER options vector.
|
756
|
+
|
757
|
+
Returns
|
758
|
+
-------
|
759
|
+
V : ndarray
|
760
|
+
Final complex voltages.
|
761
|
+
converged : bool
|
762
|
+
Flag indicating whether the power flow converged.
|
763
|
+
i : int
|
764
|
+
Number of iterations performed.
|
765
|
+
|
766
|
+
Author
|
767
|
+
------
|
768
|
+
Ray Zimmerman (PSERC Cornell)
|
769
|
+
"""
|
770
|
+
ppopt = ppoption(ppopt)
|
771
|
+
# options
|
772
|
+
tol = ppopt['PF_TOL']
|
773
|
+
max_it = ppopt['PF_MAX_IT_FD']
|
774
|
+
verbose = ppopt['VERBOSE']
|
775
|
+
|
776
|
+
# initialize
|
777
|
+
converged = 0
|
778
|
+
i = 0
|
779
|
+
V = V0
|
780
|
+
Va = np.angle(V)
|
781
|
+
Vm = abs(V)
|
782
|
+
|
783
|
+
# set up indexing for updating V
|
784
|
+
# npv = len(pv)
|
785
|
+
# npq = len(pq)
|
786
|
+
pvpq = np.r_[pv, pq]
|
787
|
+
|
788
|
+
# evaluate initial mismatch
|
789
|
+
mis = (V * np.conj(Ybus * V) - Sbus) / Vm
|
790
|
+
P = mis[pvpq].real
|
791
|
+
Q = mis[pq].imag
|
792
|
+
|
793
|
+
# check tolerance
|
794
|
+
normP = np.linalg.norm(P, inf)
|
795
|
+
normQ = np.linalg.norm(Q, inf)
|
796
|
+
logger.info(f'{i} --: |P| = {normP:.10g}, |Q| = {normQ:.10g}')
|
797
|
+
converged = normP < tol and normQ < tol
|
798
|
+
|
799
|
+
# reduce B matrices
|
800
|
+
Bp = Bp[np.array([pvpq]).T, pvpq].tocsc() # splu requires a CSC matrix
|
801
|
+
Bpp = Bpp[np.array([pq]).T, pq].tocsc()
|
802
|
+
|
803
|
+
# factor B matrices
|
804
|
+
Bp_solver = splu(Bp)
|
805
|
+
Bpp_solver = splu(Bpp)
|
806
|
+
|
807
|
+
# do P and Q iterations
|
808
|
+
while (not converged and i < max_it):
|
809
|
+
# update iteration counter
|
810
|
+
i = i + 1
|
811
|
+
|
812
|
+
# ----- do P iteration, update Va -----
|
813
|
+
dVa = -Bp_solver.solve(P)
|
814
|
+
|
815
|
+
# update voltage
|
816
|
+
Va[pvpq] = Va[pvpq] + dVa
|
817
|
+
V = Vm * np.exp(1j * Va)
|
818
|
+
|
819
|
+
# evalute mismatch
|
820
|
+
mis = (V * np.conj(Ybus * V) - Sbus) / Vm
|
821
|
+
P = mis[pvpq].real
|
822
|
+
Q = mis[pq].imag
|
823
|
+
|
824
|
+
# check tolerance
|
825
|
+
normP = np.linalg.norm(P, inf)
|
826
|
+
normQ = np.linalg.norm(Q, inf)
|
827
|
+
logger.info(f'{i} P: |P| = {normP:.10g}, |Q| = {normQ:.10g}')
|
828
|
+
if normP < tol and normQ < tol:
|
829
|
+
converged = 1
|
830
|
+
break
|
831
|
+
|
832
|
+
# ----- do Q iteration, update Vm -----
|
833
|
+
dVm = -Bpp_solver.solve(Q)
|
834
|
+
|
835
|
+
# update voltage
|
836
|
+
Vm[pq] = Vm[pq] + dVm
|
837
|
+
V = Vm * np.exp(1j * Va)
|
838
|
+
|
839
|
+
# evalute mismatch
|
840
|
+
mis = (V * np.conj(Ybus * V) - Sbus) / Vm
|
841
|
+
P = mis[pvpq].real
|
842
|
+
Q = mis[pq].imag
|
843
|
+
|
844
|
+
# check tolerance
|
845
|
+
normP = np.linalg.norm(P, inf)
|
846
|
+
normQ = np.linalg.norm(Q, inf)
|
847
|
+
logger.info(f'{i} Q: |P| = {normP:.10g}, |Q| = {normQ:.10g}')
|
848
|
+
converged = normP < tol and normQ < tol
|
849
|
+
if converged:
|
850
|
+
break
|
851
|
+
|
852
|
+
return V, converged, i
|