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,665 @@
|
|
1
|
+
"""
|
2
|
+
Make matrices.
|
3
|
+
"""
|
4
|
+
import logging
|
5
|
+
|
6
|
+
import numpy as np
|
7
|
+
|
8
|
+
from numpy import flatnonzero as find
|
9
|
+
from scipy.sparse import csr_matrix as c_sparse
|
10
|
+
from scipy.sparse import lil_matrix as l_sparse
|
11
|
+
|
12
|
+
from andes.shared import deg2rad
|
13
|
+
from ams.shared import inf
|
14
|
+
from ams.pypower.idx import IDX
|
15
|
+
|
16
|
+
import ams.pypower.utils as putil
|
17
|
+
|
18
|
+
logger = logging.getLogger(__name__)
|
19
|
+
|
20
|
+
|
21
|
+
def makeAang(baseMVA, branch, nb, ppopt):
|
22
|
+
"""
|
23
|
+
Constructs the parameters for the following linear constraint limiting
|
24
|
+
the voltage angle differences across branches, where C{Va} is the vector
|
25
|
+
of bus voltage angles. C{nb} is the number of buses::
|
26
|
+
|
27
|
+
lang <= Aang * Va <= uang
|
28
|
+
|
29
|
+
Parameters
|
30
|
+
----------
|
31
|
+
branch: np.ndarray
|
32
|
+
The branch matrix.
|
33
|
+
nb : int
|
34
|
+
The number of buses.
|
35
|
+
ppopt : dict
|
36
|
+
PYPOWER options dictionary.
|
37
|
+
|
38
|
+
Returns
|
39
|
+
-------
|
40
|
+
Aang: sparse matrix
|
41
|
+
The constraint matrix for branch angle difference limits.
|
42
|
+
lang: np.ndarray
|
43
|
+
The lower bounds on the angle difference limits.
|
44
|
+
uang: np.ndarray
|
45
|
+
The upper bounds on the angle difference limits.
|
46
|
+
iang: np.ndarray
|
47
|
+
The indices of the branches with angle difference limits.
|
48
|
+
"""
|
49
|
+
# options
|
50
|
+
ignore_ang_lim = ppopt['OPF_IGNORE_ANG_LIM']
|
51
|
+
|
52
|
+
if ignore_ang_lim:
|
53
|
+
Aang = np.zeros((0, nb))
|
54
|
+
lang = np.array([])
|
55
|
+
uang = np.array([])
|
56
|
+
iang = np.array([])
|
57
|
+
else:
|
58
|
+
iang = find(((branch[:, IDX.branch.ANGMIN] != 0) & (branch[:, IDX.branch.ANGMIN] > -360)) |
|
59
|
+
((branch[:, IDX.branch.ANGMAX] != 0) & (branch[:, IDX.branch.ANGMAX] < 360)))
|
60
|
+
iangl = find(branch[iang, IDX.branch.ANGMIN])
|
61
|
+
iangh = find(branch[iang, IDX.branch.ANGMAX])
|
62
|
+
nang = len(iang)
|
63
|
+
|
64
|
+
if nang > 0:
|
65
|
+
ii = np.r_[np.arange(nang), np.arange(nang)]
|
66
|
+
jj = np.r_[branch[iang, IDX.branch.F_BUS], branch[iang, IDX.branch.T_BUS]]
|
67
|
+
Aang = c_sparse((np.r_[np.ones(nang), -np.ones(nang)],
|
68
|
+
(ii, jj)), (nang, nb))
|
69
|
+
uang = inf * np.ones(nang)
|
70
|
+
lang = -uang
|
71
|
+
lang[iangl] = branch[iang[iangl], IDX.branch.ANGMIN] * deg2rad
|
72
|
+
uang[iangh] = branch[iang[iangh], IDX.branch.ANGMAX] * deg2rad
|
73
|
+
else:
|
74
|
+
Aang = np.zeros((0, nb))
|
75
|
+
lang = np.array([])
|
76
|
+
uang = np.array([])
|
77
|
+
|
78
|
+
return Aang, lang, uang, iang
|
79
|
+
|
80
|
+
|
81
|
+
def makeApq(baseMVA, gen):
|
82
|
+
"""
|
83
|
+
Construct linear constraints for generator capability curves,
|
84
|
+
where Pg and Qg are the real and reactive generator injections::
|
85
|
+
|
86
|
+
Apqh * [Pg, Qg] <= ubpqh
|
87
|
+
Apql * [Pg, Qg] <= ubpql
|
88
|
+
|
89
|
+
Parameters
|
90
|
+
----------
|
91
|
+
baseMVA: float
|
92
|
+
The system base MVA.
|
93
|
+
gen: np.ndarray
|
94
|
+
The generator matrix.
|
95
|
+
|
96
|
+
Example
|
97
|
+
-------
|
98
|
+
>>> Apqh, ubpqh, Apql, ubpql, data = makeApq(baseMVA, gen)
|
99
|
+
|
100
|
+
C{data} constains additional information as shown below.
|
101
|
+
|
102
|
+
data['h'] [Qc1max-Qc2max, Pc2-Pc1]
|
103
|
+
|
104
|
+
data['l'] [Qc2min-Qc1min, Pc1-Pc2]
|
105
|
+
|
106
|
+
data['ipqh'] indices of gens with general PQ cap curves (upper)
|
107
|
+
|
108
|
+
data['ipql'] indices of gens with general PQ cap curves (lower)
|
109
|
+
"""
|
110
|
+
data = {}
|
111
|
+
# data dimensions
|
112
|
+
ng = gen.shape[0] # number of dispatchable injections
|
113
|
+
|
114
|
+
# which generators require additional linear constraints
|
115
|
+
# (in addition to simple box constraints) on (Pg,Qg) to correctly
|
116
|
+
# model their PQ capability curves
|
117
|
+
ipqh = find(putil.hasPQcap(gen, 'U'))
|
118
|
+
ipql = find(putil.hasPQcap(gen, 'L'))
|
119
|
+
npqh = ipqh.shape[0] # number of general PQ capability curves (upper)
|
120
|
+
npql = ipql.shape[0] # number of general PQ capability curves (lower)
|
121
|
+
|
122
|
+
# make Apqh if there is a need to add general PQ capability curves
|
123
|
+
# use normalized coefficient rows so multipliers have right scaling
|
124
|
+
# in $$/pu
|
125
|
+
if npqh > 0:
|
126
|
+
data["h"] = np.c_[gen[ipqh, IDX.gen.QC1MAX] - gen[ipqh, IDX.gen.QC2MAX],
|
127
|
+
gen[ipqh, IDX.gen.PC2] - gen[ipqh, IDX.gen.PC1]]
|
128
|
+
ubpqh = data["h"][:, 0] * gen[ipqh, IDX.gen.PC1] + \
|
129
|
+
data["h"][:, 1] * gen[ipqh, IDX.gen.QC1MAX]
|
130
|
+
for i in range(npqh):
|
131
|
+
tmp = np.linalg.norm(data["h"][i, :])
|
132
|
+
data["h"][i, :] = data["h"][i, :] / tmp
|
133
|
+
ubpqh[i] = ubpqh[i] / tmp
|
134
|
+
Apqh = c_sparse((data["h"].flatten('F'),
|
135
|
+
(np.r_[np.arange(npqh), np.arange(npqh)], np.r_[ipqh, ipqh+ng])),
|
136
|
+
(npqh, 2*ng))
|
137
|
+
ubpqh = ubpqh / baseMVA
|
138
|
+
else:
|
139
|
+
data["h"] = np.array([])
|
140
|
+
Apqh = np.zeros((0, 2*ng))
|
141
|
+
ubpqh = np.array([])
|
142
|
+
|
143
|
+
# similarly Apql
|
144
|
+
if npql > 0:
|
145
|
+
data["l"] = np.c_[gen[ipql, IDX.gen.QC2MIN] - gen[ipql, IDX.gen.QC1MIN],
|
146
|
+
gen[ipql, IDX.gen.PC1] - gen[ipql, IDX.gen.PC2]]
|
147
|
+
ubpql = data["l"][:, 0] * gen[ipql, IDX.gen.PC1] + \
|
148
|
+
data["l"][:, 1] * gen[ipql, IDX.gen.QC1MIN]
|
149
|
+
for i in range(npql):
|
150
|
+
tmp = np.linalg.norm(data["l"][i, :])
|
151
|
+
data["l"][i, :] = data["l"][i, :] / tmp
|
152
|
+
ubpql[i] = ubpql[i] / tmp
|
153
|
+
Apql = c_sparse((data["l"].flatten('F'),
|
154
|
+
(np.r_[np.arange(npql), np.arange(npql)], np.r_[ipql, ipql+ng])),
|
155
|
+
(npql, 2*ng))
|
156
|
+
ubpql = ubpql / baseMVA
|
157
|
+
else:
|
158
|
+
data["l"] = np.array([])
|
159
|
+
Apql = np.zeros((0, 2*ng))
|
160
|
+
ubpql = np.array([])
|
161
|
+
|
162
|
+
data["ipql"] = ipql
|
163
|
+
data["ipqh"] = ipqh
|
164
|
+
|
165
|
+
return Apqh, ubpqh, Apql, ubpql, data
|
166
|
+
|
167
|
+
|
168
|
+
def makeAvl(baseMVA, gen):
|
169
|
+
"""
|
170
|
+
Constructs parameters for the following linear constraint enforcing a
|
171
|
+
constant power factor constraint for dispatchable loads::
|
172
|
+
|
173
|
+
lvl <= Avl * [Pg, Qg] <= uvl
|
174
|
+
|
175
|
+
Parameters
|
176
|
+
----------
|
177
|
+
baseMVA: float
|
178
|
+
The system base MVA.
|
179
|
+
gen: np.ndarray
|
180
|
+
The generator matrix.
|
181
|
+
"""
|
182
|
+
# data dimensions
|
183
|
+
ng = gen.shape[0] # number of dispatchable injections
|
184
|
+
Pg = gen[:, IDX.gen.PG] / baseMVA
|
185
|
+
Qg = gen[:, IDX.gen.QG] / baseMVA
|
186
|
+
Pmin = gen[:, IDX.gen.PMIN] / baseMVA
|
187
|
+
Qmin = gen[:, IDX.gen.QMIN] / baseMVA
|
188
|
+
Qmax = gen[:, IDX.gen.QMAX] / baseMVA
|
189
|
+
|
190
|
+
# Find out if any of these "generators" are actually dispatchable loads.
|
191
|
+
# (see 'help isload' for details on what constitutes a dispatchable load)
|
192
|
+
# Dispatchable loads are modeled as generators with an added constant
|
193
|
+
# power factor constraint. The power factor is derived from the original
|
194
|
+
# value of Pmin and either Qmin (for inductive loads) or Qmax (for
|
195
|
+
# capacitive loads). If both Qmin and Qmax are zero, this implies a unity
|
196
|
+
# power factor without the need for an additional constraint.
|
197
|
+
# NOTE: C{ivl} is the vector of indices of generators representing variable loads.
|
198
|
+
ivl = find(putil.isload(gen) & ((Qmin != 0) | (Qmax != 0)))
|
199
|
+
nvl = ivl.shape[0] # number of dispatchable loads
|
200
|
+
|
201
|
+
# at least one of the Q limits must be zero (corresponding to Pmax == 0)
|
202
|
+
if np.any((Qmin[ivl] != 0) & (Qmax[ivl] != 0)):
|
203
|
+
logger.debug('makeAvl: either Qmin or Qmax must be equal to zero for '
|
204
|
+
'each dispatchable load.\n')
|
205
|
+
|
206
|
+
# Initial values of IDX.gen.PG and IDX.gen.QG must be consistent with specified power
|
207
|
+
# factor This is to prevent a user from unknowingly using a case file which
|
208
|
+
# would have defined a different power factor constraint under a previous
|
209
|
+
# version which used IDX.gen.PG and IDX.gen.QG to define the power factor.
|
210
|
+
Qlim = (Qmin[ivl] == 0) * Qmax[ivl] + (Qmax[ivl] == 0) * Qmin[ivl]
|
211
|
+
if np.any(abs(Qg[ivl] - Pg[ivl] * Qlim / Pmin[ivl]) > 1e-6):
|
212
|
+
logger.debug('makeAvl: For a dispatchable load, PG and QG must be '
|
213
|
+
'consistent with the power factor defined by PMIN and '
|
214
|
+
'the Q limits.\n')
|
215
|
+
|
216
|
+
# make Avl, lvl, uvl, for lvl <= Avl * [Pg Qg] <= uvl
|
217
|
+
if nvl > 0:
|
218
|
+
xx = Pmin[ivl]
|
219
|
+
yy = Qlim
|
220
|
+
pftheta = np.arctan2(yy, xx)
|
221
|
+
pc = np.sin(pftheta)
|
222
|
+
qc = -np.cos(pftheta)
|
223
|
+
ii = np.r_[np.arange(nvl), np.arange(nvl)]
|
224
|
+
jj = np.r_[ivl, ivl + ng]
|
225
|
+
Avl = c_sparse((np.r_[pc, qc], (ii, jj)), (nvl, 2 * ng))
|
226
|
+
lvl = np.zeros(nvl)
|
227
|
+
uvl = lvl
|
228
|
+
else:
|
229
|
+
Avl = np.zeros((0, 2*ng))
|
230
|
+
lvl = np.array([])
|
231
|
+
uvl = np.array([])
|
232
|
+
|
233
|
+
return Avl, lvl, uvl, ivl
|
234
|
+
|
235
|
+
|
236
|
+
def makeB(baseMVA, bus, branch, alg):
|
237
|
+
"""
|
238
|
+
Builds the FDPF matrices, B prime and B double prime.
|
239
|
+
|
240
|
+
Parameters
|
241
|
+
----------
|
242
|
+
baseMVA: float
|
243
|
+
The system base MVA.
|
244
|
+
bus: np.ndarray
|
245
|
+
The bus matrix.
|
246
|
+
branch: np.ndarray
|
247
|
+
The branch matrix.
|
248
|
+
alg: int
|
249
|
+
The power flow algorithm, value of the C{PF_ALG} option, see L{runpf}.
|
250
|
+
|
251
|
+
Returns
|
252
|
+
-------
|
253
|
+
Bp: np.ndarray
|
254
|
+
The B prime matrix.
|
255
|
+
Bpp: np.ndarray
|
256
|
+
The B double prime matrix.
|
257
|
+
"""
|
258
|
+
# constants
|
259
|
+
nb = bus.shape[0] # number of buses
|
260
|
+
nl = branch.shape[0] # number of lines
|
261
|
+
|
262
|
+
# ----- form Bp (B prime) -----
|
263
|
+
temp_branch = np.copy(branch) # modify a copy of branch
|
264
|
+
temp_bus = np.copy(bus) # modify a copy of bus
|
265
|
+
temp_bus[:, IDX.bus.BS] = np.zeros(nb) # zero out shunts at buses
|
266
|
+
temp_branch[:, IDX.branch.BR_B] = np.zeros(nl) # zero out line charging shunts
|
267
|
+
temp_branch[:, IDX.branch.TAP] = np.ones(nl) # cancel out taps
|
268
|
+
if alg == 2: # if XB method
|
269
|
+
temp_branch[:, IDX.branch.BR_R] = np.zeros(nl) # zero out line resistance
|
270
|
+
Bp = -1 * makeYbus(baseMVA, temp_bus, temp_branch)[0].imag
|
271
|
+
|
272
|
+
# ----- form Bpp (B double prime) -----
|
273
|
+
temp_branch = np.copy(branch) # modify a copy of branch
|
274
|
+
temp_branch[:, IDX.branch.SHIFT] = np.zeros(nl) # zero out phase shifters
|
275
|
+
if alg == 3: # if BX method
|
276
|
+
temp_branch[:, IDX.branch.BR_R] = np.zeros(nl) # zero out line resistance
|
277
|
+
Bpp = -1 * makeYbus(baseMVA, bus, temp_branch)[0].imag
|
278
|
+
|
279
|
+
return Bp, Bpp
|
280
|
+
|
281
|
+
|
282
|
+
def makeBdc(baseMVA, bus, branch):
|
283
|
+
"""
|
284
|
+
Returns the B matrices and phase shift injection vectors needed for a
|
285
|
+
DC power flow.
|
286
|
+
The bus real power injections are related to bus voltage angles by::
|
287
|
+
|
288
|
+
P = Bbus * Va + PBusinj
|
289
|
+
|
290
|
+
The real power flows at the from end the lines are related to the bus
|
291
|
+
voltage angles by::
|
292
|
+
|
293
|
+
Pf = Bf * Va + Pfinj
|
294
|
+
|
295
|
+
Parameters
|
296
|
+
----------
|
297
|
+
baseMVA: float
|
298
|
+
The system base MVA.
|
299
|
+
bus: np.ndarray
|
300
|
+
The bus matrix.
|
301
|
+
branch: np.ndarray
|
302
|
+
The branch matrix.
|
303
|
+
|
304
|
+
Returns
|
305
|
+
-------
|
306
|
+
Bbus: np.ndarray
|
307
|
+
The B matrix.
|
308
|
+
Bf: np.ndarray
|
309
|
+
The Bf matrix.
|
310
|
+
PBusinj: np.ndarray
|
311
|
+
The real power injection vector at each bus.
|
312
|
+
Pfinj: np.ndarray
|
313
|
+
The real power injection vector at the "from" end of each branch.
|
314
|
+
Qfinj: np.ndarray
|
315
|
+
The reactive power injection vector at the "from" end of each branch.
|
316
|
+
"""
|
317
|
+
# constants
|
318
|
+
nb = bus.shape[0] # number of buses
|
319
|
+
nl = branch.shape[0] # number of lines
|
320
|
+
|
321
|
+
# check that bus numbers are equal to indices to bus (one set of bus nums)
|
322
|
+
if np.any(bus[:, IDX.bus.BUS_I] != list(range(nb))):
|
323
|
+
logger.debug('makeBdc: buses must be numbered consecutively in '
|
324
|
+
'bus matrix\n')
|
325
|
+
|
326
|
+
# for each branch, compute the elements of the branch B matrix and the phase
|
327
|
+
# shift "quiescent" injections, where
|
328
|
+
##
|
329
|
+
# | Pf | | Bff Bft | | Vaf | | Pfinj |
|
330
|
+
# | | = | | * | | + | |
|
331
|
+
# | Pt | | Btf Btt | | Vat | | Ptinj |
|
332
|
+
##
|
333
|
+
stat = branch[:, IDX.branch.BR_STATUS] # ones at in-service branches
|
334
|
+
b = stat / branch[:, IDX.branch.BR_X] # series susceptance
|
335
|
+
tap = np.ones(nl) # default tap ratio = 1
|
336
|
+
i = find(branch[:, IDX.branch.TAP]) # indices of non-zero tap ratios
|
337
|
+
tap[i] = branch[i, IDX.branch.TAP] # assign non-zero tap ratios
|
338
|
+
b = b / tap
|
339
|
+
|
340
|
+
# build connection matrix Cft = Cf - Ct for line and from - to buses
|
341
|
+
f = branch[:, IDX.branch.F_BUS] # list of "from" buses
|
342
|
+
t = branch[:, IDX.branch.T_BUS] # list of "to" buses
|
343
|
+
i = np.r_[range(nl), range(nl)] # double set of row indices
|
344
|
+
# connection matrix
|
345
|
+
Cft = c_sparse((np.r_[np.ones(nl), -np.ones(nl)], (i, np.r_[f, t])), (nl, nb))
|
346
|
+
|
347
|
+
# build Bf such that Bf * Va is the vector of real branch powers injected
|
348
|
+
# at each branch's "from" bus
|
349
|
+
Bf = c_sparse((np.r_[b, -b], (i, np.r_[f, t])), shape=(nl, nb)) # = spdiags(b, 0, nl, nl) * Cft
|
350
|
+
|
351
|
+
# build Bbus
|
352
|
+
Bbus = Cft.T * Bf
|
353
|
+
|
354
|
+
# build phase shift injection vectors
|
355
|
+
Pfinj = b * (-branch[:, IDX.branch.SHIFT] * deg2rad) # injected at the from bus ...
|
356
|
+
# Ptinj = -Pfinj ## and extracted at the to bus
|
357
|
+
Pbusinj = Cft.T * Pfinj # Pbusinj = Cf * Pfinj + Ct * Ptinj
|
358
|
+
|
359
|
+
return Bbus, Bf, Pbusinj, Pfinj, Cft
|
360
|
+
|
361
|
+
|
362
|
+
def makeLODF(branch, PTDF):
|
363
|
+
"""
|
364
|
+
Builds the line outage distribution factor matrix.
|
365
|
+
|
366
|
+
Parameters
|
367
|
+
----------
|
368
|
+
branch: np.ndarray
|
369
|
+
The branch matrix.
|
370
|
+
PTDF: np.ndarray
|
371
|
+
The PTDF matrix.
|
372
|
+
|
373
|
+
Returns
|
374
|
+
-------
|
375
|
+
LODF: np.ndarray
|
376
|
+
The DC line outage distribution factor matrix for a given PTDF.
|
377
|
+
The matrix is C{nbr x nbr}, where C{nbr} is the number of branches.
|
378
|
+
|
379
|
+
Example
|
380
|
+
-------
|
381
|
+
>>> H = makePTDF(baseMVA, bus, branch)
|
382
|
+
>>> LODF = makeLODF(branch, H)
|
383
|
+
"""
|
384
|
+
nl, nb = PTDF.shape
|
385
|
+
f = branch[:, IDX.branch.F_BUS]
|
386
|
+
t = branch[:, IDX.branch.T_BUS]
|
387
|
+
Cft = c_sparse((np.r_[np.ones(nl), -np.ones(nl)],
|
388
|
+
(np.r_[f, t], np.r_[np.arange(nl), np.arange(nl)])), (nb, nl))
|
389
|
+
|
390
|
+
H = PTDF * Cft
|
391
|
+
h = np.diag(H, 0)
|
392
|
+
LODF = H / (np.ones((nl, nl)) - np.ones((nl, 1)) * h.T)
|
393
|
+
LODF = LODF - np.diag(np.diag(LODF)) - np.eye(nl, nl)
|
394
|
+
|
395
|
+
return LODF
|
396
|
+
|
397
|
+
|
398
|
+
def makePTDF(baseMVA, bus, branch, slack=None):
|
399
|
+
"""
|
400
|
+
Builds the DC PTDF matrix for a given choice of slack.
|
401
|
+
|
402
|
+
Parameters
|
403
|
+
----------
|
404
|
+
baseMVA: float
|
405
|
+
The system base MVA.
|
406
|
+
bus: np.ndarray
|
407
|
+
The bus matrix.
|
408
|
+
branch: np.ndarray
|
409
|
+
The branch matrix.
|
410
|
+
slack : int, array, or matrix, optional
|
411
|
+
The slack bus number or the slack bus weight vector. The default is
|
412
|
+
to use the reference bus.
|
413
|
+
|
414
|
+
Returns
|
415
|
+
-------
|
416
|
+
H: np.ndarray
|
417
|
+
The DC PTDF matrix for a given choice of slack.
|
418
|
+
|
419
|
+
The matrix is C{nbr x nb}, where C{nbr} is the number of branches
|
420
|
+
and C{nb} is the number of buses.
|
421
|
+
|
422
|
+
The C{slack} can be a scalar (single slack bus) or an C{nb x 1} column
|
423
|
+
vector of weights specifying the proportion of the slack taken up at each bus.
|
424
|
+
|
425
|
+
If the C{slack} is not specified the reference bus is used by default.
|
426
|
+
|
427
|
+
For convenience, C{slack} can also be an C{nb x nb} matrix, where each
|
428
|
+
column specifies how the slack should be handled for injections at that bus.
|
429
|
+
"""
|
430
|
+
# use reference bus for slack by default
|
431
|
+
if slack is None:
|
432
|
+
slack = find(bus[:, IDX.bus.BUS_TYPE] == IDX.bus.REF)
|
433
|
+
slack = slack[0]
|
434
|
+
|
435
|
+
# set the slack bus to be used to compute initial PTDF
|
436
|
+
if np.isscalar(slack):
|
437
|
+
slack_bus = slack
|
438
|
+
else:
|
439
|
+
slack_bus = 0 # use bus 1 for temp slack bus
|
440
|
+
|
441
|
+
nb = bus.shape[0]
|
442
|
+
nbr = branch.shape[0]
|
443
|
+
noref = np.arange(1, nb) # use bus 1 for voltage angle reference
|
444
|
+
noslack = find(np.arange(nb) != slack_bus)
|
445
|
+
|
446
|
+
# check that bus numbers are equal to indices to bus (one set of bus numbers)
|
447
|
+
if np.any(bus[:, IDX.bus.BUS_I] != np.arange(nb)):
|
448
|
+
logger.debug('makePTDF: buses must be numbered consecutively')
|
449
|
+
|
450
|
+
# compute PTDF for single slack_bus
|
451
|
+
Bbus, Bf, _, _, _ = makeBdc(baseMVA, bus, branch)
|
452
|
+
Bbus, Bf = Bbus.todense(), Bf.todense()
|
453
|
+
H = np.zeros((nbr, nb))
|
454
|
+
H[:, noslack] = np.linalg.solve(Bbus[np.ix_(noslack, noref)].T, Bf[:, noref].T).T
|
455
|
+
# = Bf[:, noref] * inv(Bbus[np.ix_(noslack, noref)])
|
456
|
+
|
457
|
+
# distribute slack, if requested
|
458
|
+
if not np.isscalar(slack):
|
459
|
+
if len(slack.shape) == 1: # slack is a vector of weights
|
460
|
+
slack = slack / sum(slack) # normalize weights
|
461
|
+
|
462
|
+
# conceptually, we want to do ...
|
463
|
+
# H = H * (eye(nb, nb) - slack * ones((1, nb)))
|
464
|
+
# ... we just do it more efficiently
|
465
|
+
v = np.dot(H, slack)
|
466
|
+
for k in range(nb):
|
467
|
+
H[:, k] = H[:, k] - v
|
468
|
+
else:
|
469
|
+
H = np.dot(H, slack)
|
470
|
+
|
471
|
+
return H
|
472
|
+
|
473
|
+
|
474
|
+
def makeSbus(baseMVA, bus, gen):
|
475
|
+
"""
|
476
|
+
Builds the vector of complex bus power injections.
|
477
|
+
|
478
|
+
Parameters
|
479
|
+
----------
|
480
|
+
baseMVA: float
|
481
|
+
Base MVA.
|
482
|
+
bus: NumPy.array
|
483
|
+
Bus data.
|
484
|
+
gen: NumPy.array
|
485
|
+
Generator data.
|
486
|
+
|
487
|
+
Returns
|
488
|
+
-------
|
489
|
+
Sbus : NumPy.array
|
490
|
+
Complex bus power injections.
|
491
|
+
"""
|
492
|
+
# generator info
|
493
|
+
on = find(gen[:, IDX.gen.GEN_STATUS] > 0) # which generators are on?
|
494
|
+
gbus = gen[on, IDX.gen.GEN_BUS] # what buses are they at?
|
495
|
+
|
496
|
+
# form net complex bus power injection vector
|
497
|
+
nb = bus.shape[0]
|
498
|
+
ngon = on.shape[0]
|
499
|
+
# connection matrix, element i, j is 1 if gen on(j) at bus i is ON
|
500
|
+
Cg = c_sparse((np.ones(ngon), (gbus, range(ngon))), (nb, ngon))
|
501
|
+
|
502
|
+
# power injected by gens plus power injected by loads converted to p.u.
|
503
|
+
Sbus = (Cg * (gen[on, IDX.gen.PG] + 1j * gen[on, IDX.gen.QG]) -
|
504
|
+
(bus[:, IDX.bus.PD] + 1j * bus[:, IDX.bus.QD])) / baseMVA
|
505
|
+
|
506
|
+
return Sbus
|
507
|
+
|
508
|
+
|
509
|
+
def makeYbus(baseMVA, bus, branch):
|
510
|
+
"""Builds the bus admittance matrix and branch admittance matrices.
|
511
|
+
|
512
|
+
Returns the full bus admittance matrix (i.e. for all buses) and the
|
513
|
+
matrices C{Yf} and C{Yt} which, when multiplied by a complex voltage
|
514
|
+
vector, yield the vector currents injected into each line from the
|
515
|
+
"from" and "to" buses respectively of each line. Does appropriate
|
516
|
+
conversions to p.u.
|
517
|
+
|
518
|
+
@see: L{makeSbus}
|
519
|
+
|
520
|
+
@author: Ray Zimmerman (PSERC Cornell)
|
521
|
+
"""
|
522
|
+
# constants
|
523
|
+
nb = bus.shape[0] # number of buses
|
524
|
+
nl = branch.shape[0] # number of lines
|
525
|
+
|
526
|
+
# check that bus numbers are equal to indices to bus (one set of bus nums)
|
527
|
+
if np.any(bus[:, IDX.bus.BUS_I] != list(range(nb))):
|
528
|
+
logger.debug('buses must appear in order by bus number\n')
|
529
|
+
|
530
|
+
# for each branch, compute the elements of the branch admittance matrix where
|
531
|
+
##
|
532
|
+
# | If | | Yff Yft | | Vf |
|
533
|
+
# | | = | | * | |
|
534
|
+
# | It | | Ytf Ytt | | Vt |
|
535
|
+
##
|
536
|
+
stat = branch[:, IDX.branch.BR_STATUS] # ones at in-service branches
|
537
|
+
Ys = stat / (branch[:, IDX.branch.BR_R] + 1j * branch[:, IDX.branch.BR_X]) # series admittance
|
538
|
+
Bc = stat * branch[:, IDX.branch.BR_B] # line charging susceptance
|
539
|
+
tap = np.ones(nl) # default tap ratio = 1
|
540
|
+
i = np.nonzero(branch[:, IDX.branch.TAP]) # indices of non-zero tap ratios
|
541
|
+
tap[i] = branch[i, IDX.branch.TAP] # assign non-zero tap ratios
|
542
|
+
tap = tap * np.exp(1j * deg2rad * branch[:, IDX.branch.SHIFT]) # add phase shifters
|
543
|
+
|
544
|
+
Ytt = Ys + 1j * Bc / 2
|
545
|
+
Yff = Ytt / (tap * np.conj(tap))
|
546
|
+
Yft = - Ys / np.conj(tap)
|
547
|
+
Ytf = - Ys / tap
|
548
|
+
|
549
|
+
# compute shunt admittance
|
550
|
+
# if Psh is the real power consumed by the shunt at V = 1.0 p.u.
|
551
|
+
# and Qsh is the reactive power injected by the shunt at V = 1.0 p.u.
|
552
|
+
# then Psh - j Qsh = V * conj(Ysh * V) = conj(Ysh) = Gs - j Bs,
|
553
|
+
# i.e. Ysh = Psh + j Qsh, so ...
|
554
|
+
# vector of shunt admittances
|
555
|
+
Ysh = (bus[:, IDX.bus.GS] + 1j * bus[:, IDX.bus.BS]) / baseMVA
|
556
|
+
|
557
|
+
# build connection matrices
|
558
|
+
f = branch[:, IDX.branch.F_BUS] # list of "from" buses
|
559
|
+
t = branch[:, IDX.branch.T_BUS] # list of "to" buses
|
560
|
+
# connection matrix for line & from buses
|
561
|
+
Cf = c_sparse((np.ones(nl), (range(nl), f)), (nl, nb))
|
562
|
+
# connection matrix for line & to buses
|
563
|
+
Ct = c_sparse((np.ones(nl), (range(nl), t)), (nl, nb))
|
564
|
+
|
565
|
+
# build Yf and Yt such that Yf * V is the vector of complex branch currents injected
|
566
|
+
# at each branch's "from" bus, and Yt is the same for the "to" bus end
|
567
|
+
i = np.r_[range(nl), range(nl)] # double set of row indices
|
568
|
+
|
569
|
+
Yf = c_sparse((np.r_[Yff, Yft], (i, np.r_[f, t])), (nl, nb))
|
570
|
+
Yt = c_sparse((np.r_[Ytf, Ytt], (i, np.r_[f, t])), (nl, nb))
|
571
|
+
# Yf = spdiags(Yff, 0, nl, nl) * Cf + spdiags(Yft, 0, nl, nl) * Ct
|
572
|
+
# Yt = spdiags(Ytf, 0, nl, nl) * Cf + spdiags(Ytt, 0, nl, nl) * Ct
|
573
|
+
|
574
|
+
# build Ybus
|
575
|
+
Ybus = Cf.T * Yf + Ct.T * Yt + \
|
576
|
+
c_sparse((Ysh, (range(nb), range(nb))), (nb, nb))
|
577
|
+
|
578
|
+
return Ybus, Yf, Yt
|
579
|
+
|
580
|
+
|
581
|
+
def makeAy(baseMVA, ng, gencost, pgbas, qgbas, ybas):
|
582
|
+
"""Make the A matrix and RHS for the CCV formulation.
|
583
|
+
|
584
|
+
Constructs the parameters for linear "basin constraints" on C{Pg}, C{Qg}
|
585
|
+
and C{Y} used by the CCV cost formulation, expressed as::
|
586
|
+
|
587
|
+
Ay * x <= by
|
588
|
+
|
589
|
+
where C{x} is the vector of optimization variables. The starting index
|
590
|
+
within the C{x} vector for the active, reactive sources and the C{y}
|
591
|
+
variables should be provided in arguments C{pgbas}, C{qgbas}, C{ybas}.
|
592
|
+
The number of generators is C{ng}.
|
593
|
+
|
594
|
+
Assumptions: All generators are in-service. Filter any generators
|
595
|
+
that are offline from the C{gencost} matrix before calling L{makeAy}.
|
596
|
+
Efficiency depends on C{Qg} variables being after C{Pg} variables, and
|
597
|
+
the C{y} variables must be the last variables within the vector C{x} for
|
598
|
+
the dimensions of the resulting C{Ay} to be conformable with C{x}.
|
599
|
+
|
600
|
+
@author: Carlos E. Murillo-Sanchez (PSERC Cornell & Universidad
|
601
|
+
Autonoma de Manizales)
|
602
|
+
"""
|
603
|
+
# find all pwl cost rows in gencost, either real or reactive
|
604
|
+
iycost = find(gencost[:, IDX.cost.MODEL] == IDX.cost.PW_LINEAR)
|
605
|
+
|
606
|
+
# this is the number of extra "y" variables needed to model those costs
|
607
|
+
ny = iycost.shape[0]
|
608
|
+
|
609
|
+
if ny == 0:
|
610
|
+
Ay = np.zeros((0, ybas + ny - 1)) # TODO: Check size (- 1)
|
611
|
+
by = np.array([])
|
612
|
+
return Ay, by
|
613
|
+
|
614
|
+
# if p(i),p(i+1),c(i),c(i+1) define one of the cost segments, then
|
615
|
+
# the corresponding constraint on Pg (or Qg) and Y is
|
616
|
+
# c(i+1) - c(i)
|
617
|
+
# Y >= c(i) + m * (Pg - p(i)), m = ---------------
|
618
|
+
# p(i+1) - p(i)
|
619
|
+
##
|
620
|
+
# this becomes m * Pg - Y <= m*p(i) - c(i)
|
621
|
+
|
622
|
+
# Form A matrix. Use two different loops, one for the PG/Qg coefs,
|
623
|
+
# then another for the y coefs so that everything is filled in the
|
624
|
+
# same order as the compressed column sparse format used by matlab
|
625
|
+
# this should be the quickest.
|
626
|
+
|
627
|
+
m = sum(gencost[iycost, IDX.cost.NCOST].astype(int)) # total number of cost points
|
628
|
+
Ay = l_sparse((m - ny, ybas + ny - 1))
|
629
|
+
by = np.array([])
|
630
|
+
# First fill the Pg or Qg coefficients (since their columns come first)
|
631
|
+
# and the rhs
|
632
|
+
k = 0
|
633
|
+
for i in iycost:
|
634
|
+
ns = gencost[i, IDX.cost.NCOST].astype(int) # of cost points segments = ns-1
|
635
|
+
p = gencost[i, IDX.cost.COST:IDX.cost.COST + 2 * ns - 1:2] / baseMVA
|
636
|
+
c = gencost[i, IDX.cost.COST + 1:IDX.cost.COST + 2 * ns:2]
|
637
|
+
m = np.diff(c) / np.diff(p) # slopes for Pg (or Qg)
|
638
|
+
if np.any(np.diff(p) == 0):
|
639
|
+
print('makeAy: bad x axis data in row ##i of gencost matrix' % i)
|
640
|
+
b = m * p[:ns - 1] - c[:ns - 1] # and rhs
|
641
|
+
by = np.r_[by, b]
|
642
|
+
if i > ng:
|
643
|
+
sidx = qgbas + (i - ng) - 1 # this was for a q cost
|
644
|
+
else:
|
645
|
+
sidx = pgbas + i - 1 # this was for a p cost
|
646
|
+
|
647
|
+
# FIXME: Bug in SciPy 0.7.2 prevents setting with a sequence
|
648
|
+
# Ay[k:k + ns - 1, sidx] = m
|
649
|
+
for ii, kk in enumerate(range(k, k + ns - 1)):
|
650
|
+
Ay[kk, sidx] = m[ii]
|
651
|
+
|
652
|
+
k = k + ns - 1
|
653
|
+
# Now fill the y columns with -1's
|
654
|
+
k = 0
|
655
|
+
j = 0
|
656
|
+
for i in iycost:
|
657
|
+
ns = gencost[i, IDX.cost.NCOST].astype(int)
|
658
|
+
# FIXME: Bug in SciPy 0.7.2 prevents setting with a sequence
|
659
|
+
# Ay[k:k + ns - 1, ybas + j - 1] = -ones(ns - 1)
|
660
|
+
for kk in range(k, k + ns - 1):
|
661
|
+
Ay[kk, ybas + j - 1] = -1
|
662
|
+
k = k + ns - 1
|
663
|
+
j = j + 1
|
664
|
+
|
665
|
+
return Ay.tocsr(), by
|