ltbams 1.0.8__py3-none-any.whl → 1.0.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. ams/__init__.py +0 -1
  2. ams/_version.py +3 -3
  3. ams/cases/5bus/pjm5bus_demo.json +1324 -0
  4. ams/core/__init__.py +1 -0
  5. ams/core/common.py +30 -0
  6. ams/core/model.py +1 -1
  7. ams/core/symprocessor.py +1 -1
  8. ams/extension/eva.py +1 -1
  9. ams/interface.py +40 -24
  10. ams/io/matpower.py +192 -26
  11. ams/io/psse.py +278 -1
  12. ams/io/pypower.py +14 -0
  13. ams/main.py +2 -2
  14. ams/models/group.py +2 -70
  15. ams/models/static/pq.py +7 -3
  16. ams/opt/param.py +1 -2
  17. ams/report.py +3 -4
  18. ams/routines/__init__.py +2 -3
  19. ams/routines/acopf.py +5 -108
  20. ams/routines/dcopf.py +8 -0
  21. ams/routines/dcpf.py +1 -1
  22. ams/routines/ed.py +4 -2
  23. ams/routines/grbopt.py +150 -0
  24. ams/routines/pflow.py +2 -2
  25. ams/routines/pypower.py +631 -0
  26. ams/routines/routine.py +4 -10
  27. ams/routines/uc.py +2 -2
  28. ams/shared.py +30 -44
  29. ams/system.py +118 -2
  30. docs/source/api.rst +2 -0
  31. docs/source/getting_started/formats/matpower.rst +135 -0
  32. docs/source/getting_started/formats/pypower.rst +1 -2
  33. docs/source/getting_started/install.rst +9 -6
  34. docs/source/images/dcopf_time.png +0 -0
  35. docs/source/images/educ_pie.png +0 -0
  36. docs/source/release-notes.rst +29 -47
  37. {ltbams-1.0.8.dist-info → ltbams-1.0.10.dist-info}/METADATA +87 -47
  38. {ltbams-1.0.8.dist-info → ltbams-1.0.10.dist-info}/RECORD +58 -75
  39. {ltbams-1.0.8.dist-info → ltbams-1.0.10.dist-info}/WHEEL +1 -1
  40. tests/test_1st_system.py +1 -1
  41. tests/test_case.py +14 -14
  42. tests/test_export_csv.py +1 -1
  43. tests/test_interface.py +24 -2
  44. tests/test_io.py +125 -1
  45. tests/test_omodel.py +1 -1
  46. tests/test_report.py +6 -6
  47. tests/test_routine.py +2 -2
  48. tests/test_rtn_acopf.py +75 -0
  49. tests/test_rtn_dcopf.py +1 -1
  50. tests/test_rtn_dcopf2.py +1 -1
  51. tests/test_rtn_ed.py +9 -9
  52. tests/test_rtn_opf.py +142 -0
  53. tests/test_rtn_pflow.py +0 -72
  54. tests/test_rtn_pypower.py +315 -0
  55. tests/test_rtn_rted.py +8 -8
  56. tests/test_rtn_uc.py +18 -18
  57. ams/pypower/__init__.py +0 -8
  58. ams/pypower/_compat.py +0 -9
  59. ams/pypower/core/__init__.py +0 -8
  60. ams/pypower/core/pips.py +0 -894
  61. ams/pypower/core/ppoption.py +0 -244
  62. ams/pypower/core/ppver.py +0 -18
  63. ams/pypower/core/solver.py +0 -2451
  64. ams/pypower/eps.py +0 -6
  65. ams/pypower/idx.py +0 -174
  66. ams/pypower/io.py +0 -604
  67. ams/pypower/make/__init__.py +0 -11
  68. ams/pypower/make/matrices.py +0 -665
  69. ams/pypower/make/pdv.py +0 -506
  70. ams/pypower/routines/__init__.py +0 -7
  71. ams/pypower/routines/cpf.py +0 -513
  72. ams/pypower/routines/cpf_callbacks.py +0 -114
  73. ams/pypower/routines/opf.py +0 -1803
  74. ams/pypower/routines/opffcns.py +0 -1946
  75. ams/pypower/routines/pflow.py +0 -852
  76. ams/pypower/toggle.py +0 -1098
  77. ams/pypower/utils.py +0 -293
  78. ams/routines/cpf.py +0 -65
  79. ams/routines/dcpf0.py +0 -196
  80. ams/routines/pflow0.py +0 -113
  81. tests/test_rtn_dcpf.py +0 -77
  82. {ltbams-1.0.8.dist-info → ltbams-1.0.10.dist-info}/entry_points.txt +0 -0
  83. {ltbams-1.0.8.dist-info → ltbams-1.0.10.dist-info}/top_level.txt +0 -0
ams/core/__init__.py CHANGED
@@ -1,2 +1,3 @@
1
1
  from ams.core.model import Model # NOQA
2
2
  from ams.core.var import Algeb # NOQA
3
+ from ams.core.common import Config # NOQA
ams/core/common.py ADDED
@@ -0,0 +1,30 @@
1
+ import logging
2
+
3
+ from andes.core.common import Config as AndesConfig
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+
8
+ class Config(AndesConfig):
9
+ """
10
+ A class for storing configuration, can be used in system,
11
+ model, routine, and other modules.
12
+
13
+ Revised from `andes.core.common.Config`.
14
+ """
15
+
16
+ def __init__(self, name, dct=None, **kwargs):
17
+ super().__init__(name, dct, **kwargs)
18
+
19
+ def update(self, dct: dict = None, **kwargs):
20
+ """
21
+ Update the configuration from a file.
22
+ """
23
+ if dct is not None:
24
+ kwargs.update(dct)
25
+
26
+ for key, val in kwargs.items():
27
+ self._set(key, val)
28
+ self._dict[key] = val
29
+
30
+ self.check()
ams/core/model.py CHANGED
@@ -7,13 +7,13 @@ from collections import OrderedDict
7
7
  from typing import Iterable
8
8
 
9
9
  import numpy as np
10
- from andes.core.common import Config
11
10
  from andes.core.param import ExtParam
12
11
  from andes.core.service import BaseService, BackRef
13
12
  from andes.utils.func import list_flatten
14
13
 
15
14
  from ams.core.documenter import Documenter
16
15
  from ams.core.var import Algeb
16
+ from ams.core.common import Config
17
17
 
18
18
  from ams.utils.misc import deprec_get_idx
19
19
 
ams/core/symprocessor.py CHANGED
@@ -174,7 +174,7 @@ class SymProcessor:
174
174
  # Constraints
175
175
  # NOTE: constraints are included in sub_map for ExpressionCalc
176
176
  # thus, they don't have the suffix `.v`
177
- for cname, constraint in self.parent.constrs.items():
177
+ for cname, _ in self.parent.constrs.items():
178
178
  self.sub_map[rf"\b{cname}\b"] = f'self.rtn.{cname}.optz'
179
179
 
180
180
  # store tex names defined in `self.config`
ams/extension/eva.py CHANGED
@@ -17,7 +17,6 @@ from collections import OrderedDict
17
17
 
18
18
  import scipy.stats as stats
19
19
 
20
- from andes.core import Config
21
20
  from andes.core.param import NumParam
22
21
  from andes.core.model import ModelData
23
22
  from andes.shared import np, pd
@@ -25,6 +24,7 @@ from andes.utils.misc import elapsed
25
24
 
26
25
  from ams.core.model import Model
27
26
  from ams.utils.paths import ams_root
27
+ from ams.core import Config
28
28
 
29
29
  logger = logging.getLogger(__name__)
30
30
 
ams/interface.py CHANGED
@@ -3,6 +3,7 @@ Module for interfacing ANDES
3
3
  """
4
4
 
5
5
  import os
6
+ import json
6
7
  import logging
7
8
  from collections import OrderedDict, Counter
8
9
 
@@ -81,7 +82,8 @@ def sync_adsys(amsys, adsys):
81
82
  try:
82
83
  ad_mdl.set(src=param, attr='v', idx=idx,
83
84
  value=am_mdl.get(src=param, attr='v', idx=idx))
84
- except Exception:
85
+ except Exception as e:
86
+ logger.warning(f"Failed to sync parameter '{param}' for model '{mname}': {e}")
85
87
  continue
86
88
  return True
87
89
 
@@ -242,24 +244,43 @@ def parse_addfile(adsys, amsys, addfile):
242
244
  logger.debug('Addfile format guessed as %s.', key)
243
245
  break
244
246
 
245
- if key != 'xlsx':
246
- logger.error('Addfile format "%s" is not supported yet.', add_format)
247
- # FIXME: xlsx input file with dyr addfile result into KeyError: 'Toggle'
248
- # add_parser = importlib.import_module('andes.io.' + add_format)
249
- # if not add_parser.read_add(system, addfile):
250
- # logger.error('Error parsing addfile "%s" with %s parser.', addfile, add_format)
251
- return adsys
247
+ # FIXME: xlsx input file with dyr addfile result into KeyError: 'Toggle'
248
+ # add_parser = importlib.import_module('andes.io.' + add_format)
249
+ # if not add_parser.read_add(system, addfile):
250
+ # logger.error('Error parsing addfile "%s" with %s parser.', addfile, add_format)
252
251
 
253
252
  # Try parsing the addfile
254
253
  logger.info('Parsing additional file "%s"...', addfile)
255
254
 
256
- reader = pd.ExcelFile(addfile)
255
+ if add_format == 'xlsx':
256
+ reads = pd.read_excel(addfile,
257
+ sheet_name=None,
258
+ index_col=0,
259
+ engine='openpyxl',
260
+ )
261
+ elif add_format == 'json':
262
+ if isinstance(addfile, str):
263
+ f = open(addfile, 'r')
264
+ else:
265
+ f = addfile
266
+
267
+ json_in = json.load(f)
268
+
269
+ if f is not addfile:
270
+ f.close()
271
+
272
+ reads = dict()
273
+ for keys, dct in json_in.items():
274
+ reads[keys] = pd.DataFrame(dct)
275
+ else:
276
+ logger.error("Unsupported addfile format, only 'xlsx' and 'json' formats are supported.")
277
+ return adsys
257
278
 
258
279
  pflow_mdl = list(pflow_dict.keys())
259
280
 
260
281
  pflow_mdls_overlap = []
261
282
  for mdl_name in pflow_dict.keys():
262
- if mdl_name in reader.sheet_names:
283
+ if mdl_name in reads.keys():
263
284
  pflow_mdls_overlap.append(mdl_name)
264
285
 
265
286
  if len(pflow_mdls_overlap) > 0:
@@ -269,14 +290,10 @@ def parse_addfile(adsys, amsys, addfile):
269
290
 
270
291
  pflow_mdl_nonempty = [mdl for mdl in pflow_mdl if amsys.models[mdl].n > 0]
271
292
  logger.debug(f"Non-empty PFlow models: {pflow_mdl}")
272
- pflow_df_models = pd.read_excel(addfile,
273
- sheet_name=pflow_mdl_nonempty,
274
- index_col=0,
275
- engine='openpyxl',
276
- )
277
- # drop rows that all nan
278
- for name, df in pflow_df_models.items():
279
- df.dropna(axis=0, how='all', inplace=True)
293
+ pflow_df_models = {}
294
+ for key, df in reads.items():
295
+ if key in pflow_mdl_nonempty:
296
+ pflow_df_models[key] = df.dropna(axis=0, how='all', inplace=False)
280
297
 
281
298
  # collect idx_map if difference exists
282
299
  idx_map = OrderedDict([])
@@ -290,13 +307,12 @@ def parse_addfile(adsys, amsys, addfile):
290
307
  idx_map[name] = dict(zip(ad_idx, am_idx))
291
308
 
292
309
  # --- dynamic models to be added ---
293
- mdl_to_keep = list(set(reader.sheet_names) - set(pflow_mdl))
310
+ mdl_to_keep = list(set(reads.keys()) - set(pflow_mdl))
294
311
  mdl_to_keep.sort(key=str.lower)
295
- df_models = pd.read_excel(addfile,
296
- sheet_name=mdl_to_keep,
297
- index_col=0,
298
- engine='openpyxl',
299
- )
312
+ df_models = {}
313
+ for key, df in reads.items():
314
+ if key in mdl_to_keep:
315
+ df_models[key] = df.dropna(axis=0, how='all', inplace=False)
300
316
 
301
317
  # adjust models index
302
318
  for name, df in df_models.items():
ams/io/matpower.py CHANGED
@@ -6,8 +6,12 @@ import re
6
6
  import numpy as np
7
7
 
8
8
  from andes.io import read_file_like
9
+ from andes.io.xlsx import confirm_overwrite
9
10
  from andes.shared import deg2rad, rad2deg
10
11
 
12
+ from ams import __version__ as version
13
+ from ams.shared import copyright_msg, nowarranty_msg, report_time
14
+
11
15
  logger = logging.getLogger(__name__)
12
16
 
13
17
 
@@ -184,6 +188,9 @@ def mpc2system(mpc: dict, system) -> bool:
184
188
  Note that `mbase` in mpc is converted to `Sn`, but it is not actually used in
185
189
  MATPOWER nor AMS.
186
190
 
191
+ In converted AMS system, StaticGen idxes are 1-based, while the sequence follow
192
+ the order of the original MATPOWER data.
193
+
187
194
  Parameters
188
195
  ----------
189
196
  system : ams.system.System
@@ -396,14 +403,29 @@ def mpc2system(mpc: dict, system) -> bool:
396
403
  c2=c2, c1=c1, c0=c0
397
404
  )
398
405
 
406
+ # --- Area ---
407
+ area = system.Bus.area.v
408
+ area_map = {}
409
+ if area:
410
+ for a in set(area):
411
+ a_new = system.add('Area',
412
+ param_dict=dict(idx=a, name=a))
413
+ area_map[a] = a_new
414
+ system.Bus.area.v = [area_map[a] for a in area]
415
+
399
416
  # --- Zone ---
400
- zone_id = np.unique(system.Bus.zone.v).astype(int)
401
- for zone in zone_id:
402
- zone_idx = f'ZONE_{zone}'
403
- system.add('Zone', idx=zone_idx, name=None)
404
- bus_zone = system.Bus.zone.v
405
- bus_zone = [f'ZONE_{int(zone)}' for zone in bus_zone]
406
- system.Bus.zone.v = bus_zone
417
+ zone = system.Bus.zone.v
418
+ zone_map = {}
419
+ if zone:
420
+ n_zone = system.Area.n
421
+ for z in set(zone):
422
+ z_new = system.add('Zone',
423
+ param_dict=dict(idx=int(n_zone + 1),
424
+ name=f'{n_zone + 1}'))
425
+ zone_map[z] = z_new
426
+ n_zone += 1
427
+ system.Bus.zone.v = [zone_map[z] for z in zone]
428
+
407
429
  return True
408
430
 
409
431
 
@@ -429,25 +451,29 @@ def _get_bus_id_caller(bus):
429
451
 
430
452
  def system2mpc(system) -> dict:
431
453
  """
432
- Convert data from an AMS system to an mpc dict.
454
+ Convert a **setup** AMS system to a MATPOWER mpc dictionary.
455
+
456
+ This function is revised from ``andes.io.matpower.system2mpc``.
433
457
 
434
- In the ``gen`` section, slack generators preceeds PV generators.
458
+ In the ``gen`` section, slack generators are listed before PV generators.
435
459
 
436
- Compared to the ``andes.io.matpower.system2mpc``,
437
- this function includes the generator cost data in the ``gencost``
438
- section.
439
- Additionally, ``c2`` and ``c1`` are scaled by ``base_mva`` to match
440
- MATPOWER unit ``MW``.
460
+ In the converted MPC, the indices of area (bus[:, 6]) and zone (bus[:, 10])
461
+ may differ from the original MPC. However, the mapping relationship is preserved.
462
+ For example, if the original MPC numbers areas starting from 1, the converted
463
+ MPC may number them starting from 0.
464
+
465
+ The coefficients ``c2`` and ``c1`` in the generator cost data are scaled by
466
+ ``base_mva`` to match MATPOWER's unit convention (MW).
441
467
 
442
468
  Parameters
443
469
  ----------
444
470
  system : ams.core.system.System
445
- AMS system
471
+ The AMS system to be converted.
446
472
 
447
473
  Returns
448
474
  -------
449
- mpc: dict
450
- MATPOWER mpc dict
475
+ mpc : dict
476
+ A dictionary in MATPOWER format representing the converted AMS system.
451
477
  """
452
478
 
453
479
  mpc = dict(version='2',
@@ -458,12 +484,16 @@ def system2mpc(system) -> dict:
458
484
  gencost=np.zeros((system.GCost.n, 7), dtype=np.float64),
459
485
  )
460
486
 
487
+ if not system.is_setup:
488
+ logger.warning("System is not setup and will be setup now.")
489
+ system.setup()
490
+
461
491
  if system.Bus.name.v is not None:
462
492
  mpc['bus_name'] = system.Bus.name.v
463
493
 
464
494
  base_mva = system.config.mva
465
495
 
466
- # --- bus ---
496
+ # --- Bus ---
467
497
  bus = mpc['bus']
468
498
  gen = mpc['gen']
469
499
 
@@ -471,23 +501,22 @@ def system2mpc(system) -> dict:
471
501
 
472
502
  bus[:, 0] = to_busid(system.Bus.idx.v)
473
503
  bus[:, 1] = 1
504
+ if system.Area.n > 0:
505
+ bus[:, 6] = system.Area.idx2uid(system.Bus.area.v)
474
506
  bus[:, 7] = system.Bus.v0.v
475
507
  bus[:, 8] = system.Bus.a0.v * rad2deg
476
508
  bus[:, 9] = system.Bus.Vn.v
509
+ if system.Zone.n > 0:
510
+ bus[:, 10] = system.Zone.idx2uid(system.Bus.zone.v)
477
511
  bus[:, 11] = system.Bus.vmax.v
478
512
  bus[:, 12] = system.Bus.vmin.v
479
513
 
480
- # --- zone ---
481
- ZONE_I = system.Zone.idx.v
482
- if len(ZONE_I) > 0:
483
- mapping = {busi0: i for i, busi0 in enumerate(ZONE_I)}
484
- bus[:, 10] = np.array([mapping[busi0] for busi0 in system.Bus.zone.v])
485
-
486
514
  # --- PQ ---
487
515
  if system.PQ.n > 0:
488
516
  pq_pos = system.Bus.idx2uid(system.PQ.bus.v)
489
- bus[pq_pos, 2] = system.PQ.p0.v * base_mva
490
- bus[pq_pos, 3] = system.PQ.q0.v * base_mva
517
+ u = system.PQ.u.v
518
+ bus[pq_pos, 2] = u * system.PQ.p0.v * base_mva
519
+ bus[pq_pos, 3] = u * system.PQ.q0.v * base_mva
491
520
 
492
521
  # --- Shunt ---
493
522
  if system.Shunt.n > 0:
@@ -568,4 +597,141 @@ def system2mpc(system) -> dict:
568
597
  else:
569
598
  mpc.pop('gencost')
570
599
 
600
+ # --- gentype ---
601
+ stg = system.StaticGen.get_all_idxes()
602
+ gentype = system.StaticGen.get(src='gentype', attr='v', idx=stg)
603
+ if any(gentype):
604
+ mpc['gentype'] = np.array(gentype)
605
+
606
+ # --- genfuel ---
607
+ genfuel = system.StaticGen.get(src='genfuel', attr='v', idx=stg)
608
+ if any(genfuel):
609
+ mpc['genfuel'] = np.array(genfuel)
610
+
611
+ # --- Bus Name ---
612
+ if any(system.Bus.name.v):
613
+ mpc['bus_name'] = np.array(system.Bus.name.v)
614
+
571
615
  return mpc
616
+
617
+
618
+ def mpc2m(mpc: dict, outfile: str) -> str:
619
+ """
620
+ Write a MATPOWER mpc dict to a M-file.
621
+
622
+ Parameters
623
+ ----------
624
+ mpc : dict
625
+ MATPOWER mpc dictionary.
626
+ outfile : str
627
+ Path to the output M-file.
628
+ """
629
+ with open(outfile, 'w') as f:
630
+ # Add version info
631
+ f.write(f"%% Converted by AMS {version}\n")
632
+ f.write(f"%% {copyright_msg}\n\n")
633
+ f.write(f"%% {nowarranty_msg}\n")
634
+ f.write(f"%% Convert time: {report_time}\n\n")
635
+
636
+ f.write("function mpc = mpc_case\n")
637
+ f.write("mpc.version = '2';\n\n")
638
+
639
+ # Write baseMVA
640
+ f.write(f"%% system MVA base\nmpc.baseMVA = {mpc['baseMVA']};\n\n")
641
+
642
+ # Write bus data
643
+ f.write("%% bus data\n")
644
+ f.write("%% bus_i type Pd Qd Gs Bs area Vm Va baseKV zone Vmax Vmin\n")
645
+ f.write("mpc.bus = [\n")
646
+ for row in mpc['bus']:
647
+ f.write(" " + "\t".join(f"{val:.6g}" for val in row) + ";\n")
648
+ f.write("];\n\n")
649
+
650
+ # Write generator data
651
+ f.write("%% generator data\n")
652
+ f.write("%% bus Pg Qg Qmax Qmin Vg mBase status Pmax Pmin\n")
653
+ f.write("%% Pc1 Pc2 Qc1min Qc1max Qc2min Qc2max ramp_agc ramp_10 ramp_30 ramp_q apf\n")
654
+ f.write("mpc.gen = [\n")
655
+ for row in mpc['gen']:
656
+ f.write(" " + "\t".join(f"{val:.6g}" for val in row) + ";\n")
657
+ f.write("];\n\n")
658
+
659
+ # Write branch data
660
+ f.write("%% branch data\n")
661
+ f.write("%% fbus tbus r x b rateA rateB rateC ratio angle status angmin angmax PF QF PT QT\n")
662
+ f.write("mpc.branch = [\n")
663
+ for row in mpc['branch']:
664
+ f.write(" " + "\t".join(f"{val:.6g}" for val in row) + ";\n")
665
+ f.write("];\n\n")
666
+
667
+ # Write generator cost data if available
668
+ if 'gencost' in mpc:
669
+ f.write("%% generator cost data\n")
670
+ f.write("%% 1 startup shutdown n x1 y1 ... xn yn\n")
671
+ f.write("%% 2 startup shutdown n c(n-1) ... c0\n")
672
+ f.write("mpc.gencost = [\n")
673
+ for row in mpc['gencost']:
674
+ f.write(" " + "\t".join(f"{val:.6g}" for val in row) + ";\n")
675
+ f.write("];\n\n")
676
+
677
+ # Write bus names if available and not all None
678
+ if 'bus_name' in mpc and any(mpc['bus_name']):
679
+ f.write("%% bus names\n")
680
+ f.write("mpc.bus_name = {\n")
681
+ for name in mpc['bus_name']:
682
+ f.write(f" '{name}';\n")
683
+ f.write("};\n\n")
684
+
685
+ # Write generator types if available and not all None
686
+ if 'gentype' in mpc and any(mpc['gentype']):
687
+ f.write("%% generator types\n")
688
+ f.write("mpc.gentype = {\n")
689
+ for gentype in mpc['gentype']:
690
+ f.write(f" '{gentype}';\n")
691
+ f.write("};\n\n")
692
+
693
+ # Write generator fuels if available and not all None
694
+ if 'genfuel' in mpc and any(mpc['genfuel']):
695
+ f.write("%% generator fuels\n")
696
+ f.write("mpc.genfuel = {\n")
697
+ for genfuel in mpc['genfuel']:
698
+ f.write(f" '{genfuel}';\n")
699
+ f.write("};\n\n")
700
+
701
+ logger.info(f"Finished writing MATPOWER case to {outfile}")
702
+ return outfile
703
+
704
+
705
+ def write(system, outfile: str, overwrite: bool = None) -> bool:
706
+ """
707
+ Export an AMS system to a MATPOWER M-file.
708
+
709
+ This function converts an AMS system object into a MATPOWER-compatible
710
+ mpc dictionary and writes it to a specified output file in MATPOWER format.
711
+
712
+ In the converted MPC, the indices of area (bus[:, 6]) and zone (bus[:, 10])
713
+ may differ from the original MPC. However, the mapping relationship is preserved.
714
+ For example, if the original MPC numbers areas starting from 1, the converted
715
+ MPC may number them starting from 0.
716
+
717
+ Parameters
718
+ ----------
719
+ system : ams.system.System
720
+ A loaded system.
721
+ outfile : str
722
+ Path to the output file.
723
+ overwrite : bool, optional
724
+ None to prompt for overwrite selection; True to overwrite; False to not overwrite.
725
+
726
+ Returns
727
+ -------
728
+ bool
729
+ True if the file was successfully written, False otherwise.
730
+ """
731
+ if not confirm_overwrite(outfile, overwrite=overwrite):
732
+ return False
733
+
734
+ mpc = system2mpc(system)
735
+ mpc2m(mpc, outfile)
736
+ logger.info('MATPOWER m case file written to "%s"', outfile)
737
+ return True