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/interface.py
ADDED
@@ -0,0 +1,1085 @@
|
|
1
|
+
"""
|
2
|
+
Module for interfacing ANDES
|
3
|
+
"""
|
4
|
+
|
5
|
+
import os
|
6
|
+
import logging
|
7
|
+
from collections import OrderedDict, Counter
|
8
|
+
|
9
|
+
from andes.utils.misc import elapsed
|
10
|
+
from andes.system import System as andes_System
|
11
|
+
|
12
|
+
from ams.utils import create_entry
|
13
|
+
from ams.io import input_formats
|
14
|
+
from ams.shared import nan, pd, np
|
15
|
+
|
16
|
+
logger = logging.getLogger(__name__)
|
17
|
+
|
18
|
+
|
19
|
+
# Models used in ANDES PFlow
|
20
|
+
# FIXME: add DC models, e.g. Node
|
21
|
+
pflow_dict = OrderedDict([
|
22
|
+
('Bus', create_entry('Vn', 'vmax', 'vmin', 'v0', 'a0',
|
23
|
+
'xcoord', 'ycoord', 'area', 'zone',
|
24
|
+
'owner')),
|
25
|
+
('PQ', create_entry('bus', 'Vn', 'p0', 'q0', 'vmax',
|
26
|
+
'vmin', 'owner')),
|
27
|
+
('PV', create_entry('Sn', 'Vn', 'bus', 'busr', 'p0', 'q0',
|
28
|
+
'pmax', 'pmin', 'qmax', 'qmin',
|
29
|
+
'v0', 'vmax', 'vmin', 'ra', 'xs')),
|
30
|
+
('Slack', create_entry('Sn', 'Vn', 'bus', 'busr', 'p0', 'q0',
|
31
|
+
'pmax', 'pmin', 'qmax', 'qmin',
|
32
|
+
'v0', 'vmax', 'vmin', 'ra', 'xs', 'a0')),
|
33
|
+
('Shunt', create_entry('Sn', 'Vn', 'bus', 'g', 'b', 'fn')),
|
34
|
+
('Line', create_entry('bus1', 'bus2', 'Sn', 'fn', 'Vn1', 'Vn2',
|
35
|
+
'r', 'x', 'b', 'g', 'b1', 'g1', 'b2', 'g2',
|
36
|
+
'trans', 'tap', 'phi', 'rate_a', 'rate_b',
|
37
|
+
'rate_c', 'owner', 'xcoord', 'ycoord')),
|
38
|
+
('Area', create_entry()),
|
39
|
+
('Jumper', create_entry('bus1', 'bus2')),
|
40
|
+
])
|
41
|
+
|
42
|
+
# dict for guessing dynamic models given its idx
|
43
|
+
idx_guess = {'rego': 'RenGovernor',
|
44
|
+
'ree': 'RenExciter',
|
45
|
+
'rea': 'RenAerodynamics',
|
46
|
+
'rep': 'RenPitch',
|
47
|
+
'busf': 'BusFreq',
|
48
|
+
'zone': 'Zone',
|
49
|
+
'gen': 'StaticGen',
|
50
|
+
'pq': 'PQ',
|
51
|
+
'vsg': 'VSG',
|
52
|
+
'regc': 'VSG', }
|
53
|
+
|
54
|
+
|
55
|
+
def sync_adsys(amsys, adsys):
|
56
|
+
"""
|
57
|
+
Sync parameters value of PFlow models between AMS and ANDES systems.
|
58
|
+
|
59
|
+
Parameters
|
60
|
+
----------
|
61
|
+
amsys : AMS.system.System
|
62
|
+
The AMS system.
|
63
|
+
adsys : ANDES.system.System
|
64
|
+
The ANDES system.
|
65
|
+
|
66
|
+
Returns
|
67
|
+
-------
|
68
|
+
bool
|
69
|
+
True if successful.
|
70
|
+
"""
|
71
|
+
for mname, params in pflow_dict.items():
|
72
|
+
ad_mdl = adsys.__dict__[mname]
|
73
|
+
am_mdl = amsys.__dict__[mname]
|
74
|
+
idx = am_mdl.idx.v
|
75
|
+
for param in params:
|
76
|
+
if param in ['idx', 'name']:
|
77
|
+
continue
|
78
|
+
# NOTE: when setting list values to DataParam, sometimes run into error
|
79
|
+
try:
|
80
|
+
ad_mdl.set(src=param, attr='v', idx=idx,
|
81
|
+
value=am_mdl.get(src=param, attr='v', idx=idx))
|
82
|
+
except Exception:
|
83
|
+
continue
|
84
|
+
return True
|
85
|
+
|
86
|
+
|
87
|
+
def _to_andes_pflow(system, no_output=False, default_config=True, **kwargs):
|
88
|
+
"""
|
89
|
+
Helper function to convert the AMS system to an ANDES system with only
|
90
|
+
power flow models.
|
91
|
+
"""
|
92
|
+
|
93
|
+
adsys = andes_System(no_outpu=no_output, default_config=default_config, **kwargs)
|
94
|
+
# FIXME: is there a systematic way to do this? Other config might be needed
|
95
|
+
adsys.config.freq = system.config.freq
|
96
|
+
adsys.config.mva = system.config.mva
|
97
|
+
|
98
|
+
for mdl_name, mdl_cols in pflow_dict.items():
|
99
|
+
mdl = getattr(system, mdl_name)
|
100
|
+
mdl.cache.refresh("df_in") # refresh cache
|
101
|
+
for row in mdl.cache.df_in[mdl_cols].to_dict(orient='records'):
|
102
|
+
adsys.add(mdl_name, row)
|
103
|
+
|
104
|
+
sync_adsys(amsys=system, adsys=adsys)
|
105
|
+
|
106
|
+
return adsys
|
107
|
+
|
108
|
+
|
109
|
+
def to_andes(system, addfile=None,
|
110
|
+
setup=False, no_output=False,
|
111
|
+
default_config=True,
|
112
|
+
verify=False, tol=1e-3,
|
113
|
+
**kwargs):
|
114
|
+
"""
|
115
|
+
Convert the AMS system to an ANDES system.
|
116
|
+
|
117
|
+
A preferred dynamic system file to be added has following features:
|
118
|
+
1. The file contains both power flow and dynamic models.
|
119
|
+
2. The file can run in ANDES natively.
|
120
|
+
3. Power flow models are in the same shape as the AMS system.
|
121
|
+
4. Dynamic models, if any, are in the same shape as the AMS system.
|
122
|
+
|
123
|
+
This function is wrapped as the ``System`` class method ``to_andes()``.
|
124
|
+
Using the file conversion ``to_andes()`` will automatically
|
125
|
+
link the AMS system instance to the converted ANDES system instance
|
126
|
+
in the AMS system attribute ``dyn``.
|
127
|
+
|
128
|
+
It should be noted that detailed dynamic simualtion requires extra
|
129
|
+
dynamic models to be added to the ANDES system, which can be passed
|
130
|
+
through the ``addfile`` argument.
|
131
|
+
|
132
|
+
Parameters
|
133
|
+
----------
|
134
|
+
system : System
|
135
|
+
The AMS system to be converted to ANDES format.
|
136
|
+
addfile : str, optional
|
137
|
+
The additional file to be converted to ANDES dynamic mdoels.
|
138
|
+
setup : bool, optional
|
139
|
+
Whether to call `setup()` after the conversion. Default is True.
|
140
|
+
no_output : bool, optional
|
141
|
+
To ANDES system.
|
142
|
+
default_config : bool, optional
|
143
|
+
To ANDES system.
|
144
|
+
verify : bool
|
145
|
+
If True, the converted ANDES system will be verified with the source
|
146
|
+
AMS system using AC power flow.
|
147
|
+
tol : float
|
148
|
+
The tolerance of error.
|
149
|
+
|
150
|
+
Returns
|
151
|
+
-------
|
152
|
+
adsys : andes.system.System
|
153
|
+
The converted ANDES system.
|
154
|
+
|
155
|
+
Examples
|
156
|
+
--------
|
157
|
+
>>> import ams
|
158
|
+
>>> import andes
|
159
|
+
>>> sp = ams.load(ams.get_case('ieee14/ieee14_uced.xlsx'), setup=True)
|
160
|
+
>>> sa = sp.to_andes(addfile=andes.get_case('ieee14/ieee14_full.xlsx'),
|
161
|
+
... setup=False, overwrite=True, no_output=True)
|
162
|
+
|
163
|
+
Notes
|
164
|
+
-----
|
165
|
+
1. Power flow models in the addfile will be skipped and only dynamic models will be used.
|
166
|
+
2. The addfile format is guessed based on the file extension. Currently only ``xlsx`` is supported.
|
167
|
+
3. Index in the addfile is automatically adjusted when necessary.
|
168
|
+
"""
|
169
|
+
t0, _ = elapsed()
|
170
|
+
|
171
|
+
# --- convert power flow models ---
|
172
|
+
adsys = _to_andes_pflow(system, no_output=no_output, default_config=default_config, **kwargs)
|
173
|
+
|
174
|
+
_, s = elapsed(t0)
|
175
|
+
|
176
|
+
# additonal file for dynamic models
|
177
|
+
if addfile:
|
178
|
+
t_add, _ = elapsed()
|
179
|
+
|
180
|
+
# --- parse addfile ---
|
181
|
+
adsys = parse_addfile(adsys=adsys, amsys=system, addfile=addfile)
|
182
|
+
|
183
|
+
_, s_add = elapsed(t_add)
|
184
|
+
logger.info('Addfile parsed in %s.', s_add)
|
185
|
+
|
186
|
+
# fake FileManaer attributes
|
187
|
+
adsys.files = system.files
|
188
|
+
|
189
|
+
logger.info(f'System converted to ANDES in {s}.')
|
190
|
+
|
191
|
+
# finalize
|
192
|
+
system.dyn = Dynamic(amsys=system, adsys=adsys)
|
193
|
+
system.dyn.link_andes(adsys=adsys)
|
194
|
+
|
195
|
+
if setup:
|
196
|
+
adsys.setup()
|
197
|
+
elif verify:
|
198
|
+
logger.warning('PFlow verification is skipped due to no setup.')
|
199
|
+
return adsys
|
200
|
+
if verify:
|
201
|
+
verify_pf(amsys=system, adsys=adsys, tol=tol)
|
202
|
+
return adsys
|
203
|
+
|
204
|
+
|
205
|
+
def parse_addfile(adsys, amsys, addfile):
|
206
|
+
"""
|
207
|
+
Parse the addfile for ANDES dynamic file.
|
208
|
+
|
209
|
+
Parameters
|
210
|
+
----------
|
211
|
+
adsys : andes.system.System
|
212
|
+
The ANDES system instance.
|
213
|
+
amsys : ams.system.System
|
214
|
+
The AMS system instance.
|
215
|
+
addfile : str
|
216
|
+
The additional file to be converted to ANDES dynamic mdoels.
|
217
|
+
|
218
|
+
Returns
|
219
|
+
-------
|
220
|
+
adsys : andes.system.System
|
221
|
+
The ANDES system instance with dynamic models added.
|
222
|
+
"""
|
223
|
+
# guess addfile format
|
224
|
+
add_format = None
|
225
|
+
_, add_ext = os.path.splitext(addfile)
|
226
|
+
for key, val in input_formats.items():
|
227
|
+
if add_ext[1:] in val:
|
228
|
+
add_format = key
|
229
|
+
logger.debug('Addfile format guessed as %s.', key)
|
230
|
+
break
|
231
|
+
|
232
|
+
if key != 'xlsx':
|
233
|
+
logger.error('Addfile format "%s" is not supported yet.', add_format)
|
234
|
+
# FIXME: xlsx input file with dyr addfile result into KeyError: 'Toggle'
|
235
|
+
# add_parser = importlib.import_module('andes.io.' + add_format)
|
236
|
+
# if not add_parser.read_add(system, addfile):
|
237
|
+
# logger.error('Error parsing addfile "%s" with %s parser.', addfile, add_format)
|
238
|
+
return adsys
|
239
|
+
|
240
|
+
# Try parsing the addfile
|
241
|
+
logger.info('Parsing additional file "%s"...', addfile)
|
242
|
+
|
243
|
+
reader = pd.ExcelFile(addfile)
|
244
|
+
|
245
|
+
pflow_mdl = list(pflow_dict.keys())
|
246
|
+
|
247
|
+
pflow_mdls_overlap = []
|
248
|
+
for mdl_name in pflow_dict.keys():
|
249
|
+
if mdl_name in reader.sheet_names:
|
250
|
+
pflow_mdls_overlap.append(mdl_name)
|
251
|
+
|
252
|
+
if len(pflow_mdls_overlap) > 0:
|
253
|
+
msg = 'Following PFlow models in addfile will be overwritten: '
|
254
|
+
msg += ', '.join([f'<{mdl}>' for mdl in pflow_mdls_overlap])
|
255
|
+
logger.warning(msg)
|
256
|
+
|
257
|
+
pflow_mdl_nonempty = [mdl for mdl in pflow_mdl if amsys.models[mdl].n > 0]
|
258
|
+
logger.debug(f"Non-empty PFlow models: {pflow_mdl}")
|
259
|
+
pflow_df_models = pd.read_excel(addfile,
|
260
|
+
sheet_name=pflow_mdl_nonempty,
|
261
|
+
index_col=0,
|
262
|
+
engine='openpyxl',
|
263
|
+
)
|
264
|
+
# drop rows that all nan
|
265
|
+
for name, df in pflow_df_models.items():
|
266
|
+
df.dropna(axis=0, how='all', inplace=True)
|
267
|
+
|
268
|
+
# collect idx_map if difference exists
|
269
|
+
idx_map = OrderedDict([])
|
270
|
+
for name, df in pflow_df_models.items():
|
271
|
+
am_idx = amsys.models[name].idx.v
|
272
|
+
ad_idx = df['idx'].values
|
273
|
+
if len(set(am_idx)) != len(set(ad_idx)):
|
274
|
+
msg = f'<{name}> has different number of rows in addfile.'
|
275
|
+
logger.warning(msg)
|
276
|
+
if set(am_idx) != set(ad_idx):
|
277
|
+
idx_map[name] = dict(zip(ad_idx, am_idx))
|
278
|
+
|
279
|
+
# --- dynamic models to be added ---
|
280
|
+
mdl_to_keep = list(set(reader.sheet_names) - set(pflow_mdl))
|
281
|
+
mdl_to_keep.sort(key=str.lower)
|
282
|
+
df_models = pd.read_excel(addfile,
|
283
|
+
sheet_name=mdl_to_keep,
|
284
|
+
index_col=0,
|
285
|
+
engine='openpyxl',
|
286
|
+
)
|
287
|
+
|
288
|
+
# adjust models index
|
289
|
+
for name, df in df_models.items():
|
290
|
+
try:
|
291
|
+
mdl = adsys.models[name]
|
292
|
+
except KeyError:
|
293
|
+
mdl = adsys.model_aliases[name]
|
294
|
+
if len(mdl.idx_params) == 0: # skip if no idx_params
|
295
|
+
continue
|
296
|
+
for idxn, idxp in mdl.idx_params.items():
|
297
|
+
if idxp.model is None: # make a guess if no model is specified
|
298
|
+
mdl_guess = idxn.capitalize()
|
299
|
+
if mdl_guess not in adsys.models.keys():
|
300
|
+
try:
|
301
|
+
mdl_guess = idx_guess[idxp.name]
|
302
|
+
except KeyError: # set the most frequent string as the model name
|
303
|
+
split_list = []
|
304
|
+
for item in df[idxn].values:
|
305
|
+
if item is None or nan:
|
306
|
+
continue
|
307
|
+
try:
|
308
|
+
split_list.append(item.split('_'))
|
309
|
+
# Flatten the nested list and filter non-numerical strings
|
310
|
+
flattened_list = [item for sublist in split_list for item in sublist
|
311
|
+
if not isinstance(item, int)]
|
312
|
+
# Count the occurrences of non-numerical strings
|
313
|
+
string_counter = Counter(flattened_list)
|
314
|
+
# Find the most common non-numerical string
|
315
|
+
mdl_guess = string_counter.most_common(1)[0][0]
|
316
|
+
except AttributeError:
|
317
|
+
logger.error(f'Failed to parse IdxParam {name}.{idxn}.')
|
318
|
+
continue
|
319
|
+
else:
|
320
|
+
mdl_guess = idxp.model
|
321
|
+
if mdl_guess in adsys.groups.keys():
|
322
|
+
grp_idx = {}
|
323
|
+
for mname, mdl in adsys.groups[mdl_guess].models.items():
|
324
|
+
# add group index to index map
|
325
|
+
if mname in idx_map.keys():
|
326
|
+
grp_idx.update(idx_map[mname])
|
327
|
+
if len(grp_idx) == 0:
|
328
|
+
continue # no index consistency issue, skip
|
329
|
+
idx_map[mdl_guess] = grp_idx
|
330
|
+
if mdl_guess not in idx_map.keys():
|
331
|
+
continue # no index consistency issue, skip
|
332
|
+
else:
|
333
|
+
logger.debug(f'Replace map for {mdl_guess} is {idx_map[mdl_guess]}')
|
334
|
+
df[idxn] = df[idxn].replace(idx_map[mdl_guess])
|
335
|
+
logger.debug(f'Adjust {idxp.class_name} <{name}.{idxp.name}>')
|
336
|
+
|
337
|
+
# NOTE: Group TimedEvent needs special treatment
|
338
|
+
# adjust Toggle and Fault models
|
339
|
+
toggle_df = df_models.get('Toggle') or df_models.get('Toggler')
|
340
|
+
if toggle_df is not None:
|
341
|
+
toggle_df['dev'] = toggle_df.apply(replace_dev, axis=1,
|
342
|
+
mdl='model', dev='dev',
|
343
|
+
idx_map=idx_map)
|
344
|
+
|
345
|
+
alter_df = df_models.get('Alter')
|
346
|
+
if alter_df is not None:
|
347
|
+
alter_df['dev'] = alter_df.apply(replace_dev, axis=1,
|
348
|
+
mdl='model', dev='dev',
|
349
|
+
idx_map=idx_map)
|
350
|
+
|
351
|
+
# adjust Fault model
|
352
|
+
fault_df = df_models.get('Fault')
|
353
|
+
if fault_df is not None:
|
354
|
+
fault_df['bus'] = fault_df.apply(replace_dev, axis=1,
|
355
|
+
mdl='bus', dev='bus',
|
356
|
+
idx_map=idx_map)
|
357
|
+
|
358
|
+
# add dynamic models
|
359
|
+
for name, df in df_models.items():
|
360
|
+
# drop rows that all nan
|
361
|
+
df.replace(['', ' '], nan, inplace=True) # replace empty string with nan
|
362
|
+
df.dropna(axis=0, how='all', inplace=True)
|
363
|
+
# if the dynamic model also exists in AMS, use AMS parameters for overlap
|
364
|
+
if (name in amsys.models.keys()) and amsys.models[name].n > 0:
|
365
|
+
if df.shape[0] != amsys.models[name].n:
|
366
|
+
msg = f'<{name}> has different number of rows in addfile.'
|
367
|
+
logger.warning(msg)
|
368
|
+
am_params = set(amsys.models[name].params.keys())
|
369
|
+
ad_params = set(df.columns)
|
370
|
+
overlap_params = list(am_params.intersection(ad_params))
|
371
|
+
ad_rest_params = list(ad_params - am_params) + ['idx']
|
372
|
+
msg = f'Following <{name}> parameters in addfile are overwriten: '
|
373
|
+
msg += ', '.join(overlap_params)
|
374
|
+
logger.debug(msg)
|
375
|
+
tmp = amsys.models[name].cache.df_in[overlap_params]
|
376
|
+
df = pd.merge(left=tmp, right=df[ad_rest_params],
|
377
|
+
on='idx', how='left')
|
378
|
+
for row in df.to_dict(orient='records'):
|
379
|
+
adsys.add(name, row)
|
380
|
+
|
381
|
+
# --- adjust SynGen Vn with Bus Vn ---
|
382
|
+
# NOTE: RenGen and DG have no Vn, so no need to adjust
|
383
|
+
syg_idx = []
|
384
|
+
for _, syg in adsys.SynGen.models.items():
|
385
|
+
if syg.n > 0:
|
386
|
+
syg_idx += syg.idx.v
|
387
|
+
syg_bus_idx = adsys.SynGen.get(src='bus', attr='v', idx=syg_idx)
|
388
|
+
syg_bus_vn = adsys.Bus.get(src='Vn', idx=syg_bus_idx)
|
389
|
+
adsys.SynGen.set(src='Vn', idx=syg_idx, attr='v', value=syg_bus_vn)
|
390
|
+
|
391
|
+
# --- for debugging ---
|
392
|
+
adsys.df_in = df_models
|
393
|
+
|
394
|
+
return adsys
|
395
|
+
|
396
|
+
|
397
|
+
class Dynamic:
|
398
|
+
"""
|
399
|
+
ANDES interface class.
|
400
|
+
|
401
|
+
Parameters
|
402
|
+
----------
|
403
|
+
amsys : AMS.system.System
|
404
|
+
The AMS system.
|
405
|
+
adsys : ANDES.system.System
|
406
|
+
The ANDES system.
|
407
|
+
|
408
|
+
Attributes
|
409
|
+
----------
|
410
|
+
link : pandas.DataFrame
|
411
|
+
The ANDES system link table.
|
412
|
+
|
413
|
+
Notes
|
414
|
+
-----
|
415
|
+
1. Using the file conversion ``to_andes()`` will automatically
|
416
|
+
link the AMS system to the converted ANDES system in the
|
417
|
+
attribute ``dyn``.
|
418
|
+
|
419
|
+
Examples
|
420
|
+
--------
|
421
|
+
>>> import ams
|
422
|
+
>>> import andes
|
423
|
+
>>> sp = ams.load(ams.get_case('ieee14/ieee14_rted.xlsx'), setup=True)
|
424
|
+
>>> sa = sp.to_andes(setup=True,
|
425
|
+
... addfile=andes.get_case('ieee14/ieee14_wt3.xlsx'),
|
426
|
+
... overwrite=True, keep=False, no_output=True)
|
427
|
+
>>> sp.RTED.run()
|
428
|
+
>>> sp.RTED.dc2ac()
|
429
|
+
>>> sp.dyn.send() # send RTED results to ANDES system
|
430
|
+
>>> sa.PFlow.run()
|
431
|
+
>>> sp.TDS.run()
|
432
|
+
>>> sp.dyn.receive() # receive TDS results from ANDES system
|
433
|
+
"""
|
434
|
+
|
435
|
+
def __init__(self, amsys=None, adsys=None) -> None:
|
436
|
+
self.amsys = amsys # AMS system
|
437
|
+
self.adsys = adsys # ANDES system
|
438
|
+
|
439
|
+
# TODO: add summary table
|
440
|
+
self.link = None # ANDES system link table
|
441
|
+
|
442
|
+
def link_andes(self, adsys):
|
443
|
+
"""
|
444
|
+
Link the ANDES system to the AMS system.
|
445
|
+
|
446
|
+
Parameters
|
447
|
+
----------
|
448
|
+
adsys : ANDES.system.System
|
449
|
+
The ANDES system instance.
|
450
|
+
"""
|
451
|
+
self.adsys = adsys
|
452
|
+
|
453
|
+
self.link = make_link_table(self.adsys)
|
454
|
+
logger.warning(f'AMS system {hex(id(self.amsys))} is linked to the ANDES system {hex(id(adsys))}.')
|
455
|
+
|
456
|
+
@property
|
457
|
+
def is_tds(self):
|
458
|
+
"""
|
459
|
+
Indicator of whether the ANDES system is running a TDS.
|
460
|
+
This property will return ``True`` as long as TDS is initialized.
|
461
|
+
|
462
|
+
Check ``adsys.tds.TDS.init()`` for more details.
|
463
|
+
"""
|
464
|
+
return bool(self.adsys.TDS.initialized)
|
465
|
+
|
466
|
+
def _send_tgr(self, sa, sp):
|
467
|
+
"""
|
468
|
+
Sned to generator power refrence.
|
469
|
+
|
470
|
+
Notes
|
471
|
+
-----
|
472
|
+
1. AGC power reference ``paux`` is not included in this function.
|
473
|
+
"""
|
474
|
+
# 1) TurbineGov
|
475
|
+
syg_idx = sp.dyn.link['syg_idx'].dropna().tolist() # SynGen idx
|
476
|
+
# corresponding StaticGen idx in ANDES
|
477
|
+
stg_syg_idx = sa.SynGen.get(src='gen', attr='v', idx=syg_idx,
|
478
|
+
allow_none=True, default=None)
|
479
|
+
# corresponding TurbineGov idx in ANDES
|
480
|
+
gov_idx = sa.TurbineGov.find_idx(keys='syn', values=syg_idx)
|
481
|
+
# corresponding StaticGen pg in AMS
|
482
|
+
syg_ams = sp.recent.get(src='pg', attr='v', idx=stg_syg_idx,
|
483
|
+
allow_none=True, default=0)
|
484
|
+
# --- check consistency ---
|
485
|
+
syg_mask = self.link['syg_idx'].notnull() & self.link['gov_idx'].isnull()
|
486
|
+
if syg_mask.any():
|
487
|
+
logger.debug('Governor is not complete for SynGen.')
|
488
|
+
# --- pref ---
|
489
|
+
sa.TurbineGov.set(value=syg_ams, idx=gov_idx, attr='v', src='pref0')
|
490
|
+
|
491
|
+
# --- paux ---
|
492
|
+
# TODO: sync paux, using paux0
|
493
|
+
|
494
|
+
# 2) DG
|
495
|
+
dg_idx = sp.dyn.link['dg_idx'].dropna().tolist() # DG idx
|
496
|
+
# corresponding StaticGen idx in ANDES
|
497
|
+
stg_dg_idx = sa.DG.get(src='gen', attr='v', idx=dg_idx,
|
498
|
+
allow_none=True, default=None,
|
499
|
+
)
|
500
|
+
# corresponding StaticGen pg in AMS
|
501
|
+
dg_ams = sp.recent.get(src='pg', attr='v', idx=stg_dg_idx,
|
502
|
+
allow_none=True, default=0)
|
503
|
+
# --- pref ---
|
504
|
+
sa.DG.set(src='pref0', idx=dg_idx, attr='v', value=dg_ams)
|
505
|
+
# TODO: paux, using Pext0, this one should be do in other place rather than here
|
506
|
+
|
507
|
+
# 3) RenGen
|
508
|
+
# TODO: seems to be unnecessary
|
509
|
+
# which models/params are used to control output and auxillary power?
|
510
|
+
|
511
|
+
return True
|
512
|
+
|
513
|
+
def _send_dgu(self, sa, sp):
|
514
|
+
"""
|
515
|
+
Send to ANDES the dynamic generator online status.
|
516
|
+
"""
|
517
|
+
# 1) SynGen
|
518
|
+
syg_idx = sp.dyn.link['syg_idx'].dropna().tolist() # SynGen idx
|
519
|
+
# corresponding StaticGen idx in ANDES
|
520
|
+
stg_syg_idx = sa.SynGen.get(src='gen', attr='v', idx=syg_idx,
|
521
|
+
allow_none=True, default=None)
|
522
|
+
# corresponding StaticGen u in AMS
|
523
|
+
stg_u_ams = sp.StaticGen.get(src='u', attr='v', idx=stg_syg_idx,
|
524
|
+
allow_none=True, default=0)
|
525
|
+
stg_u_andes = sa.SynGen.get(src='u', attr='v', idx=syg_idx,
|
526
|
+
allow_none=True, default=0)
|
527
|
+
# 2) DG
|
528
|
+
dg_idx = sp.dyn.link['dg_idx'].dropna().tolist() # DG idx
|
529
|
+
# corresponding StaticGen idx in ANDES
|
530
|
+
stg_dg_idx = sa.DG.get(src='gen', attr='v', idx=dg_idx,
|
531
|
+
allow_none=True, default=None)
|
532
|
+
# corresponding DG u in AMS
|
533
|
+
dg_u_ams = sp.StaticGen.get(src='u', attr='v', idx=stg_dg_idx,
|
534
|
+
allow_none=True, default=0)
|
535
|
+
du_u_andes = sa.DG.get(src='u', attr='v', idx=dg_idx,
|
536
|
+
allow_none=True, default=0)
|
537
|
+
# 3) RenGen
|
538
|
+
rg_idx = sp.dyn.link['rg_idx'].dropna().tolist() # RenGen idx
|
539
|
+
# corresponding StaticGen idx in ANDES
|
540
|
+
stg_rg_idx = sa.RenGen.get(src='gen', attr='v', idx=rg_idx,
|
541
|
+
allow_none=True, default=None)
|
542
|
+
# corresponding RenGen u in AMS
|
543
|
+
rg_u_ams = sp.StaticGen.get(src='u', attr='v', idx=stg_rg_idx,
|
544
|
+
allow_none=True, default=0)
|
545
|
+
rg_u_andes = sa.RenGen.get(src='u', attr='v', idx=rg_idx,
|
546
|
+
allow_none=True, default=0)
|
547
|
+
# 4) sync results
|
548
|
+
cond = (
|
549
|
+
not np.array_equal(stg_u_ams, stg_u_andes) or
|
550
|
+
not np.array_equal(dg_u_ams, du_u_andes) or
|
551
|
+
not np.array_equal(rg_u_ams, rg_u_andes)
|
552
|
+
)
|
553
|
+
if cond:
|
554
|
+
msg = 'ANDES dynamic generator online status should be switched using Toggle!'
|
555
|
+
msg += ' Otherwise, unexpected results might occur.'
|
556
|
+
raise ValueError(msg)
|
557
|
+
# FIXME: below code seems to be unnecessary
|
558
|
+
sa.SynGen.set(src='u', idx=syg_idx, attr='v', value=stg_u_ams)
|
559
|
+
sa.DG.set(src='u', idx=dg_idx, attr='v', value=dg_u_ams)
|
560
|
+
sa.RenGen.set(src='u', idx=rg_idx, attr='v', value=rg_u_ams)
|
561
|
+
return True
|
562
|
+
|
563
|
+
def _sync_check(self, amsys, adsys):
|
564
|
+
"""
|
565
|
+
Check if AMS and ANDES systems are ready for sync.
|
566
|
+
"""
|
567
|
+
if amsys.dyn.adsys:
|
568
|
+
if amsys.dyn.adsys != adsys:
|
569
|
+
logger.error('Target ANDES system is different from the linked one, quit.')
|
570
|
+
return False
|
571
|
+
if not amsys.is_setup:
|
572
|
+
amsys.setup()
|
573
|
+
if not adsys.is_setup:
|
574
|
+
adsys.setup()
|
575
|
+
if amsys.dyn.link is None:
|
576
|
+
amsys.dyn.link = make_link_table(adsys=adsys)
|
577
|
+
|
578
|
+
def send(self, adsys=None, routine=None):
|
579
|
+
"""
|
580
|
+
Send results of the recent sovled AMS routine (``sp.recent``) to the
|
581
|
+
target ANDES system.
|
582
|
+
|
583
|
+
Note that converged AC conversion DOES NOT guarantee successful dynamic
|
584
|
+
initialization ``TDS.init()``.
|
585
|
+
Failed initialization is usually caused by limiter violation.
|
586
|
+
|
587
|
+
Parameters
|
588
|
+
----------
|
589
|
+
adsys : adsys.System.system, optional
|
590
|
+
The target ANDES dynamic system instance. If not provided, use the
|
591
|
+
linked ANDES system isntance (``sp.dyn.adsys``).
|
592
|
+
routine : str, optional
|
593
|
+
The routine to be sent to ANDES. If None, ``recent`` will be used.
|
594
|
+
"""
|
595
|
+
sa = adsys if adsys is not None else self.adsys
|
596
|
+
sp = self.amsys
|
597
|
+
self._sync_check(amsys=sp, adsys=sa)
|
598
|
+
|
599
|
+
# --- Information ---
|
600
|
+
rtn = sp.recent if routine is None else getattr(sp, routine)
|
601
|
+
if rtn is None:
|
602
|
+
logger.warning('No assigned or recent solved routine found, quit send.')
|
603
|
+
return False
|
604
|
+
elif rtn.exit_code != 0:
|
605
|
+
logger.warning(f'{sp.recent.class_name} is not solved at optimal, quit send.')
|
606
|
+
return False
|
607
|
+
else:
|
608
|
+
logger.info(f'Send <{rtn.class_name}> results to ANDES <{hex(id(sa))}>...')
|
609
|
+
|
610
|
+
# NOTE: if DC type, check if results are converted
|
611
|
+
if (rtn.type != 'ACED') and (not rtn.converted):
|
612
|
+
logger.error(f'<{rtn.class_name}> AC conversion failed or not done yet!')
|
613
|
+
|
614
|
+
# --- Mapping ---
|
615
|
+
map2 = getattr(rtn, 'map2') # mapping-to dict
|
616
|
+
if len(map2) == 0:
|
617
|
+
logger.warning(f'{rtn.class_name} has empty map2, quit send.')
|
618
|
+
return True
|
619
|
+
|
620
|
+
# NOTE: ads is short for ANDES
|
621
|
+
for vname_ams, (mname_ads, pname_ads) in map2.items():
|
622
|
+
mdl_ads = getattr(sa, mname_ads) # ANDES model or group
|
623
|
+
|
624
|
+
# --- skipping scenarios ---
|
625
|
+
if mdl_ads.n == 0:
|
626
|
+
logger.debug(f'ANDES model <{mname_ads}> is empty.')
|
627
|
+
continue
|
628
|
+
|
629
|
+
var_ams = getattr(rtn, vname_ams) # instance of AMS routine var
|
630
|
+
idx_ads = var_ams.get_all_idxes() # use AMS idx as target ANDES idx
|
631
|
+
|
632
|
+
# --- special scenarios ---
|
633
|
+
# 0. send PV bus voltage to StaticGen.v0 if not PFlow yet and AC converted
|
634
|
+
cond_vpv = (mname_ads == 'Bus') and (pname_ads == 'v0')
|
635
|
+
if cond_vpv and (not self.is_tds) and (rtn.converted):
|
636
|
+
# --- StaticGen ---
|
637
|
+
stg_idx = sp.StaticGen.get_all_idxes()
|
638
|
+
bus_stg = sp.StaticGen.get(src='bus', attr='v', idx=stg_idx)
|
639
|
+
vBus = rtn.get(src='vBus', attr='v', idx=bus_stg)
|
640
|
+
sa.StaticGen.set(src='v0', idx=stg_idx, attr='v', value=vBus)
|
641
|
+
logger.info(f'*Send <{vname_ams}> to StaticGen.v0')
|
642
|
+
|
643
|
+
# 1. gen online status; in TDS running, setting u is invalid
|
644
|
+
cond_ads_stg_u = (mname_ads in ['StaticGen', 'PV', 'Sclak']) and (pname_ads == 'u')
|
645
|
+
if cond_ads_stg_u and (self.is_tds):
|
646
|
+
logger.info(f'*Skip sending {vname_ams} to StaticGen.u during TDS')
|
647
|
+
continue
|
648
|
+
|
649
|
+
# 2. Bus voltage
|
650
|
+
cond_ads_bus_v0 = (mname_ads == 'Bus') and (pname_ads == 'v0')
|
651
|
+
if cond_ads_bus_v0 and (self.is_tds):
|
652
|
+
logger.info(f'*Skip sending {vname_ams} t0 Bus.v0 during TDS')
|
653
|
+
continue
|
654
|
+
|
655
|
+
# 3. gen power reference; in TDS running, pg should go to TurbineGov
|
656
|
+
cond_ads_stg_p0 = (mname_ads in ['StaticGen', 'PV', 'Sclak']) and (pname_ads == 'p0')
|
657
|
+
if cond_ads_stg_p0 and (self.is_tds):
|
658
|
+
# --- SynGen: TurbineGov.pref0 ---
|
659
|
+
syg_idx = sp.dyn.link['syg_idx'].dropna().tolist() # SynGen idx
|
660
|
+
# corresponding StaticGen idx in ANDES
|
661
|
+
stg_syg_idx = sa.SynGen.get(src='gen', attr='v', idx=syg_idx,
|
662
|
+
allow_none=True, default=None)
|
663
|
+
# corresponding TurbineGov idx in ANDES
|
664
|
+
gov_idx = sa.TurbineGov.find_idx(keys='syn', values=syg_idx)
|
665
|
+
# corresponding StaticGen pg in AMS
|
666
|
+
syg_ams = rtn.get(src='pg', attr='v', idx=stg_syg_idx)
|
667
|
+
# NOTE: check consistency
|
668
|
+
syg_mask = self.link['syg_idx'].notnull() & self.link['gov_idx'].isnull()
|
669
|
+
if syg_mask.any():
|
670
|
+
logger.debug('Governor is not complete for SynGen.')
|
671
|
+
# --- pref ---
|
672
|
+
sa.TurbineGov.set(src='pref0', idx=gov_idx, attr='v', value=syg_ams)
|
673
|
+
|
674
|
+
# --- DG: DG.pref0 ---
|
675
|
+
dg_idx = sp.dyn.link['dg_idx'].dropna().tolist() # DG idx
|
676
|
+
# corresponding StaticGen idx in ANDES
|
677
|
+
stg_dg_idx = sa.DG.get(src='gen', attr='v', idx=dg_idx,
|
678
|
+
allow_none=True, default=None)
|
679
|
+
# corresponding StaticGen pg in AMS
|
680
|
+
dg_ams = rtn.get(src='pg', attr='v', idx=stg_dg_idx)
|
681
|
+
# --- pref ---
|
682
|
+
sa.DG.set(src='pref0', idx=dg_idx, attr='v', value=dg_ams)
|
683
|
+
|
684
|
+
# --- RenGen: seems unnecessary ---
|
685
|
+
# TODO: which models/params are used to control output and auxillary power?
|
686
|
+
|
687
|
+
var_dest = ''
|
688
|
+
if len(syg_ams) > 0:
|
689
|
+
var_dest = 'TurbineGov.pref0'
|
690
|
+
if len(dg_ams) > 0:
|
691
|
+
var_dest += ' and DG.pref0'
|
692
|
+
logger.warning(f'*Send <{vname_ams}> to {var_dest}')
|
693
|
+
continue
|
694
|
+
|
695
|
+
# --- other scenarios ---
|
696
|
+
if _dest_check(mname=mname_ads, pname=pname_ads, idx=idx_ads, adsys=sa):
|
697
|
+
mdl_ads.set(src=pname_ads, idx=idx_ads, attr='v', value=var_ams.v)
|
698
|
+
logger.warning(f'Send <{vname_ams}> to {mname_ads}.{pname_ads}')
|
699
|
+
return True
|
700
|
+
|
701
|
+
def receive(self, adsys=None, routine=None, no_update=False):
|
702
|
+
"""
|
703
|
+
Receive ANDES system results to AMS devices.
|
704
|
+
|
705
|
+
Parameters
|
706
|
+
----------
|
707
|
+
adsys : adsys.System.system, optional
|
708
|
+
The target ANDES dynamic system instance. If not provided, use the
|
709
|
+
linked ANDES system isntance (``sp.dyn.adsys``).
|
710
|
+
routine : str, optional
|
711
|
+
The routine to be received from ANDES. If None, ``recent`` will be used.
|
712
|
+
no_update : bool, optional
|
713
|
+
True to skip update the AMS routine parameters after sync. Default is False.
|
714
|
+
"""
|
715
|
+
sa = adsys if adsys is not None else self.adsys
|
716
|
+
sp = self.amsys
|
717
|
+
self._sync_check(amsys=sp, adsys=sa)
|
718
|
+
|
719
|
+
# --- Information: not necessary in receive ---
|
720
|
+
rtn = sp.recent if routine is None else getattr(sp, routine)
|
721
|
+
if rtn is None:
|
722
|
+
logger.warning('No assigned or recent solved routine found, quit receive.')
|
723
|
+
return False
|
724
|
+
|
725
|
+
# --- Mapping ---
|
726
|
+
map1 = getattr(rtn, 'map1') # mapping-from dict
|
727
|
+
if len(map1) == 0:
|
728
|
+
logger.warning(f'{rtn.class_name} has empty map1, quit receive.')
|
729
|
+
return True
|
730
|
+
|
731
|
+
link = sp.dyn.link # link table
|
732
|
+
pname_to_update = [] # list of parameters to be updated
|
733
|
+
for vname_ams, (mname_ads, pname_ads) in map1.items():
|
734
|
+
mdl_ads = getattr(sa, mname_ads) # ANDES model or group
|
735
|
+
|
736
|
+
# --- skipping scenarios ---
|
737
|
+
if mdl_ads.n == 0:
|
738
|
+
logger.debug(f'ANDES model <{mname_ads}> is empty.')
|
739
|
+
continue
|
740
|
+
|
741
|
+
idx_ads = rtn.__dict__[vname_ams].get_all_idxes() # use AMS idx as target ANDES idx
|
742
|
+
|
743
|
+
# --- special scenarios ---
|
744
|
+
# 1. gen online status; in TDS running, take from dynamic generator
|
745
|
+
cond_ads_stg_u = (mname_ads in ['StaticGen', 'PV', 'Sclak']) and (pname_ads == 'u')
|
746
|
+
if cond_ads_stg_u and (self.is_tds):
|
747
|
+
# --- SynGen ---
|
748
|
+
u_sg = sa.SynGen.get(idx=link['syg_idx'].replace(nan, None).to_list(),
|
749
|
+
src='u', attr='v',
|
750
|
+
allow_none=True, default=0,)
|
751
|
+
# --- DG ---
|
752
|
+
u_dg = sa.DG.get(idx=link['dg_idx'].replace(nan, None).to_list(),
|
753
|
+
src='u', attr='v',
|
754
|
+
allow_none=True, default=0,)
|
755
|
+
# --- RenGen ---
|
756
|
+
u_rg = sa.RenGen.get(idx=link['rg_idx'].replace(nan, None).to_list(),
|
757
|
+
src='u', attr='v',
|
758
|
+
allow_none=True, default=0,)
|
759
|
+
# --- output ---
|
760
|
+
u_dyg = u_sg + u_rg + u_dg
|
761
|
+
# NOTE: StaticGen that has no dynamic generator
|
762
|
+
# Sync StaticGen.u first, then overwrite the ones with dynamic generator
|
763
|
+
u_stg = sa.StaticGen.get(src='u', attr='v',
|
764
|
+
idx=link['stg_idx'].values)
|
765
|
+
# NOTE: only update u if changed actually
|
766
|
+
u0_rtn = rtn.get(src=vname_ams, attr='v', idx=link['stg_idx'].values).copy()
|
767
|
+
rtn.set(src=vname_ams, idx=link['stg_idx'].values, attr='v', value=u_stg)
|
768
|
+
rtn.set(src=vname_ams, idx=link['stg_idx'].values, attr='v', value=u_dyg)
|
769
|
+
u_rtn = rtn.get(src=vname_ams, attr='v', idx=link['stg_idx'].values).copy()
|
770
|
+
if not np.array_equal(u0_rtn, u_rtn):
|
771
|
+
pname_to_update.append(vname_ams)
|
772
|
+
var_dest = ''
|
773
|
+
var_dest += 'SynGen.u' if sa.SynGen.n > 0 else ''
|
774
|
+
var_dest += ', DG.u' if sa.DG.n > 0 else ''
|
775
|
+
var_dest += ', RenGen.u' if sa.RenGen.n > 0 else ''
|
776
|
+
var_dest += ', StaicGen.u' if sa.RenGen.n+sa.SynGen.n+sa.DG.n < sa.StaticGen.n else ''
|
777
|
+
logger.info(f'Receive <{vname_ams}> from {var_dest}')
|
778
|
+
continue
|
779
|
+
|
780
|
+
# 2. gen output power; in TDS running, take from dynamic generator
|
781
|
+
cond_ads_stg_p = (mname_ads in ['StaticGen', 'PV', 'Sclak']) and (pname_ads == 'p')
|
782
|
+
if cond_ads_stg_p and (self.is_tds):
|
783
|
+
# --- SynGen ---
|
784
|
+
p_sg = sa.SynGen.get(idx=link['syg_idx'].replace(nan, None).to_list(),
|
785
|
+
src='Pe', attr='v',
|
786
|
+
allow_none=True, default=0,)
|
787
|
+
# --- DG ---
|
788
|
+
Ie_dg = sa.DG.get(idx=link['dg_idx'].replace(nan, None).to_list(),
|
789
|
+
src='Ipout_y', attr='v',
|
790
|
+
allow_none=True, default=0,)
|
791
|
+
v_dg = sa.DG.get(idx=link['dg_idx'].replace(nan, None).to_list(),
|
792
|
+
src='v', attr='v',
|
793
|
+
allow_none=True, default=0,)
|
794
|
+
p_dg = Ie_dg * v_dg
|
795
|
+
# --- RenGen ---
|
796
|
+
p_rg = sa.RenGen.get(idx=link['rg_idx'].replace(nan, None).to_list(),
|
797
|
+
src='Pe', attr='v',
|
798
|
+
allow_none=True, default=0,)
|
799
|
+
# --- output ---
|
800
|
+
p_dyg = p_sg + p_rg + p_dg
|
801
|
+
# NOTE: StaticGen that has no dynamic generator
|
802
|
+
# Sync StaticGen.p first, then overwrite the ones with dynamic generator
|
803
|
+
p_stg = sa.StaticGen.get(src='p', attr='v',
|
804
|
+
idx=link['stg_idx'].values)
|
805
|
+
rtn.set(src=vname_ams, idx=link['stg_idx'].values, attr='v', value=p_stg)
|
806
|
+
rtn.set(src=vname_ams, idx=link['stg_idx'].values, attr='v', value=p_dyg)
|
807
|
+
|
808
|
+
pname_to_update.append(vname_ams)
|
809
|
+
|
810
|
+
var_dest = ''
|
811
|
+
var_dest += 'SynGen.Pe' if sa.SynGen.n > 0 else ''
|
812
|
+
var_dest += ', DG.Pe' if sa.DG.n > 0 else ''
|
813
|
+
var_dest += ', RenGen.Pe' if sa.RenGen.n > 0 else ''
|
814
|
+
var_dest += ', StaicGen.p' if sa.RenGen.n+sa.SynGen.n+sa.DG.n < sa.StaticGen.n else ''
|
815
|
+
logger.info(f'Receive <{vname_ams}> from {var_dest}')
|
816
|
+
continue
|
817
|
+
|
818
|
+
# --- other scenarios ---
|
819
|
+
if _dest_check(mname=mname_ads, pname=pname_ads, idx=idx_ads, adsys=sa):
|
820
|
+
v_ads = mdl_ads.get(src=pname_ads, attr='v', idx=idx_ads)
|
821
|
+
rtn.set(src=vname_ams, idx=idx_ads, attr='v', value=v_ads)
|
822
|
+
pname_to_update.append(vname_ams)
|
823
|
+
logger.warning(f'Receive <{vname_ams}> from {mname_ads}.{pname_ads}')
|
824
|
+
|
825
|
+
# --- update routine parameters ---
|
826
|
+
if no_update and (len(pname_to_update) > 0):
|
827
|
+
logger.info(f'Please update <{rtn.class_name}> parameters: {pname_to_update}')
|
828
|
+
elif len(pname_to_update) > 0:
|
829
|
+
rtn.update(params=pname_to_update, build_mats=False)
|
830
|
+
return True
|
831
|
+
|
832
|
+
|
833
|
+
def _dest_check(mname, pname, idx, adsys):
|
834
|
+
"""
|
835
|
+
Check if destination is valid.
|
836
|
+
|
837
|
+
Parameters
|
838
|
+
----------
|
839
|
+
mname : str
|
840
|
+
Target ANDES model/group name.
|
841
|
+
pname : str
|
842
|
+
Target ANDES parameter name.
|
843
|
+
idx : list
|
844
|
+
Target idx.
|
845
|
+
adsys : ANDES.system.System
|
846
|
+
Target ANDES system.
|
847
|
+
"""
|
848
|
+
# --- check model ---
|
849
|
+
if not hasattr(adsys, mname):
|
850
|
+
raise ValueError(f'Model error: ANDES system <{hex(adsys)}> has no <{mname}>')
|
851
|
+
|
852
|
+
# --- check param ---
|
853
|
+
mdl = getattr(adsys, mname)
|
854
|
+
_is_grp = mname in adsys.groups.keys()
|
855
|
+
# if it is a group, iterate through all models in the group
|
856
|
+
mdls_grp_name = list(adsys.groups[mname].models.keys()) if _is_grp else [mname]
|
857
|
+
n_no_mdl = 0
|
858
|
+
for mdl_name in mdls_grp_name:
|
859
|
+
mdl_to_check = getattr(adsys, mdl_name, None)
|
860
|
+
if mdl_to_check is not None and hasattr(mdl_to_check, pname):
|
861
|
+
break # found the param in any one of the models, break
|
862
|
+
n_no_mdl += 1
|
863
|
+
if n_no_mdl == len(mdls_grp_name):
|
864
|
+
raise ValueError(f'Param error: ANDES <{mdl.class_name}> has no <{pname}>')
|
865
|
+
|
866
|
+
# --- check idx ---
|
867
|
+
if _is_grp:
|
868
|
+
_ads_idx = [v for mdl in mdl.models.values() for v in mdl.idx.v]
|
869
|
+
else:
|
870
|
+
_ads_idx = mdl.idx.v
|
871
|
+
if not set(idx).issubset(set(_ads_idx)):
|
872
|
+
idx_gap = set(idx) - set(_ads_idx)
|
873
|
+
raise ValueError(f'Idx error: ANDES <{mdl.class_name}> has no idx: {idx_gap}')
|
874
|
+
|
875
|
+
return True
|
876
|
+
|
877
|
+
|
878
|
+
def build_group_table(adsys, grp_name, param_name, mdl_name=None):
|
879
|
+
"""
|
880
|
+
Build the table for devices in a group in an ANDES System.
|
881
|
+
|
882
|
+
Parameters
|
883
|
+
----------
|
884
|
+
adsys : andes.system.System
|
885
|
+
The ANDES system to build the table
|
886
|
+
grp_name : string
|
887
|
+
The ANDES group
|
888
|
+
param_name : list of string
|
889
|
+
The common columns of a group that to be included in the table.
|
890
|
+
mdl_name : list of string
|
891
|
+
The list of models that to be included in the table. Default as all models.
|
892
|
+
|
893
|
+
Returns
|
894
|
+
-------
|
895
|
+
DataFrame
|
896
|
+
|
897
|
+
The output Dataframe contains the columns from the device
|
898
|
+
"""
|
899
|
+
grp_df = pd.DataFrame(columns=param_name)
|
900
|
+
grp = getattr(adsys, grp_name) # get the group instance
|
901
|
+
|
902
|
+
mdl_to_add = mdl_name if mdl_name else list(grp.models.keys())
|
903
|
+
mdl_dfs = [getattr(adsys, mdl).as_df()[param_name] for mdl in mdl_to_add]
|
904
|
+
|
905
|
+
grp_df = pd.concat(mdl_dfs, axis=0, ignore_index=True)
|
906
|
+
|
907
|
+
# --- type sanity check ---
|
908
|
+
mdl_1st = adsys.models[mdl_to_add[0]]
|
909
|
+
# NOTE: force IdxParam to be string type
|
910
|
+
cols_to_convert = [col for col in param_name if
|
911
|
+
(mdl_1st.params[col].class_name == 'IdxParam') and
|
912
|
+
(pd.api.types.is_numeric_dtype(grp_df[col]))]
|
913
|
+
# NOTE: if 'idx' is included, force it to be string type
|
914
|
+
if ('idx' in param_name) and pd.api.types.is_numeric_dtype(grp_df['idx']):
|
915
|
+
cols_to_convert.append('idx')
|
916
|
+
|
917
|
+
grp_df[cols_to_convert] = grp_df[cols_to_convert].astype(int).astype(str)
|
918
|
+
return grp_df
|
919
|
+
|
920
|
+
|
921
|
+
def make_link_table(adsys):
|
922
|
+
"""
|
923
|
+
Build the link table for generators and generator controllers in an ANDES
|
924
|
+
System, including ``SynGen`` and ``DG`` for now.
|
925
|
+
|
926
|
+
Parameters
|
927
|
+
----------
|
928
|
+
adsys : andes.system.System
|
929
|
+
The ANDES system to link
|
930
|
+
|
931
|
+
Returns
|
932
|
+
-------
|
933
|
+
DataFrame
|
934
|
+
|
935
|
+
Each column in the output Dataframe contains the ``idx`` of linked
|
936
|
+
``StaticGen``, ``Bus``, ``DG``, ``RenGen``, ``RenExciter``, ``SynGen``,
|
937
|
+
``Exciter``, and ``TurbineGov``, ``gammap``, ``gammaq``.
|
938
|
+
"""
|
939
|
+
# --- build group tables ---
|
940
|
+
# 1) StaticGen
|
941
|
+
ssa_stg = build_group_table(adsys=adsys, grp_name='StaticGen',
|
942
|
+
param_name=['u', 'name', 'idx', 'bus'],
|
943
|
+
mdl_name=[])
|
944
|
+
# 2) TurbineGov
|
945
|
+
ssa_gov = build_group_table(adsys=adsys, grp_name='TurbineGov',
|
946
|
+
param_name=['idx', 'syn'],
|
947
|
+
mdl_name=[])
|
948
|
+
# 3) Exciter
|
949
|
+
ssa_exc = build_group_table(adsys=adsys, grp_name='Exciter',
|
950
|
+
param_name=['idx', 'syn'],
|
951
|
+
mdl_name=[])
|
952
|
+
# 4) SynGen
|
953
|
+
ssa_syg = build_group_table(adsys=adsys, grp_name='SynGen', mdl_name=['GENCLS', 'GENROU'],
|
954
|
+
param_name=['idx', 'bus', 'gen', 'gammap', 'gammaq'])
|
955
|
+
# 5) DG
|
956
|
+
ssa_dg = build_group_table(adsys=adsys, grp_name='DG', mdl_name=[],
|
957
|
+
param_name=['idx', 'bus', 'gen', 'gammap', 'gammaq'])
|
958
|
+
# 6) RenGen
|
959
|
+
ssa_rg = build_group_table(adsys=adsys, grp_name='RenGen', mdl_name=[],
|
960
|
+
param_name=['idx', 'bus', 'gen', 'gammap', 'gammaq'])
|
961
|
+
# 7) RenExciter
|
962
|
+
ssa_rexc = build_group_table(adsys=adsys, grp_name='RenExciter', mdl_name=[],
|
963
|
+
param_name=['idx', 'reg'])
|
964
|
+
|
965
|
+
# --- build link table ---
|
966
|
+
# NOTE: use bus index as unique identifier
|
967
|
+
ssa_bus = build_group_table(adsys=adsys, grp_name='ACTopology', mdl_name=['Bus'],
|
968
|
+
param_name=['name', 'idx'])
|
969
|
+
# 1) StaticGen
|
970
|
+
ssa_key = pd.merge(left=ssa_stg.rename(columns={'name': 'stg_name', 'idx': 'stg_idx',
|
971
|
+
'bus': 'bus_idx', 'u': 'stg_u'}),
|
972
|
+
right=ssa_bus.rename(columns={'name': 'bus_name', 'idx': 'bus_idx'}),
|
973
|
+
how='left', on='bus_idx')
|
974
|
+
# 2) Dynamic Generators
|
975
|
+
ssa_syg = pd.merge(left=ssa_key, how='right', on='stg_idx',
|
976
|
+
right=ssa_syg.rename(columns={'idx': 'syg_idx', 'gen': 'stg_idx'}))
|
977
|
+
ssa_dg = pd.merge(left=ssa_key, how='right', on='stg_idx',
|
978
|
+
right=ssa_dg.rename(columns={'idx': 'dg_idx', 'gen': 'stg_idx'}))
|
979
|
+
ssa_rg = pd.merge(left=ssa_key, how='right', on='stg_idx',
|
980
|
+
right=ssa_rg.rename(columns={'idx': 'rg_idx', 'gen': 'stg_idx'}))
|
981
|
+
|
982
|
+
# NOTE: for StaticGen without Dynamic Generator, fill gammap and gammaq as 1
|
983
|
+
ssa_key0 = pd.merge(left=ssa_key, how='left', on='stg_idx',
|
984
|
+
right=ssa_syg[['stg_idx', 'syg_idx']])
|
985
|
+
ssa_key0 = pd.merge(left=ssa_key0, how='left', on='stg_idx',
|
986
|
+
right=ssa_dg[['stg_idx', 'dg_idx']])
|
987
|
+
ssa_key0 = pd.merge(left=ssa_key0, how='left', on='stg_idx',
|
988
|
+
right=ssa_rg[['stg_idx', 'rg_idx']])
|
989
|
+
|
990
|
+
# NOTE: use this instead of fillna to avoid type conversion
|
991
|
+
idxc = ['syg_idx', 'dg_idx', 'rg_idx']
|
992
|
+
ssa_key0[idxc] = ssa_key0[idxc].astype('str').replace({'nan': ''}).astype('bool')
|
993
|
+
|
994
|
+
dyr = ssa_key0['syg_idx'] + ssa_key0['dg_idx'] + ssa_key0['rg_idx']
|
995
|
+
non_dyr = np.logical_not(dyr)
|
996
|
+
ssa_dyr0 = ssa_key0[non_dyr].reset_index(drop=True)
|
997
|
+
ssa_dyr0['gammap'] = 1
|
998
|
+
ssa_dyr0['gammaq'] = 1
|
999
|
+
|
1000
|
+
ssa_key = pd.concat([ssa_syg, ssa_dg, ssa_rg, ssa_dyr0], axis=0)
|
1001
|
+
ssa_key = pd.merge(left=ssa_key,
|
1002
|
+
right=ssa_exc.rename(columns={'idx': 'exc_idx', 'syn': 'syg_idx'}),
|
1003
|
+
how='left', on='syg_idx')
|
1004
|
+
ssa_key = pd.merge(left=ssa_key,
|
1005
|
+
right=ssa_gov.rename(columns={'idx': 'gov_idx', 'syn': 'syg_idx'}),
|
1006
|
+
how='left', on='syg_idx')
|
1007
|
+
ssa_key = pd.merge(left=ssa_key, how='left', on='rg_idx',
|
1008
|
+
right=ssa_rexc.rename(columns={'idx': 'rexc_idx', 'reg': 'rg_idx'}))
|
1009
|
+
|
1010
|
+
# NOTE: other cols might be useful in the future
|
1011
|
+
# cols = ['stg_name', 'stg_u', 'stg_idx', 'bus_idx', 'dg_idx', 'rg_idx', 'rexc_idx',
|
1012
|
+
# 'syg_idx', 'exc_idx', 'gov_idx', 'bus_name', 'gammap', 'gammaq']
|
1013
|
+
# re-order columns
|
1014
|
+
cols = ['stg_idx', 'bus_idx', # static gen
|
1015
|
+
'syg_idx', 'gov_idx', # syn gen
|
1016
|
+
'dg_idx', # distributed gen
|
1017
|
+
'rg_idx', # renewable gen
|
1018
|
+
'gammap', 'gammaq', # gamma
|
1019
|
+
]
|
1020
|
+
out = ssa_key[cols].sort_values(by='stg_idx', ascending=False).reset_index(drop=True)
|
1021
|
+
return out
|
1022
|
+
|
1023
|
+
|
1024
|
+
def verify_pf(amsys, adsys, tol=1e-3):
|
1025
|
+
"""
|
1026
|
+
Verify the power flow results between AMS and ANDES.
|
1027
|
+
Note that this function will run PFlow in both systems.
|
1028
|
+
|
1029
|
+
Parameters
|
1030
|
+
----------
|
1031
|
+
sp : ams.System
|
1032
|
+
The AMS system.
|
1033
|
+
sa : andes.System
|
1034
|
+
The ANDES system.
|
1035
|
+
|
1036
|
+
Returns
|
1037
|
+
-------
|
1038
|
+
bool
|
1039
|
+
True if the power flow results are consistent; False otherwise.
|
1040
|
+
"""
|
1041
|
+
amsys.PFlow.run()
|
1042
|
+
if adsys.is_setup:
|
1043
|
+
adsys.PFlow.run()
|
1044
|
+
else:
|
1045
|
+
logger.info('ANDES system is not setup, quit verification.')
|
1046
|
+
return False
|
1047
|
+
v_check = np.allclose(amsys.Bus.v.v, adsys.Bus.v.v, atol=tol)
|
1048
|
+
a_check = np.allclose(amsys.Bus.a.v, adsys.Bus.a.v, atol=tol)
|
1049
|
+
check = v_check and a_check
|
1050
|
+
|
1051
|
+
v_diff_max = np.max(np.abs(amsys.Bus.v.v - adsys.Bus.v.v))
|
1052
|
+
a_diff_max = np.max(np.abs(amsys.Bus.a.v - adsys.Bus.a.v))
|
1053
|
+
diff_msg = f'Voltage diff max: {v_diff_max}, Angle diff max: {a_diff_max}'
|
1054
|
+
logger.debug(diff_msg)
|
1055
|
+
if check:
|
1056
|
+
logger.info('Power flow results are consistent.')
|
1057
|
+
else:
|
1058
|
+
msg = 'Power flow results are inconsistent!'
|
1059
|
+
logger.warning(msg)
|
1060
|
+
logger.warning(diff_msg)
|
1061
|
+
return check
|
1062
|
+
|
1063
|
+
|
1064
|
+
def replace_dev(row, mdl, dev, idx_map):
|
1065
|
+
"""
|
1066
|
+
Replace the device idx in the row based on the idx_map.
|
1067
|
+
|
1068
|
+
Parameters
|
1069
|
+
----------
|
1070
|
+
row : pd.Series
|
1071
|
+
The row of the DataFrame.
|
1072
|
+
mdl : str
|
1073
|
+
The column name for the Model.
|
1074
|
+
dev : str
|
1075
|
+
The column name for the Device idx.
|
1076
|
+
idx_map : dict
|
1077
|
+
The index map for replacement.
|
1078
|
+
|
1079
|
+
Returns
|
1080
|
+
-------
|
1081
|
+
str
|
1082
|
+
The new device idx.
|
1083
|
+
"""
|
1084
|
+
old_idx = row[dev]
|
1085
|
+
return idx_map.get(row[mdl], {}).get(old_idx, old_idx)
|