ltbams 0.9.9__py3-none-any.whl → 1.0.2__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 +206 -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 +231 -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.2.dist-info/METADATA +215 -0
- ltbams-1.0.2.dist-info/RECORD +188 -0
- {ltbams-0.9.9.dist-info → ltbams-1.0.2.dist-info}/WHEEL +1 -1
- ltbams-1.0.2.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.2.dist-info}/entry_points.txt +0 -0
ams/routines/dcpf0.py
ADDED
@@ -0,0 +1,196 @@
|
|
1
|
+
"""
|
2
|
+
DC power flow routines using PYPOWER.
|
3
|
+
"""
|
4
|
+
import logging
|
5
|
+
|
6
|
+
from andes.shared import deg2rad
|
7
|
+
from andes.utils.misc import elapsed
|
8
|
+
|
9
|
+
from ams.routines.routine import RoutineBase
|
10
|
+
from ams.opt import Var
|
11
|
+
from ams.pypower import runpf
|
12
|
+
from ams.pypower.core import ppoption
|
13
|
+
|
14
|
+
from ams.io.pypower import system2ppc
|
15
|
+
from ams.core.param import RParam
|
16
|
+
|
17
|
+
logger = logging.getLogger(__name__)
|
18
|
+
|
19
|
+
|
20
|
+
class DCPF0(RoutineBase):
|
21
|
+
"""
|
22
|
+
DC power flow using PYPOWER.
|
23
|
+
|
24
|
+
This class is deprecated as of version 0.9.12 and will be removed in 1.1.0.
|
25
|
+
|
26
|
+
Notes
|
27
|
+
-----
|
28
|
+
1. DCPF is solved with PYPOWER ``runpf`` function.
|
29
|
+
2. DCPF formulation is not complete yet, but this does not affect the
|
30
|
+
results because the data are passed to PYPOWER for solving.
|
31
|
+
"""
|
32
|
+
|
33
|
+
def __init__(self, system, config):
|
34
|
+
RoutineBase.__init__(self, system, config)
|
35
|
+
self.info = 'DC Power Flow'
|
36
|
+
self.type = 'PF'
|
37
|
+
|
38
|
+
self.ug = RParam(info='Gen connection status',
|
39
|
+
name='ug', tex_name=r'u_{g}',
|
40
|
+
model='StaticGen', src='u',
|
41
|
+
no_parse=True)
|
42
|
+
|
43
|
+
# --- routine data ---
|
44
|
+
self.x = RParam(info="line reactance",
|
45
|
+
name='x', tex_name='x',
|
46
|
+
unit='p.u.',
|
47
|
+
model='Line', src='x',)
|
48
|
+
self.tap = RParam(info="transformer branch tap ratio",
|
49
|
+
name='tap', tex_name=r't_{ap}',
|
50
|
+
model='Line', src='tap',
|
51
|
+
unit='float',)
|
52
|
+
self.phi = RParam(info="transformer branch phase shift in rad",
|
53
|
+
name='phi', tex_name=r'\phi',
|
54
|
+
model='Line', src='phi',
|
55
|
+
unit='radian',)
|
56
|
+
|
57
|
+
# --- load ---
|
58
|
+
self.pd = RParam(info='active deman',
|
59
|
+
name='pd', tex_name=r'p_{d}',
|
60
|
+
unit='p.u.',
|
61
|
+
model='StaticLoad', src='p0')
|
62
|
+
# --- gen ---
|
63
|
+
self.pg = Var(info='Gen active power',
|
64
|
+
unit='p.u.',
|
65
|
+
name='pg', tex_name=r'p_{g}',
|
66
|
+
model='StaticGen', src='p',)
|
67
|
+
|
68
|
+
# --- bus ---
|
69
|
+
self.aBus = Var(info='bus voltage angle',
|
70
|
+
unit='rad',
|
71
|
+
name='aBus', tex_name=r'a_{Bus}',
|
72
|
+
model='Bus', src='a',)
|
73
|
+
|
74
|
+
# --- line flow ---
|
75
|
+
self.plf = Var(info='Line flow',
|
76
|
+
unit='p.u.',
|
77
|
+
name='plf', tex_name=r'p_{lf}',
|
78
|
+
model='Line',)
|
79
|
+
|
80
|
+
def unpack(self, res):
|
81
|
+
"""
|
82
|
+
Unpack results from PYPOWER.
|
83
|
+
"""
|
84
|
+
system = self.system
|
85
|
+
mva = res['baseMVA']
|
86
|
+
|
87
|
+
# --- copy results from ppc into system algeb ---
|
88
|
+
# --- Bus ---
|
89
|
+
system.Bus.v.v = res['bus'][:, 7] # voltage magnitude
|
90
|
+
system.Bus.a.v = res['bus'][:, 8] * deg2rad # voltage angle
|
91
|
+
|
92
|
+
# --- PV ---
|
93
|
+
system.PV.p.v = res['gen'][system.Slack.n:, 1] / mva # active power
|
94
|
+
system.PV.q.v = res['gen'][system.Slack.n:, 2] / mva # reactive power
|
95
|
+
|
96
|
+
# --- Slack ---
|
97
|
+
system.Slack.p.v = res['gen'][:system.Slack.n, 1] / mva # active power
|
98
|
+
system.Slack.q.v = res['gen'][:system.Slack.n, 2] / mva # reactive power
|
99
|
+
|
100
|
+
# --- Line ---
|
101
|
+
self.plf.optz.value = res['branch'][:, 13] / mva # line flow
|
102
|
+
|
103
|
+
# --- copy results from system algeb into routine algeb ---
|
104
|
+
for vname, var in self.vars.items():
|
105
|
+
owner = getattr(system, var.model) # instance of owner, Model or Group
|
106
|
+
if var.src is None: # skip if no source variable is specified
|
107
|
+
continue
|
108
|
+
elif hasattr(owner, 'group'): # if owner is a Model instance
|
109
|
+
grp = getattr(system, owner.group)
|
110
|
+
idx = grp.get_all_idxes()
|
111
|
+
elif hasattr(owner, 'get_idx'): # if owner is a Group instance
|
112
|
+
idx = owner.get_all_idxes()
|
113
|
+
else:
|
114
|
+
msg = f"Failed to find valid source variable `{owner.class_name}.{var.src}` for "
|
115
|
+
msg += f"{self.class_name}.{vname}, skip unpacking."
|
116
|
+
logger.warning(msg)
|
117
|
+
continue
|
118
|
+
try:
|
119
|
+
logger.debug(f"Unpacking {vname} into {owner.class_name}.{var.src}.")
|
120
|
+
var.optz.value = owner.get(src=var.src, attr='v', idx=idx)
|
121
|
+
except AttributeError:
|
122
|
+
logger.debug(f"Failed to unpack {vname} into {owner.class_name}.{var.src}.")
|
123
|
+
continue
|
124
|
+
self.system.recent = self.system.routines[self.class_name]
|
125
|
+
return True
|
126
|
+
|
127
|
+
def solve(self, method=None):
|
128
|
+
"""
|
129
|
+
Solve DC power flow using PYPOWER.
|
130
|
+
"""
|
131
|
+
ppc = system2ppc(self.system)
|
132
|
+
ppopt = ppoption(PF_DC=True)
|
133
|
+
|
134
|
+
res, sstats = runpf(casedata=ppc, ppopt=ppopt)
|
135
|
+
return res, sstats
|
136
|
+
|
137
|
+
def run(self, **kwargs):
|
138
|
+
"""
|
139
|
+
Run DC pwoer flow.
|
140
|
+
*args and **kwargs go to `self.solve()`, which are not used yet.
|
141
|
+
|
142
|
+
Examples
|
143
|
+
--------
|
144
|
+
>>> ss = ams.load(ams.get_case('matpower/case14.m'))
|
145
|
+
>>> ss.DCPF.run()
|
146
|
+
|
147
|
+
Parameters
|
148
|
+
----------
|
149
|
+
method : str
|
150
|
+
Placeholder for future use.
|
151
|
+
|
152
|
+
Returns
|
153
|
+
-------
|
154
|
+
exit_code : int
|
155
|
+
Exit code of the routine.
|
156
|
+
"""
|
157
|
+
if not self.initialized:
|
158
|
+
self.init()
|
159
|
+
t0, _ = elapsed()
|
160
|
+
|
161
|
+
res, sstats = self.solve(**kwargs)
|
162
|
+
self.converged = res['success']
|
163
|
+
self.exit_code = 0 if res['success'] else 1
|
164
|
+
_, s = elapsed(t0)
|
165
|
+
self.exec_time = float(s.split(' ')[0])
|
166
|
+
n_iter = int(sstats['num_iters'])
|
167
|
+
n_iter_str = f"{n_iter} iterations " if n_iter > 1 else f"{n_iter} iteration "
|
168
|
+
if self.exit_code == 0:
|
169
|
+
msg = f"<{self.class_name}> solved in {s}, converged in "
|
170
|
+
msg += n_iter_str + f"with {sstats['solver_name']}."
|
171
|
+
logger.info(msg)
|
172
|
+
try:
|
173
|
+
self.unpack(res)
|
174
|
+
except Exception as e:
|
175
|
+
logger.error(f"Failed to unpack results from {self.class_name}.\n{e}")
|
176
|
+
return False
|
177
|
+
self.system.report()
|
178
|
+
return True
|
179
|
+
else:
|
180
|
+
msg = f"{self.class_name} failed in "
|
181
|
+
msg += f"{int(sstats['num_iters'])} iterations with "
|
182
|
+
msg += f"{sstats['solver_name']}!"
|
183
|
+
logger.warning(msg)
|
184
|
+
return False
|
185
|
+
|
186
|
+
def summary(self, **kwargs):
|
187
|
+
"""
|
188
|
+
# TODO: Print power flow summary.
|
189
|
+
"""
|
190
|
+
raise NotImplementedError
|
191
|
+
|
192
|
+
def enable(self, name):
|
193
|
+
raise NotImplementedError
|
194
|
+
|
195
|
+
def disable(self, name):
|
196
|
+
raise NotImplementedError
|
ams/routines/dopf.py
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
"""
|
2
|
+
Distributional optimal power flow (DOPF).
|
3
|
+
"""
|
4
|
+
import numpy as np
|
5
|
+
|
6
|
+
from ams.core.param import RParam
|
7
|
+
|
8
|
+
from ams.routines.dcopf import DCOPF
|
9
|
+
|
10
|
+
from ams.opt import Var, Constraint, Objective
|
11
|
+
|
12
|
+
|
13
|
+
class DOPF(DCOPF):
|
14
|
+
"""
|
15
|
+
Linearzied distribution OPF, where power loss are ignored.
|
16
|
+
|
17
|
+
UNDER DEVELOPMENT!
|
18
|
+
|
19
|
+
Reference:
|
20
|
+
|
21
|
+
[1] L. Bai, J. Wang, C. Wang, C. Chen, and F. Li, “Distribution Locational Marginal Pricing (DLMP)
|
22
|
+
for Congestion Management and Voltage Support,” IEEE Trans. Power Syst., vol. 33, no. 4,
|
23
|
+
pp. 4061–4073, Jul. 2018, doi: 10.1109/TPWRS.2017.2767632.
|
24
|
+
"""
|
25
|
+
|
26
|
+
def __init__(self, system, config):
|
27
|
+
DCOPF.__init__(self, system, config)
|
28
|
+
self.info = 'Linearzied distribution OPF'
|
29
|
+
self.type = 'DED'
|
30
|
+
|
31
|
+
# -- Data Section --
|
32
|
+
# --- generator ---
|
33
|
+
self.qmax = RParam(info='generator maximum reactive power',
|
34
|
+
name='qmax', tex_name=r'q_{max}', unit='p.u.',
|
35
|
+
model='StaticGen', src='qmax',)
|
36
|
+
self.qmin = RParam(info='generator minimum reactive power',
|
37
|
+
name='qmin', tex_name=r'q_{min}', unit='p.u.',
|
38
|
+
model='StaticGen', src='qmin',)
|
39
|
+
# --- load ---
|
40
|
+
self.qd = RParam(info='reactive demand',
|
41
|
+
name='qd', tex_name=r'q_{d}', unit='p.u.',
|
42
|
+
model='StaticLoad', src='q0',)
|
43
|
+
# --- bus ---
|
44
|
+
self.vmax = RParam(info="Bus voltage upper limit",
|
45
|
+
name='vmax', tex_name=r'v_{max}',
|
46
|
+
unit='p.u.',
|
47
|
+
model='Bus', src='vmax', no_parse=True,
|
48
|
+
)
|
49
|
+
self.vmin = RParam(info="Bus voltage lower limit",
|
50
|
+
name='vmin', tex_name=r'v_{min}',
|
51
|
+
unit='p.u.',
|
52
|
+
model='Bus', src='vmin', no_parse=True,)
|
53
|
+
# --- line ---
|
54
|
+
self.r = RParam(info='line resistance',
|
55
|
+
name='r', tex_name='r', unit='p.u.',
|
56
|
+
model='Line', src='r',
|
57
|
+
no_parse=True,)
|
58
|
+
self.x = RParam(info='line reactance',
|
59
|
+
name='x', tex_name='x', unit='p.u.',
|
60
|
+
model='Line', src='x',
|
61
|
+
no_parse=True,)
|
62
|
+
# --- Model Section ---
|
63
|
+
# --- generator ---
|
64
|
+
self.qg = Var(info='Gen reactive power',
|
65
|
+
name='qg', tex_name=r'q_{g}', unit='p.u.',
|
66
|
+
model='StaticGen', src='q',)
|
67
|
+
self.qglb = Constraint(name='qglb', is_eq=False,
|
68
|
+
info='qg min',
|
69
|
+
e_str='-qg + mul(ug, qmin)',)
|
70
|
+
self.qgub = Constraint(name='qgub', is_eq=False,
|
71
|
+
info='qg max',
|
72
|
+
e_str='qg - mul(ug, qmax)',)
|
73
|
+
# --- bus ---
|
74
|
+
self.v = Var(info='Bus voltage',
|
75
|
+
name='v', tex_name=r'v',
|
76
|
+
unit='p.u.',
|
77
|
+
model='Bus', src='v')
|
78
|
+
self.vsq = Var(info='square of Bus voltage',
|
79
|
+
name='vsq', tex_name=r'v^{2}', unit='p.u.',
|
80
|
+
model='Bus',)
|
81
|
+
self.vu = Constraint(name='vu',
|
82
|
+
info='Voltage upper limit',
|
83
|
+
e_str='vsq - vmax**2',
|
84
|
+
is_eq=False,)
|
85
|
+
self.vl = Constraint(name='vl',
|
86
|
+
info='Voltage lower limit',
|
87
|
+
e_str='-vsq + vmin**2',
|
88
|
+
is_eq=False,)
|
89
|
+
# --- line ---
|
90
|
+
self.qlf = Var(info='line reactive power',
|
91
|
+
name='qlf', tex_name=r'q_{lf}',
|
92
|
+
unit='p.u.', model='Line',)
|
93
|
+
self.lvd = Constraint(info='line voltage drop',
|
94
|
+
name='lvd', is_eq=True,
|
95
|
+
e_str='CftT@vsq - (r * plf + x * qlf)',)
|
96
|
+
# --- power balance ---
|
97
|
+
# NOTE: following Eqn seems to be wrong, double check
|
98
|
+
# g_Q(\Theta, V, Q_g) = B_{bus}V\Theta + Q_{bus,shift} + Q_d + B_{sh} - C_gQ_g = 0
|
99
|
+
self.qb = Constraint(info='reactive power balance',
|
100
|
+
name='qb', is_eq=True,
|
101
|
+
e_str='sum(qd) - sum(qg)',)
|
102
|
+
|
103
|
+
# --- objective ---
|
104
|
+
# NOTE: no need to revise objective function
|
105
|
+
|
106
|
+
def _post_solve(self):
|
107
|
+
self.v.optz.value = np.sqrt(self.vsq.optz.value)
|
108
|
+
return super()._post_solve()
|
109
|
+
|
110
|
+
|
111
|
+
class DOPFVIS(DOPF):
|
112
|
+
"""
|
113
|
+
Linearzied distribution OPF with variables for virtual inertia and damping from from REGCV1,
|
114
|
+
where power loss are ignored.
|
115
|
+
|
116
|
+
UNDER DEVELOPMENT!
|
117
|
+
|
118
|
+
Reference:
|
119
|
+
|
120
|
+
[1] L. Bai, J. Wang, C. Wang, C. Chen, and F. Li, “Distribution Locational Marginal Pricing (DLMP)
|
121
|
+
for Congestion Management and Voltage Support,” IEEE Trans. Power Syst., vol. 33, no. 4,
|
122
|
+
pp. 4061–4073, Jul. 2018, doi: 10.1109/TPWRS.2017.2767632.
|
123
|
+
"""
|
124
|
+
|
125
|
+
def __init__(self, system, config):
|
126
|
+
DOPF.__init__(self, system, config)
|
127
|
+
|
128
|
+
# --- params ---
|
129
|
+
self.cm = RParam(info='Virtual inertia cost',
|
130
|
+
name='cm', src='cm',
|
131
|
+
tex_name=r'c_{m}', unit=r'$/s',
|
132
|
+
model='VSGCost',
|
133
|
+
indexer='reg', imodel='VSG')
|
134
|
+
self.cd = RParam(info='Virtual damping cost',
|
135
|
+
name='cd', src='cd',
|
136
|
+
tex_name=r'c_{d}', unit=r'$/(p.u.)',
|
137
|
+
model='VSGCost',
|
138
|
+
indexer='reg', imodel='VSG',)
|
139
|
+
# --- vars ---
|
140
|
+
self.M = Var(info='Emulated startup time constant (M=2H) from REGCV1',
|
141
|
+
name='M', tex_name=r'M', unit='s',
|
142
|
+
model='VSG',)
|
143
|
+
self.D = Var(info='Emulated damping coefficient from REGCV1',
|
144
|
+
name='D', tex_name=r'D', unit='p.u.',
|
145
|
+
model='VSG',)
|
146
|
+
obj = 'sum(c2 * pg**2 + c1 * pg + ug * c0 + cm * M + cd * D)'
|
147
|
+
self.obj = Objective(name='tc',
|
148
|
+
info='total cost', unit='$',
|
149
|
+
e_str=obj,
|
150
|
+
sense='min',)
|
ams/routines/ed.py
ADDED
@@ -0,0 +1,312 @@
|
|
1
|
+
"""
|
2
|
+
Economic dispatch routines.
|
3
|
+
"""
|
4
|
+
import logging
|
5
|
+
import numpy as np
|
6
|
+
|
7
|
+
from ams.core.param import RParam
|
8
|
+
from ams.core.service import (NumOpDual, NumHstack,
|
9
|
+
RampSub, NumOp, LoadScale)
|
10
|
+
|
11
|
+
from ams.routines.rted import RTED, DGBase, ESD1Base
|
12
|
+
|
13
|
+
from ams.opt import Var, Constraint
|
14
|
+
|
15
|
+
logger = logging.getLogger(__name__)
|
16
|
+
|
17
|
+
|
18
|
+
class SRBase:
|
19
|
+
"""
|
20
|
+
Base class for spinning reserve.
|
21
|
+
"""
|
22
|
+
|
23
|
+
def __init__(self) -> None:
|
24
|
+
self.dsrp = RParam(info='spinning reserve requirement in percentage',
|
25
|
+
name='dsr', tex_name=r'd_{sr}',
|
26
|
+
model='SR', src='demand',
|
27
|
+
unit='%',)
|
28
|
+
self.csr = RParam(info='cost for spinning reserve',
|
29
|
+
name='csr', tex_name=r'c_{sr}',
|
30
|
+
model='SRCost', src='csr',
|
31
|
+
unit=r'$/(p.u.*h)',
|
32
|
+
indexer='gen', imodel='StaticGen',)
|
33
|
+
|
34
|
+
self.prs = Var(name='prs', tex_name=r'p_{r,s}',
|
35
|
+
info='spinning reserve', unit='p.u.',
|
36
|
+
model='StaticGen', nonneg=True,)
|
37
|
+
|
38
|
+
self.dsrpz = NumOpDual(u=self.pdz, u2=self.dsrp, fun=np.multiply,
|
39
|
+
name='dsrpz', tex_name=r'd_{s,r, p, z}',
|
40
|
+
info='zonal spinning reserve requirement in percentage',)
|
41
|
+
self.dsr = NumOpDual(u=self.dsrpz, u2=self.sd, fun=np.multiply,
|
42
|
+
rfun=np.transpose,
|
43
|
+
name='dsr', tex_name=r'd_{s,r,z}',
|
44
|
+
info='zonal spinning reserve requirement',)
|
45
|
+
|
46
|
+
# NOTE: define e_str in the scheduling model
|
47
|
+
self.prsb = Constraint(info='spinning reserve balance',
|
48
|
+
name='prsb', is_eq=True,)
|
49
|
+
self.rsr = Constraint(info='spinning reserve requirement',
|
50
|
+
name='rsr', is_eq=False,)
|
51
|
+
|
52
|
+
|
53
|
+
class MPBase:
|
54
|
+
"""
|
55
|
+
Base class for multi-period scheduling.
|
56
|
+
"""
|
57
|
+
|
58
|
+
def __init__(self) -> None:
|
59
|
+
# NOTE: Setting `ED.scale.owner` to `Horizon` will cause an error when calling `ED.scale.v`.
|
60
|
+
# This is because `Horizon` is a group that only contains the model `TimeSlot`.
|
61
|
+
# The `get` method of `Horizon` calls `andes.models.group.GroupBase.get` and results in an error.
|
62
|
+
self.sd = RParam(info='zonal load factor for ED',
|
63
|
+
name='sd', tex_name=r's_{d}',
|
64
|
+
src='sd', model='EDTSlot')
|
65
|
+
|
66
|
+
# NOTE: update timeslot.model in dispatch model if necessary
|
67
|
+
self.timeslot = RParam(info='Time slot for multi-period ED',
|
68
|
+
name='timeslot', tex_name=r't_{s,idx}',
|
69
|
+
src='idx', model='EDTSlot',
|
70
|
+
no_parse=True)
|
71
|
+
|
72
|
+
self.tlv = NumOp(u=self.timeslot, fun=np.ones_like,
|
73
|
+
args=dict(dtype=float),
|
74
|
+
expand_dims=0,
|
75
|
+
name='tlv', tex_name=r'1_{tl}',
|
76
|
+
info='time length vector',
|
77
|
+
no_parse=True)
|
78
|
+
|
79
|
+
self.pds = LoadScale(u=self.pd, sd=self.sd,
|
80
|
+
name='pds', tex_name=r'p_{d,s}',
|
81
|
+
info='Scaled load',)
|
82
|
+
|
83
|
+
self.R30 = RParam(info='30-min ramp rate',
|
84
|
+
name='R30', tex_name=r'R_{30}',
|
85
|
+
src='R30', unit='p.u./h',
|
86
|
+
model='StaticGen', no_parse=True,)
|
87
|
+
self.Mr = RampSub(u=self.pg, name='Mr', tex_name=r'M_{r}',
|
88
|
+
info='Subtraction matrix for ramping',
|
89
|
+
no_parse=True, sparse=True,)
|
90
|
+
self.RR30 = NumHstack(u=self.R30, ref=self.Mr,
|
91
|
+
name='RR30', tex_name=r'R_{30,R}',
|
92
|
+
info='Repeated ramp rate', no_parse=True,)
|
93
|
+
|
94
|
+
items_to_expand = ['ctrl', 'c0', 'pmax', 'pmin', 'pg0', 'rate_a',
|
95
|
+
'Pfinj', 'Pbusinj', 'gsh', 'ul']
|
96
|
+
for item in items_to_expand:
|
97
|
+
self.__dict__[item].expand_dims = 1
|
98
|
+
|
99
|
+
# NOTE: extend pg to 2D matrix: row for gen and col for timeslot
|
100
|
+
self.pg.horizon = self.timeslot
|
101
|
+
self.pg.info = '2D Gen power'
|
102
|
+
self.aBus.horizon = self.timeslot
|
103
|
+
self.aBus.info = '2D Bus angle'
|
104
|
+
self.vBus.horizon = self.timeslot
|
105
|
+
self.vBus.info = '2D Bus voltage'
|
106
|
+
self.pi.horizon = self.timeslot
|
107
|
+
|
108
|
+
|
109
|
+
class ED(RTED, MPBase, SRBase):
|
110
|
+
"""
|
111
|
+
DC-based multi-period economic dispatch (ED).
|
112
|
+
Dispath interval ``config.t`` (:math:`T_{cfg}`) is introduced,
|
113
|
+
1 [Hour] by default.
|
114
|
+
|
115
|
+
ED extends DCOPF as follows:
|
116
|
+
|
117
|
+
- Vars ``pg``, ``pru``, ``prd`` are extended to 2D
|
118
|
+
- 2D Vars ``rgu`` and ``rgd`` are introduced
|
119
|
+
- Param ``ug`` is sourced from ``EDTSlot.ug`` as generator commitment
|
120
|
+
|
121
|
+
Notes
|
122
|
+
-----
|
123
|
+
1. Formulations has been adjusted with interval ``config.t``
|
124
|
+
|
125
|
+
2. The tie-line flow is not implemented in this model.
|
126
|
+
|
127
|
+
3. `EDTSlot.ug` is used instead of `StaticGen.u` for generator commitment.
|
128
|
+
"""
|
129
|
+
|
130
|
+
def __init__(self, system, config):
|
131
|
+
RTED.__init__(self, system, config)
|
132
|
+
MPBase.__init__(self)
|
133
|
+
SRBase.__init__(self)
|
134
|
+
|
135
|
+
self.config.t = 1 # scheduling interval in hour
|
136
|
+
self.config.add_extra("_help",
|
137
|
+
t="time interval in hours",
|
138
|
+
)
|
139
|
+
|
140
|
+
self.info = 'Economic dispatch'
|
141
|
+
self.type = 'DCED'
|
142
|
+
|
143
|
+
self.ug.info = 'unit commitment decisions'
|
144
|
+
self.ug.model = 'EDTSlot'
|
145
|
+
self.ug.src = 'ug'
|
146
|
+
# self.ug.tex_name = r'u_{g}',
|
147
|
+
|
148
|
+
self.dud.expand_dims = 1
|
149
|
+
self.ddd.expand_dims = 1
|
150
|
+
self.amin.expand_dims = 1
|
151
|
+
self.amax.expand_dims = 1
|
152
|
+
|
153
|
+
# --- Data Section ---
|
154
|
+
self.ugt = NumOp(u=self.ug, fun=np.transpose,
|
155
|
+
name='ugt', tex_name=r'u_{g}',
|
156
|
+
info='input ug transpose',
|
157
|
+
no_parse=True)
|
158
|
+
|
159
|
+
# --- Model Section ---
|
160
|
+
# --- gen ---
|
161
|
+
self.ctrle.u2 = self.ugt
|
162
|
+
self.nctrle.u2 = self.ugt
|
163
|
+
pmaxe = 'mul(mul(nctrle, pg0), tlv) + mul(mul(ctrle, tlv), pmax)'
|
164
|
+
self.pmaxe.e_str = pmaxe
|
165
|
+
self.pmaxe.horizon = self.timeslot
|
166
|
+
pmine = 'mul(mul(nctrle, pg0), tlv) + mul(mul(ctrle, tlv), pmin)'
|
167
|
+
self.pmine.e_str = pmine
|
168
|
+
self.pmine.horizon = self.timeslot
|
169
|
+
self.pglb.e_str = '-pg + pmine'
|
170
|
+
self.pgub.e_str = 'pg - pmaxe'
|
171
|
+
|
172
|
+
self.pru.horizon = self.timeslot
|
173
|
+
self.pru.info = '2D RegUp power'
|
174
|
+
self.prd.horizon = self.timeslot
|
175
|
+
self.prd.info = '2D RegDn power'
|
176
|
+
|
177
|
+
self.prs.horizon = self.timeslot
|
178
|
+
self.prsb.e_str = 'mul(ugt, pmax@tlv - pg) - prs'
|
179
|
+
self.rsr.e_str = '-gs@prs + dsr'
|
180
|
+
|
181
|
+
# --- line ---
|
182
|
+
self.plf.horizon = self.timeslot
|
183
|
+
self.plf.info = '2D Line flow'
|
184
|
+
self.plflb.e_str = '-Bf@aBus - Pfinj@tlv - mul(ul, rate_a)@tlv'
|
185
|
+
self.plfub.e_str = 'Bf@aBus + Pfinj@tlv - mul(ul, rate_a)@tlv'
|
186
|
+
self.alflb.e_str = '-CftT@aBus + amin@tlv'
|
187
|
+
self.alfub.e_str = 'CftT@aBus - amax@tlv'
|
188
|
+
|
189
|
+
self.plf.e_str = 'Bf@aBus + Pfinj@tlv'
|
190
|
+
|
191
|
+
# --- power balance ---
|
192
|
+
self.pb.e_str = 'Bbus@aBus + Pbusinj@tlv + Cl@pds + Csh@gsh@tlv - Cg@pg'
|
193
|
+
|
194
|
+
# --- ramping ---
|
195
|
+
self.rbu.e_str = 'gs@mul(ugt, pru) - mul(dud, tlv)'
|
196
|
+
self.rbd.e_str = 'gs@mul(ugt, prd) - mul(ddd, tlv)'
|
197
|
+
|
198
|
+
self.rru.e_str = 'pg + pru - mul(mul(ugt, pmax), tlv)'
|
199
|
+
self.rrd.e_str = '-pg + prd + mul(mul(ugt, pmin), tlv)'
|
200
|
+
|
201
|
+
self.rgu.e_str = 'pg @ Mr - t dot RR30'
|
202
|
+
self.rgd.e_str = '-pg @ Mr - t dot RR30'
|
203
|
+
|
204
|
+
self.rgu0 = Constraint(name='rgu0',
|
205
|
+
info='Initial gen ramping up',
|
206
|
+
e_str='mul(ugt[:, 0], pg[:, 0] - pg0[:, 0] - R30)',
|
207
|
+
is_eq=False,)
|
208
|
+
self.rgd0 = Constraint(name='rgd0',
|
209
|
+
info='Initial gen ramping down',
|
210
|
+
e_str='mul(ugt[:, 0], -pg[:, 0] + pg0[:, 0] - R30)',
|
211
|
+
is_eq=False,)
|
212
|
+
|
213
|
+
# --- objective ---
|
214
|
+
cost = 'sum(t**2 dot c2 @ pg**2)'
|
215
|
+
cost += '+ t dot sum(c1 @ pg + csr @ prs)'
|
216
|
+
cost += '+ sum(mul(ugt, mul(c0, tlv)))'
|
217
|
+
self.obj.e_str = cost
|
218
|
+
|
219
|
+
def dc2ac(self, **kwargs):
|
220
|
+
"""
|
221
|
+
AC conversion ``dc2ac`` is not implemented yet for
|
222
|
+
multi-period scheduling.
|
223
|
+
"""
|
224
|
+
return NotImplementedError
|
225
|
+
|
226
|
+
def unpack(self, **kwargs):
|
227
|
+
"""
|
228
|
+
Multi-period scheduling will not unpack results from
|
229
|
+
solver into devices.
|
230
|
+
|
231
|
+
# TODO: unpack first period results, and allow input
|
232
|
+
# to specify which period to unpack.
|
233
|
+
"""
|
234
|
+
return None
|
235
|
+
|
236
|
+
|
237
|
+
class EDDG(ED, DGBase):
|
238
|
+
"""
|
239
|
+
ED with distributed generation :ref:`DG`.
|
240
|
+
|
241
|
+
Note that EDDG only inlcudes DG output power. If ESD1 is included,
|
242
|
+
EDES should be used instead, otherwise there is no SOC.
|
243
|
+
"""
|
244
|
+
|
245
|
+
def __init__(self, system, config):
|
246
|
+
ED.__init__(self, system, config)
|
247
|
+
DGBase.__init__(self)
|
248
|
+
|
249
|
+
self.info = 'Economic dispatch with distributed generation'
|
250
|
+
self.type = 'DCED'
|
251
|
+
|
252
|
+
# NOTE: extend vars to 2D
|
253
|
+
self.pgdg.horizon = self.timeslot
|
254
|
+
|
255
|
+
|
256
|
+
class ESD1MPBase(ESD1Base):
|
257
|
+
"""
|
258
|
+
Extended base class for energy storage in multi-period scheduling.
|
259
|
+
"""
|
260
|
+
|
261
|
+
def __init__(self):
|
262
|
+
ESD1Base.__init__(self)
|
263
|
+
|
264
|
+
self.Mre = RampSub(u=self.SOC, name='Mre', tex_name=r'M_{r,ES}',
|
265
|
+
info='Subtraction matrix for SOC',
|
266
|
+
no_parse=True, sparse=True,)
|
267
|
+
self.EnR = NumHstack(u=self.En, ref=self.Mre,
|
268
|
+
name='EnR', tex_name=r'E_{n,R}',
|
269
|
+
info='Repeated En as 2D matrix, (ng, ng-1)')
|
270
|
+
self.EtaCR = NumHstack(u=self.EtaC, ref=self.Mre,
|
271
|
+
name='EtaCR', tex_name=r'\eta_{c,R}',
|
272
|
+
info='Repeated Etac as 2D matrix, (ng, ng-1)')
|
273
|
+
self.REtaDR = NumHstack(u=self.REtaD, ref=self.Mre,
|
274
|
+
name='REtaDR', tex_name=r'R_{\eta_d,R}',
|
275
|
+
info='Repeated REtaD as 2D matrix, (ng, ng-1)')
|
276
|
+
SOCb = 'mul(EnR, SOC @ Mre) - t dot mul(EtaCR, zce[:, 1:])'
|
277
|
+
SOCb += ' + t dot mul(REtaDR, zde[:, 1:])'
|
278
|
+
self.SOCb.e_str = SOCb
|
279
|
+
|
280
|
+
SOCb0 = 'mul(En, SOC[:, 0] - SOCinit) - t dot mul(EtaC, zce[:, 0])'
|
281
|
+
SOCb0 += ' + t dot mul(REtaD, zde[:, 0])'
|
282
|
+
self.SOCb0 = Constraint(name='SOCb0', is_eq=True,
|
283
|
+
info='ESD1 SOC initial balance',
|
284
|
+
e_str=SOCb0,)
|
285
|
+
|
286
|
+
self.SOCr = Constraint(name='SOCr', is_eq=True,
|
287
|
+
info='SOC requirement',
|
288
|
+
e_str='SOC[:, -1] - SOCinit',)
|
289
|
+
|
290
|
+
|
291
|
+
class EDES(ED, ESD1MPBase):
|
292
|
+
"""
|
293
|
+
ED with energy storage :ref:`ESD1`.
|
294
|
+
The bilinear term in the formulation is linearized with big-M method.
|
295
|
+
"""
|
296
|
+
|
297
|
+
def __init__(self, system, config):
|
298
|
+
ED.__init__(self, system, config)
|
299
|
+
ESD1MPBase.__init__(self)
|
300
|
+
|
301
|
+
self.info = 'Economic dispatch with energy storage'
|
302
|
+
self.type = 'DCED'
|
303
|
+
|
304
|
+
# NOTE: extend vars to 2D
|
305
|
+
self.pgdg.horizon = self.timeslot
|
306
|
+
self.SOC.horizon = self.timeslot
|
307
|
+
self.pce.horizon = self.timeslot
|
308
|
+
self.pde.horizon = self.timeslot
|
309
|
+
self.uce.horizon = self.timeslot
|
310
|
+
self.ude.horizon = self.timeslot
|
311
|
+
self.zce.horizon = self.timeslot
|
312
|
+
self.zde.horizon = self.timeslot
|