ltbams 1.0.8__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.
Files changed (83) hide show
  1. ams/__init__.py +0 -1
  2. ams/_version.py +3 -3
  3. ams/cases/5bus/pjm5bus_demo.json +1324 -0
  4. ams/core/__init__.py +1 -0
  5. ams/core/common.py +30 -0
  6. ams/core/model.py +1 -1
  7. ams/core/symprocessor.py +1 -1
  8. ams/extension/eva.py +1 -1
  9. ams/interface.py +40 -24
  10. ams/io/matpower.py +192 -26
  11. ams/io/psse.py +278 -1
  12. ams/io/pypower.py +14 -0
  13. ams/main.py +2 -2
  14. ams/models/group.py +2 -70
  15. ams/models/static/pq.py +7 -3
  16. ams/opt/param.py +1 -2
  17. ams/report.py +3 -4
  18. ams/routines/__init__.py +2 -3
  19. ams/routines/acopf.py +5 -108
  20. ams/routines/dcopf.py +8 -0
  21. ams/routines/dcpf.py +1 -1
  22. ams/routines/ed.py +4 -2
  23. ams/routines/grbopt.py +150 -0
  24. ams/routines/pflow.py +2 -2
  25. ams/routines/pypower.py +631 -0
  26. ams/routines/routine.py +4 -10
  27. ams/routines/uc.py +2 -2
  28. ams/shared.py +30 -44
  29. ams/system.py +118 -2
  30. docs/source/api.rst +2 -0
  31. docs/source/getting_started/formats/matpower.rst +135 -0
  32. docs/source/getting_started/formats/pypower.rst +1 -2
  33. docs/source/getting_started/install.rst +9 -6
  34. docs/source/images/dcopf_time.png +0 -0
  35. docs/source/images/educ_pie.png +0 -0
  36. docs/source/release-notes.rst +29 -47
  37. {ltbams-1.0.8.dist-info → ltbams-1.0.10.dist-info}/METADATA +87 -47
  38. {ltbams-1.0.8.dist-info → ltbams-1.0.10.dist-info}/RECORD +58 -75
  39. {ltbams-1.0.8.dist-info → ltbams-1.0.10.dist-info}/WHEEL +1 -1
  40. tests/test_1st_system.py +1 -1
  41. tests/test_case.py +14 -14
  42. tests/test_export_csv.py +1 -1
  43. tests/test_interface.py +24 -2
  44. tests/test_io.py +125 -1
  45. tests/test_omodel.py +1 -1
  46. tests/test_report.py +6 -6
  47. tests/test_routine.py +2 -2
  48. tests/test_rtn_acopf.py +75 -0
  49. tests/test_rtn_dcopf.py +1 -1
  50. tests/test_rtn_dcopf2.py +1 -1
  51. tests/test_rtn_ed.py +9 -9
  52. tests/test_rtn_opf.py +142 -0
  53. tests/test_rtn_pflow.py +0 -72
  54. tests/test_rtn_pypower.py +315 -0
  55. tests/test_rtn_rted.py +8 -8
  56. tests/test_rtn_uc.py +18 -18
  57. ams/pypower/__init__.py +0 -8
  58. ams/pypower/_compat.py +0 -9
  59. ams/pypower/core/__init__.py +0 -8
  60. ams/pypower/core/pips.py +0 -894
  61. ams/pypower/core/ppoption.py +0 -244
  62. ams/pypower/core/ppver.py +0 -18
  63. ams/pypower/core/solver.py +0 -2451
  64. ams/pypower/eps.py +0 -6
  65. ams/pypower/idx.py +0 -174
  66. ams/pypower/io.py +0 -604
  67. ams/pypower/make/__init__.py +0 -11
  68. ams/pypower/make/matrices.py +0 -665
  69. ams/pypower/make/pdv.py +0 -506
  70. ams/pypower/routines/__init__.py +0 -7
  71. ams/pypower/routines/cpf.py +0 -513
  72. ams/pypower/routines/cpf_callbacks.py +0 -114
  73. ams/pypower/routines/opf.py +0 -1803
  74. ams/pypower/routines/opffcns.py +0 -1946
  75. ams/pypower/routines/pflow.py +0 -852
  76. ams/pypower/toggle.py +0 -1098
  77. ams/pypower/utils.py +0 -293
  78. ams/routines/cpf.py +0 -65
  79. ams/routines/dcpf0.py +0 -196
  80. ams/routines/pflow0.py +0 -113
  81. tests/test_rtn_dcpf.py +0 -77
  82. {ltbams-1.0.8.dist-info → ltbams-1.0.10.dist-info}/entry_points.txt +0 -0
  83. {ltbams-1.0.8.dist-info → ltbams-1.0.10.dist-info}/top_level.txt +0 -0
@@ -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 _ppc2ams(self):
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.