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/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