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.
Files changed (191) hide show
  1. ams/__init__.py +4 -11
  2. ams/_version.py +3 -3
  3. ams/cases/5bus/pjm5bus_demo.xlsx +0 -0
  4. ams/cases/5bus/pjm5bus_jumper.xlsx +0 -0
  5. ams/cases/5bus/pjm5bus_uced.json +1062 -0
  6. ams/cases/5bus/pjm5bus_uced.xlsx +0 -0
  7. ams/cases/5bus/pjm5bus_uced_esd1.xlsx +0 -0
  8. ams/cases/5bus/pjm5bus_uced_ev.xlsx +0 -0
  9. ams/cases/ieee123/ieee123.xlsx +0 -0
  10. ams/cases/ieee123/ieee123_regcv1.xlsx +0 -0
  11. ams/cases/ieee14/ieee14.json +1166 -0
  12. ams/cases/ieee14/ieee14.raw +92 -0
  13. ams/cases/ieee14/ieee14_conn.xlsx +0 -0
  14. ams/cases/ieee14/ieee14_uced.xlsx +0 -0
  15. ams/cases/ieee39/ieee39.xlsx +0 -0
  16. ams/cases/ieee39/ieee39_uced.xlsx +0 -0
  17. ams/cases/ieee39/ieee39_uced_esd1.xlsx +0 -0
  18. ams/cases/ieee39/ieee39_uced_pvd1.xlsx +0 -0
  19. ams/cases/ieee39/ieee39_uced_vis.xlsx +0 -0
  20. ams/cases/matpower/benchmark.json +1594 -0
  21. ams/cases/matpower/case118.m +787 -0
  22. ams/cases/matpower/case14.m +129 -0
  23. ams/cases/matpower/case300.m +1315 -0
  24. ams/cases/matpower/case39.m +205 -0
  25. ams/cases/matpower/case5.m +62 -0
  26. ams/cases/matpower/case_ACTIVSg2000.m +9460 -0
  27. ams/cases/npcc/npcc.m +644 -0
  28. ams/cases/npcc/npcc_uced.xlsx +0 -0
  29. ams/cases/pglib/pglib_opf_case39_epri__api.m +243 -0
  30. ams/cases/wecc/wecc.m +714 -0
  31. ams/cases/wecc/wecc_uced.xlsx +0 -0
  32. ams/cli.py +6 -0
  33. ams/core/__init__.py +2 -0
  34. ams/core/documenter.py +652 -0
  35. ams/core/matprocessor.py +782 -0
  36. ams/core/model.py +330 -0
  37. ams/core/param.py +322 -0
  38. ams/core/service.py +918 -0
  39. ams/core/symprocessor.py +224 -0
  40. ams/core/var.py +59 -0
  41. ams/extension/__init__.py +5 -0
  42. ams/extension/eva.py +401 -0
  43. ams/interface.py +1085 -0
  44. ams/io/__init__.py +133 -0
  45. ams/io/json.py +82 -0
  46. ams/io/matpower.py +406 -0
  47. ams/io/psse.py +6 -0
  48. ams/io/pypower.py +103 -0
  49. ams/io/xlsx.py +80 -0
  50. ams/main.py +81 -4
  51. ams/models/__init__.py +24 -0
  52. ams/models/area.py +40 -0
  53. ams/models/bus.py +52 -0
  54. ams/models/cost.py +169 -0
  55. ams/models/distributed/__init__.py +3 -0
  56. ams/models/distributed/esd1.py +71 -0
  57. ams/models/distributed/ev.py +60 -0
  58. ams/models/distributed/pvd1.py +67 -0
  59. ams/models/group.py +231 -0
  60. ams/models/info.py +26 -0
  61. ams/models/line.py +238 -0
  62. ams/models/renewable/__init__.py +5 -0
  63. ams/models/renewable/regc.py +119 -0
  64. ams/models/reserve.py +94 -0
  65. ams/models/shunt.py +14 -0
  66. ams/models/static/__init__.py +2 -0
  67. ams/models/static/gen.py +165 -0
  68. ams/models/static/pq.py +61 -0
  69. ams/models/timeslot.py +69 -0
  70. ams/models/zone.py +49 -0
  71. ams/opt/__init__.py +12 -0
  72. ams/opt/constraint.py +175 -0
  73. ams/opt/exprcalc.py +127 -0
  74. ams/opt/expression.py +188 -0
  75. ams/opt/objective.py +174 -0
  76. ams/opt/omodel.py +432 -0
  77. ams/opt/optzbase.py +192 -0
  78. ams/opt/param.py +156 -0
  79. ams/opt/var.py +233 -0
  80. ams/pypower/__init__.py +8 -0
  81. ams/pypower/_compat.py +9 -0
  82. ams/pypower/core/__init__.py +8 -0
  83. ams/pypower/core/pips.py +894 -0
  84. ams/pypower/core/ppoption.py +244 -0
  85. ams/pypower/core/ppver.py +18 -0
  86. ams/pypower/core/solver.py +2451 -0
  87. ams/pypower/eps.py +6 -0
  88. ams/pypower/idx.py +174 -0
  89. ams/pypower/io.py +604 -0
  90. ams/pypower/make/__init__.py +11 -0
  91. ams/pypower/make/matrices.py +665 -0
  92. ams/pypower/make/pdv.py +506 -0
  93. ams/pypower/routines/__init__.py +7 -0
  94. ams/pypower/routines/cpf.py +513 -0
  95. ams/pypower/routines/cpf_callbacks.py +114 -0
  96. ams/pypower/routines/opf.py +1803 -0
  97. ams/pypower/routines/opffcns.py +1946 -0
  98. ams/pypower/routines/pflow.py +852 -0
  99. ams/pypower/toggle.py +1098 -0
  100. ams/pypower/utils.py +293 -0
  101. ams/report.py +212 -50
  102. ams/routines/__init__.py +23 -0
  103. ams/routines/acopf.py +117 -0
  104. ams/routines/cpf.py +65 -0
  105. ams/routines/dcopf.py +241 -0
  106. ams/routines/dcpf.py +209 -0
  107. ams/routines/dcpf0.py +196 -0
  108. ams/routines/dopf.py +150 -0
  109. ams/routines/ed.py +312 -0
  110. ams/routines/pflow.py +255 -0
  111. ams/routines/pflow0.py +113 -0
  112. ams/routines/routine.py +1033 -0
  113. ams/routines/rted.py +519 -0
  114. ams/routines/type.py +160 -0
  115. ams/routines/uc.py +376 -0
  116. ams/shared.py +63 -9
  117. ams/system.py +61 -22
  118. ams/utils/__init__.py +3 -0
  119. ams/utils/misc.py +77 -0
  120. ams/utils/paths.py +257 -0
  121. docs/Makefile +21 -0
  122. docs/make.bat +35 -0
  123. docs/source/_templates/autosummary/base.rst +5 -0
  124. docs/source/_templates/autosummary/class.rst +35 -0
  125. docs/source/_templates/autosummary/module.rst +65 -0
  126. docs/source/_templates/autosummary/module_toctree.rst +66 -0
  127. docs/source/api.rst +102 -0
  128. docs/source/conf.py +206 -0
  129. docs/source/examples/index.rst +34 -0
  130. docs/source/genmodelref.py +61 -0
  131. docs/source/genroutineref.py +47 -0
  132. docs/source/getting_started/copyright.rst +20 -0
  133. docs/source/getting_started/formats/index.rst +20 -0
  134. docs/source/getting_started/formats/matpower.rst +183 -0
  135. docs/source/getting_started/formats/psse.rst +46 -0
  136. docs/source/getting_started/formats/pypower.rst +223 -0
  137. docs/source/getting_started/formats/xlsx.png +0 -0
  138. docs/source/getting_started/formats/xlsx.rst +23 -0
  139. docs/source/getting_started/index.rst +76 -0
  140. docs/source/getting_started/install.rst +231 -0
  141. docs/source/getting_started/overview.rst +26 -0
  142. docs/source/getting_started/testcase.rst +45 -0
  143. docs/source/getting_started/verification.rst +13 -0
  144. docs/source/images/curent.ico +0 -0
  145. docs/source/images/dcopf_time.png +0 -0
  146. docs/source/images/sponsors/CURENT_Logo_NameOnTrans.png +0 -0
  147. docs/source/images/sponsors/CURENT_Logo_Transparent.png +0 -0
  148. docs/source/images/sponsors/CURENT_Logo_Transparent_Name.png +0 -0
  149. docs/source/images/sponsors/doe.png +0 -0
  150. docs/source/index.rst +108 -0
  151. docs/source/modeling/example.rst +159 -0
  152. docs/source/modeling/index.rst +17 -0
  153. docs/source/modeling/model.rst +210 -0
  154. docs/source/modeling/routine.rst +122 -0
  155. docs/source/modeling/system.rst +51 -0
  156. docs/source/release-notes.rst +398 -0
  157. ltbams-1.0.2.dist-info/METADATA +215 -0
  158. ltbams-1.0.2.dist-info/RECORD +188 -0
  159. {ltbams-0.9.9.dist-info → ltbams-1.0.2.dist-info}/WHEEL +1 -1
  160. ltbams-1.0.2.dist-info/top_level.txt +3 -0
  161. tests/__init__.py +0 -0
  162. tests/test_1st_system.py +33 -0
  163. tests/test_addressing.py +40 -0
  164. tests/test_andes_mats.py +61 -0
  165. tests/test_case.py +266 -0
  166. tests/test_cli.py +34 -0
  167. tests/test_export_csv.py +89 -0
  168. tests/test_group.py +83 -0
  169. tests/test_interface.py +216 -0
  170. tests/test_io.py +32 -0
  171. tests/test_jumper.py +27 -0
  172. tests/test_known_good.py +267 -0
  173. tests/test_matp.py +437 -0
  174. tests/test_model.py +54 -0
  175. tests/test_omodel.py +119 -0
  176. tests/test_paths.py +22 -0
  177. tests/test_report.py +251 -0
  178. tests/test_repr.py +21 -0
  179. tests/test_routine.py +178 -0
  180. tests/test_rtn_dcopf.py +101 -0
  181. tests/test_rtn_dcpf.py +77 -0
  182. tests/test_rtn_ed.py +279 -0
  183. tests/test_rtn_pflow.py +219 -0
  184. tests/test_rtn_rted.py +273 -0
  185. tests/test_rtn_uc.py +248 -0
  186. tests/test_service.py +73 -0
  187. ltbams-0.9.9.dist-info/LICENSE +0 -692
  188. ltbams-0.9.9.dist-info/METADATA +0 -859
  189. ltbams-0.9.9.dist-info/RECORD +0 -14
  190. ltbams-0.9.9.dist-info/top_level.txt +0 -1
  191. {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',))