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
@@ -0,0 +1,224 @@
1
+ """
2
+ Symbolic processor class for AMS routines.
3
+
4
+ This module is revised from ``andes.core.symprocessor``.
5
+ """
6
+
7
+ import logging
8
+ from collections import OrderedDict
9
+
10
+ import sympy as sp
11
+
12
+ from andes.utils.misc import elapsed
13
+ from ams.core.matprocessor import MatProcessor
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class SymProcessor:
19
+ """
20
+ Class for symbolic processing in AMS routine.
21
+
22
+ Parameters
23
+ ----------
24
+ parent : ams.routines.base.BaseRoutine
25
+ Routine instance to process.
26
+
27
+ Attributes
28
+ ----------
29
+ sub_map : dict
30
+ Substitution map for symbolic processing.
31
+ tex_map : dict
32
+ Tex substitution map for documentation.
33
+ val_map : dict
34
+ Value substitution map for post-solving value evaluation.
35
+ """
36
+
37
+ def __init__(self, parent):
38
+ self.parent = parent
39
+ self.inputs_dict = OrderedDict()
40
+ self.services_dict = OrderedDict()
41
+ self.config = parent.config
42
+ self.class_name = parent.class_name
43
+ self.tex_names = OrderedDict()
44
+ self.tex_map = OrderedDict()
45
+
46
+ lang = "cp" # TODO: might need to be generalized to other solvers
47
+ # only used for CVXPY
48
+ self.sub_map = OrderedDict([
49
+ (r'\b(\w+)\s*\*\s*(\w+)\b', r'\1 @ \2'),
50
+ (r'\b(\w+)\s+dot\s+(\w+)\b', r'\1 * \2'),
51
+ (r' dot ', r' * '),
52
+ (r'\bsum\b', f'{lang}.sum'),
53
+ (r'\bvar\b', f'{lang}.Variable'),
54
+ (r'\bparam\b', f'{lang}.Parameter'),
55
+ (r'\bconst\b', f'{lang}.Constant'),
56
+ (r'\bproblem\b', f'{lang}.Problem'),
57
+ (r'\bmultiply\b', f'{lang}.multiply'),
58
+ (r'\bmul\b', f'{lang}.multiply'), # alias for multiply
59
+ (r'\bvstack\b', f'{lang}.vstack'),
60
+ (r'\bnorm\b', f'{lang}.norm'),
61
+ (r'\bpos\b', f'{lang}.pos'),
62
+ (r'\bpower\b', f'{lang}.power'),
63
+ (r'\bsign\b', f'{lang}.sign'),
64
+ (r'\bsquare\b', f'{lang}.square'),
65
+ (r'\bquad_over_lin\b', f'{lang}.quad_over_lin'),
66
+ (r'\bdiag\b', f'{lang}.diag'),
67
+ (r'\bquad_form\b', f'{lang}.quad_form'),
68
+ (r'\bsum_squares\b', f'{lang}.sum_squares'),
69
+ ])
70
+
71
+ self.tex_map = OrderedDict([
72
+ (r'\*\*(\d+)', '^{\\1}'),
73
+ (r'\b(\w+)\s*\*\s*(\w+)\b', r'\1 \2'),
74
+ (r'\@', r' '),
75
+ (r'dot', r' '),
76
+ (r'sum_squares\((.*?)\)', r"SUM((\1))^2"),
77
+ (r'multiply\(([^,]+), ([^)]+)\)', r'\1 \2'),
78
+ (r'\bnp.linalg.pinv(\d+)', r'\1^{\-1}'),
79
+ (r'\bpos\b', 'F^{+}'),
80
+ (r'mul\((.*?),\s*(.*?)\)', r'\1 \2'),
81
+ (r'\bmul\b\((.*?),\s*(.*?)\)', r'\1 \2'),
82
+ (r'\bsum\b', 'SUM'),
83
+ (r'power\((.*?),\s*(\d+)\)', r'\1^\2'),
84
+ (r'(\w+).dual_variables\[0\]', r'\phi[\1]'),
85
+ ])
86
+
87
+ # mapping dict for evaluating expressions
88
+ self.val_map = OrderedDict([
89
+ (r'(== 0|<= 0)$', ''), # remove the comparison operator
90
+ (r'cp\.(Minimize|Maximize)', r'float'), # remove cp.Minimize/Maximize
91
+ (r'\bcp.\b', 'np.'),
92
+ (r'\bexp\b', 'np.exp'),
93
+ (r'\blog\b', 'np.log'),
94
+ (r'\bconj\b', 'np.conj'),
95
+ ])
96
+
97
+ self.status = {
98
+ 'optimal': 0,
99
+ 'infeasible': 1,
100
+ 'unbounded': 2,
101
+ 'infeasible_inaccurate': 3,
102
+ 'unbounded_inaccurate': 4,
103
+ 'optimal_inaccurate': 5,
104
+ 'solver_error': 6,
105
+ 'time_limit': 7,
106
+ 'interrupted': 8,
107
+ 'unknown': 9,
108
+ 'infeasible_or_unbounded': 1.5,
109
+ 'user_limit': 10,
110
+ }
111
+
112
+ def generate_symbols(self, force_generate=False):
113
+ """
114
+ Generate symbols for all variables.
115
+ """
116
+ logger.debug(f'Entering symbol generation for <{self.parent.class_name}>')
117
+
118
+ if (not force_generate) and self.parent._syms:
119
+ logger.debug(' - Symbols already generated')
120
+ return True
121
+ t, _ = elapsed()
122
+
123
+ # process tex_names defined in routines
124
+ # -----------------------------------------------------------
125
+ for key in self.parent.tex_names.keys():
126
+ self.tex_names[key] = sp.symbols(self.parent.tex_names[key])
127
+
128
+ # Vars
129
+ for vname, var in self.parent.vars.items():
130
+ self.inputs_dict[vname] = sp.symbols(f'{vname}')
131
+ self.sub_map[rf"\b{vname}\b"] = f"self.om.{vname}"
132
+ self.tex_map[rf"\b{vname}\b"] = rf'{var.tex_name}'
133
+ self.val_map[rf"\b{vname}\b"] = f"rtn.{vname}.v"
134
+
135
+ # RParams
136
+ for rpname, rparam in self.parent.rparams.items():
137
+ tmp = sp.symbols(f'{rparam.name}')
138
+ self.inputs_dict[rpname] = tmp
139
+ sub_name = ''
140
+ if isinstance(rparam.owner, MatProcessor):
141
+ # sparse matrices are accessed from MatProcessor
142
+ # otherwise, dense matrices are accessed from Routine
143
+ if rparam.sparse:
144
+ sub_name = f'self.rtn.system.mats.{rpname}._v'
145
+ else:
146
+ sub_name = f'self.rtn.{rpname}.v'
147
+ elif rparam.no_parse:
148
+ sub_name = f'self.rtn.{rpname}.v'
149
+ else:
150
+ sub_name = f'self.om.{rpname}'
151
+ self.sub_map[rf"\b{rpname}\b"] = sub_name
152
+ self.tex_map[rf"\b{rpname}\b"] = f'{rparam.tex_name}'
153
+ if not rparam.no_parse:
154
+ self.val_map[rf"\b{rpname}\b"] = f"rtn.{rpname}.v"
155
+
156
+ # Routine Services
157
+ for sname, service in self.parent.services.items():
158
+ tmp = sp.symbols(f'{service.name}')
159
+ self.services_dict[sname] = tmp
160
+ self.inputs_dict[sname] = tmp
161
+ sub_name = f'self.rtn.{sname}.v' if service.no_parse else f'self.om.{sname}'
162
+ self.sub_map[rf"\b{sname}\b"] = sub_name
163
+ self.tex_map[rf"\b{sname}\b"] = f'{service.tex_name}'
164
+ if not service.no_parse:
165
+ self.val_map[rf"\b{sname}\b"] = f"rtn.{sname}.v"
166
+
167
+ # Expressions
168
+ for ename, expr in self.parent.exprs.items():
169
+ self.inputs_dict[ename] = sp.symbols(f'{ename}')
170
+ self.sub_map[rf"\b{ename}\b"] = f"self.om.{ename}"
171
+ self.val_map[rf"\b{ename}\b"] = f"rtn.{ename}.v"
172
+ self.tex_map[rf"\b{ename}\b"] = f'{expr.tex_name}'
173
+
174
+ # Constraints
175
+ # NOTE: constraints are included in sub_map for ExpressionCalc
176
+ # thus, they don't have the suffix `.v`
177
+ for cname, constraint in self.parent.constrs.items():
178
+ self.sub_map[rf"\b{cname}\b"] = f'self.rtn.{cname}.optz'
179
+
180
+ # store tex names defined in `self.config`
181
+ for key in self.config.as_dict():
182
+ tmp = sp.symbols(key)
183
+ self.sub_map[rf"\b{key}\b"] = f'self.rtn.config.{key}'
184
+ if key not in self.config.tex_names.keys():
185
+ logger.debug(f'No tex name for config.{key}')
186
+ self.tex_map[rf"\b{key}\b"] = key
187
+ else:
188
+ self.tex_map[rf"\b{key}\b"] = self.config.tex_names[key]
189
+ self.inputs_dict[key] = tmp
190
+ if key in self.config.tex_names:
191
+ self.tex_names[tmp] = sp.Symbol(self.config.tex_names[key])
192
+
193
+ # store tex names for pretty printing replacement later
194
+ for var in self.inputs_dict:
195
+ if var in self.parent.__dict__ and self.parent.__dict__[var].tex_name is not None:
196
+ self.tex_names[sp.symbols(var)] = sp.symbols(self.parent.__dict__[var].tex_name)
197
+
198
+ # additional variables by conventions that are defined in ``BaseRoutine``
199
+ self.inputs_dict['sys_f'] = sp.symbols('sys_f')
200
+ self.inputs_dict['sys_mva'] = sp.symbols('sys_mva')
201
+
202
+ self.parent._syms = True
203
+ _, s = elapsed(t)
204
+
205
+ logger.debug(f' - Symbols generated in {s}')
206
+ return self.parent._syms
207
+
208
+ def _check_expr_symbols(self, expr):
209
+ """
210
+ Check if expression contains unknown symbols.
211
+ """
212
+ fs = expr.free_symbols
213
+ for item in fs:
214
+ if item not in self.inputs_dict.values():
215
+ raise ValueError(f'{self.class_name} expression "{expr}" contains unknown symbol "{item}"')
216
+
217
+ fs = sorted(fs, key=lambda s: s.name)
218
+ return fs
219
+
220
+ def generate_pretty_print(self):
221
+ """
222
+ Generate pretty print math formulation.
223
+ """
224
+ raise NotImplementedError
ams/core/var.py ADDED
@@ -0,0 +1,59 @@
1
+ """
2
+ Base class for variables.
3
+ """
4
+
5
+ from typing import Optional
6
+ import logging
7
+
8
+ import numpy as np
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class Algeb:
14
+ """
15
+ Algebraic variable class.
16
+
17
+ This class is simplified from ``andes.core.var.Algeb``.
18
+ """
19
+
20
+ def __init__(self,
21
+ name: Optional[str] = None,
22
+ tex_name: Optional[str] = None,
23
+ info: Optional[str] = None,
24
+ unit: Optional[str] = None,
25
+ ):
26
+ self.name = name
27
+ self.info = info
28
+ self.unit = unit
29
+
30
+ self.tex_name = tex_name if tex_name else name
31
+ self.owner = None # instance of the owner Model
32
+ self.id = None # variable internal index inside a model (assigned in run time)
33
+
34
+ # TODO: set a
35
+ # address into the variable and equation arrays (dae.f/dae.g and dae.x/dae.y)
36
+ self.a: np.ndarray = np.array([], dtype=int)
37
+
38
+ self.v: np.ndarray = np.array([], dtype=float) # variable value array
39
+
40
+ def __repr__(self):
41
+ if self.owner.n == 0:
42
+ span = []
43
+
44
+ elif 1 <= self.owner.n <= 20:
45
+ span = f'a={self.a}, v={self.v}'
46
+
47
+ else:
48
+ span = []
49
+ span.append(self.a[0])
50
+ span.append(self.a[-1])
51
+ span.append(self.a[1] - self.a[0])
52
+ span = ':'.join([str(i) for i in span])
53
+ span = 'a=[' + span + ']'
54
+
55
+ return f'{self.__class__.__name__}: {self.owner.__class__.__name__}.{self.name}, {span}'
56
+
57
+ @property
58
+ def class_name(self):
59
+ return self.__class__.__name__
@@ -0,0 +1,5 @@
1
+ """
2
+ Extension module.
3
+ """
4
+
5
+ from ams.extension import eva # NOQA
ams/extension/eva.py ADDED
@@ -0,0 +1,401 @@
1
+ """
2
+ EV Aggregator module.
3
+
4
+ EVD is the generated datasets, and EVA is the aggregator model.
5
+
6
+ Reference:
7
+ [1] J. Wang et al., "Electric Vehicles Charging Time Constrained Deliverable Provision of Secondary
8
+ Frequency Regulation," in IEEE Transactions on Smart Grid, doi: 10.1109/TSG.2024.3356948.
9
+ [2] M. Wang, Y. Mu, Q. Shi, H. Jia and F. Li, "Electric Vehicle Aggregator Modeling and Control for
10
+ Frequency Regulation Considering Progressive State Recovery," in IEEE Transactions on Smart Grid,
11
+ vol. 11, no. 5, pp. 4176-4189, Sept. 2020, doi: 10.1109/TSG.2020.2981843.
12
+ """
13
+
14
+ import logging
15
+ import itertools
16
+ from collections import OrderedDict
17
+
18
+ import scipy.stats as stats
19
+
20
+ from andes.core import Config
21
+ from andes.core.param import NumParam
22
+ from andes.core.model import ModelData
23
+ from andes.shared import np, pd
24
+ from andes.utils.misc import elapsed
25
+
26
+ from ams.core.model import Model
27
+ from ams.utils.paths import ams_root
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ # NOTE: following definition comes from ref[2], except `tt` that is assumed by ref[1]
33
+ # normal distribution parameters
34
+ ndist = {'soci': {'mu': 0.3, 'var': 0.05, 'lb': 0.2, 'ub': 0.4},
35
+ 'socd': {'mu': 0.8, 'var': 0.03, 'lb': 0.7, 'ub': 0.9},
36
+ 'ts1': {'mu': -6.5, 'var': 3.4, 'lb': 0.0, 'ub': 5.5},
37
+ 'ts2': {'mu': 17.5, 'var': 3.4, 'lb': 5.5, 'ub': 24.0},
38
+ 'tf1': {'mu': 8.9, 'var': 3.4, 'lb': 0.0, 'ub': 20.9},
39
+ 'tf2': {'mu': 32.9, 'var': 3.4, 'lb': 20.9, 'ub': 24.0},
40
+ 'tt': {'mu': 0.5, 'var': 0.02, 'lb': 0, 'ub': 1}}
41
+ # uniform distribution parameters
42
+ udist = {'Pc': {'lb': 5.0, 'ub': 7.0},
43
+ 'Pd': {'lb': 5.0, 'ub': 7.0},
44
+ 'nc': {'lb': 0.88, 'ub': 0.95},
45
+ 'nd': {'lb': 0.88, 'ub': 0.95},
46
+ 'Q': {'lb': 20.0, 'ub': 30.0}}
47
+
48
+
49
+ class EVD(ModelData, Model):
50
+ """
51
+ In the EVD, each single EV is recorded as a device with its own parameters.
52
+ The parameters are generated from given statistical distributions.
53
+ """
54
+
55
+ def __init__(self, N=10000, Ns=20, Tagc=4, SOCf=0.2, r=0.5,
56
+ t=18, seed=None, name='EVA', A_csv=None):
57
+ """
58
+ Initialize the EV aggregation model.
59
+
60
+ Parameters
61
+ ----------
62
+ N : int, optional
63
+ Number of related EVs, default is 10000.
64
+ Ns : int, optional
65
+ Number of SOC intervals, default is 20.
66
+ Tagc : int, optional
67
+ AGC time intervals in seconds, default is 4.
68
+ SOCf : float, optional
69
+ Force charge SOC level between 0 and 1, default is 0.2.
70
+ r : float, optional
71
+ Ratio of time range 1 to time range 2 between 0 and 1, default is 0.5.
72
+ seed : int or None, optional
73
+ Seed for random number generator, default is None.
74
+ t : int, optional
75
+ Current time in 24 hours, default is 18.
76
+ name : str, optional
77
+ Name of the EVA, default is 'EVA'.
78
+ A_csv : str, optional
79
+ Path to the CSV file containing the state space matrix A, default is None.
80
+ """
81
+ # inherit attributes and methods from ANDES `ModelData` and AMS `Model`
82
+ ModelData.__init__(self)
83
+ Model.__init__(self, system=None, config=None)
84
+
85
+ self.evdname = name
86
+
87
+ # internal flags
88
+ self.is_setup = False # if EVA has been setup
89
+
90
+ self.t = np.array(t, dtype=float) # time in 24 hours
91
+ self.eva = None # EV Aggregator
92
+ self.A_csv = A_csv # path to the A matrix
93
+
94
+ # manually set config as EVA is not processed by the system
95
+ self.config = Config(self.__class__.__name__)
96
+ self.config.add(OrderedDict((('n', int(N)),
97
+ ('ns', Ns),
98
+ ('tagc', Tagc),
99
+ ('socf', SOCf),
100
+ ('r', r),
101
+ ('socl', 0),
102
+ ('socu', 1),
103
+ ('tf', self.t),
104
+ ('prumax', 0),
105
+ ('prdmax', 0),
106
+ ('seed', seed),
107
+ )))
108
+ self.config.add_extra("_help",
109
+ n="Number of related EVs",
110
+ ns="SOC intervals",
111
+ tagc="AGC time intervals in seconds",
112
+ socf="Force charge SOC level",
113
+ r="ratio of time range 1 to time range 2",
114
+ socl="lowest SOC limit",
115
+ socu="highest SOC limit",
116
+ tf="EVA running end time in 24 hours",
117
+ prumax="maximum power of regulation up, in MW",
118
+ prdmax="maximum power of regulation down, in MW",
119
+ seed='seed (or None) for random number generator',
120
+ )
121
+ self.config.add_extra("_tex",
122
+ n='N_{ev}',
123
+ ns='N_s',
124
+ tagc='T_{agc}',
125
+ socf='SOC_f',
126
+ r='r',
127
+ socl='SOC_{l}',
128
+ socu='SOC_{u}',
129
+ tf='T_f',
130
+ prumax='P_{ru,max}',
131
+ prdmax='P_{rd,max}',
132
+ seed='seed',
133
+ )
134
+ self.config.add_extra("_alt",
135
+ n='int',
136
+ ns="int",
137
+ tagc="float",
138
+ socf="float",
139
+ r="float",
140
+ socl="float",
141
+ socu="float",
142
+ tf="float",
143
+ prumax="float",
144
+ prdmax="float",
145
+ seed='int or None',
146
+ )
147
+
148
+ unit = self.config.socu / self.config.ns
149
+ self.soc_intv = OrderedDict({
150
+ i: (np.around(i * unit, 2), np.around((i + 1) * unit, 2))
151
+ for i in range(self.config.ns)
152
+ })
153
+
154
+ # NOTE: the parameters and variables are declared here and populated in `setup()`
155
+ # param `idx`, `name`, and `u` are already included in `ModelData`
156
+ # variables here are actually declared as parameters for memory saving
157
+ # because ams.core.var.Var has more overhead
158
+
159
+ # --- parameters ---
160
+ self.namax = NumParam(default=0,
161
+ info='maximum number of action')
162
+ self.ts = NumParam(default=0, vrange=(0, 24),
163
+ info='arrive time, in 24 hours')
164
+ self.tf = NumParam(default=0, vrange=(0, 24),
165
+ info='departure time, in 24 hours')
166
+ self.tt = NumParam(default=0,
167
+ info='Tolerance of increased charging time, in hours')
168
+ self.soci = NumParam(default=0,
169
+ info='initial SOC')
170
+ self.socd = NumParam(default=0,
171
+ info='demand SOC')
172
+ self.Pc = NumParam(default=0,
173
+ info='rated charging power, in kW')
174
+ self.Pd = NumParam(default=0,
175
+ info='rated discharging power, in kW')
176
+ self.nc = NumParam(default=0,
177
+ info='charging efficiency',
178
+ vrange=(0, 1))
179
+ self.nd = NumParam(default=0,
180
+ info='discharging efficiency',
181
+ vrange=(0, 1))
182
+ self.Q = NumParam(default=0,
183
+ info='rated capacity, in kWh')
184
+
185
+ # --- variables ---
186
+ self.soc0 = NumParam(default=0,
187
+ info='previous SOC')
188
+ self.u0 = NumParam(default=0,
189
+ info='previous online status')
190
+ self.na0 = NumParam(default=0,
191
+ info='previous action number')
192
+ self.soc = NumParam(default=0,
193
+ info='SOC')
194
+ self.na = NumParam(default=0,
195
+ info='action number')
196
+
197
+ def setup(self, ndist=ndist, udist=udist):
198
+ """
199
+ Setup the EV aggregation model.
200
+
201
+ Parameters
202
+ ----------
203
+ ndist : dict, optional
204
+ Normal distribution parameters, default by built-in `ndist`.
205
+ udist : dict, optional
206
+ Uniform distribution parameters, default by built-in `udist`.
207
+
208
+ Returns
209
+ -------
210
+ is_setup : bool
211
+ If the setup is successful.
212
+ """
213
+ if self.is_setup:
214
+ logger.warning(f'{self.evdname} aggregator has been setup, setup twice is not allowed.')
215
+ return False
216
+
217
+ t0, _ = elapsed()
218
+
219
+ # manually set attributes as EVA is not processed by the system
220
+ self.n = self.config.n
221
+ self.idx.v = ['SEV_' + str(i+1) for i in range(self.config.n)]
222
+ self.name.v = ['SEV ' + str(i+1) for i in range(self.config.n)]
223
+ self.u.v = np.array(self.u.v, dtype=int)
224
+ self.uid = {self.idx.v[i]: i for i in range(self.config.n)}
225
+
226
+ # --- populate parameters' value ---
227
+ # set `soci`, `socd`, `tt`
228
+ self.soci.v = build_truncnorm(ndist['soci']['mu'], ndist['soci']['var'],
229
+ ndist['soci']['lb'], ndist['soci']['ub'],
230
+ self.config.n, self.config.seed)
231
+ self.socd.v = build_truncnorm(ndist['socd']['mu'], ndist['socd']['var'],
232
+ ndist['socd']['lb'], ndist['socd']['ub'],
233
+ self.config.n, self.config.seed)
234
+ self.tt.v = build_truncnorm(ndist['tt']['mu'], ndist['tt']['var'],
235
+ ndist['tt']['lb'], ndist['tt']['ub'],
236
+ self.config.n, self.config.seed)
237
+ # set `ts`, `tf`
238
+ tdf = pd.DataFrame({
239
+ col: build_truncnorm(ndist[col]['mu'], ndist[col]['var'],
240
+ ndist[col]['lb'], ndist[col]['ub'],
241
+ self.config.n, self.config.seed)
242
+ for col in ['ts1', 'ts2', 'tf1', 'tf2']
243
+ })
244
+
245
+ nev_t1 = int(self.config.n * self.config.r) # number of EVs in time range 1
246
+ tp1 = tdf[['ts1', 'tf1']].sample(n=nev_t1, random_state=self.config.seed)
247
+ tp2 = tdf[['ts2', 'tf2']].sample(n=self.config.n-nev_t1, random_state=self.config.seed)
248
+ tp = pd.concat([tp1, tp2], axis=0).reset_index(drop=True).fillna(0)
249
+ tp['ts'] = tp['ts1'] + tp['ts2']
250
+ tp['tf'] = tp['tf1'] + tp['tf2']
251
+ # Swap ts and tf if ts > tf
252
+ check = tp['ts'] > tp['tf']
253
+ tp.loc[check, ['ts', 'tf']] = tp.loc[check, ['tf', 'ts']].values
254
+
255
+ self.ts.v = tp['ts'].values
256
+ self.tf.v = tp['tf'].values
257
+
258
+ # set `Pc`, `Pd`, `nc`, `nd`, `Q`
259
+ # NOTE: here it assumes (1) Pc == Pd, (2) nc == nd given by ref[2]
260
+ if self.config.seed is not None:
261
+ np.random.seed(self.config.seed)
262
+ self.Pc.v = np.random.uniform(udist['Pc']['lb'], udist['Pc']['ub'], self.config.n)
263
+ self.Pd.v = self.Pc.v
264
+ self.nc.v = np.random.uniform(udist['nc']['lb'], udist['nc']['ub'], self.config.n)
265
+ self.nd.v = self.nc.v
266
+ self.Q.v = np.random.uniform(udist['Q']['lb'], udist['Q']['ub'], self.config.n)
267
+
268
+ # --- adjust variables given current time ---
269
+ self.g_u() # update online status
270
+ # adjust SOC considering random behavior
271
+ # NOTE: here we ignore the AGC participation before the current time `self.t`
272
+
273
+ # stayed time for the EVs arrived before t, reset negative time to 0
274
+ tc = np.maximum(self.t - self.ts.v, 0)
275
+ self.soc.v = self.soci.v + tc * self.Pc.v * self.nc.v / self.Q.v # charge them
276
+
277
+ tr = (self.socd.v - self.soci.v) * self.Q.v / self.Pc.v / self.nc.v # time needed to charge to socd
278
+
279
+ # ratio of stay/required time, stay less than required time reset to 1
280
+ kt = np.maximum(tc / tr, 1)
281
+ socp = self.socd.v + np.log(kt) * (1 - self.socd.v) # log scale higher than socd
282
+ mask = kt > 1
283
+ self.soc.v[mask] = socp[mask] # Update soc
284
+
285
+ # clip soc to min/max
286
+ self.soc.v = np.clip(self.soc.v, self.config.socl, self.config.socu)
287
+
288
+ self.soc0.v = self.soc.v.copy()
289
+ self.u0.v = self.u.v.copy()
290
+
291
+ self.evd = EVA(evd=self, A_csv=self.A_csv)
292
+
293
+ self.is_setup = True
294
+
295
+ _, s = elapsed(t0)
296
+ msg = f'{self.evdname} aggregator setup in {s}, and the current time is {self.t} H.\n'
297
+ msg += f'It has {self.config.n} EVs in total and {self.u.v.sum()} EVs online.'
298
+ logger.info(msg)
299
+
300
+ return self.is_setup
301
+
302
+ def g_u(self):
303
+ """
304
+ Update online status of EVs based on current time.
305
+ """
306
+ self.u.v = ((self.ts.v <= self.t) & (self.t <= self.tf.v)).astype(int)
307
+
308
+ return True
309
+
310
+
311
+ class EVA:
312
+ """
313
+ State space modeling based EV aggregation model.
314
+ """
315
+
316
+ def __init__(self, evd, A_csv=None):
317
+ """
318
+ Parameters
319
+ ----------
320
+ EVD : ams.extension.eva.EVD
321
+ EV Aggregator model.
322
+ A_csv : str, optional
323
+ Path to the CSV file containing the state space matrix A, default is None.
324
+ """
325
+ self.parent = evd
326
+
327
+ # states of EV, intersection of charging status and SOC intervals
328
+ # C: charging, I: idle, D: discharging
329
+ states = list(itertools.product(['C', 'I', 'D'], self.parent.soc_intv.keys()))
330
+ self.state = OrderedDict(((''.join(str(i) for i in s), 0.0) for s in states))
331
+
332
+ # NOTE: 3*ns comes from the intersection of charging status and SOC intervals
333
+ ns = self.parent.config.ns
334
+ # NOTE: x, A will be updated in `setup()`
335
+ self.x = np.zeros(3*ns)
336
+
337
+ # A matrix
338
+ default_A_csv = ams_root() + '/extension/Aest.csv'
339
+ if A_csv:
340
+ try:
341
+ self.A = pd.read_csv(A_csv).values
342
+ logger.debug(f'Loaded A matrix from {A_csv}.')
343
+ except FileNotFoundError:
344
+ self.A = pd.read_csv(default_A_csv).values
345
+ logger.debug(f'File {A_csv} not found, using default A matrix.')
346
+ else:
347
+ self.A = pd.read_csv(default_A_csv).values
348
+ logger.debug('No A matrix provided, using default A matrix.')
349
+
350
+ mate = np.eye(ns)
351
+ mat0 = np.zeros((ns, ns))
352
+ self.B = np.vstack((-mate, mate, mat0))
353
+ self.C = np.vstack((mat0, -mate, mate))
354
+
355
+ # SSM variables
356
+ kde = stats.gaussian_kde(self.parent.Pc.v)
357
+ step = 0.01
358
+ Pl_values = np.arange(self.parent.Pc.v.min(), self.parent.Pc.v.max(), step)
359
+ self.Pave = 1e-3 * np.sum([Pl * kde.integrate_box(Pl, Pl + step) for Pl in Pl_values]) # kw to MW
360
+
361
+ # NOTE: D, Da, Db, Dc, Dd will be scaled by Pave later in `setup()`
362
+ vec1 = np.ones((1, ns))
363
+ vec0 = np.zeros((1, ns))
364
+ self.D = self.Pave * np.hstack((-vec1, vec0, vec0))
365
+ self.Da = self.Pave * np.hstack((vec0, vec0, vec1))
366
+ self.Db = self.Pave * np.hstack((vec1, vec1, vec1))
367
+ self.Db[0, ns] = 0 # low charged EVs don't DC
368
+ self.Dc = self.Pave * np.hstack((-vec1, vec0, vec0))
369
+ self.Dd = self.Pave * np.hstack((-vec1, -vec1, -vec1))
370
+ self.Dd[0, 2*ns-1] = 0 # overcharged EVs don't C
371
+
372
+
373
+ def build_truncnorm(mu, var, lb, ub, n, seed):
374
+ """
375
+ Helper function to generate truncated normal distribution
376
+ using scipy.stats.
377
+
378
+ Parameters
379
+ ----------
380
+ mu : float
381
+ Mean of the normal distribution.
382
+ var : float
383
+ Variance of the normal distribution.
384
+ lb : float
385
+ Lower bound of the truncated distribution.
386
+ ub : float
387
+ Upper bound of the truncated distribution.
388
+ n : int
389
+ Number of samples to generate.
390
+ seed : int
391
+ Random seed to use.
392
+
393
+ Returns
394
+ -------
395
+ samples : ndarray
396
+ Generated samples.
397
+ """
398
+ a = (lb - mu) / var
399
+ b = (ub - mu) / var
400
+ distribution = stats.truncnorm(a, b, loc=mu, scale=var)
401
+ return distribution.rvs(n, random_state=seed)