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,1946 @@
|
|
1
|
+
"""
|
2
|
+
Module for OPF functions.
|
3
|
+
"""
|
4
|
+
import logging
|
5
|
+
from copy import deepcopy
|
6
|
+
|
7
|
+
import numpy as np
|
8
|
+
from numpy import flatnonzero as find
|
9
|
+
|
10
|
+
import scipy.sparse as sp
|
11
|
+
from scipy.sparse import csr_matrix as c_sparse
|
12
|
+
from scipy.sparse import lil_matrix as l_sparse
|
13
|
+
|
14
|
+
from ams.pypower.utils import isload, get_reorder, set_reorder
|
15
|
+
from ams.pypower.idx import IDX
|
16
|
+
from ams.pypower.make import (d2Sbus_dV2, dSbus_dV, dIbr_dV,
|
17
|
+
d2AIbr_dV2, d2ASbr_dV2, dSbr_dV,
|
18
|
+
makeSbus, dAbr_dV)
|
19
|
+
|
20
|
+
from ams.shared import inf
|
21
|
+
|
22
|
+
logger = logging.getLogger(__name__)
|
23
|
+
|
24
|
+
|
25
|
+
def opf_hessfcn(x, lmbda, om, Ybus, Yf, Yt, ppopt, il=None, cost_mult=1.0):
|
26
|
+
"""
|
27
|
+
Evaluates Hessian of Lagrangian for AC OPF.
|
28
|
+
|
29
|
+
Hessian evaluation function for AC optimal power flow, suitable
|
30
|
+
for use with L{pips}.
|
31
|
+
|
32
|
+
Examples::
|
33
|
+
Lxx = opf_hessfcn(x, lmbda, om, Ybus, Yf, Yt, ppopt)
|
34
|
+
Lxx = opf_hessfcn(x, lmbda, om, Ybus, Yf, Yt, ppopt, il)
|
35
|
+
Lxx = opf_hessfcn(x, lmbda, om, Ybus, Yf, Yt, ppopt, il, cost_mult)
|
36
|
+
|
37
|
+
@param x: optimization vector
|
38
|
+
@param lmbda: C{eqnonlin} - Lagrange multipliers on power balance
|
39
|
+
equations. C{ineqnonlin} - Kuhn-Tucker multipliers on constrained
|
40
|
+
branch flows.
|
41
|
+
@param om: OPF model object
|
42
|
+
@param Ybus: bus admittance matrix
|
43
|
+
@param Yf: admittance matrix for "from" end of constrained branches
|
44
|
+
@param Yt: admittance matrix for "to" end of constrained branches
|
45
|
+
@param ppopt: PYPOWER options vector
|
46
|
+
@param il: (optional) vector of branch indices corresponding to
|
47
|
+
branches with flow limits (all others are assumed to be unconstrained).
|
48
|
+
The default is C{range(nl)} (all branches). C{Yf} and C{Yt} contain
|
49
|
+
only the rows corresponding to C{il}.
|
50
|
+
@param cost_mult: (optional) Scale factor to be applied to the cost
|
51
|
+
(default = 1).
|
52
|
+
|
53
|
+
@return: Hessian of the Lagrangian.
|
54
|
+
|
55
|
+
@see: L{opf_costfcn}, L{opf_consfcn}
|
56
|
+
|
57
|
+
@author: Ray Zimmerman (PSERC Cornell)
|
58
|
+
@author: Carlos E. Murillo-Sanchez (PSERC Cornell & Universidad
|
59
|
+
Autonoma de Manizales)
|
60
|
+
"""
|
61
|
+
# ----- initialize -----
|
62
|
+
# unpack data
|
63
|
+
ppc = om.get_ppc()
|
64
|
+
baseMVA, bus, gen, branch, gencost = \
|
65
|
+
ppc["baseMVA"], ppc["bus"], ppc["gen"], ppc["branch"], ppc["gencost"]
|
66
|
+
cp = om.get_cost_params()
|
67
|
+
N, Cw, H, dd, rh, kk, mm = \
|
68
|
+
cp["N"], cp["Cw"], cp["H"], cp["dd"], cp["rh"], cp["kk"], cp["mm"]
|
69
|
+
vv, _, _, _ = om.get_idx()
|
70
|
+
|
71
|
+
# unpack needed parameters
|
72
|
+
nb = bus.shape[0] # number of buses
|
73
|
+
nl = branch.shape[0] # number of branches
|
74
|
+
ng = gen.shape[0] # number of dispatchable injections
|
75
|
+
nxyz = len(x) # total number of control vars of all types
|
76
|
+
|
77
|
+
# set default constrained lines
|
78
|
+
if il is None:
|
79
|
+
il = np.arange(nl) # all lines have limits by default
|
80
|
+
nl2 = len(il) # number of constrained lines
|
81
|
+
|
82
|
+
# grab Pg & Qg
|
83
|
+
Pg = x[vv["i1"]["Pg"]:vv["iN"]["Pg"]] # active generation in p.u.
|
84
|
+
Qg = x[vv["i1"]["Qg"]:vv["iN"]["Qg"]] # reactive generation in p.u.
|
85
|
+
|
86
|
+
# put Pg & Qg back in gen
|
87
|
+
gen[:, IDX.gen.PG] = Pg * baseMVA # active generation in MW
|
88
|
+
gen[:, IDX.gen.QG] = Qg * baseMVA # reactive generation in MVAr
|
89
|
+
|
90
|
+
# reconstruct V
|
91
|
+
Va = x[vv["i1"]["Va"]:vv["iN"]["Va"]]
|
92
|
+
Vm = x[vv["i1"]["Vm"]:vv["iN"]["Vm"]]
|
93
|
+
V = Vm * np.exp(1j * Va)
|
94
|
+
nxtra = nxyz - 2 * nb
|
95
|
+
pcost = gencost[np.arange(ng), :]
|
96
|
+
if gencost.shape[0] > ng:
|
97
|
+
qcost = gencost[np.arange(ng, 2 * ng), :]
|
98
|
+
else:
|
99
|
+
qcost = np.array([])
|
100
|
+
|
101
|
+
# ----- evaluate d2f -----
|
102
|
+
d2f_dPg2 = np.zeros(ng) # c_sparse((ng, 1)) ## w.r.t. p.u. Pg
|
103
|
+
d2f_dQg2 = np.zeros(ng) # c_sparse((ng, 1)) ## w.r.t. p.u. Qg
|
104
|
+
ipolp = find(pcost[:, IDX.cost.MODEL] == IDX.cost.POLYNOMIAL)
|
105
|
+
d2f_dPg2[ipolp] = \
|
106
|
+
baseMVA**2 * polycost(pcost[ipolp, :], Pg[ipolp] * baseMVA, 2)
|
107
|
+
if np.any(qcost): # Qg is not free
|
108
|
+
ipolq = find(qcost[:, IDX.cost.MODEL] == IDX.cost.POLYNOMIAL)
|
109
|
+
d2f_dQg2[ipolq] = \
|
110
|
+
baseMVA**2 * polycost(qcost[ipolq, :], Qg[ipolq] * baseMVA, 2)
|
111
|
+
i = np.r_[np.arange(vv["i1"]["Pg"], vv["iN"]["Pg"]),
|
112
|
+
np.arange(vv["i1"]["Qg"], vv["iN"]["Qg"])]
|
113
|
+
# d2f = c_sparse((sp.vstack([d2f_dPg2, d2f_dQg2]).toarray().flatten(),
|
114
|
+
# (i, i)), shape=(nxyz, nxyz))
|
115
|
+
d2f = c_sparse((np.r_[d2f_dPg2, d2f_dQg2], (i, i)), (nxyz, nxyz))
|
116
|
+
|
117
|
+
# generalized cost
|
118
|
+
if sp.issparse(N) and N.nnz > 0:
|
119
|
+
nw = N.shape[0]
|
120
|
+
r = N * x - rh # Nx - rhat
|
121
|
+
iLT = find(r < -kk) # below dead zone
|
122
|
+
iEQ = find((r == 0) & (kk == 0)) # dead zone doesn't exist
|
123
|
+
iGT = find(r > kk) # above dead zone
|
124
|
+
iND = np.r_[iLT, iEQ, iGT] # rows that are Not in the Dead region
|
125
|
+
iL = find(dd == 1) # rows using linear function
|
126
|
+
iQ = find(dd == 2) # rows using quadratic function
|
127
|
+
LL = c_sparse((np.ones(len(iL)), (iL, iL)), (nw, nw))
|
128
|
+
QQ = c_sparse((np.ones(len(iQ)), (iQ, iQ)), (nw, nw))
|
129
|
+
kbar = c_sparse((np.r_[np.ones(len(iLT)), np.zeros(len(iEQ)), -np.ones(len(iGT))],
|
130
|
+
(iND, iND)), (nw, nw)) * kk
|
131
|
+
rr = r + kbar # apply non-dead zone shift
|
132
|
+
M = c_sparse((mm[iND], (iND, iND)), (nw, nw)) # dead zone or scale
|
133
|
+
diagrr = c_sparse((rr, (np.arange(nw), np.arange(nw))), (nw, nw))
|
134
|
+
|
135
|
+
# linear rows multiplied by rr(i), quadratic rows by rr(i)^2
|
136
|
+
w = M * (LL + QQ * diagrr) * rr
|
137
|
+
HwC = H * w + Cw
|
138
|
+
AA = N.T * M * (LL + 2 * QQ * diagrr)
|
139
|
+
|
140
|
+
d2f = d2f + AA * H * AA.T + 2 * N.T * M * QQ * \
|
141
|
+
c_sparse((HwC, (np.arange(nw), np.arange(nw))), (nw, nw)) * N
|
142
|
+
d2f = d2f * cost_mult
|
143
|
+
|
144
|
+
# ----- evaluate Hessian of power balance constraints -----
|
145
|
+
nlam = int(len(lmbda["eqnonlin"]) / 2)
|
146
|
+
lamP = lmbda["eqnonlin"][:nlam]
|
147
|
+
lamQ = lmbda["eqnonlin"][nlam:nlam + nlam]
|
148
|
+
Gpaa, Gpav, Gpva, Gpvv = d2Sbus_dV2(Ybus, V, lamP)
|
149
|
+
Gqaa, Gqav, Gqva, Gqvv = d2Sbus_dV2(Ybus, V, lamQ)
|
150
|
+
|
151
|
+
d2G = sp.vstack([
|
152
|
+
sp.hstack([
|
153
|
+
sp.vstack([sp.hstack([Gpaa, Gpav]),
|
154
|
+
sp.hstack([Gpva, Gpvv])]).real +
|
155
|
+
sp.vstack([sp.hstack([Gqaa, Gqav]),
|
156
|
+
sp.hstack([Gqva, Gqvv])]).imag,
|
157
|
+
c_sparse((2 * nb, nxtra))]),
|
158
|
+
sp.hstack([
|
159
|
+
c_sparse((nxtra, 2 * nb)),
|
160
|
+
c_sparse((nxtra, nxtra))
|
161
|
+
])
|
162
|
+
], "csr")
|
163
|
+
|
164
|
+
# ----- evaluate Hessian of flow constraints -----
|
165
|
+
nmu = int(len(lmbda["ineqnonlin"]) / 2)
|
166
|
+
muF = lmbda["ineqnonlin"][:nmu]
|
167
|
+
muT = lmbda["ineqnonlin"][nmu:nmu + nmu]
|
168
|
+
if ppopt['OPF_FLOW_LIM'] == 2: # current
|
169
|
+
dIf_dVa, dIf_dVm, dIt_dVa, dIt_dVm, If, It = dIbr_dV(branch, Yf, Yt, V)
|
170
|
+
Hfaa, Hfav, Hfva, Hfvv = d2AIbr_dV2(dIf_dVa, dIf_dVm, If, Yf, V, muF)
|
171
|
+
Htaa, Htav, Htva, Htvv = d2AIbr_dV2(dIt_dVa, dIt_dVm, It, Yt, V, muT)
|
172
|
+
else:
|
173
|
+
f = branch[il, IDX.branch.F_BUS].astype(int) # list of "from" buses
|
174
|
+
t = branch[il, IDX.branch.T_BUS].astype(int) # list of "to" buses
|
175
|
+
# connection matrix for line & from buses
|
176
|
+
Cf = c_sparse((np.ones(nl2), (np.arange(nl2), f)), (nl2, nb))
|
177
|
+
# connection matrix for line & to buses
|
178
|
+
Ct = c_sparse((np.ones(nl2), (np.arange(nl2), t)), (nl2, nb))
|
179
|
+
dSf_dVa, dSf_dVm, dSt_dVa, dSt_dVm, Sf, St = \
|
180
|
+
dSbr_dV(branch[il, :], Yf, Yt, V)
|
181
|
+
if ppopt['OPF_FLOW_LIM'] == 1: # real power
|
182
|
+
Hfaa, Hfav, Hfva, Hfvv = d2ASbr_dV2(dSf_dVa.real, dSf_dVm.real,
|
183
|
+
Sf.real, Cf, Yf, V, muF)
|
184
|
+
Htaa, Htav, Htva, Htvv = d2ASbr_dV2(dSt_dVa.real, dSt_dVm.real,
|
185
|
+
St.real, Ct, Yt, V, muT)
|
186
|
+
else: # apparent power
|
187
|
+
Hfaa, Hfav, Hfva, Hfvv = \
|
188
|
+
d2ASbr_dV2(dSf_dVa, dSf_dVm, Sf, Cf, Yf, V, muF)
|
189
|
+
Htaa, Htav, Htva, Htvv = \
|
190
|
+
d2ASbr_dV2(dSt_dVa, dSt_dVm, St, Ct, Yt, V, muT)
|
191
|
+
|
192
|
+
d2H = sp.vstack([
|
193
|
+
sp.hstack([
|
194
|
+
sp.vstack([sp.hstack([Hfaa, Hfav]),
|
195
|
+
sp.hstack([Hfva, Hfvv])]) +
|
196
|
+
sp.vstack([sp.hstack([Htaa, Htav]),
|
197
|
+
sp.hstack([Htva, Htvv])]),
|
198
|
+
c_sparse((2 * nb, nxtra))
|
199
|
+
]),
|
200
|
+
sp.hstack([
|
201
|
+
c_sparse((nxtra, 2 * nb)),
|
202
|
+
c_sparse((nxtra, nxtra))
|
203
|
+
])
|
204
|
+
], "csr")
|
205
|
+
|
206
|
+
# ----- do numerical check using (central) finite differences -----
|
207
|
+
if 0:
|
208
|
+
nx = len(x)
|
209
|
+
step = 1e-5
|
210
|
+
num_d2f = c_sparse((nx, nx))
|
211
|
+
num_d2G = c_sparse((nx, nx))
|
212
|
+
num_d2H = c_sparse((nx, nx))
|
213
|
+
for i in range(nx):
|
214
|
+
xp = x
|
215
|
+
xm = x
|
216
|
+
xp[i] = x[i] + step / 2
|
217
|
+
xm[i] = x[i] - step / 2
|
218
|
+
# evaluate cost & gradients
|
219
|
+
_, dfp = opf_costfcn(xp, om)
|
220
|
+
_, dfm = opf_costfcn(xm, om)
|
221
|
+
# evaluate constraints & gradients
|
222
|
+
_, _, dHp, dGp = opf_consfcn(xp, om, Ybus, Yf, Yt, ppopt, il)
|
223
|
+
_, _, dHm, dGm = opf_consfcn(xm, om, Ybus, Yf, Yt, ppopt, il)
|
224
|
+
num_d2f[:, i] = cost_mult * (dfp - dfm) / step
|
225
|
+
num_d2G[:, i] = (dGp - dGm) * lmbda["eqnonlin"] / step
|
226
|
+
num_d2H[:, i] = (dHp - dHm) * lmbda["ineqnonlin"] / step
|
227
|
+
d2f_err = max(max(abs(d2f - num_d2f)))
|
228
|
+
d2G_err = max(max(abs(d2G - num_d2G)))
|
229
|
+
d2H_err = max(max(abs(d2H - num_d2H)))
|
230
|
+
if d2f_err > 1e-6:
|
231
|
+
print('Max difference in d2f: %g' % d2f_err)
|
232
|
+
if d2G_err > 1e-5:
|
233
|
+
print('Max difference in d2G: %g' % d2G_err)
|
234
|
+
if d2H_err > 1e-6:
|
235
|
+
print('Max difference in d2H: %g' % d2H_err)
|
236
|
+
|
237
|
+
return d2f + d2G + d2H
|
238
|
+
|
239
|
+
|
240
|
+
def opf_consfcn(x, om, Ybus, Yf, Yt, ppopt, il=None, *args):
|
241
|
+
"""
|
242
|
+
Evaluates nonlinear constraints and their Jacobian for OPF.
|
243
|
+
|
244
|
+
Constraint evaluation function for AC optimal power flow, suitable
|
245
|
+
for use with L{pips}. Computes constraint vectors and their gradients.
|
246
|
+
|
247
|
+
@param x: optimization vector
|
248
|
+
@param om: OPF model object
|
249
|
+
@param Ybus: bus admittance matrix
|
250
|
+
@param Yf: admittance matrix for "from" end of constrained branches
|
251
|
+
@param Yt: admittance matrix for "to" end of constrained branches
|
252
|
+
@param ppopt: PYPOWER options vector
|
253
|
+
@param il: (optional) vector of branch indices corresponding to
|
254
|
+
branches with flow limits (all others are assumed to be
|
255
|
+
unconstrained). The default is C{range(nl)} (all branches).
|
256
|
+
C{Yf} and C{Yt} contain only the rows corresponding to C{il}.
|
257
|
+
|
258
|
+
@return: C{h} - vector of inequality constraint values (flow limits)
|
259
|
+
limit^2 - flow^2, where the flow can be apparent power real power or
|
260
|
+
current, depending on value of C{OPF_FLOW_LIM} in C{ppopt} (only for
|
261
|
+
constrained lines). C{g} - vector of equality constraint values (power
|
262
|
+
balances). C{dh} - (optional) inequality constraint gradients, column
|
263
|
+
j is gradient of h(j). C{dg} - (optional) equality constraint gradients.
|
264
|
+
|
265
|
+
@see: L{opf_costfcn}, L{opf_hessfcn}
|
266
|
+
|
267
|
+
@author: Carlos E. Murillo-Sanchez (PSERC Cornell & Universidad
|
268
|
+
Autonoma de Manizales)
|
269
|
+
@author: Ray Zimmerman (PSERC Cornell)
|
270
|
+
"""
|
271
|
+
# ----- initialize -----
|
272
|
+
|
273
|
+
# unpack data
|
274
|
+
ppc = om.get_ppc()
|
275
|
+
baseMVA, bus, gen, branch = \
|
276
|
+
ppc["baseMVA"], ppc["bus"], ppc["gen"], ppc["branch"]
|
277
|
+
vv, _, _, _ = om.get_idx()
|
278
|
+
|
279
|
+
# problem dimensions
|
280
|
+
nb = bus.shape[0] # number of buses
|
281
|
+
nl = branch.shape[0] # number of branches
|
282
|
+
ng = gen.shape[0] # number of dispatchable injections
|
283
|
+
nxyz = len(x) # total number of control vars of all types
|
284
|
+
|
285
|
+
# set default constrained lines
|
286
|
+
if il is None:
|
287
|
+
il = np.arange(nl) # all lines have limits by default
|
288
|
+
nl2 = len(il) # number of constrained lines
|
289
|
+
|
290
|
+
# grab Pg & Qg
|
291
|
+
Pg = x[vv["i1"]["Pg"]:vv["iN"]["Pg"]] # active generation in p.u.
|
292
|
+
Qg = x[vv["i1"]["Qg"]:vv["iN"]["Qg"]] # reactive generation in p.u.
|
293
|
+
|
294
|
+
# put Pg & Qg back in gen
|
295
|
+
gen[:, IDX.gen.PG] = Pg * baseMVA # active generation in MW
|
296
|
+
gen[:, IDX.gen.QG] = Qg * baseMVA # reactive generation in MVAr
|
297
|
+
|
298
|
+
# rebuild Sbus
|
299
|
+
Sbus = makeSbus(baseMVA, bus, gen) # net injected power in p.u.
|
300
|
+
|
301
|
+
# ----- evaluate constraints -----
|
302
|
+
# reconstruct V
|
303
|
+
Va = x[vv["i1"]["Va"]:vv["iN"]["Va"]]
|
304
|
+
Vm = x[vv["i1"]["Vm"]:vv["iN"]["Vm"]]
|
305
|
+
V = Vm * np.exp(1j * Va)
|
306
|
+
|
307
|
+
# evaluate power flow equations
|
308
|
+
mis = V * np.conj(Ybus * V) - Sbus
|
309
|
+
|
310
|
+
# ----- evaluate constraint function values -----
|
311
|
+
# first, the equality constraints (power flow)
|
312
|
+
g = np.r_[mis.real, # active power mismatch for all buses
|
313
|
+
mis.imag] # reactive power mismatch for all buses
|
314
|
+
|
315
|
+
# then, the inequality constraints (branch flow limits)
|
316
|
+
if nl2 > 0:
|
317
|
+
flow_max = (branch[il, IDX.branch.RATE_A] / baseMVA)**2
|
318
|
+
flow_max[flow_max == 0] = inf
|
319
|
+
if ppopt['OPF_FLOW_LIM'] == 2: # current magnitude limit, |I|
|
320
|
+
If = Yf * V
|
321
|
+
It = Yt * V
|
322
|
+
h = np.r_[If * np.conj(If) - flow_max, # branch I limits (from bus)
|
323
|
+
It * np.conj(It) - flow_max].real # branch I limits (to bus)
|
324
|
+
else:
|
325
|
+
# compute branch power flows
|
326
|
+
# complex power injected at "from" bus (p.u.)
|
327
|
+
Sf = V[branch[il, IDX.branch.F_BUS].astype(int)] * np.conj(Yf * V)
|
328
|
+
# complex power injected at "to" bus (p.u.)
|
329
|
+
St = V[branch[il, IDX.branch.F_BUS].astype(int)] * np.conj(Yt * V)
|
330
|
+
if ppopt['OPF_FLOW_LIM'] == 1: # active power limit, P (Pan Wei)
|
331
|
+
h = np.r_[Sf.real**2 - flow_max, # branch P limits (from bus)
|
332
|
+
St.real**2 - flow_max] # branch P limits (to bus)
|
333
|
+
else: # apparent power limit, |S|
|
334
|
+
h = np.r_[Sf * np.conj(Sf) - flow_max, # branch S limits (from bus)
|
335
|
+
St * np.conj(St) - flow_max].real # branch S limits (to bus)
|
336
|
+
else:
|
337
|
+
h = np.zeros((0, 1))
|
338
|
+
|
339
|
+
# ----- evaluate partials of constraints -----
|
340
|
+
# index ranges
|
341
|
+
iVa = np.arange(vv["i1"]["Va"], vv["iN"]["Va"])
|
342
|
+
iVm = np.arange(vv["i1"]["Vm"], vv["iN"]["Vm"])
|
343
|
+
iPg = np.arange(vv["i1"]["Pg"], vv["iN"]["Pg"])
|
344
|
+
iQg = np.arange(vv["i1"]["Qg"], vv["iN"]["Qg"])
|
345
|
+
iVaVmPgQg = np.r_[iVa, iVm, iPg, iQg].T
|
346
|
+
|
347
|
+
# compute partials of injected bus powers
|
348
|
+
dSbus_dVm, dSbus_dVa = dSbus_dV(Ybus, V) # w.r.t. V
|
349
|
+
# Pbus w.r.t. Pg, Qbus w.r.t. Qg
|
350
|
+
neg_Cg = c_sparse((-np.ones(ng), (gen[:, IDX.gen.GEN_BUS], range(ng))), (nb, ng))
|
351
|
+
|
352
|
+
# construct Jacobian of equality constraints (power flow) and transpose it
|
353
|
+
dg = l_sparse((2 * nb, nxyz))
|
354
|
+
blank = c_sparse((nb, ng))
|
355
|
+
dg[:, iVaVmPgQg] = sp.vstack([
|
356
|
+
# P mismatch w.r.t Va, Vm, Pg, Qg
|
357
|
+
sp.hstack([dSbus_dVa.real, dSbus_dVm.real, neg_Cg, blank]),
|
358
|
+
# Q mismatch w.r.t Va, Vm, Pg, Qg
|
359
|
+
sp.hstack([dSbus_dVa.imag, dSbus_dVm.imag, blank, neg_Cg])
|
360
|
+
], "csr")
|
361
|
+
dg = dg.T
|
362
|
+
|
363
|
+
if nl2 > 0:
|
364
|
+
# compute partials of Flows w.r.t. V
|
365
|
+
if ppopt['OPF_FLOW_LIM'] == 2: # current
|
366
|
+
dFf_dVa, dFf_dVm, dFt_dVa, dFt_dVm, Ff, Ft = \
|
367
|
+
dIbr_dV(branch[il, :], Yf, Yt, V)
|
368
|
+
else: # power
|
369
|
+
dFf_dVa, dFf_dVm, dFt_dVa, dFt_dVm, Ff, Ft = \
|
370
|
+
dSbr_dV(branch[il, :], Yf, Yt, V)
|
371
|
+
if ppopt['OPF_FLOW_LIM'] == 1: # real part of flow (active power)
|
372
|
+
dFf_dVa = dFf_dVa.real
|
373
|
+
dFf_dVm = dFf_dVm.real
|
374
|
+
dFt_dVa = dFt_dVa.real
|
375
|
+
dFt_dVm = dFt_dVm.real
|
376
|
+
Ff = Ff.real
|
377
|
+
Ft = Ft.real
|
378
|
+
|
379
|
+
# squared magnitude of flow (of complex power or current, or real power)
|
380
|
+
df_dVa, df_dVm, dt_dVa, dt_dVm = \
|
381
|
+
dAbr_dV(dFf_dVa, dFf_dVm, dFt_dVa, dFt_dVm, Ff, Ft)
|
382
|
+
|
383
|
+
# construct Jacobian of inequality constraints (branch limits)
|
384
|
+
# and transpose it.
|
385
|
+
dh = l_sparse((2 * nl2, nxyz))
|
386
|
+
dh[:, np.r_[iVa, iVm].T] = sp.vstack([
|
387
|
+
sp.hstack([df_dVa, df_dVm]), # "from" flow limit
|
388
|
+
sp.hstack([dt_dVa, dt_dVm]) # "to" flow limit
|
389
|
+
], "csr")
|
390
|
+
dh = dh.T
|
391
|
+
else:
|
392
|
+
dh = None
|
393
|
+
|
394
|
+
return h, g, dh, dg
|
395
|
+
|
396
|
+
|
397
|
+
def opf_costfcn(x, om, return_hessian=False):
|
398
|
+
"""
|
399
|
+
Evaluates objective function, gradient and Hessian for OPF.
|
400
|
+
|
401
|
+
Objective function evaluation routine for AC optimal power flow,
|
402
|
+
suitable for use with L{pips}. Computes objective function value,
|
403
|
+
gradient and Hessian.
|
404
|
+
|
405
|
+
@param x: optimization vector
|
406
|
+
@param om: OPF model object
|
407
|
+
|
408
|
+
@return: C{F} - value of objective function. C{df} - (optional) gradient
|
409
|
+
of objective function (column vector). C{d2f} - (optional) Hessian of
|
410
|
+
objective function (sparse matrix).
|
411
|
+
|
412
|
+
@see: L{opf_consfcn}, L{opf_hessfcn}
|
413
|
+
|
414
|
+
@author: Carlos E. Murillo-Sanchez (PSERC Cornell & Universidad
|
415
|
+
Autonoma de Manizales)
|
416
|
+
@author: Ray Zimmerman (PSERC Cornell)
|
417
|
+
"""
|
418
|
+
# ----- initialize -----
|
419
|
+
# unpack data
|
420
|
+
ppc = om.get_ppc()
|
421
|
+
baseMVA, gen, gencost = ppc["baseMVA"], ppc["gen"], ppc["gencost"]
|
422
|
+
cp = om.get_cost_params()
|
423
|
+
N, Cw, H, dd, rh, kk, mm = \
|
424
|
+
cp["N"], cp["Cw"], cp["H"], cp["dd"], cp["rh"], cp["kk"], cp["mm"]
|
425
|
+
vv, _, _, _ = om.get_idx()
|
426
|
+
|
427
|
+
# problem dimensions
|
428
|
+
ng = gen.shape[0] # number of dispatchable injections
|
429
|
+
ny = om.getN('var', 'y') # number of piece-wise linear costs
|
430
|
+
nxyz = len(x) # total number of control vars of all types
|
431
|
+
|
432
|
+
# grab Pg & Qg
|
433
|
+
Pg = x[vv["i1"]["Pg"]:vv["iN"]["Pg"]] # active generation in p.u.
|
434
|
+
Qg = x[vv["i1"]["Qg"]:vv["iN"]["Qg"]] # reactive generation in p.u.
|
435
|
+
|
436
|
+
# ----- evaluate objective function -----
|
437
|
+
# polynomial cost of P and Q
|
438
|
+
# use totcost only on polynomial cost in the minimization problem
|
439
|
+
# formulation, pwl cost is the sum of the y variables.
|
440
|
+
ipol = find(gencost[:, IDX.cost.MODEL] == IDX.cost.POLYNOMIAL) # poly MW and MVAr costs
|
441
|
+
xx = np.r_[Pg, Qg] * baseMVA
|
442
|
+
if any(ipol):
|
443
|
+
f = sum(totcost(gencost[ipol, :], xx[ipol])) # cost of poly P or Q
|
444
|
+
else:
|
445
|
+
f = 0
|
446
|
+
|
447
|
+
# piecewise linear cost of P and Q
|
448
|
+
if ny > 0:
|
449
|
+
ccost = c_sparse((np.ones(ny),
|
450
|
+
(np.zeros(ny), np.arange(vv["i1"]["y"], vv["iN"]["y"]))),
|
451
|
+
(1, nxyz)).toarray().flatten()
|
452
|
+
f = f + np.dot(ccost, x)
|
453
|
+
else:
|
454
|
+
ccost = np.zeros(nxyz)
|
455
|
+
|
456
|
+
# generalized cost term
|
457
|
+
if sp.issparse(N) and N.nnz > 0:
|
458
|
+
nw = N.shape[0]
|
459
|
+
r = N * x - rh # Nx - rhat
|
460
|
+
iLT = find(r < -kk) # below dead zone
|
461
|
+
iEQ = find((r == 0) & (kk == 0)) # dead zone doesn't exist
|
462
|
+
iGT = find(r > kk) # above dead zone
|
463
|
+
iND = np.r_[iLT, iEQ, iGT] # rows that are Not in the Dead region
|
464
|
+
iL = find(dd == 1) # rows using linear function
|
465
|
+
iQ = find(dd == 2) # rows using quadratic function
|
466
|
+
LL = c_sparse((np.ones(len(iL)), (iL, iL)), (nw, nw))
|
467
|
+
QQ = c_sparse((np.ones(len(iQ)), (iQ, iQ)), (nw, nw))
|
468
|
+
kbar = c_sparse((np.r_[np.ones(len(iLT)), np.zeros(len(iEQ)), -np.ones(len(iGT))],
|
469
|
+
(iND, iND)), (nw, nw)) * kk
|
470
|
+
rr = r + kbar # apply non-dead zone shift
|
471
|
+
M = c_sparse((mm[iND], (iND, iND)), (nw, nw)) # dead zone or scale
|
472
|
+
diagrr = c_sparse((rr, (np.arange(nw), np.arange(nw))), (nw, nw))
|
473
|
+
|
474
|
+
# linear rows multiplied by rr(i), quadratic rows by rr(i)^2
|
475
|
+
w = M * (LL + QQ * diagrr) * rr
|
476
|
+
|
477
|
+
f = f + np.dot(w * H, w) / 2 + np.dot(Cw, w)
|
478
|
+
|
479
|
+
# ----- evaluate cost gradient -----
|
480
|
+
# index ranges
|
481
|
+
iPg = range(vv["i1"]["Pg"], vv["iN"]["Pg"])
|
482
|
+
iQg = range(vv["i1"]["Qg"], vv["iN"]["Qg"])
|
483
|
+
|
484
|
+
# polynomial cost of P and Q
|
485
|
+
df_dPgQg = np.zeros(2 * ng) # w.r.t p.u. Pg and Qg
|
486
|
+
df_dPgQg[ipol] = baseMVA * polycost(gencost[ipol, :], xx[ipol], 1)
|
487
|
+
df = np.zeros(nxyz)
|
488
|
+
df[iPg] = df_dPgQg[:ng]
|
489
|
+
df[iQg] = df_dPgQg[ng:ng + ng]
|
490
|
+
|
491
|
+
# piecewise linear cost of P and Q
|
492
|
+
df = df + ccost # The linear cost row is additive wrt any nonlinear cost.
|
493
|
+
|
494
|
+
# generalized cost term
|
495
|
+
if sp.issparse(N) and N.nnz > 0:
|
496
|
+
HwC = H * w + Cw
|
497
|
+
AA = N.T * M * (LL + 2 * QQ * diagrr)
|
498
|
+
df = df + AA * HwC
|
499
|
+
|
500
|
+
# numerical check
|
501
|
+
if 0: # 1 to check, 0 to skip check
|
502
|
+
ddff = np.zeros(df.shape)
|
503
|
+
step = 1e-7
|
504
|
+
tol = 1e-3
|
505
|
+
for k in range(len(x)):
|
506
|
+
xx = x
|
507
|
+
xx[k] = xx[k] + step
|
508
|
+
ddff[k] = (opf_costfcn(xx, om) - f) / step
|
509
|
+
if max(abs(ddff - df)) > tol:
|
510
|
+
idx = find(abs(ddff - df) == max(abs(ddff - df)))
|
511
|
+
print('Mismatch in gradient')
|
512
|
+
print('idx df(num) df diff')
|
513
|
+
print('%4d%16g%16g%16g' %
|
514
|
+
(range(len(df)), ddff.T, df.T, abs(ddff - df).T))
|
515
|
+
print('MAX')
|
516
|
+
print('%4d%16g%16g%16g' %
|
517
|
+
(idx.T, ddff[idx].T, df[idx].T,
|
518
|
+
abs(ddff[idx] - df[idx]).T))
|
519
|
+
|
520
|
+
if not return_hessian:
|
521
|
+
return f, df
|
522
|
+
|
523
|
+
# ---- evaluate cost Hessian -----
|
524
|
+
pcost = gencost[range(ng), :]
|
525
|
+
if gencost.shape[0] > ng:
|
526
|
+
qcost = gencost[ng + 1:2 * ng, :]
|
527
|
+
else:
|
528
|
+
qcost = np.array([])
|
529
|
+
|
530
|
+
# polynomial generator costs
|
531
|
+
d2f_dPg2 = np.zeros(ng) # w.r.t. p.u. Pg
|
532
|
+
d2f_dQg2 = np.zeros(ng) # w.r.t. p.u. Qg
|
533
|
+
ipolp = find(pcost[:, IDX.cost.MODEL] == IDX.cost.POLYNOMIAL)
|
534
|
+
d2f_dPg2[ipolp] = \
|
535
|
+
baseMVA**2 * polycost(pcost[ipolp, :], Pg[ipolp]*baseMVA, 2)
|
536
|
+
if any(qcost): # Qg is not free
|
537
|
+
ipolq = find(qcost[:, IDX.cost.MODEL] == IDX.cost.POLYNOMIAL)
|
538
|
+
d2f_dQg2[ipolq] = \
|
539
|
+
baseMVA**2 * polycost(qcost[ipolq, :], Qg[ipolq] * baseMVA, 2)
|
540
|
+
i = np.r_[iPg, iQg].T
|
541
|
+
d2f = c_sparse((np.r_[d2f_dPg2, d2f_dQg2], (i, i)), (nxyz, nxyz))
|
542
|
+
|
543
|
+
# generalized cost
|
544
|
+
if N is not None and sp.issparse(N):
|
545
|
+
d2f = d2f + AA * H * AA.T + 2 * N.T * M * QQ * \
|
546
|
+
c_sparse((HwC, (range(nw), range(nw))), (nw, nw)) * N
|
547
|
+
|
548
|
+
return f, df, d2f
|
549
|
+
|
550
|
+
|
551
|
+
def run_userfcn(userfcn, stage, *args2):
|
552
|
+
"""
|
553
|
+
Runs the userfcn callbacks for a given stage.
|
554
|
+
|
555
|
+
Example::
|
556
|
+
ppc = om.get_mpc()
|
557
|
+
om = run_userfcn(ppc['userfcn'], 'formulation', om)
|
558
|
+
|
559
|
+
@param userfcn: the 'userfcn' field of ppc, populated by L{add_userfcn}
|
560
|
+
@param stage: the name of the callback stage begin executed
|
561
|
+
(additional arguments) some stages require additional arguments.
|
562
|
+
|
563
|
+
@see: L{add_userfcn}, L{remove_userfcn}, L{toggle_reserves},
|
564
|
+
L{toggle_iflims}, L{runopf_w_res}.
|
565
|
+
|
566
|
+
@author: Ray Zimmerman (PSERC Cornell)
|
567
|
+
"""
|
568
|
+
rv = args2[0]
|
569
|
+
if (len(userfcn) > 0) and (stage in userfcn):
|
570
|
+
for k in range(len(userfcn[stage])):
|
571
|
+
if 'args' in userfcn[stage][k]:
|
572
|
+
args = userfcn[stage][k]['args']
|
573
|
+
else:
|
574
|
+
args = []
|
575
|
+
|
576
|
+
if stage in ['ext2int', 'formulation', 'int2ext']:
|
577
|
+
# ppc = userfcn_*_ext2int(ppc, args)
|
578
|
+
# om = userfcn_*_formulation(om, args)
|
579
|
+
# results = userfcn_*_int2ext(results, args)
|
580
|
+
rv = userfcn[stage][k]['fcn'](rv, args)
|
581
|
+
elif stage in ['printpf', 'savecase']:
|
582
|
+
# results = userfcn_*_printpf(results, fd, ppopt, args)
|
583
|
+
# ppc = userfcn_*_savecase(mpc, fd, prefix, args)
|
584
|
+
fdprint = args2[1]
|
585
|
+
ppoptprint = args2[2]
|
586
|
+
rv = userfcn[stage][k]['fcn'](rv, fdprint, ppoptprint, args)
|
587
|
+
|
588
|
+
return rv
|
589
|
+
|
590
|
+
|
591
|
+
def add_userfcn(ppc, stage, fcn, args=None, allow_multiple=False):
|
592
|
+
"""
|
593
|
+
Appends a userfcn to the list to be called for a case.
|
594
|
+
|
595
|
+
A userfcn is a callback function that can be called automatically by
|
596
|
+
PYPOWER at one of various stages in a simulation.
|
597
|
+
|
598
|
+
Currently there are 5 different callback stages defined. Each stage has
|
599
|
+
a name, and by convention, the name of a user-defined callback function
|
600
|
+
ends with the name of the stage. The following is a description of each
|
601
|
+
stage, when it is called and the input and output arguments which vary
|
602
|
+
depending on the stage. The reserves example (see L{runopf_w_res}) is used
|
603
|
+
to illustrate how these callback userfcns might be used.
|
604
|
+
|
605
|
+
1. C{'ext2int'}
|
606
|
+
|
607
|
+
Called from L{ext2int} immediately after the case is converted from
|
608
|
+
external to internal indexing. Inputs are a PYPOWER case dict (C{ppc}),
|
609
|
+
freshly converted to internal indexing and any (optional) C{args} value
|
610
|
+
supplied via L{add_userfcn}. Output is the (presumably updated) C{ppc}.
|
611
|
+
This is typically used to reorder any input arguments that may be needed
|
612
|
+
in internal ordering by the formulation stage.
|
613
|
+
|
614
|
+
E.g. C{ppc = userfcn_reserves_ext2int(ppc, args)}
|
615
|
+
|
616
|
+
2. C{'formulation'}
|
617
|
+
|
618
|
+
Called from L{opf} after the OPF Model (C{om}) object has been
|
619
|
+
initialized with the standard OPF formulation, but before calling the
|
620
|
+
solver. Inputs are the C{om} object and any (optional) C{args} supplied
|
621
|
+
via L{add_userfcn}. Output is the C{om} object. This is the ideal place
|
622
|
+
to add any additional vars, constraints or costs to the OPF formulation.
|
623
|
+
|
624
|
+
E.g. C{om = userfcn_reserves_formulation(om, args)}
|
625
|
+
|
626
|
+
3. C{'int2ext'}
|
627
|
+
|
628
|
+
Called from L{int2ext} immediately before the resulting case is converted
|
629
|
+
from internal back to external indexing. Inputs are the C{results} dict
|
630
|
+
and any (optional) C{args} supplied via C{add_userfcn}. Output is the
|
631
|
+
C{results} dict. This is typically used to convert any results to
|
632
|
+
external indexing and populate any corresponding fields in the
|
633
|
+
C{results} dict.
|
634
|
+
|
635
|
+
E.g. C{results = userfcn_reserves_int2ext(results, args)}
|
636
|
+
|
637
|
+
4. C{'printpf'}
|
638
|
+
|
639
|
+
Called from L{printpf} after the pretty-printing of the standard OPF
|
640
|
+
output. Inputs are the C{results} dict, the file descriptor to write to,
|
641
|
+
a PYPOWER options dict, and any (optional) C{args} supplied via
|
642
|
+
L{add_userfcn}. Output is the C{results} dict. This is typically used for
|
643
|
+
any additional pretty-printing of results.
|
644
|
+
|
645
|
+
E.g. C{results = userfcn_reserves_printpf(results, fd, ppopt, args)}
|
646
|
+
|
647
|
+
5. C{'savecase'}
|
648
|
+
|
649
|
+
Called from L{savecase} when saving a case dict to a Python file after
|
650
|
+
printing all of the other data to the file. Inputs are the case dict,
|
651
|
+
the file descriptor to write to, the variable prefix (typically 'ppc')
|
652
|
+
and any (optional) C{args} supplied via L{add_userfcn}. Output is the
|
653
|
+
case dict. This is typically used to write any non-standard case dict
|
654
|
+
fields to the case file.
|
655
|
+
|
656
|
+
E.g. C{ppc = userfcn_reserves_printpf(ppc, fd, prefix, args)}
|
657
|
+
|
658
|
+
@param ppc: the case dict
|
659
|
+
@param stage: the name of the stage at which this function should be
|
660
|
+
called: ext2int, formulation, int2ext, printpf
|
661
|
+
@param fcn: the name of the userfcn
|
662
|
+
@param args: (optional) the value to be passed as an argument to the
|
663
|
+
userfcn
|
664
|
+
@param allow_multiple: (optional) if True, allows the same function to
|
665
|
+
be added more than once.
|
666
|
+
|
667
|
+
@see: L{run_userfcn}, L{remove_userfcn}, L{toggle_reserves},
|
668
|
+
L{toggle_iflims}, L{runopf_w_res}.
|
669
|
+
|
670
|
+
@author: Ray Zimmerman (PSERC Cornell)
|
671
|
+
"""
|
672
|
+
if args is None:
|
673
|
+
args = []
|
674
|
+
|
675
|
+
if stage not in ['ext2int', 'formulation', 'int2ext', 'printpf', 'savecase']:
|
676
|
+
logger.debug('add_userfcn : \'%s\' is not the name of a valid callback stage\n' % stage)
|
677
|
+
|
678
|
+
n = 0
|
679
|
+
if 'userfcn' in ppc:
|
680
|
+
if stage in ppc['userfcn']:
|
681
|
+
n = len(ppc['userfcn'][stage]) # + 1
|
682
|
+
if not allow_multiple:
|
683
|
+
for k in range(n):
|
684
|
+
if ppc['userfcn'][stage][k]['fcn'] == fcn:
|
685
|
+
logger.debug('add_userfcn: the function \'%s\' has already been added\n' % fcn.__name__)
|
686
|
+
else:
|
687
|
+
ppc['userfcn'][stage] = []
|
688
|
+
else:
|
689
|
+
ppc['userfcn'] = {stage: []}
|
690
|
+
|
691
|
+
ppc['userfcn'][stage].append({'fcn': fcn})
|
692
|
+
if len(args) > 0:
|
693
|
+
ppc['userfcn'][stage][n]['args'] = args
|
694
|
+
|
695
|
+
return ppc
|
696
|
+
|
697
|
+
|
698
|
+
def remove_userfcn(ppc, stage, fcn):
|
699
|
+
"""
|
700
|
+
Removes a userfcn from the list to be called for a case.
|
701
|
+
|
702
|
+
A userfcn is a callback function that can be called automatically by
|
703
|
+
PYPOWER at one of various stages in a simulation. This function removes
|
704
|
+
the last instance of the userfcn for the given C{stage} with the function
|
705
|
+
handle specified by C{fcn}.
|
706
|
+
|
707
|
+
@see: L{add_userfcn}, L{run_userfcn}, L{toggle_reserves},
|
708
|
+
L{toggle_iflims}, L{runopf_w_res}
|
709
|
+
|
710
|
+
@author: Ray Zimmerman (PSERC Cornell)
|
711
|
+
"""
|
712
|
+
n = len(ppc['userfcn'][stage])
|
713
|
+
|
714
|
+
for k in range(n - 1, -1, -1):
|
715
|
+
if ppc['userfcn'][stage][k]['fcn'] == fcn:
|
716
|
+
del ppc['userfcn'][stage][k]
|
717
|
+
break
|
718
|
+
|
719
|
+
return ppc
|
720
|
+
|
721
|
+
|
722
|
+
def totcost(gencost, Pg):
|
723
|
+
"""
|
724
|
+
Computes total cost for generators at given output level.
|
725
|
+
|
726
|
+
Computes total cost for generators given a matrix in gencost format and
|
727
|
+
a column vector or matrix of generation levels. The return value has the
|
728
|
+
same dimensions as PG. Each row of C{gencost} is used to evaluate the
|
729
|
+
cost at the points specified in the corresponding row of C{Pg}.
|
730
|
+
|
731
|
+
@author: Ray Zimmerman (PSERC Cornell)
|
732
|
+
@author: Carlos E. Murillo-Sanchez (PSERC Cornell & Universidad
|
733
|
+
Autonoma de Manizales)
|
734
|
+
"""
|
735
|
+
ng, m = gencost.shape
|
736
|
+
totalcost = np.zeros(ng)
|
737
|
+
|
738
|
+
if len(gencost) > 0:
|
739
|
+
ipwl = find(gencost[:, IDX.cost.MODEL] == IDX.cost.PW_LINEAR)
|
740
|
+
ipol = find(gencost[:, IDX.cost.MODEL] == IDX.cost.POLYNOMIAL)
|
741
|
+
if len(ipwl) > 0:
|
742
|
+
p = gencost[:, IDX.cost.COST:(m-1):2]
|
743
|
+
c = gencost[:, (IDX.cost.COST+1):m:2]
|
744
|
+
|
745
|
+
for i in ipwl:
|
746
|
+
ncost = gencost[i, IDX.cost.NCOST]
|
747
|
+
for k in np.arange(ncost - 1, dtype=int):
|
748
|
+
p1, p2 = p[i, k], p[i, k+1]
|
749
|
+
c1, c2 = c[i, k], c[i, k+1]
|
750
|
+
m = (c2 - c1) / (p2 - p1)
|
751
|
+
b = c1 - m * p1
|
752
|
+
Pgen = Pg[i]
|
753
|
+
if Pgen < p2:
|
754
|
+
totalcost[i] = m * Pgen + b
|
755
|
+
break
|
756
|
+
totalcost[i] = m * Pgen + b
|
757
|
+
|
758
|
+
if len(ipol) > 0:
|
759
|
+
totalcost[ipol] = polycost(gencost[ipol, :], Pg[ipol])
|
760
|
+
|
761
|
+
return totalcost
|
762
|
+
|
763
|
+
|
764
|
+
def modcost(gencost, alpha, modtype='SCALE_F'):
|
765
|
+
"""Modifies generator costs by shifting or scaling (F or X).
|
766
|
+
|
767
|
+
For each generator cost F(X) (for real or reactive power) in
|
768
|
+
C{gencost}, this function modifies the cost by scaling or shifting
|
769
|
+
the function by C{alpha}, depending on the value of C{modtype}, and
|
770
|
+
and returns the modified C{gencost}. Rows of C{gencost} can be a mix
|
771
|
+
of polynomial or piecewise linear costs.
|
772
|
+
|
773
|
+
C{modtype} takes one of the 4 possible values (let F_alpha(X) denote the
|
774
|
+
the modified function)::
|
775
|
+
SCALE_F (default) : F_alpha(X) == F(X) * ALPHA
|
776
|
+
SCALE_X : F_alpha(X * ALPHA) == F(X)
|
777
|
+
SHIFT_F : F_alpha(X) == F(X) + ALPHA
|
778
|
+
SHIFT_X : F_alpha(X + ALPHA) == F(X)
|
779
|
+
|
780
|
+
@author: Ray Zimmerman (PSERC Cornell)
|
781
|
+
"""
|
782
|
+
gencost = gencost.copy()
|
783
|
+
|
784
|
+
ng, m = gencost.shape
|
785
|
+
if ng != 0:
|
786
|
+
ipwl = find(gencost[:, IDX.cost.MODEL] == IDX.cost.PW_LINEAR)
|
787
|
+
ipol = find(gencost[:, IDX.cost.MODEL] == IDX.cost.POLYNOMIAL)
|
788
|
+
c = gencost[ipol, IDX.cost.COST:m]
|
789
|
+
|
790
|
+
if modtype == 'SCALE_F':
|
791
|
+
gencost[ipol, IDX.cost.COST:m] = alpha * c
|
792
|
+
gencost[ipwl, IDX.cost.COST+1:m:2] = alpha * gencost[ipwl, IDX.cost.COST + 1:m:2]
|
793
|
+
elif modtype == 'SCALE_X':
|
794
|
+
for k in range(len(ipol)):
|
795
|
+
n = gencost[ipol[k], IDX.cost.NCOST].astype(int)
|
796
|
+
for i in range(n):
|
797
|
+
gencost[ipol[k], IDX.cost.COST + i] = c[k, i] / alpha**(n - i - 1)
|
798
|
+
gencost[ipwl, IDX.cost.COST:m - 1:2] = alpha * gencost[ipwl, IDX.cost.COST:m - 1:2]
|
799
|
+
elif modtype == 'SHIFT_F':
|
800
|
+
for k in range(len(ipol)):
|
801
|
+
n = gencost[ipol[k], IDX.cost.NCOST].astype(int)
|
802
|
+
gencost[ipol[k], IDX.cost.COST + n - 1] = alpha + c[k, n - 1]
|
803
|
+
gencost[ipwl, IDX.cost.COST+1:m:2] = alpha + gencost[ipwl, IDX.cost.COST + 1:m:2]
|
804
|
+
elif modtype == 'SHIFT_X':
|
805
|
+
for k in range(len(ipol)):
|
806
|
+
n = gencost[ipol[k], IDX.cost.NCOST].astype(int)
|
807
|
+
gencost[ipol[k], IDX.cost.COST:IDX.cost.COST + n] = \
|
808
|
+
polyshift(c[k, :n].T, alpha).T
|
809
|
+
gencost[ipwl, IDX.cost.COST:m - 1:2] = alpha + gencost[ipwl, IDX.cost.COST:m - 1:2]
|
810
|
+
else:
|
811
|
+
logger.debug('modcost: "%s" is not a valid modtype\n' % modtype)
|
812
|
+
|
813
|
+
return gencost
|
814
|
+
|
815
|
+
|
816
|
+
def polyshift(c, a):
|
817
|
+
"""
|
818
|
+
Returns the coefficients of a horizontally shifted polynomial.
|
819
|
+
|
820
|
+
C{d = polyshift(c, a)} shifts to the right by C{a}, the polynomial whose
|
821
|
+
coefficients are given in the column vector C{c}.
|
822
|
+
|
823
|
+
Example: For any polynomial with C{n} coefficients in C{c}, and any values
|
824
|
+
for C{x} and shift C{a}, the C{f - f0} should be zero::
|
825
|
+
x = rand
|
826
|
+
a = rand
|
827
|
+
c = rand(n, 1);
|
828
|
+
f0 = polyval(c, x)
|
829
|
+
f = polyval(polyshift(c, a), x+a)
|
830
|
+
"""
|
831
|
+
n = len(c)
|
832
|
+
d = np.zeros(c.shape)
|
833
|
+
A = pow(-a * np.ones(n), np.arange(n))
|
834
|
+
b = np.ones(n)
|
835
|
+
for k in range(n):
|
836
|
+
d[n - k - 1] = np.dot(b, c[n - k - 1::-1] * A[:n - k])
|
837
|
+
b = np.cumsum(b[:n - k - 1])
|
838
|
+
|
839
|
+
return d
|
840
|
+
|
841
|
+
|
842
|
+
def polycost(gencost, Pg, der=0):
|
843
|
+
"""
|
844
|
+
Evaluates polynomial generator cost & derivatives.
|
845
|
+
|
846
|
+
C{f = polycost(gencost, Pg)} returns the vector of costs evaluated at C{Pg}
|
847
|
+
|
848
|
+
C{df = polycost(gencost, Pg, 1)} returns the vector of first derivatives
|
849
|
+
of costs evaluated at C{Pg}
|
850
|
+
|
851
|
+
C{d2f = polycost(gencost, Pg, 2)} returns the vector of second derivatives
|
852
|
+
of costs evaluated at C{Pg}
|
853
|
+
|
854
|
+
C{gencost} must contain only polynomial costs
|
855
|
+
C{Pg} is in MW, not p.u. (works for C{Qg} too)
|
856
|
+
|
857
|
+
@author: Ray Zimmerman (PSERC Cornell)
|
858
|
+
"""
|
859
|
+
if gencost.size == 0:
|
860
|
+
# User has a purely linear piecewise problem, exit early with empty array
|
861
|
+
return []
|
862
|
+
|
863
|
+
if any(gencost[:, IDX.cost.MODEL] == IDX.cost.PW_LINEAR):
|
864
|
+
logger.debug('polycost: all costs must be polynomial\n')
|
865
|
+
|
866
|
+
ng = len(Pg)
|
867
|
+
maxN = max(gencost[:, IDX.cost.NCOST].astype(int))
|
868
|
+
minN = min(gencost[:, IDX.cost.NCOST].astype(int))
|
869
|
+
|
870
|
+
# form coefficient matrix where 1st column is constant term, 2nd linear, etc.
|
871
|
+
c = np.zeros((ng, maxN))
|
872
|
+
for n in np.arange(minN, maxN + 1):
|
873
|
+
k = find(gencost[:, IDX.cost.NCOST] == n) # cost with n coefficients
|
874
|
+
c[k, :n] = gencost[k, (IDX.cost.COST + n - 1):IDX.cost.COST - 1:-1]
|
875
|
+
|
876
|
+
# do derivatives
|
877
|
+
for d in range(1, der + 1):
|
878
|
+
if c.shape[1] >= 2:
|
879
|
+
c = c[:, 1:maxN - d + 1]
|
880
|
+
else:
|
881
|
+
c = np.zeros((ng, 1))
|
882
|
+
break
|
883
|
+
|
884
|
+
for k in range(2, maxN - d + 1):
|
885
|
+
c[:, k-1] = c[:, k-1] * k
|
886
|
+
|
887
|
+
# evaluate polynomial
|
888
|
+
if len(c) == 0:
|
889
|
+
f = np.zeros(Pg.shape)
|
890
|
+
else:
|
891
|
+
f = c[:, :1].flatten() # constant term
|
892
|
+
for k in range(1, c.shape[1]):
|
893
|
+
f = f + c[:, k] * Pg**k
|
894
|
+
|
895
|
+
return f
|
896
|
+
|
897
|
+
|
898
|
+
def pqcost(gencost, ng, on=None):
|
899
|
+
"""
|
900
|
+
Splits the gencost variable into two pieces if costs are given for Qg.
|
901
|
+
|
902
|
+
Checks whether C{gencost} has cost information for reactive power
|
903
|
+
generation (rows C{ng+1} to C{2*ng}). If so, it returns the first C{ng}
|
904
|
+
rows in C{pcost} and the last C{ng} rows in C{qcost}. Otherwise, leaves
|
905
|
+
C{qcost} empty. Also does some error checking.
|
906
|
+
If C{on} is specified (list of indices of generators which are on line)
|
907
|
+
it only returns the rows corresponding to these generators.
|
908
|
+
|
909
|
+
@author: Ray Zimmerman (PSERC Cornell)
|
910
|
+
"""
|
911
|
+
if on is None:
|
912
|
+
on = np.arange(ng)
|
913
|
+
|
914
|
+
if gencost.shape[0] == ng:
|
915
|
+
pcost = gencost[on, :]
|
916
|
+
qcost = np.array([])
|
917
|
+
elif gencost.shape[0] == 2 * ng:
|
918
|
+
pcost = gencost[on, :]
|
919
|
+
qcost = gencost[on + ng, :]
|
920
|
+
else:
|
921
|
+
logger.info('pqcost: gencost has wrong number of rows')
|
922
|
+
|
923
|
+
return pcost, qcost
|
924
|
+
|
925
|
+
|
926
|
+
def poly2pwl(polycost, Pmin, Pmax, npts):
|
927
|
+
"""
|
928
|
+
Converts polynomial cost variable to piecewise linear.
|
929
|
+
|
930
|
+
Converts the polynomial cost variable C{polycost} into a piece-wise linear
|
931
|
+
cost by evaluating at zero and then at C{npts} evenly spaced points between
|
932
|
+
C{Pmin} and C{Pmax}. If C{Pmin <= 0} (such as for reactive power, where
|
933
|
+
C{P} really means C{Q}) it just uses C{npts} evenly spaced points between
|
934
|
+
C{Pmin} and C{Pmax}.
|
935
|
+
"""
|
936
|
+
pwlcost = polycost
|
937
|
+
# size of piece being changed
|
938
|
+
m, n = polycost.shape
|
939
|
+
# change cost model
|
940
|
+
pwlcost[:, IDX.cost.MODEL] = IDX.cost.PW_LINEAR * np.ones(m)
|
941
|
+
# zero out old data
|
942
|
+
pwlcost[:, IDX.cost.COST:IDX.cost.COST + n] = np.zeros(pwlcost[:, IDX.cost.COST:IDX.cost.COST + n].shape)
|
943
|
+
# change number of data points
|
944
|
+
pwlcost[:, IDX.cost.NCOST] = npts * IDX.cost.ones(m)
|
945
|
+
|
946
|
+
for i in range(m):
|
947
|
+
if Pmin[i] == 0:
|
948
|
+
step = (Pmax[i] - Pmin[i]) / (npts - 1)
|
949
|
+
xx = range(Pmin[i], step, Pmax[i])
|
950
|
+
elif Pmin[i] > 0:
|
951
|
+
step = (Pmax[i] - Pmin[i]) / (npts - 2)
|
952
|
+
xx = r_[0, range(Pmin[i], step, Pmax[i])]
|
953
|
+
elif Pmin[i] < 0 & Pmax[i] > 0: # for when P really means Q
|
954
|
+
step = (Pmax[i] - Pmin[i]) / (npts - 1)
|
955
|
+
xx = range(Pmin[i], step, Pmax[i])
|
956
|
+
yy = totcost(polycost[i, :], xx)
|
957
|
+
pwlcost[i, IDX.cost.COST:2:(IDX.cost.COST + 2*(npts-1))] = xx
|
958
|
+
pwlcost[i, (IDX.cost.COST+1):2:(IDX.cost.COST + 2*(npts-1) + 1)] = yy
|
959
|
+
|
960
|
+
return pwlcost
|
961
|
+
|
962
|
+
|
963
|
+
def scale_load(load, bus, gen=None, load_zone=None, opt=None):
|
964
|
+
"""
|
965
|
+
Scales fixed and/or dispatchable loads.
|
966
|
+
|
967
|
+
Assumes consecutive bus numbering when dealing with dispatchable loads.
|
968
|
+
|
969
|
+
@param load: Each element specifies the amount of scaling for the
|
970
|
+
corresponding load zone, either as a direct scale factor
|
971
|
+
or as a target quantity. If there are C{nz} load zones this
|
972
|
+
vector has C{nz} elements.
|
973
|
+
@param bus: Standard C{bus} matrix with C{nb} rows, where the fixed active
|
974
|
+
and reactive loads available for scaling are specified in
|
975
|
+
columns C{PD} and C{QD}
|
976
|
+
@param gen: (optional) standard C{gen} matrix with C{ng} rows, where the
|
977
|
+
dispatchable loads available for scaling are specified by
|
978
|
+
columns C{PG}, C{QG}, C{PMIN}, C{QMIN} and C{QMAX} (in rows for which
|
979
|
+
C{isload(gen)} returns C{true}). If C{gen} is empty, it assumes
|
980
|
+
there are no dispatchable loads.
|
981
|
+
@param load_zone: (optional) C{nb} element vector where the value of
|
982
|
+
each element is either zero or the index of the load zone
|
983
|
+
to which the corresponding bus belongs. If C{load_zone[b] = k}
|
984
|
+
then the loads at bus C{b} will be scaled according to the
|
985
|
+
value of C{load[k]}. If C{load_zone[b] = 0}, the loads at bus C{b}
|
986
|
+
will not be modified. If C{load_zone} is empty, the default is
|
987
|
+
determined by the dimensions of the C{load} vector. If C{load} is
|
988
|
+
a scalar, a single system-wide zone including all buses is
|
989
|
+
used, i.e. C{load_zone = ones(nb)}. If C{load} is a vector, the
|
990
|
+
default C{load_zone} is defined as the areas specified in the
|
991
|
+
C{bus} matrix, i.e. C{load_zone = bus[:, BUS_AREA]}, and C{load}
|
992
|
+
should have dimension C{= max(bus[:, BUS_AREA])}.
|
993
|
+
@param opt: (optional) dict with three possible fields, 'scale',
|
994
|
+
'pq' and 'which' that determine the behavior as follows:
|
995
|
+
- C{scale} (default is 'FACTOR')
|
996
|
+
- 'FACTOR' : C{load} consists of direct scale factors, where
|
997
|
+
C{load[k] =} scale factor C{R[k]} for zone C{k}
|
998
|
+
- 'QUANTITY' : C{load} consists of target quantities, where
|
999
|
+
C{load[k] =} desired total active load in MW for
|
1000
|
+
zone C{k} after scaling by an appropriate C{R(k)}
|
1001
|
+
- C{pq} (default is 'PQ')
|
1002
|
+
- 'PQ' : scale both active and reactive loads
|
1003
|
+
- 'P' : scale only active loads
|
1004
|
+
- C{which} (default is 'BOTH' if GEN is provided, else 'FIXED')
|
1005
|
+
- 'FIXED' : scale only fixed loads
|
1006
|
+
- 'DISPATCHABLE' : scale only dispatchable loads
|
1007
|
+
- 'BOTH' : scale both fixed and dispatchable loads
|
1008
|
+
|
1009
|
+
@see: L{total_load}
|
1010
|
+
|
1011
|
+
@author: Ray Zimmerman (PSERC Cornell)
|
1012
|
+
"""
|
1013
|
+
nb = bus.shape[0] # number of buses
|
1014
|
+
|
1015
|
+
# ----- process inputs -----
|
1016
|
+
bus = bus.copy()
|
1017
|
+
if gen is None:
|
1018
|
+
gen = np.array([])
|
1019
|
+
else:
|
1020
|
+
gen = gen.copy()
|
1021
|
+
if load_zone is None:
|
1022
|
+
load_zone = np.array([], int)
|
1023
|
+
if opt is None:
|
1024
|
+
opt = {}
|
1025
|
+
|
1026
|
+
# fill out and check opt
|
1027
|
+
if len(gen) == 0:
|
1028
|
+
opt["which"] = 'FIXED'
|
1029
|
+
if 'pq' not in opt:
|
1030
|
+
opt["pq"] = 'PQ' # 'PQ' or 'P'
|
1031
|
+
if 'which' not in opt:
|
1032
|
+
opt["which"] = 'BOTH' # 'FIXED', 'DISPATCHABLE' or 'BOTH'
|
1033
|
+
if 'scale' not in opt:
|
1034
|
+
opt["scale"] = 'FACTOR' # 'FACTOR' or 'QUANTITY'
|
1035
|
+
if (opt["pq"] != 'P') and (opt["pq"] != 'PQ'):
|
1036
|
+
logger.debug("scale_load: opt['pq'] must equal 'PQ' or 'P'\n")
|
1037
|
+
if (opt["which"][0] != 'F') and (opt["which"][0] != 'D') and (opt["which"][0] != 'B'):
|
1038
|
+
logger.debug("scale_load: opt.which should be 'FIXED, 'DISPATCHABLE or 'BOTH'\n")
|
1039
|
+
if (opt["scale"][0] != 'F') and (opt["scale"][0] != 'Q'):
|
1040
|
+
logger.debug("scale_load: opt.scale should be 'FACTOR or 'QUANTITY'\n")
|
1041
|
+
if (len(gen) == 0) and (opt["which"][0] != 'F'):
|
1042
|
+
logger.debug('scale_load: need gen matrix to scale dispatchable loads\n')
|
1043
|
+
|
1044
|
+
# create dispatchable load connection matrix
|
1045
|
+
if len(gen) > 0:
|
1046
|
+
ng = gen.shape[0]
|
1047
|
+
is_ld = isload(gen) & (gen[:, IDX.gen.GEN_STATUS] > 0)
|
1048
|
+
ld = find(is_ld)
|
1049
|
+
|
1050
|
+
# create map of external bus numbers to bus indices
|
1051
|
+
i2e = bus[:, IDX.bus.BUS_I].astype(int)
|
1052
|
+
e2i = np.zeros(max(i2e) + 1, int)
|
1053
|
+
e2i[i2e] = np.arange(nb)
|
1054
|
+
|
1055
|
+
gbus = gen[:, IDX.gen.GEN_BUS].astype(int)
|
1056
|
+
Cld = c_sparse((is_ld, (e2i[gbus], np.arange(ng))), (nb, ng))
|
1057
|
+
else:
|
1058
|
+
ng = 0
|
1059
|
+
ld = np.array([], int)
|
1060
|
+
|
1061
|
+
if len(load_zone) == 0:
|
1062
|
+
if len(load) == 1: # make a single zone of all load buses
|
1063
|
+
load_zone = np.zeros(nb, int) # initialize
|
1064
|
+
load_zone[bus[:, IDX.bus.PD] != 0 or bus[:, IDX.bus.QD] != 0] = 1 # FIXED loads
|
1065
|
+
if len(gen) > 0:
|
1066
|
+
gbus = gen[ld, IDX.gen.GEN_BUS].astype(int)
|
1067
|
+
load_zone[e2i[gbus]] = 1 # DISPATCHABLE loads
|
1068
|
+
else: # use areas defined in bus data as zones
|
1069
|
+
load_zone = bus[:, IDX.bus.BUS_AREA]
|
1070
|
+
|
1071
|
+
# check load_zone to make sure it's consistent with size of load vector
|
1072
|
+
if max(load_zone) > len(load):
|
1073
|
+
logger.debug('scale_load: load vector must have a value for each load zone specified\n')
|
1074
|
+
|
1075
|
+
# ----- compute scale factors for each zone -----
|
1076
|
+
scale = load.copy()
|
1077
|
+
Pdd = np.zeros(nb) # dispatchable P at each bus
|
1078
|
+
if opt["scale"][0] == 'Q': # 'QUANTITY'
|
1079
|
+
# find load capacity from dispatchable loads
|
1080
|
+
if len(gen) > 0:
|
1081
|
+
Pdd = -Cld * gen[:, IDX.gen.PMIN]
|
1082
|
+
|
1083
|
+
# compute scale factors
|
1084
|
+
for k in range(len(load)):
|
1085
|
+
idx = find(load_zone == k + 1)
|
1086
|
+
fixed = sum(bus[idx, IDX.bus.PD])
|
1087
|
+
dispatchable = sum(Pdd[idx])
|
1088
|
+
total = fixed + dispatchable
|
1089
|
+
if opt["which"][0] == 'B': # 'BOTH'
|
1090
|
+
if total != 0:
|
1091
|
+
scale[k] = load[k] / total
|
1092
|
+
elif load[k] == total:
|
1093
|
+
scale[k] = 1
|
1094
|
+
else:
|
1095
|
+
raise ValueError(
|
1096
|
+
'scale_load: impossible to make zone %d load equal %g by scaling non-existent loads' %
|
1097
|
+
(k, load[k]))
|
1098
|
+
elif opt["which"][0] == 'F': # 'FIXED'
|
1099
|
+
if fixed != 0:
|
1100
|
+
scale[k] = (load[k] - dispatchable) / fixed
|
1101
|
+
elif load[k] == dispatchable:
|
1102
|
+
scale[k] = 1
|
1103
|
+
else:
|
1104
|
+
raise ValueError(
|
1105
|
+
'scale_load: impossible to make zone %d load equal %g by scaling non-existent fixed load' %
|
1106
|
+
(k, load[k]))
|
1107
|
+
elif opt["which"][0] == 'D': # 'DISPATCHABLE'
|
1108
|
+
if dispatchable != 0:
|
1109
|
+
scale[k] = (load[k] - fixed) / dispatchable
|
1110
|
+
elif load[k] == fixed:
|
1111
|
+
scale[k] = 1
|
1112
|
+
else:
|
1113
|
+
raise ValueError(
|
1114
|
+
'scale_load: impossible to make zone %d load equal %g by scaling non-existent dispatchable load' % (k, load[k]))
|
1115
|
+
|
1116
|
+
# ----- do the scaling -----
|
1117
|
+
# fixed loads
|
1118
|
+
if opt["which"][0] != 'D': # includes 'FIXED', not 'DISPATCHABLE' only
|
1119
|
+
for k in range(len(scale)):
|
1120
|
+
idx = find(load_zone == k + 1)
|
1121
|
+
bus[idx, IDX.bus.PD] = bus[idx, IDX.bus.PD] * scale[k]
|
1122
|
+
if opt["pq"] == 'PQ':
|
1123
|
+
bus[idx, IDX.bus.QD] = bus[idx, IDX.bus.QD] * scale[k]
|
1124
|
+
|
1125
|
+
# dispatchable loads
|
1126
|
+
if opt["which"][0] != 'F': # includes 'DISPATCHABLE', not 'FIXED' only
|
1127
|
+
for k in range(len(scale)):
|
1128
|
+
idx = find(load_zone == k + 1)
|
1129
|
+
gbus = gen[ld, IDX.gen.GEN_BUS].astype(int)
|
1130
|
+
i = find(np.in1d(e2i[gbus], idx))
|
1131
|
+
ig = ld[i]
|
1132
|
+
|
1133
|
+
gen[np.ix_(ig, [IDX.gen.PG, IDX.gen.PMIN])] = gen[np.ix_(ig, [IDX.gen.PG, IDX.gen.PMIN])] * scale[k]
|
1134
|
+
if opt["pq"] == 'PQ':
|
1135
|
+
gen[np.ix_(ig, [IDX.gen.QG, IDX.gen.QMIN, IDX.gen.QMAX])] = gen[np.ix_(
|
1136
|
+
ig, [IDX.gen.QG, IDX.gen.QMIN, IDX.gen.QMAX])] * scale[k]
|
1137
|
+
|
1138
|
+
return bus, gen
|
1139
|
+
|
1140
|
+
|
1141
|
+
def update_mupq(baseMVA, gen, mu_PQh, mu_PQl, data):
|
1142
|
+
"""
|
1143
|
+
Updates values of generator limit shadow prices.
|
1144
|
+
|
1145
|
+
Updates the values of C{MU_PMIN}, C{MU_PMAX}, C{MU_QMIN}, C{MU_QMAX} based
|
1146
|
+
on any shadow prices on the sloped portions of the generator
|
1147
|
+
capability curve constraints.
|
1148
|
+
|
1149
|
+
@param mu_PQh: shadow prices on upper sloped portion of capability curves
|
1150
|
+
@param mu_PQl: shadow prices on lower sloped portion of capability curves
|
1151
|
+
@param data: "data" dict returned by L{makeApq}
|
1152
|
+
|
1153
|
+
@see: C{makeApq}.
|
1154
|
+
|
1155
|
+
@author: Ray Zimmerman (PSERC Cornell)
|
1156
|
+
@author: Carlos E. Murillo-Sanchez (PSERC Cornell & Universidad
|
1157
|
+
Autonoma de Manizales)
|
1158
|
+
"""
|
1159
|
+
# extract the constraint parameters
|
1160
|
+
ipqh, ipql, Apqhdata, Apqldata = \
|
1161
|
+
data['ipqh'], data['ipql'], data['h'], data['l']
|
1162
|
+
|
1163
|
+
# combine original limit multipliers into single value
|
1164
|
+
muP = gen[:, IDX.gen.MU_PMAX] - gen[:, IDX.gen.MU_PMIN]
|
1165
|
+
muQ = gen[:, IDX.gen.MU_QMAX] - gen[:, IDX.gen.MU_QMIN]
|
1166
|
+
|
1167
|
+
# add P and Q components of multipliers on upper sloped constraint
|
1168
|
+
muP[ipqh] = muP[ipqh] - mu_PQh * Apqhdata[:, 0] / baseMVA
|
1169
|
+
muQ[ipqh] = muQ[ipqh] - mu_PQh * Apqhdata[:, 1] / baseMVA
|
1170
|
+
|
1171
|
+
# add P and Q components of multipliers on lower sloped constraint
|
1172
|
+
muP[ipql] = muP[ipql] - mu_PQl * Apqldata[:, 0] / baseMVA
|
1173
|
+
muQ[ipql] = muQ[ipql] - mu_PQl * Apqldata[:, 1] / baseMVA
|
1174
|
+
|
1175
|
+
# split back into upper and lower multipliers based on sign
|
1176
|
+
gen[:, IDX.gen.MU_PMAX] = (muP > 0) * muP
|
1177
|
+
gen[:, IDX.gen.MU_PMIN] = (muP < 0) * -muP
|
1178
|
+
gen[:, IDX.gen.MU_QMAX] = (muQ > 0) * muQ
|
1179
|
+
gen[:, IDX.gen.MU_QMIN] = (muQ < 0) * -muQ
|
1180
|
+
|
1181
|
+
return gen
|
1182
|
+
|
1183
|
+
|
1184
|
+
def int2ext(ppc, val_or_field=None, oldval=None, ordering=None, dim=0):
|
1185
|
+
"""
|
1186
|
+
Converts internal to external bus numbering.
|
1187
|
+
|
1188
|
+
C{ppc = int2ext(ppc)}
|
1189
|
+
|
1190
|
+
If the input is a single PYPOWER case dict, then it restores all
|
1191
|
+
buses, generators and branches that were removed because of being
|
1192
|
+
isolated or off-line, and reverts to the original generator ordering
|
1193
|
+
and original bus numbering. This requires that the 'order' key
|
1194
|
+
created by L{ext2int} be in place.
|
1195
|
+
|
1196
|
+
Example::
|
1197
|
+
ppc = int2ext(ppc)
|
1198
|
+
|
1199
|
+
@see: L{ext2int}, L{i2e_field}, L{i2e_data}
|
1200
|
+
|
1201
|
+
@author: Ray Zimmerman (PSERC Cornell)
|
1202
|
+
"""
|
1203
|
+
ppc = deepcopy(ppc)
|
1204
|
+
if val_or_field is None: # nargin == 1
|
1205
|
+
if 'order' not in ppc:
|
1206
|
+
logger.debug('int2ext: ppc does not have the "order" field '
|
1207
|
+
'required for conversion back to external numbering.\n')
|
1208
|
+
o = ppc["order"]
|
1209
|
+
|
1210
|
+
if o["state"] == 'i':
|
1211
|
+
# execute userfcn callbacks for 'int2ext' stage
|
1212
|
+
if 'userfcn' in ppc:
|
1213
|
+
ppc = run_userfcn(ppc["userfcn"], 'int2ext', ppc)
|
1214
|
+
|
1215
|
+
# save data matrices with internal ordering & restore originals
|
1216
|
+
o["int"] = {}
|
1217
|
+
o["int"]["bus"] = ppc["bus"].copy()
|
1218
|
+
o["int"]["branch"] = ppc["branch"].copy()
|
1219
|
+
o["int"]["gen"] = ppc["gen"].copy()
|
1220
|
+
ppc["bus"] = o["ext"]["bus"].copy()
|
1221
|
+
ppc["branch"] = o["ext"]["branch"].copy()
|
1222
|
+
ppc["gen"] = o["ext"]["gen"].copy()
|
1223
|
+
if 'gencost' in ppc:
|
1224
|
+
o["int"]["gencost"] = ppc["gencost"].copy()
|
1225
|
+
ppc["gencost"] = o["ext"]["gencost"].copy()
|
1226
|
+
if 'areas' in ppc:
|
1227
|
+
o["int"]["areas"] = ppc["areas"].copy()
|
1228
|
+
ppc["areas"] = o["ext"]["areas"].copy()
|
1229
|
+
if 'A' in ppc:
|
1230
|
+
o["int"]["A"] = ppc["A"].copy()
|
1231
|
+
ppc["A"] = o["ext"]["A"].copy()
|
1232
|
+
if 'N' in ppc:
|
1233
|
+
o["int"]["N"] = ppc["N"].copy()
|
1234
|
+
ppc["N"] = o["ext"]["N"].copy()
|
1235
|
+
|
1236
|
+
# update data (in bus, branch and gen only)
|
1237
|
+
ppc["bus"][o["bus"]["status"]["on"], :] = \
|
1238
|
+
o["int"]["bus"]
|
1239
|
+
ppc["branch"][o["branch"]["status"]["on"], :] = \
|
1240
|
+
o["int"]["branch"]
|
1241
|
+
ppc["gen"][o["gen"]["status"]["on"], :] = \
|
1242
|
+
o["int"]["gen"][o["gen"]["i2e"], :]
|
1243
|
+
if 'areas' in ppc:
|
1244
|
+
ppc["areas"][o["areas"]["status"]["on"], :] = \
|
1245
|
+
o["int"]["areas"]
|
1246
|
+
|
1247
|
+
# revert to original bus numbers
|
1248
|
+
ppc["bus"][o["bus"]["status"]["on"], IDX.bus.BUS_I] = \
|
1249
|
+
o["bus"]["i2e"][ppc["bus"][o["bus"]["status"]["on"], IDX.bus.BUS_I].astype(int)]
|
1250
|
+
ppc["branch"][o["branch"]["status"]["on"], IDX.branch.F_BUS] = \
|
1251
|
+
o["bus"]["i2e"][ppc["branch"]
|
1252
|
+
[o["branch"]["status"]["on"], IDX.branch.F_BUS].astype(int)]
|
1253
|
+
ppc["branch"][o["branch"]["status"]["on"], IDX.branch.T_BUS] = \
|
1254
|
+
o["bus"]["i2e"][ppc["branch"]
|
1255
|
+
[o["branch"]["status"]["on"], IDX.branch.T_BUS].astype(int)]
|
1256
|
+
ppc["gen"][o["gen"]["status"]["on"], IDX.gen.GEN_BUS] = \
|
1257
|
+
o["bus"]["i2e"][ppc["gen"]
|
1258
|
+
[o["gen"]["status"]["on"], IDX.gen.GEN_BUS].astype(int)]
|
1259
|
+
if 'areas' in ppc:
|
1260
|
+
ppc["areas"][o["areas"]["status"]["on"], IDX.area.PRICE_REF_BUS] = \
|
1261
|
+
o["bus"]["i2e"][ppc["areas"]
|
1262
|
+
[o["areas"]["status"]["on"], IDX.area.PRICE_REF_BUS].astype(int)]
|
1263
|
+
|
1264
|
+
if 'ext' in o:
|
1265
|
+
del o['ext']
|
1266
|
+
o["state"] = 'e'
|
1267
|
+
ppc["order"] = o
|
1268
|
+
else:
|
1269
|
+
logger.debug('int2ext: ppc claims it is already using '
|
1270
|
+
'external numbering.\n')
|
1271
|
+
else: # convert extra data
|
1272
|
+
if isinstance(val_or_field, str) or isinstance(val_or_field, list):
|
1273
|
+
# field (key)
|
1274
|
+
logger.warning(
|
1275
|
+
'Calls of the form MPC = INT2EXT(MPC, '
|
1276
|
+
'FIELD_NAME'
|
1277
|
+
', ...) have been deprecated. Please replace INT2EXT with I2E_FIELD.')
|
1278
|
+
bus, gen = val_or_field, oldval
|
1279
|
+
if ordering is not None:
|
1280
|
+
dim = ordering
|
1281
|
+
ppc = i2e_field(ppc, bus, gen, dim)
|
1282
|
+
else:
|
1283
|
+
# value
|
1284
|
+
logger.warning(
|
1285
|
+
'Calls of the form VAL = INT2EXT(MPC, VAL, ...) have been deprecated. Please replace INT2EXT with I2E_DATA.')
|
1286
|
+
bus, gen, branch = val_or_field, oldval, ordering
|
1287
|
+
ppc = i2e_data(ppc, bus, gen, branch, dim)
|
1288
|
+
|
1289
|
+
return ppc
|
1290
|
+
|
1291
|
+
|
1292
|
+
def int2ext1(i2e, bus, gen, branch, areas):
|
1293
|
+
"""
|
1294
|
+
Converts from the consecutive internal bus numbers back to the originals
|
1295
|
+
using the mapping provided by the I2E vector returned from C{ext2int}.
|
1296
|
+
|
1297
|
+
@see: L{ext2int}
|
1298
|
+
@see: U{http://www.pserc.cornell.edu/matpower/}
|
1299
|
+
"""
|
1300
|
+
bus[:, IDX.bus.BUS_I] = i2e[bus[:, IDX.bus.BUS_I].astype(int)]
|
1301
|
+
gen[:, IDX.gen.GEN_BUS] = i2e[gen[:, IDX.gen.GEN_BUS].astype(int)]
|
1302
|
+
branch[:, IDX.branch.F_BUS] = i2e[branch[:, IDX.branch.F_BUS].astype(int)]
|
1303
|
+
branch[:, IDX.branch.T_BUS] = i2e[branch[:, IDX.branch.T_BUS].astype(int)]
|
1304
|
+
|
1305
|
+
if areas != None and len(areas) > 0:
|
1306
|
+
areas[:, IDX.area.PRICE_REF_BUS] = i2e[areas[:, IDX.area.PRICE_REF_BUS].astype(int)]
|
1307
|
+
return bus, gen, branch, areas
|
1308
|
+
|
1309
|
+
return bus, gen, branch
|
1310
|
+
|
1311
|
+
|
1312
|
+
def e2i_data(ppc, val, ordering, dim=0):
|
1313
|
+
"""
|
1314
|
+
Converts data from external to internal indexing.
|
1315
|
+
|
1316
|
+
When given a case dict that has already been converted to
|
1317
|
+
internal indexing, this function can be used to convert other data
|
1318
|
+
structures as well by passing in 2 or 3 extra parameters in
|
1319
|
+
addition to the case dict. If the value passed in the 2nd
|
1320
|
+
argument is a column vector, it will be converted according to the
|
1321
|
+
C{ordering} specified by the 3rd argument (described below). If C{val}
|
1322
|
+
is an n-dimensional matrix, then the optional 4th argument (C{dim},
|
1323
|
+
default = 0) can be used to specify which dimension to reorder.
|
1324
|
+
The return value in this case is the value passed in, converted
|
1325
|
+
to internal indexing.
|
1326
|
+
|
1327
|
+
The 3rd argument, C{ordering}, is used to indicate whether the data
|
1328
|
+
corresponds to bus-, gen- or branch-ordered data. It can be one
|
1329
|
+
of the following three strings: 'bus', 'gen' or 'branch'. For
|
1330
|
+
data structures with multiple blocks of data, ordered by bus,
|
1331
|
+
gen or branch, they can be converted with a single call by
|
1332
|
+
specifying C{ordering} as a list of strings.
|
1333
|
+
|
1334
|
+
Any extra elements, rows, columns, etc. beyond those indicated
|
1335
|
+
in C{ordering}, are not disturbed.
|
1336
|
+
|
1337
|
+
Examples:
|
1338
|
+
A_int = e2i_data(ppc, A_ext, ['bus','bus','gen','gen'], 1)
|
1339
|
+
|
1340
|
+
Converts an A matrix for user-supplied OPF constraints from
|
1341
|
+
external to internal ordering, where the columns of the A
|
1342
|
+
matrix correspond to bus voltage angles, then voltage
|
1343
|
+
magnitudes, then generator real power injections and finally
|
1344
|
+
generator reactive power injections.
|
1345
|
+
|
1346
|
+
gencost_int = e2i_data(ppc, gencost_ext, ['gen','gen'], 0)
|
1347
|
+
|
1348
|
+
Converts a GENCOST matrix that has both real and reactive power
|
1349
|
+
costs (in rows 1--ng and ng+1--2*ng, respectively).
|
1350
|
+
"""
|
1351
|
+
if 'order' not in ppc:
|
1352
|
+
logger.debug('e2i_data: ppc does not have the \'order\' field '
|
1353
|
+
'required to convert from external to internal numbering.\n')
|
1354
|
+
return
|
1355
|
+
|
1356
|
+
o = ppc['order']
|
1357
|
+
if o['state'] != 'i':
|
1358
|
+
logger.debug('e2i_data: ppc does not have internal ordering '
|
1359
|
+
'data available, call ext2int first\n')
|
1360
|
+
return
|
1361
|
+
|
1362
|
+
if isinstance(ordering, str): # single set
|
1363
|
+
if ordering == 'gen':
|
1364
|
+
idx = o[ordering]["status"]["on"][o[ordering]["e2i"]]
|
1365
|
+
else:
|
1366
|
+
idx = o[ordering]["status"]["on"]
|
1367
|
+
val = get_reorder(val, idx, dim)
|
1368
|
+
else: # multiple: sets
|
1369
|
+
b = 0 # base
|
1370
|
+
new_v = []
|
1371
|
+
for ordr in ordering:
|
1372
|
+
n = o["ext"][ordr].shape[0]
|
1373
|
+
v = get_reorder(val, b + np.arange(n), dim)
|
1374
|
+
new_v.append(e2i_data(ppc, v, ordr, dim))
|
1375
|
+
b = b + n
|
1376
|
+
n = val.shape[dim]
|
1377
|
+
if n > b: # the rest
|
1378
|
+
v = get_reorder(val, np.arange(b, n), dim)
|
1379
|
+
new_v.append(v)
|
1380
|
+
|
1381
|
+
if sp.issparse(new_v[0]):
|
1382
|
+
if dim == 0:
|
1383
|
+
sp.vstack(new_v, 'csr')
|
1384
|
+
elif dim == 1:
|
1385
|
+
sp.hstack(new_v, 'csr')
|
1386
|
+
else:
|
1387
|
+
raise ValueError('dim (%d) may be 0 or 1' % dim)
|
1388
|
+
else:
|
1389
|
+
val = np.concatenate(new_v, dim)
|
1390
|
+
return val
|
1391
|
+
|
1392
|
+
|
1393
|
+
def e2i_field(ppc, field, ordering, dim=0):
|
1394
|
+
"""
|
1395
|
+
Converts fields of C{ppc} from external to internal indexing.
|
1396
|
+
|
1397
|
+
This function performs several different tasks, depending on the
|
1398
|
+
arguments passed.
|
1399
|
+
|
1400
|
+
When given a case dict that has already been converted to
|
1401
|
+
internal indexing, this function can be used to convert other data
|
1402
|
+
structures as well by passing in 2 or 3 extra parameters in
|
1403
|
+
addition to the case dict.
|
1404
|
+
|
1405
|
+
The 2nd argument is a string or list of strings, specifying
|
1406
|
+
a field in the case dict whose value should be converted by
|
1407
|
+
a corresponding call to L{e2i_data}. In this case, the converted value
|
1408
|
+
is stored back in the specified field, the original value is
|
1409
|
+
saved for later use and the updated case dict is returned.
|
1410
|
+
If C{field} is a list of strings, they specify nested fields.
|
1411
|
+
|
1412
|
+
The 3rd and optional 4th arguments are simply passed along to
|
1413
|
+
the call to L{e2i_data}.
|
1414
|
+
|
1415
|
+
Examples:
|
1416
|
+
ppc = e2i_field(ppc, ['reserves', 'cost'], 'gen')
|
1417
|
+
|
1418
|
+
Reorders rows of ppc['reserves']['cost'] to match internal generator
|
1419
|
+
ordering.
|
1420
|
+
|
1421
|
+
ppc = e2i_field(ppc, ['reserves', 'zones'], 'gen', 1)
|
1422
|
+
|
1423
|
+
Reorders columns of ppc['reserves']['zones'] to match internal
|
1424
|
+
generator ordering.
|
1425
|
+
|
1426
|
+
@see: L{i2e_field}, L{e2i_data}, L{ext2int}
|
1427
|
+
"""
|
1428
|
+
if isinstance(field, str):
|
1429
|
+
key = '["%s"]' % field
|
1430
|
+
else:
|
1431
|
+
key = '["%s"]' % '"]["'.join(field)
|
1432
|
+
|
1433
|
+
v_ext = ppc["order"]["ext"]
|
1434
|
+
for fld in field:
|
1435
|
+
if fld not in v_ext:
|
1436
|
+
v_ext[fld] = {}
|
1437
|
+
v_ext = v_ext[fld]
|
1438
|
+
|
1439
|
+
exec('ppc["order"]["ext"]%s = ppc%s.copy()' % (key, key))
|
1440
|
+
exec('ppc%s = e2i_data(ppc, ppc%s, ordering, dim)' % (key, key))
|
1441
|
+
|
1442
|
+
return ppc
|
1443
|
+
|
1444
|
+
|
1445
|
+
def ext2int(ppc, val_or_field=None, ordering=None, dim=0):
|
1446
|
+
"""
|
1447
|
+
Converts external to internal indexing.
|
1448
|
+
|
1449
|
+
This function has two forms, the old form that operates on
|
1450
|
+
and returns individual matrices and the new form that operates
|
1451
|
+
on and returns an entire PYPOWER case dict.
|
1452
|
+
|
1453
|
+
1. C{ppc = ext2int(ppc)}
|
1454
|
+
|
1455
|
+
If the input is a single PYPOWER case dict, then all isolated
|
1456
|
+
buses, off-line generators and branches are removed along with any
|
1457
|
+
generators, branches or areas connected to isolated buses. Then the
|
1458
|
+
buses are renumbered consecutively, beginning at 0, and the
|
1459
|
+
generators are sorted by increasing bus number. Any 'ext2int'
|
1460
|
+
callback routines registered in the case are also invoked
|
1461
|
+
automatically. All of the related
|
1462
|
+
indexing information and the original data matrices are stored under
|
1463
|
+
the 'order' key of the dict to be used by C{int2ext} to perform
|
1464
|
+
the reverse conversions. If the case is already using internal
|
1465
|
+
numbering it is returned unchanged.
|
1466
|
+
|
1467
|
+
Example::
|
1468
|
+
ppc = ext2int(ppc)
|
1469
|
+
|
1470
|
+
@see: L{int2ext}, L{e2i_field}, L{e2i_data}
|
1471
|
+
|
1472
|
+
@author: Ray Zimmerman (PSERC Cornell)
|
1473
|
+
"""
|
1474
|
+
ppc = deepcopy(ppc)
|
1475
|
+
if val_or_field is None: # nargin == 1
|
1476
|
+
first = 'order' not in ppc
|
1477
|
+
if first or ppc["order"]["state"] == 'e':
|
1478
|
+
# initialize order
|
1479
|
+
if first:
|
1480
|
+
o = {
|
1481
|
+
'ext': {
|
1482
|
+
'bus': None,
|
1483
|
+
'branch': None,
|
1484
|
+
'gen': None
|
1485
|
+
},
|
1486
|
+
'bus': {'e2i': None,
|
1487
|
+
'i2e': None,
|
1488
|
+
'status': {}},
|
1489
|
+
'gen': {'e2i': None,
|
1490
|
+
'i2e': None,
|
1491
|
+
'status': {}},
|
1492
|
+
'branch': {'status': {}}
|
1493
|
+
}
|
1494
|
+
else:
|
1495
|
+
o = ppc["order"]
|
1496
|
+
|
1497
|
+
# sizes
|
1498
|
+
nb = ppc["bus"].shape[0]
|
1499
|
+
ng = ppc["gen"].shape[0]
|
1500
|
+
ng0 = ng
|
1501
|
+
if 'A' in ppc:
|
1502
|
+
dc = True if ppc["A"].shape[1] < (2 * nb + 2 * ng) else False
|
1503
|
+
elif 'N' in ppc:
|
1504
|
+
dc = True if ppc["N"].shape[1] < (2 * nb + 2 * ng) else False
|
1505
|
+
else:
|
1506
|
+
dc = False
|
1507
|
+
|
1508
|
+
# save data matrices with external ordering
|
1509
|
+
if 'ext' not in o:
|
1510
|
+
o['ext'] = {}
|
1511
|
+
# Note: these dictionaries contain mixed float/int data,
|
1512
|
+
# so don't cast them all astype(int) for numpy/scipy indexing
|
1513
|
+
o["ext"]["bus"] = ppc["bus"].copy()
|
1514
|
+
o["ext"]["branch"] = ppc["branch"].copy()
|
1515
|
+
o["ext"]["gen"] = ppc["gen"].copy()
|
1516
|
+
if 'areas' in ppc:
|
1517
|
+
if len(ppc["areas"]) == 0: # if areas field is empty
|
1518
|
+
del ppc['areas'] # delete it (so it's ignored)
|
1519
|
+
else: # otherwise
|
1520
|
+
o["ext"]["areas"] = ppc["areas"].copy() # save it
|
1521
|
+
|
1522
|
+
# check that all buses have a valid BUS_TYPE
|
1523
|
+
bt = ppc["bus"][:, IDX.bus.BUS_TYPE]
|
1524
|
+
err = find(~((bt == IDX.bus.PQ) | (bt == IDX.bus.PV) |
|
1525
|
+
(bt == IDX.bus.REF) | (bt == IDX.bus.NONE)))
|
1526
|
+
if len(err) > 0:
|
1527
|
+
logger.debug('ext2int: bus %d has an invalid BUS_TYPE\n' % err)
|
1528
|
+
|
1529
|
+
# determine which buses, branches, gens are connected and
|
1530
|
+
# in-service
|
1531
|
+
n2i = c_sparse((range(nb), (ppc["bus"][:, IDX.bus.BUS_I], np.zeros(nb))),
|
1532
|
+
shape=(max(ppc["bus"][:, IDX.bus.BUS_I].astype(int)) + 1, 1))
|
1533
|
+
n2i = (np.array(n2i.todense().flatten())[0, :]).astype(int) # as 1D array
|
1534
|
+
bs = (bt != IDX.bus.NONE) # bus status
|
1535
|
+
o["bus"]["status"]["on"] = find(bs) # connected
|
1536
|
+
o["bus"]["status"]["off"] = find(~bs) # isolated
|
1537
|
+
gs = ((ppc["gen"][:, IDX.gen.GEN_STATUS] > 0) & # gen status
|
1538
|
+
bs[n2i[ppc["gen"][:, IDX.gen.GEN_BUS].astype(int)]])
|
1539
|
+
o["gen"]["status"]["on"] = find(gs) # on and connected
|
1540
|
+
o["gen"]["status"]["off"] = find(~gs) # off or isolated
|
1541
|
+
brs = (ppc["branch"][:, IDX.branch.BR_STATUS].astype(int) & # branch status
|
1542
|
+
bs[n2i[ppc["branch"][:, IDX.branch.F_BUS].astype(int)]] &
|
1543
|
+
bs[n2i[ppc["branch"][:, IDX.branch.T_BUS].astype(int)]]).astype(bool)
|
1544
|
+
o["branch"]["status"]["on"] = find(brs) # on and conn
|
1545
|
+
o["branch"]["status"]["off"] = find(~brs)
|
1546
|
+
if 'areas' in ppc:
|
1547
|
+
ar = bs[n2i[ppc["areas"][:, IDX.area.PRICE_REF_BUS].astype(int)]]
|
1548
|
+
o["areas"] = {"status": {}}
|
1549
|
+
o["areas"]["status"]["on"] = find(ar)
|
1550
|
+
o["areas"]["status"]["off"] = find(~ar)
|
1551
|
+
|
1552
|
+
# delete stuff that is "out"
|
1553
|
+
if len(o["bus"]["status"]["off"]) > 0:
|
1554
|
+
# ppc["bus"][o["bus"]["status"]["off"], :] = array([])
|
1555
|
+
ppc["bus"] = ppc["bus"][o["bus"]["status"]["on"], :]
|
1556
|
+
if len(o["branch"]["status"]["off"]) > 0:
|
1557
|
+
# ppc["branch"][o["branch"]["status"]["off"], :] = array([])
|
1558
|
+
ppc["branch"] = ppc["branch"][o["branch"]["status"]["on"], :]
|
1559
|
+
if len(o["gen"]["status"]["off"]) > 0:
|
1560
|
+
# ppc["gen"][o["gen"]["status"]["off"], :] = array([])
|
1561
|
+
ppc["gen"] = ppc["gen"][o["gen"]["status"]["on"], :]
|
1562
|
+
if 'areas' in ppc and (len(o["areas"]["status"]["off"]) > 0):
|
1563
|
+
# ppc["areas"][o["areas"]["status"]["off"], :] = array([])
|
1564
|
+
ppc["areas"] = ppc["areas"][o["areas"]["status"]["on"], :]
|
1565
|
+
|
1566
|
+
# update size
|
1567
|
+
nb = ppc["bus"].shape[0]
|
1568
|
+
|
1569
|
+
# apply consecutive bus numbering
|
1570
|
+
o["bus"]["i2e"] = ppc["bus"][:, IDX.bus.BUS_I].copy()
|
1571
|
+
o["bus"]["e2i"] = np.zeros(max(o["bus"]["i2e"]).astype(int) + 1)
|
1572
|
+
o["bus"]["e2i"][o["bus"]["i2e"].astype(int)] = np.arange(nb)
|
1573
|
+
ppc["bus"][:, IDX.bus.BUS_I] = \
|
1574
|
+
o["bus"]["e2i"][ppc["bus"][:, IDX.bus.BUS_I].astype(int)].copy()
|
1575
|
+
ppc["gen"][:, IDX.gen.GEN_BUS] = \
|
1576
|
+
o["bus"]["e2i"][ppc["gen"][:, IDX.gen.GEN_BUS].astype(int)].copy()
|
1577
|
+
ppc["branch"][:, IDX.branch.F_BUS] = \
|
1578
|
+
o["bus"]["e2i"][ppc["branch"][:, IDX.branch.F_BUS].astype(int)].copy()
|
1579
|
+
ppc["branch"][:, IDX.branch.T_BUS] = \
|
1580
|
+
o["bus"]["e2i"][ppc["branch"][:, IDX.branch.T_BUS].astype(int)].copy()
|
1581
|
+
if 'areas' in ppc:
|
1582
|
+
ppc["areas"][:, IDX.area.PRICE_REF_BUS] = \
|
1583
|
+
o["bus"]["e2i"][ppc["areas"][:,
|
1584
|
+
IDX.area.PRICE_REF_BUS].astype(int)].copy()
|
1585
|
+
|
1586
|
+
# reorder gens in order of increasing bus number
|
1587
|
+
o["gen"]["e2i"] = np.argsort(ppc["gen"][:, IDX.gen.GEN_BUS])
|
1588
|
+
o["gen"]["i2e"] = np.argsort(o["gen"]["e2i"])
|
1589
|
+
|
1590
|
+
ppc["gen"] = ppc["gen"][o["gen"]["e2i"].astype(int), :]
|
1591
|
+
|
1592
|
+
if 'int' in o:
|
1593
|
+
del o['int']
|
1594
|
+
o["state"] = 'i'
|
1595
|
+
ppc["order"] = o
|
1596
|
+
|
1597
|
+
# update gencost, A and N
|
1598
|
+
if 'gencost' in ppc:
|
1599
|
+
ordering = ['gen'] # Pg cost only
|
1600
|
+
if ppc["gencost"].shape[0] == (2 * ng0):
|
1601
|
+
ordering.append('gen') # include Qg cost
|
1602
|
+
ppc = e2i_field(ppc, 'gencost', ordering)
|
1603
|
+
if 'A' in ppc or 'N' in ppc:
|
1604
|
+
if dc:
|
1605
|
+
ordering = ['bus', 'gen']
|
1606
|
+
else:
|
1607
|
+
ordering = ['bus', 'bus', 'gen', 'gen']
|
1608
|
+
if 'A' in ppc:
|
1609
|
+
ppc = e2i_field(ppc, 'A', ordering, 1)
|
1610
|
+
if 'N' in ppc:
|
1611
|
+
ppc = e2i_field(ppc, 'N', ordering, 1)
|
1612
|
+
|
1613
|
+
# execute userfcn callbacks for 'ext2int' stage
|
1614
|
+
if 'userfcn' in ppc:
|
1615
|
+
ppc = run_userfcn(ppc['userfcn'], 'ext2int', ppc)
|
1616
|
+
else: # convert extra data
|
1617
|
+
if isinstance(val_or_field, str) or isinstance(val_or_field, list):
|
1618
|
+
# field
|
1619
|
+
logger.warning('Calls of the form ppc = ext2int(ppc, '
|
1620
|
+
'\'field_name\', ...) have been deprecated. Please '
|
1621
|
+
'replace ext2int with e2i_field.', DeprecationWarning)
|
1622
|
+
gen, branch = val_or_field, ordering
|
1623
|
+
ppc = e2i_field(ppc, gen, branch, dim)
|
1624
|
+
|
1625
|
+
else:
|
1626
|
+
# value
|
1627
|
+
logger.warning('Calls of the form val = ext2int(ppc, val, ...) have been '
|
1628
|
+
'deprecated. Please replace ext2int with e2i_data.',
|
1629
|
+
DeprecationWarning)
|
1630
|
+
gen, branch = val_or_field, ordering
|
1631
|
+
ppc = e2i_data(ppc, gen, branch, dim)
|
1632
|
+
|
1633
|
+
return ppc
|
1634
|
+
|
1635
|
+
|
1636
|
+
def ext2int1(bus, gen, branch, areas=None):
|
1637
|
+
"""
|
1638
|
+
Converts from (possibly non-consecutive) external bus numbers to
|
1639
|
+
consecutive internal bus numbers which start at 1. Changes are made
|
1640
|
+
to BUS, GEN, BRANCH and optionally AREAS matrices, which are returned
|
1641
|
+
along with a vector of indices I2E that can be passed to INT2EXT to
|
1642
|
+
perform the reverse conversion.
|
1643
|
+
|
1644
|
+
@see: L{int2ext}
|
1645
|
+
@see: U{http://www.pserc.cornell.edu/matpower/}
|
1646
|
+
"""
|
1647
|
+
i2e = bus[:, IDX.bus.BUS_I].astype(int)
|
1648
|
+
e2i = np.zeros(max(i2e) + 1)
|
1649
|
+
e2i[i2e] = np.arange(bus.shape[0])
|
1650
|
+
|
1651
|
+
bus[:, IDX.bus.BUS_I] = e2i[bus[:, IDX.bus.BUS_I].astype(int)]
|
1652
|
+
gen[:, IDX.gen.GEN_BUS] = e2i[gen[:, IDX.gen.GEN_BUS].astype(int)]
|
1653
|
+
branch[:, IDX.branch.F_BUS] = e2i[branch[:, IDX.branch.F_BUS].astype(int)]
|
1654
|
+
branch[:, IDX.branch.T_BUS] = e2i[branch[:, IDX.branch.T_BUS].astype(int)]
|
1655
|
+
if areas is not None and len(areas) > 0:
|
1656
|
+
areas[:, IDX.area.PRICE_REF_BUS] = e2i[areas[:, IDX.area.PRICE_REF_BUS].astype(int)]
|
1657
|
+
|
1658
|
+
return i2e, bus, gen, branch, areas
|
1659
|
+
|
1660
|
+
return i2e, bus, gen, branch
|
1661
|
+
|
1662
|
+
|
1663
|
+
def i2e_data(ppc, val, oldval, ordering, dim=0):
|
1664
|
+
"""
|
1665
|
+
Converts data from internal to external bus numbering.
|
1666
|
+
|
1667
|
+
Parameters
|
1668
|
+
----------
|
1669
|
+
ppc : dict
|
1670
|
+
The case dict.
|
1671
|
+
val : Numpy.array
|
1672
|
+
The data to be converted.
|
1673
|
+
oldval : Numpy.array
|
1674
|
+
The data to be used for off-line gens, branches, isolated buses,
|
1675
|
+
connected gens and branches.
|
1676
|
+
ordering : str or list of str
|
1677
|
+
The ordering of the data. Can be one of the following three
|
1678
|
+
strings: 'bus', 'gen' or 'branch'. For data structures with
|
1679
|
+
multiple blocks of data, ordered by bus, gen or branch, they
|
1680
|
+
can be converted with a single call by specifying C[ordering}
|
1681
|
+
as a list of strings.
|
1682
|
+
dim : int, optional
|
1683
|
+
The dimension to reorder. Default is 0.
|
1684
|
+
|
1685
|
+
Returns
|
1686
|
+
-------
|
1687
|
+
val : Numpy.array
|
1688
|
+
The converted data.
|
1689
|
+
|
1690
|
+
Examples
|
1691
|
+
--------
|
1692
|
+
Converts an A matrix for user-supplied OPF constraints from
|
1693
|
+
internal to external ordering, where the columns of the A
|
1694
|
+
matrix correspond to bus voltage angles, then voltage
|
1695
|
+
magnitudes, then generator real power injections and finally
|
1696
|
+
generator reactive power injections.
|
1697
|
+
>>> A_ext = i2e_data(ppc, A_int, A_orig, ['bus','bus','gen','gen'], 1)
|
1698
|
+
|
1699
|
+
Converts a C{gencost} matrix that has both real and reactive power
|
1700
|
+
costs (in rows 1--ng and ng+1--2*ng, respectively).
|
1701
|
+
|
1702
|
+
>>> gencost_ext = i2e_data(ppc, gencost_int, gencost_orig, ['gen','gen'], 0)
|
1703
|
+
|
1704
|
+
For a case dict using internal indexing, this function can be
|
1705
|
+
used to convert other data structures as well by passing in 3 or 4
|
1706
|
+
extra parameters in addition to the case dict. If the value passed
|
1707
|
+
in the 2nd argument C{val} is a column vector, it will be converted
|
1708
|
+
according to the ordering specified by the 4th argument (C{ordering},
|
1709
|
+
described below). If C{val} is an n-dimensional matrix, then the
|
1710
|
+
optional 5th argument (C{dim}, default = 0) can be used to specify
|
1711
|
+
which dimension to reorder. The 3rd argument (C{oldval}) is used to
|
1712
|
+
initialize the return value before converting C{val} to external
|
1713
|
+
indexing. In particular, any data corresponding to off-line gens
|
1714
|
+
or branches or isolated buses or any connected gens or branches
|
1715
|
+
will be taken from C{oldval}, with C[val} supplying the rest of the
|
1716
|
+
returned data.
|
1717
|
+
|
1718
|
+
The C{ordering} argument is used to indicate whether the data
|
1719
|
+
corresponds to bus-, gen- or branch-ordered data. It can be one
|
1720
|
+
of the following three strings: 'bus', 'gen' or 'branch'. For
|
1721
|
+
data structures with multiple blocks of data, ordered by bus,
|
1722
|
+
gen or branch, they can be converted with a single call by
|
1723
|
+
specifying C[ordering} as a list of strings.
|
1724
|
+
|
1725
|
+
Any extra elements, rows, columns, etc. beyond those indicated
|
1726
|
+
in C{ordering}, are not disturbed.
|
1727
|
+
|
1728
|
+
@see: L{e2i_data}, L{i2e_field}, L{int2ext}.
|
1729
|
+
"""
|
1730
|
+
if 'order' not in ppc:
|
1731
|
+
logger.debug('i2e_data: ppc does not have the \'order\' field '
|
1732
|
+
'required for conversion back to external numbering.\n')
|
1733
|
+
return
|
1734
|
+
|
1735
|
+
o = ppc["order"]
|
1736
|
+
if o['state'] != 'i':
|
1737
|
+
logger.debug('i2e_data: ppc does not appear to be in internal '
|
1738
|
+
'order\n')
|
1739
|
+
return
|
1740
|
+
|
1741
|
+
if isinstance(ordering, str): # single set
|
1742
|
+
if ordering == 'gen':
|
1743
|
+
v = get_reorder(val, o[ordering]["i2e"], dim)
|
1744
|
+
else:
|
1745
|
+
v = val
|
1746
|
+
val = set_reorder(oldval, v, o[ordering]["status"]["on"], dim)
|
1747
|
+
else: # multiple sets
|
1748
|
+
be = 0 # base, external indexing
|
1749
|
+
bi = 0 # base, internal indexing
|
1750
|
+
new_v = []
|
1751
|
+
for ordr in ordering:
|
1752
|
+
ne = o["ext"][ordr].shape[0]
|
1753
|
+
ni = ppc[ordr].shape[0]
|
1754
|
+
v = get_reorder(val, bi + np.arange(ni), dim)
|
1755
|
+
oldv = get_reorder(oldval, be + np.arange(ne), dim)
|
1756
|
+
new_v.append(int2ext(ppc, v, oldv, ordr, dim))
|
1757
|
+
be = be + ne
|
1758
|
+
bi = bi + ni
|
1759
|
+
ni = val.shape[dim]
|
1760
|
+
if ni > bi: # the rest
|
1761
|
+
v = get_reorder(val, np.arange(bi, ni), dim)
|
1762
|
+
new_v.append(v)
|
1763
|
+
val = np.concatenate(new_v, dim)
|
1764
|
+
|
1765
|
+
return val
|
1766
|
+
|
1767
|
+
|
1768
|
+
def i2e_field(ppc, field, ordering, dim=0):
|
1769
|
+
"""
|
1770
|
+
Converts fields of MPC from internal to external bus numbering.
|
1771
|
+
|
1772
|
+
Parameters
|
1773
|
+
----------
|
1774
|
+
ppc : dict
|
1775
|
+
The case dict.
|
1776
|
+
field : str or list of str
|
1777
|
+
The field to be converted. If C{field} is a list of strings,
|
1778
|
+
they specify nested fields.
|
1779
|
+
ordering : str or list of str
|
1780
|
+
The ordering of the data. Can be one of the following three
|
1781
|
+
strings: 'bus', 'gen' or 'branch'. For data structures with
|
1782
|
+
multiple blocks of data, ordered by bus, gen or branch, they
|
1783
|
+
can be converted with a single call by specifying C[ordering}
|
1784
|
+
as a list of strings.
|
1785
|
+
dim : int, optional
|
1786
|
+
The dimension to reorder. Default is 0.
|
1787
|
+
|
1788
|
+
Returns
|
1789
|
+
-------
|
1790
|
+
ppc : dict
|
1791
|
+
The updated case dict.
|
1792
|
+
|
1793
|
+
For a case dict using internal indexing, this function can be
|
1794
|
+
used to convert other data structures as well by passing in 2 or 3
|
1795
|
+
extra parameters in addition to the case dict.
|
1796
|
+
|
1797
|
+
If the 2nd argument is a string or list of strings, it
|
1798
|
+
specifies a field in the case dict whose value should be
|
1799
|
+
converted by L{i2e_data}. In this case, the corresponding
|
1800
|
+
C{oldval} is taken from where it was stored by L{ext2int} in
|
1801
|
+
ppc['order']['ext'] and the updated case dict is returned.
|
1802
|
+
If C{field} is a list of strings, they specify nested fields.
|
1803
|
+
|
1804
|
+
The 3rd and optional 4th arguments are simply passed along to
|
1805
|
+
the call to L{i2e_data}.
|
1806
|
+
|
1807
|
+
Examples:
|
1808
|
+
ppc = i2e_field(ppc, ['reserves', 'cost'], 'gen')
|
1809
|
+
|
1810
|
+
Reorders rows of ppc['reserves']['cost'] to match external generator
|
1811
|
+
ordering.
|
1812
|
+
|
1813
|
+
ppc = i2e_field(ppc, ['reserves', 'zones'], 'gen', 1)
|
1814
|
+
|
1815
|
+
Reorders columns of ppc.reserves.zones to match external
|
1816
|
+
generator ordering.
|
1817
|
+
|
1818
|
+
@see: L{e2i_field}, L{i2e_data}, L{int2ext}.
|
1819
|
+
"""
|
1820
|
+
if 'int' not in ppc['order']:
|
1821
|
+
ppc['order']['int'] = {}
|
1822
|
+
|
1823
|
+
if isinstance(field, str):
|
1824
|
+
key = '["%s"]' % field
|
1825
|
+
else: # nested dicts
|
1826
|
+
key = '["%s"]' % '"]["'.join(field)
|
1827
|
+
|
1828
|
+
v_int = ppc["order"]["int"]
|
1829
|
+
for fld in field:
|
1830
|
+
if fld not in v_int:
|
1831
|
+
v_int[fld] = {}
|
1832
|
+
v_int = v_int[fld]
|
1833
|
+
|
1834
|
+
exec('ppc["order"]["int"]%s = ppc%s.copy()' % (key, key))
|
1835
|
+
exec('ppc%s = i2e_data(ppc, ppc%s, ppc["order"]["ext"]%s, ordering, dim)' %
|
1836
|
+
(key, key, key))
|
1837
|
+
|
1838
|
+
return ppc
|
1839
|
+
|
1840
|
+
|
1841
|
+
def total_load(bus, gen=None, load_zone=None, which_type=None):
|
1842
|
+
"""
|
1843
|
+
Returns vector of total load in each load zone.
|
1844
|
+
|
1845
|
+
@param bus: standard C{bus} matrix with C{nb} rows, where the fixed active
|
1846
|
+
and reactive loads are specified in columns C{PD} and C{QD}
|
1847
|
+
|
1848
|
+
@param gen: (optional) standard C{gen} matrix with C{ng} rows, where the
|
1849
|
+
dispatchable loads are specified by columns C{PG}, C{QG}, C{PMIN},
|
1850
|
+
C{QMIN} and C{QMAX} (in rows for which C{isload(GEN)} returns C{True}).
|
1851
|
+
If C{gen} is empty, it assumes there are no dispatchable loads.
|
1852
|
+
|
1853
|
+
@param load_zone: (optional) C{nb} element vector where the value of
|
1854
|
+
each element is either zero or the index of the load zone
|
1855
|
+
to which the corresponding bus belongs. If C{load_zone(b) = k}
|
1856
|
+
then the loads at bus C{b} will added to the values of C{Pd[k]} and
|
1857
|
+
C{Qd[k]}. If C{load_zone} is empty, the default is defined as the areas
|
1858
|
+
specified in the C{bus} matrix, i.e. C{load_zone = bus[:, BUS_AREA]}
|
1859
|
+
and load will have dimension C{= max(bus[:, BUS_AREA])}. If
|
1860
|
+
C{load_zone = 'all'}, the result is a scalar with the total system
|
1861
|
+
load.
|
1862
|
+
|
1863
|
+
@param which_type: (default is 'BOTH' if C{gen} is provided, else 'FIXED')
|
1864
|
+
- 'FIXED' : sum only fixed loads
|
1865
|
+
- 'DISPATCHABLE' : sum only dispatchable loads
|
1866
|
+
- 'BOTH' : sum both fixed and dispatchable loads
|
1867
|
+
|
1868
|
+
@see: L{scale_load}
|
1869
|
+
|
1870
|
+
@author: Ray Zimmerman (PSERC Cornell)
|
1871
|
+
"""
|
1872
|
+
nb = bus.shape[0] # number of buses
|
1873
|
+
|
1874
|
+
if gen is None:
|
1875
|
+
gen = np.array([])
|
1876
|
+
if load_zone is None:
|
1877
|
+
load_zone = np.array([], int)
|
1878
|
+
|
1879
|
+
# fill out and check which_type
|
1880
|
+
if len(gen) == 0:
|
1881
|
+
which_type = 'FIXED'
|
1882
|
+
|
1883
|
+
if (which_type == None) and (len(gen) > 0):
|
1884
|
+
which_type = 'BOTH' # 'FIXED', 'DISPATCHABLE' or 'BOTH'
|
1885
|
+
|
1886
|
+
if (which_type[0] != 'F') and (which_type[0] != 'D') and (which_type[0] != 'B'):
|
1887
|
+
logger.debug("total_load: which_type should be 'FIXED, 'DISPATCHABLE or 'BOTH'\n")
|
1888
|
+
|
1889
|
+
want_Q = True
|
1890
|
+
want_fixed = (which_type[0] == 'B') | (which_type[0] == 'F')
|
1891
|
+
want_disp = (which_type[0] == 'B') | (which_type[0] == 'D')
|
1892
|
+
|
1893
|
+
# initialize load_zone
|
1894
|
+
if isinstance(load_zone, str) and (load_zone == 'all'):
|
1895
|
+
load_zone = np.ones(nb, int) # make a single zone of all buses
|
1896
|
+
elif len(load_zone) == 0:
|
1897
|
+
load_zone = bus[:, IDX.bus.BUS_AREA].astype(int) # use areas defined in bus data as zones
|
1898
|
+
|
1899
|
+
nz = max(load_zone) # number of load zones
|
1900
|
+
|
1901
|
+
# fixed load at each bus, & initialize dispatchable
|
1902
|
+
if want_fixed:
|
1903
|
+
Pdf = bus[:, IDX.bus.PD] # real power
|
1904
|
+
if want_Q:
|
1905
|
+
Qdf = bus[:, IDX.bus.QD] # reactive power
|
1906
|
+
else:
|
1907
|
+
Pdf = np.zeros(nb) # real power
|
1908
|
+
if want_Q:
|
1909
|
+
Qdf = np.zeros(nb) # reactive power
|
1910
|
+
|
1911
|
+
# dispatchable load at each bus
|
1912
|
+
if want_disp: # need dispatchable
|
1913
|
+
ng = gen.shape[0]
|
1914
|
+
is_ld = isload(gen) & (gen[:, IDX.gen.GEN_STATUS] > 0)
|
1915
|
+
ld = find(is_ld)
|
1916
|
+
|
1917
|
+
# create map of external bus numbers to bus indices
|
1918
|
+
i2e = bus[:, IDX.bus.BUS_I].astype(int)
|
1919
|
+
e2i = zeros(max(i2e) + 1)
|
1920
|
+
e2i[i2e] = arange(nb)
|
1921
|
+
|
1922
|
+
gbus = gen[:, IDX.gen.GEN_BUS].astype(int)
|
1923
|
+
Cld = c_sparse((is_ld, (e2i[gbus], np.arange(ng))), (nb, ng))
|
1924
|
+
Pdd = -Cld * gen[:, IDX.gen.PMIN] # real power
|
1925
|
+
if want_Q:
|
1926
|
+
Q = np.zeros(ng)
|
1927
|
+
Q[ld] = (gen[ld, IDX.gen.QMIN] == 0) * gen[ld, IDX.gen.QMAX] + \
|
1928
|
+
(gen[ld, IDX.gen.QMAX] == 0) * gen[ld, IDX.gen.QMIN]
|
1929
|
+
Qdd = -Cld * Q # reactive power
|
1930
|
+
else:
|
1931
|
+
Pdd = np.zeros(nb)
|
1932
|
+
if want_Q:
|
1933
|
+
Qdd = np.zeros(nb)
|
1934
|
+
|
1935
|
+
# compute load sums
|
1936
|
+
Pd = np.zeros(nz)
|
1937
|
+
if want_Q:
|
1938
|
+
Qd = np.zeros(nz)
|
1939
|
+
|
1940
|
+
for k in range(1, nz + 1):
|
1941
|
+
idx = find(load_zone == k)
|
1942
|
+
Pd[k - 1] = sum(Pdf[idx]) + sum(Pdd[idx])
|
1943
|
+
if want_Q:
|
1944
|
+
Qd[k - 1] = sum(Qdf[idx]) + sum(Qdd[idx])
|
1945
|
+
|
1946
|
+
return Pd, Qd
|