ltbams 1.0.7__py3-none-any.whl → 1.0.9__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/_version.py +3 -3
- ams/cases/hawaii40/Hawaii40.m +375 -0
- ams/cases/matpower/case5.m +25 -0
- ams/io/__init__.py +1 -0
- ams/io/matpower.py +335 -25
- ams/io/psse.py +1 -1
- ams/io/pypower.py +14 -0
- ams/models/group.py +7 -5
- ams/models/line.py +1 -162
- ams/models/static/gen.py +9 -1
- ams/report.py +3 -4
- ams/routines/dcpf0.py +1 -1
- ams/shared.py +4 -1
- docs/source/getting_started/copyright.rst +1 -1
- docs/source/getting_started/formats/matpower.rst +135 -0
- docs/source/getting_started/formats/pypower.rst +1 -2
- docs/source/release-notes.rst +20 -0
- {ltbams-1.0.7.dist-info → ltbams-1.0.9.dist-info}/METADATA +3 -3
- {ltbams-1.0.7.dist-info → ltbams-1.0.9.dist-info}/RECORD +23 -23
- {ltbams-1.0.7.dist-info → ltbams-1.0.9.dist-info}/WHEEL +1 -1
- tests/test_io.py +90 -1
- tests/test_andes_mats.py +0 -61
- {ltbams-1.0.7.dist-info → ltbams-1.0.9.dist-info}/entry_points.txt +0 -0
- {ltbams-1.0.7.dist-info → ltbams-1.0.9.dist-info}/top_level.txt +0 -0
ams/io/matpower.py
CHANGED
@@ -2,11 +2,16 @@
|
|
2
2
|
MATPOWER parser.
|
3
3
|
"""
|
4
4
|
import logging
|
5
|
+
import re
|
5
6
|
import numpy as np
|
6
7
|
|
7
|
-
from andes.io
|
8
|
+
from andes.io import read_file_like
|
9
|
+
from andes.io.xlsx import confirm_overwrite
|
8
10
|
from andes.shared import deg2rad, rad2deg
|
9
11
|
|
12
|
+
from ams import __version__ as version
|
13
|
+
from ams.shared import copyright_msg, nowarranty_msg, report_time
|
14
|
+
|
10
15
|
logger = logging.getLogger(__name__)
|
11
16
|
|
12
17
|
|
@@ -29,6 +34,151 @@ def read(system, file):
|
|
29
34
|
return mpc2system(mpc, system)
|
30
35
|
|
31
36
|
|
37
|
+
def m2mpc(infile: str) -> dict:
|
38
|
+
"""
|
39
|
+
Parse a MATPOWER file and return a dictionary containing the parsed data.
|
40
|
+
|
41
|
+
This function processes MATPOWER case files and extracts relevant fields
|
42
|
+
into a structured dictionary. It is revised from ``andes.io.matpower.m2mpc``.
|
43
|
+
|
44
|
+
Supported fields include:
|
45
|
+
- `baseMVA`: The system base power in MVA.
|
46
|
+
- `bus`: Bus data, including voltage, load, and generation information.
|
47
|
+
- `bus_name`: Names of the buses (if available).
|
48
|
+
- `gen`: Generator data, including power limits and voltage setpoints.
|
49
|
+
- `branch`: Branch data, including line impedances and ratings.
|
50
|
+
- `gencost`: Generator cost data (parsed but not used in this implementation).
|
51
|
+
- `areas`: Area data (parsed but not used in this implementation).
|
52
|
+
- `gentype`: Generator type information (if available).
|
53
|
+
- `genfuel`: Generator fuel type information (if available).
|
54
|
+
|
55
|
+
Parameters
|
56
|
+
----------
|
57
|
+
infile : str
|
58
|
+
Path to the MATPOWER file to be parsed.
|
59
|
+
|
60
|
+
Returns
|
61
|
+
-------
|
62
|
+
dict
|
63
|
+
A dictionary containing the parsed MATPOWER data, where keys correspond
|
64
|
+
to MATPOWER struct names and values are numpy arrays or lists.
|
65
|
+
"""
|
66
|
+
|
67
|
+
func = re.compile(r'function\s')
|
68
|
+
mva = re.compile(r'\s*mpc.baseMVA\s*=\s*')
|
69
|
+
bus = re.compile(r'\s*mpc.bus\s*=\s*\[?')
|
70
|
+
gen = re.compile(r'\s*mpc.gen\s*=\s*\[')
|
71
|
+
branch = re.compile(r'\s*mpc.branch\s*=\s*\[')
|
72
|
+
area = re.compile(r'\s*mpc.areas\s*=\s*\[')
|
73
|
+
gencost = re.compile(r'\s*mpc.gencost\s*=\s*\[')
|
74
|
+
bus_name = re.compile(r'\s*mpc.bus_name\s*=\s*{')
|
75
|
+
gentype = re.compile(r'\s*mpc.gentype\s*=\s*{')
|
76
|
+
genfuel = re.compile(r'\s*mpc.genfuel\s*=\s*{')
|
77
|
+
end = re.compile(r'\s*[\];}]')
|
78
|
+
has_digit = re.compile(r'.*\d+\s*]?;?')
|
79
|
+
|
80
|
+
field = None
|
81
|
+
info = True
|
82
|
+
|
83
|
+
mpc = {
|
84
|
+
'version': 2, # not in use
|
85
|
+
'baseMVA': 100,
|
86
|
+
'bus': [],
|
87
|
+
'gen': [],
|
88
|
+
'branch': [],
|
89
|
+
'area': [],
|
90
|
+
'gencost': [],
|
91
|
+
'bus_name': [],
|
92
|
+
'gentype': [],
|
93
|
+
'genfuel': [],
|
94
|
+
}
|
95
|
+
|
96
|
+
input_list = read_file_like(infile)
|
97
|
+
|
98
|
+
for line in input_list:
|
99
|
+
line = line.strip().rstrip(';')
|
100
|
+
if not line:
|
101
|
+
continue
|
102
|
+
elif func.search(line): # skip function declaration
|
103
|
+
continue
|
104
|
+
elif len(line.split('%')[0]) == 0:
|
105
|
+
if info is True:
|
106
|
+
logger.info(line[1:])
|
107
|
+
info = False
|
108
|
+
else:
|
109
|
+
continue
|
110
|
+
elif mva.search(line):
|
111
|
+
mpc["baseMVA"] = float(line.split('=')[1])
|
112
|
+
|
113
|
+
if not field:
|
114
|
+
if bus.search(line):
|
115
|
+
field = 'bus'
|
116
|
+
elif gen.search(line):
|
117
|
+
field = 'gen'
|
118
|
+
elif branch.search(line):
|
119
|
+
field = 'branch'
|
120
|
+
elif area.search(line):
|
121
|
+
field = 'area'
|
122
|
+
elif gencost.search(line):
|
123
|
+
field = 'gencost'
|
124
|
+
elif bus_name.search(line):
|
125
|
+
field = 'bus_name'
|
126
|
+
elif gentype.search(line):
|
127
|
+
field = 'gentype'
|
128
|
+
elif genfuel.search(line):
|
129
|
+
field = 'genfuel'
|
130
|
+
else:
|
131
|
+
continue
|
132
|
+
elif end.search(line):
|
133
|
+
field = None
|
134
|
+
continue
|
135
|
+
|
136
|
+
# parse mpc sections
|
137
|
+
if field:
|
138
|
+
if line.find('=') >= 0:
|
139
|
+
line = line.split('=')[1]
|
140
|
+
if line.find('[') >= 0:
|
141
|
+
line = re.sub(r'\[', '', line)
|
142
|
+
elif line.find('{') >= 0:
|
143
|
+
line = re.sub(r'{', '', line)
|
144
|
+
|
145
|
+
if field in ['bus_name', 'gentype', 'genfuel']:
|
146
|
+
# Handle string-based fields
|
147
|
+
line = line.split(';')
|
148
|
+
data = [i.strip('\'').strip() for i in line if i.strip()]
|
149
|
+
mpc[field].extend(data)
|
150
|
+
else:
|
151
|
+
if not has_digit.search(line):
|
152
|
+
continue
|
153
|
+
line = line.split('%')[0].strip()
|
154
|
+
line = line.split(';')
|
155
|
+
for item in line:
|
156
|
+
if not has_digit.search(item):
|
157
|
+
continue
|
158
|
+
try:
|
159
|
+
data = np.array([float(val) for val in item.split()])
|
160
|
+
except Exception as e:
|
161
|
+
logger.error('Error parsing "%s"', infile)
|
162
|
+
raise e
|
163
|
+
mpc[field].append(data)
|
164
|
+
|
165
|
+
# convert mpc to np array
|
166
|
+
mpc_array = dict()
|
167
|
+
for key, val in mpc.items():
|
168
|
+
if isinstance(val, (float, int)):
|
169
|
+
mpc_array[key] = val
|
170
|
+
elif isinstance(val, list):
|
171
|
+
if len(val) == 0:
|
172
|
+
continue
|
173
|
+
if key in ['bus_name', 'gentype', 'genfuel']:
|
174
|
+
mpc_array[key] = np.array(val, dtype=object)
|
175
|
+
else:
|
176
|
+
mpc_array[key] = np.array(val)
|
177
|
+
else:
|
178
|
+
raise NotImplementedError("Unknown type for mpc, ", type(val))
|
179
|
+
return mpc_array
|
180
|
+
|
181
|
+
|
32
182
|
def mpc2system(mpc: dict, system) -> bool:
|
33
183
|
"""
|
34
184
|
Load an mpc dict into an empty AMS system.
|
@@ -97,7 +247,20 @@ def mpc2system(mpc: dict, system) -> bool:
|
|
97
247
|
mpc_gen[:, 19] = system.PV.Rq.default * base_mva / 60
|
98
248
|
else:
|
99
249
|
mpc_gen = mpc['gen']
|
100
|
-
|
250
|
+
|
251
|
+
# Ensure 'gentype' and 'genfuel' keys exist in mpc, with default values if missing
|
252
|
+
gentype = mpc.get('gentype', [''] * mpc_gen.shape[0])
|
253
|
+
genfuel = mpc.get('genfuel', [''] * mpc_gen.shape[0])
|
254
|
+
|
255
|
+
# Validate lengths of 'gentype' and 'genfuel' against the number of generators
|
256
|
+
if len(gentype) != mpc_gen.shape[0]:
|
257
|
+
raise ValueError(
|
258
|
+
f"'gentype' length ({len(gentype)}) does not match the number of generators ({mpc_gen.shape[0]})")
|
259
|
+
if len(genfuel) != mpc_gen.shape[0]:
|
260
|
+
raise ValueError(
|
261
|
+
f"'genfuel' length ({len(genfuel)}) does not match the number of generators ({mpc_gen.shape[0]})")
|
262
|
+
|
263
|
+
for data, gt, gf in zip(mpc_gen, gentype, genfuel):
|
101
264
|
# bus pg qg qmax qmin vg mbase status pmax pmin
|
102
265
|
# 0 1 2 3 4 5 6 7 8 9
|
103
266
|
# pc1 pc2 qc1min qc1max qc2min qc2max ramp_agc ramp_10
|
@@ -142,7 +305,7 @@ def mpc2system(mpc: dict, system) -> bool:
|
|
142
305
|
Qc2min=qc2min, Qc2max=qc2max,
|
143
306
|
Ragc=ramp_agc, R10=ramp_10,
|
144
307
|
R30=ramp_30, Rq=ramp_q,
|
145
|
-
apf=apf)
|
308
|
+
apf=apf, gentype=gt, genfuel=gf)
|
146
309
|
else:
|
147
310
|
system.add('PV', idx=gen_idx, bus=bus_idx, busr=bus_idx,
|
148
311
|
name=None,
|
@@ -155,7 +318,7 @@ def mpc2system(mpc: dict, system) -> bool:
|
|
155
318
|
Qc2min=qc2min, Qc2max=qc2max,
|
156
319
|
Ragc=ramp_agc, R10=ramp_10,
|
157
320
|
R30=ramp_30, Rq=ramp_q,
|
158
|
-
apf=apf)
|
321
|
+
apf=apf, gentype=gt, genfuel=gf)
|
159
322
|
|
160
323
|
for data in mpc['branch']:
|
161
324
|
# fbus tbus r x b rateA rateB rateC ratio angle
|
@@ -204,9 +367,8 @@ def mpc2system(mpc: dict, system) -> bool:
|
|
204
367
|
gen_idx = np.arange(mpc['gen'].shape[0]) + 1
|
205
368
|
mpc_cost = mpc['gencost']
|
206
369
|
if mpc_cost[0, 0] == 1:
|
207
|
-
logger.warning("Type 1 gencost detected
|
208
|
-
"
|
209
|
-
"Default type 2 cost parameters will be used as a fallback."
|
370
|
+
logger.warning("Type 1 gencost detected, which is not supported in AMS.\n"
|
371
|
+
"Default type 2 cost parameters will be used as a fallback.\n"
|
210
372
|
"It is recommended to manually convert the gencost data to type 2.")
|
211
373
|
mpc_cost = np.repeat(np.array([[2, 0, 0, 3, 0, 0, 0]]),
|
212
374
|
mpc_cost.shape[0], axis=0)
|
@@ -238,14 +400,24 @@ def mpc2system(mpc: dict, system) -> bool:
|
|
238
400
|
c2=c2, c1=c1, c0=c0
|
239
401
|
)
|
240
402
|
|
403
|
+
# --- Area ---
|
404
|
+
area_id = np.unique(system.Bus.area.v).astype(int)
|
405
|
+
for area in area_id:
|
406
|
+
area_idx = f'AREA_{area}'
|
407
|
+
system.add('Area', idx=area_idx, name=area)
|
408
|
+
bus_area = system.Bus.area.v
|
409
|
+
bus_area = [f'AREA_{int(area)}' for area in bus_area]
|
410
|
+
system.Bus.area.v = bus_area
|
411
|
+
|
241
412
|
# --- Zone ---
|
242
413
|
zone_id = np.unique(system.Bus.zone.v).astype(int)
|
243
414
|
for zone in zone_id:
|
244
415
|
zone_idx = f'ZONE_{zone}'
|
245
|
-
system.add('Zone', idx=zone_idx, name=
|
416
|
+
system.add('Zone', idx=zone_idx, name=zone)
|
246
417
|
bus_zone = system.Bus.zone.v
|
247
418
|
bus_zone = [f'ZONE_{int(zone)}' for zone in bus_zone]
|
248
419
|
system.Bus.zone.v = bus_zone
|
420
|
+
|
249
421
|
return True
|
250
422
|
|
251
423
|
|
@@ -271,25 +443,29 @@ def _get_bus_id_caller(bus):
|
|
271
443
|
|
272
444
|
def system2mpc(system) -> dict:
|
273
445
|
"""
|
274
|
-
Convert
|
446
|
+
Convert a **setup** AMS system to a MATPOWER mpc dictionary.
|
447
|
+
|
448
|
+
This function is revised from ``andes.io.matpower.system2mpc``.
|
275
449
|
|
276
|
-
In the ``gen`` section, slack generators
|
450
|
+
In the ``gen`` section, slack generators are listed before PV generators.
|
277
451
|
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
452
|
+
In the converted MPC, the indices of area (bus[:, 6]) and zone (bus[:, 10])
|
453
|
+
may differ from the original MPC. However, the mapping relationship is preserved.
|
454
|
+
For example, if the original MPC numbers areas starting from 1, the converted
|
455
|
+
MPC may number them starting from 0.
|
456
|
+
|
457
|
+
The coefficients ``c2`` and ``c1`` in the generator cost data are scaled by
|
458
|
+
``base_mva`` to match MATPOWER's unit convention (MW).
|
283
459
|
|
284
460
|
Parameters
|
285
461
|
----------
|
286
462
|
system : ams.core.system.System
|
287
|
-
AMS system
|
463
|
+
The AMS system to be converted.
|
288
464
|
|
289
465
|
Returns
|
290
466
|
-------
|
291
|
-
mpc: dict
|
292
|
-
MATPOWER
|
467
|
+
mpc : dict
|
468
|
+
A dictionary in MATPOWER format representing the converted AMS system.
|
293
469
|
"""
|
294
470
|
|
295
471
|
mpc = dict(version='2',
|
@@ -300,12 +476,16 @@ def system2mpc(system) -> dict:
|
|
300
476
|
gencost=np.zeros((system.GCost.n, 7), dtype=np.float64),
|
301
477
|
)
|
302
478
|
|
479
|
+
if not system.is_setup:
|
480
|
+
logger.warning("System is not setup and will be setup now.")
|
481
|
+
system.setup()
|
482
|
+
|
303
483
|
if system.Bus.name.v is not None:
|
304
484
|
mpc['bus_name'] = system.Bus.name.v
|
305
485
|
|
306
486
|
base_mva = system.config.mva
|
307
487
|
|
308
|
-
# ---
|
488
|
+
# --- Bus ---
|
309
489
|
bus = mpc['bus']
|
310
490
|
gen = mpc['gen']
|
311
491
|
|
@@ -313,18 +493,16 @@ def system2mpc(system) -> dict:
|
|
313
493
|
|
314
494
|
bus[:, 0] = to_busid(system.Bus.idx.v)
|
315
495
|
bus[:, 1] = 1
|
496
|
+
if system.Area.n > 0:
|
497
|
+
bus[:, 6] = system.Area.idx2uid(system.Bus.area.v)
|
316
498
|
bus[:, 7] = system.Bus.v0.v
|
317
499
|
bus[:, 8] = system.Bus.a0.v * rad2deg
|
318
500
|
bus[:, 9] = system.Bus.Vn.v
|
501
|
+
if system.Zone.n > 0:
|
502
|
+
bus[:, 10] = system.Zone.idx2uid(system.Bus.zone.v)
|
319
503
|
bus[:, 11] = system.Bus.vmax.v
|
320
504
|
bus[:, 12] = system.Bus.vmin.v
|
321
505
|
|
322
|
-
# --- zone ---
|
323
|
-
ZONE_I = system.Zone.idx.v
|
324
|
-
if len(ZONE_I) > 0:
|
325
|
-
mapping = {busi0: i for i, busi0 in enumerate(ZONE_I)}
|
326
|
-
bus[:, 10] = np.array([mapping[busi0] for busi0 in system.Bus.zone.v])
|
327
|
-
|
328
506
|
# --- PQ ---
|
329
507
|
if system.PQ.n > 0:
|
330
508
|
pq_pos = system.Bus.idx2uid(system.PQ.bus.v)
|
@@ -410,4 +588,136 @@ def system2mpc(system) -> dict:
|
|
410
588
|
else:
|
411
589
|
mpc.pop('gencost')
|
412
590
|
|
591
|
+
# --- gentype ---
|
592
|
+
stg = system.StaticGen.get_all_idxes()
|
593
|
+
gentype = system.StaticGen.get(src='gentype', attr='v', idx=stg)
|
594
|
+
if any(gentype):
|
595
|
+
mpc['gentype'] = np.array(gentype)
|
596
|
+
|
597
|
+
# --- genfuel ---
|
598
|
+
genfuel = system.StaticGen.get(src='genfuel', attr='v', idx=stg)
|
599
|
+
if any(genfuel):
|
600
|
+
mpc['genfuel'] = np.array(genfuel)
|
601
|
+
|
602
|
+
# --- Bus Name ---
|
603
|
+
if any(system.Bus.name.v):
|
604
|
+
mpc['bus_name'] = np.array(system.Bus.name.v)
|
605
|
+
|
413
606
|
return mpc
|
607
|
+
|
608
|
+
|
609
|
+
def mpc2m(mpc: dict, outfile: str) -> str:
|
610
|
+
"""
|
611
|
+
Write a MATPOWER mpc dict to a M-file.
|
612
|
+
|
613
|
+
Parameters
|
614
|
+
----------
|
615
|
+
mpc : dict
|
616
|
+
MATPOWER mpc dictionary.
|
617
|
+
outfile : str
|
618
|
+
Path to the output M-file.
|
619
|
+
"""
|
620
|
+
with open(outfile, 'w') as f:
|
621
|
+
# Add version info
|
622
|
+
f.write(f"%% Converted by AMS {version}\n")
|
623
|
+
f.write(f"%% {copyright_msg}\n\n")
|
624
|
+
f.write(f"%% {nowarranty_msg}\n")
|
625
|
+
f.write(f"%% Convert time: {report_time}\n\n")
|
626
|
+
|
627
|
+
f.write("function mpc = mpc_case\n")
|
628
|
+
f.write("mpc.version = '2';\n\n")
|
629
|
+
|
630
|
+
# Write baseMVA
|
631
|
+
f.write(f"%% system MVA base\nmpc.baseMVA = {mpc['baseMVA']};\n\n")
|
632
|
+
|
633
|
+
# Write bus data
|
634
|
+
f.write("%% bus data\n")
|
635
|
+
f.write("%% bus_i type Pd Qd Gs Bs area Vm Va baseKV zone Vmax Vmin\n")
|
636
|
+
f.write("mpc.bus = [\n")
|
637
|
+
for row in mpc['bus']:
|
638
|
+
f.write(" " + "\t".join(f"{val:.6g}" for val in row) + ";\n")
|
639
|
+
f.write("];\n\n")
|
640
|
+
|
641
|
+
# Write generator data
|
642
|
+
f.write("%% generator data\n")
|
643
|
+
f.write("%% bus Pg Qg Qmax Qmin Vg mBase status Pmax Pmin\n")
|
644
|
+
f.write("%% Pc1 Pc2 Qc1min Qc1max Qc2min Qc2max ramp_agc ramp_10 ramp_30 ramp_q apf\n")
|
645
|
+
f.write("mpc.gen = [\n")
|
646
|
+
for row in mpc['gen']:
|
647
|
+
f.write(" " + "\t".join(f"{val:.6g}" for val in row) + ";\n")
|
648
|
+
f.write("];\n\n")
|
649
|
+
|
650
|
+
# Write branch data
|
651
|
+
f.write("%% branch data\n")
|
652
|
+
f.write("%% fbus tbus r x b rateA rateB rateC ratio angle status angmin angmax PF QF PT QT\n")
|
653
|
+
f.write("mpc.branch = [\n")
|
654
|
+
for row in mpc['branch']:
|
655
|
+
f.write(" " + "\t".join(f"{val:.6g}" for val in row) + ";\n")
|
656
|
+
f.write("];\n\n")
|
657
|
+
|
658
|
+
# Write generator cost data if available
|
659
|
+
if 'gencost' in mpc:
|
660
|
+
f.write("%% generator cost data\n")
|
661
|
+
f.write("%% 1 startup shutdown n x1 y1 ... xn yn\n")
|
662
|
+
f.write("%% 2 startup shutdown n c(n-1) ... c0\n")
|
663
|
+
f.write("mpc.gencost = [\n")
|
664
|
+
for row in mpc['gencost']:
|
665
|
+
f.write(" " + "\t".join(f"{val:.6g}" for val in row) + ";\n")
|
666
|
+
f.write("];\n\n")
|
667
|
+
|
668
|
+
# Write bus names if available and not all None
|
669
|
+
if 'bus_name' in mpc and any(mpc['bus_name']):
|
670
|
+
f.write("%% bus names\n")
|
671
|
+
f.write("mpc.bus_name = {\n")
|
672
|
+
for name in mpc['bus_name']:
|
673
|
+
f.write(f" '{name}';\n")
|
674
|
+
f.write("};\n\n")
|
675
|
+
|
676
|
+
# Write generator types if available and not all None
|
677
|
+
if 'gentype' in mpc and any(mpc['gentype']):
|
678
|
+
f.write("%% generator types\n")
|
679
|
+
f.write("mpc.gentype = {\n")
|
680
|
+
for gentype in mpc['gentype']:
|
681
|
+
f.write(f" '{gentype}';\n")
|
682
|
+
f.write("};\n\n")
|
683
|
+
|
684
|
+
# Write generator fuels if available and not all None
|
685
|
+
if 'genfuel' in mpc and any(mpc['genfuel']):
|
686
|
+
f.write("%% generator fuels\n")
|
687
|
+
f.write("mpc.genfuel = {\n")
|
688
|
+
for genfuel in mpc['genfuel']:
|
689
|
+
f.write(f" '{genfuel}';\n")
|
690
|
+
f.write("};\n\n")
|
691
|
+
|
692
|
+
logger.info(f"Finished writing MATPOWER case to {outfile}")
|
693
|
+
return outfile
|
694
|
+
|
695
|
+
|
696
|
+
def write(system, outfile: str, overwrite: bool = None) -> bool:
|
697
|
+
"""
|
698
|
+
Export an AMS system to a MATPOWER mpc file.
|
699
|
+
|
700
|
+
This function converts an AMS system object into a MATPOWER-compatible
|
701
|
+
mpc dictionary and writes it to a specified output file in MATPOWER format.
|
702
|
+
|
703
|
+
Parameters
|
704
|
+
----------
|
705
|
+
system : ams.system.System
|
706
|
+
A loaded system.
|
707
|
+
outfile : str
|
708
|
+
Path to the output file.
|
709
|
+
overwrite : bool, optional
|
710
|
+
None to prompt for overwrite selection; True to overwrite; False to not overwrite.
|
711
|
+
|
712
|
+
Returns
|
713
|
+
-------
|
714
|
+
bool
|
715
|
+
True if the file was successfully written, False otherwise.
|
716
|
+
"""
|
717
|
+
if not confirm_overwrite(outfile, overwrite=overwrite):
|
718
|
+
return False
|
719
|
+
|
720
|
+
mpc = system2mpc(system)
|
721
|
+
mpc2m(mpc, outfile)
|
722
|
+
logger.info('MATPOWER m case file written to "%s"', outfile)
|
723
|
+
return True
|
ams/io/psse.py
CHANGED
ams/io/pypower.py
CHANGED
@@ -101,3 +101,17 @@ def system2ppc(system) -> dict:
|
|
101
101
|
mpc['branch'][:, 0] = np.array([bus_map[busi0] for busi0 in mpc['branch'][:, 0].astype(int)])
|
102
102
|
mpc['branch'][:, 1] = np.array([bus_map[busi0] for busi0 in mpc['branch'][:, 1].astype(int)])
|
103
103
|
return mpc
|
104
|
+
|
105
|
+
|
106
|
+
def ppc2py(ppc: dict, outfile: str) -> str:
|
107
|
+
"""
|
108
|
+
Placeholder.
|
109
|
+
"""
|
110
|
+
return NotImplemented
|
111
|
+
|
112
|
+
|
113
|
+
def write(system, outfile: str) -> bool:
|
114
|
+
"""
|
115
|
+
Placeholder.
|
116
|
+
"""
|
117
|
+
return NotImplemented
|
ams/models/group.py
CHANGED
@@ -175,20 +175,22 @@ class Reserve(GroupBase):
|
|
175
175
|
|
176
176
|
class StaticGen(GroupBase):
|
177
177
|
"""
|
178
|
-
Generator
|
178
|
+
Static Generator Group.
|
179
|
+
|
180
|
+
The generator types and fuel types are referenced from MATPOWER.
|
179
181
|
|
180
182
|
Notes
|
181
183
|
-----
|
182
|
-
For co-simulation with ANDES,
|
183
|
-
|
184
|
-
|
184
|
+
For co-simulation with ANDES, refer to the `ANDES StaticGen Documentation
|
185
|
+
<https://docs.andes.app/en/latest/groupdoc/StaticGen.html#staticgen>`_ for
|
186
|
+
replacing static generators with dynamic generators.
|
185
187
|
"""
|
186
188
|
|
187
189
|
def __init__(self):
|
188
190
|
super().__init__()
|
189
191
|
self.common_params.extend(('bus', 'Sn', 'Vn', 'p0', 'q0', 'ra', 'xs', 'subidx',
|
190
192
|
'pmax', 'pmin', 'pg0', 'ctrl', 'R10', 'td1', 'td2',
|
191
|
-
'area', 'zone'))
|
193
|
+
'area', 'zone', 'gentype', 'genfuel'))
|
192
194
|
self.common_vars.extend(('p', 'q'))
|
193
195
|
|
194
196
|
|
ams/models/line.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
from andes.models.line.line import LineData
|
2
2
|
from andes.models.line.jumper import JumperData
|
3
3
|
from andes.core.param import NumParam
|
4
|
-
from andes.shared import deg2rad
|
4
|
+
from andes.shared import deg2rad
|
5
5
|
|
6
6
|
from ams.core.model import Model
|
7
7
|
|
@@ -49,167 +49,6 @@ class Line(LineData, Model):
|
|
49
49
|
self.a1a = None
|
50
50
|
self.a2a = None
|
51
51
|
|
52
|
-
# NOTE: following code are minly copied from `andes.models.line.Line`
|
53
|
-
# and they are not fully verified
|
54
|
-
# potential issues:
|
55
|
-
# `build_Bp` contains 'fdxb', which is not included in the input parameters,
|
56
|
-
# and the results are the negative of `Bbus` from `makeBdc` in PYPOWER
|
57
|
-
# `build_Bpp` ignores the line resistance for all three methods
|
58
|
-
# `build_Bdc` results are the negative of `Bbus` from `makeBdc` in PYPOWER
|
59
|
-
# `build_y` results have inignorable differences at diagonal elements with `makeYbus` in PYPOWER
|
60
|
-
|
61
|
-
def build_y(self):
|
62
|
-
"""
|
63
|
-
Build bus admittance matrix. Copied from ``andes.models.line.line.Line``.
|
64
|
-
|
65
|
-
Returns
|
66
|
-
-------
|
67
|
-
Y : spmatrix
|
68
|
-
Bus admittance matrix.
|
69
|
-
"""
|
70
|
-
|
71
|
-
nb = self.system.Bus.n
|
72
|
-
|
73
|
-
y1 = self.u.v * (self.g1.v + self.b1.v * 1j)
|
74
|
-
y2 = self.u.v * (self.g2.v + self.b2.v * 1j)
|
75
|
-
y12 = self.u.v / (self.r.v + self.x.v * 1j)
|
76
|
-
m = self.tap.v * np.exp(1j * self.phi.v)
|
77
|
-
m2 = self.tap.v**2
|
78
|
-
mconj = np.conj(m)
|
79
|
-
|
80
|
-
# build self and mutual admittances into Y
|
81
|
-
Y = spmatrix((y12 + y1 / m2), self.a1a, self.a1a, (nb, nb), 'z')
|
82
|
-
Y -= spmatrix(y12 / mconj, self.a1a, self.a2a, (nb, nb), 'z')
|
83
|
-
Y -= spmatrix(y12 / m, self.a2a, self.a1a, (nb, nb), 'z')
|
84
|
-
Y += spmatrix(y12 + y2, self.a2a, self.a2a, (nb, nb), 'z')
|
85
|
-
|
86
|
-
return Y
|
87
|
-
|
88
|
-
def build_Bp(self, method='fdpf'):
|
89
|
-
"""
|
90
|
-
Function for building B' matrix.
|
91
|
-
|
92
|
-
Parameters
|
93
|
-
----------
|
94
|
-
method : str
|
95
|
-
Method for building B' matrix. Choose from 'fdpf', 'fdbx', 'dcpf'.
|
96
|
-
|
97
|
-
Returns
|
98
|
-
-------
|
99
|
-
Bp : spmatrix
|
100
|
-
B' matrix.
|
101
|
-
"""
|
102
|
-
nb = self.system.Bus.n
|
103
|
-
|
104
|
-
if method not in ("fdpf", "fdbx", "dcpf"):
|
105
|
-
raise ValueError(f"Invalid method {method}; choose from 'fdpf', 'fdbx', 'dcpf'")
|
106
|
-
|
107
|
-
# Build B prime matrix -- FDPF
|
108
|
-
# `y1`` neglects line charging shunt, and g1 is usually 0 in HV lines
|
109
|
-
# `y2`` neglects line charging shunt, and g2 is usually 0 in HV lines
|
110
|
-
y1 = self.u.v * self.g1.v
|
111
|
-
y2 = self.u.v * self.g2.v
|
112
|
-
|
113
|
-
# `m` neglected tap ratio
|
114
|
-
m = np.exp(self.phi.v * 1j)
|
115
|
-
mconj = np.conj(m)
|
116
|
-
m2 = np.ones(self.n)
|
117
|
-
|
118
|
-
if method in ('fdxb', 'dcpf'):
|
119
|
-
# neglect line resistance in Bp in XB method
|
120
|
-
y12 = self.u.v / (self.x.v * 1j)
|
121
|
-
else:
|
122
|
-
y12 = self.u.v / (self.r.v + self.x.v * 1j)
|
123
|
-
|
124
|
-
Bdc = spmatrix((y12 + y1) / m2, self.a1a, self.a1a, (nb, nb), 'z')
|
125
|
-
Bdc -= spmatrix(y12 / mconj, self.a1a, self.a2a, (nb, nb), 'z')
|
126
|
-
Bdc -= spmatrix(y12 / m, self.a2a, self.a1a, (nb, nb), 'z')
|
127
|
-
Bdc += spmatrix(y12 + y2, self.a2a, self.a2a, (nb, nb), 'z')
|
128
|
-
Bdc = Bdc.imag()
|
129
|
-
|
130
|
-
for item in range(nb):
|
131
|
-
if abs(Bdc[item, item]) == 0:
|
132
|
-
Bdc[item, item] = 1e-6 + 0j
|
133
|
-
|
134
|
-
return Bdc
|
135
|
-
|
136
|
-
def build_Bpp(self, method='fdpf'):
|
137
|
-
"""
|
138
|
-
Function for building B'' matrix.
|
139
|
-
|
140
|
-
Parameters
|
141
|
-
----------
|
142
|
-
method : str
|
143
|
-
Method for building B'' matrix. Choose from 'fdpf', 'fdbx', 'dcpf'.
|
144
|
-
|
145
|
-
Returns
|
146
|
-
-------
|
147
|
-
Bpp : spmatrix
|
148
|
-
B'' matrix.
|
149
|
-
"""
|
150
|
-
|
151
|
-
nb = self.system.Bus.n
|
152
|
-
|
153
|
-
if method not in ("fdpf", "fdbx", "dcpf"):
|
154
|
-
raise ValueError(f"Invalid method {method}; choose from 'fdpf', 'fdbx', 'dcpf'")
|
155
|
-
|
156
|
-
# Build B double prime matrix
|
157
|
-
# y1 neglected line charging shunt, and g1 is usually 0 in HV lines
|
158
|
-
# y2 neglected line charging shunt, and g2 is usually 0 in HV lines
|
159
|
-
# m neglected phase shifter
|
160
|
-
y1 = self.u.v * (self.g1.v + self.b1.v * 1j)
|
161
|
-
y2 = self.u.v * (self.g2.v + self.b2.v * 1j)
|
162
|
-
|
163
|
-
m = self.tap.v
|
164
|
-
m2 = abs(m)**2
|
165
|
-
|
166
|
-
if method in ('fdbx', 'fdpf', 'dcpf'):
|
167
|
-
# neglect line resistance in Bpp in BX method
|
168
|
-
y12 = self.u.v / (self.x.v * 1j)
|
169
|
-
else:
|
170
|
-
y12 = self.u.v / (self.r.v + self.x.v * 1j)
|
171
|
-
|
172
|
-
Bpp = spmatrix((y12 + y1) / m2, self.a1a, self.a1a, (nb, nb), 'z')
|
173
|
-
Bpp -= spmatrix(y12 / np.conj(m), self.a1a, self.a2a, (nb, nb), 'z')
|
174
|
-
Bpp -= spmatrix(y12 / m, self.a2a, self.a1a, (nb, nb), 'z')
|
175
|
-
Bpp += spmatrix(y12 + y2, self.a2a, self.a2a, (nb, nb), 'z')
|
176
|
-
Bpp = Bpp.imag()
|
177
|
-
|
178
|
-
for item in range(nb):
|
179
|
-
if abs(Bpp[item, item]) == 0:
|
180
|
-
Bpp[item, item] = 1e-6 + 0j
|
181
|
-
|
182
|
-
return Bpp
|
183
|
-
|
184
|
-
def build_Bdc(self):
|
185
|
-
"""
|
186
|
-
The MATPOWER-flavor Bdc matrix for DC power flow.
|
187
|
-
|
188
|
-
The method neglects line charging and line resistance. It retains tap ratio.
|
189
|
-
|
190
|
-
Returns
|
191
|
-
-------
|
192
|
-
Bdc : spmatrix
|
193
|
-
Bdc matrix.
|
194
|
-
"""
|
195
|
-
|
196
|
-
nb = self.system.Bus.n
|
197
|
-
|
198
|
-
y12 = self.u.v / (self.x.v * 1j)
|
199
|
-
y12 = y12 / self.tap.v
|
200
|
-
|
201
|
-
Bdc = spmatrix(y12, self.a1a, self.a1a, (nb, nb), 'z')
|
202
|
-
Bdc -= spmatrix(y12, self.a1a, self.a2a, (nb, nb), 'z')
|
203
|
-
Bdc -= spmatrix(y12, self.a2a, self.a1a, (nb, nb), 'z')
|
204
|
-
Bdc += spmatrix(y12, self.a2a, self.a2a, (nb, nb), 'z')
|
205
|
-
Bdc = Bdc.imag()
|
206
|
-
|
207
|
-
for item in range(nb):
|
208
|
-
if abs(Bdc[item, item]) == 0:
|
209
|
-
Bdc[item, item] = 1e-6
|
210
|
-
|
211
|
-
return Bdc
|
212
|
-
|
213
52
|
|
214
53
|
class Jumper(JumperData, Model):
|
215
54
|
"""
|