ltbams 1.0.12__py3-none-any.whl → 1.0.14__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 (75) hide show
  1. ams/_version.py +3 -3
  2. ams/cli.py +2 -7
  3. ams/core/common.py +7 -3
  4. ams/core/documenter.py +2 -1
  5. ams/core/matprocessor.py +174 -108
  6. ams/core/model.py +14 -6
  7. ams/core/param.py +5 -3
  8. ams/core/symprocessor.py +8 -2
  9. ams/core/var.py +1 -1
  10. ams/extension/eva.py +11 -7
  11. ams/interface.py +7 -7
  12. ams/io/json.py +20 -16
  13. ams/io/matpower.py +10 -6
  14. ams/io/psse.py +4 -1
  15. ams/io/xlsx.py +21 -16
  16. ams/main.py +53 -45
  17. ams/models/distributed/esd1.py +4 -7
  18. ams/models/distributed/ev.py +10 -6
  19. ams/models/distributed/pvd1.py +4 -7
  20. ams/models/group.py +17 -18
  21. ams/models/renewable/regc.py +14 -22
  22. ams/models/timeslot.py +30 -0
  23. ams/models/zone.py +2 -4
  24. ams/opt/exprcalc.py +11 -0
  25. ams/opt/optzbase.py +4 -3
  26. ams/report.py +2 -7
  27. ams/routines/dcopf.py +7 -4
  28. ams/routines/dcopf2.py +14 -4
  29. ams/routines/dopf.py +2 -2
  30. ams/routines/ed.py +5 -5
  31. ams/routines/grbopt.py +2 -0
  32. ams/routines/pflow.py +1 -1
  33. ams/routines/pypower.py +8 -0
  34. ams/routines/routine.py +125 -1
  35. ams/routines/rted.py +5 -5
  36. ams/routines/uc.py +2 -2
  37. ams/shared.py +99 -2
  38. ams/system.py +103 -18
  39. ams/utils/paths.py +6 -10
  40. docs/source/genroutineref.py +12 -0
  41. docs/source/index.rst +4 -3
  42. docs/source/release-notes.rst +14 -0
  43. {ltbams-1.0.12.dist-info → ltbams-1.0.14.dist-info}/METADATA +21 -23
  44. {ltbams-1.0.12.dist-info → ltbams-1.0.14.dist-info}/RECORD +47 -75
  45. {ltbams-1.0.12.dist-info → ltbams-1.0.14.dist-info}/top_level.txt +0 -1
  46. tests/__init__.py +0 -0
  47. tests/test_1st_system.py +0 -64
  48. tests/test_addressing.py +0 -40
  49. tests/test_case.py +0 -301
  50. tests/test_cli.py +0 -34
  51. tests/test_export_csv.py +0 -89
  52. tests/test_group.py +0 -83
  53. tests/test_interface.py +0 -238
  54. tests/test_io.py +0 -190
  55. tests/test_jumper.py +0 -27
  56. tests/test_known_good.py +0 -267
  57. tests/test_matp.py +0 -437
  58. tests/test_model.py +0 -54
  59. tests/test_omodel.py +0 -119
  60. tests/test_paths.py +0 -89
  61. tests/test_report.py +0 -251
  62. tests/test_repr.py +0 -21
  63. tests/test_routine.py +0 -178
  64. tests/test_rtn_acopf.py +0 -75
  65. tests/test_rtn_dcopf.py +0 -121
  66. tests/test_rtn_dcopf2.py +0 -103
  67. tests/test_rtn_ed.py +0 -279
  68. tests/test_rtn_opf.py +0 -142
  69. tests/test_rtn_pflow.py +0 -147
  70. tests/test_rtn_pypower.py +0 -315
  71. tests/test_rtn_rted.py +0 -273
  72. tests/test_rtn_uc.py +0 -248
  73. tests/test_service.py +0 -73
  74. {ltbams-1.0.12.dist-info → ltbams-1.0.14.dist-info}/WHEEL +0 -0
  75. {ltbams-1.0.12.dist-info → ltbams-1.0.14.dist-info}/entry_points.txt +0 -0
ams/models/timeslot.py CHANGED
@@ -10,12 +10,42 @@ from ams.core.model import Model
10
10
  def str_list_oconv(x):
11
11
  """
12
12
  Convert list into a list literal.
13
+
14
+ Revised from `andes.models.timeseries.str_list_oconv`, where
15
+ the output type is converted to a list of strings.
13
16
  """
14
17
  # NOTE: convert elements to string from number first, then join them
15
18
  str_x = [str(i) for i in x]
16
19
  return ','.join(str_x)
17
20
 
18
21
 
22
+ class GCommit(ModelData, Model):
23
+ """
24
+ Time slot model for generator commitment decisions.
25
+
26
+ This class holds commitment decisions for generators,
27
+ and should be used in multi-period scheduling routines that need
28
+ generator commitment decisions.
29
+
30
+ For example, in Unit Commitment (UC) problems, there is a variable
31
+ `ugd` representing the unit commitment decisions for each generator.
32
+ After solving the UC problem, the `ugd` values can be used for
33
+ Economic Dispatch (ED) as a parameter.
34
+
35
+ .. versionadded:: 1.0.13
36
+ """
37
+
38
+ def __init__(self, system=None, config=None):
39
+ ModelData.__init__(self)
40
+ Model.__init__(self, system, config)
41
+
42
+ self.ug = NumParam(info='unit commitment decisions',
43
+ tex_name=r'u_{g}',
44
+ iconvert=str_list_iconv,
45
+ oconvert=str_list_oconv,
46
+ vtype=int)
47
+
48
+
19
49
  class TimeSlot(ModelData, Model):
20
50
  """
21
51
  Base model for time slot data used in multi-interval scheduling.
ams/models/zone.py CHANGED
@@ -17,9 +17,8 @@ class Zone(ModelData, Model):
17
17
 
18
18
  Notes
19
19
  -----
20
- 1. Zone is a collection of buses.
21
- 2. Model ``Zone`` is not actually defined in ANDES.
22
-
20
+ - A zone is a collection of buses.
21
+ - The ``Zone`` model is not defined in ANDES up to version 1.9.3.
23
22
  """
24
23
 
25
24
  def __init__(self, system, config):
@@ -39,7 +38,6 @@ class Zone(ModelData, Model):
39
38
  -------
40
39
  str
41
40
  Formatted table
42
-
43
41
  """
44
42
  if self.n:
45
43
  header = ['Zone ID', 'Bus ID']
ams/opt/exprcalc.py CHANGED
@@ -102,6 +102,17 @@ class ExpressionCalc(OptzBase):
102
102
  else:
103
103
  return self.optz.value
104
104
 
105
+ @v.setter
106
+ def v(self, value):
107
+ """
108
+ Set the ExpressionCalc value.
109
+ """
110
+ if self.optz is None:
111
+ raise ValueError("ExpressionCalc is not evaluated yet.")
112
+ if not isinstance(value, (int, float, np.ndarray)):
113
+ raise TypeError(f"Value must be a number or numpy array, got {type(value)}.")
114
+ self.optz.value = value
115
+
105
116
  @property
106
117
  def e(self):
107
118
  """
ams/opt/optzbase.py CHANGED
@@ -174,13 +174,14 @@ class OptzBase:
174
174
  """
175
175
  Return all the indexes of this item.
176
176
 
177
- .. note::
178
- New in version 1.0.0.
179
-
180
177
  Returns
181
178
  -------
182
179
  list
183
180
  A list of indexes.
181
+
182
+ Notes
183
+ -----
184
+ .. versionadded:: 1.0.0
184
185
  """
185
186
 
186
187
  if self.is_group:
ams/report.py CHANGED
@@ -30,13 +30,8 @@ def report_info(system) -> list:
30
30
 
31
31
  class Report:
32
32
  """
33
- Report class to store routine analysis reports.
34
-
35
- Notes
36
- -----
37
- Revised from the ANDES project (https://github.com/CURENT/andes).
38
- Original author: Hantao Cui
39
- License: GPL3
33
+ Report class to store routine analysis reports,
34
+ revised from `andes.report.Report`.
40
35
  """
41
36
 
42
37
  def __init__(self, system):
ams/routines/dcopf.py CHANGED
@@ -17,19 +17,22 @@ logger = logging.getLogger(__name__)
17
17
 
18
18
  class DCOPF(DCPFBase):
19
19
  """
20
- DC optimal power flow (DCOPF).
20
+ DC optimal power flow (DCOPF) using B-theta formulation.
21
21
 
22
22
  Notes
23
23
  -----
24
- 1. The nodal price is calculated as ``pi`` in ``pic``.
25
- 2. Devices online status of ``StaticGen``, ``StaticLoad``, and ``Shunt`` are considered in the connectivity
26
- matrices ``Cft``, ``Cg``, ``Cl``, and ``Csh``.
24
+ - The nodal price is calculated as ``pi`` in ``pic``.
25
+ - Devices online status of ``StaticGen``, ``StaticLoad``, and ``Shunt`` are considered in the connectivity
26
+ matrices ``Cft``, ``Cg``, ``Cl``, and ``Csh``.
27
27
 
28
28
  References
29
29
  ----------
30
30
  1. R. D. Zimmerman, C. E. Murillo-Sanchez, and R. J. Thomas, “MATPOWER: Steady-State
31
31
  Operations, Planning, and Analysis Tools for Power Systems Research and Education,” IEEE
32
32
  Trans. Power Syst., vol. 26, no. 1, pp. 12-19, Feb. 2011
33
+ 2. Y. Chen et al., "Security-Constrained Unit Commitment for Electricity Market: Modeling,
34
+ Solution Methods, and Future Challenges," in IEEE Transactions on Power Systems, vol. 38, no. 5,
35
+ pp. 4668-4681, Sept. 2023
33
36
  """
34
37
 
35
38
  def __init__(self, system, config):
ams/routines/dcopf2.py CHANGED
@@ -18,15 +18,25 @@ logger = logging.getLogger(__name__)
18
18
 
19
19
  class DCOPF2(DCOPF):
20
20
  """
21
- DC optimal power flow (DCOPF) using PTDF.
21
+ DC optimal power flow (DCOPF) using PTDF formulation.
22
+
22
23
  For large cases, it is recommended to build the PTDF first, especially when incremental
23
24
  build is necessary.
24
25
 
25
26
  Notes
26
27
  -----
27
- 1. This routine requires PTDF matrix.
28
- 2. Nodal price ``pi`` is calculated with three parts.
29
- 3. Bus angle ``aBus`` is calculated after solving the problem.
28
+ - This routine requires PTDF matrix.
29
+ - Nodal price ``pi`` is calculated with three parts.
30
+ - Bus angle ``aBus`` is calculated after solving the problem.
31
+
32
+ References
33
+ ----------
34
+ 1. R. D. Zimmerman, C. E. Murillo-Sanchez, and R. J. Thomas, “MATPOWER: Steady-State
35
+ Operations, Planning, and Analysis Tools for Power Systems Research and Education,” IEEE
36
+ Trans. Power Syst., vol. 26, no. 1, pp. 12-19, Feb. 2011
37
+ 2. Y. Chen et al., "Security-Constrained Unit Commitment for Electricity Market: Modeling,
38
+ Solution Methods, and Future Challenges," in IEEE Transactions on Power Systems, vol. 38, no. 5,
39
+ pp. 4668-4681, Sept. 2023
30
40
  """
31
41
 
32
42
  def __init__(self, system, config):
ams/routines/dopf.py CHANGED
@@ -17,7 +17,7 @@ class DOPF(DCOPF):
17
17
  UNDER DEVELOPMENT!
18
18
 
19
19
  References
20
- -----------------
20
+ ----------
21
21
  1. L. Bai, J. Wang, C. Wang, C. Chen, and F. Li, “Distribution Locational Marginal Pricing (DLMP)
22
22
  for Congestion Management and Voltage Support,” IEEE Trans. Power Syst., vol. 33, no. 4,
23
23
  pp. 4061-4073, Jul. 2018, doi: 10.1109/TPWRS.2017.2767632.
@@ -116,7 +116,7 @@ class DOPFVIS(DOPF):
116
116
  UNDER DEVELOPMENT!
117
117
 
118
118
  References
119
- -----------------
119
+ ----------
120
120
  1. L. Bai, J. Wang, C. Wang, C. Chen, and F. Li, “Distribution Locational Marginal Pricing (DLMP)
121
121
  for Congestion Management and Voltage Support,” IEEE Trans. Power Syst., vol. 33, no. 4,
122
122
  pp. 4061-4073, Jul. 2018, doi: 10.1109/TPWRS.2017.2767632.
ams/routines/ed.py CHANGED
@@ -120,11 +120,11 @@ class ED(RTED, MPBase, SRBase):
120
120
 
121
121
  Notes
122
122
  -----
123
- 1. Formulations has been adjusted with interval ``config.t``
124
- 2. The tie-line flow is not implemented in this model.
125
- 3. ``EDTSlot.ug`` is used instead of ``StaticGen.u`` for generator commitment.
126
- 4. Following reserves are balanced for each "Area": RegUp reserve ``rbu``,
127
- RegDn reserve ``rbd``, and Spinning reserve ``rsr``.
123
+ - Formulations has been adjusted with interval ``config.t``
124
+ - The tie-line flow is not implemented in this model.
125
+ - ``EDTSlot.ug`` is used instead of ``StaticGen.u`` for generator commitment.
126
+ - Following reserves are balanced for each "Area": RegUp reserve ``rbu``,
127
+ RegDn reserve ``rbd``, and Spinning reserve ``rsr``.
128
128
  """
129
129
 
130
130
  def __init__(self, system, config):
ams/routines/grbopt.py CHANGED
@@ -34,6 +34,8 @@ class OPF(DCPF1):
34
34
  Refer to the gurobi-optimods documentation for further details:
35
35
 
36
36
  https://gurobi-optimods.readthedocs.io/en/stable/mods/opf/opf.html
37
+
38
+ .. versionadded:: 1.0.10
37
39
  """
38
40
 
39
41
  def __init__(self, system, config):
ams/routines/pflow.py CHANGED
@@ -30,7 +30,7 @@ class PFlow(RoutineBase):
30
30
  ----------
31
31
  1. M. L. Crow, Computational methods for electric power systems. 2015.
32
32
  2. ANDES Documentation - Simulation and Plot.
33
- https://docs.andes.app/en/latest/_examples/ex1.html
33
+ https://andes.readthedocs.io/en/stable/_examples/ex1.html
34
34
  """
35
35
 
36
36
  def __init__(self, system, config):
ams/routines/pypower.py CHANGED
@@ -32,6 +32,8 @@ class DCPF1(RoutineBase):
32
32
  - This class does not implement the AMS-style DC power flow formulation.
33
33
  - For detailed mathematical formulations and algorithmic details, refer to the
34
34
  MATPOWER User's Manual, section on Power Flow.
35
+
36
+ .. versionadded:: 1.0.10
35
37
  """
36
38
 
37
39
  def __init__(self, system, config):
@@ -362,6 +364,8 @@ class PFlow1(DCPF1):
362
364
  MATPOWER User's Manual, section on Power Flow.
363
365
  - Fast-Decoupled (XB version) and Fast-Decoupled (BX version) algorithms are
364
366
  not fully supported yet.
367
+
368
+ .. versionadded:: 1.0.10
365
369
  """
366
370
 
367
371
  def __init__(self, system, config):
@@ -436,6 +440,8 @@ class DCOPF1(DCPF1):
436
440
  - For detailed mathematical formulations and algorithmic details, refer to the
437
441
  MATPOWER User's Manual, section on Optimal Power Flow.
438
442
  - Algorithms 400, 500, 600, and 700 are not fully supported yet.
443
+
444
+ .. versionadded:: 1.0.10
439
445
  """
440
446
 
441
447
  def __init__(self, system, config):
@@ -586,6 +592,8 @@ class ACOPF1(DCOPF1):
586
592
  - This class does not implement the AMS-style AC optimal power flow formulation.
587
593
  - For detailed mathematical formulations and algorithmic details, refer to the
588
594
  MATPOWER User's Manual, section on Optimal Power Flow.
595
+
596
+ .. versionadded:: 1.0.10
589
597
  """
590
598
 
591
599
  def __init__(self, system, config):
ams/routines/routine.py CHANGED
@@ -3,8 +3,9 @@ Module for routine data.
3
3
  """
4
4
 
5
5
  import logging
6
- from typing import Optional, Union, Type, Iterable, Dict
6
+ import json
7
7
  from collections import OrderedDict
8
+ from typing import Optional, Union, Type, Iterable, Dict
8
9
 
9
10
  import numpy as np
10
11
 
@@ -431,6 +432,95 @@ class RoutineBase:
431
432
  logger.warning(msg)
432
433
  return False
433
434
 
435
+ def load_json(self, path):
436
+ """
437
+ Load scheduling results from a json file.
438
+
439
+ Parameters
440
+ ----------
441
+ path : str
442
+ Path of the json file to load.
443
+
444
+ Returns
445
+ -------
446
+ bool
447
+ True if the loading is successful, False otherwise.
448
+
449
+ .. versionadded:: 1.0.13
450
+ """
451
+ try:
452
+ with open(path, 'r') as f:
453
+ data = json.load(f)
454
+ except Exception as e:
455
+ logger.error(f"Failed to load JSON file: {e}")
456
+ return False
457
+
458
+ if not self.initialized:
459
+ self.init()
460
+
461
+ # Unpack variables and expressions from JSON
462
+ for group, group_data in data.items():
463
+ if not isinstance(group_data, dict):
464
+ continue
465
+ for key, values in group_data.items():
466
+ if key == 'idx':
467
+ continue
468
+ # Find the corresponding variable or expression
469
+ if key in self.vars:
470
+ var = self.vars[key]
471
+ # Assign values to the variable
472
+ try:
473
+ var.v = np.array(values)
474
+ except Exception as e:
475
+ logger.warning(f"Failed to assign values to var '{key}': {e}")
476
+ elif key in self.exprs:
477
+ continue
478
+ elif key in self.exprcs:
479
+ exprc = self.exprcs[key]
480
+ # Assign values to the expression calculation
481
+ try:
482
+ exprc.v = np.array(values)
483
+ except Exception as e:
484
+ logger.warning(f"Failed to assign values to exprc '{key}': {e}")
485
+ logger.info(f"Loaded results from {path}")
486
+ return True
487
+
488
+ def export_json(self, path=None):
489
+ """
490
+ Export scheduling results to a json file.
491
+
492
+ Parameters
493
+ ----------
494
+ path : str, optional
495
+ Path of the json file to export.
496
+
497
+ Returns
498
+ -------
499
+ str
500
+ The exported json file name
501
+
502
+ .. versionadded:: 1.0.13
503
+ """
504
+ if not self.converged:
505
+ logger.warning("Routine did not converge, aborting export.")
506
+ return None
507
+
508
+ path, file_name = get_export_path(self.system,
509
+ self.class_name + '_out',
510
+ path=path, fmt='json')
511
+
512
+ data_dict = dict()
513
+
514
+ group_data(self, data_dict, self.vars, 'v')
515
+ group_data(self, data_dict, self.exprs, 'v')
516
+ group_data(self, data_dict, self.exprcs, 'v')
517
+
518
+ with open(path, 'w') as f:
519
+ json.dump(data_dict, f, indent=4,
520
+ default=lambda x: x.tolist() if isinstance(x, np.ndarray) else x)
521
+
522
+ return file_name
523
+
434
524
  def export_csv(self, path=None):
435
525
  """
436
526
  Export scheduling results to a csv file.
@@ -1022,3 +1112,37 @@ def collect_data(rtn: RoutineBase, data_dict: Dict, items: Dict, attr: str):
1022
1112
  logger.debug(f"Error with collecting data for '{key}': {e}")
1023
1113
  data_v = [np.nan] * len(idx_v)
1024
1114
  data_dict.update(OrderedDict(zip([f'{key} {dev}' for dev in idx_v], data_v)))
1115
+
1116
+
1117
+ def group_data(rtn: RoutineBase, data_dict: Dict, items: Dict, attr: str):
1118
+ """
1119
+ Collect data for export from grouped items.
1120
+
1121
+ Parameters
1122
+ ----------
1123
+ rtn : ams.routines.routine.RoutineBase
1124
+ The routine to collect data from.
1125
+ data_dict : Dict
1126
+ The data dictionary to populate.
1127
+ items : dict
1128
+ Dictionary of items to collect data from.
1129
+ attr : str
1130
+ Attribute to collect data for.
1131
+
1132
+ .. versionadded:: 1.0.13
1133
+ """
1134
+ for key, item in items.items():
1135
+ if item.owner is None:
1136
+ continue
1137
+ if item.owner.class_name not in data_dict.keys():
1138
+ idx_v = item.get_all_idxes()
1139
+ data_dict[item.owner.class_name] = dict(idx=idx_v)
1140
+ else:
1141
+ idx_v = data_dict[item.owner.class_name]['idx']
1142
+ try:
1143
+ data_v = rtn.get(src=key, attr=attr, idx=idx_v,
1144
+ horizon=rtn.timeslot.v if hasattr(rtn, 'timeslot') else None)
1145
+ except Exception as e:
1146
+ logger.warning(f"Error with collecting data for '{key}': {e}")
1147
+ data_v = [np.nan] * item.owner.n
1148
+ data_dict[item.owner.class_name][key] = data_v
ams/routines/rted.py CHANGED
@@ -125,10 +125,10 @@ class RTED(DCOPF, RTEDBase, SFRBase):
125
125
 
126
126
  Notes
127
127
  -----
128
- 1. Formulations has been adjusted with interval ``config.t``, 5/60 [Hour] by default.
129
- 2. The tie-line flow related constraints are ommited in this formulation.
130
- 3. The power balance is solved for the entire system.
131
- 4. The SFR is solved for each area.
128
+ - Formulations has been adjusted with interval ``config.t``, 5/60 [Hour] by default.
129
+ - The tie-line flow related constraints are ommited in this formulation.
130
+ - Power generation is balanced for the entire system.
131
+ - SFR is balanced for each area.
132
132
  """
133
133
 
134
134
  def __init__(self, system, config):
@@ -487,7 +487,7 @@ class RTEDVIS(RTED, VISBase):
487
487
  Please ensure that the parameters `dvm` and `dvd` are set according to the system base.
488
488
 
489
489
  References
490
- -----------------
490
+ ----------
491
491
  1. B. She, F. Li, H. Cui, J. Wang, Q. Zhang and R. Bo, "Virtual Inertia Scheduling (VIS) for
492
492
  Real-Time Economic Dispatch of IBR-Penetrated Power Systems," in IEEE Transactions on
493
493
  Sustainable Energy, vol. 15, no. 2, pp. 938-951, April 2024, doi: 10.1109/TSTE.2023.3319307.
ams/routines/uc.py CHANGED
@@ -69,8 +69,8 @@ class UC(DCOPF, RTEDBase, MPBase, SRBase, NSRBase):
69
69
 
70
70
  Notes
71
71
  -----
72
- 1. Formulations has been adjusted with interval ``config.t``
73
- 2. The tie-line flow has not been implemented in formulations.
72
+ - The formulations has been adjusted with interval ``config.t``
73
+ - The tie-line flow has not been implemented in formulations.
74
74
 
75
75
  References
76
76
  ----------
ams/shared.py CHANGED
@@ -4,6 +4,7 @@ Shared constants and delayed imports.
4
4
  This module is supplementary to the ``andes.shared`` module.
5
5
  """
6
6
  import logging
7
+ import sys
7
8
  import unittest
8
9
  from functools import wraps
9
10
  from time import strftime
@@ -14,6 +15,7 @@ from andes.utils.lazyimport import LazyImport
14
15
 
15
16
  from andes.system import System as adSystem
16
17
 
18
+ from ._version import get_versions
17
19
 
18
20
  logger = logging.getLogger(__name__)
19
21
 
@@ -24,6 +26,7 @@ ppoption = LazyImport('from pypower.ppoption import ppoption')
24
26
  runpf = LazyImport('from pypower.runpf import runpf')
25
27
  runopf = LazyImport('from pypower.runopf import runopf')
26
28
  opf = LazyImport('from gurobi_optimods import opf')
29
+ tqdm = LazyImport('from tqdm.auto import tqdm')
27
30
 
28
31
  # --- an empty ANDES system ---
29
32
  empty_adsys = adSystem(autogen_stale=False)
@@ -44,6 +47,15 @@ _max_length = 80 # NOQA
44
47
  copyright_msg = "Copyright (C) 2023-2025 Jinning Wang"
45
48
  nowarranty_msg = "AMS comes with ABSOLUTELY NO WARRANTY"
46
49
  report_time = strftime("%m/%d/%Y %I:%M:%S %p")
50
+ version_msg = f"AMS {get_versions()['version']}"
51
+
52
+ summary_row = {'field': 'Info',
53
+ 'comment': version_msg,
54
+ 'comment2': report_time,
55
+ 'comment3': nowarranty_msg,
56
+ 'comment4': copyright_msg}
57
+
58
+ summary_name = "Summary" # ensure model Summary's name is consistent
47
59
 
48
60
  # NOTE: copied from CVXPY documentation, last checked on 2024/10/30, v1.5
49
61
  mip_solvers = ['CBC', 'COPT', 'GLPK_MI', 'CPLEX', 'GUROBI',
@@ -118,7 +130,7 @@ def skip_unittest_without_PYPOWER(f):
118
130
  try:
119
131
  import pypower # NOQA
120
132
  except ImportError:
121
- raise unittest.SkipTest("PYPOWER is not installed.")
133
+ raise unittest.SkipTest("PYPOWER is not available.")
122
134
  return f(*args, **kwargs)
123
135
  return wrapper
124
136
 
@@ -131,6 +143,91 @@ def skip_unittest_without_gurobi_optimods(f):
131
143
  try:
132
144
  import gurobi_optimods # NOQA
133
145
  except ImportError:
134
- raise unittest.SkipTest("Gurobi is not installed.")
146
+ raise unittest.SkipTest("gurobi_optimods is not available.")
135
147
  return f(*args, **kwargs)
136
148
  return wrapper
149
+
150
+
151
+ def _init_pbar(total, unit, no_tqdm):
152
+ """Initializes and returns a tqdm progress bar."""
153
+ pbar = tqdm(total=total, unit=unit, ncols=80, ascii=True,
154
+ file=sys.stdout, disable=no_tqdm)
155
+ pbar.update(0)
156
+ return pbar
157
+
158
+
159
+ def _update_pbar(pbar, current, total):
160
+ """Updates and closes the progress bar."""
161
+ perc = np.round(min((current / total) * 100, 100), 2)
162
+ if pbar.total is not None: # Check if pbar is still valid
163
+ last_pc = pbar.n / pbar.total * 100 # Get current percentage based on updated value
164
+ else:
165
+ last_pc = 0
166
+
167
+ perc_diff = perc - last_pc
168
+ if perc_diff >= 1:
169
+ pbar.update(perc_diff)
170
+
171
+ # Ensure pbar finishes at 100% and closes
172
+ if pbar.n < pbar.total: # Check if it's not already at total
173
+ pbar.update(pbar.total - pbar.n) # Update remaining
174
+ pbar.close()
175
+
176
+
177
+ def ams_params_not_in_andes(mdl_name, am_params):
178
+ """
179
+ Helper function to return parameters not in the ANDES model.
180
+ If the model is not in the ANDES system, it returns an empty list.
181
+
182
+ Parameters
183
+ ----------
184
+ mdl_name : str
185
+ The name of the model.
186
+ am_params : list
187
+ A list of parameters from the AMS model.
188
+
189
+ Returns
190
+ -------
191
+ list
192
+ A list of parameters that are not in the ANDES model.
193
+ """
194
+ if mdl_name not in ad_models:
195
+ return []
196
+ ad_params = list(empty_adsys.models[mdl_name].params.keys())
197
+ return list(set(am_params) - set(ad_params))
198
+
199
+
200
+ def model2df(instance, skip_empty, to_andes):
201
+ """
202
+ Prepare a DataFrame from the model instance for output.
203
+
204
+ Parameters
205
+ ----------
206
+ instance : ams.model.Model
207
+ The model instance to prepare.
208
+ skip_empty : bool
209
+ Whether to skip empty models.
210
+ to_andes : bool
211
+ Whether to prepare the DataFrame for ANDES.
212
+
213
+ Returns
214
+ -------
215
+ pd.DataFrame
216
+ The prepared DataFrame.
217
+ """
218
+ name = instance.class_name
219
+
220
+ if skip_empty and instance.n == 0:
221
+ return None
222
+
223
+ if name not in ad_models and to_andes:
224
+ return None
225
+
226
+ instance.cache.refresh("df_in")
227
+ df = instance.cache.df_in
228
+
229
+ if to_andes:
230
+ skipped_params = ams_params_not_in_andes(name, df.columns.tolist())
231
+ df = df.drop(skipped_params, axis=1, errors='ignore')
232
+
233
+ return df