ltbams 0.9.9__py3-none-any.whl → 1.0.2a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ams/__init__.py +4 -11
- ams/_version.py +3 -3
- ams/cases/5bus/pjm5bus_demo.xlsx +0 -0
- ams/cases/5bus/pjm5bus_jumper.xlsx +0 -0
- ams/cases/5bus/pjm5bus_uced.json +1062 -0
- ams/cases/5bus/pjm5bus_uced.xlsx +0 -0
- ams/cases/5bus/pjm5bus_uced_esd1.xlsx +0 -0
- ams/cases/5bus/pjm5bus_uced_ev.xlsx +0 -0
- ams/cases/ieee123/ieee123.xlsx +0 -0
- ams/cases/ieee123/ieee123_regcv1.xlsx +0 -0
- ams/cases/ieee14/ieee14.json +1166 -0
- ams/cases/ieee14/ieee14.raw +92 -0
- ams/cases/ieee14/ieee14_conn.xlsx +0 -0
- ams/cases/ieee14/ieee14_uced.xlsx +0 -0
- ams/cases/ieee39/ieee39.xlsx +0 -0
- ams/cases/ieee39/ieee39_uced.xlsx +0 -0
- ams/cases/ieee39/ieee39_uced_esd1.xlsx +0 -0
- ams/cases/ieee39/ieee39_uced_pvd1.xlsx +0 -0
- ams/cases/ieee39/ieee39_uced_vis.xlsx +0 -0
- ams/cases/matpower/benchmark.json +1594 -0
- ams/cases/matpower/case118.m +787 -0
- ams/cases/matpower/case14.m +129 -0
- ams/cases/matpower/case300.m +1315 -0
- ams/cases/matpower/case39.m +205 -0
- ams/cases/matpower/case5.m +62 -0
- ams/cases/matpower/case_ACTIVSg2000.m +9460 -0
- ams/cases/npcc/npcc.m +644 -0
- ams/cases/npcc/npcc_uced.xlsx +0 -0
- ams/cases/pglib/pglib_opf_case39_epri__api.m +243 -0
- ams/cases/wecc/wecc.m +714 -0
- ams/cases/wecc/wecc_uced.xlsx +0 -0
- ams/cli.py +6 -0
- ams/core/__init__.py +2 -0
- ams/core/documenter.py +652 -0
- ams/core/matprocessor.py +782 -0
- ams/core/model.py +330 -0
- ams/core/param.py +322 -0
- ams/core/service.py +918 -0
- ams/core/symprocessor.py +224 -0
- ams/core/var.py +59 -0
- ams/extension/__init__.py +5 -0
- ams/extension/eva.py +401 -0
- ams/interface.py +1085 -0
- ams/io/__init__.py +133 -0
- ams/io/json.py +82 -0
- ams/io/matpower.py +406 -0
- ams/io/psse.py +6 -0
- ams/io/pypower.py +103 -0
- ams/io/xlsx.py +80 -0
- ams/main.py +81 -4
- ams/models/__init__.py +24 -0
- ams/models/area.py +40 -0
- ams/models/bus.py +52 -0
- ams/models/cost.py +169 -0
- ams/models/distributed/__init__.py +3 -0
- ams/models/distributed/esd1.py +71 -0
- ams/models/distributed/ev.py +60 -0
- ams/models/distributed/pvd1.py +67 -0
- ams/models/group.py +231 -0
- ams/models/info.py +26 -0
- ams/models/line.py +238 -0
- ams/models/renewable/__init__.py +5 -0
- ams/models/renewable/regc.py +119 -0
- ams/models/reserve.py +94 -0
- ams/models/shunt.py +14 -0
- ams/models/static/__init__.py +2 -0
- ams/models/static/gen.py +165 -0
- ams/models/static/pq.py +61 -0
- ams/models/timeslot.py +69 -0
- ams/models/zone.py +49 -0
- ams/opt/__init__.py +12 -0
- ams/opt/constraint.py +175 -0
- ams/opt/exprcalc.py +127 -0
- ams/opt/expression.py +188 -0
- ams/opt/objective.py +174 -0
- ams/opt/omodel.py +432 -0
- ams/opt/optzbase.py +192 -0
- ams/opt/param.py +156 -0
- ams/opt/var.py +233 -0
- ams/pypower/__init__.py +8 -0
- ams/pypower/_compat.py +9 -0
- ams/pypower/core/__init__.py +8 -0
- ams/pypower/core/pips.py +894 -0
- ams/pypower/core/ppoption.py +244 -0
- ams/pypower/core/ppver.py +18 -0
- ams/pypower/core/solver.py +2451 -0
- ams/pypower/eps.py +6 -0
- ams/pypower/idx.py +174 -0
- ams/pypower/io.py +604 -0
- ams/pypower/make/__init__.py +11 -0
- ams/pypower/make/matrices.py +665 -0
- ams/pypower/make/pdv.py +506 -0
- ams/pypower/routines/__init__.py +7 -0
- ams/pypower/routines/cpf.py +513 -0
- ams/pypower/routines/cpf_callbacks.py +114 -0
- ams/pypower/routines/opf.py +1803 -0
- ams/pypower/routines/opffcns.py +1946 -0
- ams/pypower/routines/pflow.py +852 -0
- ams/pypower/toggle.py +1098 -0
- ams/pypower/utils.py +293 -0
- ams/report.py +212 -50
- ams/routines/__init__.py +23 -0
- ams/routines/acopf.py +117 -0
- ams/routines/cpf.py +65 -0
- ams/routines/dcopf.py +241 -0
- ams/routines/dcpf.py +209 -0
- ams/routines/dcpf0.py +196 -0
- ams/routines/dopf.py +150 -0
- ams/routines/ed.py +312 -0
- ams/routines/pflow.py +255 -0
- ams/routines/pflow0.py +113 -0
- ams/routines/routine.py +1033 -0
- ams/routines/rted.py +519 -0
- ams/routines/type.py +160 -0
- ams/routines/uc.py +376 -0
- ams/shared.py +63 -9
- ams/system.py +61 -22
- ams/utils/__init__.py +3 -0
- ams/utils/misc.py +77 -0
- ams/utils/paths.py +257 -0
- docs/Makefile +21 -0
- docs/make.bat +35 -0
- docs/source/_templates/autosummary/base.rst +5 -0
- docs/source/_templates/autosummary/class.rst +35 -0
- docs/source/_templates/autosummary/module.rst +65 -0
- docs/source/_templates/autosummary/module_toctree.rst +66 -0
- docs/source/api.rst +102 -0
- docs/source/conf.py +203 -0
- docs/source/examples/index.rst +34 -0
- docs/source/genmodelref.py +61 -0
- docs/source/genroutineref.py +47 -0
- docs/source/getting_started/copyright.rst +20 -0
- docs/source/getting_started/formats/index.rst +20 -0
- docs/source/getting_started/formats/matpower.rst +183 -0
- docs/source/getting_started/formats/psse.rst +46 -0
- docs/source/getting_started/formats/pypower.rst +223 -0
- docs/source/getting_started/formats/xlsx.png +0 -0
- docs/source/getting_started/formats/xlsx.rst +23 -0
- docs/source/getting_started/index.rst +76 -0
- docs/source/getting_started/install.rst +234 -0
- docs/source/getting_started/overview.rst +26 -0
- docs/source/getting_started/testcase.rst +45 -0
- docs/source/getting_started/verification.rst +13 -0
- docs/source/images/curent.ico +0 -0
- docs/source/images/dcopf_time.png +0 -0
- docs/source/images/sponsors/CURENT_Logo_NameOnTrans.png +0 -0
- docs/source/images/sponsors/CURENT_Logo_Transparent.png +0 -0
- docs/source/images/sponsors/CURENT_Logo_Transparent_Name.png +0 -0
- docs/source/images/sponsors/doe.png +0 -0
- docs/source/index.rst +108 -0
- docs/source/modeling/example.rst +159 -0
- docs/source/modeling/index.rst +17 -0
- docs/source/modeling/model.rst +210 -0
- docs/source/modeling/routine.rst +122 -0
- docs/source/modeling/system.rst +51 -0
- docs/source/release-notes.rst +398 -0
- ltbams-1.0.2a1.dist-info/METADATA +210 -0
- ltbams-1.0.2a1.dist-info/RECORD +188 -0
- {ltbams-0.9.9.dist-info → ltbams-1.0.2a1.dist-info}/WHEEL +1 -1
- ltbams-1.0.2a1.dist-info/top_level.txt +3 -0
- tests/__init__.py +0 -0
- tests/test_1st_system.py +33 -0
- tests/test_addressing.py +40 -0
- tests/test_andes_mats.py +61 -0
- tests/test_case.py +266 -0
- tests/test_cli.py +34 -0
- tests/test_export_csv.py +89 -0
- tests/test_group.py +83 -0
- tests/test_interface.py +216 -0
- tests/test_io.py +32 -0
- tests/test_jumper.py +27 -0
- tests/test_known_good.py +267 -0
- tests/test_matp.py +437 -0
- tests/test_model.py +54 -0
- tests/test_omodel.py +119 -0
- tests/test_paths.py +22 -0
- tests/test_report.py +251 -0
- tests/test_repr.py +21 -0
- tests/test_routine.py +178 -0
- tests/test_rtn_dcopf.py +101 -0
- tests/test_rtn_dcpf.py +77 -0
- tests/test_rtn_ed.py +279 -0
- tests/test_rtn_pflow.py +219 -0
- tests/test_rtn_rted.py +273 -0
- tests/test_rtn_uc.py +248 -0
- tests/test_service.py +73 -0
- ltbams-0.9.9.dist-info/LICENSE +0 -692
- ltbams-0.9.9.dist-info/METADATA +0 -859
- ltbams-0.9.9.dist-info/RECORD +0 -14
- ltbams-0.9.9.dist-info/top_level.txt +0 -1
- {ltbams-0.9.9.dist-info → ltbams-1.0.2a1.dist-info}/entry_points.txt +0 -0
ams/core/symprocessor.py
ADDED
@@ -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__
|
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)
|