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
ams/pypower/toggle.py ADDED
@@ -0,0 +1,1098 @@
1
+ """
2
+ PYPOWER module for toggling OPF elements.
3
+ """
4
+ import logging
5
+
6
+ import numpy as np
7
+ from numpy import flatnonzero as find
8
+
9
+ import scipy.sparse as sp
10
+ from scipy.sparse import csr_matrix as c_sparse
11
+ from scipy.sparse import lil_matrix as l_sparse
12
+
13
+ from ams.shared import inf
14
+ from ams.pypower.make import makeBdc
15
+ from ams.pypower.idx import IDX
16
+ from ams.pypower.routines.opffcns import (add_userfcn, remove_userfcn,
17
+ e2i_field, i2e_field, i2e_data,
18
+ isload)
19
+
20
+ from pprint import pprint
21
+
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ def toggle_iflims(ppc, on_off):
27
+ """
28
+ Enable or disable set of interface flow constraints.
29
+
30
+ Enables or disables a set of OPF userfcn callbacks to implement
31
+ interface flow limits based on a DC flow model.
32
+
33
+ These callbacks expect to find an 'if' field in the input C{ppc}, where
34
+ C{ppc['if']} is a dict with the following fields:
35
+ - C{map} C{n x 2}, defines each interface in terms of a set of
36
+ branch indices and directions. Interface I is defined
37
+ by the set of rows whose 1st col is equal to I. The
38
+ 2nd column is a branch index multiplied by 1 or -1
39
+ respectively for lines whose orientation is the same
40
+ as or opposite to that of the interface.
41
+ - C{lims} C{nif x 3}, defines the DC model flow limits in MW
42
+ for specified interfaces. The 2nd and 3rd columns specify
43
+ the lower and upper limits on the (DC model) flow
44
+ across the interface, respectively. Normally, the lower
45
+ limit is negative, indicating a flow in the opposite
46
+ direction.
47
+
48
+ The 'int2ext' callback also packages up results and stores them in
49
+ the following output fields of C{results['if']}:
50
+ - C{P} - C{nif x 1}, actual flow across each interface in MW
51
+ - C{mu.l} - C{nif x 1}, shadow price on lower flow limit, ($/MW)
52
+ - C{mu.u} - C{nif x 1}, shadow price on upper flow limit, ($/MW)
53
+
54
+ @see: L{add_userfcn}, L{remove_userfcn}, L{run_userfcn},
55
+ L{t.t_case30_userfcns}.
56
+
57
+ @author: Ray Zimmerman (PSERC Cornell)
58
+ """
59
+ if on_off == 'on':
60
+ # check for proper reserve inputs
61
+ if ('if' not in ppc) | (not isinstance(ppc['if'], dict)) | \
62
+ ('map' not in ppc['if']) | \
63
+ ('lims' not in ppc['if']):
64
+ logger.debug('toggle_iflims: case must contain an \'if\' field, a struct defining \'map\' and \'lims\'')
65
+
66
+ # add callback functions
67
+ # note: assumes all necessary data included in 1st arg (ppc, om, results)
68
+ # so, no additional explicit args are needed
69
+ ppc = add_userfcn(ppc, 'ext2int', userfcn_iflims_ext2int)
70
+ ppc = add_userfcn(ppc, 'formulation', userfcn_iflims_formulation)
71
+ ppc = add_userfcn(ppc, 'int2ext', userfcn_iflims_int2ext)
72
+ ppc = add_userfcn(ppc, 'printpf', userfcn_iflims_printpf)
73
+ ppc = add_userfcn(ppc, 'savecase', userfcn_iflims_savecase)
74
+ elif on_off == 'off':
75
+ ppc = remove_userfcn(ppc, 'savecase', userfcn_iflims_savecase)
76
+ ppc = remove_userfcn(ppc, 'printpf', userfcn_iflims_printpf)
77
+ ppc = remove_userfcn(ppc, 'int2ext', userfcn_iflims_int2ext)
78
+ ppc = remove_userfcn(ppc, 'formulation', userfcn_iflims_formulation)
79
+ ppc = remove_userfcn(ppc, 'ext2int', userfcn_iflims_ext2int)
80
+ else:
81
+ logger.debug('toggle_iflims: 2nd argument must be either \'on\' or \'off\'')
82
+
83
+ return ppc
84
+
85
+
86
+ def userfcn_iflims_ext2int(ppc, *args):
87
+ """
88
+ This is the 'ext2int' stage userfcn callback that prepares the input
89
+ data for the formulation stage. It expects to find an 'if' field in
90
+ ppc as described above. The optional args are not currently used.
91
+ """
92
+ # initialize some things
93
+ ifmap = ppc['if']['map']
94
+ o = ppc['order']
95
+ nl0 = o['ext']['branch'].shape[0] # original number of branches
96
+ nl = ppc['branch'].shape[0] # number of on-line branches
97
+
98
+ # save if.map for external indexing
99
+ ppc['order']['ext']['ifmap'] = ifmap
100
+
101
+ # ----- convert stuff to internal indexing -----
102
+ e2i = np.zeros(nl0)
103
+ e2i[o['branch']['status']['on']] = np.arange(nl) # ext->int branch index mapping
104
+ d = np.sign(ifmap[:, 1])
105
+ br = abs(ifmap[:, 1]).astype(int)
106
+ ifmap[:, 1] = d * e2i[br]
107
+
108
+ ifmap = np.delete(ifmap, find(ifmap[:, 1] == 0), 0) # delete branches that are out
109
+
110
+ ppc['if']['map'] = ifmap
111
+
112
+ return ppc
113
+
114
+
115
+ def userfcn_iflims_formulation(om, *args):
116
+ """
117
+ This is the 'formulation' stage userfcn callback that defines the
118
+ user costs and constraints for interface flow limits. It expects to
119
+ find an 'if' field in the ppc stored in om, as described above. The
120
+ optional args are not currently used.
121
+ """
122
+ # initialize some things
123
+ ppc = om.get_ppc()
124
+ baseMVA, bus, branch = ppc['baseMVA'], ppc['bus'], ppc['branch']
125
+ ifmap = ppc['if']['map']
126
+ iflims = ppc['if']['lims']
127
+
128
+ # form B matrices for DC model
129
+ _, Bf, _, Pfinj = makeBdc(baseMVA, bus, branch)
130
+ n = Bf.shape[1] # dim of theta
131
+
132
+ # form constraints
133
+ ifidx = np.unique(iflims[:, 0]) # interface number list
134
+ nifs = len(ifidx) # number of interfaces
135
+ Aif = l_sparse((nifs, n))
136
+ lif = np.zeros(nifs)
137
+ uif = np.zeros(nifs)
138
+ for k in range(nifs):
139
+ # extract branch indices
140
+ br = ifmap[ifmap[:, 0] == ifidx[k], 1]
141
+ if len(br) == 0:
142
+ logger.debug('userfcn_iflims_formulation: interface %d has no in-service branches\n' % k)
143
+
144
+ d = np.sign(br)
145
+ br = abs(br)
146
+ Ak = c_sparse((1, n)) # Ak = sum( d(i) * Bf(i, :) )
147
+ bk = 0 # bk = sum( d(i) * Pfinj(i) )
148
+ for i in range(len(br)):
149
+ Ak = Ak + d[i] * Bf[br[i], :]
150
+ bk = bk + d[i] * Pfinj[br[i]]
151
+
152
+ Aif[k, :] = Ak
153
+ lif[k] = iflims[k, 1] / baseMVA - bk
154
+ uif[k] = iflims[k, 2] / baseMVA - bk
155
+
156
+ # add interface constraint
157
+ om.add_constraints('iflims', Aif, lif, uif, ['Va']) # nifs
158
+
159
+ return om
160
+
161
+
162
+ def userfcn_iflims_int2ext(results, *args):
163
+ """
164
+ This is the 'int2ext' stage userfcn callback that converts everything
165
+ back to external indexing and packages up the results. It expects to
166
+ find an 'if' field in the C{results} dict as described for ppc above.
167
+ It also expects the results to contain solved branch flows and linear
168
+ constraints named 'iflims' which are used to populate output fields
169
+ in C{results['if']}. The optional args are not currently used.
170
+ """
171
+ # get internal ifmap
172
+ ifmap = results['if']['map']
173
+ iflims = results['if']['lims']
174
+
175
+ # ----- convert stuff back to external indexing -----
176
+ results['if']['map'] = results['order']['ext']['ifmap']
177
+
178
+ # ----- results post-processing -----
179
+ ifidx = np.unique(iflims[:, 0]) # interface number list
180
+ nifs = len(ifidx) # number of interfaces
181
+ results['if']['P'] = np.zeros(nifs)
182
+ for k in range(nifs):
183
+ # extract branch indices
184
+ br = ifmap[ifmap[:, 0] == ifidx[k], 1]
185
+ d = np.sign(br)
186
+ br = abs(br)
187
+ results['if']['P'][k] = sum(d * results['branch'][br, IDX.branch.PF])
188
+
189
+ if 'mu' not in results['if']:
190
+ results['if']['mu'] = {}
191
+ results['if']['mu']['l'] = results['lin']['mu']['l']['iflims']
192
+ results['if']['mu']['u'] = results['lin']['mu']['u']['iflims']
193
+
194
+ return results
195
+
196
+
197
+ def userfcn_iflims_printpf(results, fd, ppopt, *args):
198
+ """
199
+ This is the 'printpf' stage userfcn callback that pretty-prints the
200
+ results. It expects a C{results} dict, a file descriptor and a PYPOWER
201
+ options vector. The optional args are not currently used.
202
+ """
203
+ # ----- print results -----
204
+ OUT_ALL = ppopt['OUT_ALL']
205
+ # ctol = ppopt['OPF_VIOLATION'] ## constraint violation tolerance
206
+ ptol = 1e-6 # tolerance for displaying shadow prices
207
+
208
+ if OUT_ALL != 0:
209
+ iflims = results['if']['lims']
210
+ fd.write('\n================================================================================')
211
+ fd.write('\n| Interface Flow Limits |')
212
+ fd.write('\n================================================================================')
213
+ fd.write('\n Interface Shadow Prc Lower Lim Flow Upper Lim Shadow Prc')
214
+ fd.write('\n # ($/MW) (MW) (MW) (MW) ($/MW) ')
215
+ fd.write('\n---------- ---------- ---------- ---------- ---------- -----------')
216
+ ifidx = np.unique(iflims[:, 0]) # interface number list
217
+ nifs = len(ifidx) # number of interfaces
218
+ for k in range(nifs):
219
+ fd.write('\n%6d ', iflims(k, 1))
220
+ if results['if']['mu']['l'][k] > ptol:
221
+ fd.write('%14.3f' % results['if']['mu']['l'][k])
222
+ else:
223
+ fd.write(' - ')
224
+
225
+ fd.write('%12.2f%12.2f%12.2f' % (iflims[k, 1], results['if']['P'][k], iflims[k, 2]))
226
+ if results['if']['mu']['u'][k] > ptol:
227
+ fd.write('%13.3f' % results['if']['mu']['u'][k])
228
+ else:
229
+ fd.write(' - ')
230
+
231
+ fd.write('\n')
232
+
233
+ return results
234
+
235
+
236
+ def userfcn_iflims_savecase(ppc, fd, prefix, *args):
237
+ """
238
+ This is the 'savecase' stage userfcn callback that prints the Python
239
+ file code to save the 'if' field in the case file. It expects a
240
+ PYPOWER case dict (ppc), a file descriptor and variable prefix
241
+ (usually 'ppc'). The optional args are not currently used.
242
+ """
243
+ ifmap = ppc['if']['map']
244
+ iflims = ppc['if']['lims']
245
+
246
+ fd.write('\n####----- Interface Flow Limit Data -----####\n')
247
+ fd.write('#### interface<->branch map data\n')
248
+ fd.write('##\tifnum\tbranchidx (negative defines opposite direction)\n')
249
+ fd.write('%sif.map = [\n' % prefix)
250
+ fd.write('\t%d\t%d;\n' % ifmap.T)
251
+ fd.write('];\n')
252
+
253
+ fd.write('\n#### interface flow limit data (based on DC model)\n')
254
+ fd.write('#### (lower limit should be negative for opposite direction)\n')
255
+ fd.write('##\tifnum\tlower\tupper\n')
256
+ fd.write('%sif.lims = [\n' % prefix)
257
+ fd.write('\t%d\t%g\t%g;\n' % iflims.T)
258
+ fd.write('];\n')
259
+
260
+ # save output fields for solved case
261
+ if ('P' in ppc['if']):
262
+ fd.write('\n#### solved values\n')
263
+ fd.write('%sif.P = %s\n' % (prefix, pprint(ppc['if']['P'])))
264
+ fd.write('%sif.mu.l = %s\n' % (prefix, pprint(ppc['if']['mu']['l'])))
265
+ fd.write('%sif.mu.u = %s\n' % (prefix, pprint(ppc['if']['mu']['u'])))
266
+
267
+ return ppc
268
+
269
+
270
+ def toggle_dcline(ppc, on_off):
271
+ """
272
+ Enable or disable DC line modeling.
273
+
274
+ Enables or disables a set of OPF userfcn callbacks to implement
275
+ DC lines as a pair of linked generators. While it uses the OPF
276
+ extension mechanism, this implementation works for simple power
277
+ flow as well as OPF problems.
278
+
279
+ These callbacks expect to find a 'dcline' field in the input MPC,
280
+ where MPC.dcline is an ndc x 17 matrix with columns as defined
281
+ in IDX_DCLINE, where ndc is the number of DC lines.
282
+
283
+ The 'int2ext' callback also packages up flow results and stores them
284
+ in appropriate columns of MPC.dcline.
285
+
286
+ NOTE: Because of the way this extension modifies the number of
287
+ rows in the gen and gencost matrices, caution must be taken
288
+ when using it with other extensions that deal with generators.
289
+
290
+ Examples:
291
+ ppc = loadcase('t_case9_dcline')
292
+ ppc = toggle_dcline(ppc, 'on')
293
+ results1 = runpf(ppc)
294
+ results2 = runopf(ppc)
295
+
296
+ @see: L{idx_dcline}, L{add_userfcn}, L{remove_userfcn}, L{run_userfcn}.
297
+ """
298
+ if on_off == 'on':
299
+
300
+ # check for proper input data
301
+
302
+ if 'dcline' not in ppc or ppc['dcline'].shape[1] < IDX.dcline.LOSS1 + 1:
303
+ raise ValueError('toggle_dcline: case must contain a '
304
+ '\'dcline\' field, an ndc x %d matrix.', IDX.dcline.LOSS1)
305
+
306
+ if 'dclinecost' in ppc and ppc['dcline'].shape[0] != ppc['dclinecost'].shape[0]:
307
+ raise ValueError('toggle_dcline: number of rows in \'dcline\''
308
+ ' field (%d) and \'dclinecost\' field (%d) do not match.' %
309
+ (ppc['dcline'].shape[0], ppc['dclinecost'].shape[0]))
310
+
311
+ k = find(ppc['dcline'][:, IDX.dcline.LOSS1] < 0)
312
+ if len(k) > 0:
313
+ logger.warning('toggle_dcline: linear loss term is negative for DC line '
314
+ 'from bus %d to %d\n' %
315
+ ppc['dcline'][k, IDX.dcline.F_BUS:IDX.dcline.T_BUS + 1].T)
316
+
317
+ # add callback functions
318
+ # note: assumes all necessary data included in 1st arg (ppc, om, results)
319
+ # so, no additional explicit args are needed
320
+ ppc = add_userfcn(ppc, 'ext2int', userfcn_dcline_ext2int)
321
+ ppc = add_userfcn(ppc, 'formulation', userfcn_dcline_formulation)
322
+ ppc = add_userfcn(ppc, 'int2ext', userfcn_dcline_int2ext)
323
+ ppc = add_userfcn(ppc, 'printpf', userfcn_dcline_printpf)
324
+ ppc = add_userfcn(ppc, 'savecase', userfcn_dcline_savecase)
325
+ elif on_off == 'off':
326
+ ppc = remove_userfcn(ppc, 'savecase', userfcn_dcline_savecase)
327
+ ppc = remove_userfcn(ppc, 'printpf', userfcn_dcline_printpf)
328
+ ppc = remove_userfcn(ppc, 'int2ext', userfcn_dcline_int2ext)
329
+ ppc = remove_userfcn(ppc, 'formulation', userfcn_dcline_formulation)
330
+ ppc = remove_userfcn(ppc, 'ext2int', userfcn_dcline_ext2int)
331
+ else:
332
+ raise ValueError('toggle_dcline: 2nd argument must be either '
333
+ '\'on\' or \'off\'')
334
+
335
+ return ppc
336
+
337
+
338
+ # ----- ext2int ------------------------------------------------------
339
+ def userfcn_dcline_ext2int(ppc, args):
340
+ """This is the 'ext2int' stage userfcn callback that prepares the input
341
+ data for the formulation stage. It expects to find a 'dcline' field
342
+ in ppc as described above. The optional args are not currently used.
343
+ It adds two dummy generators for each in-service DC line, with the
344
+ appropriate upper and lower generation bounds and corresponding
345
+ zero-cost entries in gencost.
346
+ """
347
+ # initialize some things
348
+ if 'dclinecost' in ppc:
349
+ havecost = True
350
+ else:
351
+ havecost = False
352
+
353
+ # save version with external indexing
354
+ ppc['order']['ext']['dcline'] = ppc['dcline'] # external indexing
355
+ if havecost:
356
+ ppc['order']['ext']['dclinecost'] = ppc['dclinecost'] # external indexing
357
+
358
+ ppc['order']['ext']['status'] = {}
359
+ # work with only in-service DC lines
360
+ ppc['order']['ext']['status']['on'] = find(ppc['dcline'][:, IDX.dcline.BR_STATUS] > 0)
361
+ ppc['order']['ext']['status']['off'] = find(ppc['dcline'][:, IDX.dcline.BR_STATUS] <= 0)
362
+
363
+ # remove out-of-service DC lines
364
+ dc = ppc['dcline'][ppc['order']['ext']['status']['on'], :] # only in-service DC lines
365
+ if havecost:
366
+ dcc = ppc['dclinecost'][ppc['order']['ext']['status']['on'], :] # only in-service DC lines
367
+ ppc['dclinecost'] = dcc
368
+
369
+ ndc = dc.shape[0] # number of in-service DC lines
370
+ o = ppc['order']
371
+
372
+ # ----- convert stuff to internal indexing -----
373
+ dc[:, IDX.dcline.F_BUS] = o['bus']['e2i'][dc[:, IDX.dcline.F_BUS]]
374
+ dc[:, IDX.dcline.T_BUS] = o['bus']['e2i'][dc[:, IDX.dcline.T_BUS]]
375
+ ppc['dcline'] = dc
376
+
377
+ # ----- create gens to represent DC line terminals -----
378
+ # ensure consistency of initial values of IDX.branch.PF, PT and losses
379
+ # (for simple power flow cases)
380
+ dc[:, IDX.dcline.PT] = dc[:, IDX.dcline.PF] - (dc[:, IDX.dcline.LOSS0] + dc[:,
381
+ IDX.dcline.LOSS1] * dc[:, IDX.dcline.PF])
382
+
383
+ # create gens
384
+ fg = np.zeros((ndc, ppc['gen'].shape[1]))
385
+ fg[:, IDX.gen.MBSAE] = 100
386
+ fg[:, IDX.gen.GEN_STATUS] = dc[:, IDX.dcline.BR_STATUS] # status (should be all 1's)
387
+ fg[:, IDX.gen.PMIN] = -inf
388
+ fg[:, IDX.gen.PMAX] = inf
389
+ tg = fg.copy()
390
+ fg[:, IDX.gen.GEN_BUS] = dc[:, IDX.dcline.F_BUS] # from bus
391
+ tg[:, IDX.gen.GEN_BUS] = dc[:, IDX.dcline.T_BUS] # to bus
392
+ fg[:, IDX.gen.PG] = -dc[:, IDX.dcline.PF] # flow (extracted at "from")
393
+ tg[:, IDX.gen.PG] = dc[:, IDX.dcline.PT] # flow (injected at "to")
394
+ fg[:, IDX.gen.QG] = dc[:, IDX.dcline.QF] # VAr injection at "from"
395
+ tg[:, IDX.gen.QG] = dc[:, IDX.dcline.QT] # VAr injection at "to"
396
+ fg[:, IDX.gen.VG] = dc[:, IDX.dcline.VF] # voltage set-point at "from"
397
+ tg[:, IDX.gen.VG] = dc[:, IDX.dcline.VT] # voltage set-point at "to"
398
+ k = find(dc[:, IDX.dcline.PMIN] >= 0) # min positive direction flow
399
+ if len(k) > 0: # contrain at "from" end
400
+ fg[k, IDX.gen.PMAX] = -dc[k, IDX.dcline.PMIN] # "from" extraction lower lim
401
+
402
+ k = find(dc[:, IDX.dcline.IDX.gen.PMAX] >= 0) # max positive direction flow
403
+ if len(k) > 0: # contrain at "from" end
404
+ fg[k, IDX.gen.PMIN] = -dc[k, IDX.dcline.IDX.gen.PMAX] # "from" extraction upper lim
405
+
406
+ k = find(dc[:, IDX.dcline.PMIN] < 0) # max negative direction flow
407
+ if len(k) > 0: # contrain at "to" end
408
+ tg[k, IDX.gen.PMIN] = dc[k, IDX.dcline.PMIN] # "to" injection lower lim
409
+
410
+ k = find(dc[:, IDX.dcline.IDX.gen.PMAX] < 0) # min negative direction flow
411
+ if len(k) > 0: # contrain at "to" end
412
+ tg[k, IDX.gen.PMAX] = dc[k, IDX.dcline.IDX.gen.PMAX] # "to" injection upper lim
413
+
414
+ fg[:, IDX.gen.QMIN] = dc[:, IDX.dcline.QMINF] # "from" VAr injection lower lim
415
+ fg[:, IDX.gen.QMAX] = dc[:, IDX.dcline.QMAXF] # "from" VAr injection upper lim
416
+ tg[:, IDX.gen.QMIN] = dc[:, IDX.dcline.QMINT] # "to" VAr injection lower lim
417
+ tg[:, IDX.gen.QMAX] = dc[:, IDX.dcline.QMAXT] # "to" VAr injection upper lim
418
+
419
+ # fudge IDX.gen.PMAX a bit if necessary to avoid triggering
420
+ # dispatchable load constant power factor constraints
421
+ fg[isload(fg), IDX.gen.PMAX] = -1e-6
422
+ tg[isload(tg), IDX.gen.PMAX] = -1e-6
423
+
424
+ # set all terminal buses to IDX.bus.PV (except ref bus)
425
+ refbus = find(ppc['bus'][:, IDX.bus.BUS_TYPE] == IDX.bus.REF)
426
+ ppc['bus'][dc[:, IDX.dcline.F_BUS], IDX.bus.BUS_TYPE] = IDX.bus.PV
427
+ ppc['bus'][dc[:, IDX.dcline.T_BUS], IDX.bus.BUS_TYPE] = IDX.bus.PV
428
+ ppc['bus'][refbus, IDX.bus.BUS_TYPE] = IDX.bus.REF
429
+
430
+ # append dummy gens
431
+ ppc['gen'] = np.r_[ppc['gen'], fg, tg]
432
+
433
+ # gencost
434
+ if 'gencost' in ppc and len(ppc['gencost']) > 0:
435
+ ngcr, ngcc = ppc['gencost'].shape # dimensions of gencost
436
+ if havecost: # user has provided costs
437
+ ndccc = dcc.shape[1] # number of dclinecost columns
438
+ ccc = max(np.r_[ngcc, ndccc]) # number of columns in new gencost
439
+ if ccc > ngcc: # right zero-pad gencost
440
+ ppc.gencost = np.c_[ppc['gencost'], np.zeros(ngcr, ccc-ngcc)]
441
+
442
+ # flip function across vertical axis and append to gencost
443
+ # (PF for DC line = -PG for dummy gen at "from" bus)
444
+ for k in range(ndc):
445
+ if dcc[k, IDX.cost.MODEL] == IDX.cost.POLYNOMIAL:
446
+ nc = dcc[k, IDX.cost.NCOST]
447
+ temp = dcc[k, IDX.cost.NCOST + range(nc + 1)]
448
+ # flip sign on coefficients of odd terms
449
+ # (every other starting with linear term,
450
+ # that is, the next to last one)
451
+ # temp((nc-1):-2:1) = -temp((nc-1):-2:1)
452
+ temp[range(nc, 0, -2)] = -temp[range(nc, 0, -2)]
453
+ else: # dcc(k, IDX.cost.MODEL) == PW_LINEAR
454
+ nc = dcc[k, IDX.cost.NCOST]
455
+ temp = dcc[k, IDX.cost.NCOST + range(2*nc + 1)]
456
+ # switch sign on horizontal coordinate
457
+ xx = -temp[range(0, 2 * nc + 1, 2)]
458
+ yy = temp[range(1, 2 * nc + 1, 2)]
459
+ temp[range(0, 2*nc + 1, 2)] = xx[-1::-1]
460
+ temp[range(1, 2*nc + 1, 2)] = yy[-1::-1]
461
+
462
+ padding = np.zeros(ccc - IDX.cost.NCOST - len(temp))
463
+ gck = np.c_[dcc[k, :IDX.cost.NCOST + 1], temp, padding]
464
+
465
+ # append to gencost
466
+ ppc['gencost'] = np.r_[ppc['gencost'], gck]
467
+
468
+ # use zero cost on "to" end gen
469
+ tgc = np.ones((ndc, 1)) * [2, 0, 0, 2, np.zeros(ccc-4)]
470
+ ppc['gencost'] = np.c_[ppc['gencost'], tgc]
471
+ else:
472
+ # use zero cost as default
473
+ dcgc = np.ones((2 * ndc, 1)) * np.concatenate([np.array([2, 0, 0, 2]), np.zeros(ngcc-4)])
474
+ ppc['gencost'] = np.r_[ppc['gencost'], dcgc]
475
+
476
+ return ppc
477
+
478
+
479
+ # ----- formulation --------------------------------------------------
480
+ def userfcn_dcline_formulation(om, args):
481
+ """
482
+ This is the 'formulation' stage userfcn callback that defines the
483
+ user constraints for the dummy generators representing DC lines.
484
+ It expects to find a 'dcline' field in the ppc stored in om, as
485
+ described above. By the time it is passed to this callback,
486
+ MPC.dcline should contain only in-service lines and the from and
487
+ two bus columns should be converted to internal indexing. The
488
+ optional args are not currently used.
489
+
490
+ If Pf, Pt and Ploss are the flow at the "from" end, flow at the
491
+ "to" end and loss respectively, and L0 and L1 are the linear loss
492
+ coefficients, the the relationships between them is given by:
493
+ Pf - Ploss = Pt
494
+ Ploss = L0 + L1 * Pf
495
+ If Pgf and Pgt represent the injections of the dummy generators
496
+ representing the DC line injections into the network, then
497
+ Pgf = -Pf and Pgt = Pt, and we can combine all of the above to
498
+ get the following constraint on Pgf ang Pgt:
499
+ -Pgf - (L0 - L1 * Pgf) = Pgt
500
+ which can be written:
501
+ -L0 <= (1 - L1) * Pgf + Pgt <= -L0
502
+ """
503
+ # initialize some things
504
+ ppc = om.get_ppc()
505
+ dc = ppc['dcline']
506
+ ndc = dc.shape[0] # number of in-service DC lines
507
+ ng = ppc['gen'].shape[0] - 2 * ndc # number of original gens/disp loads
508
+
509
+ # constraints
510
+ nL0 = -dc[:, IDX.dcline.LOSS0] / ppc['baseMVA']
511
+ L1 = dc[:, IDX.dcline.LOSS1]
512
+ Adc = sp.hstack([c_sparse((ndc, ng)), sp.spdiags(1-L1, 0, ndc, ndc), sp.eye(ndc, ndc)], format="csr")
513
+
514
+ # add them to the model
515
+ om = om.add_constraints('dcline', Adc, nL0, nL0, ['Pg'])
516
+
517
+ return om
518
+
519
+
520
+ # ----- int2ext ------------------------------------------------------
521
+ def userfcn_dcline_int2ext(results, args):
522
+ """
523
+ This is the 'int2ext' stage userfcn callback that converts everything
524
+ back to external indexing and packages up the results. It expects to
525
+ find a 'dcline' field in the results struct as described for ppc
526
+ above. It also expects that the last 2*ndc entries in the gen and
527
+ gencost matrices correspond to the in-service DC lines (where ndc is
528
+ the number of rows in MPC.dcline. These extra rows are removed from
529
+ gen and gencost and the flow is taken from the PG of these gens and
530
+ placed in the flow column of the appropiate dcline row. The
531
+ optional args are not currently used.
532
+ """
533
+ # initialize some things
534
+ o = results['order']
535
+ k = find(o['ext']['dcline'][:, IDX.dcline.BR_STATUS])
536
+ ndc = len(k) # number of in-service DC lines
537
+ ng = results['gen'].shape[0] - 2*ndc # number of original gens/disp loads
538
+
539
+ # extract dummy gens
540
+ fg = results['gen'][ng:ng + ndc, :]
541
+ tg = results['gen'][ng + ndc:ng + 2 * ndc, :]
542
+
543
+ # remove dummy gens
544
+ # results['gen'] = results['gen'][:ng + 1, :]
545
+ # results['gencost'] = results['gencost'][:ng + 1, :]
546
+ results['gen'] = results['gen'][:ng, :]
547
+ results['gencost'] = results['gencost'][:ng, :]
548
+
549
+ # get the solved flows
550
+ results['dcline'][:, IDX.dcline.PF] = -fg[:, IDX.gen.PG]
551
+ results['dcline'][:, IDX.dcline.PT] = tg[:, IDX.gen.PG]
552
+ results['dcline'][:, IDX.dcline.QF] = fg[:, IDX.gen.QG]
553
+ results['dcline'][:, IDX.dcline.QT] = tg[:, IDX.gen.QG]
554
+ results['dcline'][:, IDX.dcline.VF] = fg[:, IDX.gen.VG]
555
+ results['dcline'][:, IDX.dcline.VT] = tg[:, IDX.gen.VG]
556
+ if fg.shape[1] >= IDX.gen.MU_QMIN:
557
+ results['dcline'] = np.c_[results['dcline'], np.zeros((ndc, 6))]
558
+ results['dcline'][:, IDX.dcline.MU_PMIN] = fg[:, IDX.gen.MU_PMAX] + tg[:, IDX.gen.MU_PMIN]
559
+ results['dcline'][:, IDX.dcline.MU_PMAX] = fg[:, IDX.gen.MU_PMIN] + tg[:, IDX.gen.MU_PMAX]
560
+ results['dcline'][:, IDX.dcline.MU_QMINF] = fg[:, IDX.gen.MU_QMIN]
561
+ results['dcline'][:, IDX.dcline.MU_QMAXF] = fg[:, IDX.gen.MU_QMAX]
562
+ results['dcline'][:, IDX.dcline.MU_QMINT] = tg[:, IDX.gen.MU_QMIN]
563
+ results['dcline'][:, IDX.dcline.MU_QMAXT] = tg[:, IDX.gen.MU_QMAX]
564
+
565
+ results['order']['int'] = {}
566
+ # ----- convert stuff back to external indexing -----
567
+ results['order']['int']['dcline'] = results['dcline'] # save internal version
568
+ # copy results to external version
569
+ o['ext']['dcline'][k, IDX.dcline.PF:c['VT'] + 1] = results['dcline'][:, IDX.dcline.PF:c['VT'] + 1]
570
+ if results['dcline'].shape[1] == IDX.dcline.MU_QMAXT + 1:
571
+ o['ext']['dcline'] = np.c_[o['ext']['dcline'], np.zeros((ndc, 6))]
572
+ o['ext']['dcline'][k, IDX.dcline.MU_PMIN:IDX.dcline.MU_QMAXT + 1] = \
573
+ results['dcline'][:, IDX.dcline.MU_PMIN:IDX.dcline.MU_QMAXT + 1]
574
+
575
+ results['dcline'] = o['ext']['dcline'] # use external version
576
+
577
+ return results
578
+
579
+
580
+ # ----- printpf ------------------------------------------------------
581
+ def userfcn_dcline_printpf(results, fd, ppopt, args):
582
+ """
583
+ This is the 'printpf' stage userfcn callback that pretty-prints the
584
+ results. It expects a results struct, a file descriptor and a MATPOWER
585
+ options vector. The optional args are not currently used.
586
+ """
587
+ # options
588
+ OUT_ALL = ppopt['OUT_ALL']
589
+ OUT_BRANCH = OUT_ALL == 1 or (OUT_ALL == -1 and ppopt['OUT_BRANCH'])
590
+ if OUT_ALL == -1:
591
+ OUT_ALL_LIM = ppopt['OUT_ALL_LIM']
592
+ elif OUT_ALL == 1:
593
+ OUT_ALL_LIM = 2
594
+ else:
595
+ OUT_ALL_LIM = 0
596
+
597
+ if OUT_ALL_LIM == -1:
598
+ OUT_LINE_LIM = ppopt['OUT_LINE_LIM']
599
+ else:
600
+ OUT_LINE_LIM = OUT_ALL_LIM
601
+
602
+ ctol = ppopt['OPF_VIOLATION'] # constraint violation tolerance
603
+ ptol = 1e-4 # tolerance for displaying shadow prices
604
+
605
+ # ----- print results -----
606
+ dc = results['dcline']
607
+ ndc = dc.shape[0]
608
+ kk = find(dc[:, IDX.dcline.BR_STATUS] != 0)
609
+ if OUT_BRANCH:
610
+ fd.write('\n================================================================================')
611
+ fd.write('\n| DC Line Data |')
612
+ fd.write('\n================================================================================')
613
+ fd.write('\n Line From To Power Flow Loss Reactive Inj (MVAr)')
614
+ fd.write('\n # Bus Bus From (MW) To (MW) (MW) From To ')
615
+ fd.write('\n------ ------ ------ --------- --------- --------- --------- ---------')
616
+ loss = 0
617
+ for k in range(ndc):
618
+ if dc[k, IDX.dcline.BR_STATUS]: # status on
619
+ fd.write(
620
+ '\n{0:5.0f}{1:8.0f}{2:8.0f}{3:11.2f}{4:11.2f}{5:11.2f}{6:11.2f}{7:11.2f}'.format(
621
+ *np.r_
622
+ [k, dc[k, IDX.dcline.F_BUS: IDX.dcline.T_BUS + 1],
623
+ dc[k, IDX.dcline.PF: IDX.dcline.PT + 1],
624
+ dc[k, IDX.dcline.PF] - dc[k, IDX.dcline.PT],
625
+ dc[k, IDX.dcline.QF: IDX.dcline.QT + 1]]))
626
+
627
+ loss = loss + dc[k, IDX.dcline.PF] - dc[k, IDX.dcline.PT]
628
+ else:
629
+ fd.write('\n%5d%8d%8d%11s%11s%11s%11s%11s' %
630
+ (k, dc[k, IDX.dcline.F_BUS:IDX.dcline.T_BUS + 1], '- ', '- ', '- ', '- ', '- '))
631
+
632
+ fd.write('\n ---------')
633
+ fd.write('\n Total:{0:11.2f}\n'.format(loss))
634
+
635
+ if OUT_LINE_LIM == 2 or (OUT_LINE_LIM == 1 and
636
+ (np.any(dc[kk, IDX.dcline.PF] > dc[kk, IDX.dcline.PMAX] - ctol) or
637
+ np.any(dc[kk, IDX.dcline.MU_PMIN] > ptol) or
638
+ np.any(dc[kk, IDX.dcline.MU_PMAX] > ptol))):
639
+ fd.write('\n================================================================================')
640
+ fd.write('\n| DC Line Constraints |')
641
+ fd.write('\n================================================================================')
642
+ fd.write('\n Line From To Minimum Actual Flow Maximum')
643
+ fd.write('\n # Bus Bus Pmin mu Pmin (MW) Pmax Pmax mu ')
644
+ fd.write('\n------ ------ ------ --------- --------- --------- --------- ---------')
645
+ for k in range(ndc):
646
+ if OUT_LINE_LIM == 2 or (OUT_LINE_LIM == 1 and
647
+ (dc[k, IDX.dcline.PF] > dc[k, IDX.dcline.PMAX] - ctol or
648
+ dc[k, IDX.dcline.MU_PMIN] > ptol or
649
+ dc[k, IDX.dcline.MU_PMAX] > ptol)):
650
+ if dc[k, IDX.dcline.BR_STATUS]: # status on
651
+ fd.write('\n{0:5.0f}{1:8.0f}{2:8.0f}'.format(
652
+ *np.r_[k, dc[k, IDX.dcline.F_BUS:IDX.dcline.T_BUS + 1]]))
653
+ # fd.write('\n%5d%8d%8d' % (k + 1, dc[k, IDX.dcline.F_BUS:IDX.dcline.T_BUS + 1] ))
654
+ if dc[k, IDX.dcline.MU_PMIN] > ptol:
655
+ fd.write('{0:11.3f}'.format(dc[k, IDX.dcline.MU_PMIN]))
656
+ else:
657
+ fd.write('%11s' % ('- '))
658
+
659
+ fd.write('{0:11.2f}{1:11.2f}{2:11.2f}'
660
+ .format(*np.r_[dc[k, IDX.dcline.PMIN], dc[k, IDX.dcline.PF], dc[k, IDX.dcline.PMAX]]))
661
+ if dc[k, IDX.dcline.MU_PMAX] > ptol:
662
+ fd.write('{0:11.3f}'.format(dc[k, IDX.dcline.MU_PMAX]))
663
+ else:
664
+ fd.write('%11s' % ('- '))
665
+
666
+ else:
667
+ fd.write('\n%5d%8d%8d%11s%11s%11s%11s%11s' %
668
+ (k, dc[k, IDX.dcline.F_BUS:IDX.dcline.T_BUS + 1], '- ', '- ', '- ', '- ', '- '))
669
+
670
+ fd.write('\n')
671
+
672
+ return results
673
+
674
+
675
+ # ----- savecase -----------------------------------------------------
676
+ def userfcn_dcline_savecase(ppc, fd, prefix, args):
677
+ """
678
+ This is the 'savecase' stage userfcn callback that prints the Py-file
679
+ code to save the 'dcline' field in the case file. It expects a
680
+ PYPOWER case dict (ppc), a file descriptor and variable prefix
681
+ (usually 'ppc.'). The optional args are not currently used.
682
+ """
683
+ # save it
684
+ ncols = ppc['dcline'].shape[1]
685
+ fd.write('\n####----- DC Line Data -----####\n')
686
+ if ncols < IDX.dcline.MU_QMAXT:
687
+ fd.write('##\tfbus\ttbus\tstatus\tPf\tPt\tQf\tQt\tVf\tVt\tPmin\tPmax\tQminF\tQmaxF\tQminT\tQmaxT\tloss0\tloss1\n')
688
+ else:
689
+ fd.write('##\tfbus\ttbus\tstatus\tPf\tPt\tQf\tQt\tVf\tVt\tPmin\tPmax\tQminF\tQmaxF\tQminT\tQmaxT\tloss0\tloss1\tmuPmin\tmuPmax\tmuQminF\tmuQmaxF\tmuQminT\tmuQmaxT\n')
690
+
691
+ template = '\t%d\t%d\t%d\t%.9g\t%.9g\t%.9g\t%.9g\t%.9g\t%.9g\t%.9g\t%.9g\t%.9g\t%.9g\t%.9g\t%.9g\t%.9g\t%.9g'
692
+ if ncols == IDX.dcline.MU_QMAXT + 1:
693
+ template = [template, '\t%.4f\t%.4f\t%.4f\t%.4f\t%.4f\t%.4f']
694
+
695
+ template = template + ';\n'
696
+ fd.write('%sdcline = [\n' % prefix)
697
+ fd.write(template, ppc['dcline'].T)
698
+ fd.write('];\n')
699
+
700
+ return ppc
701
+
702
+
703
+ def toggle_reserves(ppc, on_off):
704
+ """
705
+ Enable or disable fixed reserve requirements.
706
+
707
+ Enables or disables a set of OPF userfcn callbacks to implement
708
+ co-optimization of reserves with fixed zonal reserve requirements.
709
+
710
+ These callbacks expect to find a 'reserves' field in the input C{ppc},
711
+ where C{ppc['reserves']} is a dict with the following fields:
712
+ - C{zones} C{nrz x ng}, C{zone(i, j) = 1}, if gen C{j} belongs
713
+ to zone C{i} 0, otherwise
714
+ - C{req} C{nrz x 1}, zonal reserve requirement in MW
715
+ - C{cost} (C{ng} or C{ngr}) C{x 1}, cost of reserves in $/MW
716
+ - C{qty} (C{ng} or C{ngr}) C{x 1}, max quantity of reserves
717
+ in MW (optional)
718
+ where C{nrz} is the number of reserve zones and C{ngr} is the number of
719
+ generators belonging to at least one reserve zone and C{ng} is the total
720
+ number of generators.
721
+
722
+ The 'int2ext' callback also packages up results and stores them in
723
+ the following output fields of C{results['reserves']}:
724
+ - C{R} - C{ng x 1}, reserves provided by each gen in MW
725
+ - C{Rmin} - C{ng x 1}, lower limit on reserves provided by
726
+ each gen, (MW)
727
+ - C{Rmax} - C{ng x 1}, upper limit on reserves provided by
728
+ each gen, (MW)
729
+ - C{mu.l} - C{ng x 1}, shadow price on reserve lower limit, ($/MW)
730
+ - C{mu.u} - C{ng x 1}, shadow price on reserve upper limit, ($/MW)
731
+ - C{mu.Pmax} - C{ng x 1}, shadow price on C{Pg + R <= Pmax}
732
+ constraint, ($/MW)
733
+ - C{prc} - C{ng x 1}, reserve price for each gen equal to
734
+ maximum of the shadow prices on the zonal requirement constraint
735
+ for each zone the generator belongs to
736
+
737
+ @see: L{runopf_w_res}, L{add_userfcn}, L{remove_userfcn}, L{run_userfcn},
738
+ L{t.t_case30_userfcns}
739
+
740
+ @author: Ray Zimmerman (PSERC Cornell)
741
+ """
742
+ if on_off == 'on':
743
+ # check for proper reserve inputs
744
+ if ('reserves' not in ppc) | (not isinstance(ppc['reserves'], dict)) | \
745
+ ('zones' not in ppc['reserves']) | \
746
+ ('req' not in ppc['reserves']) | \
747
+ ('cost' not in ppc['reserves']):
748
+ logger.debug(
749
+ 'toggle_reserves: case must contain a \'reserves\' field, a struct defining \'zones\', \'req\' and \'cost\'\n')
750
+
751
+ # add callback functions
752
+ # note: assumes all necessary data included in 1st arg (ppc, om, results)
753
+ # so, no additional explicit args are needed
754
+ ppc = add_userfcn(ppc, 'ext2int', userfcn_reserves_ext2int)
755
+ ppc = add_userfcn(ppc, 'formulation', userfcn_reserves_formulation)
756
+ ppc = add_userfcn(ppc, 'int2ext', userfcn_reserves_int2ext)
757
+ ppc = add_userfcn(ppc, 'printpf', userfcn_reserves_printpf)
758
+ ppc = add_userfcn(ppc, 'savecase', userfcn_reserves_savecase)
759
+ elif on_off == 'off':
760
+ ppc = remove_userfcn(ppc, 'savecase', userfcn_reserves_savecase)
761
+ ppc = remove_userfcn(ppc, 'printpf', userfcn_reserves_printpf)
762
+ ppc = remove_userfcn(ppc, 'int2ext', userfcn_reserves_int2ext)
763
+ ppc = remove_userfcn(ppc, 'formulation', userfcn_reserves_formulation)
764
+ ppc = remove_userfcn(ppc, 'ext2int', userfcn_reserves_ext2int)
765
+ else:
766
+ logger.debug('toggle_reserves: 2nd argument must be either ''on'' or ''off''')
767
+
768
+ return ppc
769
+
770
+
771
+ def userfcn_reserves_ext2int(ppc, *args):
772
+ """
773
+ This is the 'ext2int' stage userfcn callback that prepares the input
774
+ data for the formulation stage. It expects to find a 'reserves' field
775
+ in ppc as described above. The optional args are not currently used.
776
+ """
777
+ # initialize some things
778
+ r = ppc['reserves']
779
+ o = ppc['order']
780
+ ng0 = o['ext']['gen'].shape[0] # number of original gens (+ disp loads)
781
+ nrz = r['req'].shape[0] # number of reserve zones
782
+ if nrz > 1:
783
+ ppc['reserves']['rgens'] = np.any(r['zones'], 0) # mask of gens available to provide reserves
784
+ else:
785
+ ppc['reserves']['rgens'] = r['zones']
786
+
787
+ igr = find(ppc['reserves']['rgens']) # indices of gens available to provide reserves
788
+ ngr = len(igr) # number of gens available to provide reserves
789
+
790
+ # check data for consistent dimensions
791
+ if r['zones'].shape[0] != nrz:
792
+ logger.debug('userfcn_reserves_ext2int: the number of rows in ppc[\'reserves\'][\'req\'] (%d) and ppc[\'reserves\'][\'zones\'] (%d) must match\n' % (
793
+ nrz, r['zones'].shape[0]))
794
+
795
+ if (r['cost'].shape[0] != ng0) & (r['cost'].shape[0] != ngr):
796
+ logger.debug('userfcn_reserves_ext2int: the number of rows in ppc[\'reserves\'][\'cost\'] (%d) must equal the total number of generators (%d) or the number of generators able to provide reserves (%d)\n' % (
797
+ r['cost'].shape[0], ng0, ngr))
798
+
799
+ if 'qty' in r:
800
+ if r['qty'].shape[0] != r['cost'].shape[0]:
801
+ logger.debug('userfcn_reserves_ext2int: ppc[\'reserves\'][\'cost\'] (%d x 1) and ppc[\'reserves\'][\'qty\'] (%d x 1) must be the same dimension\n' % (
802
+ r['cost'].shape[0], r['qty'].shape[0]))
803
+
804
+ # convert both cost and qty from ngr x 1 to full ng x 1 vectors if necessary
805
+ if r['cost'].shape[0] < ng0:
806
+ if 'original' not in ppc['reserves']:
807
+ ppc['reserves']['original'] = {}
808
+ ppc['reserves']['original']['cost'] = r['cost'].copy() # save original
809
+ cost = np.zeros(ng0)
810
+ cost[igr] = r['cost']
811
+ ppc['reserves']['cost'] = cost
812
+ if 'qty' in r:
813
+ ppc['reserves']['original']['qty'] = r['qty'].copy() # save original
814
+ qty = np.zeros(ng0)
815
+ qty[igr] = r['qty']
816
+ ppc['reserves']['qty'] = qty
817
+
818
+ # ----- convert stuff to internal indexing -----
819
+ # convert all reserve parameters (zones, costs, qty, rgens)
820
+ if 'qty' in r:
821
+ ppc = e2i_field(ppc, ['reserves', 'qty'], 'gen')
822
+
823
+ ppc = e2i_field(ppc, ['reserves', 'cost'], 'gen')
824
+ ppc = e2i_field(ppc, ['reserves', 'zones'], 'gen', 1)
825
+ ppc = e2i_field(ppc, ['reserves', 'rgens'], 'gen', 1)
826
+
827
+ # save indices of gens available to provide reserves
828
+ ppc['order']['ext']['reserves']['igr'] = igr # external indexing
829
+ ppc['reserves']['igr'] = find(ppc['reserves']['rgens']) # internal indexing
830
+
831
+ return ppc
832
+
833
+
834
+ def userfcn_reserves_formulation(om, *args):
835
+ """
836
+ This is the 'formulation' stage userfcn callback that defines the
837
+ user costs and constraints for fixed reserves. It expects to find
838
+ a 'reserves' field in the ppc stored in om, as described above.
839
+ By the time it is passed to this callback, ppc['reserves'] should
840
+ have two additional fields:
841
+ - C{igr} C{1 x ngr}, indices of generators available for reserves
842
+ - C{rgens} C{1 x ng}, 1 if gen avaiable for reserves, 0 otherwise
843
+ It is also assumed that if cost or qty were C{ngr x 1}, they have been
844
+ expanded to C{ng x 1} and that everything has been converted to
845
+ internal indexing, i.e. all gens are on-line (by the 'ext2int'
846
+ callback). The optional args are not currently used.
847
+ """
848
+ # initialize some things
849
+ ppc = om.get_ppc()
850
+ r = ppc['reserves']
851
+ igr = r['igr'] # indices of gens available to provide reserves
852
+ ngr = len(igr) # number of gens available to provide reserves
853
+ ng = ppc['gen'].shape[0] # number of on-line gens (+ disp loads)
854
+
855
+ # variable bounds
856
+ Rmin = np.zeros(ngr) # bound below by 0
857
+ Rmax = inf * np.ones(ngr) # bound above by ...
858
+ k = find(ppc['gen'][igr, IDX.gen.RAMP_10])
859
+ Rmax[k] = ppc['gen'][igr[k], IDX.gen.RAMP_10] # ... ramp rate and ...
860
+ if 'qty' in r:
861
+ k = find(r['qty'][igr] < Rmax)
862
+ Rmax[k] = r['qty'][igr[k]] # ... stated max reserve qty
863
+ Rmax = Rmax / ppc['baseMVA']
864
+
865
+ # constraints
866
+ I = sp.eye(ngr, ngr, format='csr') # identity matrix
867
+ Ar = sp.hstack([c_sparse((np.ones(ngr), (np.arange(ngr), igr)), (ngr, ng)), I], 'csr')
868
+ ur = ppc['gen'][igr, IDX.gen.PMAX] / ppc['baseMVA']
869
+ lreq = r['req'] / ppc['baseMVA']
870
+
871
+ # cost
872
+ Cw = r['cost'][igr] * ppc['baseMVA'] # per unit cost coefficients
873
+
874
+ # add them to the model
875
+ om.add_vars('R', ngr, [], Rmin, Rmax)
876
+ om.add_constraints('Pg_plus_R', Ar, [], ur, ['Pg', 'R'])
877
+ om.add_constraints('Rreq', c_sparse(r['zones'][:, igr]), lreq, [], ['R'])
878
+ om.add_costs('Rcost', {'N': I, 'Cw': Cw}, ['R'])
879
+
880
+ return om
881
+
882
+
883
+ def userfcn_reserves_int2ext(results, *args):
884
+ """
885
+ This is the 'int2ext' stage userfcn callback that converts everything
886
+ back to external indexing and packages up the results. It expects to
887
+ find a 'reserves' field in the results struct as described for ppc
888
+ above, including the two additional fields 'igr' and 'rgens'. It also
889
+ expects the results to contain a variable 'R' and linear constraints
890
+ 'Pg_plus_R' and 'Rreq' which are used to populate output fields in
891
+ results.reserves. The optional args are not currently used.
892
+ """
893
+ # initialize some things
894
+ r = results['reserves']
895
+
896
+ # grab some info in internal indexing order
897
+ igr = r['igr'] # indices of gens available to provide reserves
898
+ ng = results['gen'].shape[0] # number of on-line gens (+ disp loads)
899
+
900
+ # ----- convert stuff back to external indexing -----
901
+ # convert all reserve parameters (zones, costs, qty, rgens)
902
+ if 'qty' in r:
903
+ results = i2e_field(results, ['reserves', 'qty'], ordering='gen')
904
+
905
+ results = i2e_field(results, ['reserves', 'cost'], ordering='gen')
906
+ results = i2e_field(results, ['reserves', 'zones'], ordering='gen', dim=1)
907
+ results = i2e_field(results, ['reserves', 'rgens'], ordering='gen', dim=1)
908
+ results['order']['int']['reserves']['igr'] = results['reserves']['igr'] # save internal version
909
+ results['reserves']['igr'] = results['order']['ext']['reserves']['igr'] # use external version
910
+ r = results['reserves'] # update
911
+ o = results['order'] # update
912
+
913
+ # grab same info in external indexing order
914
+ igr0 = r['igr'] # indices of gens available to provide reserves
915
+ ng0 = o['ext']['gen'].shape[0] # number of gens (+ disp loads)
916
+
917
+ # ----- results post-processing -----
918
+ # get the results (per gen reserves, multipliers) with internal gen indexing
919
+ # and convert from p.u. to per MW units
920
+ _, Rl, Ru = results['om'].getv('R')
921
+ R = np.zeros(ng)
922
+ Rmin = np.zeros(ng)
923
+ Rmax = np.zeros(ng)
924
+ mu_l = np.zeros(ng)
925
+ mu_u = np.zeros(ng)
926
+ mu_Pmax = np.zeros(ng)
927
+ R[igr] = results['var']['val']['R'] * results['baseMVA']
928
+ Rmin[igr] = Rl * results['baseMVA']
929
+ Rmax[igr] = Ru * results['baseMVA']
930
+ mu_l[igr] = results['var']['mu']['l']['R'] / results['baseMVA']
931
+ mu_u[igr] = results['var']['mu']['u']['R'] / results['baseMVA']
932
+ mu_Pmax[igr] = results['lin']['mu']['u']['Pg_plus_R'] / results['baseMVA']
933
+
934
+ # store in results in results struct
935
+ z = np.zeros(ng0)
936
+ results['reserves']['R'] = i2e_data(results, R, z, 'gen')
937
+ results['reserves']['Rmin'] = i2e_data(results, Rmin, z, 'gen')
938
+ results['reserves']['Rmax'] = i2e_data(results, Rmax, z, 'gen')
939
+ if 'mu' not in results['reserves']:
940
+ results['reserves']['mu'] = {}
941
+ results['reserves']['mu']['l'] = i2e_data(results, mu_l, z, 'gen')
942
+ results['reserves']['mu']['u'] = i2e_data(results, mu_u, z, 'gen')
943
+ results['reserves']['mu']['Pmax'] = i2e_data(results, mu_Pmax, z, 'gen')
944
+ results['reserves']['prc'] = z
945
+ for k in igr0:
946
+ iz = find(r['zones'][:, k])
947
+ results['reserves']['prc'][k] = sum(results['lin']['mu']['l']['Rreq'][iz]) / results['baseMVA']
948
+
949
+ results['reserves']['totalcost'] = results['cost']['Rcost']
950
+
951
+ # replace ng x 1 cost, qty with ngr x 1 originals
952
+ if 'original' in r:
953
+ if 'qty' in r:
954
+ results['reserves']['qty'] = r['original']['qty']
955
+ results['reserves']['cost'] = r['original']['cost']
956
+ del results['reserves']['original']
957
+
958
+ return results
959
+
960
+
961
+ def userfcn_reserves_printpf(results, fd, ppopt, *args):
962
+ """
963
+ This is the 'printpf' stage userfcn callback that pretty-prints the
964
+ results. It expects a C{results} dict, a file descriptor and a PYPOWER
965
+ options vector. The optional args are not currently used.
966
+ """
967
+ # ----- print results -----
968
+ r = results['reserves']
969
+ nrz = r['req'].shape[0]
970
+ OUT_ALL = ppopt['OUT_ALL']
971
+ if OUT_ALL != 0:
972
+ fd.write('\n================================================================================')
973
+ fd.write('\n| Reserves |')
974
+ fd.write('\n================================================================================')
975
+ fd.write('\n Gen Bus Status Reserves Price')
976
+ fd.write('\n # # (MW) ($/MW) Included in Zones ...')
977
+ fd.write('\n---- ----- ------ -------- -------- ------------------------')
978
+ for k in r['igr']:
979
+ iz = find(r['zones'][:, k])
980
+ fd.write('\n%3d %6d %2d ' %
981
+ (k, results['gen'][k, IDX.gen.GEN_BUS],
982
+ results['gen'][k, IDX.gen.GEN_STATUS]))
983
+ if (results['gen'][k, IDX.gen.GEN_STATUS] > 0) & (abs(results['reserves']['R'][k]) > 1e-6):
984
+ fd.write('%10.2f' % results['reserves']['R'][k])
985
+ else:
986
+ fd.write(' - ')
987
+
988
+ fd.write('%10.2f ' % results['reserves']['prc'][k])
989
+ for i in range(len(iz)):
990
+ if i != 0:
991
+ fd.write(', ')
992
+ fd.write('%d' % iz[i])
993
+
994
+ fd.write('\n --------')
995
+ fd.write('\n Total:%10.2f Total Cost: $%.2f' %
996
+ (sum(results['reserves']['R'][r['igr']]), results['reserves']['totalcost']))
997
+ fd.write('\n')
998
+
999
+ fd.write('\nZone Reserves Price ')
1000
+ fd.write('\n # (MW) ($/MW) ')
1001
+ fd.write('\n---- -------- --------')
1002
+ for k in range(nrz):
1003
+ iz = find(r['zones'][k, :]) # gens in zone k
1004
+ fd.write('\n%3d%10.2f%10.2f' % (k, sum(results['reserves']['R'][iz]),
1005
+ results['lin']['mu']['l']['Rreq'][k] / results['baseMVA']))
1006
+ fd.write('\n')
1007
+
1008
+ fd.write('\n================================================================================')
1009
+ fd.write('\n| Reserve Limits |')
1010
+ fd.write('\n================================================================================')
1011
+ fd.write('\n Gen Bus Status Rmin mu Rmin Reserves Rmax Rmax mu Pmax mu ')
1012
+ fd.write('\n # # ($/MW) (MW) (MW) (MW) ($/MW) ($/MW) ')
1013
+ fd.write('\n---- ----- ------ -------- -------- -------- -------- -------- --------')
1014
+ for k in r['igr']:
1015
+ fd.write('\n%3d %6d %2d ' %
1016
+ (k, results['gen'][k, IDX.gen.GEN_BUS],
1017
+ results['gen'][k, IDX.gen.GEN_STATUS]))
1018
+ if (results['gen'][k, IDX.gen.GEN_STATUS] > 0) & (results['reserves']['mu']['l'][k] > 1e-6):
1019
+ fd.write('%10.2f' % results['reserves']['mu']['l'][k])
1020
+ else:
1021
+ fd.write(' - ')
1022
+
1023
+ fd.write('%10.2f' % results['reserves']['Rmin'][k])
1024
+ if (results['gen'][k, IDX.gen.GEN_STATUS] > 0) & (abs(results['reserves']['R'][k]) > 1e-6):
1025
+ fd.write('%10.2f' % results['reserves']['R'][k])
1026
+ else:
1027
+ fd.write(' - ')
1028
+
1029
+ fd.write('%10.2f' % results['reserves']['Rmax'][k])
1030
+ if (results['gen'][k, IDX.gen.GEN_STATUS] > 0) & (results['reserves']['mu']['u'][k] > 1e-6):
1031
+ fd.write('%10.2f' % results['reserves']['mu']['u'][k])
1032
+ else:
1033
+ fd.write(' - ')
1034
+
1035
+ if (results['gen'][k, IDX.gen.GEN_STATUS] > 0) & (results['reserves']['mu']['Pmax'][k] > 1e-6):
1036
+ fd.write('%10.2f' % results['reserves']['mu']['Pmax'][k])
1037
+ else:
1038
+ fd.write(' - ')
1039
+
1040
+ fd.write('\n --------')
1041
+ fd.write('\n Total:%10.2f' % sum(results['reserves']['R'][r['igr']]))
1042
+ fd.write('\n')
1043
+
1044
+ return results
1045
+
1046
+
1047
+ def userfcn_reserves_savecase(ppc, fd, prefix, *args):
1048
+ """
1049
+ This is the 'savecase' stage userfcn callback that prints the Python
1050
+ file code to save the 'reserves' field in the case file. It expects a
1051
+ PYPOWER case dict (ppc), a file descriptor and variable prefix
1052
+ (usually 'ppc'). The optional args are not currently used.
1053
+ """
1054
+ r = ppc['reserves']
1055
+
1056
+ fd.write('\n####----- Reserve Data -----####\n')
1057
+ fd.write('#### reserve zones, element i, j is 1 if gen j is in zone i, 0 otherwise\n')
1058
+ fd.write('%sreserves.zones = [\n' % prefix)
1059
+ template = ''
1060
+ for _ in range(r['zones'].shape[1]):
1061
+ template = template + '\t%d'
1062
+ template = template + ';\n'
1063
+ fd.write(template, r.zones.T)
1064
+ fd.write('];\n')
1065
+
1066
+ fd.write('\n#### reserve requirements for each zone in MW\n')
1067
+ fd.write('%sreserves.req = [\t%g' % (prefix, r['req'][0]))
1068
+ if len(r['req']) > 1:
1069
+ fd.write(';\t%g' % r['req'][1:])
1070
+ fd.write('\t];\n')
1071
+
1072
+ fd.write('\n#### reserve costs in $/MW for each gen that belongs to at least 1 zone\n')
1073
+ fd.write('#### (same order as gens, but skipping any gen that does not belong to any zone)\n')
1074
+ fd.write('%sreserves.cost = [\t%g' % (prefix, r['cost'][0]))
1075
+ if len(r['cost']) > 1:
1076
+ fd.write(';\t%g' % r['cost'][1:])
1077
+ fd.write('\t];\n')
1078
+
1079
+ if 'qty' in r:
1080
+ fd.write('\n#### OPTIONAL max reserve quantities for each gen that belongs to at least 1 zone\n')
1081
+ fd.write('#### (same order as gens, but skipping any gen that does not belong to any zone)\n')
1082
+ fd.write('%sreserves.qty = [\t%g' % (prefix, r['qty'][0]))
1083
+ if len(r['qty']) > 1:
1084
+ fd.write(';\t%g' % r['qty'][1:])
1085
+ fd.write('\t];\n')
1086
+
1087
+ # save output fields for solved case
1088
+ if 'R' in r:
1089
+ fd.write('\n#### solved values\n')
1090
+ fd.write('%sreserves.R = %s\n' % (prefix, pprint(r['R'])))
1091
+ fd.write('%sreserves.Rmin = %s\n' % (prefix, pprint(r['Rmin'])))
1092
+ fd.write('%sreserves.Rmax = %s\n' % (prefix, pprint(r['Rmax'])))
1093
+ fd.write('%sreserves.mu.l = %s\n' % (prefix, pprint(r['mu']['l'])))
1094
+ fd.write('%sreserves.mu.u = %s\n' % (prefix, pprint(r['mu']['u'])))
1095
+ fd.write('%sreserves.prc = %s\n' % (prefix, pprint(r['prc'])))
1096
+ fd.write('%sreserves.totalcost = %s\n' % (prefix, pprint(r['totalcost'])))
1097
+
1098
+ return ppc