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/rted.py
ADDED
@@ -0,0 +1,519 @@
|
|
1
|
+
"""
|
2
|
+
Real-time economic dispatch.
|
3
|
+
"""
|
4
|
+
import logging
|
5
|
+
from collections import OrderedDict
|
6
|
+
import numpy as np
|
7
|
+
|
8
|
+
from ams.core.param import RParam
|
9
|
+
from ams.core.service import ZonalSum, VarSelect, NumOp, NumOpDual
|
10
|
+
from ams.routines.dcopf import DCOPF
|
11
|
+
|
12
|
+
from ams.opt import Var, Constraint
|
13
|
+
|
14
|
+
logger = logging.getLogger(__name__)
|
15
|
+
|
16
|
+
|
17
|
+
class RTEDBase:
|
18
|
+
"""
|
19
|
+
Base class for real-time economic dispatch (RTED).
|
20
|
+
"""
|
21
|
+
|
22
|
+
def __init__(self):
|
23
|
+
# --- zone ---
|
24
|
+
self.zg = RParam(info='Gen zone',
|
25
|
+
name='zg', tex_name='z_{one,g}',
|
26
|
+
model='StaticGen', src='zone',
|
27
|
+
no_parse=True)
|
28
|
+
self.zd = RParam(info='Load zone',
|
29
|
+
name='zd', tex_name='z_{one,d}',
|
30
|
+
model='StaticLoad', src='zone',
|
31
|
+
no_parse=True)
|
32
|
+
self.gs = ZonalSum(u=self.zg, zone='Zone',
|
33
|
+
name='gs', tex_name=r'S_{g}',
|
34
|
+
info='Sum Gen vars vector in shape of zone',
|
35
|
+
no_parse=True, sparse=True)
|
36
|
+
self.ds = ZonalSum(u=self.zd, zone='Zone',
|
37
|
+
name='ds', tex_name=r'S_{d}',
|
38
|
+
info='Sum pd vector in shape of zone',
|
39
|
+
no_parse=True,)
|
40
|
+
self.pdz = NumOpDual(u=self.ds, u2=self.pd,
|
41
|
+
fun=np.multiply,
|
42
|
+
rfun=np.sum, rargs=dict(axis=1),
|
43
|
+
expand_dims=0,
|
44
|
+
name='pdz', tex_name=r'p_{d,z}',
|
45
|
+
unit='p.u.', info='zonal total load',
|
46
|
+
no_parse=True,)
|
47
|
+
# --- generator ---
|
48
|
+
self.R10 = RParam(info='10-min ramp rate',
|
49
|
+
name='R10', tex_name=r'R_{10}',
|
50
|
+
model='StaticGen', src='R10',
|
51
|
+
unit='p.u./h',)
|
52
|
+
|
53
|
+
|
54
|
+
class SFRBase:
|
55
|
+
"""
|
56
|
+
Base class for SFR used in DCED.
|
57
|
+
"""
|
58
|
+
|
59
|
+
def __init__(self):
|
60
|
+
# --- SFR cost ---
|
61
|
+
self.cru = RParam(info='RegUp reserve coefficient',
|
62
|
+
name='cru', tex_name=r'c_{r,u}',
|
63
|
+
model='SFRCost', src='cru',
|
64
|
+
indexer='gen', imodel='StaticGen',
|
65
|
+
unit=r'$/(p.u.)',)
|
66
|
+
self.crd = RParam(info='RegDown reserve coefficient',
|
67
|
+
name='crd', tex_name=r'c_{r,d}',
|
68
|
+
model='SFRCost', src='crd',
|
69
|
+
indexer='gen', imodel='StaticGen',
|
70
|
+
unit=r'$/(p.u.)',)
|
71
|
+
# --- reserve requirement ---
|
72
|
+
self.du = RParam(info='RegUp reserve requirement in percentage',
|
73
|
+
name='du', tex_name=r'd_{u}',
|
74
|
+
model='SFR', src='du',
|
75
|
+
unit='%', no_parse=True,)
|
76
|
+
self.dd = RParam(info='RegDown reserve requirement in percentage',
|
77
|
+
name='dd', tex_name=r'd_{d}',
|
78
|
+
model='SFR', src='dd',
|
79
|
+
unit='%', no_parse=True,)
|
80
|
+
self.dud = NumOpDual(u=self.pdz, u2=self.du, fun=np.multiply,
|
81
|
+
rfun=np.reshape, rargs=dict(newshape=(-1,)),
|
82
|
+
name='dud', tex_name=r'd_{u, d}',
|
83
|
+
info='zonal RegUp reserve requirement',)
|
84
|
+
self.ddd = NumOpDual(u=self.pdz, u2=self.dd, fun=np.multiply,
|
85
|
+
rfun=np.reshape, rargs=dict(newshape=(-1,)),
|
86
|
+
name='ddd', tex_name=r'd_{d, d}',
|
87
|
+
info='zonal RegDn reserve requirement',)
|
88
|
+
# --- SFR ---
|
89
|
+
self.pru = Var(info='RegUp reserve',
|
90
|
+
unit='p.u.', name='pru', tex_name=r'p_{r,u}',
|
91
|
+
model='StaticGen', nonneg=True,)
|
92
|
+
self.prd = Var(info='RegDn reserve',
|
93
|
+
unit='p.u.', name='prd', tex_name=r'p_{r,d}',
|
94
|
+
model='StaticGen', nonneg=True,)
|
95
|
+
# NOTE: define e_str in scheduling routine
|
96
|
+
self.rbu = Constraint(name='rbu', is_eq=True,
|
97
|
+
info='RegUp reserve balance',)
|
98
|
+
self.rbd = Constraint(name='rbd', is_eq=True,
|
99
|
+
info='RegDn reserve balance',)
|
100
|
+
self.rru = Constraint(name='rru', is_eq=False,
|
101
|
+
info='RegUp reserve source',)
|
102
|
+
self.rrd = Constraint(name='rrd', is_eq=False,
|
103
|
+
info='RegDn reserve source',)
|
104
|
+
self.rgu = Constraint(name='rgu', is_eq=False,
|
105
|
+
info='Gen ramping up',)
|
106
|
+
self.rgd = Constraint(name='rgd', is_eq=False,
|
107
|
+
info='Gen ramping down',)
|
108
|
+
|
109
|
+
|
110
|
+
class RTED(DCOPF, RTEDBase, SFRBase):
|
111
|
+
"""
|
112
|
+
DC-based real-time economic dispatch (RTED).
|
113
|
+
RTED extends DCOPF with:
|
114
|
+
|
115
|
+
- Mapping dicts to interface with ANDES
|
116
|
+
- Function ``dc2ac`` to do the AC conversion
|
117
|
+
- Vars for SFR reserve: ``pru`` and ``prd``
|
118
|
+
- Param for linear SFR cost: ``cru`` and ``crd``
|
119
|
+
- Param for SFR requirement: ``du`` and ``dd``
|
120
|
+
- Param for ramping: start point ``pg0`` and ramping limit ``R10``
|
121
|
+
- Param ``pg0``, which can be retrieved from dynamic simulation results.
|
122
|
+
|
123
|
+
The function ``dc2ac`` sets the ``vBus`` value from solved ACOPF.
|
124
|
+
Without this conversion, dynamic simulation might fail due to the gap between
|
125
|
+
DC-based dispatch results and AC-based dynamic initialization.
|
126
|
+
|
127
|
+
Notes
|
128
|
+
-----
|
129
|
+
1. Formulations has been adjusted with interval ``config.t``, 5/60 [Hour] by default.
|
130
|
+
|
131
|
+
2. The tie-line flow has not been implemented in formulations.
|
132
|
+
"""
|
133
|
+
|
134
|
+
def __init__(self, system, config):
|
135
|
+
DCOPF.__init__(self, system, config)
|
136
|
+
RTEDBase.__init__(self)
|
137
|
+
SFRBase.__init__(self)
|
138
|
+
|
139
|
+
self.config.add(OrderedDict((('t', 5/60),
|
140
|
+
)))
|
141
|
+
self.config.add_extra("_help",
|
142
|
+
t="time interval in hours",
|
143
|
+
)
|
144
|
+
self.config.add_extra("_tex",
|
145
|
+
t='T_{cfg}',
|
146
|
+
)
|
147
|
+
|
148
|
+
self.info = 'Real-time economic dispatch'
|
149
|
+
self.type = 'DCED'
|
150
|
+
|
151
|
+
# --- Mapping Section ---
|
152
|
+
# Add p -> pg0 in from map
|
153
|
+
self.map1.update({
|
154
|
+
'pg0': ('StaticGen', 'p'),
|
155
|
+
})
|
156
|
+
# nothing to do with to map
|
157
|
+
|
158
|
+
# --- Model Section ---
|
159
|
+
# --- SFR ---
|
160
|
+
# RegUp/Dn reserve balance
|
161
|
+
self.rbu.e_str = 'gs @ mul(ug, pru) - dud'
|
162
|
+
self.rbd.e_str = 'gs @ mul(ug, prd) - ddd'
|
163
|
+
# RegUp/Dn reserve source
|
164
|
+
self.rru.e_str = 'mul(ug, (pg + pru)) - mul(ug, pmaxe)'
|
165
|
+
self.rrd.e_str = 'mul(ug, (-pg + prd)) + mul(ug, pmine)'
|
166
|
+
# Gen ramping up/down
|
167
|
+
self.rgu.e_str = 'mul(ug, (pg-pg0-R10))'
|
168
|
+
self.rgd.e_str = 'mul(ug, (-pg+pg0-R10))'
|
169
|
+
|
170
|
+
# --- objective ---
|
171
|
+
self.obj.info = 'total generation and reserve cost'
|
172
|
+
# NOTE: the product involved t should use ``dot``
|
173
|
+
cost = 't**2 dot sum(mul(c2, pg**2)) + sum(ug * c0)'
|
174
|
+
_to_sum = 'c1 @ pg + cru * pru + crd * prd'
|
175
|
+
cost += f'+ t dot sum({_to_sum})'
|
176
|
+
self.obj.e_str = cost
|
177
|
+
|
178
|
+
def dc2ac(self, kloss=1.0, **kwargs):
|
179
|
+
"""
|
180
|
+
Convert the RTED results with ACOPF.
|
181
|
+
|
182
|
+
Parameters
|
183
|
+
----------
|
184
|
+
kloss : float, optional
|
185
|
+
The loss factor for the conversion. Defaults to 1.2.
|
186
|
+
"""
|
187
|
+
exec_time = self.exec_time
|
188
|
+
if self.exec_time == 0 or self.exit_code != 0:
|
189
|
+
logger.warning(f'{self.class_name} is not executed successfully, quit conversion.')
|
190
|
+
return False
|
191
|
+
# set pru and prd into pmin and pmax
|
192
|
+
pr_idx = self.pru.get_all_idxes()
|
193
|
+
pmin0 = self.system.StaticGen.get(src='pmin', attr='v', idx=pr_idx)
|
194
|
+
pmax0 = self.system.StaticGen.get(src='pmax', attr='v', idx=pr_idx)
|
195
|
+
p00 = self.system.StaticGen.get(src='p0', attr='v', idx=pr_idx)
|
196
|
+
|
197
|
+
# --- ACOPF ---
|
198
|
+
# scale up load
|
199
|
+
pq_idx = self.system.StaticLoad.get_all_idxes()
|
200
|
+
pd0 = self.system.StaticLoad.get(src='p0', attr='v', idx=pq_idx).copy()
|
201
|
+
qd0 = self.system.StaticLoad.get(src='q0', attr='v', idx=pq_idx).copy()
|
202
|
+
self.system.StaticLoad.set(src='p0', idx=pq_idx, attr='v', value=pd0 * kloss)
|
203
|
+
self.system.StaticLoad.set(src='q0', idx=pq_idx, attr='v', value=qd0 * kloss)
|
204
|
+
# preserve generator reserve
|
205
|
+
ACOPF = self.system.ACOPF
|
206
|
+
pmin = pmin0 + self.prd.v
|
207
|
+
pmax = pmax0 - self.pru.v
|
208
|
+
self.system.StaticGen.set(src='pmin', idx=pr_idx, attr='v', value=pmin)
|
209
|
+
self.system.StaticGen.set(src='pmax', idx=pr_idx, attr='v', value=pmax)
|
210
|
+
self.system.StaticGen.set(src='p0', idx=pr_idx, attr='v', value=self.pg.v)
|
211
|
+
# run ACOPF
|
212
|
+
ACOPF.run()
|
213
|
+
# scale load back
|
214
|
+
self.system.StaticLoad.set(src='p0', idx=pq_idx, attr='v', value=pd0)
|
215
|
+
self.system.StaticLoad.set(src='q0', idx=pq_idx, attr='v', value=qd0)
|
216
|
+
if not ACOPF.exit_code == 0:
|
217
|
+
logger.warning('<ACOPF> did not converge, conversion failed.')
|
218
|
+
self.vBus.optz.value = np.ones(self.system.Bus.n)
|
219
|
+
self.aBus.optz.value = np.zeros(self.system.Bus.n)
|
220
|
+
return False
|
221
|
+
|
222
|
+
self.pg.optz.value = ACOPF.pg.v
|
223
|
+
self.vBus.optz.value = ACOPF.vBus.v
|
224
|
+
self.aBus.optz.value = ACOPF.aBus.v
|
225
|
+
self.exec_time = exec_time
|
226
|
+
|
227
|
+
# reset pmin, pmax, p0
|
228
|
+
self.system.StaticGen.set(src='pmin', idx=pr_idx, attr='v', value=pmin0)
|
229
|
+
self.system.StaticGen.set(src='pmax', idx=pr_idx, attr='v', value=pmax0)
|
230
|
+
self.system.StaticGen.set(src='p0', idx=pr_idx, attr='v', value=p00)
|
231
|
+
|
232
|
+
# --- set status ---
|
233
|
+
self.system.recent = self
|
234
|
+
self.converted = True
|
235
|
+
logger.warning(f'<{self.class_name}> converted to AC.')
|
236
|
+
return True
|
237
|
+
|
238
|
+
|
239
|
+
class DGBase:
|
240
|
+
"""
|
241
|
+
Base class for DG used in DCED.
|
242
|
+
"""
|
243
|
+
|
244
|
+
def __init__(self):
|
245
|
+
# --- params ---
|
246
|
+
self.gendg = RParam(info='gen of DG',
|
247
|
+
name='gendg', tex_name=r'g_{DG}',
|
248
|
+
model='DG', src='gen',
|
249
|
+
no_parse=True,)
|
250
|
+
info = 'Ratio of DG.pge w.r.t to that of static generator'
|
251
|
+
self.gammapdg = RParam(name='gammapd', tex_name=r'\gamma_{p,DG}',
|
252
|
+
model='DG', src='gammap',
|
253
|
+
no_parse=True, info=info)
|
254
|
+
|
255
|
+
# --- vars ---
|
256
|
+
# TODO: maybe there will be constraints on pgd, maybe upper/lower bound?
|
257
|
+
# TODO: this might requre new device like DGSlot
|
258
|
+
self.pgdg = Var(info='DG output power',
|
259
|
+
unit='p.u.', name='pgdg',
|
260
|
+
tex_name=r'p_{g,DG}',
|
261
|
+
model='DG',)
|
262
|
+
|
263
|
+
# --- constraints ---
|
264
|
+
self.cdg = VarSelect(u=self.pg, indexer='gendg',
|
265
|
+
name='cd', tex_name=r'C_{DG}',
|
266
|
+
info='Select DG power from pg',
|
267
|
+
gamma='gammapdg',
|
268
|
+
no_parse=True, sparse=True,)
|
269
|
+
self.cdgb = Constraint(name='cdgb', is_eq=True,
|
270
|
+
info='Select DG power from pg',
|
271
|
+
e_str='cdg @ pg - pgdg',)
|
272
|
+
|
273
|
+
|
274
|
+
class RTEDDG(RTED, DGBase):
|
275
|
+
"""
|
276
|
+
RTED with distributed generator :ref:`DG`.
|
277
|
+
|
278
|
+
Note that RTEDDG only inlcudes DG output power. If ESD1 is included,
|
279
|
+
RTEDES should be used instead, otherwise there is no SOC.
|
280
|
+
"""
|
281
|
+
|
282
|
+
def __init__(self, system, config):
|
283
|
+
RTED.__init__(self, system, config)
|
284
|
+
DGBase.__init__(self)
|
285
|
+
self.info = 'Real-time economic dispatch with DG'
|
286
|
+
self.type = 'DCED'
|
287
|
+
|
288
|
+
|
289
|
+
class ESD1Base(DGBase):
|
290
|
+
"""
|
291
|
+
Base class for ESD1 used in DCED.
|
292
|
+
"""
|
293
|
+
|
294
|
+
def __init__(self):
|
295
|
+
DGBase.__init__(self)
|
296
|
+
|
297
|
+
# --- params ---
|
298
|
+
self.En = RParam(info='Rated energy capacity',
|
299
|
+
name='En', src='En',
|
300
|
+
tex_name='E_n', unit='MWh',
|
301
|
+
model='ESD1', no_parse=True,)
|
302
|
+
self.SOCmax = RParam(info='Maximum allowed value for SOC in limiter',
|
303
|
+
name='SOCmax', src='SOCmax',
|
304
|
+
tex_name=r'SOC_{max}', unit='%',
|
305
|
+
model='ESD1',)
|
306
|
+
self.SOCmin = RParam(info='Minimum required value for SOC in limiter',
|
307
|
+
name='SOCmin', src='SOCmin',
|
308
|
+
tex_name=r'SOC_{min}', unit='%',
|
309
|
+
model='ESD1',)
|
310
|
+
self.SOCinit = RParam(info='Initial SOC',
|
311
|
+
name='SOCinit', src='SOCinit',
|
312
|
+
tex_name=r'SOC_{init}', unit='%',
|
313
|
+
model='ESD1',)
|
314
|
+
self.EtaC = RParam(info='Efficiency during charging',
|
315
|
+
name='EtaC', src='EtaC',
|
316
|
+
tex_name=r'\eta_c', unit='%',
|
317
|
+
model='ESD1', no_parse=True,)
|
318
|
+
self.EtaD = RParam(info='Efficiency during discharging',
|
319
|
+
name='EtaD', src='EtaD',
|
320
|
+
tex_name=r'\eta_d', unit='%',
|
321
|
+
model='ESD1', no_parse=True,)
|
322
|
+
|
323
|
+
# --- service ---
|
324
|
+
self.REtaD = NumOp(name='REtaD', tex_name=r'\frac{1}{\eta_d}',
|
325
|
+
u=self.EtaD, fun=np.reciprocal,)
|
326
|
+
self.Mb = NumOp(info='10 times of max of pmax as big M',
|
327
|
+
name='Mb', tex_name=r'M_{big}',
|
328
|
+
u=self.pmax, fun=np.max,
|
329
|
+
rfun=np.dot, rargs=dict(b=10),
|
330
|
+
array_out=False,)
|
331
|
+
|
332
|
+
# --- vars ---
|
333
|
+
self.SOC = Var(info='ESD1 State of Charge', unit='%',
|
334
|
+
name='SOC', tex_name=r'SOC',
|
335
|
+
model='ESD1', pos=True,
|
336
|
+
v0=self.SOCinit,)
|
337
|
+
self.SOClb = Constraint(name='SOClb', is_eq=False,
|
338
|
+
info='SOC lower bound',
|
339
|
+
e_str='-SOC + SOCmin',)
|
340
|
+
self.SOCub = Constraint(name='SOCub', is_eq=False,
|
341
|
+
info='SOC upper bound',
|
342
|
+
e_str='SOC - SOCmax',)
|
343
|
+
self.pce = Var(info='ESD1 charging power',
|
344
|
+
unit='p.u.', name='pce',
|
345
|
+
tex_name=r'p_{c,ESD}',
|
346
|
+
model='ESD1', nonneg=True,)
|
347
|
+
self.pde = Var(info='ESD1 discharging power',
|
348
|
+
unit='p.u.', name='pde',
|
349
|
+
tex_name=r'p_{d,ESD}',
|
350
|
+
model='ESD1', nonneg=True,)
|
351
|
+
self.uce = Var(info='ESD1 charging decision',
|
352
|
+
name='uce', tex_name=r'u_{c,ESD}',
|
353
|
+
model='ESD1', boolean=True,)
|
354
|
+
self.ude = Var(info='ESD1 discharging decision',
|
355
|
+
name='ude', tex_name=r'u_{d,ESD}',
|
356
|
+
model='ESD1', boolean=True,)
|
357
|
+
self.zce = Var(name='zce', tex_name=r'z_{c,ESD}',
|
358
|
+
model='ESD1', nonneg=True,)
|
359
|
+
self.zce.info = 'Aux var for charging, '
|
360
|
+
self.zce.info += ':math:`z_{c,ESD}=u_{c,ESD}*p_{c,ESD}`'
|
361
|
+
self.zde = Var(name='zde', tex_name=r'z_{d,ESD}',
|
362
|
+
model='ESD1', nonneg=True,)
|
363
|
+
self.zde.info = 'Aux var for discharging, '
|
364
|
+
self.zde.info += ':math:`z_{d,ESD}=u_{d,ESD}*p_{d,ESD}`'
|
365
|
+
|
366
|
+
# NOTE: to ensure consistency with DG based routiens,
|
367
|
+
# here we select ESD1 power from DG rather than StaticGen
|
368
|
+
self.genesd = RParam(info='gen of ESD1',
|
369
|
+
name='genesd', tex_name=r'g_{ESD}',
|
370
|
+
model='ESD1', src='idx',
|
371
|
+
no_parse=True,)
|
372
|
+
self.ces = VarSelect(u=self.pgdg, indexer='genesd',
|
373
|
+
name='ces', tex_name=r'C_{ESD}',
|
374
|
+
info='Select ESD power from DG',
|
375
|
+
no_parse=True)
|
376
|
+
self.cescb = Constraint(name='cescb', is_eq=True,
|
377
|
+
info='Select pce from DG',
|
378
|
+
e_str='ces @ pgdg - pce',)
|
379
|
+
self.cesdb = Constraint(name='cesdb', is_eq=True,
|
380
|
+
info='Select pde from DG',
|
381
|
+
e_str='ces @ pgdg - pde',)
|
382
|
+
|
383
|
+
# --- constraints ---
|
384
|
+
self.cdb = Constraint(name='cdb', is_eq=True,
|
385
|
+
info='Charging decision bound',
|
386
|
+
e_str='uce + ude - 1',)
|
387
|
+
|
388
|
+
self.zce1 = Constraint(name='zce1', is_eq=False, info='zce bound 1',
|
389
|
+
e_str='-zce + pce',)
|
390
|
+
self.zce2 = Constraint(name='zce2', is_eq=False, info='zce bound 2',
|
391
|
+
e_str='zce - pce - Mb dot (1-uce)',)
|
392
|
+
self.zce3 = Constraint(name='zce3', is_eq=False, info='zce bound 3',
|
393
|
+
e_str='zce - Mb dot uce',)
|
394
|
+
|
395
|
+
self.zde1 = Constraint(name='zde1', is_eq=False, info='zde bound 1',
|
396
|
+
e_str='-zde + pde',)
|
397
|
+
self.zde2 = Constraint(name='zde2', is_eq=False, info='zde bound 2',
|
398
|
+
e_str='zde - pde - Mb dot (1-ude)',)
|
399
|
+
self.zde3 = Constraint(name='zde3', is_eq=False, info='zde bound 3',
|
400
|
+
e_str='zde - Mb dot ude',)
|
401
|
+
|
402
|
+
SOCb = 'mul(En, (SOC - SOCinit)) - t dot mul(EtaC, zce)'
|
403
|
+
SOCb += '+ t dot mul(REtaD, zde)'
|
404
|
+
self.SOCb = Constraint(name='SOCb', is_eq=True,
|
405
|
+
info='ESD1 SOC balance',
|
406
|
+
e_str=SOCb,)
|
407
|
+
|
408
|
+
|
409
|
+
class RTEDES(RTED, ESD1Base):
|
410
|
+
"""
|
411
|
+
RTED with energy storage :ref:`ESD1`.
|
412
|
+
The bilinear term in the formulation is linearized with big-M method.
|
413
|
+
"""
|
414
|
+
|
415
|
+
def __init__(self, system, config):
|
416
|
+
RTED.__init__(self, system, config)
|
417
|
+
ESD1Base.__init__(self)
|
418
|
+
self.info = 'Real-time economic dispatch with energy storage'
|
419
|
+
self.type = 'DCED'
|
420
|
+
|
421
|
+
|
422
|
+
class VISBase:
|
423
|
+
"""
|
424
|
+
Base class for virtual inertia scheduling.
|
425
|
+
"""
|
426
|
+
|
427
|
+
def __init__(self) -> None:
|
428
|
+
# --- Data Section ---
|
429
|
+
self.cm = RParam(info='Virtual inertia cost',
|
430
|
+
name='cm', src='cm',
|
431
|
+
tex_name=r'c_{m}', unit=r'$/s',
|
432
|
+
model='VSGCost',
|
433
|
+
indexer='reg', imodel='VSG')
|
434
|
+
self.cd = RParam(info='Virtual damping cost',
|
435
|
+
name='cd', src='cd',
|
436
|
+
tex_name=r'c_{d}', unit=r'$/(p.u.)',
|
437
|
+
model='VSGCost',
|
438
|
+
indexer='reg', imodel='VSG',)
|
439
|
+
self.zvsg = RParam(info='VSG zone',
|
440
|
+
name='zvsg', tex_name='z_{one,vsg}',
|
441
|
+
model='VSG', src='zone',
|
442
|
+
no_parse=True)
|
443
|
+
self.Mmax = RParam(info='Maximum inertia emulation',
|
444
|
+
name='Mmax', tex_name='M_{max}',
|
445
|
+
model='VSG', src='Mmax',
|
446
|
+
unit='s',)
|
447
|
+
self.Dmax = RParam(info='Maximum damping emulation',
|
448
|
+
name='Dmax', tex_name='D_{max}',
|
449
|
+
model='VSG', src='Dmax',
|
450
|
+
unit='p.u.',)
|
451
|
+
self.dvm = RParam(info='Emulated inertia requirement',
|
452
|
+
name='dvm', tex_name=r'd_{v,m}',
|
453
|
+
unit='s',
|
454
|
+
model='VSGR', src='dvm',)
|
455
|
+
self.dvd = RParam(info='Emulated damping requirement',
|
456
|
+
name='dvd', tex_name=r'd_{v,d}',
|
457
|
+
unit='p.u.',
|
458
|
+
model='VSGR', src='dvd',)
|
459
|
+
|
460
|
+
# --- Model Section ---
|
461
|
+
self.M = Var(info='Emulated startup time constant (M=2H)',
|
462
|
+
name='M', tex_name=r'M', unit='s',
|
463
|
+
model='VSG', src='M',
|
464
|
+
nonneg=True,)
|
465
|
+
self.D = Var(info='Emulated damping coefficient',
|
466
|
+
name='D', tex_name=r'D', unit='p.u.',
|
467
|
+
model='VSG', src='D',
|
468
|
+
nonneg=True,)
|
469
|
+
|
470
|
+
self.gvsg = ZonalSum(u=self.zvsg, zone='Zone',
|
471
|
+
name='gvsg', tex_name=r'S_{g}',
|
472
|
+
info='Sum VSG vars vector in shape of zone',
|
473
|
+
no_parse=True)
|
474
|
+
self.Mub = Constraint(name='Mub', is_eq=False,
|
475
|
+
info='M upper bound',
|
476
|
+
e_str='M - Mmax',)
|
477
|
+
self.Dub = Constraint(name='Dub', is_eq=False,
|
478
|
+
info='D upper bound',
|
479
|
+
e_str='D - Dmax',)
|
480
|
+
self.Mreq = Constraint(name='Mreq', is_eq=True,
|
481
|
+
info='Emulated inertia requirement',
|
482
|
+
e_str='-gvsg@M + dvm',)
|
483
|
+
self.Dreq = Constraint(name='Dreq', is_eq=True,
|
484
|
+
info='Emulated damping requirement',
|
485
|
+
e_str='-gvsg@D + dvd',)
|
486
|
+
|
487
|
+
# NOTE: revise the objective function to include virtual inertia cost
|
488
|
+
|
489
|
+
|
490
|
+
class RTEDVIS(RTED, VISBase):
|
491
|
+
"""
|
492
|
+
RTED with virtual inertia scheduling.
|
493
|
+
|
494
|
+
This class implements real-time economic dispatch with virtual inertia scheduling.
|
495
|
+
Please ensure that the parameters `dvm` and `dvd` are set according to the system base.
|
496
|
+
|
497
|
+
Reference:
|
498
|
+
|
499
|
+
[1] B. She, F. Li, H. Cui, J. Wang, Q. Zhang and R. Bo, "Virtual
|
500
|
+
Inertia Scheduling (VIS) for Real-time Economic Dispatch of
|
501
|
+
IBRs-penetrated Power Systems," in IEEE Transactions on
|
502
|
+
Sustainable Energy, doi: 10.1109/TSTE.2023.3319307.
|
503
|
+
"""
|
504
|
+
|
505
|
+
def __init__(self, system, config):
|
506
|
+
RTED.__init__(self, system, config)
|
507
|
+
VISBase.__init__(self)
|
508
|
+
self.info = 'Real-time economic dispatch with virtual inertia scheduling'
|
509
|
+
self.type = 'DCED'
|
510
|
+
|
511
|
+
# --- objective ---
|
512
|
+
self.obj.info = 'total generation and reserve cost'
|
513
|
+
vsgcost = '+ t dot sum(cm * M + cd * D)'
|
514
|
+
self.obj.e_str += vsgcost
|
515
|
+
|
516
|
+
self.map2.update({
|
517
|
+
'M': ('RenGen', 'M'),
|
518
|
+
'D': ('RenGen', 'D'),
|
519
|
+
})
|
ams/routines/type.py
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
import logging
|
2
|
+
import inspect
|
3
|
+
from collections import OrderedDict
|
4
|
+
|
5
|
+
logger = logging.getLogger(__name__)
|
6
|
+
|
7
|
+
|
8
|
+
class TypeBase:
|
9
|
+
"""
|
10
|
+
Base class for types.
|
11
|
+
"""
|
12
|
+
|
13
|
+
def __init__(self):
|
14
|
+
|
15
|
+
self.common_rparams = []
|
16
|
+
self.common_vars = []
|
17
|
+
self.common_constrs = []
|
18
|
+
|
19
|
+
self.routines = OrderedDict()
|
20
|
+
|
21
|
+
@property
|
22
|
+
def class_name(self):
|
23
|
+
return self.__class__.__name__
|
24
|
+
|
25
|
+
@property
|
26
|
+
def n(self):
|
27
|
+
"""
|
28
|
+
Total number of routines.
|
29
|
+
"""
|
30
|
+
return len(self.routines)
|
31
|
+
|
32
|
+
def __repr__(self):
|
33
|
+
dev_text = 'routine' if self.n == 1 else 'routines'
|
34
|
+
return f'{self.class_name} ({self.n} {dev_text}) at {hex(id(self))}'
|
35
|
+
|
36
|
+
def doc(self, export='plain'):
|
37
|
+
"""
|
38
|
+
Return the documentation of the type in a string.
|
39
|
+
"""
|
40
|
+
out = ''
|
41
|
+
if export == 'rest':
|
42
|
+
out += f'.. _{self.class_name}:\n\n'
|
43
|
+
group_header = '=' * 80 + '\n'
|
44
|
+
else:
|
45
|
+
group_header = ''
|
46
|
+
|
47
|
+
if export == 'rest':
|
48
|
+
out += group_header + f'{self.class_name}\n' + group_header
|
49
|
+
else:
|
50
|
+
out += group_header + f'Type <{self.class_name}>\n' + group_header
|
51
|
+
|
52
|
+
if self.__doc__ is not None:
|
53
|
+
out += inspect.cleandoc(self.__doc__) + '\n\n'
|
54
|
+
|
55
|
+
if len(self.common_rparams):
|
56
|
+
out += 'Common Parameters: ' + ', '.join(self.common_rparams)
|
57
|
+
out += '\n\n'
|
58
|
+
if len(self.common_vars):
|
59
|
+
out += 'Common Vars: ' + ', '.join(self.common_vars)
|
60
|
+
out += '\n\n'
|
61
|
+
if len(self.common_constrs):
|
62
|
+
out += 'Common Constraints: ' + ', '.join(self.common_constrs)
|
63
|
+
out += '\n\n'
|
64
|
+
if len(self.routines):
|
65
|
+
out += 'Available routines:\n'
|
66
|
+
rtn_name_list = list(self.routines.keys())
|
67
|
+
|
68
|
+
if export == 'rest':
|
69
|
+
def add_reference(name_list):
|
70
|
+
return [f'{item}_' for item in name_list]
|
71
|
+
|
72
|
+
rtn_name_list = add_reference(rtn_name_list)
|
73
|
+
|
74
|
+
out += ',\n'.join(rtn_name_list) + '\n'
|
75
|
+
|
76
|
+
return out
|
77
|
+
|
78
|
+
def doc_all(self, export='plain'):
|
79
|
+
"""
|
80
|
+
Return documentation of the type and its routines.
|
81
|
+
|
82
|
+
Parameters
|
83
|
+
----------
|
84
|
+
export : 'plain' or 'rest'
|
85
|
+
Export format, plain-text or RestructuredText
|
86
|
+
|
87
|
+
Returns
|
88
|
+
-------
|
89
|
+
str
|
90
|
+
|
91
|
+
"""
|
92
|
+
out = self.doc(export=export)
|
93
|
+
out += '\n'
|
94
|
+
for instance in self.routines.values():
|
95
|
+
out += instance.doc(export=export)
|
96
|
+
out += '\n'
|
97
|
+
return out
|
98
|
+
|
99
|
+
|
100
|
+
class UndefinedType(TypeBase):
|
101
|
+
"""
|
102
|
+
The undefined type.
|
103
|
+
"""
|
104
|
+
|
105
|
+
def __init__(self):
|
106
|
+
TypeBase.__init__(self)
|
107
|
+
|
108
|
+
|
109
|
+
class PF(TypeBase):
|
110
|
+
"""
|
111
|
+
Type for power flow routines.
|
112
|
+
"""
|
113
|
+
|
114
|
+
def __init__(self):
|
115
|
+
TypeBase.__init__(self)
|
116
|
+
self.common_rparams.extend(('pd',))
|
117
|
+
self.common_vars.extend(('pg',))
|
118
|
+
|
119
|
+
|
120
|
+
class DCED(TypeBase):
|
121
|
+
"""
|
122
|
+
Type for DC-based economic dispatch.
|
123
|
+
"""
|
124
|
+
|
125
|
+
def __init__(self):
|
126
|
+
TypeBase.__init__(self)
|
127
|
+
self.common_rparams.extend(('c2', 'c1', 'c0', 'pmax', 'pmin', 'pd', 'ptdf', 'rate_a',))
|
128
|
+
self.common_vars.extend(('pg',))
|
129
|
+
self.common_constrs.extend(('pb', 'lub', 'llb'))
|
130
|
+
|
131
|
+
|
132
|
+
class DCUC(TypeBase):
|
133
|
+
"""
|
134
|
+
Type for DC-based unit commitment.
|
135
|
+
"""
|
136
|
+
|
137
|
+
def __init__(self):
|
138
|
+
TypeBase.__init__(self)
|
139
|
+
# TODO: add common parameters and variables
|
140
|
+
|
141
|
+
|
142
|
+
class DED(TypeBase):
|
143
|
+
"""
|
144
|
+
Type for Distributional economic dispatch.
|
145
|
+
"""
|
146
|
+
|
147
|
+
def __init__(self):
|
148
|
+
TypeBase.__init__(self)
|
149
|
+
# TODO: add common parameters and variables
|
150
|
+
|
151
|
+
|
152
|
+
class ACED(DCED):
|
153
|
+
"""
|
154
|
+
Type for AC-based economic dispatch.
|
155
|
+
"""
|
156
|
+
|
157
|
+
def __init__(self):
|
158
|
+
DCED.__init__(self)
|
159
|
+
self.common_rparams.extend(('qd',))
|
160
|
+
self.common_vars.extend(('aBus', 'vBus', 'qg',))
|