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.
Files changed (191) hide show
  1. ams/__init__.py +4 -11
  2. ams/_version.py +3 -3
  3. ams/cases/5bus/pjm5bus_demo.xlsx +0 -0
  4. ams/cases/5bus/pjm5bus_jumper.xlsx +0 -0
  5. ams/cases/5bus/pjm5bus_uced.json +1062 -0
  6. ams/cases/5bus/pjm5bus_uced.xlsx +0 -0
  7. ams/cases/5bus/pjm5bus_uced_esd1.xlsx +0 -0
  8. ams/cases/5bus/pjm5bus_uced_ev.xlsx +0 -0
  9. ams/cases/ieee123/ieee123.xlsx +0 -0
  10. ams/cases/ieee123/ieee123_regcv1.xlsx +0 -0
  11. ams/cases/ieee14/ieee14.json +1166 -0
  12. ams/cases/ieee14/ieee14.raw +92 -0
  13. ams/cases/ieee14/ieee14_conn.xlsx +0 -0
  14. ams/cases/ieee14/ieee14_uced.xlsx +0 -0
  15. ams/cases/ieee39/ieee39.xlsx +0 -0
  16. ams/cases/ieee39/ieee39_uced.xlsx +0 -0
  17. ams/cases/ieee39/ieee39_uced_esd1.xlsx +0 -0
  18. ams/cases/ieee39/ieee39_uced_pvd1.xlsx +0 -0
  19. ams/cases/ieee39/ieee39_uced_vis.xlsx +0 -0
  20. ams/cases/matpower/benchmark.json +1594 -0
  21. ams/cases/matpower/case118.m +787 -0
  22. ams/cases/matpower/case14.m +129 -0
  23. ams/cases/matpower/case300.m +1315 -0
  24. ams/cases/matpower/case39.m +205 -0
  25. ams/cases/matpower/case5.m +62 -0
  26. ams/cases/matpower/case_ACTIVSg2000.m +9460 -0
  27. ams/cases/npcc/npcc.m +644 -0
  28. ams/cases/npcc/npcc_uced.xlsx +0 -0
  29. ams/cases/pglib/pglib_opf_case39_epri__api.m +243 -0
  30. ams/cases/wecc/wecc.m +714 -0
  31. ams/cases/wecc/wecc_uced.xlsx +0 -0
  32. ams/cli.py +6 -0
  33. ams/core/__init__.py +2 -0
  34. ams/core/documenter.py +652 -0
  35. ams/core/matprocessor.py +782 -0
  36. ams/core/model.py +330 -0
  37. ams/core/param.py +322 -0
  38. ams/core/service.py +918 -0
  39. ams/core/symprocessor.py +224 -0
  40. ams/core/var.py +59 -0
  41. ams/extension/__init__.py +5 -0
  42. ams/extension/eva.py +401 -0
  43. ams/interface.py +1085 -0
  44. ams/io/__init__.py +133 -0
  45. ams/io/json.py +82 -0
  46. ams/io/matpower.py +406 -0
  47. ams/io/psse.py +6 -0
  48. ams/io/pypower.py +103 -0
  49. ams/io/xlsx.py +80 -0
  50. ams/main.py +81 -4
  51. ams/models/__init__.py +24 -0
  52. ams/models/area.py +40 -0
  53. ams/models/bus.py +52 -0
  54. ams/models/cost.py +169 -0
  55. ams/models/distributed/__init__.py +3 -0
  56. ams/models/distributed/esd1.py +71 -0
  57. ams/models/distributed/ev.py +60 -0
  58. ams/models/distributed/pvd1.py +67 -0
  59. ams/models/group.py +231 -0
  60. ams/models/info.py +26 -0
  61. ams/models/line.py +238 -0
  62. ams/models/renewable/__init__.py +5 -0
  63. ams/models/renewable/regc.py +119 -0
  64. ams/models/reserve.py +94 -0
  65. ams/models/shunt.py +14 -0
  66. ams/models/static/__init__.py +2 -0
  67. ams/models/static/gen.py +165 -0
  68. ams/models/static/pq.py +61 -0
  69. ams/models/timeslot.py +69 -0
  70. ams/models/zone.py +49 -0
  71. ams/opt/__init__.py +12 -0
  72. ams/opt/constraint.py +175 -0
  73. ams/opt/exprcalc.py +127 -0
  74. ams/opt/expression.py +188 -0
  75. ams/opt/objective.py +174 -0
  76. ams/opt/omodel.py +432 -0
  77. ams/opt/optzbase.py +192 -0
  78. ams/opt/param.py +156 -0
  79. ams/opt/var.py +233 -0
  80. ams/pypower/__init__.py +8 -0
  81. ams/pypower/_compat.py +9 -0
  82. ams/pypower/core/__init__.py +8 -0
  83. ams/pypower/core/pips.py +894 -0
  84. ams/pypower/core/ppoption.py +244 -0
  85. ams/pypower/core/ppver.py +18 -0
  86. ams/pypower/core/solver.py +2451 -0
  87. ams/pypower/eps.py +6 -0
  88. ams/pypower/idx.py +174 -0
  89. ams/pypower/io.py +604 -0
  90. ams/pypower/make/__init__.py +11 -0
  91. ams/pypower/make/matrices.py +665 -0
  92. ams/pypower/make/pdv.py +506 -0
  93. ams/pypower/routines/__init__.py +7 -0
  94. ams/pypower/routines/cpf.py +513 -0
  95. ams/pypower/routines/cpf_callbacks.py +114 -0
  96. ams/pypower/routines/opf.py +1803 -0
  97. ams/pypower/routines/opffcns.py +1946 -0
  98. ams/pypower/routines/pflow.py +852 -0
  99. ams/pypower/toggle.py +1098 -0
  100. ams/pypower/utils.py +293 -0
  101. ams/report.py +212 -50
  102. ams/routines/__init__.py +23 -0
  103. ams/routines/acopf.py +117 -0
  104. ams/routines/cpf.py +65 -0
  105. ams/routines/dcopf.py +241 -0
  106. ams/routines/dcpf.py +209 -0
  107. ams/routines/dcpf0.py +196 -0
  108. ams/routines/dopf.py +150 -0
  109. ams/routines/ed.py +312 -0
  110. ams/routines/pflow.py +255 -0
  111. ams/routines/pflow0.py +113 -0
  112. ams/routines/routine.py +1033 -0
  113. ams/routines/rted.py +519 -0
  114. ams/routines/type.py +160 -0
  115. ams/routines/uc.py +376 -0
  116. ams/shared.py +63 -9
  117. ams/system.py +61 -22
  118. ams/utils/__init__.py +3 -0
  119. ams/utils/misc.py +77 -0
  120. ams/utils/paths.py +257 -0
  121. docs/Makefile +21 -0
  122. docs/make.bat +35 -0
  123. docs/source/_templates/autosummary/base.rst +5 -0
  124. docs/source/_templates/autosummary/class.rst +35 -0
  125. docs/source/_templates/autosummary/module.rst +65 -0
  126. docs/source/_templates/autosummary/module_toctree.rst +66 -0
  127. docs/source/api.rst +102 -0
  128. docs/source/conf.py +203 -0
  129. docs/source/examples/index.rst +34 -0
  130. docs/source/genmodelref.py +61 -0
  131. docs/source/genroutineref.py +47 -0
  132. docs/source/getting_started/copyright.rst +20 -0
  133. docs/source/getting_started/formats/index.rst +20 -0
  134. docs/source/getting_started/formats/matpower.rst +183 -0
  135. docs/source/getting_started/formats/psse.rst +46 -0
  136. docs/source/getting_started/formats/pypower.rst +223 -0
  137. docs/source/getting_started/formats/xlsx.png +0 -0
  138. docs/source/getting_started/formats/xlsx.rst +23 -0
  139. docs/source/getting_started/index.rst +76 -0
  140. docs/source/getting_started/install.rst +234 -0
  141. docs/source/getting_started/overview.rst +26 -0
  142. docs/source/getting_started/testcase.rst +45 -0
  143. docs/source/getting_started/verification.rst +13 -0
  144. docs/source/images/curent.ico +0 -0
  145. docs/source/images/dcopf_time.png +0 -0
  146. docs/source/images/sponsors/CURENT_Logo_NameOnTrans.png +0 -0
  147. docs/source/images/sponsors/CURENT_Logo_Transparent.png +0 -0
  148. docs/source/images/sponsors/CURENT_Logo_Transparent_Name.png +0 -0
  149. docs/source/images/sponsors/doe.png +0 -0
  150. docs/source/index.rst +108 -0
  151. docs/source/modeling/example.rst +159 -0
  152. docs/source/modeling/index.rst +17 -0
  153. docs/source/modeling/model.rst +210 -0
  154. docs/source/modeling/routine.rst +122 -0
  155. docs/source/modeling/system.rst +51 -0
  156. docs/source/release-notes.rst +398 -0
  157. ltbams-1.0.2a1.dist-info/METADATA +210 -0
  158. ltbams-1.0.2a1.dist-info/RECORD +188 -0
  159. {ltbams-0.9.9.dist-info → ltbams-1.0.2a1.dist-info}/WHEEL +1 -1
  160. ltbams-1.0.2a1.dist-info/top_level.txt +3 -0
  161. tests/__init__.py +0 -0
  162. tests/test_1st_system.py +33 -0
  163. tests/test_addressing.py +40 -0
  164. tests/test_andes_mats.py +61 -0
  165. tests/test_case.py +266 -0
  166. tests/test_cli.py +34 -0
  167. tests/test_export_csv.py +89 -0
  168. tests/test_group.py +83 -0
  169. tests/test_interface.py +216 -0
  170. tests/test_io.py +32 -0
  171. tests/test_jumper.py +27 -0
  172. tests/test_known_good.py +267 -0
  173. tests/test_matp.py +437 -0
  174. tests/test_model.py +54 -0
  175. tests/test_omodel.py +119 -0
  176. tests/test_paths.py +22 -0
  177. tests/test_report.py +251 -0
  178. tests/test_repr.py +21 -0
  179. tests/test_routine.py +178 -0
  180. tests/test_rtn_dcopf.py +101 -0
  181. tests/test_rtn_dcpf.py +77 -0
  182. tests/test_rtn_ed.py +279 -0
  183. tests/test_rtn_pflow.py +219 -0
  184. tests/test_rtn_rted.py +273 -0
  185. tests/test_rtn_uc.py +248 -0
  186. tests/test_service.py +73 -0
  187. ltbams-0.9.9.dist-info/LICENSE +0 -692
  188. ltbams-0.9.9.dist-info/METADATA +0 -859
  189. ltbams-0.9.9.dist-info/RECORD +0 -14
  190. ltbams-0.9.9.dist-info/top_level.txt +0 -1
  191. {ltbams-0.9.9.dist-info → ltbams-1.0.2a1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,1033 @@
1
+ """
2
+ Module for routine data.
3
+ """
4
+
5
+ import logging
6
+ import os
7
+ from typing import Optional, Union, Type, Iterable, Dict
8
+ from collections import OrderedDict
9
+
10
+ import numpy as np
11
+
12
+ from andes.core import Config
13
+ from andes.utils.misc import elapsed
14
+
15
+ from ams.core.param import RParam
16
+ from ams.core.symprocessor import SymProcessor
17
+ from ams.core.documenter import RDocumenter
18
+ from ams.core.service import RBaseService, ValueService
19
+ from ams.opt import OModel
20
+ from ams.opt import Param, Var, Constraint, Objective, ExpressionCalc, Expression
21
+
22
+ from ams.shared import pd
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class RoutineBase:
28
+ """
29
+ Class to hold descriptive routine models and data mapping.
30
+
31
+ Attributes
32
+ ----------
33
+ system : Optional[Type]
34
+ The system object associated with the routine.
35
+ config : Config
36
+ Configuration object for the routine.
37
+ info : Optional[str]
38
+ Information about the routine.
39
+ tex_names : OrderedDict
40
+ LaTeX names for the routine parameters.
41
+ syms : SymProcessor
42
+ Symbolic processor for the routine.
43
+ _syms : bool
44
+ Flag indicating whether symbols have been generated.
45
+ rparams : OrderedDict
46
+ Registry for RParam objects.
47
+ services : OrderedDict
48
+ Registry for service objects.
49
+ params : OrderedDict
50
+ Registry for Param objects.
51
+ vars : OrderedDict
52
+ Registry for Var objects.
53
+ constrs : OrderedDict
54
+ Registry for Constraint objects.
55
+ exprcs : OrderedDict
56
+ Registry for ExpressionCalc objects.
57
+ exprs : OrderedDict
58
+ Registry for Expression objects.
59
+ obj : Optional[Objective]
60
+ Objective of the routine.
61
+ initialized : bool
62
+ Flag indicating whether the routine has been initialized.
63
+ type : str
64
+ Type of the routine.
65
+ docum : RDocumenter
66
+ Documentation generator for the routine.
67
+ map1 : OrderedDict
68
+ Mapping from ANDES.
69
+ map2 : OrderedDict
70
+ Mapping to ANDES.
71
+ om : OModel
72
+ Optimization model for the routine.
73
+ exec_time : float
74
+ Execution time of the routine.
75
+ exit_code : int
76
+ Exit code of the routine.
77
+ converged : bool
78
+ Flag indicating whether the routine has converged.
79
+ converted : bool
80
+ Flag indicating whether AC conversion has been performed.
81
+ """
82
+
83
+ def __init__(self, system=None, config=None):
84
+ """
85
+ Initialize the routine.
86
+
87
+ Parameters
88
+ ----------
89
+ system : Optional[Type]
90
+ The system object associated with the routine.
91
+ config : Optional[dict]
92
+ Configuration dictionary for the routine.
93
+ """
94
+ self.system = system
95
+ self.config = Config(self.class_name)
96
+ self.info = None
97
+ self.tex_names = OrderedDict(
98
+ (
99
+ ("sys_f", "f_{sys}"),
100
+ ("sys_mva", "S_{b,sys}"),
101
+ )
102
+ )
103
+ self.syms = SymProcessor(self) # symbolic processor
104
+ self._syms = False # symbol generation flag
105
+
106
+ self.rparams = OrderedDict() # RParam registry
107
+ self.services = OrderedDict() # Service registry
108
+ self.params = OrderedDict() # Param registry
109
+ self.vars = OrderedDict() # Var registry
110
+ self.constrs = OrderedDict() # Constraint registry
111
+ self.exprcs = OrderedDict() # ExpressionCalc registry
112
+ self.exprs = OrderedDict() # Expression registry
113
+ self.obj = None # Objective
114
+ self.initialized = False # initialization flag
115
+ self.type = "UndefinedType" # routine type
116
+ self.docum = RDocumenter(self) # documentation generator
117
+
118
+ # --- sync mapping ---
119
+ self.map1 = OrderedDict() # from ANDES
120
+ self.map2 = OrderedDict() # to ANDES
121
+
122
+ # --- optimization modeling ---
123
+ self.om = OModel(routine=self) # optimization model
124
+
125
+ if config is not None:
126
+ self.config.load(config)
127
+
128
+ # NOTE: the difference between exit_code and converged is that
129
+ # exit_code is the solver exit code, while converged is the
130
+ # convergence flag of the routine.
131
+ self.exec_time = 0.0 # running time
132
+ self.exit_code = 0 # exit code
133
+ self.converged = False # convergence flag
134
+ self.converted = False # AC conversion flag
135
+
136
+ @property
137
+ def class_name(self):
138
+ return self.__class__.__name__
139
+
140
+ def get(self, src: str, idx, attr: str = 'v',
141
+ horizon: Optional[Union[int, str, Iterable]] = None):
142
+ """
143
+ Get the value of a variable or parameter.
144
+
145
+ Parameters
146
+ ----------
147
+ src: str
148
+ Name of the variable or parameter.
149
+ idx: int, str, or list
150
+ Index of the variable or parameter.
151
+ attr: str
152
+ Attribute name.
153
+ horizon: list, optional
154
+ Horizon index.
155
+ """
156
+ if src not in self.__dict__.keys():
157
+ raise ValueError(f"<{src}> does not exist in <<{self.class_name}>.")
158
+ item = self.__dict__[src]
159
+
160
+ if not hasattr(item, attr):
161
+ raise ValueError(f"{attr} does not exist in {self.class_name}.{src}.")
162
+
163
+ idx_all = item.get_all_idxes()
164
+
165
+ if idx_all is None:
166
+ raise ValueError(f"<{self.class_name}> item <{src}> has no idx.")
167
+
168
+ is_format = False # whether the idx is formatted as a list
169
+ idx_u = None
170
+ if isinstance(idx, (str, int)):
171
+ idx_u = [idx]
172
+ is_format = True
173
+ elif isinstance(idx, (np.ndarray, pd.Series)):
174
+ idx_u = idx.tolist()
175
+ elif isinstance(idx, list):
176
+ idx_u = idx.copy()
177
+
178
+ loc = [idx_all.index(idxe) if idxe in idx_all else None for idxe in idx_u]
179
+ if None in loc:
180
+ idx_none = [idxe for idxe in idx_u if idxe not in idx_all]
181
+ msg = f"Var <{self.class_name}.{src}> does not contain value with idx={idx_none}"
182
+ raise ValueError(msg)
183
+ out = getattr(item, attr)[loc]
184
+
185
+ if horizon is not None:
186
+ if item.horizon is None:
187
+ raise ValueError(f"horizon is not defined for {self.class_name}.{src}.")
188
+ horizon_all = item.horizon.get_all_idxes()
189
+ if not isinstance(horizon, list):
190
+ raise TypeError(f"horizon must be a list, not {type(horizon)}.")
191
+ loc_h = [
192
+ horizon_all.index(idxe) if idxe in horizon_all else None
193
+ for idxe in horizon
194
+ ]
195
+ if None in loc_h:
196
+ idx_none = [idxe for idxe in horizon if idxe not in horizon_all]
197
+ msg = f"Var <{self.class_name}.{src}> does not contain horizon with idx={idx_none}"
198
+ raise ValueError(msg)
199
+ out = out[:, loc_h]
200
+ if out.shape[1] == 1:
201
+ out = out[:, 0]
202
+
203
+ return out[0] if is_format else out
204
+
205
+ def set(self, src: str, idx, attr: str = "v", value=0.0):
206
+ """
207
+ Set the value of an attribute of a routine parameter.
208
+
209
+ Performs ``self.<src>.<attr>[idx] = value``. This method will not modify
210
+ the input values from the case file that have not been converted to the
211
+ system base. As a result, changes applied by this method will not affect
212
+ the dumped case file.
213
+
214
+ To alter parameters and reflect it in the case file, use :meth:`alter`
215
+ instead.
216
+
217
+ Parameters
218
+ ----------
219
+ src : str
220
+ Name of the model property
221
+ idx : str, int, float, array-like
222
+ Indices of the devices
223
+ attr : str, optional, default='v'
224
+ The internal attribute of the property to get.
225
+ ``v`` for values, ``a`` for address, and ``e`` for equation value.
226
+ value : array-like
227
+ New values to be set
228
+
229
+ Returns
230
+ -------
231
+ bool
232
+ True when successful.
233
+ """
234
+ if self.__dict__[src].owner is not None:
235
+ # TODO: fit to `_v` type param in the future
236
+ owner = self.__dict__[src].owner
237
+ src0 = self.__dict__[src].src
238
+ try:
239
+ res = owner.set(src=src0, idx=idx, attr=attr, value=value)
240
+ return res
241
+ except KeyError as e:
242
+ msg = f"Failed to set <{src0}> in <{owner.class_name}>. "
243
+ msg += f"Original error: {e}"
244
+ raise KeyError(msg)
245
+ else:
246
+ # FIXME: add idx for non-grouped variables
247
+ raise TypeError(f"Variable {self.name} has no owner.")
248
+
249
+ def doc(self, max_width=78, export="plain"):
250
+ """
251
+ Retrieve routine documentation as a string.
252
+ """
253
+ return self.docum.get(max_width=max_width, export=export)
254
+
255
+ def _get_off_constrs(self):
256
+ """
257
+ Chcek if constraints are turned off.
258
+ """
259
+ disabled = []
260
+ for cname, c in self.constrs.items():
261
+ if c.is_disabled:
262
+ disabled.append(cname)
263
+ if len(disabled) > 0:
264
+ msg = "Disabled constraints: "
265
+ d_str = [f'{constr}' for constr in disabled]
266
+ msg += ", ".join(d_str)
267
+ logger.warning(msg)
268
+ return disabled
269
+
270
+ def _data_check(self, info=True):
271
+ """
272
+ Check if data is valid for a routine.
273
+
274
+ Parameters
275
+ ----------
276
+ info: bool
277
+ Whether to print warning messages.
278
+ """
279
+ logger.debug(f"Entering data check for <{self.class_name}>")
280
+ no_input = []
281
+ owner_list = []
282
+ for rname, rparam in self.rparams.items():
283
+ if rparam.owner is not None:
284
+ # NOTE: skip checking Shunt.g
285
+ if (rparam.owner.class_name == 'Shunt') and (rparam.src == 'g'):
286
+ pass
287
+ elif rparam.owner.n == 0:
288
+ no_input.append(rname)
289
+ owner_list.append(rparam.owner.class_name)
290
+ # TODO: add more data config check?
291
+ if rparam.config.pos:
292
+ if not np.all(rparam.v > 0):
293
+ logger.warning(f"RParam <{rname}> should have all positive values.")
294
+ if len(no_input) > 0:
295
+ if info:
296
+ msg = f"Following models are missing in input: {set(owner_list)}"
297
+ logger.error(msg)
298
+ return False
299
+ # TODO: add data validation for RParam, typical range, etc.
300
+ logger.debug(" -> Data check passed")
301
+ return True
302
+
303
+ def init(self, **kwargs):
304
+ """
305
+ Initialize the routine.
306
+
307
+ Other parameters
308
+ ----------------
309
+ force: bool
310
+ Whether to force initialization regardless of the current initialization status.
311
+ force_mats: bool
312
+ Whether to force build the system matrices, goes to `self.system.mats.build()`.
313
+ force_constr: bool
314
+ Whether to turn on all constraints.
315
+ force_om: bool
316
+ Whether to force initialize the optimization model.
317
+ """
318
+ force = kwargs.pop('force', False)
319
+ force_mats = kwargs.pop('force_mats', False)
320
+ force_constr = kwargs.pop('force_constr', False)
321
+ force_om = kwargs.pop('force_om', False)
322
+
323
+ skip_all = not (force and force_mats) and self.initialized and self.om.initialized
324
+
325
+ if skip_all:
326
+ logger.debug(f"{self.class_name} has already been initialized.")
327
+ return True
328
+
329
+ t0, _ = elapsed()
330
+ # --- data check ---
331
+ self._data_check()
332
+
333
+ # --- turn on all constrs ---
334
+ if force_constr:
335
+ for constr in self.constrs.values():
336
+ constr.is_disabled = False
337
+
338
+ # --- matrix build ---
339
+ self.system.mats.build(force=force_mats)
340
+
341
+ # --- constraint check ---
342
+ _ = self._get_off_constrs()
343
+
344
+ if not self.om.initialized:
345
+ self.om.init(force=force_om)
346
+ _, s_init = elapsed(t0)
347
+
348
+ msg = f"<{self.class_name}> "
349
+ if self.om.initialized:
350
+ msg += f"initialized in {s_init}."
351
+ self.initialized = True
352
+ else:
353
+ msg += "initialization failed!"
354
+ self.initialized = False
355
+ logger.info(msg)
356
+ return self.initialized
357
+
358
+ def solve(self, **kwargs):
359
+ """
360
+ Solve the routine optimization model.
361
+ """
362
+ raise NotImplementedError
363
+
364
+ def unpack(self, **kwargs):
365
+ """
366
+ Unpack the results.
367
+ """
368
+ raise NotImplementedError
369
+
370
+ def _post_solve(self):
371
+ """
372
+ Post-solve calculation.
373
+ """
374
+ raise NotImplementedError
375
+
376
+ def run(self, **kwargs):
377
+ """
378
+ Run the routine.
379
+ args and kwargs go to `self.solve()`.
380
+
381
+ Force initialization (`force_init=True`) will do the following:
382
+ - Rebuild the system matrices
383
+ - Enable all constraints
384
+ - Reinitialize the optimization model
385
+
386
+ Parameters
387
+ ----------
388
+ force_init: bool
389
+ Whether to force re-initialize the routine.
390
+ force_mats: bool
391
+ Whether to force build the system matrices.
392
+ force_constr: bool
393
+ Whether to turn on all constraints.
394
+ force_om: bool
395
+ Whether to force initialize the OModel.
396
+ """
397
+ # --- setup check ---
398
+ force_init = kwargs.pop('force_init', False)
399
+ force_mats = kwargs.pop('force_mats', False)
400
+ force_constr = kwargs.pop('force_constr', False)
401
+ force_om = kwargs.pop('force_om', False)
402
+ self.init(force=force_init, force_mats=force_mats,
403
+ force_constr=force_constr, force_om=force_om)
404
+
405
+ # --- solve optimization ---
406
+ t0, _ = elapsed()
407
+ _ = self.solve(**kwargs)
408
+ status = self.om.prob.status
409
+ self.exit_code = self.syms.status[status]
410
+ self.converged = self.exit_code == 0
411
+ _, s = elapsed(t0)
412
+ self.exec_time = float(s.split(" ")[0])
413
+ sstats = self.om.prob.solver_stats # solver stats
414
+ if sstats.num_iters is None:
415
+ n_iter = -1
416
+ else:
417
+ n_iter = int(sstats.num_iters)
418
+ n_iter_str = f"{n_iter} iterations " if n_iter > 1 else f"{n_iter} iteration "
419
+ if self.exit_code == 0:
420
+ msg = f"<{self.class_name}> solved as {status} in {s}, converged in "
421
+ msg += n_iter_str + f"with {sstats.solver_name}."
422
+ logger.warning(msg)
423
+ self.unpack(**kwargs)
424
+ self._post_solve()
425
+ self.system.report()
426
+ return True
427
+ else:
428
+ msg = f"{self.class_name} failed as {status} in "
429
+ msg += n_iter_str + f"with {sstats.solver_name}!"
430
+ logger.warning(msg)
431
+ return False
432
+
433
+ def export_csv(self, path=None):
434
+ """
435
+ Export scheduling results to a csv file.
436
+ For multi-period routines, the column "Time" is the time index of
437
+ ``timeslot.v``, which usually comes from ``EDTSlot`` or ``UCTSlot``.
438
+ The rest columns are the variables registered in ``vars``.
439
+
440
+ For single-period routines, the column "Time" have a pseduo value of "T1".
441
+
442
+ Parameters
443
+ ----------
444
+ path : str
445
+ path of the csv file to save
446
+
447
+ Returns
448
+ -------
449
+ export_path
450
+ The path of the exported csv file
451
+ """
452
+ if not self.converged:
453
+ logger.warning("Routine did not converge, aborting export.")
454
+ return None
455
+
456
+ if not path:
457
+ if self.system.files.fullname is None:
458
+ logger.info("Input file name not detacted. Using `Untitled`.")
459
+ file_name = f'Untitled_{self.class_name}'
460
+ else:
461
+ file_name = os.path.splitext(self.system.files.fullname)[0]
462
+ file_name += f'_{self.class_name}'
463
+ path = os.path.join(os.getcwd(), file_name + '.csv')
464
+
465
+ data_dict = initialize_data_dict(self)
466
+
467
+ collect_data(self, data_dict, self.vars, 'v')
468
+ collect_data(self, data_dict, self.exprs, 'v')
469
+ collect_data(self, data_dict, self.exprcs, 'v')
470
+
471
+ if 'T1' in data_dict['Time']:
472
+ data_dict = OrderedDict([(k, [v]) for k, v in data_dict.items()])
473
+
474
+ pd.DataFrame(data_dict).to_csv(path, index=False)
475
+
476
+ return file_name + '.csv'
477
+
478
+ def summary(self, **kwargs):
479
+ """
480
+ Summary interface
481
+ """
482
+ raise NotImplementedError
483
+
484
+ def __repr__(self):
485
+ return f"{self.class_name} at {hex(id(self))}"
486
+
487
+ def _ppc2ams(self):
488
+ """
489
+ Convert PYPOWER results to AMS.
490
+ """
491
+ raise NotImplementedError
492
+
493
+ def dc2ac(self, **kwargs):
494
+ """
495
+ Convert the DC-based results with ACOPF.
496
+ """
497
+ raise NotImplementedError
498
+
499
+ def _check_attribute(self, key, value):
500
+ """
501
+ Check the attribute pair for valid names while instantiating the class.
502
+
503
+ This function assigns `owner` to the model itself, assigns the name and tex_name.
504
+ """
505
+ if key in self.__dict__:
506
+ existing_keys = []
507
+ for rtn_type in ["constrs", "vars", "rparams", "services"]:
508
+ if rtn_type in self.__dict__:
509
+ existing_keys += list(self.__dict__[rtn_type].keys())
510
+ if key in existing_keys:
511
+ msg = f"Attribute <{key}> already exists in <{self.class_name}>."
512
+ logger.warning(msg)
513
+
514
+ # register owner routine instance of following attributes
515
+ if isinstance(value, (RBaseService)):
516
+ value.rtn = self
517
+
518
+ def __setattr__(self, key, value):
519
+ """
520
+ Overload the setattr function to register attributes.
521
+
522
+ Parameters
523
+ ----------
524
+ key: str
525
+ name of the attribute
526
+ value:
527
+ value of the attribute
528
+ """
529
+
530
+ # NOTE: value.id is not in use yet
531
+ if isinstance(value, Var):
532
+ value.id = len(self.vars)
533
+ self._check_attribute(key, value)
534
+ self._register_attribute(key, value)
535
+
536
+ super(RoutineBase, self).__setattr__(key, value)
537
+
538
+ def _register_attribute(self, key, value):
539
+ """
540
+ Register a pair of attributes to the routine instance.
541
+
542
+ Called within ``__setattr__``, this is where the magic happens.
543
+ Subclass attributes are automatically registered based on the variable type.
544
+ """
545
+ if isinstance(value, (Param, Var, Constraint, Objective, ExpressionCalc, Expression)):
546
+ value.om = self.om
547
+ value.rtn = self
548
+ if isinstance(value, Param):
549
+ self.params[key] = value
550
+ self.om.params[key] = None # cp.Parameter
551
+ if isinstance(value, Var):
552
+ self.vars[key] = value
553
+ self.om.vars[key] = None # cp.Variable
554
+ elif isinstance(value, Constraint):
555
+ self.constrs[key] = value
556
+ self.om.constrs[key] = None # cp.Constraint
557
+ elif isinstance(value, Expression):
558
+ self.exprs[key] = value
559
+ self.om.exprs[key] = None # cp.Expression
560
+ elif isinstance(value, ExpressionCalc):
561
+ self.exprcs[key] = value
562
+ elif isinstance(value, RParam):
563
+ self.rparams[key] = value
564
+ elif isinstance(value, RBaseService):
565
+ self.services[key] = value
566
+
567
+ def update(self, params=None, build_mats=False):
568
+ """
569
+ Update the values of Parameters in the optimization model.
570
+
571
+ This method is particularly important when some `RParams` are
572
+ linked with system matrices.
573
+ In such cases, setting `build_mats=True` is necessary to rebuild
574
+ these matrices for the changes to take effect.
575
+ This is common in scenarios involving topology changes, connection statuses,
576
+ or load value modifications.
577
+ If unsure, it is advisable to use `build_mats=True` as a precautionary measure.
578
+
579
+ Parameters
580
+ ----------
581
+ params: Parameter, str, or list
582
+ Parameter, Parameter name, or a list of parameter names to be updated.
583
+ If None, all parameters will be updated.
584
+ build_mats: bool
585
+ True to rebuild the system matrices. Set to False to speed up the process
586
+ if no system matrices are changed.
587
+ """
588
+ t0, _ = elapsed()
589
+ re_finalize = False
590
+ # sanitize input
591
+ sparams = []
592
+ if params is None:
593
+ sparams = [val for val in self.params.values()]
594
+ build_mats = True
595
+ elif isinstance(params, Param):
596
+ sparams = [params]
597
+ elif isinstance(params, str):
598
+ sparams = [self.params[params]]
599
+ elif isinstance(params, list):
600
+ sparams = [self.params[param] for param in params if isinstance(param, str)]
601
+ for param in sparams:
602
+ param.update()
603
+
604
+ for param in sparams:
605
+ if param.optz is None: # means no_parse=True
606
+ re_finalize = True
607
+ break
608
+
609
+ self.system.mats.build(force=build_mats)
610
+
611
+ if re_finalize:
612
+ logger.warning(f"<{self.class_name}> reinit OModel due to non-parametric change.")
613
+ self.om.evaluate(force=True)
614
+ self.om.finalize(force=True)
615
+
616
+ results = self.om.update(params=sparams)
617
+ t0, s0 = elapsed(t0)
618
+ logger.debug(f"Update params in {s0}.")
619
+ return results
620
+
621
+ def __delattr__(self, name):
622
+ """
623
+ Overload the delattr function to unregister attributes.
624
+
625
+ Parameters
626
+ ----------
627
+ name: str
628
+ name of the attribute
629
+ """
630
+ self._unregister_attribute(name)
631
+ if name == "obj":
632
+ self.obj = None
633
+ else:
634
+ super().__delattr__(name) # Call the superclass implementation
635
+
636
+ def _unregister_attribute(self, name):
637
+ """
638
+ Unregister a pair of attributes from the routine instance.
639
+
640
+ Called within ``__delattr__``, this is where the magic happens.
641
+ Subclass attributes are automatically unregistered based on the variable type.
642
+ """
643
+ if name in self.vars:
644
+ del self.vars[name]
645
+ if name in self.om.vars:
646
+ del self.om.vars[name]
647
+ elif name in self.rparams:
648
+ del self.rparams[name]
649
+ elif name in self.constrs:
650
+ del self.constrs[name]
651
+ if name in self.om.constrs:
652
+ del self.om.constrs[name]
653
+ elif name in self.services:
654
+ del self.services[name]
655
+
656
+ def enable(self, name):
657
+ """
658
+ Enable a constraint by name.
659
+
660
+ Parameters
661
+ ----------
662
+ name: str or list
663
+ name of the constraint to be enabled
664
+ """
665
+ if isinstance(name, list):
666
+ constr_act = []
667
+ for n in name:
668
+ if n not in self.constrs:
669
+ logger.warning(f"Constraint <{n}> not found.")
670
+ continue
671
+ if not self.constrs[n].is_disabled:
672
+ logger.warning(f"Constraint <{n}> has already been enabled.")
673
+ continue
674
+ self.constrs[n].is_disabled = False
675
+ self.om.finalized = False
676
+ constr_act.append(n)
677
+ if len(constr_act) > 0:
678
+ msg = ", ".join(constr_act)
679
+ logger.warning(f"Turn on constraints: {msg}")
680
+ return True
681
+
682
+ if name in self.constrs:
683
+ if not self.constrs[name].is_disabled:
684
+ logger.warning(f"Constraint <{name}> has already been enabled.")
685
+ else:
686
+ self.constrs[name].is_disabled = False
687
+ self.om.finalized = False
688
+ logger.warning(f"Turn on constraint <{name}>.")
689
+ return True
690
+
691
+ def disable(self, name):
692
+ """
693
+ Disable a constraint by name.
694
+
695
+ Parameters
696
+ ----------
697
+ name: str or list
698
+ name of the constraint to be disabled
699
+ """
700
+ if isinstance(name, list):
701
+ constr_act = []
702
+ for n in name:
703
+ if n not in self.constrs:
704
+ logger.warning(f"Constraint <{n}> not found.")
705
+ elif self.constrs[n].is_disabled:
706
+ logger.warning(f"Constraint <{n}> has already been disabled.")
707
+ else:
708
+ self.constrs[n].is_disabled = True
709
+ self.om.finalized = False
710
+ constr_act.append(n)
711
+ if len(constr_act) > 0:
712
+ msg = ", ".join(constr_act)
713
+ logger.warning(f"Turn off constraints: {msg}")
714
+ return True
715
+
716
+ if name in self.constrs:
717
+ if self.constrs[name].is_disabled:
718
+ logger.warning(f"Constraint <{name}> has already been disabled.")
719
+ else:
720
+ self.constrs[name].is_disabled = True
721
+ self.om.finalized = False
722
+ logger.warning(f"Turn off constraint <{name}>.")
723
+ return True
724
+
725
+ logger.warning(f"Constraint <{name}> not found.")
726
+
727
+ def _post_add_check(self):
728
+ """
729
+ Post-addition check.
730
+ """
731
+ # --- reset routine status ---
732
+ self.initialized = False
733
+ self.exec_time = 0.0
734
+ self.exit_code = 0
735
+ # --- reset symprocessor status ---
736
+ self._syms = False
737
+ # --- reset optimization model status ---
738
+ self.om.parsed = False
739
+ self.om.evaluated = False
740
+ self.om.finalized = False
741
+ # --- reset OModel parser status ---
742
+ self.om.parsed = False
743
+
744
+ def addRParam(self,
745
+ name: str,
746
+ tex_name: Optional[str] = None,
747
+ info: Optional[str] = None,
748
+ src: Optional[str] = None,
749
+ unit: Optional[str] = None,
750
+ model: Optional[str] = None,
751
+ v: Optional[np.ndarray] = None,
752
+ indexer: Optional[str] = None,
753
+ imodel: Optional[str] = None,):
754
+ """
755
+ Add `RParam` to the routine.
756
+
757
+ Parameters
758
+ ----------
759
+ name : str
760
+ Name of this parameter. If not provided, `name` will be set
761
+ to the attribute name.
762
+ tex_name : str, optional
763
+ LaTeX-formatted parameter name. If not provided, `tex_name`
764
+ will be assigned the same as `name`.
765
+ info : str, optional
766
+ A description of this parameter
767
+ src : str, optional
768
+ Source name of the parameter.
769
+ unit : str, optional
770
+ Unit of the parameter.
771
+ model : str, optional
772
+ Name of the owner model or group.
773
+ v : np.ndarray, optional
774
+ External value of the parameter.
775
+ indexer : str, optional
776
+ Indexer of the parameter.
777
+ imodel : str, optional
778
+ Name of the owner model or group of the indexer.
779
+ """
780
+ item = RParam(name=name, tex_name=tex_name, info=info, src=src, unit=unit,
781
+ model=model, v=v, indexer=indexer, imodel=imodel)
782
+
783
+ # add the parameter as an routine attribute
784
+ setattr(self, name, item)
785
+
786
+ # NOTE: manually register the owner of the parameter
787
+ # This is skipped in ``addVars`` because of ``Var.__setattr__``
788
+ item.rtn = self
789
+
790
+ # check variable owner validity if given
791
+ if model is not None:
792
+ if item.model in self.system.groups.keys():
793
+ item.is_group = True
794
+ item.owner = self.system.groups[item.model]
795
+ elif item.model in self.system.models.keys():
796
+ item.owner = self.system.models[item.model]
797
+ else:
798
+ msg = f'Model indicator \'{item.model}\' of <{item.rtn.class_name}.{name}>'
799
+ msg += ' is not a model or group. Likely a modeling error.'
800
+ logger.warning(msg)
801
+
802
+ self._post_add_check()
803
+ return item
804
+
805
+ def addService(self,
806
+ name: str,
807
+ value: np.ndarray,
808
+ tex_name: str = None,
809
+ unit: str = None,
810
+ info: str = None,
811
+ vtype: Type = None,):
812
+ """
813
+ Add `ValueService` to the routine.
814
+
815
+ Parameters
816
+ ----------
817
+ name : str
818
+ Instance name.
819
+ value : np.ndarray
820
+ Value.
821
+ tex_name : str, optional
822
+ TeX name.
823
+ unit : str, optional
824
+ Unit.
825
+ info : str, optional
826
+ Description.
827
+ vtype : Type, optional
828
+ Variable type.
829
+ """
830
+ item = ValueService(name=name, tex_name=tex_name,
831
+ unit=unit, info=info,
832
+ vtype=vtype, value=value)
833
+ # add the service as an routine attribute
834
+ setattr(self, name, item)
835
+
836
+ self._post_add_check()
837
+
838
+ return item
839
+
840
+ def addConstrs(self,
841
+ name: str,
842
+ e_str: str,
843
+ info: Optional[str] = None,
844
+ is_eq: Optional[str] = False,):
845
+ """
846
+ Add `Constraint` to the routine. to the routine.
847
+
848
+ Parameters
849
+ ----------
850
+ name : str
851
+ Constraint name. One should typically assigning the name directly because
852
+ it will be automatically assigned by the model. The value of ``name``
853
+ will be the symbol name to be used in expressions.
854
+ e_str : str
855
+ Constraint expression string.
856
+ info : str, optional
857
+ Descriptive information
858
+ is_eq : str, optional
859
+ Flag indicating if the constraint is an equality constraint. False indicates
860
+ an inequality constraint in the form of `<= 0`.
861
+ """
862
+ item = Constraint(name=name, e_str=e_str, info=info, is_eq=is_eq)
863
+ # add the constraint as an routine attribute
864
+ setattr(self, name, item)
865
+
866
+ self._post_add_check()
867
+
868
+ return item
869
+
870
+ def addVars(self,
871
+ name: str,
872
+ model: Optional[str] = None,
873
+ shape: Optional[Union[int, tuple]] = None,
874
+ tex_name: Optional[str] = None,
875
+ info: Optional[str] = None,
876
+ src: Optional[str] = None,
877
+ unit: Optional[str] = None,
878
+ horizon: Optional[RParam] = None,
879
+ nonneg: Optional[bool] = False,
880
+ nonpos: Optional[bool] = False,
881
+ cplx: Optional[bool] = False,
882
+ imag: Optional[bool] = False,
883
+ symmetric: Optional[bool] = False,
884
+ diag: Optional[bool] = False,
885
+ psd: Optional[bool] = False,
886
+ nsd: Optional[bool] = False,
887
+ hermitian: Optional[bool] = False,
888
+ boolean: Optional[bool] = False,
889
+ integer: Optional[bool] = False,
890
+ pos: Optional[bool] = False,
891
+ neg: Optional[bool] = False,):
892
+ """
893
+ Add a variable to the routine.
894
+
895
+ Parameters
896
+ ----------
897
+ name : str, optional
898
+ Variable name. One should typically assigning the name directly because
899
+ it will be automatically assigned by the model. The value of ``name``
900
+ will be the symbol name to be used in expressions.
901
+ model : str, optional
902
+ Name of the owner model or group.
903
+ shape : int or tuple, optional
904
+ Shape of the variable. If is None, the shape of `model` will be used.
905
+ info : str, optional
906
+ Descriptive information
907
+ unit : str, optional
908
+ Unit
909
+ tex_name : str
910
+ LaTeX-formatted variable symbol. If is None, the value of `name` will be
911
+ used.
912
+ src : str, optional
913
+ Source variable name. If is None, the value of `name` will be used.
914
+ lb : str, optional
915
+ Lower bound
916
+ ub : str, optional
917
+ Upper bound
918
+ horizon : ams.routines.RParam, optional
919
+ Horizon idx.
920
+ nonneg : bool, optional
921
+ Non-negative variable
922
+ nonpos : bool, optional
923
+ Non-positive variable
924
+ cplx : bool, optional
925
+ Complex variable
926
+ imag : bool, optional
927
+ Imaginary variable
928
+ symmetric : bool, optional
929
+ Symmetric variable
930
+ diag : bool, optional
931
+ Diagonal variable
932
+ psd : bool, optional
933
+ Positive semi-definite variable
934
+ nsd : bool, optional
935
+ Negative semi-definite variable
936
+ hermitian : bool, optional
937
+ Hermitian variable
938
+ bool : bool, optional
939
+ Boolean variable
940
+ integer : bool, optional
941
+ Integer variable
942
+ pos : bool, optional
943
+ Positive variable
944
+ neg : bool, optional
945
+ Negative variable
946
+ """
947
+ if model is None and shape is None:
948
+ raise ValueError("Either model or shape must be specified.")
949
+ item = Var(name=name, tex_name=tex_name,
950
+ info=info, src=src, unit=unit,
951
+ model=model, shape=shape, horizon=horizon,
952
+ nonneg=nonneg, nonpos=nonpos,
953
+ cplx=cplx, imag=imag,
954
+ symmetric=symmetric, diag=diag,
955
+ psd=psd, nsd=nsd, hermitian=hermitian,
956
+ boolean=boolean, integer=integer,
957
+ pos=pos, neg=neg, )
958
+
959
+ # add the variable as an routine attribute
960
+ setattr(self, name, item)
961
+
962
+ # check variable owner validity if given
963
+ if model is not None:
964
+ if item.model in self.system.groups.keys():
965
+ item.is_group = True
966
+ item.owner = self.system.groups[item.model]
967
+ elif item.model in self.system.models.keys():
968
+ item.owner = self.system.models[item.model]
969
+ else:
970
+ msg = (
971
+ f"Model indicator '{item.model}' of <{item.rtn.class_name}.{name}>"
972
+ )
973
+ msg += " is not a model or group. Likely a modeling error."
974
+ logger.warning(msg)
975
+
976
+ self._post_add_check()
977
+
978
+ return item
979
+
980
+ def _initial_guess(self):
981
+ """
982
+ Generate initial guess for the optimization model.
983
+ """
984
+ raise NotImplementedError
985
+
986
+
987
+ def initialize_data_dict(rtn: RoutineBase):
988
+ """
989
+ Initialize the data dictionary for export.
990
+
991
+ Parameters
992
+ ----------
993
+ rtn : ams.routines.routine.RoutineBase
994
+ The routine to collect data from
995
+
996
+ Returns
997
+ -------
998
+ OrderedDict
999
+ The initialized data dictionary.
1000
+ """
1001
+ if hasattr(rtn, 'timeslot'):
1002
+ timeslot = rtn.timeslot.v.copy()
1003
+ return OrderedDict([('Time', timeslot)])
1004
+ else:
1005
+ return OrderedDict([('Time', 'T1')])
1006
+
1007
+
1008
+ def collect_data(rtn: RoutineBase, data_dict: Dict, items: Dict, attr: str):
1009
+ """
1010
+ Collect data for export.
1011
+
1012
+ Parameters
1013
+ ----------
1014
+ rtn : ams.routines.routine.RoutineBase
1015
+ The routine to collect data from.
1016
+ data_dict : OrderedDict
1017
+ The data dictionary to populate.
1018
+ items : dict
1019
+ Dictionary of items to collect data from.
1020
+ attr : str
1021
+ Attribute to collect data for.
1022
+ """
1023
+ for key, item in items.items():
1024
+ if item.owner is None:
1025
+ continue
1026
+ idx_v = item.get_all_idxes()
1027
+ try:
1028
+ data_v = rtn.get(src=key, attr=attr, idx=idx_v,
1029
+ horizon=rtn.timeslot.v if hasattr(rtn, 'timeslot') else None).round(6)
1030
+ except Exception as e:
1031
+ logger.debug(f"Error with collecting data for '{key}': {e}")
1032
+ data_v = [np.nan] * len(idx_v)
1033
+ data_dict.update(OrderedDict(zip([f'{key} {dev}' for dev in idx_v], data_v)))