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