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/acopf.py
CHANGED
@@ -1,117 +1,14 @@
|
|
1
1
|
"""
|
2
|
-
ACOPF routines
|
2
|
+
ACOPF routines.
|
3
3
|
"""
|
4
|
-
import logging
|
5
|
-
from collections import OrderedDict
|
6
4
|
|
7
|
-
from ams.pypower import
|
8
|
-
from ams.pypower.core import ppoption
|
5
|
+
from ams.routines.pypower import ACOPF1
|
9
6
|
|
10
|
-
from ams.io.pypower import system2ppc
|
11
|
-
from ams.core.param import RParam
|
12
7
|
|
13
|
-
|
14
|
-
from ams.opt import Var, Constraint, Objective
|
15
|
-
|
16
|
-
logger = logging.getLogger(__name__)
|
17
|
-
|
18
|
-
|
19
|
-
class ACOPF(DCPF0):
|
8
|
+
class ACOPF(ACOPF1):
|
20
9
|
"""
|
21
|
-
|
22
|
-
|
23
|
-
Notes
|
24
|
-
-----
|
25
|
-
1. ACOPF is solved with PYPOWER ``runopf`` function.
|
26
|
-
2. ACOPF formulation in AMS style is NOT DONE YET,
|
27
|
-
but this does not affect the results
|
28
|
-
because the data are passed to PYPOWER for solving.
|
10
|
+
Alias for ACOPF1.
|
29
11
|
"""
|
30
12
|
|
31
13
|
def __init__(self, system, config):
|
32
|
-
|
33
|
-
self.info = 'AC Optimal Power Flow'
|
34
|
-
self.type = 'ACED'
|
35
|
-
|
36
|
-
self.map1 = OrderedDict() # ACOPF does not receive
|
37
|
-
self.map2.update({
|
38
|
-
'vBus': ('Bus', 'v0'),
|
39
|
-
'ug': ('StaticGen', 'u'),
|
40
|
-
'pg': ('StaticGen', 'p0'),
|
41
|
-
})
|
42
|
-
|
43
|
-
# --- params ---
|
44
|
-
self.c2 = RParam(info='Gen cost coefficient 2',
|
45
|
-
name='c2', tex_name=r'c_{2}',
|
46
|
-
unit=r'$/(p.u.^2)', model='GCost',
|
47
|
-
indexer='gen', imodel='StaticGen',
|
48
|
-
nonneg=True)
|
49
|
-
self.c1 = RParam(info='Gen cost coefficient 1',
|
50
|
-
name='c1', tex_name=r'c_{1}',
|
51
|
-
unit=r'$/(p.u.)', model='GCost',
|
52
|
-
indexer='gen', imodel='StaticGen',)
|
53
|
-
self.c0 = RParam(info='Gen cost coefficient 0',
|
54
|
-
name='c0', tex_name=r'c_{0}',
|
55
|
-
unit=r'$', model='GCost',
|
56
|
-
indexer='gen', imodel='StaticGen',
|
57
|
-
no_parse=True)
|
58
|
-
self.qd = RParam(info='reactive demand',
|
59
|
-
name='qd', tex_name=r'q_{d}',
|
60
|
-
model='StaticLoad', src='q0',
|
61
|
-
unit='p.u.',)
|
62
|
-
# --- bus ---
|
63
|
-
self.vBus = Var(info='Bus voltage magnitude',
|
64
|
-
unit='p.u.',
|
65
|
-
name='vBus', tex_name=r'v_{Bus}',
|
66
|
-
src='v', model='Bus',)
|
67
|
-
# --- gen ---
|
68
|
-
self.qg = Var(info='Gen reactive power',
|
69
|
-
unit='p.u.',
|
70
|
-
name='qg', tex_name=r'q_{g}',
|
71
|
-
model='StaticGen', src='q',)
|
72
|
-
# --- constraints ---
|
73
|
-
self.pb = Constraint(name='pb',
|
74
|
-
info='power balance',
|
75
|
-
e_str='sum(pd) - sum(pg)',
|
76
|
-
is_eq=True,)
|
77
|
-
# TODO: ACOPF formulation
|
78
|
-
# --- objective ---
|
79
|
-
self.obj = Objective(name='obj',
|
80
|
-
info='total cost',
|
81
|
-
e_str='sum(c2 * pg**2 + c1 * pg + c0)',
|
82
|
-
sense='min',)
|
83
|
-
|
84
|
-
def solve(self, method=None, **kwargs):
|
85
|
-
"""
|
86
|
-
Solve ACOPF using PYPOWER with PIPS.
|
87
|
-
"""
|
88
|
-
ppc = system2ppc(self.system)
|
89
|
-
ppopt = ppoption()
|
90
|
-
res, sstats = runopf(casedata=ppc, ppopt=ppopt, **kwargs)
|
91
|
-
return res, sstats
|
92
|
-
|
93
|
-
def run(self, **kwargs):
|
94
|
-
"""
|
95
|
-
Run ACOPF using PYPOWER with PIPS.
|
96
|
-
*args and **kwargs go to `self.solve()`, which are not used yet.
|
97
|
-
|
98
|
-
Examples
|
99
|
-
--------
|
100
|
-
>>> ss = ams.load(ams.get_case('matpower/case14.m'))
|
101
|
-
>>> ss.ACOPF.run()
|
102
|
-
|
103
|
-
Parameters
|
104
|
-
----------
|
105
|
-
force_init : bool
|
106
|
-
Force initialization.
|
107
|
-
no_code : bool
|
108
|
-
Disable showing code.
|
109
|
-
method : str
|
110
|
-
Placeholder for future use.
|
111
|
-
|
112
|
-
Returns
|
113
|
-
-------
|
114
|
-
exit_code : int
|
115
|
-
Exit code of the routine.
|
116
|
-
"""
|
117
|
-
super().run(**kwargs)
|
14
|
+
super().__init__(system, config)
|
ams/routines/dcopf.py
CHANGED
@@ -141,6 +141,14 @@ class DCOPF(DCPFBase):
|
|
141
141
|
name='pi', unit='$/p.u.',
|
142
142
|
model='Bus', src=None,
|
143
143
|
e_str='pb.dual_variables[0]')
|
144
|
+
self.mu1 = ExpressionCalc(info='Lagrange multipliers, dual of <plflb>',
|
145
|
+
name='mu1', unit='$/p.u.',
|
146
|
+
model='Line', src=None,
|
147
|
+
e_str='plflb.dual_variables[0]')
|
148
|
+
self.mu2 = ExpressionCalc(info='Lagrange multipliers, dual of <plfub>',
|
149
|
+
name='mu2', unit='$/p.u.',
|
150
|
+
model='Line', src=None,
|
151
|
+
e_str='plfub.dual_variables[0]')
|
144
152
|
|
145
153
|
# --- objective ---
|
146
154
|
obj = 'sum(mul(c2, pg**2))'
|
ams/routines/dcpf.py
CHANGED
ams/routines/ed.py
CHANGED
@@ -104,6 +104,8 @@ class MPBase:
|
|
104
104
|
self.vBus.horizon = self.timeslot
|
105
105
|
self.vBus.info = '2D Bus voltage'
|
106
106
|
self.pi.horizon = self.timeslot
|
107
|
+
self.mu1.horizon = self.timeslot
|
108
|
+
self.mu2.horizon = self.timeslot
|
107
109
|
|
108
110
|
|
109
111
|
class ED(RTED, MPBase, SRBase):
|
@@ -214,14 +216,14 @@ class ED(RTED, MPBase, SRBase):
|
|
214
216
|
cost += '+ sum(mul(ugt, mul(c0, tlv)))'
|
215
217
|
self.obj.e_str = cost
|
216
218
|
|
217
|
-
def dc2ac(self, **kwargs):
|
219
|
+
def dc2ac(self, kloss=1.0, **kwargs):
|
218
220
|
"""
|
219
221
|
AC conversion ``dc2ac`` is not implemented yet for
|
220
222
|
multi-period scheduling.
|
221
223
|
"""
|
222
224
|
return NotImplementedError
|
223
225
|
|
224
|
-
def unpack(self, **kwargs):
|
226
|
+
def unpack(self, res, **kwargs):
|
225
227
|
"""
|
226
228
|
Multi-period scheduling will not unpack results from
|
227
229
|
solver into devices.
|
ams/routines/grbopt.py
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
"""
|
2
|
+
Routines using gurobi-optimods.
|
3
|
+
"""
|
4
|
+
import logging
|
5
|
+
|
6
|
+
import io
|
7
|
+
|
8
|
+
import scipy.io
|
9
|
+
|
10
|
+
from andes.shared import np, pd
|
11
|
+
from andes.utils.misc import elapsed
|
12
|
+
|
13
|
+
from ams.io.pypower import system2ppc
|
14
|
+
|
15
|
+
from ams.opt import Var, Objective
|
16
|
+
|
17
|
+
from ams.routines.pypower import DCPF1
|
18
|
+
from ams.shared import opf
|
19
|
+
|
20
|
+
logger = logging.getLogger(__name__)
|
21
|
+
|
22
|
+
|
23
|
+
class OPF(DCPF1):
|
24
|
+
"""
|
25
|
+
Optimal Power Flow (OPF) routine using gurobi-optimods.
|
26
|
+
|
27
|
+
This class provides an interface for performing optimal power flow analysis
|
28
|
+
with gurobi-optimods, supporting both AC and DC OPF formulations.
|
29
|
+
|
30
|
+
In addition to optimizing generator dispatch, this routine can also optimize
|
31
|
+
transmission line statuses (branch switching), enabling topology optimization.
|
32
|
+
Refer to the gurobi-optimods documentation for further details:
|
33
|
+
|
34
|
+
https://gurobi-optimods.readthedocs.io/en/stable/mods/opf/opf.html
|
35
|
+
"""
|
36
|
+
|
37
|
+
def __init__(self, system, config):
|
38
|
+
DCPF1.__init__(self, system, config)
|
39
|
+
self.info = 'Optimal Power Flow'
|
40
|
+
self.type = 'ACED'
|
41
|
+
|
42
|
+
self.obj = Objective(name='obj',
|
43
|
+
info='total cost, placeholder',
|
44
|
+
e_str='sum(c2 * pg**2 + c1 * pg + c0)',
|
45
|
+
sense='min',)
|
46
|
+
|
47
|
+
self.pi = Var(info='Lagrange multiplier on real power mismatch',
|
48
|
+
name='pi', unit='$/p.u.',
|
49
|
+
model='Bus', src=None,)
|
50
|
+
|
51
|
+
self.uld = Var(info='Line commitment decision',
|
52
|
+
name='uld', tex_name=r'u_{l,d}',
|
53
|
+
model='Line', src='u',)
|
54
|
+
|
55
|
+
def solve(self, **kwargs):
|
56
|
+
ppc = system2ppc(self.system)
|
57
|
+
mat = io.BytesIO()
|
58
|
+
scipy.io.savemat(mat, {'mpc': ppc})
|
59
|
+
mat.seek(0)
|
60
|
+
res = opf.solve_opf(opf.read_case_matpower(mat), **kwargs)
|
61
|
+
return res
|
62
|
+
|
63
|
+
def unpack(self, res, **kwargs):
|
64
|
+
"""
|
65
|
+
Unpack the results from the gurobi-optimods.
|
66
|
+
"""
|
67
|
+
# NOTE: Map gurobi-optimods results to PPC-compatible format.
|
68
|
+
# Only relevant columns are populated, as required by `DCOPF.unpack()`.
|
69
|
+
# If future versions of gurobi-optimods provide additional outputs,
|
70
|
+
# this mapping may need to be updated to extract and assign new fields.
|
71
|
+
|
72
|
+
res_new = dict()
|
73
|
+
res_new['success'] = res['success']
|
74
|
+
res_new['et'] = res['et']
|
75
|
+
res_new['f'] = res['f']
|
76
|
+
res_new['baseMVA'] = res['baseMVA']
|
77
|
+
|
78
|
+
bus = pd.DataFrame(res['bus'])
|
79
|
+
res_new['bus'] = np.zeros((self.system.Bus.n, 17))
|
80
|
+
res_new['bus'][:, 7] = bus['Vm'].values
|
81
|
+
res_new['bus'][:, 8] = bus['Va'].values
|
82
|
+
# NOTE: As of v2.3.2, gurobi-optimods does not return LMP
|
83
|
+
|
84
|
+
gen = pd.DataFrame(res['gen'])
|
85
|
+
res_new['gen'] = np.zeros((self.system.StaticGen.n, 14))
|
86
|
+
res_new['gen'][:, 1] = gen['Pg'].values
|
87
|
+
res_new['gen'][:, 2] = gen['Qg'].values
|
88
|
+
|
89
|
+
branch = pd.DataFrame(res['branch'])
|
90
|
+
res_new['branch'] = np.zeros((self.system.Line.n, 14))
|
91
|
+
res_new['branch'][:, 13] = branch['Pf'].values
|
92
|
+
# NOTE: unpack branch_switching decision
|
93
|
+
res_new['branch'][:, 10] = branch['switching'].values
|
94
|
+
return super().unpack(res_new)
|
95
|
+
|
96
|
+
def run(self, **kwargs):
|
97
|
+
"""
|
98
|
+
Run the OPF routine using gurobi-optimods.
|
99
|
+
|
100
|
+
This method invokes `self.solve(**kwargs)`, which internally utilizes
|
101
|
+
`gurobi-optimods` to solve the OPF problem.
|
102
|
+
|
103
|
+
Keyword arguments
|
104
|
+
-------------------
|
105
|
+
- ``opftype`` : str
|
106
|
+
Type of OPF to solve (default: 'AC').
|
107
|
+
- ``branch_switching`` : bool
|
108
|
+
Enable branch switching (default: False).
|
109
|
+
- ``min_active_branches`` : float
|
110
|
+
Defines the minimum number of branches that must be turned on when
|
111
|
+
branch switching is active, i.e. the minimum number of turned on
|
112
|
+
branches is equal to ``numbranches * min_active_branches``. Has no
|
113
|
+
effect if ``branch_switching`` is set to False.
|
114
|
+
- ``use_mip_start`` : bool
|
115
|
+
Use MIP start (default: False).
|
116
|
+
- ``time_limit`` : float
|
117
|
+
Time limit for the solver (default: 0.0, no limit).
|
118
|
+
"""
|
119
|
+
if not self.initialized:
|
120
|
+
self.init()
|
121
|
+
t0, _ = elapsed()
|
122
|
+
|
123
|
+
# --- solve optimization ---
|
124
|
+
t0, _ = elapsed()
|
125
|
+
res = self.solve(**kwargs)
|
126
|
+
self.converged = res['success']
|
127
|
+
self.exit_code = 0 if res['success'] else 1
|
128
|
+
_, s = elapsed(t0)
|
129
|
+
self.exec_time = float(s.split(" ")[0])
|
130
|
+
try:
|
131
|
+
n_iter = res['raw']['output']['iterations']
|
132
|
+
except Exception:
|
133
|
+
n_iter = -1
|
134
|
+
n_iter_str = f"{n_iter} iterations " if n_iter > 1 else f"{n_iter} iteration "
|
135
|
+
if self.exit_code == 0:
|
136
|
+
msg = f"<{self.class_name}> converged in {s}, "
|
137
|
+
msg += n_iter_str + "with gurobi-optimods."
|
138
|
+
logger.warning(msg)
|
139
|
+
try:
|
140
|
+
self.unpack(res)
|
141
|
+
except Exception as e:
|
142
|
+
logger.error(f"Failed to unpack results from {self.class_name}.\n{e}")
|
143
|
+
return False
|
144
|
+
self.system.report()
|
145
|
+
return True
|
146
|
+
else:
|
147
|
+
msg = f"{self.class_name} failed to converge in {s}, "
|
148
|
+
msg += n_iter_str + "with gurobi-optimods."
|
149
|
+
logger.warning(msg)
|
150
|
+
return False
|
ams/routines/pflow.py
CHANGED
@@ -130,7 +130,7 @@ class PFlow(RoutineBase):
|
|
130
130
|
_, s = elapsed(t0)
|
131
131
|
self.exec_time = float(s.split(" ")[0])
|
132
132
|
|
133
|
-
self.unpack()
|
133
|
+
self.unpack(res=None)
|
134
134
|
return self.converged
|
135
135
|
|
136
136
|
def _post_solve(self):
|
@@ -139,7 +139,7 @@ class PFlow(RoutineBase):
|
|
139
139
|
"""
|
140
140
|
return True
|
141
141
|
|
142
|
-
def unpack(self, **kwargs):
|
142
|
+
def unpack(self, res, **kwargs):
|
143
143
|
"""
|
144
144
|
Unpack the results from ANDES PFlow routine.
|
145
145
|
"""
|