ltbams 0.9.9__py3-none-any.whl → 1.0.2a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ams/__init__.py +4 -11
- ams/_version.py +3 -3
- ams/cases/5bus/pjm5bus_demo.xlsx +0 -0
- ams/cases/5bus/pjm5bus_jumper.xlsx +0 -0
- ams/cases/5bus/pjm5bus_uced.json +1062 -0
- ams/cases/5bus/pjm5bus_uced.xlsx +0 -0
- ams/cases/5bus/pjm5bus_uced_esd1.xlsx +0 -0
- ams/cases/5bus/pjm5bus_uced_ev.xlsx +0 -0
- ams/cases/ieee123/ieee123.xlsx +0 -0
- ams/cases/ieee123/ieee123_regcv1.xlsx +0 -0
- ams/cases/ieee14/ieee14.json +1166 -0
- ams/cases/ieee14/ieee14.raw +92 -0
- ams/cases/ieee14/ieee14_conn.xlsx +0 -0
- ams/cases/ieee14/ieee14_uced.xlsx +0 -0
- ams/cases/ieee39/ieee39.xlsx +0 -0
- ams/cases/ieee39/ieee39_uced.xlsx +0 -0
- ams/cases/ieee39/ieee39_uced_esd1.xlsx +0 -0
- ams/cases/ieee39/ieee39_uced_pvd1.xlsx +0 -0
- ams/cases/ieee39/ieee39_uced_vis.xlsx +0 -0
- ams/cases/matpower/benchmark.json +1594 -0
- ams/cases/matpower/case118.m +787 -0
- ams/cases/matpower/case14.m +129 -0
- ams/cases/matpower/case300.m +1315 -0
- ams/cases/matpower/case39.m +205 -0
- ams/cases/matpower/case5.m +62 -0
- ams/cases/matpower/case_ACTIVSg2000.m +9460 -0
- ams/cases/npcc/npcc.m +644 -0
- ams/cases/npcc/npcc_uced.xlsx +0 -0
- ams/cases/pglib/pglib_opf_case39_epri__api.m +243 -0
- ams/cases/wecc/wecc.m +714 -0
- ams/cases/wecc/wecc_uced.xlsx +0 -0
- ams/cli.py +6 -0
- ams/core/__init__.py +2 -0
- ams/core/documenter.py +652 -0
- ams/core/matprocessor.py +782 -0
- ams/core/model.py +330 -0
- ams/core/param.py +322 -0
- ams/core/service.py +918 -0
- ams/core/symprocessor.py +224 -0
- ams/core/var.py +59 -0
- ams/extension/__init__.py +5 -0
- ams/extension/eva.py +401 -0
- ams/interface.py +1085 -0
- ams/io/__init__.py +133 -0
- ams/io/json.py +82 -0
- ams/io/matpower.py +406 -0
- ams/io/psse.py +6 -0
- ams/io/pypower.py +103 -0
- ams/io/xlsx.py +80 -0
- ams/main.py +81 -4
- ams/models/__init__.py +24 -0
- ams/models/area.py +40 -0
- ams/models/bus.py +52 -0
- ams/models/cost.py +169 -0
- ams/models/distributed/__init__.py +3 -0
- ams/models/distributed/esd1.py +71 -0
- ams/models/distributed/ev.py +60 -0
- ams/models/distributed/pvd1.py +67 -0
- ams/models/group.py +231 -0
- ams/models/info.py +26 -0
- ams/models/line.py +238 -0
- ams/models/renewable/__init__.py +5 -0
- ams/models/renewable/regc.py +119 -0
- ams/models/reserve.py +94 -0
- ams/models/shunt.py +14 -0
- ams/models/static/__init__.py +2 -0
- ams/models/static/gen.py +165 -0
- ams/models/static/pq.py +61 -0
- ams/models/timeslot.py +69 -0
- ams/models/zone.py +49 -0
- ams/opt/__init__.py +12 -0
- ams/opt/constraint.py +175 -0
- ams/opt/exprcalc.py +127 -0
- ams/opt/expression.py +188 -0
- ams/opt/objective.py +174 -0
- ams/opt/omodel.py +432 -0
- ams/opt/optzbase.py +192 -0
- ams/opt/param.py +156 -0
- ams/opt/var.py +233 -0
- ams/pypower/__init__.py +8 -0
- ams/pypower/_compat.py +9 -0
- ams/pypower/core/__init__.py +8 -0
- ams/pypower/core/pips.py +894 -0
- ams/pypower/core/ppoption.py +244 -0
- ams/pypower/core/ppver.py +18 -0
- ams/pypower/core/solver.py +2451 -0
- ams/pypower/eps.py +6 -0
- ams/pypower/idx.py +174 -0
- ams/pypower/io.py +604 -0
- ams/pypower/make/__init__.py +11 -0
- ams/pypower/make/matrices.py +665 -0
- ams/pypower/make/pdv.py +506 -0
- ams/pypower/routines/__init__.py +7 -0
- ams/pypower/routines/cpf.py +513 -0
- ams/pypower/routines/cpf_callbacks.py +114 -0
- ams/pypower/routines/opf.py +1803 -0
- ams/pypower/routines/opffcns.py +1946 -0
- ams/pypower/routines/pflow.py +852 -0
- ams/pypower/toggle.py +1098 -0
- ams/pypower/utils.py +293 -0
- ams/report.py +212 -50
- ams/routines/__init__.py +23 -0
- ams/routines/acopf.py +117 -0
- ams/routines/cpf.py +65 -0
- ams/routines/dcopf.py +241 -0
- ams/routines/dcpf.py +209 -0
- ams/routines/dcpf0.py +196 -0
- ams/routines/dopf.py +150 -0
- ams/routines/ed.py +312 -0
- ams/routines/pflow.py +255 -0
- ams/routines/pflow0.py +113 -0
- ams/routines/routine.py +1033 -0
- ams/routines/rted.py +519 -0
- ams/routines/type.py +160 -0
- ams/routines/uc.py +376 -0
- ams/shared.py +63 -9
- ams/system.py +61 -22
- ams/utils/__init__.py +3 -0
- ams/utils/misc.py +77 -0
- ams/utils/paths.py +257 -0
- docs/Makefile +21 -0
- docs/make.bat +35 -0
- docs/source/_templates/autosummary/base.rst +5 -0
- docs/source/_templates/autosummary/class.rst +35 -0
- docs/source/_templates/autosummary/module.rst +65 -0
- docs/source/_templates/autosummary/module_toctree.rst +66 -0
- docs/source/api.rst +102 -0
- docs/source/conf.py +203 -0
- docs/source/examples/index.rst +34 -0
- docs/source/genmodelref.py +61 -0
- docs/source/genroutineref.py +47 -0
- docs/source/getting_started/copyright.rst +20 -0
- docs/source/getting_started/formats/index.rst +20 -0
- docs/source/getting_started/formats/matpower.rst +183 -0
- docs/source/getting_started/formats/psse.rst +46 -0
- docs/source/getting_started/formats/pypower.rst +223 -0
- docs/source/getting_started/formats/xlsx.png +0 -0
- docs/source/getting_started/formats/xlsx.rst +23 -0
- docs/source/getting_started/index.rst +76 -0
- docs/source/getting_started/install.rst +234 -0
- docs/source/getting_started/overview.rst +26 -0
- docs/source/getting_started/testcase.rst +45 -0
- docs/source/getting_started/verification.rst +13 -0
- docs/source/images/curent.ico +0 -0
- docs/source/images/dcopf_time.png +0 -0
- docs/source/images/sponsors/CURENT_Logo_NameOnTrans.png +0 -0
- docs/source/images/sponsors/CURENT_Logo_Transparent.png +0 -0
- docs/source/images/sponsors/CURENT_Logo_Transparent_Name.png +0 -0
- docs/source/images/sponsors/doe.png +0 -0
- docs/source/index.rst +108 -0
- docs/source/modeling/example.rst +159 -0
- docs/source/modeling/index.rst +17 -0
- docs/source/modeling/model.rst +210 -0
- docs/source/modeling/routine.rst +122 -0
- docs/source/modeling/system.rst +51 -0
- docs/source/release-notes.rst +398 -0
- ltbams-1.0.2a1.dist-info/METADATA +210 -0
- ltbams-1.0.2a1.dist-info/RECORD +188 -0
- {ltbams-0.9.9.dist-info → ltbams-1.0.2a1.dist-info}/WHEEL +1 -1
- ltbams-1.0.2a1.dist-info/top_level.txt +3 -0
- tests/__init__.py +0 -0
- tests/test_1st_system.py +33 -0
- tests/test_addressing.py +40 -0
- tests/test_andes_mats.py +61 -0
- tests/test_case.py +266 -0
- tests/test_cli.py +34 -0
- tests/test_export_csv.py +89 -0
- tests/test_group.py +83 -0
- tests/test_interface.py +216 -0
- tests/test_io.py +32 -0
- tests/test_jumper.py +27 -0
- tests/test_known_good.py +267 -0
- tests/test_matp.py +437 -0
- tests/test_model.py +54 -0
- tests/test_omodel.py +119 -0
- tests/test_paths.py +22 -0
- tests/test_report.py +251 -0
- tests/test_repr.py +21 -0
- tests/test_routine.py +178 -0
- tests/test_rtn_dcopf.py +101 -0
- tests/test_rtn_dcpf.py +77 -0
- tests/test_rtn_ed.py +279 -0
- tests/test_rtn_pflow.py +219 -0
- tests/test_rtn_rted.py +273 -0
- tests/test_rtn_uc.py +248 -0
- tests/test_service.py +73 -0
- ltbams-0.9.9.dist-info/LICENSE +0 -692
- ltbams-0.9.9.dist-info/METADATA +0 -859
- ltbams-0.9.9.dist-info/RECORD +0 -14
- ltbams-0.9.9.dist-info/top_level.txt +0 -1
- {ltbams-0.9.9.dist-info → ltbams-1.0.2a1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,1803 @@
|
|
1
|
+
"""
|
2
|
+
Module to solve OPF.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import logging
|
6
|
+
from copy import deepcopy
|
7
|
+
|
8
|
+
import numpy as np
|
9
|
+
from numpy import flatnonzero as find
|
10
|
+
|
11
|
+
import scipy.sparse as sp
|
12
|
+
from scipy.sparse import csr_matrix as c_sparse
|
13
|
+
|
14
|
+
from andes.shared import deg2rad
|
15
|
+
from andes.utils.misc import elapsed
|
16
|
+
|
17
|
+
from ams.pypower.core import (ppoption, pipsopf_solver, ipoptopf_solver)
|
18
|
+
from ams.pypower.utils import isload, fairmax
|
19
|
+
from ams.pypower.idx import IDX
|
20
|
+
from ams.pypower.io import loadcase
|
21
|
+
from ams.pypower.make import (makeYbus, makeAvl, makeApq,
|
22
|
+
makeAang, makeAy)
|
23
|
+
|
24
|
+
import ams.pypower.routines.opffcns as opfcn
|
25
|
+
|
26
|
+
from ams.pypower.toggle import toggle_reserves
|
27
|
+
|
28
|
+
from ams.shared import inf
|
29
|
+
|
30
|
+
logger = logging.getLogger(__name__)
|
31
|
+
|
32
|
+
|
33
|
+
def runopf(casedata, ppopt):
|
34
|
+
"""
|
35
|
+
Runs an optimal power flow.
|
36
|
+
|
37
|
+
@see: L{rundcopf}, L{runuopf}
|
38
|
+
|
39
|
+
@author: Ray Zimmerman (PSERC Cornell)
|
40
|
+
"""
|
41
|
+
sstats = dict(solver_name='PYPOWER',
|
42
|
+
num_iters=1) # solver stats
|
43
|
+
ppopt = ppoption(ppopt)
|
44
|
+
r = fopf(casedata, ppopt)
|
45
|
+
sstats['solver_name'] = 'PYPOWER-PIPS'
|
46
|
+
sstats['num_iters'] = r['raw']['output']['iterations']
|
47
|
+
return r, sstats
|
48
|
+
|
49
|
+
|
50
|
+
def runuopf(casedata, ppopt):
|
51
|
+
"""
|
52
|
+
Runs an optimal power flow with unit-decommitment heuristic.
|
53
|
+
|
54
|
+
@see: L{rundcopf}, L{runuopf}
|
55
|
+
|
56
|
+
@author: Ray Zimmerman (PSERC Cornell)
|
57
|
+
"""
|
58
|
+
# default arguments
|
59
|
+
ppopt = ppoption(ppopt)
|
60
|
+
|
61
|
+
# ----- run the unit de-commitment / optimal power flow -----
|
62
|
+
r = uopf(casedata, ppopt)
|
63
|
+
|
64
|
+
return r
|
65
|
+
|
66
|
+
|
67
|
+
def runduopf(casedata, ppopt):
|
68
|
+
"""
|
69
|
+
Runs a DC optimal power flow with unit-decommitment heuristic.
|
70
|
+
|
71
|
+
@see: L{rundcopf}, L{runuopf}
|
72
|
+
|
73
|
+
@author: Ray Zimmerman (PSERC Cornell)
|
74
|
+
"""
|
75
|
+
# default arguments
|
76
|
+
ppopt = ppoption(ppopt, PF_DC=True)
|
77
|
+
|
78
|
+
return runuopf(casedata, ppopt)
|
79
|
+
|
80
|
+
|
81
|
+
def runopf_w_res(*args):
|
82
|
+
"""
|
83
|
+
Runs an optimal power flow with fixed zonal reserves.
|
84
|
+
|
85
|
+
Runs an optimal power flow with the addition of reserve requirements
|
86
|
+
specified as a set of fixed zonal reserves. See L{runopf} for a
|
87
|
+
description of the input and output arguments, which are the same,
|
88
|
+
with the exception that the case file or dict C{casedata} must define
|
89
|
+
a 'reserves' field, which is a dict with the following fields:
|
90
|
+
- C{zones} C{nrz x ng}, C{zone(i, j) = 1}, if gen C{j} belongs
|
91
|
+
to zone C{i} 0, otherwise
|
92
|
+
- C{req} C{nrz x 1}, zonal reserve requirement in MW
|
93
|
+
- C{cost} (C{ng} or C{ngr}) C{x 1}, cost of reserves in $/MW
|
94
|
+
- C{qty} (C{ng} or C{ngr}) C{x 1}, max quantity of reserves
|
95
|
+
in MW (optional)
|
96
|
+
where C{nrz} is the number of reserve zones and C{ngr} is the number of
|
97
|
+
generators belonging to at least one reserve zone and C{ng} is the total
|
98
|
+
number of generators.
|
99
|
+
|
100
|
+
In addition to the normal OPF output, the C{results} dict contains a
|
101
|
+
new 'reserves' field with the following fields, in addition to those
|
102
|
+
provided in the input:
|
103
|
+
- C{R} - C{ng x 1}, reserves provided by each gen in MW
|
104
|
+
- C{Rmin} - C{ng x 1}, lower limit on reserves provided by
|
105
|
+
each gen, (MW)
|
106
|
+
- C{Rmax} - C{ng x 1}, upper limit on reserves provided by
|
107
|
+
each gen, (MW)
|
108
|
+
- C{mu.l} - C{ng x 1}, shadow price on reserve lower limit, ($/MW)
|
109
|
+
- C{mu.u} - C{ng x 1}, shadow price on reserve upper limit, ($/MW)
|
110
|
+
- C{mu.Pmax} - C{ng x 1}, shadow price on C{Pg + R <= Pmax}
|
111
|
+
constraint, ($/MW)
|
112
|
+
- C{prc} - C{ng x 1}, reserve price for each gen equal to
|
113
|
+
maximum of the shadow prices on the zonal requirement constraint
|
114
|
+
for each zone the generator belongs to
|
115
|
+
|
116
|
+
See L{t.t_case30_userfcns} for an example case file with fixed reserves,
|
117
|
+
and L{toggle_reserves} for the implementation.
|
118
|
+
|
119
|
+
Calling syntax options::
|
120
|
+
results = runopf_w_res(casedata)
|
121
|
+
results = runopf_w_res(casedata, ppopt)
|
122
|
+
results = runopf_w_res(casedata, ppopt, fname)
|
123
|
+
results = runopf_w_res(casedata, [popt, fname, solvedcase)
|
124
|
+
results, success = runopf_w_res(...)
|
125
|
+
|
126
|
+
Example::
|
127
|
+
results = runopf_w_res('t_case30_userfcns')
|
128
|
+
|
129
|
+
@see: L{runopf}, L{toggle_reserves}, L{t.t_case30_userfcns}
|
130
|
+
|
131
|
+
@author: Ray Zimmerman (PSERC Cornell)
|
132
|
+
"""
|
133
|
+
ppc = loadcase(args[0])
|
134
|
+
ppc = toggle_reserves(ppc, 'on')
|
135
|
+
|
136
|
+
r = runopf(ppc, *args[1:])
|
137
|
+
r = toggle_reserves(r, 'off')
|
138
|
+
|
139
|
+
return r
|
140
|
+
|
141
|
+
|
142
|
+
def fopf(*args):
|
143
|
+
"""
|
144
|
+
Solve an optimal power flow, return a `results` dict,
|
145
|
+
previously named ``opf``.
|
146
|
+
|
147
|
+
The data for the problem can be specified in one of three ways:
|
148
|
+
1. a string (ppc) containing the file name of a PYPOWER case
|
149
|
+
which defines the data matrices baseMVA, bus, gen, branch, and
|
150
|
+
gencost (areas is not used at all, it is only included for
|
151
|
+
backward compatibility of the API).
|
152
|
+
2. a dict (ppc) containing the data matrices as fields.
|
153
|
+
3. the individual data matrices themselves.
|
154
|
+
|
155
|
+
The optional user parameters for user constraints (A, l, u), user costs
|
156
|
+
(N, fparm, H, Cw), user variable initializer (z0), and user variable
|
157
|
+
limits (zl, zu) can also be specified as fields in a case dict,
|
158
|
+
either passed in directly or defined in a case file referenced by name.
|
159
|
+
|
160
|
+
When specified, A, l, u represent additional linear constraints on the
|
161
|
+
optimization variables, l <= A*[x z] <= u. If the user specifies an A
|
162
|
+
matrix that has more columns than the number of "x" (OPF) variables,
|
163
|
+
then there are extra linearly constrained "z" variables. For an
|
164
|
+
explanation of the formulation used and instructions for forming the
|
165
|
+
A matrix, see the MATPOWER manual.
|
166
|
+
|
167
|
+
A generalized cost on all variables can be applied if input arguments
|
168
|
+
N, fparm, H, and Cw are specified. First, a linear transformation
|
169
|
+
of the optimization variables is defined by means of r = N * [x z].
|
170
|
+
Then, to each element of r a function is applied as encoded in the
|
171
|
+
fparm matrix (see MATPOWER manual). If the resulting vector is named
|
172
|
+
w, then H and Cw define a quadratic cost on w:
|
173
|
+
(1/2)*w'*H*w + Cw * w. H and N should be sparse matrices and H
|
174
|
+
should also be symmetric.
|
175
|
+
|
176
|
+
The optional ppopt vector specifies PYPOWER options. If the OPF
|
177
|
+
algorithm is not explicitly set in the options, PYPOWER will use the default
|
178
|
+
solver, based on a primal-dual interior point method. For the AC OPF, this
|
179
|
+
is OPF_ALG = 560. For the DC OPF, the default is OPF_ALG_DC = 200.
|
180
|
+
See L{ppoption} for more details on the available OPF solvers and other OPF
|
181
|
+
options and their default values.
|
182
|
+
|
183
|
+
The solved case is returned in a single results dict (described
|
184
|
+
below). Also returned are the final objective function value (f) and a
|
185
|
+
flag which is True if the algorithm was successful in finding a solution
|
186
|
+
(success). Additional optional return values are an algorithm specific
|
187
|
+
return status (info), elapsed time in seconds (et), the constraint
|
188
|
+
vector (g), the Jacobian matrix (jac), and the vector of variables
|
189
|
+
(xr) as well as the constraint multipliers (pimul).
|
190
|
+
|
191
|
+
The single results dict is a PYPOWER case struct (ppc) with the
|
192
|
+
usual baseMVA, bus, branch, gen, gencost fields, along with the
|
193
|
+
following additional fields:
|
194
|
+
|
195
|
+
- order see 'help ext2int' for details of this field
|
196
|
+
- et elapsed time in seconds for solving OPF
|
197
|
+
- success 1 if solver converged successfully, 0 otherwise
|
198
|
+
- om OPF model object, see 'help opf_model'
|
199
|
+
- x final value of optimization variables (internal order)
|
200
|
+
- f final objective function value
|
201
|
+
- mu shadow prices on ...
|
202
|
+
- var
|
203
|
+
- l lower bounds on variables
|
204
|
+
- u upper bounds on variables
|
205
|
+
- nln
|
206
|
+
- l lower bounds on nonlinear constraints
|
207
|
+
- u upper bounds on nonlinear constraints
|
208
|
+
- lin
|
209
|
+
- l lower bounds on linear constraints
|
210
|
+
- u upper bounds on linear constraints
|
211
|
+
- g (optional) constraint values
|
212
|
+
- dg (optional) constraint 1st derivatives
|
213
|
+
- df (optional) obj fun 1st derivatives (not yet implemented)
|
214
|
+
- d2f (optional) obj fun 2nd derivatives (not yet implemented)
|
215
|
+
- raw raw solver output in form returned by MINOS, and more
|
216
|
+
- xr final value of optimization variables
|
217
|
+
- pimul constraint multipliers
|
218
|
+
- info solver specific termination code
|
219
|
+
- output solver specific output information
|
220
|
+
- alg algorithm code of solver used
|
221
|
+
- var
|
222
|
+
- val optimization variable values, by named block
|
223
|
+
- Va voltage angles
|
224
|
+
- Vm voltage magnitudes (AC only)
|
225
|
+
- Pg real power injections
|
226
|
+
- Qg reactive power injections (AC only)
|
227
|
+
- y constrained cost variable (only if have pwl costs)
|
228
|
+
- (other) any user-defined variable blocks
|
229
|
+
- mu variable bound shadow prices, by named block
|
230
|
+
- l lower bound shadow prices
|
231
|
+
- Va, Vm, Pg, Qg, y, (other)
|
232
|
+
- u upper bound shadow prices
|
233
|
+
- Va, Vm, Pg, Qg, y, (other)
|
234
|
+
- nln (AC only)
|
235
|
+
- mu shadow prices on nonlinear constraints, by named block
|
236
|
+
- l lower bounds
|
237
|
+
- Pmis real power mismatch equations
|
238
|
+
- Qmis reactive power mismatch equations
|
239
|
+
- Sf flow limits at "from" end of branches
|
240
|
+
- St flow limits at "to" end of branches
|
241
|
+
- u upper bounds
|
242
|
+
- Pmis, Qmis, Sf, St
|
243
|
+
- lin
|
244
|
+
- mu shadow prices on linear constraints, by named block
|
245
|
+
- l lower bounds
|
246
|
+
- Pmis real power mistmatch equations (DC only)
|
247
|
+
- Pf flow limits at "from" end of branches (DC only)
|
248
|
+
- Pt flow limits at "to" end of branches (DC only)
|
249
|
+
- PQh upper portion of gen PQ-capability curve (AC only)
|
250
|
+
- PQl lower portion of gen PQ-capability curve (AC only)
|
251
|
+
- vl constant power factor constraint for loads
|
252
|
+
- ycon basin constraints for CCV for pwl costs
|
253
|
+
- (other) any user-defined constraint blocks
|
254
|
+
- u upper bounds
|
255
|
+
- Pmis, Pf, Pt, PQh, PQl, vl, ycon, (other)
|
256
|
+
- cost user-defined cost values, by named block
|
257
|
+
|
258
|
+
Author
|
259
|
+
------
|
260
|
+
Ray Zimmerman (PSERC Cornell)
|
261
|
+
|
262
|
+
Carlos E. Murillo-Sanchez (PSERC Cornell & Universidad Autonoma de Manizales)
|
263
|
+
"""
|
264
|
+
# ----- initialization -----
|
265
|
+
t0, _ = elapsed() # start timer
|
266
|
+
|
267
|
+
# process input arguments
|
268
|
+
ppc, ppopt = opf_args2(*args)
|
269
|
+
|
270
|
+
# add zero columns to bus, gen, branch for multipliers, etc if needed
|
271
|
+
nb = np.shape(ppc['bus'])[0] # number of buses
|
272
|
+
nl = np.shape(ppc['branch'])[0] # number of branches
|
273
|
+
ng = np.shape(ppc['gen'])[0] # number of dispatchable injections
|
274
|
+
if np.shape(ppc['bus'])[1] < IDX.bus.MU_VMIN + 1:
|
275
|
+
ppc['bus'] = np.c_[ppc['bus'], np.zeros((nb, IDX.bus.MU_VMIN + 1 - np.shape(ppc['bus'])[1]))]
|
276
|
+
|
277
|
+
if np.shape(ppc['gen'])[1] < IDX.gen.MU_QMIN + 1:
|
278
|
+
ppc['gen'] = np.c_[ppc['gen'], np.zeros((ng, IDX.gen.MU_QMIN + 1 - np.shape(ppc['gen'])[1]))]
|
279
|
+
|
280
|
+
if np.shape(ppc['branch'])[1] < IDX.branch.MU_ANGMAX + 1:
|
281
|
+
ppc['branch'] = np.c_[ppc['branch'], np.zeros((nl, IDX.branch.MU_ANGMAX + 1 - np.shape(ppc['branch'])[1]))]
|
282
|
+
|
283
|
+
# ----- convert to internal numbering, remove out-of-service stuff -----
|
284
|
+
ppc = opfcn.ext2int(ppc)
|
285
|
+
|
286
|
+
# ----- construct OPF model object -----
|
287
|
+
om = opf_setup(ppc, ppopt)
|
288
|
+
|
289
|
+
# ----- execute the OPF -----
|
290
|
+
results, success, raw = opf_execute(om, ppopt)
|
291
|
+
|
292
|
+
# ----- revert to original ordering, including out-of-service stuff -----
|
293
|
+
results = opfcn.int2ext(results)
|
294
|
+
|
295
|
+
# zero out result fields of out-of-service gens & branches
|
296
|
+
if len(results['order']['gen']['status']['off']) > 0:
|
297
|
+
results['gen'][
|
298
|
+
np.ix_(
|
299
|
+
results['order']['gen']['status']['off'],
|
300
|
+
[IDX.gen.PG,
|
301
|
+
IDX.gen.QG,
|
302
|
+
IDX.gen.MU_PMAX,
|
303
|
+
IDX.gen.MU_PMIN])] = 0
|
304
|
+
|
305
|
+
if len(results['order']['branch']['status']['off']) > 0:
|
306
|
+
results['branch'][
|
307
|
+
np.ix_(
|
308
|
+
results['order']['branch']['status']['off'],
|
309
|
+
[IDX.branch.PF,
|
310
|
+
IDX.branch.QF,
|
311
|
+
IDX.branch.PT,
|
312
|
+
IDX.branch.QT,
|
313
|
+
IDX.branch.MU_SF,
|
314
|
+
IDX.branch.MU_ST,
|
315
|
+
IDX.branch.MU_ANGMIN,
|
316
|
+
IDX.branch.MU_ANGMAX])] = 0
|
317
|
+
|
318
|
+
# ----- finish preparing output -----
|
319
|
+
_, results['et'] = elapsed(t0)
|
320
|
+
|
321
|
+
results['success'] = success
|
322
|
+
results['raw'] = raw
|
323
|
+
|
324
|
+
return results
|
325
|
+
|
326
|
+
|
327
|
+
def opf_setup(ppc, ppopt):
|
328
|
+
"""Constructs an OPF model object from a PYPOWER case dict.
|
329
|
+
|
330
|
+
Assumes that ppc is a PYPOWER case dict with internal indexing,
|
331
|
+
all equipment in-service, etc.
|
332
|
+
|
333
|
+
@see: L{opf}, L{ext2int}, L{opf_execute}
|
334
|
+
|
335
|
+
@author: Ray Zimmerman (PSERC Cornell)
|
336
|
+
@author: Carlos E. Murillo-Sanchez (PSERC Cornell & Universidad
|
337
|
+
Autonoma de Manizales)
|
338
|
+
"""
|
339
|
+
# options
|
340
|
+
alg = ppopt['OPF_ALG']
|
341
|
+
|
342
|
+
# data dimensions
|
343
|
+
nb = ppc['bus'].shape[0] # number of buses
|
344
|
+
nl = ppc['branch'].shape[0] # number of branches
|
345
|
+
ng = ppc['gen'].shape[0] # number of dispatchable injections
|
346
|
+
if 'A' in ppc:
|
347
|
+
nusr = ppc['A'].shape[0] # number of linear user constraints
|
348
|
+
else:
|
349
|
+
nusr = 0
|
350
|
+
|
351
|
+
if 'N' in ppc:
|
352
|
+
nw = ppc['N'].shape[0] # number of general cost vars, w
|
353
|
+
else:
|
354
|
+
nw = 0
|
355
|
+
|
356
|
+
# convert single-block piecewise-linear costs into linear polynomial cost
|
357
|
+
pwl1 = find((ppc['gencost'][:, IDX.cost.MODEL] == IDX.cost.PW_LINEAR)
|
358
|
+
& (ppc['gencost'][:, IDX.cost.NCOST] == 2))
|
359
|
+
# p1 = np.array([])
|
360
|
+
if len(pwl1) > 0:
|
361
|
+
x0 = ppc['gencost'][pwl1, IDX.cost.COST]
|
362
|
+
y0 = ppc['gencost'][pwl1, IDX.cost.COST + 1]
|
363
|
+
x1 = ppc['gencost'][pwl1, IDX.cost.COST + 2]
|
364
|
+
y1 = ppc['gencost'][pwl1, IDX.cost.COST + 3]
|
365
|
+
m = (y1 - y0) / (x1 - x0)
|
366
|
+
b = y0 - m * x0
|
367
|
+
ppc['gencost'][pwl1, IDX.cost.MODEL] = IDX.cost.POLYNOMIAL
|
368
|
+
ppc['gencost'][pwl1, IDX.cost.NCOST] = 2
|
369
|
+
ppc['gencost'][pwl1, IDX.cost.COST:IDX.cost.COST + 2] = np.r_[m, b]
|
370
|
+
|
371
|
+
# create (read-only) copies of individual fields for convenience
|
372
|
+
baseMVA, bus, gen, branch, gencost, _, lbu, ubu, ppopt, \
|
373
|
+
_, fparm, H, Cw, z0, zl, zu, userfcn, _ = opf_args(ppc, ppopt)
|
374
|
+
|
375
|
+
# warn if there is more than one reference bus
|
376
|
+
refs = find(bus[:, IDX.bus.BUS_TYPE] == IDX.bus.REF)
|
377
|
+
if len(refs) > 1:
|
378
|
+
errstr = 'opf_setup: Warning: Multiple reference buses.\n' + \
|
379
|
+
' For a system with islands, a reference bus in each island\n' + \
|
380
|
+
' may help convergence, but in a fully connected system such\n' + \
|
381
|
+
' a situation is probably not reasonable.\n\n'
|
382
|
+
logger.info(errstr)
|
383
|
+
|
384
|
+
# set up initial variables and bounds
|
385
|
+
gbus = gen[:, IDX.gen.GEN_BUS].astype(int)
|
386
|
+
Va = bus[:, IDX.bus.VA] * deg2rad
|
387
|
+
Vm = bus[:, IDX.bus.VM].copy()
|
388
|
+
Vm[gbus] = gen[:, IDX.gen.VG] # buses with gens, init Vm from gen data
|
389
|
+
Pg = gen[:, IDX.gen.PG] / baseMVA
|
390
|
+
Qg = gen[:, IDX.gen.QG] / baseMVA
|
391
|
+
Pmin = gen[:, IDX.gen.PMIN] / baseMVA
|
392
|
+
Pmax = gen[:, IDX.gen.PMAX] / baseMVA
|
393
|
+
Qmin = gen[:, IDX.gen.QMIN] / baseMVA
|
394
|
+
Qmax = gen[:, IDX.gen.QMAX] / baseMVA
|
395
|
+
|
396
|
+
# AC model with more problem dimensions
|
397
|
+
nv = nb # number of voltage magnitude vars
|
398
|
+
nq = ng # number of Qg vars
|
399
|
+
q1 = ng # index of 1st Qg column in Ay
|
400
|
+
|
401
|
+
# dispatchable load, constant power factor constraints
|
402
|
+
Avl, lvl, uvl, _ = makeAvl(baseMVA, gen)
|
403
|
+
|
404
|
+
# generator PQ capability curve constraints
|
405
|
+
Apqh, ubpqh, Apql, ubpql, Apqdata = makeApq(baseMVA, gen)
|
406
|
+
|
407
|
+
user_vars = ['Va', 'Vm', 'Pg', 'Qg']
|
408
|
+
ycon_vars = ['Pg', 'Qg', 'y']
|
409
|
+
|
410
|
+
# voltage angle reference constraints
|
411
|
+
Vau = inf * np.ones(nb)
|
412
|
+
Val = -Vau
|
413
|
+
Vau[refs] = Va[refs]
|
414
|
+
Val[refs] = Va[refs]
|
415
|
+
|
416
|
+
# branch voltage angle difference limits
|
417
|
+
Aang, lang, uang, iang = makeAang(baseMVA, branch, nb, ppopt)
|
418
|
+
|
419
|
+
# basin constraints for piece-wise linear gen cost variables
|
420
|
+
if alg == 545 or alg == 550: # SC-PDIPM or TRALM, no CCV cost vars
|
421
|
+
ny = 0
|
422
|
+
Ay = None
|
423
|
+
by = np.array([])
|
424
|
+
else:
|
425
|
+
ipwl = find(gencost[:, IDX.cost.MODEL] == IDX.cost.PW_LINEAR) # piece-wise linear costs
|
426
|
+
ny = ipwl.shape[0] # number of piece-wise linear cost vars
|
427
|
+
Ay, by = makeAy(baseMVA, ng, gencost, 1, q1, 1+ng+nq)
|
428
|
+
|
429
|
+
if np.any((gencost[:, IDX.cost.MODEL] != IDX.cost.POLYNOMIAL) &
|
430
|
+
(gencost[:, IDX.cost.MODEL] != IDX.cost.PW_LINEAR)):
|
431
|
+
logger.debug('opf_setup: some generator cost rows have invalid MODEL value\n')
|
432
|
+
|
433
|
+
# more problem dimensions
|
434
|
+
nx = nb+nv + ng+nq # number of standard OPF control variables
|
435
|
+
if nusr:
|
436
|
+
nz = ppc['A'].shape[1] - nx # number of user z variables
|
437
|
+
if nz < 0:
|
438
|
+
logger.debug('opf_setup: user supplied A matrix must have at least %d columns.\n' % nx)
|
439
|
+
else:
|
440
|
+
nz = 0 # number of user z variables
|
441
|
+
if nw: # still need to check number of columns of N
|
442
|
+
if ppc['N'].shape[1] != nx:
|
443
|
+
logger.debug('opf_setup: user supplied N matrix must have %d columns.\n' % nx)
|
444
|
+
|
445
|
+
# construct OPF model object
|
446
|
+
om = opf_model(ppc)
|
447
|
+
if len(pwl1) > 0:
|
448
|
+
om.userdata('pwl1', pwl1)
|
449
|
+
|
450
|
+
om.userdata('Apqdata', Apqdata)
|
451
|
+
om.userdata('iang', iang)
|
452
|
+
om.add_vars('Va', nb, Va, Val, Vau)
|
453
|
+
om.add_vars('Vm', nb, Vm, bus[:, IDX.bus.VMIN], bus[:, IDX.bus.VMAX])
|
454
|
+
om.add_vars('Pg', ng, Pg, Pmin, Pmax)
|
455
|
+
om.add_vars('Qg', ng, Qg, Qmin, Qmax)
|
456
|
+
om.add_constraints('Pmis', nb, 'nonlinear')
|
457
|
+
om.add_constraints('Qmis', nb, 'nonlinear')
|
458
|
+
om.add_constraints('Sf', nl, 'nonlinear')
|
459
|
+
om.add_constraints('St', nl, 'nonlinear')
|
460
|
+
om.add_constraints('PQh', Apqh, np.array([]), ubpqh, ['Pg', 'Qg']) # npqh
|
461
|
+
om.add_constraints('PQl', Apql, np.array([]), ubpql, ['Pg', 'Qg']) # npql
|
462
|
+
om.add_constraints('vl', Avl, lvl, uvl, ['Pg', 'Qg']) # nvl
|
463
|
+
om.add_constraints('ang', Aang, lang, uang, ['Va']) # nang
|
464
|
+
|
465
|
+
# y vars, constraints for piece-wise linear gen costs
|
466
|
+
if ny > 0:
|
467
|
+
om.add_vars('y', ny)
|
468
|
+
om.add_constraints('ycon', Ay, np.array([]), by, ycon_vars) # ncony
|
469
|
+
|
470
|
+
# add user vars, constraints and costs (as specified via A, ..., N, ...)
|
471
|
+
if nz > 0:
|
472
|
+
om.add_vars('z', nz, z0, zl, zu)
|
473
|
+
user_vars.append('z')
|
474
|
+
|
475
|
+
if nusr:
|
476
|
+
om.add_constraints('usr', ppc['A'], lbu, ubu, user_vars) # nusr
|
477
|
+
|
478
|
+
if nw:
|
479
|
+
user_cost = {}
|
480
|
+
user_cost['N'] = ppc['N']
|
481
|
+
user_cost['Cw'] = Cw
|
482
|
+
if len(fparm) > 0:
|
483
|
+
user_cost['dd'] = fparm[:, 0]
|
484
|
+
user_cost['rh'] = fparm[:, 1]
|
485
|
+
user_cost['kk'] = fparm[:, 2]
|
486
|
+
user_cost['mm'] = fparm[:, 3]
|
487
|
+
|
488
|
+
# if len(H) > 0:
|
489
|
+
user_cost['H'] = H
|
490
|
+
|
491
|
+
om.add_costs('usr', user_cost, user_vars)
|
492
|
+
|
493
|
+
# execute userfcn callbacks for 'formulation' stage
|
494
|
+
opfcn.run_userfcn(userfcn, 'formulation', om)
|
495
|
+
|
496
|
+
return om
|
497
|
+
|
498
|
+
|
499
|
+
class opf_model(object):
|
500
|
+
"""This class implements the OPF model object used to encapsulate
|
501
|
+
a given OPF problem formulation. It allows for access to optimization
|
502
|
+
variables, constraints and costs in named blocks, keeping track of the
|
503
|
+
ordering and indexing of the blocks as variables, constraints and costs
|
504
|
+
are added to the problem.
|
505
|
+
|
506
|
+
@author: Ray Zimmerman (PSERC Cornell)
|
507
|
+
"""
|
508
|
+
|
509
|
+
def __init__(self, ppc):
|
510
|
+
#: PYPOWER case dict used to build the object.
|
511
|
+
self.ppc = ppc
|
512
|
+
|
513
|
+
#: data for optimization variable sets that make up the
|
514
|
+
# full optimization variable x
|
515
|
+
self.var = {
|
516
|
+
'idx': {
|
517
|
+
'i1': {}, # starting index within x
|
518
|
+
'iN': {}, # ending index within x
|
519
|
+
'N': {} # number of elements in this variable set
|
520
|
+
},
|
521
|
+
'N': 0, # total number of elements in x
|
522
|
+
'NS': 0, # number of variable sets or named blocks
|
523
|
+
'data': { # bounds and initial value data
|
524
|
+
'v0': {}, # vector of initial values
|
525
|
+
'vl': {}, # vector of lower bounds
|
526
|
+
'vu': {}, # vector of upper bounds
|
527
|
+
},
|
528
|
+
'order': [] # list of names for variable blocks in the order they appear in x
|
529
|
+
}
|
530
|
+
|
531
|
+
#: data for nonlinear constraints that make up the
|
532
|
+
# full set of nonlinear constraints ghn(x)
|
533
|
+
self.nln = {
|
534
|
+
'idx': {
|
535
|
+
'i1': {}, # starting index within ghn(x)
|
536
|
+
'iN': {}, # ending index within ghn(x)
|
537
|
+
'N': {} # number of elements in this constraint set
|
538
|
+
},
|
539
|
+
'N': 0, # total number of elements in ghn(x)
|
540
|
+
'NS': 0, # number of nonlinear constraint sets or named blocks
|
541
|
+
'order': [] # list of names for nonlinear constraint blocks in the order they appear in ghn(x)
|
542
|
+
}
|
543
|
+
|
544
|
+
#: data for linear constraints that make up the
|
545
|
+
# full set of linear constraints ghl(x)
|
546
|
+
self.lin = {
|
547
|
+
'idx': {
|
548
|
+
'i1': {}, # starting index within ghl(x)
|
549
|
+
'iN': {}, # ending index within ghl(x)
|
550
|
+
'N': {} # number of elements in this constraint set
|
551
|
+
},
|
552
|
+
'N': 0, # total number of elements in ghl(x)
|
553
|
+
'NS': 0, # number of linear constraint sets or named blocks
|
554
|
+
'data': { # data for l <= A*xx <= u linear constraints
|
555
|
+
'A': {}, # sparse linear constraint matrix
|
556
|
+
'l': {}, # left hand side vector, bounding A*x below
|
557
|
+
'u': {}, # right hand side vector, bounding A*x above
|
558
|
+
'vs': {} # cell array of variable sets that define the xx for this constraint block
|
559
|
+
},
|
560
|
+
'order': [] # list of names for linear constraint blocks in the order they appear in ghl(x)
|
561
|
+
}
|
562
|
+
|
563
|
+
#: data for user-defined costs
|
564
|
+
self.cost = {
|
565
|
+
'idx': {
|
566
|
+
'i1': {}, # starting row index within full N matrix
|
567
|
+
'iN': {}, # ending row index within full N matrix
|
568
|
+
'N': {} # number of rows in this cost block in full N matrix
|
569
|
+
},
|
570
|
+
'N': 0, # total number of rows in full N matrix
|
571
|
+
'NS': 0, # number of cost blocks
|
572
|
+
'data': { # data for each user-defined cost block
|
573
|
+
'N': {}, # see help for add_costs() for details
|
574
|
+
'H': {}, # "
|
575
|
+
'Cw': {}, # "
|
576
|
+
'dd': {}, # "
|
577
|
+
'rh': {}, # "
|
578
|
+
'kk': {}, # "
|
579
|
+
'mm': {}, # "
|
580
|
+
'vs': {} # list of variable sets that define xx for this cost block, where the N for this block multiplies xx'
|
581
|
+
},
|
582
|
+
'order': [] # of names for cost blocks in the order they appear in the rows of the full N matrix
|
583
|
+
}
|
584
|
+
|
585
|
+
self.user_data = {}
|
586
|
+
|
587
|
+
# def __repr__(self):
|
588
|
+
# """String representation of the object.
|
589
|
+
# """
|
590
|
+
# s = ''
|
591
|
+
# if self.var['NS']:
|
592
|
+
# s += '\n%-22s %5s %8s %8s %8s\n' % ('VARIABLES', 'name', 'i1', 'iN', 'N')
|
593
|
+
# s += '%-22s %5s %8s %8s %8s\n' % ('=========', '------', '-----', '-----', '------')
|
594
|
+
# for k in range(self.var['NS']):
|
595
|
+
# name = self.var['order'][k]
|
596
|
+
# idx = self.var['idx']
|
597
|
+
# s += '%15d:%12s %8d %8d %8d\n' % (k, name, idx['i1'][name], idx['iN'][name], idx['N'][name])
|
598
|
+
|
599
|
+
# s += '%15s%31s\n' % (('var[\'NS\'] = %d' % self.var['NS']), ('var[\'N\'] = %d' % self.var['N']))
|
600
|
+
# s += '\n'
|
601
|
+
# else:
|
602
|
+
# s += '%s : <none>\n', 'VARIABLES'
|
603
|
+
|
604
|
+
# if self.nln['NS']:
|
605
|
+
# s += '\n%-22s %5s %8s %8s %8s\n' % ('NON-LINEAR CONSTRAINTS', 'name', 'i1', 'iN', 'N')
|
606
|
+
# s += '%-22s %5s %8s %8s %8s\n' % ('======================', '------', '-----', '-----', '------')
|
607
|
+
# for k in range(self.nln['NS']):
|
608
|
+
# name = self.nln['order'][k]
|
609
|
+
# idx = self.nln['idx']
|
610
|
+
# s += '%15d:%12s %8d %8d %8d\n' % (k, name, idx['i1'][name], idx['iN'][name], idx['N'][name])
|
611
|
+
|
612
|
+
# s += '%15s%31s\n' % (('nln.NS = %d' % self.nln['NS']), ('nln.N = %d' % self.nln['N']))
|
613
|
+
# s += '\n'
|
614
|
+
# else:
|
615
|
+
# s += '%s : <none>\n', 'NON-LINEAR CONSTRAINTS'
|
616
|
+
|
617
|
+
# if self.lin['NS']:
|
618
|
+
# s += '\n%-22s %5s %8s %8s %8s\n' % ('LINEAR CONSTRAINTS', 'name', 'i1', 'iN', 'N')
|
619
|
+
# s += '%-22s %5s %8s %8s %8s\n' % ('==================', '------', '-----', '-----', '------')
|
620
|
+
# for k in range(self.lin['NS']):
|
621
|
+
# name = self.lin['order'][k]
|
622
|
+
# idx = self.lin['idx']
|
623
|
+
# s += '%15d:%12s %8d %8d %8d\n' % (k, name, idx['i1'][name], idx['iN'][name], idx['N'][name])
|
624
|
+
|
625
|
+
# s += '%15s%31s\n' % (('lin.NS = %d' % self.lin['NS']), ('lin.N = %d' % self.lin['N']))
|
626
|
+
# s += '\n'
|
627
|
+
# else:
|
628
|
+
# s += '%s : <none>\n', 'LINEAR CONSTRAINTS'
|
629
|
+
|
630
|
+
# if self.cost['NS']:
|
631
|
+
# s += '\n%-22s %5s %8s %8s %8s\n' % ('COSTS', 'name', 'i1', 'iN', 'N')
|
632
|
+
# s += '%-22s %5s %8s %8s %8s\n' % ('=====', '------', '-----', '-----', '------')
|
633
|
+
# for k in range(self.cost['NS']):
|
634
|
+
# name = self.cost['order'][k]
|
635
|
+
# idx = self.cost['idx']
|
636
|
+
# s += '%15d:%12s %8d %8d %8d\n' % (k, name, idx['i1'][name], idx['iN'][name], idx['N'][name])
|
637
|
+
|
638
|
+
# s += '%15s%31s\n' % (('cost.NS = %d' % self.cost['NS']), ('cost.N = %d' % self.cost['N']))
|
639
|
+
# s += '\n'
|
640
|
+
# else:
|
641
|
+
# s += '%s : <none>\n' % 'COSTS'
|
642
|
+
|
643
|
+
# #s += ' ppc = '
|
644
|
+
# #if len(self.ppc):
|
645
|
+
# # s += '\n'
|
646
|
+
# #
|
647
|
+
# #s += str(self.ppc) + '\n'
|
648
|
+
|
649
|
+
# s += ' userdata = '
|
650
|
+
# if len(self.user_data):
|
651
|
+
# s += '\n'
|
652
|
+
|
653
|
+
# s += str(self.user_data)
|
654
|
+
|
655
|
+
# return s
|
656
|
+
|
657
|
+
def add_constraints(self, name, AorN, l, u=None, varsets=None):
|
658
|
+
"""Adds a set of constraints to the model.
|
659
|
+
|
660
|
+
Linear constraints are of the form C{l <= A * x <= u}, where
|
661
|
+
C{x} is a vector made of of the vars specified in C{varsets} (in
|
662
|
+
the order given). This allows the C{A} matrix to be defined only
|
663
|
+
in terms of the relevant variables without the need to manually
|
664
|
+
create a lot of zero columns. If C{varsets} is empty, C{x} is taken
|
665
|
+
to be the full vector of all optimization variables. If C{l} or
|
666
|
+
C{u} are empty, they are assumed to be appropriately sized vectors
|
667
|
+
of C{-Inf} and C{Inf}, respectively.
|
668
|
+
|
669
|
+
For nonlinear constraints, the 3rd argument, C{N}, is the number
|
670
|
+
of constraints in the set. Currently, this is used internally
|
671
|
+
by PYPOWER, but there is no way for the user to specify
|
672
|
+
additional nonlinear constraints.
|
673
|
+
"""
|
674
|
+
if u is None: # nonlinear
|
675
|
+
# prevent duplicate named constraint sets
|
676
|
+
if name in self.nln["idx"]["N"]:
|
677
|
+
logger.debug("opf_model.add_constraints: nonlinear constraint set named '%s' already exists\n" % name)
|
678
|
+
|
679
|
+
# add info about this nonlinear constraint set
|
680
|
+
self.nln["idx"]["i1"][name] = self.nln["N"] # + 1 ## starting index
|
681
|
+
self.nln["idx"]["iN"][name] = self.nln["N"] + AorN # ing index
|
682
|
+
self.nln["idx"]["N"][name] = AorN # number of constraints
|
683
|
+
|
684
|
+
# update number of nonlinear constraints and constraint sets
|
685
|
+
self.nln["N"] = self.nln["idx"]["iN"][name]
|
686
|
+
self.nln["NS"] = self.nln["NS"] + 1
|
687
|
+
|
688
|
+
# put name in ordered list of constraint sets
|
689
|
+
# self.nln["order"][self.nln["NS"]] = name
|
690
|
+
self.nln["order"].append(name)
|
691
|
+
else: # linear
|
692
|
+
# prevent duplicate named constraint sets
|
693
|
+
if name in self.lin["idx"]["N"]:
|
694
|
+
logger.debug('opf_model.add_constraints: linear constraint set named ''%s'' already exists\n' % name)
|
695
|
+
|
696
|
+
if varsets is None:
|
697
|
+
varsets = []
|
698
|
+
|
699
|
+
N, M = AorN.shape
|
700
|
+
if len(l) == 0: # default l is -Inf
|
701
|
+
l = -inf * np.ones(N)
|
702
|
+
|
703
|
+
if len(u) == 0: # default u is Inf
|
704
|
+
u = inf * np.ones(N)
|
705
|
+
|
706
|
+
if len(varsets) == 0:
|
707
|
+
varsets = self.var["order"]
|
708
|
+
|
709
|
+
# check sizes
|
710
|
+
if (l.shape[0] != N) or (u.shape[0] != N):
|
711
|
+
logger.debug('opf_model.add_constraints: sizes of A, l and u must match\n')
|
712
|
+
|
713
|
+
nv = 0
|
714
|
+
for k in range(len(varsets)):
|
715
|
+
nv = nv + self.var["idx"]["N"][varsets[k]]
|
716
|
+
|
717
|
+
if M != nv:
|
718
|
+
logger.debug(
|
719
|
+
'opf_model.add_constraints: number of columns of A does not match\nnumber of variables, A is %d x %d, nv = %d\n' % (N, M, nv))
|
720
|
+
|
721
|
+
# add info about this linear constraint set
|
722
|
+
self.lin["idx"]["i1"][name] = self.lin["N"] # + 1 ## starting index
|
723
|
+
self.lin["idx"]["iN"][name] = self.lin["N"] + N # ing index
|
724
|
+
self.lin["idx"]["N"][name] = N # number of constraints
|
725
|
+
self.lin["data"]["A"][name] = AorN
|
726
|
+
self.lin["data"]["l"][name] = l
|
727
|
+
self.lin["data"]["u"][name] = u
|
728
|
+
self.lin["data"]["vs"][name] = varsets
|
729
|
+
|
730
|
+
# update number of vars and var sets
|
731
|
+
self.lin["N"] = self.lin["idx"]["iN"][name]
|
732
|
+
self.lin["NS"] = self.lin["NS"] + 1
|
733
|
+
|
734
|
+
# put name in ordered list of var sets
|
735
|
+
# self.lin["order"][self.lin["NS"]] = name
|
736
|
+
self.lin["order"].append(name)
|
737
|
+
|
738
|
+
def add_costs(self, name, cp, varsets):
|
739
|
+
"""Adds a set of user costs to the model.
|
740
|
+
|
741
|
+
Adds a named block of user-defined costs to the model. Each set is
|
742
|
+
defined by the C{cp} dict described below. All user-defined sets of
|
743
|
+
costs are combined together into a single set of cost parameters in
|
744
|
+
a single C{cp} dict by L{build_cost_params}. This full aggregate set of
|
745
|
+
cost parameters can be retrieved from the model by L{get_cost_params}.
|
746
|
+
|
747
|
+
Let C{x} refer to the vector formed by combining the specified
|
748
|
+
C{varsets}, and C{f_u(x, cp)} be the cost at C{x} corresponding to the
|
749
|
+
cost parameters contained in C{cp}, where C{cp} is a dict with the
|
750
|
+
following fields::
|
751
|
+
N - nw x nx sparse matrix
|
752
|
+
Cw - nw x 1 vector
|
753
|
+
H - nw x nw sparse matrix (optional, all zeros by default)
|
754
|
+
dd, mm - nw x 1 vectors (optional, all ones by default)
|
755
|
+
rh, kk - nw x 1 vectors (optional, all zeros by default)
|
756
|
+
|
757
|
+
These parameters are used as follows to compute C{f_u(x, CP)}::
|
758
|
+
|
759
|
+
R = N*x - rh
|
760
|
+
|
761
|
+
/ kk(i), R(i) < -kk(i)
|
762
|
+
K(i) = < 0, -kk(i) <= R(i) <= kk(i)
|
763
|
+
\ -kk(i), R(i) > kk(i)
|
764
|
+
|
765
|
+
RR = R + K
|
766
|
+
|
767
|
+
U(i) = / 0, -kk(i) <= R(i) <= kk(i)
|
768
|
+
\ 1, otherwise
|
769
|
+
|
770
|
+
DDL(i) = / 1, dd(i) = 1
|
771
|
+
\ 0, otherwise
|
772
|
+
|
773
|
+
DDQ(i) = / 1, dd(i) = 2
|
774
|
+
\ 0, otherwise
|
775
|
+
|
776
|
+
Dl = diag(mm) * diag(U) * diag(DDL)
|
777
|
+
Dq = diag(mm) * diag(U) * diag(DDQ)
|
778
|
+
|
779
|
+
w = (Dl + Dq * diag(RR)) * RR
|
780
|
+
|
781
|
+
f_u(x, CP) = 1/2 * w'*H*w + Cw'*w
|
782
|
+
"""
|
783
|
+
# prevent duplicate named cost sets
|
784
|
+
if name in self.cost["idx"]["N"]:
|
785
|
+
logger.debug('opf_model.add_costs: cost set named \'%s\' already exists\n' % name)
|
786
|
+
|
787
|
+
if varsets is None:
|
788
|
+
varsets = []
|
789
|
+
|
790
|
+
if len(varsets) == 0:
|
791
|
+
varsets = self.var["order"]
|
792
|
+
|
793
|
+
nw, nx = cp["N"].shape
|
794
|
+
|
795
|
+
# check sizes
|
796
|
+
nv = 0
|
797
|
+
for k in range(len(varsets)):
|
798
|
+
nv = nv + self.var["idx"]["N"][varsets[k]]
|
799
|
+
|
800
|
+
if nx != nv:
|
801
|
+
if nw == 0:
|
802
|
+
cp["N"] = c_sparse(nw, nx)
|
803
|
+
else:
|
804
|
+
logger.debug(
|
805
|
+
'opf_model.add_costs: number of columns in N (%d x %d) does not match\nnumber of variables (%d)\n' %
|
806
|
+
(nw, nx, nv))
|
807
|
+
|
808
|
+
if cp["Cw"].shape[0] != nw:
|
809
|
+
logger.debug('opf_model.add_costs: number of rows of Cw (%d x %d) and N (%d x %d) must match\n' %
|
810
|
+
(cp["Cw"].shape[0], nw, nx))
|
811
|
+
|
812
|
+
if 'H' in cp:
|
813
|
+
if (cp["H"].shape[0] != nw) | (cp["H"].shape[1] != nw):
|
814
|
+
logger.debug('opf_model.add_costs: both dimensions of H (%d x %d) must match the number of rows in N (%d x %d)\n' % (
|
815
|
+
cp["H"].shape, nw, nx))
|
816
|
+
|
817
|
+
if 'dd' in cp:
|
818
|
+
if cp["dd"].shape[0] != nw:
|
819
|
+
logger.debug(
|
820
|
+
'opf_model.add_costs: number of rows of dd (%d x %d) and N (%d x %d) must match\n' %
|
821
|
+
(cp["dd"].shape, nw, nx))
|
822
|
+
|
823
|
+
if 'rh' in cp:
|
824
|
+
if cp["rh"].shape[0] != nw:
|
825
|
+
logger.debug(
|
826
|
+
'opf_model.add_costs: number of rows of rh (%d x %d) and N (%d x %d) must match\n' %
|
827
|
+
(cp["rh"].shape, nw, nx))
|
828
|
+
|
829
|
+
if 'kk' in cp:
|
830
|
+
if cp["kk"].shape[0] != nw:
|
831
|
+
logger.debug(
|
832
|
+
'opf_model.add_costs: number of rows of kk (%d x %d) and N (%d x %d) must match\n' %
|
833
|
+
(cp["kk"].shape, nw, nx))
|
834
|
+
|
835
|
+
if 'mm' in cp:
|
836
|
+
if cp["mm"].shape[0] != nw:
|
837
|
+
logger.debug(
|
838
|
+
'opf_model.add_costs: number of rows of mm (%d x %d) and N (%d x %d) must match\n' %
|
839
|
+
(cp["mm"].shape, nw, nx))
|
840
|
+
|
841
|
+
# add info about this user cost set
|
842
|
+
self.cost["idx"]["i1"][name] = self.cost["N"] # + 1 ## starting index
|
843
|
+
self.cost["idx"]["iN"][name] = self.cost["N"] + nw # ing index
|
844
|
+
self.cost["idx"]["N"][name] = nw # number of costs (nw)
|
845
|
+
self.cost["data"]["N"][name] = cp["N"]
|
846
|
+
self.cost["data"]["Cw"][name] = cp["Cw"]
|
847
|
+
self.cost["data"]["vs"][name] = varsets
|
848
|
+
if 'H' in cp:
|
849
|
+
self.cost["data"]["H"][name] = cp["H"]
|
850
|
+
|
851
|
+
if 'dd' in cp:
|
852
|
+
self.cost["data"]["dd"]["name"] = cp["dd"]
|
853
|
+
|
854
|
+
if 'rh' in cp:
|
855
|
+
self.cost["data"]["rh"]["name"] = cp["rh"]
|
856
|
+
|
857
|
+
if 'kk' in cp:
|
858
|
+
self.cost["data"]["kk"]["name"] = cp["kk"]
|
859
|
+
|
860
|
+
if 'mm' in cp:
|
861
|
+
self.cost["data"]["mm"]["name"] = cp["mm"]
|
862
|
+
|
863
|
+
# update number of vars and var sets
|
864
|
+
self.cost["N"] = self.cost["idx"]["iN"][name]
|
865
|
+
self.cost["NS"] = self.cost["NS"] + 1
|
866
|
+
|
867
|
+
# put name in ordered list of var sets
|
868
|
+
self.cost["order"].append(name)
|
869
|
+
|
870
|
+
def add_vars(self, name, N, v0=None, vl=None, vu=None):
|
871
|
+
""" Adds a set of variables to the model.
|
872
|
+
|
873
|
+
Adds a set of variables to the model, where N is the number of
|
874
|
+
variables in the set, C{v0} is the initial value of those variables,
|
875
|
+
and C{vl} and C{vu} are the lower and upper bounds on the variables.
|
876
|
+
The defaults for the last three arguments, which are optional,
|
877
|
+
are for all values to be initialized to zero (C{v0 = 0}) and unbounded
|
878
|
+
(C{VL = -Inf, VU = Inf}).
|
879
|
+
"""
|
880
|
+
# prevent duplicate named var sets
|
881
|
+
if name in self.var["idx"]["N"]:
|
882
|
+
logger.debug('opf_model.add_vars: variable set named ''%s'' already exists\n' % name)
|
883
|
+
|
884
|
+
if v0 is None or len(v0) == 0:
|
885
|
+
v0 = np.zeros(N) # init to zero by default
|
886
|
+
|
887
|
+
if vl is None or len(vl) == 0:
|
888
|
+
vl = -inf * np.ones(N) # unbounded below by default
|
889
|
+
|
890
|
+
if vu is None or len(vu) == 0:
|
891
|
+
vu = inf * np.ones(N) # unbounded above by default
|
892
|
+
|
893
|
+
# add info about this var set
|
894
|
+
self.var["idx"]["i1"][name] = self.var["N"] # + 1 ## starting index
|
895
|
+
self.var["idx"]["iN"][name] = self.var["N"] + N # ing index
|
896
|
+
self.var["idx"]["N"][name] = N # number of vars
|
897
|
+
self.var["data"]["v0"][name] = v0 # initial value
|
898
|
+
self.var["data"]["vl"][name] = vl # lower bound
|
899
|
+
self.var["data"]["vu"][name] = vu # upper bound
|
900
|
+
|
901
|
+
# update number of vars and var sets
|
902
|
+
self.var["N"] = self.var["idx"]["iN"][name]
|
903
|
+
self.var["NS"] = self.var["NS"] + 1
|
904
|
+
|
905
|
+
# put name in ordered list of var sets
|
906
|
+
# self.var["order"][self.var["NS"]] = name
|
907
|
+
self.var["order"].append(name)
|
908
|
+
|
909
|
+
def build_cost_params(self):
|
910
|
+
"""Builds and saves the full generalized cost parameters.
|
911
|
+
|
912
|
+
Builds the full set of cost parameters from the individual named
|
913
|
+
sub-sets added via L{add_costs}. Skips the building process if it has
|
914
|
+
already been done, unless a second input argument is present.
|
915
|
+
|
916
|
+
These cost parameters can be retrieved by calling L{get_cost_params}
|
917
|
+
and the user-defined costs evaluated by calling L{compute_cost}.
|
918
|
+
"""
|
919
|
+
# initialize parameters
|
920
|
+
nw = self.cost["N"]
|
921
|
+
# nnzN = 0
|
922
|
+
# nnzH = 0
|
923
|
+
# for k in range(self.cost["NS"]):
|
924
|
+
# name = self.cost["order"][k]
|
925
|
+
# nnzN = nnzN + nnz(self.cost["data"]["N"][name])
|
926
|
+
# if name in self.cost["data"]["H"]:
|
927
|
+
# nnzH = nnzH + nnz(self.cost["data"]["H"][name])
|
928
|
+
|
929
|
+
# FIXME Zero dimensional sparse matrices
|
930
|
+
N = np.zeros((nw, self.var["N"]))
|
931
|
+
H = np.zeros((nw, nw)) # default => no quadratic term
|
932
|
+
|
933
|
+
Cw = np.zeros(nw)
|
934
|
+
dd = np.ones(nw) # default => linear
|
935
|
+
rh = np.zeros(nw) # default => no shift
|
936
|
+
kk = np.zeros(nw) # default => no dead zone
|
937
|
+
mm = np.ones(nw) # default => no scaling
|
938
|
+
|
939
|
+
# fill in each piece
|
940
|
+
for k in range(self.cost["NS"]):
|
941
|
+
name = self.cost["order"][k]
|
942
|
+
Nk = self.cost["data"]["N"][name] # N for kth cost set
|
943
|
+
i1 = self.cost["idx"]["i1"][name] # starting row index
|
944
|
+
iN = self.cost["idx"]["iN"][name] # ing row index
|
945
|
+
if self.cost["idx"]["N"][name]: # non-zero number of rows to add
|
946
|
+
vsl = self.cost["data"]["vs"][name] # var set list
|
947
|
+
kN = 0 # initialize last col of Nk used
|
948
|
+
for v in vsl:
|
949
|
+
j1 = self.var["idx"]["i1"][v] # starting column in N
|
950
|
+
jN = self.var["idx"]["iN"][v] # ing column in N
|
951
|
+
k1 = kN # starting column in Nk
|
952
|
+
kN = kN + self.var["idx"]["N"][v] # ing column in Nk
|
953
|
+
N[i1:iN, j1:jN] = Nk[:, k1:kN].todense()
|
954
|
+
|
955
|
+
Cw[i1:iN] = self.cost["data"]["Cw"][name]
|
956
|
+
if name in self.cost["data"]["H"]:
|
957
|
+
H[i1:iN, i1:iN] = self.cost["data"]["H"][name].todense()
|
958
|
+
|
959
|
+
if name in self.cost["data"]["dd"]:
|
960
|
+
dd[i1:iN] = self.cost["data"]["dd"][name]
|
961
|
+
|
962
|
+
if name in self.cost["data"]["rh"]:
|
963
|
+
rh[i1:iN] = self.cost["data"]["rh"][name]
|
964
|
+
|
965
|
+
if name in self.cost["data"]["kk"]:
|
966
|
+
kk[i1:iN] = self.cost["data"]["kk"][name]
|
967
|
+
|
968
|
+
if name in self.cost["data"]["mm"]:
|
969
|
+
mm[i1:iN] = self.cost["data"]["mm"][name]
|
970
|
+
|
971
|
+
if nw:
|
972
|
+
N = c_sparse(N)
|
973
|
+
H = c_sparse(H)
|
974
|
+
|
975
|
+
# save in object
|
976
|
+
self.cost["params"] = {
|
977
|
+
'N': N, 'Cw': Cw, 'H': H, 'dd': dd, 'rh': rh, 'kk': kk, 'mm': mm}
|
978
|
+
|
979
|
+
def compute_cost(self, x, name=None):
|
980
|
+
""" Computes a user-defined cost.
|
981
|
+
|
982
|
+
Computes the value of a user defined cost, either for all user
|
983
|
+
defined costs or for a named set of costs. Requires calling
|
984
|
+
L{build_cost_params} first to build the full set of parameters.
|
985
|
+
|
986
|
+
Let C{x} be the full set of optimization variables and C{f_u(x, cp)} be
|
987
|
+
the user-defined cost at C{x}, corresponding to the set of cost
|
988
|
+
parameters in the C{cp} dict returned by L{get_cost_params}, where
|
989
|
+
C{cp} is a dict with the following fields::
|
990
|
+
N - nw x nx sparse matrix
|
991
|
+
Cw - nw x 1 vector
|
992
|
+
H - nw x nw sparse matrix (optional, all zeros by default)
|
993
|
+
dd, mm - nw x 1 vectors (optional, all ones by default)
|
994
|
+
rh, kk - nw x 1 vectors (optional, all zeros by default)
|
995
|
+
|
996
|
+
These parameters are used as follows to compute C{f_u(x, cp)}::
|
997
|
+
|
998
|
+
R = N*x - rh
|
999
|
+
|
1000
|
+
/ kk(i), R(i) < -kk(i)
|
1001
|
+
K(i) = < 0, -kk(i) <= R(i) <= kk(i)
|
1002
|
+
\ -kk(i), R(i) > kk(i)
|
1003
|
+
|
1004
|
+
RR = R + K
|
1005
|
+
|
1006
|
+
U(i) = / 0, -kk(i) <= R(i) <= kk(i)
|
1007
|
+
\ 1, otherwise
|
1008
|
+
|
1009
|
+
DDL(i) = / 1, dd(i) = 1
|
1010
|
+
\ 0, otherwise
|
1011
|
+
|
1012
|
+
DDQ(i) = / 1, dd(i) = 2
|
1013
|
+
\ 0, otherwise
|
1014
|
+
|
1015
|
+
Dl = diag(mm) * diag(U) * diag(DDL)
|
1016
|
+
Dq = diag(mm) * diag(U) * diag(DDQ)
|
1017
|
+
|
1018
|
+
w = (Dl + Dq * diag(RR)) * RR
|
1019
|
+
|
1020
|
+
F_U(X, CP) = 1/2 * w'*H*w + Cw'*w
|
1021
|
+
"""
|
1022
|
+
if name is None:
|
1023
|
+
cp = self.get_cost_params()
|
1024
|
+
else:
|
1025
|
+
cp = self.get_cost_params(name)
|
1026
|
+
|
1027
|
+
N, Cw, H, dd, rh, kk, mm = \
|
1028
|
+
cp["N"], cp["Cw"], cp["H"], cp["dd"], cp["rh"], cp["kk"], cp["mm"]
|
1029
|
+
nw = N.shape[0]
|
1030
|
+
r = N * x - rh # Nx - rhat
|
1031
|
+
iLT = find(r < -kk) # below dead zone
|
1032
|
+
iEQ = find((r == 0) & (kk == 0)) # dead zone doesn't exist
|
1033
|
+
iGT = find(r > kk) # above dead zone
|
1034
|
+
iND = np.r_[iLT, iEQ, iGT] # rows that are Not in the Dead region
|
1035
|
+
iL = find(dd == 1) # rows using linear function
|
1036
|
+
iQ = find(dd == 2) # rows using quadratic function
|
1037
|
+
LL = c_sparse((np.ones(len(iL)), (iL, iL)), (nw, nw))
|
1038
|
+
QQ = c_sparse((np.ones(len(iQ)), (iQ, iQ)), (nw, nw))
|
1039
|
+
kbar = c_sparse((np.r_[np.ones(len(iLT)),
|
1040
|
+
np.zeros(len(iEQ)),
|
1041
|
+
-np.ones(len(iGT))], (iND, iND)), (nw, nw)) * kk
|
1042
|
+
rr = r + kbar # apply non-dead zone shift
|
1043
|
+
M = c_sparse((mm[iND], (iND, iND)), (nw, nw)) # dead zone or scale
|
1044
|
+
diagrr = c_sparse((rr, (np.arange(nw), np.arange(nw))), (nw, nw))
|
1045
|
+
|
1046
|
+
# linear rows multiplied by rr(i), quadratic rows by rr(i)^2
|
1047
|
+
w = M * (LL + QQ * diagrr) * rr
|
1048
|
+
|
1049
|
+
f = np.dot(w * H, w) / 2 + np.dot(Cw, w)
|
1050
|
+
|
1051
|
+
return f
|
1052
|
+
|
1053
|
+
def get_cost_params(self, name=None):
|
1054
|
+
"""Returns the cost parameter struct for user-defined costs.
|
1055
|
+
|
1056
|
+
Requires calling L{build_cost_params} first to build the full set of
|
1057
|
+
parameters. Returns the full cost parameter struct for all user-defined
|
1058
|
+
costs that incorporates all of the named cost sets added via
|
1059
|
+
L{add_costs}, or, if a name is provided it returns the cost dict
|
1060
|
+
corresponding to the named set of cost rows (C{N} still has full number
|
1061
|
+
of columns).
|
1062
|
+
|
1063
|
+
The cost parameters are returned in a dict with the following fields::
|
1064
|
+
N - nw x nx sparse matrix
|
1065
|
+
Cw - nw x 1 vector
|
1066
|
+
H - nw x nw sparse matrix (optional, all zeros by default)
|
1067
|
+
dd, mm - nw x 1 vectors (optional, all ones by default)
|
1068
|
+
rh, kk - nw x 1 vectors (optional, all zeros by default)
|
1069
|
+
"""
|
1070
|
+
if not 'params' in self.cost:
|
1071
|
+
logger.debug('opf_model.get_cost_params: must call build_cost_params first\n')
|
1072
|
+
|
1073
|
+
cp = self.cost["params"]
|
1074
|
+
|
1075
|
+
if name is not None:
|
1076
|
+
if self.getN('cost', name):
|
1077
|
+
idx = np.arange(self.cost["idx"]["i1"][name], self.cost["idx"]["iN"][name])
|
1078
|
+
nwa = self.cost["idx"]["i1"][name]
|
1079
|
+
nwb = self.cost["idx"]["iN"][name]
|
1080
|
+
cp["N"] = cp["N"][idx, :]
|
1081
|
+
cp["Cw"] = cp["Cw"][idx]
|
1082
|
+
cp["H"] = cp["H"][nwa:nwb, nwa:nwb]
|
1083
|
+
cp["dd"] = cp["dd"][idx]
|
1084
|
+
cp["rh"] = cp["rh"][idx]
|
1085
|
+
cp["kk"] = cp["kk"][idx]
|
1086
|
+
cp["mm"] = cp["mm"][idx]
|
1087
|
+
|
1088
|
+
return cp
|
1089
|
+
|
1090
|
+
def get_idx(self):
|
1091
|
+
""" Returns the idx struct for vars, lin/nln constraints, costs.
|
1092
|
+
|
1093
|
+
Returns a structure for each with the beginning and ending
|
1094
|
+
index value and the number of elements for each named block.
|
1095
|
+
The 'i1' field (that's a one) is a dict with all of the
|
1096
|
+
starting indices, 'iN' contains all the ending indices and
|
1097
|
+
'N' contains all the sizes. Each is a dict whose keys are
|
1098
|
+
the named blocks.
|
1099
|
+
|
1100
|
+
Examples::
|
1101
|
+
[vv, ll, nn] = get_idx(om)
|
1102
|
+
|
1103
|
+
For a variable block named 'z' we have::
|
1104
|
+
vv['i1']['z'] - starting index for 'z' in optimization vector x
|
1105
|
+
vv['iN']['z'] - ending index for 'z' in optimization vector x
|
1106
|
+
vv["N"] - number of elements in 'z'
|
1107
|
+
|
1108
|
+
To extract a 'z' variable from x::
|
1109
|
+
z = x(vv['i1']['z']:vv['iN']['z'])
|
1110
|
+
|
1111
|
+
To extract the multipliers on a linear constraint set
|
1112
|
+
named 'foo', where mu_l and mu_u are the full set of
|
1113
|
+
linear constraint multipliers::
|
1114
|
+
mu_l_foo = mu_l(ll['i1']['foo']:ll['iN']['foo'])
|
1115
|
+
mu_u_foo = mu_u(ll['i1']['foo']:ll['iN']['foo'])
|
1116
|
+
|
1117
|
+
The number of nonlinear constraints in a set named 'bar'::
|
1118
|
+
nbar = nn["N"].bar
|
1119
|
+
(note: the following is preferable ::
|
1120
|
+
nbar = getN(om, 'nln', 'bar')
|
1121
|
+
... if you haven't already called L{get_idx} to get C{nn}.)
|
1122
|
+
"""
|
1123
|
+
vv = self.var["idx"]
|
1124
|
+
ll = self.lin["idx"]
|
1125
|
+
nn = self.nln["idx"]
|
1126
|
+
cc = self.cost["idx"]
|
1127
|
+
|
1128
|
+
return vv, ll, nn, cc
|
1129
|
+
|
1130
|
+
def get_ppc(self):
|
1131
|
+
"""Returns the PYPOWER case dict.
|
1132
|
+
"""
|
1133
|
+
return self.ppc
|
1134
|
+
|
1135
|
+
def getN(self, selector, name=None):
|
1136
|
+
"""Returns the number of variables, constraints or cost rows.
|
1137
|
+
|
1138
|
+
Returns either the total number of variables/constraints/cost rows
|
1139
|
+
or the number corresponding to a specified named block.
|
1140
|
+
|
1141
|
+
Examples::
|
1142
|
+
N = getN(om, 'var') : total number of variables
|
1143
|
+
N = getN(om, 'lin') : total number of linear constraints
|
1144
|
+
N = getN(om, 'nln') : total number of nonlinear constraints
|
1145
|
+
N = getN(om, 'cost') : total number of cost rows (in N)
|
1146
|
+
N = getN(om, 'var', name) : number of variables in named set
|
1147
|
+
N = getN(om, 'lin', name) : number of linear constraints in named set
|
1148
|
+
N = getN(om, 'nln', name) : number of nonlinear cons. in named set
|
1149
|
+
N = getN(om, 'cost', name) : number of cost rows (in N) in named set
|
1150
|
+
"""
|
1151
|
+
if name is None:
|
1152
|
+
N = getattr(self, selector)["N"]
|
1153
|
+
else:
|
1154
|
+
if name in getattr(self, selector)["idx"]["N"]:
|
1155
|
+
N = getattr(self, selector)["idx"]["N"][name]
|
1156
|
+
else:
|
1157
|
+
N = 0
|
1158
|
+
return N
|
1159
|
+
|
1160
|
+
def getv(self, name=None):
|
1161
|
+
"""Returns initial value, lower bound and upper bound for opt variables.
|
1162
|
+
|
1163
|
+
Returns the initial value, lower bound and upper bound for the full
|
1164
|
+
optimization variable vector, or for a specific named variable set.
|
1165
|
+
|
1166
|
+
Examples::
|
1167
|
+
x, xmin, xmax = getv(om)
|
1168
|
+
Pg, Pmin, Pmax = getv(om, 'Pg')
|
1169
|
+
"""
|
1170
|
+
if name is None:
|
1171
|
+
v0 = np.array([])
|
1172
|
+
vl = np.array([])
|
1173
|
+
vu = np.array([])
|
1174
|
+
for k in range(self.var["NS"]):
|
1175
|
+
name = self.var["order"][k]
|
1176
|
+
v0 = np.r_[v0, self.var["data"]["v0"][name]]
|
1177
|
+
vl = np.r_[vl, self.var["data"]["vl"][name]]
|
1178
|
+
vu = np.r_[vu, self.var["data"]["vu"][name]]
|
1179
|
+
else:
|
1180
|
+
if name in self.var["idx"]["N"]:
|
1181
|
+
v0 = self.var["data"]["v0"][name]
|
1182
|
+
vl = self.var["data"]["vl"][name]
|
1183
|
+
vu = self.var["data"]["vu"][name]
|
1184
|
+
else:
|
1185
|
+
v0 = np.array([])
|
1186
|
+
vl = np.array([])
|
1187
|
+
vu = np.array([])
|
1188
|
+
|
1189
|
+
return v0, vl, vu
|
1190
|
+
|
1191
|
+
def linear_constraints(self):
|
1192
|
+
"""Builds and returns the full set of linear constraints.
|
1193
|
+
|
1194
|
+
Builds the full set of linear constraints based on those added by
|
1195
|
+
L{add_constraints}::
|
1196
|
+
|
1197
|
+
L <= A * x <= U
|
1198
|
+
"""
|
1199
|
+
|
1200
|
+
# initialize A, l and u
|
1201
|
+
# nnzA = 0
|
1202
|
+
# for k in range(self.lin["NS"]):
|
1203
|
+
# nnzA = nnzA + nnz(self.lin["data"].A.(self.lin.order{k}))
|
1204
|
+
|
1205
|
+
if self.lin["N"]:
|
1206
|
+
A = sp.lil_matrix((self.lin["N"], self.var["N"]))
|
1207
|
+
u = inf * np.ones(self.lin["N"])
|
1208
|
+
l = -u
|
1209
|
+
else:
|
1210
|
+
A = None
|
1211
|
+
u = np.array([])
|
1212
|
+
l = np.array([])
|
1213
|
+
|
1214
|
+
return A, l, u
|
1215
|
+
|
1216
|
+
# fill in each piece
|
1217
|
+
for k in range(self.lin["NS"]):
|
1218
|
+
name = self.lin["order"][k]
|
1219
|
+
N = self.lin["idx"]["N"][name]
|
1220
|
+
if N: # non-zero number of rows to add
|
1221
|
+
Ak = self.lin["data"]["A"][name] # A for kth linear constrain set
|
1222
|
+
i1 = self.lin["idx"]["i1"][name] # starting row index
|
1223
|
+
iN = self.lin["idx"]["iN"][name] # ing row index
|
1224
|
+
vsl = self.lin["data"]["vs"][name] # var set list
|
1225
|
+
kN = 0 # initialize last col of Ak used
|
1226
|
+
# FIXME: Sparse matrix with fancy indexing
|
1227
|
+
Ai = np.zeros((N, self.var["N"]))
|
1228
|
+
for v in vsl:
|
1229
|
+
j1 = self.var["idx"]["i1"][v] # starting column in A
|
1230
|
+
jN = self.var["idx"]["iN"][v] # ing column in A
|
1231
|
+
k1 = kN # starting column in Ak
|
1232
|
+
kN = kN + self.var["idx"]["N"][v] # ing column in Ak
|
1233
|
+
Ai[:, j1:jN] = Ak[:, k1:kN].todense()
|
1234
|
+
|
1235
|
+
A[i1:iN, :] = Ai
|
1236
|
+
|
1237
|
+
l[i1:iN] = self.lin["data"]["l"][name]
|
1238
|
+
u[i1:iN] = self.lin["data"]["u"][name]
|
1239
|
+
|
1240
|
+
return A.tocsr(), l, u
|
1241
|
+
|
1242
|
+
def userdata(self, name, val=None):
|
1243
|
+
"""Used to save or retrieve values of user data.
|
1244
|
+
|
1245
|
+
This function allows the user to save any arbitrary data in the object
|
1246
|
+
for later use. This can be useful when using a user function to add
|
1247
|
+
variables, constraints, costs, etc. For example, suppose some special
|
1248
|
+
indexing is constructed when adding some variables or constraints.
|
1249
|
+
This indexing data can be stored and used later to "unpack" the results
|
1250
|
+
of the solved case.
|
1251
|
+
"""
|
1252
|
+
if val is not None:
|
1253
|
+
self.user_data[name] = val
|
1254
|
+
return self
|
1255
|
+
else:
|
1256
|
+
if name in self.user_data:
|
1257
|
+
return self.user_data[name]
|
1258
|
+
else:
|
1259
|
+
return np.array([])
|
1260
|
+
|
1261
|
+
def opf_execute(om, ppopt):
|
1262
|
+
"""
|
1263
|
+
Executes the OPF specified by an OPF model object.
|
1264
|
+
|
1265
|
+
C{results} are returned with internal indexing, all equipment
|
1266
|
+
in-service, etc.
|
1267
|
+
|
1268
|
+
@see: L{opf}, L{opf_setup}
|
1269
|
+
|
1270
|
+
@author: Ray Zimmerman (PSERC Cornell)
|
1271
|
+
"""
|
1272
|
+
# ----- setup -----
|
1273
|
+
# options
|
1274
|
+
alg = ppopt['OPF_ALG']
|
1275
|
+
|
1276
|
+
# build user-defined costs
|
1277
|
+
om.build_cost_params()
|
1278
|
+
|
1279
|
+
# get indexing
|
1280
|
+
vv, ll, nn, _ = om.get_idx()
|
1281
|
+
|
1282
|
+
# ----- run AC OPF solver -----
|
1283
|
+
# if OPF_ALG not set, choose best available option
|
1284
|
+
if alg == 0:
|
1285
|
+
alg = 560 # MIPS
|
1286
|
+
|
1287
|
+
# update deprecated algorithm codes to new, generalized formulation equivalents
|
1288
|
+
if alg == 100 | alg == 200: # CONSTR
|
1289
|
+
alg = 300
|
1290
|
+
elif alg == 120 | alg == 220: # dense LP
|
1291
|
+
alg = 320
|
1292
|
+
elif alg == 140 | alg == 240: # sparse (relaxed) LP
|
1293
|
+
alg = 340
|
1294
|
+
elif alg == 160 | alg == 260: # sparse (full) LP
|
1295
|
+
alg = 360
|
1296
|
+
|
1297
|
+
ppopt['OPF_ALG_POLY'] = alg
|
1298
|
+
|
1299
|
+
# run specific AC OPF solver
|
1300
|
+
if alg == 560 or alg == 565: # PIPS
|
1301
|
+
results, success, raw = pipsopf_solver(om, ppopt)
|
1302
|
+
elif alg == 580: # IPOPT
|
1303
|
+
try:
|
1304
|
+
__import__('pyipopt')
|
1305
|
+
results, success, raw = ipoptopf_solver(om, ppopt)
|
1306
|
+
except ImportError:
|
1307
|
+
raise ImportError('OPF_ALG %d requires IPOPT '
|
1308
|
+
'(see https://projects.coin-or.org/Ipopt/)' %
|
1309
|
+
alg)
|
1310
|
+
else:
|
1311
|
+
logger.debug('opf_execute: OPF_ALG %d is not a valid algorithm code\n' % alg)
|
1312
|
+
|
1313
|
+
if ('output' not in raw) or ('alg' not in raw['output']):
|
1314
|
+
raw['output']['alg'] = alg
|
1315
|
+
|
1316
|
+
if success:
|
1317
|
+
# copy bus voltages back to gen matrix
|
1318
|
+
results['gen'][:, IDX.gen.VG] = results['bus'][
|
1319
|
+
results['gen'][:, IDX.gen.GEN_BUS].astype(int),
|
1320
|
+
IDX.bus.VM]
|
1321
|
+
|
1322
|
+
# gen PQ capability curve multipliers
|
1323
|
+
if (ll['N']['PQh'] > 0) | (ll['N']['PQl'] > 0):
|
1324
|
+
mu_PQh = results['mu']['lin']['l'][ll['i1']['PQh']:ll['iN']['PQh']
|
1325
|
+
] - results['mu']['lin']['u'][ll['i1']['PQh']:ll['iN']['PQh']]
|
1326
|
+
mu_PQl = results['mu']['lin']['l'][ll['i1']['PQl']:ll['iN']['PQl']
|
1327
|
+
] - results['mu']['lin']['u'][ll['i1']['PQl']:ll['iN']['PQl']]
|
1328
|
+
Apqdata = om.userdata('Apqdata')
|
1329
|
+
results['gen'] = opfcn.update_mupq(results['baseMVA'], results['gen'], mu_PQh, mu_PQl, Apqdata)
|
1330
|
+
|
1331
|
+
# compute g, dg, f, df, d2f if requested by RETURN_RAW_DER = 1
|
1332
|
+
if ppopt['RETURN_RAW_DER']:
|
1333
|
+
# move from results to raw if using v4.0 of MINOPF or TSPOPF
|
1334
|
+
if 'dg' in results:
|
1335
|
+
raw = {}
|
1336
|
+
raw['dg'] = results['dg']
|
1337
|
+
raw['g'] = results['g']
|
1338
|
+
|
1339
|
+
# compute g, dg, unless already done by post-v4.0 MINOPF or TSPOPF
|
1340
|
+
if 'dg' not in raw:
|
1341
|
+
ppc = om.get_ppc()
|
1342
|
+
Ybus, Yf, Yt = makeYbus(ppc['baseMVA'], ppc['bus'], ppc['branch'])
|
1343
|
+
g, geq, dg, dgeq = opf_consfcn(results['x'], om, Ybus, Yf, Yt, ppopt)
|
1344
|
+
raw['g'] = np.r_[geq, g]
|
1345
|
+
raw['dg'] = np.r_[dgeq.T, dg.T] # true Jacobian organization
|
1346
|
+
|
1347
|
+
# compute df, d2f
|
1348
|
+
_, df, d2f = opf_costfcn(results['x'], om, True)
|
1349
|
+
raw['df'] = df
|
1350
|
+
raw['d2f'] = d2f
|
1351
|
+
|
1352
|
+
# delete g and dg fieldsfrom results if using v4.0 of MINOPF or TSPOPF
|
1353
|
+
if 'dg' in results:
|
1354
|
+
del results['dg']
|
1355
|
+
del results['g']
|
1356
|
+
|
1357
|
+
# angle limit constraint multipliers
|
1358
|
+
if ll['N']['ang'] > 0:
|
1359
|
+
iang = om.userdata('iang')
|
1360
|
+
results['branch'][
|
1361
|
+
iang, IDX.branch.MU_ANGMIN] = results['mu']['lin']['l'][
|
1362
|
+
ll['i1']['ang']: ll['iN']['ang']] * deg2rad
|
1363
|
+
results['branch'][
|
1364
|
+
iang, IDX.branch.MU_ANGMAX] = results['mu']['lin']['u'][
|
1365
|
+
ll['i1']['ang']: ll['iN']['ang']] * deg2rad
|
1366
|
+
else:
|
1367
|
+
# assign empty g, dg, f, df, d2f if requested by RETURN_RAW_DER = 1
|
1368
|
+
raw['dg'] = np.array([])
|
1369
|
+
raw['g'] = np.array([])
|
1370
|
+
raw['df'] = np.array([])
|
1371
|
+
raw['d2f'] = np.array([])
|
1372
|
+
|
1373
|
+
# assign values and limit shadow prices for variables
|
1374
|
+
if om.var['order']:
|
1375
|
+
results['var'] = {'val': {}, 'mu': {'l': {}, 'u': {}}}
|
1376
|
+
for name in om.var['order']:
|
1377
|
+
if om.getN('var', name):
|
1378
|
+
idx = np.arange(vv['i1'][name], vv['iN'][name])
|
1379
|
+
results['var']['val'][name] = results['x'][idx]
|
1380
|
+
results['var']['mu']['l'][name] = results['mu']['var']['l'][idx]
|
1381
|
+
results['var']['mu']['u'][name] = results['mu']['var']['u'][idx]
|
1382
|
+
|
1383
|
+
# assign shadow prices for linear constraints
|
1384
|
+
if om.lin['order']:
|
1385
|
+
results['lin'] = {'mu': {'l': {}, 'u': {}}}
|
1386
|
+
for name in om.lin['order']:
|
1387
|
+
if om.getN('lin', name):
|
1388
|
+
idx = np.arange(ll['i1'][name], ll['iN'][name])
|
1389
|
+
results['lin']['mu']['l'][name] = results['mu']['lin']['l'][idx]
|
1390
|
+
results['lin']['mu']['u'][name] = results['mu']['lin']['u'][idx]
|
1391
|
+
|
1392
|
+
# assign shadow prices for nonlinear constraints
|
1393
|
+
if om.nln['order']:
|
1394
|
+
results['nln'] = {'mu': {'l': {}, 'u': {}}}
|
1395
|
+
for name in om.nln['order']:
|
1396
|
+
if om.getN('nln', name):
|
1397
|
+
idx = np.arange(nn['i1'][name], nn['iN'][name])
|
1398
|
+
results['nln']['mu']['l'][name] = results['mu']['nln']['l'][idx]
|
1399
|
+
results['nln']['mu']['u'][name] = results['mu']['nln']['u'][idx]
|
1400
|
+
|
1401
|
+
# assign values for components of user cost
|
1402
|
+
if om.cost['order']:
|
1403
|
+
results['cost'] = {}
|
1404
|
+
for name in om.cost['order']:
|
1405
|
+
if om.getN('cost', name):
|
1406
|
+
results['cost'][name] = om.compute_cost(results['x'], name)
|
1407
|
+
|
1408
|
+
# if single-block PWL costs were converted to POLY, insert dummy y into x
|
1409
|
+
# Note: The "y" portion of x will be nonsense, but everything should at
|
1410
|
+
# least be in the expected locations.
|
1411
|
+
pwl1 = om.userdata('pwl1')
|
1412
|
+
if (len(pwl1) > 0) and (alg != 545) and (alg != 550):
|
1413
|
+
# get indexing
|
1414
|
+
vv, _, _, _ = om.get_idx()
|
1415
|
+
nx = vv['iN']['Qg']
|
1416
|
+
|
1417
|
+
y = np.zeros(len(pwl1))
|
1418
|
+
raw['xr'] = np.r_[raw['xr'][:nx], y, raw['xr'][nx:]]
|
1419
|
+
results['x'] = np.r_[results['x'][:nx], y, results['x'][nx:]]
|
1420
|
+
|
1421
|
+
return results, success, raw
|
1422
|
+
|
1423
|
+
|
1424
|
+
def opf_args(*args):
|
1425
|
+
"""Parses and initializes OPF input arguments.
|
1426
|
+
|
1427
|
+
Returns the full set of initialized OPF input arguments, filling in
|
1428
|
+
default values for missing arguments. See Examples below for the
|
1429
|
+
possible calling syntax options.
|
1430
|
+
|
1431
|
+
Input arguments options::
|
1432
|
+
|
1433
|
+
opf_args(ppc)
|
1434
|
+
opf_args(ppc, ppopt)
|
1435
|
+
opf_args(ppc, userfcn, ppopt)
|
1436
|
+
opf_args(ppc, A, l, u)
|
1437
|
+
opf_args(ppc, A, l, u, ppopt)
|
1438
|
+
opf_args(ppc, A, l, u, ppopt, N, fparm, H, Cw)
|
1439
|
+
opf_args(ppc, A, l, u, ppopt, N, fparm, H, Cw, z0, zl, zu)
|
1440
|
+
|
1441
|
+
opf_args(baseMVA, bus, gen, branch, areas, gencost)
|
1442
|
+
opf_args(baseMVA, bus, gen, branch, areas, gencost, ppopt)
|
1443
|
+
opf_args(baseMVA, bus, gen, branch, areas, gencost, userfcn, ppopt)
|
1444
|
+
opf_args(baseMVA, bus, gen, branch, areas, gencost, A, l, u)
|
1445
|
+
opf_args(baseMVA, bus, gen, branch, areas, gencost, A, l, u, ppopt)
|
1446
|
+
opf_args(baseMVA, bus, gen, branch, areas, gencost, A, l, u, ...
|
1447
|
+
ppopt, N, fparm, H, Cw)
|
1448
|
+
opf_args(baseMVA, bus, gen, branch, areas, gencost, A, l, u, ...
|
1449
|
+
ppopt, N, fparm, H, Cw, z0, zl, zu)
|
1450
|
+
|
1451
|
+
The data for the problem can be specified in one of three ways:
|
1452
|
+
1. a string (ppc) containing the file name of a PYPOWER case
|
1453
|
+
which defines the data matrices baseMVA, bus, gen, branch, and
|
1454
|
+
gencost (areas is not used at all, it is only included for
|
1455
|
+
backward compatibility of the API).
|
1456
|
+
2. a dict (ppc) containing the data matrices as fields.
|
1457
|
+
3. the individual data matrices themselves.
|
1458
|
+
|
1459
|
+
The optional user parameters for user constraints (C{A, l, u}), user costs
|
1460
|
+
(C{N, fparm, H, Cw}), user variable initializer (z0), and user variable
|
1461
|
+
limits (C{zl, zu}) can also be specified as fields in a case dict,
|
1462
|
+
either passed in directly or defined in a case file referenced by name.
|
1463
|
+
|
1464
|
+
When specified, C{A, l, u} represent additional linear constraints on the
|
1465
|
+
optimization variables, C{l <= A*[x z] <= u}. If the user specifies an C{A}
|
1466
|
+
matrix that has more columns than the number of "C{x}" (OPF) variables,
|
1467
|
+
then there are extra linearly constrained "C{z}" variables. For an
|
1468
|
+
explanation of the formulation used and instructions for forming the
|
1469
|
+
C{A} matrix, see the MATPOWER manual.
|
1470
|
+
|
1471
|
+
A generalized cost on all variables can be applied if input arguments
|
1472
|
+
C{N}, C{fparm}, C{H} and C{Cw} are specified. First, a linear
|
1473
|
+
transformation of the optimization variables is defined by means of
|
1474
|
+
C{r = N * [x z]}. Then, to each element of r a function is applied as
|
1475
|
+
encoded in the C{fparm} matrix (see Matpower manual). If the resulting
|
1476
|
+
vector is named C{w}, then C{H} and C{Cw} define a quadratic cost on
|
1477
|
+
C{w}: C{(1/2)*w'*H*w + Cw * w}.
|
1478
|
+
C{H} and C{N} should be sparse matrices and C{H} should also be symmetric.
|
1479
|
+
|
1480
|
+
The optional C{ppopt} vector specifies PYPOWER options. See L{ppoption}
|
1481
|
+
for details and default values.
|
1482
|
+
|
1483
|
+
@author: Ray Zimmerman (PSERC Cornell)
|
1484
|
+
@author: Carlos E. Murillo-Sanchez (PSERC Cornell & Universidad
|
1485
|
+
Autonoma de Manizales)
|
1486
|
+
"""
|
1487
|
+
# nargin = len([arg for arg in [baseMVA, bus, gen, branch, areas, gencost,
|
1488
|
+
# Au, lbu, ubu, ppopt, N, fparm, H, Cw,
|
1489
|
+
# z0, zl, zu] if arg is not None])
|
1490
|
+
nargin = len(args)
|
1491
|
+
userfcn = np.array([])
|
1492
|
+
|
1493
|
+
# parsing filename or dict
|
1494
|
+
if isinstance(args[0], str) or isinstance(args[0], dict):
|
1495
|
+
if nargin in [1, 2, 3, 4, 5, 9, 12]:
|
1496
|
+
casefile = args[0]
|
1497
|
+
if nargin == 12:
|
1498
|
+
baseMVA, bus, gen, branch, areas, gencost, Au, lbu, ubu, ppopt, N, fparm = args
|
1499
|
+
elif nargin == 9:
|
1500
|
+
baseMVA, bus, gen, branch, areas, gencost, Au, lbu, ubu = args
|
1501
|
+
elif nargin == 5:
|
1502
|
+
baseMVA, bus, gen, branch, areas = args
|
1503
|
+
elif nargin == 4:
|
1504
|
+
baseMVA, bus, gen, branch = args
|
1505
|
+
elif nargin == 3:
|
1506
|
+
baseMVA, bus, gen = args
|
1507
|
+
userfcn = bus
|
1508
|
+
elif nargin == 2:
|
1509
|
+
baseMVA, bus = args
|
1510
|
+
elif nargin == 1:
|
1511
|
+
pass # Use default values for all variables
|
1512
|
+
else:
|
1513
|
+
logger.debug('opf_args: Incorrect input arg order, number or type\n')
|
1514
|
+
|
1515
|
+
# Set default values for variables if they are not provided
|
1516
|
+
zu = np.array([]) if nargin in [1, 2, 3, 4, 5, 9, 12] else zu
|
1517
|
+
zl = np.array([]) if nargin in [1, 2, 3, 4, 5, 9, 12] else zl
|
1518
|
+
z0 = np.array([]) if nargin in [1, 2, 3, 4, 5, 9, 12] else z0
|
1519
|
+
Cw = np.array([]) if nargin in [1, 2, 3, 4, 5, 9, 12] else Cw
|
1520
|
+
H = None if nargin in [1, 2, 3, 4, 5, 9, 12] else H
|
1521
|
+
fparm = np.array([]) if nargin in [1, 2, 3, 4, 5, 9, 12] else fparm
|
1522
|
+
N = None if nargin in [1, 2, 3, 4, 5, 9, 12] else N
|
1523
|
+
ppopt = ppoption() if nargin in [1, 2, 3, 4, 5, 9, 12] else ppopt
|
1524
|
+
ubu = np.array([]) if nargin in [1, 2, 3, 4, 5, 9, 12] else ubu
|
1525
|
+
lbu = np.array([]) if nargin in [1, 2, 3, 4, 5, 9, 12] else lbu
|
1526
|
+
Au = None if nargin in [1, 2, 3, 4, 5, 9, 12] else Au
|
1527
|
+
else:
|
1528
|
+
logger.debug('opf_args: Incorrect input arg order, number or type\n')
|
1529
|
+
|
1530
|
+
ppc = loadcase(casefile)
|
1531
|
+
baseMVA, bus, gen, branch, gencost = \
|
1532
|
+
ppc['baseMVA'], ppc['bus'], ppc['gen'], ppc['branch'], ppc['gencost']
|
1533
|
+
if 'areas' in ppc:
|
1534
|
+
areas = ppc['areas']
|
1535
|
+
else:
|
1536
|
+
areas = np.array([])
|
1537
|
+
if Au is None and 'A' in ppc:
|
1538
|
+
Au, lbu, ubu = ppc["A"], ppc["l"], ppc["u"]
|
1539
|
+
if N is None and 'N' in ppc: # these two must go together
|
1540
|
+
N, Cw = ppc["N"], ppc["Cw"]
|
1541
|
+
if H is None and 'H' in ppc: # will default to zeros
|
1542
|
+
H = ppc["H"]
|
1543
|
+
if (fparm is None or len(fparm) == 0) and 'fparm' in ppc: # will default to [1 0 0 1]
|
1544
|
+
fparm = ppc["fparm"]
|
1545
|
+
if (z0 is None or len(z0) == 0) and 'z0' in ppc:
|
1546
|
+
z0 = ppc["z0"]
|
1547
|
+
if (zl is None or len(zl) == 0) and 'zl' in ppc:
|
1548
|
+
zl = ppc["zl"]
|
1549
|
+
if (zu is None or len(zu) == 0) and 'zu' in ppc:
|
1550
|
+
zu = ppc["zu"]
|
1551
|
+
if (userfcn is None or len(userfcn) == 0) and 'userfcn' in ppc:
|
1552
|
+
userfcn = ppc['userfcn']
|
1553
|
+
else: # passing individual data matrices
|
1554
|
+
# ----opf(baseMVA, bus, gen, branch, areas, gencost, Au, lbu, ubu, ppopt, N, fparm, H, Cw, z0, zl, zu)
|
1555
|
+
# 17 opf(baseMVA, bus, gen, branch, areas, gencost, Au, lbu, ubu, ppopt, N, fparm, H, Cw, z0, zl, zu)
|
1556
|
+
# 14 opf(baseMVA, bus, gen, branch, areas, gencost, Au, lbu, ubu, ppopt, N, fparm, H, Cw)
|
1557
|
+
# 10 opf(baseMVA, bus, gen, branch, areas, gencost, Au, lbu, ubu, ppopt)
|
1558
|
+
# 9 opf(baseMVA, bus, gen, branch, areas, gencost, Au, lbu, ubu)
|
1559
|
+
# 8 opf(baseMVA, bus, gen, branch, areas, gencost, userfcn, ppopt)
|
1560
|
+
# 7 opf(baseMVA, bus, gen, branch, areas, gencost, ppopt)
|
1561
|
+
# 6 opf(baseMVA, bus, gen, branch, areas, gencost)
|
1562
|
+
if nargin in [6, 7, 8, 9, 10, 14, 17]:
|
1563
|
+
if nargin == 17:
|
1564
|
+
baseMVA, bus, gen, branch, areas, gencost, Au, lbu, ubu, ppopt, N, fparm, H, Cw, z0, zl, zu = args
|
1565
|
+
elif nargin == 14:
|
1566
|
+
baseMVA, bus, gen, branch, areas, gencost, Au, lbu, ubu, ppopt, N, fparm, H, Cw = args
|
1567
|
+
zu = np.array([])
|
1568
|
+
zl = np.array([])
|
1569
|
+
z0 = np.array([])
|
1570
|
+
elif nargin == 10:
|
1571
|
+
baseMVA, bus, gen, branch, areas, gencost, Au, lbu, ubu, ppopt = args
|
1572
|
+
zu = np.array([])
|
1573
|
+
zl = np.array([])
|
1574
|
+
z0 = np.array([])
|
1575
|
+
Cw = np.array([])
|
1576
|
+
H = None
|
1577
|
+
fparm = np.array([])
|
1578
|
+
N = None
|
1579
|
+
elif nargin == 9:
|
1580
|
+
baseMVA, bus, gen, branch, areas, gencost, Au, lbu, ubu = args
|
1581
|
+
zu = np.array([])
|
1582
|
+
zl = np.array([])
|
1583
|
+
z0 = np.array([])
|
1584
|
+
Cw = np.array([])
|
1585
|
+
H = None
|
1586
|
+
fparm = np.array([])
|
1587
|
+
N = None
|
1588
|
+
ppopt = ppoption()
|
1589
|
+
elif nargin == 8:
|
1590
|
+
baseMVA, bus, gen, branch, areas, gencost, userfcn, ppopt = args
|
1591
|
+
zu = np.array([])
|
1592
|
+
zl = np.array([])
|
1593
|
+
z0 = np.array([])
|
1594
|
+
Cw = np.array([])
|
1595
|
+
H = None
|
1596
|
+
fparm = np.array([])
|
1597
|
+
N = None
|
1598
|
+
ubu = np.array([])
|
1599
|
+
lbu = np.array([])
|
1600
|
+
Au = None
|
1601
|
+
elif nargin == 7:
|
1602
|
+
baseMVA, bus, gen, branch, areas, gencost, ppopt = args
|
1603
|
+
zu = np.array([])
|
1604
|
+
zl = np.array([])
|
1605
|
+
z0 = np.array([])
|
1606
|
+
Cw = np.array([])
|
1607
|
+
H = None
|
1608
|
+
fparm = np.array([])
|
1609
|
+
N = None
|
1610
|
+
ubu = np.array([])
|
1611
|
+
lbu = np.array([])
|
1612
|
+
Au = None
|
1613
|
+
elif nargin == 6:
|
1614
|
+
baseMVA, bus, gen, branch, areas, gencost = args
|
1615
|
+
zu = np.array([])
|
1616
|
+
zl = np.array([])
|
1617
|
+
z0 = np.array([])
|
1618
|
+
Cw = np.array([])
|
1619
|
+
H = None
|
1620
|
+
fparm = np.array([])
|
1621
|
+
N = None
|
1622
|
+
ppopt = ppoption()
|
1623
|
+
ubu = np.array([])
|
1624
|
+
lbu = np.array([])
|
1625
|
+
Au = None
|
1626
|
+
else:
|
1627
|
+
logger.debug('opf_args: Incorrect input arg order, number or type\n')
|
1628
|
+
|
1629
|
+
if N is not None:
|
1630
|
+
nw = N.shape[0]
|
1631
|
+
else:
|
1632
|
+
nw = 0
|
1633
|
+
|
1634
|
+
if nw:
|
1635
|
+
if Cw.shape[0] != nw:
|
1636
|
+
logger.debug('opf_args.m: dimension mismatch between N and Cw in '
|
1637
|
+
'generalized cost parameters\n')
|
1638
|
+
if len(fparm) > 0 and fparm.shape[0] != nw:
|
1639
|
+
logger.debug('opf_args.m: dimension mismatch between N and fparm '
|
1640
|
+
'in generalized cost parameters\n')
|
1641
|
+
if (H is not None) and (H.shape[0] != nw | H.shape[0] != nw):
|
1642
|
+
logger.debug('opf_args.m: dimension mismatch between N and H in '
|
1643
|
+
'generalized cost parameters\n')
|
1644
|
+
if Au is not None:
|
1645
|
+
if Au.shape[0] > 0 and N.shape[1] != Au.shape[1]:
|
1646
|
+
logger.debug('opf_args.m: A and N must have the same number '
|
1647
|
+
'of columns\n')
|
1648
|
+
# make sure N and H are sparse
|
1649
|
+
if not sp.issparse(N):
|
1650
|
+
logger.debug('opf_args.m: N must be sparse in generalized cost '
|
1651
|
+
'parameters\n')
|
1652
|
+
if not sp.issparse(H):
|
1653
|
+
logger.debug('opf_args.m: H must be sparse in generalized cost parameters\n')
|
1654
|
+
|
1655
|
+
if Au is not None and not sp.issparse(Au):
|
1656
|
+
logger.debug('opf_args.m: Au must be sparse\n')
|
1657
|
+
if ppopt == None or len(ppopt) == 0:
|
1658
|
+
ppopt = ppoption()
|
1659
|
+
|
1660
|
+
return baseMVA, bus, gen, branch, gencost, Au, lbu, ubu, \
|
1661
|
+
ppopt, N, fparm, H, Cw, z0, zl, zu, userfcn, areas
|
1662
|
+
|
1663
|
+
|
1664
|
+
def opf_args2(*args):
|
1665
|
+
"""
|
1666
|
+
Parses and initializes OPF input arguments.
|
1667
|
+
"""
|
1668
|
+
baseMVA, bus, gen, branch, gencost, Au, lbu, ubu, \
|
1669
|
+
ppopt, N, fparm, H, Cw, z0, zl, zu, userfcn, areas = opf_args(*args)
|
1670
|
+
|
1671
|
+
ppc = args[0] if isinstance(args[0], dict) else {}
|
1672
|
+
|
1673
|
+
ppc['baseMVA'] = baseMVA
|
1674
|
+
ppc['bus'] = bus
|
1675
|
+
ppc['gen'] = gen
|
1676
|
+
ppc['branch'] = branch
|
1677
|
+
ppc['gencost'] = gencost
|
1678
|
+
|
1679
|
+
if areas is not None and len(areas) > 0:
|
1680
|
+
ppc["areas"] = areas
|
1681
|
+
if lbu is not None and len(lbu) > 0:
|
1682
|
+
ppc["A"], ppc["l"], ppc["u"] = Au, lbu, ubu
|
1683
|
+
if Cw is not None and len(Cw) > 0:
|
1684
|
+
ppc["N"], ppc["Cw"] = N, Cw
|
1685
|
+
if len(fparm) > 0:
|
1686
|
+
ppc["fparm"] = fparm
|
1687
|
+
# if len(H) > 0:
|
1688
|
+
ppc["H"] = H
|
1689
|
+
if z0 is not None and len(z0) > 0:
|
1690
|
+
ppc["z0"] = z0
|
1691
|
+
if zl is not None and len(zl) > 0:
|
1692
|
+
ppc["zl"] = zl
|
1693
|
+
if zu is not None and len(zu) > 0:
|
1694
|
+
ppc["zu"] = zu
|
1695
|
+
if userfcn is not None and len(userfcn) > 0:
|
1696
|
+
ppc["userfcn"] = userfcn
|
1697
|
+
|
1698
|
+
return ppc, ppopt
|
1699
|
+
|
1700
|
+
|
1701
|
+
def uopf(*args):
|
1702
|
+
"""Solves combined unit decommitment / optimal power flow.
|
1703
|
+
|
1704
|
+
Solves a combined unit decommitment and optimal power flow for a single
|
1705
|
+
time period. Uses an algorithm similar to dynamic programming. It proceeds
|
1706
|
+
through a sequence of stages, where stage C{N} has C{N} generators shut
|
1707
|
+
down, starting with C{N=0}. In each stage, it forms a list of candidates
|
1708
|
+
(gens at their C{Pmin} limits) and computes the cost with each one of them
|
1709
|
+
shut down. It selects the least cost case as the starting point for the
|
1710
|
+
next stage, continuing until there are no more candidates to be shut down
|
1711
|
+
or no more improvement can be gained by shutting something down.
|
1712
|
+
If C{verbose} in ppopt (see L{ppoption} is C{true}, it prints progress
|
1713
|
+
info, if it is > 1 it prints the output of each individual opf.
|
1714
|
+
|
1715
|
+
@see: L{opf}, L{runuopf}
|
1716
|
+
|
1717
|
+
@author: Ray Zimmerman (PSERC Cornell)
|
1718
|
+
"""
|
1719
|
+
# ----- initialization -----
|
1720
|
+
t0, _ = elapsed() # start timer
|
1721
|
+
|
1722
|
+
# process input arguments
|
1723
|
+
ppc, ppopt = opf_args2(*args)
|
1724
|
+
|
1725
|
+
# options
|
1726
|
+
verbose = ppopt["VERBOSE"]
|
1727
|
+
if verbose: # turn down verbosity one level for calls to opf
|
1728
|
+
ppopt = ppoption(ppopt, VERBOSE=verbose - 1)
|
1729
|
+
|
1730
|
+
# ----- do combined unit commitment/optimal power flow -----
|
1731
|
+
|
1732
|
+
# check for sum(Pmin) > total load, decommit as necessary
|
1733
|
+
on = find((ppc["gen"][:, IDX.gen.GEN_STATUS] > 0) & ~isload(ppc["gen"])) # gens in service
|
1734
|
+
onld = find((ppc["gen"][:, IDX.gen.GEN_STATUS] > 0) & isload(ppc["gen"])) # disp loads in serv
|
1735
|
+
load_capacity = sum(ppc["bus"][:, IDX.bus.PD]) - sum(ppc["gen"][onld, IDX.gen.PMIN]) # total load capacity
|
1736
|
+
Pmin = ppc["gen"][on, IDX.gen.PMIN]
|
1737
|
+
while sum(Pmin) > load_capacity:
|
1738
|
+
# shut down most expensive unit
|
1739
|
+
avgPmincost = opfcn.totcost(ppc["gencost"][on, :], Pmin) / Pmin
|
1740
|
+
_, i = fairmax(avgPmincost) # pick one with max avg cost at Pmin
|
1741
|
+
i = on[i] # convert to generator index
|
1742
|
+
|
1743
|
+
if verbose:
|
1744
|
+
print('Shutting down generator %d so all Pmin limits can be satisfied.\n' % i)
|
1745
|
+
|
1746
|
+
# set generation to zero
|
1747
|
+
ppc["gen"][i, [IDX.gen.PG, IDX.gen.QG, IDX.gen.GEN_STATUS]] = 0
|
1748
|
+
|
1749
|
+
# update minimum gen capacity
|
1750
|
+
on = find((ppc["gen"][:, IDX.gen.GEN_STATUS] > 0) & ~isload(ppc["gen"])) # gens in service
|
1751
|
+
Pmin = ppc["gen"][on, IDX.gen.PMIN]
|
1752
|
+
|
1753
|
+
# run initial opf
|
1754
|
+
results = fopf(ppc, ppopt)
|
1755
|
+
|
1756
|
+
# best case so far
|
1757
|
+
results1 = deepcopy(results)
|
1758
|
+
|
1759
|
+
# best case for this stage (ie. with n gens shut down, n=0,1,2 ...)
|
1760
|
+
results0 = deepcopy(results1)
|
1761
|
+
ppc["bus"] = results0["bus"].copy() # use these V as starting point for OPF
|
1762
|
+
|
1763
|
+
while True:
|
1764
|
+
# get candidates for shutdown
|
1765
|
+
candidates = find((results0["gen"][:, IDX.gen.MU_PMIN] > 0) & (results0["gen"][:, IDX.gen.PMIN] > 0))
|
1766
|
+
if len(candidates) == 0:
|
1767
|
+
break
|
1768
|
+
|
1769
|
+
# do not check for further decommitment unless we
|
1770
|
+
# see something better during this stage
|
1771
|
+
done = True
|
1772
|
+
|
1773
|
+
for k in candidates:
|
1774
|
+
# start with best for this stage
|
1775
|
+
ppc["gen"] = results0["gen"].copy()
|
1776
|
+
|
1777
|
+
# shut down gen k
|
1778
|
+
ppc["gen"][k, [IDX.gen.PG, IDX.gen.QG, IDX.gen.GEN_STATUS]] = 0
|
1779
|
+
|
1780
|
+
# run opf
|
1781
|
+
results = fopf(ppc, ppopt)
|
1782
|
+
|
1783
|
+
# something better?
|
1784
|
+
if results['success'] and (results["f"] < results1["f"]):
|
1785
|
+
results1 = deepcopy(results)
|
1786
|
+
k1 = k
|
1787
|
+
done = False # make sure we check for further decommitment
|
1788
|
+
|
1789
|
+
if done:
|
1790
|
+
# decommits at this stage did not help, so let's quit
|
1791
|
+
break
|
1792
|
+
else:
|
1793
|
+
# shutting something else down helps, so let's keep going
|
1794
|
+
if verbose:
|
1795
|
+
print('Shutting down generator %d.\n' % k1)
|
1796
|
+
|
1797
|
+
results0 = deepcopy(results1)
|
1798
|
+
ppc["bus"] = results0["bus"].copy() # use these V as starting point for OPF
|
1799
|
+
|
1800
|
+
# compute elapsed time
|
1801
|
+
_, results0['et'] = elapsed(t0)
|
1802
|
+
|
1803
|
+
return results0
|