ltbams 0.9.9__py3-none-any.whl → 1.0.2a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ams/__init__.py +4 -11
- ams/_version.py +3 -3
- ams/cases/5bus/pjm5bus_demo.xlsx +0 -0
- ams/cases/5bus/pjm5bus_jumper.xlsx +0 -0
- ams/cases/5bus/pjm5bus_uced.json +1062 -0
- ams/cases/5bus/pjm5bus_uced.xlsx +0 -0
- ams/cases/5bus/pjm5bus_uced_esd1.xlsx +0 -0
- ams/cases/5bus/pjm5bus_uced_ev.xlsx +0 -0
- ams/cases/ieee123/ieee123.xlsx +0 -0
- ams/cases/ieee123/ieee123_regcv1.xlsx +0 -0
- ams/cases/ieee14/ieee14.json +1166 -0
- ams/cases/ieee14/ieee14.raw +92 -0
- ams/cases/ieee14/ieee14_conn.xlsx +0 -0
- ams/cases/ieee14/ieee14_uced.xlsx +0 -0
- ams/cases/ieee39/ieee39.xlsx +0 -0
- ams/cases/ieee39/ieee39_uced.xlsx +0 -0
- ams/cases/ieee39/ieee39_uced_esd1.xlsx +0 -0
- ams/cases/ieee39/ieee39_uced_pvd1.xlsx +0 -0
- ams/cases/ieee39/ieee39_uced_vis.xlsx +0 -0
- ams/cases/matpower/benchmark.json +1594 -0
- ams/cases/matpower/case118.m +787 -0
- ams/cases/matpower/case14.m +129 -0
- ams/cases/matpower/case300.m +1315 -0
- ams/cases/matpower/case39.m +205 -0
- ams/cases/matpower/case5.m +62 -0
- ams/cases/matpower/case_ACTIVSg2000.m +9460 -0
- ams/cases/npcc/npcc.m +644 -0
- ams/cases/npcc/npcc_uced.xlsx +0 -0
- ams/cases/pglib/pglib_opf_case39_epri__api.m +243 -0
- ams/cases/wecc/wecc.m +714 -0
- ams/cases/wecc/wecc_uced.xlsx +0 -0
- ams/cli.py +6 -0
- ams/core/__init__.py +2 -0
- ams/core/documenter.py +652 -0
- ams/core/matprocessor.py +782 -0
- ams/core/model.py +330 -0
- ams/core/param.py +322 -0
- ams/core/service.py +918 -0
- ams/core/symprocessor.py +224 -0
- ams/core/var.py +59 -0
- ams/extension/__init__.py +5 -0
- ams/extension/eva.py +401 -0
- ams/interface.py +1085 -0
- ams/io/__init__.py +133 -0
- ams/io/json.py +82 -0
- ams/io/matpower.py +406 -0
- ams/io/psse.py +6 -0
- ams/io/pypower.py +103 -0
- ams/io/xlsx.py +80 -0
- ams/main.py +81 -4
- ams/models/__init__.py +24 -0
- ams/models/area.py +40 -0
- ams/models/bus.py +52 -0
- ams/models/cost.py +169 -0
- ams/models/distributed/__init__.py +3 -0
- ams/models/distributed/esd1.py +71 -0
- ams/models/distributed/ev.py +60 -0
- ams/models/distributed/pvd1.py +67 -0
- ams/models/group.py +231 -0
- ams/models/info.py +26 -0
- ams/models/line.py +238 -0
- ams/models/renewable/__init__.py +5 -0
- ams/models/renewable/regc.py +119 -0
- ams/models/reserve.py +94 -0
- ams/models/shunt.py +14 -0
- ams/models/static/__init__.py +2 -0
- ams/models/static/gen.py +165 -0
- ams/models/static/pq.py +61 -0
- ams/models/timeslot.py +69 -0
- ams/models/zone.py +49 -0
- ams/opt/__init__.py +12 -0
- ams/opt/constraint.py +175 -0
- ams/opt/exprcalc.py +127 -0
- ams/opt/expression.py +188 -0
- ams/opt/objective.py +174 -0
- ams/opt/omodel.py +432 -0
- ams/opt/optzbase.py +192 -0
- ams/opt/param.py +156 -0
- ams/opt/var.py +233 -0
- ams/pypower/__init__.py +8 -0
- ams/pypower/_compat.py +9 -0
- ams/pypower/core/__init__.py +8 -0
- ams/pypower/core/pips.py +894 -0
- ams/pypower/core/ppoption.py +244 -0
- ams/pypower/core/ppver.py +18 -0
- ams/pypower/core/solver.py +2451 -0
- ams/pypower/eps.py +6 -0
- ams/pypower/idx.py +174 -0
- ams/pypower/io.py +604 -0
- ams/pypower/make/__init__.py +11 -0
- ams/pypower/make/matrices.py +665 -0
- ams/pypower/make/pdv.py +506 -0
- ams/pypower/routines/__init__.py +7 -0
- ams/pypower/routines/cpf.py +513 -0
- ams/pypower/routines/cpf_callbacks.py +114 -0
- ams/pypower/routines/opf.py +1803 -0
- ams/pypower/routines/opffcns.py +1946 -0
- ams/pypower/routines/pflow.py +852 -0
- ams/pypower/toggle.py +1098 -0
- ams/pypower/utils.py +293 -0
- ams/report.py +212 -50
- ams/routines/__init__.py +23 -0
- ams/routines/acopf.py +117 -0
- ams/routines/cpf.py +65 -0
- ams/routines/dcopf.py +241 -0
- ams/routines/dcpf.py +209 -0
- ams/routines/dcpf0.py +196 -0
- ams/routines/dopf.py +150 -0
- ams/routines/ed.py +312 -0
- ams/routines/pflow.py +255 -0
- ams/routines/pflow0.py +113 -0
- ams/routines/routine.py +1033 -0
- ams/routines/rted.py +519 -0
- ams/routines/type.py +160 -0
- ams/routines/uc.py +376 -0
- ams/shared.py +63 -9
- ams/system.py +61 -22
- ams/utils/__init__.py +3 -0
- ams/utils/misc.py +77 -0
- ams/utils/paths.py +257 -0
- docs/Makefile +21 -0
- docs/make.bat +35 -0
- docs/source/_templates/autosummary/base.rst +5 -0
- docs/source/_templates/autosummary/class.rst +35 -0
- docs/source/_templates/autosummary/module.rst +65 -0
- docs/source/_templates/autosummary/module_toctree.rst +66 -0
- docs/source/api.rst +102 -0
- docs/source/conf.py +203 -0
- docs/source/examples/index.rst +34 -0
- docs/source/genmodelref.py +61 -0
- docs/source/genroutineref.py +47 -0
- docs/source/getting_started/copyright.rst +20 -0
- docs/source/getting_started/formats/index.rst +20 -0
- docs/source/getting_started/formats/matpower.rst +183 -0
- docs/source/getting_started/formats/psse.rst +46 -0
- docs/source/getting_started/formats/pypower.rst +223 -0
- docs/source/getting_started/formats/xlsx.png +0 -0
- docs/source/getting_started/formats/xlsx.rst +23 -0
- docs/source/getting_started/index.rst +76 -0
- docs/source/getting_started/install.rst +234 -0
- docs/source/getting_started/overview.rst +26 -0
- docs/source/getting_started/testcase.rst +45 -0
- docs/source/getting_started/verification.rst +13 -0
- docs/source/images/curent.ico +0 -0
- docs/source/images/dcopf_time.png +0 -0
- docs/source/images/sponsors/CURENT_Logo_NameOnTrans.png +0 -0
- docs/source/images/sponsors/CURENT_Logo_Transparent.png +0 -0
- docs/source/images/sponsors/CURENT_Logo_Transparent_Name.png +0 -0
- docs/source/images/sponsors/doe.png +0 -0
- docs/source/index.rst +108 -0
- docs/source/modeling/example.rst +159 -0
- docs/source/modeling/index.rst +17 -0
- docs/source/modeling/model.rst +210 -0
- docs/source/modeling/routine.rst +122 -0
- docs/source/modeling/system.rst +51 -0
- docs/source/release-notes.rst +398 -0
- ltbams-1.0.2a1.dist-info/METADATA +210 -0
- ltbams-1.0.2a1.dist-info/RECORD +188 -0
- {ltbams-0.9.9.dist-info → ltbams-1.0.2a1.dist-info}/WHEEL +1 -1
- ltbams-1.0.2a1.dist-info/top_level.txt +3 -0
- tests/__init__.py +0 -0
- tests/test_1st_system.py +33 -0
- tests/test_addressing.py +40 -0
- tests/test_andes_mats.py +61 -0
- tests/test_case.py +266 -0
- tests/test_cli.py +34 -0
- tests/test_export_csv.py +89 -0
- tests/test_group.py +83 -0
- tests/test_interface.py +216 -0
- tests/test_io.py +32 -0
- tests/test_jumper.py +27 -0
- tests/test_known_good.py +267 -0
- tests/test_matp.py +437 -0
- tests/test_model.py +54 -0
- tests/test_omodel.py +119 -0
- tests/test_paths.py +22 -0
- tests/test_report.py +251 -0
- tests/test_repr.py +21 -0
- tests/test_routine.py +178 -0
- tests/test_rtn_dcopf.py +101 -0
- tests/test_rtn_dcpf.py +77 -0
- tests/test_rtn_ed.py +279 -0
- tests/test_rtn_pflow.py +219 -0
- tests/test_rtn_rted.py +273 -0
- tests/test_rtn_uc.py +248 -0
- tests/test_service.py +73 -0
- ltbams-0.9.9.dist-info/LICENSE +0 -692
- ltbams-0.9.9.dist-info/METADATA +0 -859
- ltbams-0.9.9.dist-info/RECORD +0 -14
- ltbams-0.9.9.dist-info/top_level.txt +0 -1
- {ltbams-0.9.9.dist-info → ltbams-1.0.2a1.dist-info}/entry_points.txt +0 -0
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
|