ltbams 1.0.9__py3-none-any.whl → 1.0.10__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 +0 -1
- ams/_version.py +3 -3
- ams/cases/5bus/pjm5bus_demo.json +1324 -0
- ams/core/__init__.py +1 -0
- ams/core/common.py +30 -0
- ams/core/model.py +1 -1
- ams/core/symprocessor.py +1 -1
- ams/extension/eva.py +1 -1
- ams/interface.py +40 -24
- ams/io/matpower.py +31 -17
- ams/io/psse.py +278 -1
- ams/main.py +2 -2
- ams/models/group.py +2 -1
- ams/models/static/pq.py +7 -3
- ams/opt/param.py +1 -2
- ams/routines/__init__.py +2 -3
- ams/routines/acopf.py +5 -108
- ams/routines/dcopf.py +8 -0
- ams/routines/dcpf.py +1 -1
- ams/routines/ed.py +4 -2
- ams/routines/grbopt.py +150 -0
- ams/routines/pflow.py +2 -2
- ams/routines/pypower.py +631 -0
- ams/routines/routine.py +4 -10
- ams/routines/uc.py +2 -2
- ams/shared.py +26 -43
- ams/system.py +118 -2
- docs/source/api.rst +2 -0
- docs/source/getting_started/install.rst +9 -6
- docs/source/images/dcopf_time.png +0 -0
- docs/source/images/educ_pie.png +0 -0
- docs/source/release-notes.rst +21 -47
- {ltbams-1.0.9.dist-info → ltbams-1.0.10.dist-info}/METADATA +87 -47
- {ltbams-1.0.9.dist-info → ltbams-1.0.10.dist-info}/RECORD +54 -71
- {ltbams-1.0.9.dist-info → ltbams-1.0.10.dist-info}/WHEEL +1 -1
- tests/test_1st_system.py +1 -1
- tests/test_case.py +14 -14
- tests/test_export_csv.py +1 -1
- tests/test_interface.py +24 -2
- tests/test_io.py +50 -0
- tests/test_omodel.py +1 -1
- tests/test_report.py +6 -6
- tests/test_routine.py +2 -2
- tests/test_rtn_acopf.py +75 -0
- tests/test_rtn_dcopf.py +1 -1
- tests/test_rtn_dcopf2.py +1 -1
- tests/test_rtn_ed.py +9 -9
- tests/test_rtn_opf.py +142 -0
- tests/test_rtn_pflow.py +0 -72
- tests/test_rtn_pypower.py +315 -0
- tests/test_rtn_rted.py +8 -8
- tests/test_rtn_uc.py +18 -18
- ams/pypower/__init__.py +0 -8
- ams/pypower/_compat.py +0 -9
- ams/pypower/core/__init__.py +0 -8
- ams/pypower/core/pips.py +0 -894
- ams/pypower/core/ppoption.py +0 -244
- ams/pypower/core/ppver.py +0 -18
- ams/pypower/core/solver.py +0 -2451
- ams/pypower/eps.py +0 -6
- ams/pypower/idx.py +0 -174
- ams/pypower/io.py +0 -604
- ams/pypower/make/__init__.py +0 -11
- ams/pypower/make/matrices.py +0 -665
- ams/pypower/make/pdv.py +0 -506
- ams/pypower/routines/__init__.py +0 -7
- ams/pypower/routines/cpf.py +0 -513
- ams/pypower/routines/cpf_callbacks.py +0 -114
- ams/pypower/routines/opf.py +0 -1803
- ams/pypower/routines/opffcns.py +0 -1946
- ams/pypower/routines/pflow.py +0 -852
- ams/pypower/toggle.py +0 -1098
- ams/pypower/utils.py +0 -293
- ams/routines/cpf.py +0 -65
- ams/routines/dcpf0.py +0 -196
- ams/routines/pflow0.py +0 -113
- tests/test_rtn_dcpf.py +0 -77
- {ltbams-1.0.9.dist-info → ltbams-1.0.10.dist-info}/entry_points.txt +0 -0
- {ltbams-1.0.9.dist-info → ltbams-1.0.10.dist-info}/top_level.txt +0 -0
ams/routines/pypower.py
ADDED
@@ -0,0 +1,631 @@
|
|
1
|
+
"""
|
2
|
+
Routines using PYPOWER.
|
3
|
+
"""
|
4
|
+
import logging
|
5
|
+
from typing import Optional, Union, Type
|
6
|
+
from collections import OrderedDict
|
7
|
+
|
8
|
+
from andes.shared import deg2rad, np
|
9
|
+
from andes.utils.misc import elapsed
|
10
|
+
|
11
|
+
from ams.io.pypower import system2ppc
|
12
|
+
from ams.core.param import RParam
|
13
|
+
|
14
|
+
from ams.opt import Var, Objective, ExpressionCalc
|
15
|
+
from ams.routines.routine import RoutineBase
|
16
|
+
from ams.shared import ppoption, runpf, runopf
|
17
|
+
|
18
|
+
logger = logging.getLogger(__name__)
|
19
|
+
|
20
|
+
|
21
|
+
class DCPF1(RoutineBase):
|
22
|
+
"""
|
23
|
+
DC Power Flow using PYPOWER.
|
24
|
+
|
25
|
+
This routine provides a wrapper for running DC power flow analysis using the
|
26
|
+
PYPOWER.
|
27
|
+
It leverages PYPOWER's internal DC power flow solver and maps results back to
|
28
|
+
the AMS system.
|
29
|
+
|
30
|
+
Notes
|
31
|
+
-----
|
32
|
+
- This class does not implement the AMS-style DC power flow formulation.
|
33
|
+
- For detailed mathematical formulations and algorithmic details, refer to the
|
34
|
+
MATPOWER User's Manual, section on Power Flow.
|
35
|
+
"""
|
36
|
+
|
37
|
+
def __init__(self, system, config):
|
38
|
+
RoutineBase.__init__(self, system, config)
|
39
|
+
self.info = 'DC Power Flow'
|
40
|
+
self.type = 'PF'
|
41
|
+
|
42
|
+
self.map1 = OrderedDict() # DCPF does not receive
|
43
|
+
self.map2.update({
|
44
|
+
'vBus': ('Bus', 'v0'),
|
45
|
+
'ug': ('StaticGen', 'u'),
|
46
|
+
'pg': ('StaticGen', 'p0'),
|
47
|
+
})
|
48
|
+
|
49
|
+
self.config.add(OrderedDict((('verbose', 1),
|
50
|
+
('out_all', 0),
|
51
|
+
('out_sys_sum', 1),
|
52
|
+
('out_area_sum', 0),
|
53
|
+
('out_bus', 1),
|
54
|
+
('out_branch', 1),
|
55
|
+
('out_gen', 0),
|
56
|
+
('out_all_lim', -1),
|
57
|
+
('out_v_lim', 1),
|
58
|
+
('out_line_lim', 1),
|
59
|
+
('out_pg_lim', 1),
|
60
|
+
('out_qg_lim', 1),
|
61
|
+
)))
|
62
|
+
self.config.add_extra("_help",
|
63
|
+
verbose="0: no progress info, 1: little, 2: lots, 3: all",
|
64
|
+
out_all="-1: individual flags control what prints, 0: none, 1: all",
|
65
|
+
out_sys_sum="print system summary",
|
66
|
+
out_area_sum="print area summaries",
|
67
|
+
out_bus="print bus detail",
|
68
|
+
out_branch="print branch detail",
|
69
|
+
out_gen="print generator detail (OUT_BUS also includes gen info)",
|
70
|
+
out_all_lim="-1: individual flags, 0: none, 1: binding, 2: all",
|
71
|
+
out_v_lim="0: don't print, 1: binding constraints only, 2: all constraints",
|
72
|
+
out_line_lim="0: don't print, 1: binding constraints only, 2: all constraints",
|
73
|
+
out_pg_lim="0: don't print, 1: binding constraints only, 2: all constraints",
|
74
|
+
out_qg_lim="0: don't print, 1: binding constraints only, 2: all constraints",
|
75
|
+
)
|
76
|
+
self.config.add_extra("_alt",
|
77
|
+
verbose=(0, 1, 2, 3),
|
78
|
+
out_all=(-1, 0, 1),
|
79
|
+
out_sys_sum=(0, 1),
|
80
|
+
out_area_sum=(0, 1),
|
81
|
+
out_bus=(0, 1),
|
82
|
+
out_branch=(0, 1),
|
83
|
+
out_gen=(0, 1),
|
84
|
+
out_all_lim=(-1, 0, 1, 2),
|
85
|
+
out_v_lim=(0, 1, 2),
|
86
|
+
out_line_lim=(0, 1, 2),
|
87
|
+
out_pg_lim=(0, 1, 2),
|
88
|
+
out_qg_lim=(0, 1, 2),
|
89
|
+
)
|
90
|
+
self.config.add_extra("_tex",
|
91
|
+
verbose=r'v_{erbose}',
|
92
|
+
out_all=r'o_{ut\_all}',
|
93
|
+
out_sys_sum=r'o_{ut\_sys\_sum}',
|
94
|
+
out_area_sum=r'o_{ut\_area\_sum}',
|
95
|
+
out_bus=r'o_{ut\_bus}',
|
96
|
+
out_branch=r'o_{ut\_branch}',
|
97
|
+
out_gen=r'o_{ut\_gen}',
|
98
|
+
out_all_lim=r'o_{ut\_all\_lim}',
|
99
|
+
out_v_lim=r'o_{ut\_v\_lim}',
|
100
|
+
out_line_lim=r'o_{ut\_line\_lim}',
|
101
|
+
out_pg_lim=r'o_{ut\_pg\_lim}',
|
102
|
+
out_qg_lim=r'o_{ut\_qg\_lim}',
|
103
|
+
)
|
104
|
+
|
105
|
+
self.ug = RParam(info='Gen connection status',
|
106
|
+
name='ug', tex_name=r'u_{g}',
|
107
|
+
model='StaticGen', src='u',
|
108
|
+
no_parse=True)
|
109
|
+
|
110
|
+
self.c2 = RParam(info='Gen cost coefficient 2',
|
111
|
+
name='c2', tex_name=r'c_{2}',
|
112
|
+
unit=r'$/(p.u.^2)', model='GCost',
|
113
|
+
indexer='gen', imodel='StaticGen',
|
114
|
+
nonneg=True, no_parse=True)
|
115
|
+
self.c1 = RParam(info='Gen cost coefficient 1',
|
116
|
+
name='c1', tex_name=r'c_{1}',
|
117
|
+
unit=r'$/(p.u.)', model='GCost',
|
118
|
+
indexer='gen', imodel='StaticGen',)
|
119
|
+
self.c0 = RParam(info='Gen cost coefficient 0',
|
120
|
+
name='c0', tex_name=r'c_{0}',
|
121
|
+
unit=r'$', model='GCost',
|
122
|
+
indexer='gen', imodel='StaticGen',
|
123
|
+
no_parse=True)
|
124
|
+
|
125
|
+
# --- bus ---
|
126
|
+
self.aBus = Var(info='bus voltage angle',
|
127
|
+
unit='rad',
|
128
|
+
name='aBus', tex_name=r'a_{Bus}',
|
129
|
+
model='Bus', src='a',)
|
130
|
+
self.vBus = Var(info='Bus voltage magnitude',
|
131
|
+
unit='p.u.',
|
132
|
+
name='vBus', tex_name=r'v_{Bus}',
|
133
|
+
src='v', model='Bus',)
|
134
|
+
# --- gen ---
|
135
|
+
self.pg = Var(info='Gen active power',
|
136
|
+
unit='p.u.',
|
137
|
+
name='pg', tex_name=r'p_{g}',
|
138
|
+
model='StaticGen', src='p',)
|
139
|
+
self.qg = Var(info='Gen reactive power',
|
140
|
+
unit='p.u.',
|
141
|
+
name='qg', tex_name=r'q_{g}',
|
142
|
+
model='StaticGen', src='q',)
|
143
|
+
# --- line flow ---
|
144
|
+
self.plf = Var(info='Line flow',
|
145
|
+
unit='p.u.',
|
146
|
+
name='plf', tex_name=r'p_{lf}',
|
147
|
+
model='Line',)
|
148
|
+
# --- objective ---
|
149
|
+
self.obj = Objective(name='obj',
|
150
|
+
info='total cost',
|
151
|
+
e_str='0',
|
152
|
+
sense='min',)
|
153
|
+
|
154
|
+
# --- total cost ---
|
155
|
+
tcost = 'sum(mul(c2, pg**2))'
|
156
|
+
tcost += '+ sum(mul(c1, pg))'
|
157
|
+
tcost += '+ sum(mul(ug, c0))'
|
158
|
+
self.tcost = ExpressionCalc(info='Total cost', unit='$',
|
159
|
+
model=None, src=None,
|
160
|
+
e_str=tcost)
|
161
|
+
|
162
|
+
def solve(self, **kwargs):
|
163
|
+
"""
|
164
|
+
Solve by PYPOWER.
|
165
|
+
"""
|
166
|
+
ppc = system2ppc(self.system)
|
167
|
+
config = {key.upper(): value for key, value in self.config._dict.items()}
|
168
|
+
# Enforece DC power flow
|
169
|
+
ppopt = ppoption(PF_DC=True, **config)
|
170
|
+
res, _ = runpf(casedata=ppc, ppopt=ppopt)
|
171
|
+
return res
|
172
|
+
|
173
|
+
def unpack(self, res, **kwargs):
|
174
|
+
"""
|
175
|
+
Unpack results from PYPOWER.
|
176
|
+
"""
|
177
|
+
system = self.system
|
178
|
+
mva = res['baseMVA']
|
179
|
+
|
180
|
+
# --- copy results from ppc into system algeb ---
|
181
|
+
# --- Bus ---
|
182
|
+
system.Bus.v.v = res['bus'][:, 7] # voltage magnitude
|
183
|
+
system.Bus.a.v = res['bus'][:, 8] * deg2rad # voltage angle
|
184
|
+
|
185
|
+
# --- PV ---
|
186
|
+
system.PV.p.v = res['gen'][system.Slack.n:, 1] / mva # active power
|
187
|
+
system.PV.q.v = res['gen'][system.Slack.n:, 2] / mva # reactive power
|
188
|
+
|
189
|
+
# --- Slack ---
|
190
|
+
system.Slack.p.v = res['gen'][:system.Slack.n, 1] / mva # active power
|
191
|
+
system.Slack.q.v = res['gen'][:system.Slack.n, 2] / mva # reactive power
|
192
|
+
|
193
|
+
# --- Line ---
|
194
|
+
self.plf.optz.value = res['branch'][:, 13] / mva # line flow
|
195
|
+
|
196
|
+
# NOTE: In PYPOWER, branch status is not optimized and this assignment
|
197
|
+
# typically has no effect on results. However, in some extensions (e.g., gurobi-optimods),
|
198
|
+
# branch status may be optimized. This line ensures that the system's branch status
|
199
|
+
# is updated to reflect the results from the solver, if applicable.
|
200
|
+
|
201
|
+
system.Line.u.v = res['branch'][:, 10]
|
202
|
+
|
203
|
+
# --- copy results from system algeb into routine algeb ---
|
204
|
+
for vname, var in self.vars.items():
|
205
|
+
owner = getattr(system, var.model) # instance of owner, Model or Group
|
206
|
+
if var.src is None: # skip if no source variable is specified
|
207
|
+
continue
|
208
|
+
elif hasattr(owner, 'group'): # if owner is a Model instance
|
209
|
+
grp = getattr(system, owner.group)
|
210
|
+
idx = grp.get_all_idxes()
|
211
|
+
elif hasattr(owner, 'get_idx'): # if owner is a Group instance
|
212
|
+
idx = owner.get_all_idxes()
|
213
|
+
else:
|
214
|
+
msg = f"Failed to find valid source variable `{owner.class_name}.{var.src}` for "
|
215
|
+
msg += f"{self.class_name}.{vname}, skip unpacking."
|
216
|
+
logger.warning(msg)
|
217
|
+
continue
|
218
|
+
try:
|
219
|
+
logger.debug(f"Unpacking {vname} into {owner.class_name}.{var.src}.")
|
220
|
+
var.optz.value = owner.get(src=var.src, attr='v', idx=idx)
|
221
|
+
except AttributeError:
|
222
|
+
logger.debug(f"Failed to unpack {vname} into {owner.class_name}.{var.src}.")
|
223
|
+
continue
|
224
|
+
self.system.recent = self.system.routines[self.class_name]
|
225
|
+
return True
|
226
|
+
|
227
|
+
def run(self, **kwargs):
|
228
|
+
"""
|
229
|
+
Run the DC power flow using PYPOWER.
|
230
|
+
|
231
|
+
Returns
|
232
|
+
-------
|
233
|
+
bool
|
234
|
+
True if the optimization converged successfully, False otherwise.
|
235
|
+
"""
|
236
|
+
if not self.initialized:
|
237
|
+
self.init()
|
238
|
+
t0, _ = elapsed()
|
239
|
+
|
240
|
+
# --- solve optimization ---
|
241
|
+
t0, _ = elapsed()
|
242
|
+
res = self.solve(**kwargs)
|
243
|
+
self.converged = res['success']
|
244
|
+
self.exit_code = 0 if res['success'] else 1
|
245
|
+
_, s = elapsed(t0)
|
246
|
+
self.exec_time = float(s.split(" ")[0])
|
247
|
+
try:
|
248
|
+
n_iter = res['raw']['output']['iterations']
|
249
|
+
except Exception:
|
250
|
+
n_iter = -1
|
251
|
+
n_iter_str = f"{n_iter} iterations " if n_iter > 1 else f"{n_iter} iteration "
|
252
|
+
if self.exit_code == 0:
|
253
|
+
msg = f"<{self.class_name}> converged in {s}, "
|
254
|
+
msg += n_iter_str + "with PYPOWER."
|
255
|
+
logger.warning(msg)
|
256
|
+
try:
|
257
|
+
self.unpack(res)
|
258
|
+
except Exception as e:
|
259
|
+
logger.error(f"Failed to unpack results from {self.class_name}.\n{e}")
|
260
|
+
return False
|
261
|
+
self.system.report()
|
262
|
+
return True
|
263
|
+
else:
|
264
|
+
msg = f"{self.class_name} failed to converge in {s}, "
|
265
|
+
msg += n_iter_str + "with PYPOWER."
|
266
|
+
logger.warning(msg)
|
267
|
+
return False
|
268
|
+
|
269
|
+
def _get_off_constrs(self):
|
270
|
+
pass
|
271
|
+
|
272
|
+
def _data_check(self, info=True, **kwargs):
|
273
|
+
pass
|
274
|
+
|
275
|
+
def update(self, params=None, build_mats=False, **kwargs):
|
276
|
+
pass
|
277
|
+
|
278
|
+
def enable(self, name):
|
279
|
+
raise NotImplementedError
|
280
|
+
|
281
|
+
def disable(self, name):
|
282
|
+
raise NotImplementedError
|
283
|
+
|
284
|
+
def _post_add_check(self):
|
285
|
+
pass
|
286
|
+
|
287
|
+
def addRParam(self,
|
288
|
+
name: str,
|
289
|
+
tex_name: Optional[str] = None,
|
290
|
+
info: Optional[str] = None,
|
291
|
+
src: Optional[str] = None,
|
292
|
+
unit: Optional[str] = None,
|
293
|
+
model: Optional[str] = None,
|
294
|
+
v: Optional[np.ndarray] = None,
|
295
|
+
indexer: Optional[str] = None,
|
296
|
+
imodel: Optional[str] = None,):
|
297
|
+
raise NotImplementedError
|
298
|
+
|
299
|
+
def addService(self,
|
300
|
+
name: str,
|
301
|
+
value: np.ndarray,
|
302
|
+
tex_name: str = None,
|
303
|
+
unit: str = None,
|
304
|
+
info: str = None,
|
305
|
+
vtype: Type = None,):
|
306
|
+
raise NotImplementedError
|
307
|
+
|
308
|
+
def addConstrs(self,
|
309
|
+
name: str,
|
310
|
+
e_str: str,
|
311
|
+
info: Optional[str] = None,
|
312
|
+
is_eq: Optional[str] = False,):
|
313
|
+
raise NotImplementedError
|
314
|
+
|
315
|
+
def addVars(self,
|
316
|
+
name: str,
|
317
|
+
model: Optional[str] = None,
|
318
|
+
shape: Optional[Union[int, tuple]] = None,
|
319
|
+
tex_name: Optional[str] = None,
|
320
|
+
info: Optional[str] = None,
|
321
|
+
src: Optional[str] = None,
|
322
|
+
unit: Optional[str] = None,
|
323
|
+
horizon: Optional[RParam] = None,
|
324
|
+
nonneg: Optional[bool] = False,
|
325
|
+
nonpos: Optional[bool] = False,
|
326
|
+
cplx: Optional[bool] = False,
|
327
|
+
imag: Optional[bool] = False,
|
328
|
+
symmetric: Optional[bool] = False,
|
329
|
+
diag: Optional[bool] = False,
|
330
|
+
psd: Optional[bool] = False,
|
331
|
+
nsd: Optional[bool] = False,
|
332
|
+
hermitian: Optional[bool] = False,
|
333
|
+
boolean: Optional[bool] = False,
|
334
|
+
integer: Optional[bool] = False,
|
335
|
+
pos: Optional[bool] = False,
|
336
|
+
neg: Optional[bool] = False,):
|
337
|
+
raise NotImplementedError
|
338
|
+
|
339
|
+
|
340
|
+
class PFlow1(DCPF1):
|
341
|
+
"""
|
342
|
+
Power Flow using PYPOWER.
|
343
|
+
|
344
|
+
This routine provides a wrapper for running power flow analysis using the
|
345
|
+
PYPOWER.
|
346
|
+
It leverages PYPOWER's internal power flow solver and maps results back to the
|
347
|
+
AMS system.
|
348
|
+
|
349
|
+
Known Issues
|
350
|
+
------------
|
351
|
+
- Fast-Decoupled (XB version) and Fast-Decoupled (BX version) algorithms are
|
352
|
+
not fully supported yet.
|
353
|
+
|
354
|
+
Notes
|
355
|
+
-----
|
356
|
+
- This class does not implement the AMS-style power flow formulation.
|
357
|
+
- For detailed mathematical formulations and algorithmic details, refer to the
|
358
|
+
MATPOWER User's Manual, section on Power Flow.
|
359
|
+
"""
|
360
|
+
|
361
|
+
def __init__(self, system, config):
|
362
|
+
DCPF1.__init__(self, system, config)
|
363
|
+
self.info = 'Power Flow'
|
364
|
+
self.type = 'PF'
|
365
|
+
|
366
|
+
# PFlow does not receive nor send
|
367
|
+
self.map1 = OrderedDict()
|
368
|
+
self.map2 = OrderedDict()
|
369
|
+
|
370
|
+
self.config.add(OrderedDict((('pf_alg', 1),
|
371
|
+
('pf_tol', 1e-8),
|
372
|
+
('pf_max_it', 10),
|
373
|
+
('pf_max_it_fd', 30),
|
374
|
+
('pf_max_it_gs', 1000),
|
375
|
+
('enforce_q_lims', 0),
|
376
|
+
)))
|
377
|
+
self.config.add_extra("_help",
|
378
|
+
pf_alg="1: Newton, 2: Fast-Decoupled XB, 3: Fast-Decoupled BX, 4: Gauss Seidel",
|
379
|
+
pf_tol="termination tolerance on per unit P & Q mismatch",
|
380
|
+
pf_max_it="maximum number of iterations for Newton's method",
|
381
|
+
pf_max_it_fd="maximum number of iterations for fast decoupled method",
|
382
|
+
pf_max_it_gs="maximum number of iterations for Gauss-Seidel method",
|
383
|
+
enforce_q_lims="enforce gen reactive power limits, at expense of |V|",
|
384
|
+
)
|
385
|
+
self.config.add_extra("_alt",
|
386
|
+
pf_alg=(1, 2, 3, 4),
|
387
|
+
pf_tol=(0.0, 1e-8),
|
388
|
+
pf_max_it=">1",
|
389
|
+
pf_max_it_fd=">1",
|
390
|
+
pf_max_it_gs=">1",
|
391
|
+
enforce_q_lims=(0, 1),
|
392
|
+
)
|
393
|
+
|
394
|
+
def solve(self, **kwargs):
|
395
|
+
ppc = system2ppc(self.system)
|
396
|
+
config = {key.upper(): value for key, value in self.config._dict.items()}
|
397
|
+
# Enforece AC power flow
|
398
|
+
ppopt = ppoption(PF_DC=False, **config)
|
399
|
+
res, _ = runpf(casedata=ppc, ppopt=ppopt)
|
400
|
+
return res
|
401
|
+
|
402
|
+
def run(self, **kwargs):
|
403
|
+
"""
|
404
|
+
Run the power flow using PYPOWER.
|
405
|
+
|
406
|
+
Returns
|
407
|
+
-------
|
408
|
+
bool
|
409
|
+
True if the optimization converged successfully, False otherwise.
|
410
|
+
"""
|
411
|
+
return super().run(**kwargs)
|
412
|
+
|
413
|
+
|
414
|
+
class DCOPF1(DCPF1):
|
415
|
+
"""
|
416
|
+
DC optimal power flow using PYPOWER.
|
417
|
+
|
418
|
+
This routine provides a wrapper for running DC optimal power flow analysis using
|
419
|
+
the PYPOWER.
|
420
|
+
It leverages PYPOWER's internal DC optimal power flow solver and maps results
|
421
|
+
back to the AMS system.
|
422
|
+
|
423
|
+
In PYPOWER, the ``c0`` term (the constant coefficient in the generator cost
|
424
|
+
function) is always included in the objective, regardless of the generator's
|
425
|
+
commitment status. See `pypower/opf_costfcn.py` for implementation details.
|
426
|
+
|
427
|
+
Known Issues
|
428
|
+
------------
|
429
|
+
- Algorithms 400, 500, 600, and 700 are not fully supported yet.
|
430
|
+
|
431
|
+
Notes
|
432
|
+
-----
|
433
|
+
- This class does not implement the AMS-style DC optimal power flow formulation.
|
434
|
+
- For detailed mathematical formulations and algorithmic details, refer to the
|
435
|
+
MATPOWER User's Manual, section on Optimal Power Flow.
|
436
|
+
"""
|
437
|
+
|
438
|
+
def __init__(self, system, config):
|
439
|
+
DCPF1.__init__(self, system, config)
|
440
|
+
self.info = 'DC Optimal Power Flow'
|
441
|
+
self.type = 'DCED'
|
442
|
+
|
443
|
+
self.map1 = OrderedDict() # DCOPF does not receive
|
444
|
+
self.map2.update({
|
445
|
+
'vBus': ('Bus', 'v0'),
|
446
|
+
'ug': ('StaticGen', 'u'),
|
447
|
+
'pg': ('StaticGen', 'p0'),
|
448
|
+
})
|
449
|
+
self.config.add(OrderedDict((('opf_alg_dc', 200),
|
450
|
+
('opf_violation', 5e-6),
|
451
|
+
('opf_flow_lim', 0),
|
452
|
+
('opf_ignore_ang_lim', 0),
|
453
|
+
('grb_method', 1),
|
454
|
+
('grb_timelimit', float('inf')),
|
455
|
+
('grb_threads', 0),
|
456
|
+
('grb_opt', 0),
|
457
|
+
('pdipm_feastol', 0),
|
458
|
+
('pdipm_gradtol', 1e-6),
|
459
|
+
('pdipm_comptol', 1e-6),
|
460
|
+
('pdipm_costtol', 1e-6),
|
461
|
+
('pdipm_max_it', 150),
|
462
|
+
('scpdipm_red_it', 20),
|
463
|
+
)))
|
464
|
+
opf_alg_dc = "0: choose default solver based on availability, 200: PIPS, 250: PIPS-sc, "
|
465
|
+
opf_alg_dc += "400: IPOPT, 500: CPLEX, 600: MOSEK, 700: GUROBI"
|
466
|
+
opf_flow_lim = "qty to limit for branch flow constraints: 0 - apparent power flow (limit in MVA), "
|
467
|
+
opf_flow_lim += "1 - active power flow (limit in MW), "
|
468
|
+
opf_flow_lim += "2 - current magnitude (limit in MVA at 1 p.u. voltage)"
|
469
|
+
grb_method = "0 - primal simplex, 1 - dual simplex, 2 - barrier, 3 - concurrent (LP only), "
|
470
|
+
grb_method += "4 - deterministic concurrent (LP only)"
|
471
|
+
pdipm_feastol = "feasibility (equality) tolerance for Primal-Dual Interior Points Methods, "
|
472
|
+
pdipm_feastol += "set to value of OPF_VIOLATION by default"
|
473
|
+
pdipm_gradtol = "gradient tolerance for Primal-Dual Interior Points Methods"
|
474
|
+
pdipm_comptol = "complementary condition (inequality) tolerance for Primal-Dual Interior Points Methods"
|
475
|
+
scpdipm_red_it = "maximum reductions per iteration for Step-Control Primal-Dual Interior Points Methods"
|
476
|
+
self.config.add_extra("_help",
|
477
|
+
opf_alg_dc=opf_alg_dc,
|
478
|
+
opf_violation="constraint violation tolerance",
|
479
|
+
opf_flow_lim=opf_flow_lim,
|
480
|
+
opf_ignore_ang_lim="ignore angle difference limits for branches even if specified",
|
481
|
+
grb_method=grb_method,
|
482
|
+
grb_timelimit="maximum time allowed for solver (TimeLimit)",
|
483
|
+
grb_threads="(auto) maximum number of threads to use (Threads)",
|
484
|
+
grb_opt="See gurobi_options() for details",
|
485
|
+
pdipm_feastol=pdipm_feastol,
|
486
|
+
pdipm_gradtol=pdipm_gradtol,
|
487
|
+
pdipm_comptol=pdipm_comptol,
|
488
|
+
pdipm_costtol="optimality tolerance for Primal-Dual Interior Points Methods",
|
489
|
+
pdipm_max_it="maximum iterations for Primal-Dual Interior Points Methods",
|
490
|
+
scpdipm_red_it=scpdipm_red_it,
|
491
|
+
)
|
492
|
+
self.config.add_extra("_alt",
|
493
|
+
opf_alg_dc=(0, 200, 250, 400, 500, 600, 700),
|
494
|
+
opf_violation=">=0",
|
495
|
+
opf_flow_lim=(0, 1, 2),
|
496
|
+
opf_ignore_ang_lim=(0, 1),
|
497
|
+
grb_method=(0, 1, 2, 3, 4),
|
498
|
+
grb_timelimit=(0, float('inf')),
|
499
|
+
grb_threads=(0, 1),
|
500
|
+
grb_opt=(0, 1),
|
501
|
+
pdipm_feastol=">=0",
|
502
|
+
pdipm_gradtol=">=0",
|
503
|
+
pdipm_comptol=">=0",
|
504
|
+
pdipm_costtol=">=0",
|
505
|
+
pdipm_max_it=">=0",
|
506
|
+
scpdipm_red_it=">=0",
|
507
|
+
)
|
508
|
+
self.config.add_extra("_tex",
|
509
|
+
opf_alg_dc=r'o_{pf\_alg\_dc}',
|
510
|
+
opf_violation=r'o_{pf\_violation}',
|
511
|
+
opf_flow_lim=r'o_{pf\_flow\_lim}',
|
512
|
+
opf_ignore_ang_lim=r'o_{pf\_ignore\_ang\_lim}',
|
513
|
+
grb_method=r'o_{grb\_method}',
|
514
|
+
grb_timelimit=r'o_{grb\_timelimit}',
|
515
|
+
grb_threads=r'o_{grb\_threads}',
|
516
|
+
grb_opt=r'o_{grb\_opt}',
|
517
|
+
pdipm_feastol=r'o_{pdipm\_feastol}',
|
518
|
+
pdipm_gradtol=r'o_{pdipm\_gradtol}',
|
519
|
+
pdipm_comptol=r'o_{pdipm\_comptol}',
|
520
|
+
pdipm_costtol=r'o_{pdipm\_costtol}',
|
521
|
+
pdipm_max_it=r'o_{pdipm\_max\_it}',
|
522
|
+
scpdipm_red_it=r'o_{scpdipm\_red\_it}',
|
523
|
+
)
|
524
|
+
|
525
|
+
self.obj = Objective(name='obj',
|
526
|
+
info='total cost, placeholder',
|
527
|
+
e_str='sum(c2 * pg**2 + c1 * pg + c0)',
|
528
|
+
sense='min',)
|
529
|
+
|
530
|
+
self.pi = Var(info='Lagrange multiplier on real power mismatch',
|
531
|
+
name='pi', unit='$/p.u.',
|
532
|
+
model='Bus', src=None,)
|
533
|
+
self.piq = Var(info='Lagrange multiplier on reactive power mismatch',
|
534
|
+
name='piq', unit='$/p.u.',
|
535
|
+
model='Bus', src=None,)
|
536
|
+
|
537
|
+
self.mu1 = Var(info='Kuhn-Tucker multiplier on MVA limit at bus1',
|
538
|
+
name='mu1', unit='$/p.u.',
|
539
|
+
model='Line', src=None,)
|
540
|
+
self.mu2 = Var(info='Kuhn-Tucker multiplier on MVA limit at bus2',
|
541
|
+
name='mu2', unit='$/p.u.',
|
542
|
+
model='Line', src=None,)
|
543
|
+
|
544
|
+
def solve(self, **kwargs):
|
545
|
+
ppc = system2ppc(self.system)
|
546
|
+
config = {key.upper(): value for key, value in self.config._dict.items()}
|
547
|
+
ppopt = ppoption(PF_DC=True, **config) # Enforce DCOPF
|
548
|
+
res = runopf(casedata=ppc, ppopt=ppopt)
|
549
|
+
return res
|
550
|
+
|
551
|
+
def unpack(self, res, **kwargs):
|
552
|
+
mva = res['baseMVA']
|
553
|
+
self.pi.optz.value = res['bus'][:, 13] / mva
|
554
|
+
self.piq.optz.value = res['bus'][:, 14] / mva
|
555
|
+
self.mu1.optz.value = res['branch'][:, 17] / mva
|
556
|
+
self.mu2.optz.value = res['branch'][:, 18] / mva
|
557
|
+
return super().unpack(res)
|
558
|
+
|
559
|
+
def run(self, **kwargs):
|
560
|
+
"""
|
561
|
+
Run the DCOPF routine using PYPOWER.
|
562
|
+
|
563
|
+
Returns
|
564
|
+
-------
|
565
|
+
bool
|
566
|
+
True if the optimization converged successfully, False otherwise.
|
567
|
+
"""
|
568
|
+
return super().run(**kwargs)
|
569
|
+
|
570
|
+
|
571
|
+
class ACOPF1(DCOPF1):
|
572
|
+
"""
|
573
|
+
AC optimal power flow using PYPOWER.
|
574
|
+
|
575
|
+
This routine provides a wrapper for running AC optimal power flow analysis using
|
576
|
+
the PYPOWER.
|
577
|
+
It leverages PYPOWER's internal AC optimal power flow solver and maps results
|
578
|
+
back to the AMS system.
|
579
|
+
|
580
|
+
In PYPOWER, the ``c0`` term (the constant coefficient in the generator cost
|
581
|
+
function) is always included in the objective, regardless of the generator's
|
582
|
+
commitment status. See `pypower/opf_costfcn.py` for implementation details.
|
583
|
+
|
584
|
+
Notes
|
585
|
+
-----
|
586
|
+
- This class does not implement the AMS-style AC optimal power flow formulation.
|
587
|
+
- For detailed mathematical formulations and algorithmic details, refer to the
|
588
|
+
MATPOWER User's Manual, section on Optimal Power Flow.
|
589
|
+
"""
|
590
|
+
|
591
|
+
def __init__(self, system, config):
|
592
|
+
DCOPF1.__init__(self, system, config)
|
593
|
+
self.info = 'AC Optimal Power Flow'
|
594
|
+
self.type = 'ACED'
|
595
|
+
|
596
|
+
self.map1 = OrderedDict() # ACOPF does not receive
|
597
|
+
self.map2.update({
|
598
|
+
'vBus': ('Bus', 'v0'),
|
599
|
+
'ug': ('StaticGen', 'u'),
|
600
|
+
'pg': ('StaticGen', 'p0'),
|
601
|
+
})
|
602
|
+
|
603
|
+
self.config.add(OrderedDict((('opf_alg', 0),
|
604
|
+
)))
|
605
|
+
self.config.add_extra("_help",
|
606
|
+
opf_alg="algorithm to use for OPF: 0 - default, 580 - PIPS"
|
607
|
+
)
|
608
|
+
self.config.add_extra("_alt",
|
609
|
+
opf_alg=(0, 580),
|
610
|
+
)
|
611
|
+
self.config.add_extra("_tex",
|
612
|
+
opf_alg=r'o_{pf\_alg}',
|
613
|
+
)
|
614
|
+
|
615
|
+
def solve(self, **kwargs):
|
616
|
+
ppc = system2ppc(self.system)
|
617
|
+
config = {key.upper(): value for key, value in self.config._dict.items()}
|
618
|
+
ppopt = ppoption(PF_DC=False, **config)
|
619
|
+
res = runopf(casedata=ppc, ppopt=ppopt)
|
620
|
+
return res
|
621
|
+
|
622
|
+
def run(self, **kwargs):
|
623
|
+
"""
|
624
|
+
Run the ACOPF routine using PYPOWER.
|
625
|
+
|
626
|
+
Returns
|
627
|
+
-------
|
628
|
+
bool
|
629
|
+
True if the optimization converged successfully, False otherwise.
|
630
|
+
"""
|
631
|
+
super().run(**kwargs)
|
ams/routines/routine.py
CHANGED
@@ -9,9 +9,9 @@ from collections import OrderedDict
|
|
9
9
|
|
10
10
|
import numpy as np
|
11
11
|
|
12
|
-
from andes.core import Config
|
13
12
|
from andes.utils.misc import elapsed
|
14
13
|
|
14
|
+
from ams.core import Config
|
15
15
|
from ams.core.param import RParam
|
16
16
|
from ams.core.symprocessor import SymProcessor
|
17
17
|
from ams.core.documenter import RDocumenter
|
@@ -361,7 +361,7 @@ class RoutineBase:
|
|
361
361
|
"""
|
362
362
|
raise NotImplementedError
|
363
363
|
|
364
|
-
def unpack(self, **kwargs):
|
364
|
+
def unpack(self, res, **kwargs):
|
365
365
|
"""
|
366
366
|
Unpack the results.
|
367
367
|
"""
|
@@ -420,7 +420,7 @@ class RoutineBase:
|
|
420
420
|
msg = f"<{self.class_name}> solved as {status} in {s}, converged in "
|
421
421
|
msg += n_iter_str + f"with {sstats.solver_name}."
|
422
422
|
logger.warning(msg)
|
423
|
-
self.unpack(**kwargs)
|
423
|
+
self.unpack(res=None, **kwargs)
|
424
424
|
self._post_solve()
|
425
425
|
self.system.report()
|
426
426
|
return True
|
@@ -484,13 +484,7 @@ class RoutineBase:
|
|
484
484
|
def __repr__(self):
|
485
485
|
return f"{self.class_name} at {hex(id(self))}"
|
486
486
|
|
487
|
-
def
|
488
|
-
"""
|
489
|
-
Convert PYPOWER results to AMS.
|
490
|
-
"""
|
491
|
-
raise NotImplementedError
|
492
|
-
|
493
|
-
def dc2ac(self, **kwargs):
|
487
|
+
def dc2ac(self, kloss=1.0, **kwargs):
|
494
488
|
"""
|
495
489
|
Convert the DC-based results with ACOPF.
|
496
490
|
"""
|
ams/routines/uc.py
CHANGED
@@ -315,14 +315,14 @@ class UC(DCOPF, RTEDBase, MPBase, SRBase, NSRBase):
|
|
315
315
|
self._initial_guess()
|
316
316
|
return super().init(**kwargs)
|
317
317
|
|
318
|
-
def dc2ac(self, **kwargs):
|
318
|
+
def dc2ac(self, kloss=1.0, **kwargs):
|
319
319
|
"""
|
320
320
|
AC conversion ``dc2ac`` is not implemented yet for
|
321
321
|
multi-period scheduling.
|
322
322
|
"""
|
323
323
|
return NotImplementedError
|
324
324
|
|
325
|
-
def unpack(self, **kwargs):
|
325
|
+
def unpack(self, res, **kwargs):
|
326
326
|
"""
|
327
327
|
Multi-period scheduling will not unpack results from
|
328
328
|
solver into devices.
|