ltbams 0.9.9__py3-none-any.whl → 1.0.2a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ams/__init__.py +4 -11
- ams/_version.py +3 -3
- ams/cases/5bus/pjm5bus_demo.xlsx +0 -0
- ams/cases/5bus/pjm5bus_jumper.xlsx +0 -0
- ams/cases/5bus/pjm5bus_uced.json +1062 -0
- ams/cases/5bus/pjm5bus_uced.xlsx +0 -0
- ams/cases/5bus/pjm5bus_uced_esd1.xlsx +0 -0
- ams/cases/5bus/pjm5bus_uced_ev.xlsx +0 -0
- ams/cases/ieee123/ieee123.xlsx +0 -0
- ams/cases/ieee123/ieee123_regcv1.xlsx +0 -0
- ams/cases/ieee14/ieee14.json +1166 -0
- ams/cases/ieee14/ieee14.raw +92 -0
- ams/cases/ieee14/ieee14_conn.xlsx +0 -0
- ams/cases/ieee14/ieee14_uced.xlsx +0 -0
- ams/cases/ieee39/ieee39.xlsx +0 -0
- ams/cases/ieee39/ieee39_uced.xlsx +0 -0
- ams/cases/ieee39/ieee39_uced_esd1.xlsx +0 -0
- ams/cases/ieee39/ieee39_uced_pvd1.xlsx +0 -0
- ams/cases/ieee39/ieee39_uced_vis.xlsx +0 -0
- ams/cases/matpower/benchmark.json +1594 -0
- ams/cases/matpower/case118.m +787 -0
- ams/cases/matpower/case14.m +129 -0
- ams/cases/matpower/case300.m +1315 -0
- ams/cases/matpower/case39.m +205 -0
- ams/cases/matpower/case5.m +62 -0
- ams/cases/matpower/case_ACTIVSg2000.m +9460 -0
- ams/cases/npcc/npcc.m +644 -0
- ams/cases/npcc/npcc_uced.xlsx +0 -0
- ams/cases/pglib/pglib_opf_case39_epri__api.m +243 -0
- ams/cases/wecc/wecc.m +714 -0
- ams/cases/wecc/wecc_uced.xlsx +0 -0
- ams/cli.py +6 -0
- ams/core/__init__.py +2 -0
- ams/core/documenter.py +652 -0
- ams/core/matprocessor.py +782 -0
- ams/core/model.py +330 -0
- ams/core/param.py +322 -0
- ams/core/service.py +918 -0
- ams/core/symprocessor.py +224 -0
- ams/core/var.py +59 -0
- ams/extension/__init__.py +5 -0
- ams/extension/eva.py +401 -0
- ams/interface.py +1085 -0
- ams/io/__init__.py +133 -0
- ams/io/json.py +82 -0
- ams/io/matpower.py +406 -0
- ams/io/psse.py +6 -0
- ams/io/pypower.py +103 -0
- ams/io/xlsx.py +80 -0
- ams/main.py +81 -4
- ams/models/__init__.py +24 -0
- ams/models/area.py +40 -0
- ams/models/bus.py +52 -0
- ams/models/cost.py +169 -0
- ams/models/distributed/__init__.py +3 -0
- ams/models/distributed/esd1.py +71 -0
- ams/models/distributed/ev.py +60 -0
- ams/models/distributed/pvd1.py +67 -0
- ams/models/group.py +231 -0
- ams/models/info.py +26 -0
- ams/models/line.py +238 -0
- ams/models/renewable/__init__.py +5 -0
- ams/models/renewable/regc.py +119 -0
- ams/models/reserve.py +94 -0
- ams/models/shunt.py +14 -0
- ams/models/static/__init__.py +2 -0
- ams/models/static/gen.py +165 -0
- ams/models/static/pq.py +61 -0
- ams/models/timeslot.py +69 -0
- ams/models/zone.py +49 -0
- ams/opt/__init__.py +12 -0
- ams/opt/constraint.py +175 -0
- ams/opt/exprcalc.py +127 -0
- ams/opt/expression.py +188 -0
- ams/opt/objective.py +174 -0
- ams/opt/omodel.py +432 -0
- ams/opt/optzbase.py +192 -0
- ams/opt/param.py +156 -0
- ams/opt/var.py +233 -0
- ams/pypower/__init__.py +8 -0
- ams/pypower/_compat.py +9 -0
- ams/pypower/core/__init__.py +8 -0
- ams/pypower/core/pips.py +894 -0
- ams/pypower/core/ppoption.py +244 -0
- ams/pypower/core/ppver.py +18 -0
- ams/pypower/core/solver.py +2451 -0
- ams/pypower/eps.py +6 -0
- ams/pypower/idx.py +174 -0
- ams/pypower/io.py +604 -0
- ams/pypower/make/__init__.py +11 -0
- ams/pypower/make/matrices.py +665 -0
- ams/pypower/make/pdv.py +506 -0
- ams/pypower/routines/__init__.py +7 -0
- ams/pypower/routines/cpf.py +513 -0
- ams/pypower/routines/cpf_callbacks.py +114 -0
- ams/pypower/routines/opf.py +1803 -0
- ams/pypower/routines/opffcns.py +1946 -0
- ams/pypower/routines/pflow.py +852 -0
- ams/pypower/toggle.py +1098 -0
- ams/pypower/utils.py +293 -0
- ams/report.py +212 -50
- ams/routines/__init__.py +23 -0
- ams/routines/acopf.py +117 -0
- ams/routines/cpf.py +65 -0
- ams/routines/dcopf.py +241 -0
- ams/routines/dcpf.py +209 -0
- ams/routines/dcpf0.py +196 -0
- ams/routines/dopf.py +150 -0
- ams/routines/ed.py +312 -0
- ams/routines/pflow.py +255 -0
- ams/routines/pflow0.py +113 -0
- ams/routines/routine.py +1033 -0
- ams/routines/rted.py +519 -0
- ams/routines/type.py +160 -0
- ams/routines/uc.py +376 -0
- ams/shared.py +63 -9
- ams/system.py +61 -22
- ams/utils/__init__.py +3 -0
- ams/utils/misc.py +77 -0
- ams/utils/paths.py +257 -0
- docs/Makefile +21 -0
- docs/make.bat +35 -0
- docs/source/_templates/autosummary/base.rst +5 -0
- docs/source/_templates/autosummary/class.rst +35 -0
- docs/source/_templates/autosummary/module.rst +65 -0
- docs/source/_templates/autosummary/module_toctree.rst +66 -0
- docs/source/api.rst +102 -0
- docs/source/conf.py +203 -0
- docs/source/examples/index.rst +34 -0
- docs/source/genmodelref.py +61 -0
- docs/source/genroutineref.py +47 -0
- docs/source/getting_started/copyright.rst +20 -0
- docs/source/getting_started/formats/index.rst +20 -0
- docs/source/getting_started/formats/matpower.rst +183 -0
- docs/source/getting_started/formats/psse.rst +46 -0
- docs/source/getting_started/formats/pypower.rst +223 -0
- docs/source/getting_started/formats/xlsx.png +0 -0
- docs/source/getting_started/formats/xlsx.rst +23 -0
- docs/source/getting_started/index.rst +76 -0
- docs/source/getting_started/install.rst +234 -0
- docs/source/getting_started/overview.rst +26 -0
- docs/source/getting_started/testcase.rst +45 -0
- docs/source/getting_started/verification.rst +13 -0
- docs/source/images/curent.ico +0 -0
- docs/source/images/dcopf_time.png +0 -0
- docs/source/images/sponsors/CURENT_Logo_NameOnTrans.png +0 -0
- docs/source/images/sponsors/CURENT_Logo_Transparent.png +0 -0
- docs/source/images/sponsors/CURENT_Logo_Transparent_Name.png +0 -0
- docs/source/images/sponsors/doe.png +0 -0
- docs/source/index.rst +108 -0
- docs/source/modeling/example.rst +159 -0
- docs/source/modeling/index.rst +17 -0
- docs/source/modeling/model.rst +210 -0
- docs/source/modeling/routine.rst +122 -0
- docs/source/modeling/system.rst +51 -0
- docs/source/release-notes.rst +398 -0
- ltbams-1.0.2a1.dist-info/METADATA +210 -0
- ltbams-1.0.2a1.dist-info/RECORD +188 -0
- {ltbams-0.9.9.dist-info → ltbams-1.0.2a1.dist-info}/WHEEL +1 -1
- ltbams-1.0.2a1.dist-info/top_level.txt +3 -0
- tests/__init__.py +0 -0
- tests/test_1st_system.py +33 -0
- tests/test_addressing.py +40 -0
- tests/test_andes_mats.py +61 -0
- tests/test_case.py +266 -0
- tests/test_cli.py +34 -0
- tests/test_export_csv.py +89 -0
- tests/test_group.py +83 -0
- tests/test_interface.py +216 -0
- tests/test_io.py +32 -0
- tests/test_jumper.py +27 -0
- tests/test_known_good.py +267 -0
- tests/test_matp.py +437 -0
- tests/test_model.py +54 -0
- tests/test_omodel.py +119 -0
- tests/test_paths.py +22 -0
- tests/test_report.py +251 -0
- tests/test_repr.py +21 -0
- tests/test_routine.py +178 -0
- tests/test_rtn_dcopf.py +101 -0
- tests/test_rtn_dcpf.py +77 -0
- tests/test_rtn_ed.py +279 -0
- tests/test_rtn_pflow.py +219 -0
- tests/test_rtn_rted.py +273 -0
- tests/test_rtn_uc.py +248 -0
- tests/test_service.py +73 -0
- ltbams-0.9.9.dist-info/LICENSE +0 -692
- ltbams-0.9.9.dist-info/METADATA +0 -859
- ltbams-0.9.9.dist-info/RECORD +0 -14
- ltbams-0.9.9.dist-info/top_level.txt +0 -1
- {ltbams-0.9.9.dist-info → ltbams-1.0.2a1.dist-info}/entry_points.txt +0 -0
ams/routines/uc.py
ADDED
@@ -0,0 +1,376 @@
|
|
1
|
+
"""
|
2
|
+
Unit commitment routines.
|
3
|
+
"""
|
4
|
+
import logging
|
5
|
+
from collections import OrderedDict
|
6
|
+
import numpy as np
|
7
|
+
import pandas as pd
|
8
|
+
|
9
|
+
from ams.core.param import RParam
|
10
|
+
from ams.core.service import (NumOp, NumOpDual, MinDur)
|
11
|
+
from ams.routines.dcopf import DCOPF
|
12
|
+
from ams.routines.rted import RTEDBase
|
13
|
+
from ams.routines.ed import SRBase, MPBase, ESD1MPBase, DGBase
|
14
|
+
|
15
|
+
from ams.opt import Var, Constraint
|
16
|
+
|
17
|
+
logger = logging.getLogger(__name__)
|
18
|
+
|
19
|
+
|
20
|
+
class NSRBase:
|
21
|
+
"""
|
22
|
+
Base class for non-spinning reserve.
|
23
|
+
"""
|
24
|
+
|
25
|
+
def __init__(self) -> None:
|
26
|
+
self.cnsr = RParam(info='cost for non-spinning reserve',
|
27
|
+
name='cnsr', tex_name=r'c_{nsr}',
|
28
|
+
model='NSRCost', src='cnsr',
|
29
|
+
unit=r'$/(p.u.*h)',
|
30
|
+
indexer='gen', imodel='StaticGen',)
|
31
|
+
self.dnsrp = RParam(info='non-spinning reserve requirement in percentage',
|
32
|
+
name='dnsr', tex_name=r'd_{nsr}',
|
33
|
+
model='NSR', src='demand',
|
34
|
+
unit='%',)
|
35
|
+
self.prns = Var(info='non-spinning reserve',
|
36
|
+
name='prns', tex_name=r'p_{r, ns}',
|
37
|
+
model='StaticGen', nonneg=True,)
|
38
|
+
|
39
|
+
self.dnsrpz = NumOpDual(u=self.pdz, u2=self.dnsrp, fun=np.multiply,
|
40
|
+
name='dnsrpz', tex_name=r'd_{nsr, p, z}',
|
41
|
+
info='zonal non-spinning reserve requirement in percentage',)
|
42
|
+
self.dnsr = NumOpDual(u=self.dnsrpz, u2=self.sd, fun=np.multiply,
|
43
|
+
rfun=np.transpose,
|
44
|
+
name='dnsr', tex_name=r'd_{nsr}',
|
45
|
+
info='zonal non-spinning reserve requirement',
|
46
|
+
no_parse=True,)
|
47
|
+
|
48
|
+
# NOTE: define e_str in dispatch model
|
49
|
+
self.prnsb = Constraint(info='non-spinning reserve balance',
|
50
|
+
name='prnsb', is_eq=True,)
|
51
|
+
self.rnsr = Constraint(info='non-spinning reserve requirement',
|
52
|
+
name='rnsr', is_eq=False,)
|
53
|
+
|
54
|
+
|
55
|
+
class UC(DCOPF, RTEDBase, MPBase, SRBase, NSRBase):
|
56
|
+
"""
|
57
|
+
DC-based unit commitment (UC):
|
58
|
+
The bilinear term in the formulation is linearized with big-M method.
|
59
|
+
|
60
|
+
Non-negative var `pdu` is introduced as unserved load with its penalty `cdp`.
|
61
|
+
|
62
|
+
Constraints include power balance, ramping, spinning reserve, non-spinning reserve,
|
63
|
+
minimum ON/OFF duration.
|
64
|
+
The cost inludes generation cost, startup cost, shutdown cost, spinning reserve cost,
|
65
|
+
non-spinning reserve cost, and unserved load penalty.
|
66
|
+
|
67
|
+
Method ``_initial_guess`` is used to make initial guess for commitment decision if all
|
68
|
+
generators are online at initial. It is a simple heuristic method, which may not be optimal.
|
69
|
+
|
70
|
+
Notes
|
71
|
+
-----
|
72
|
+
1. Formulations has been adjusted with interval ``config.t``
|
73
|
+
|
74
|
+
3. The tie-line flow has not been implemented in formulations.
|
75
|
+
|
76
|
+
References
|
77
|
+
----------
|
78
|
+
1. Huang, Y., Pardalos, P. M., & Zheng, Q. P. (2017). Electrical power unit commitment: deterministic and
|
79
|
+
two-stage stochastic programming models and algorithms. Springer.
|
80
|
+
|
81
|
+
2. D. A. Tejada-Arango, S. Lumbreras, P. Sánchez-Martín and A. Ramos, "Which Unit-Commitment Formulation
|
82
|
+
is Best? A Comparison Framework," in IEEE Transactions on Power Systems, vol. 35, no. 4, pp. 2926-2936,
|
83
|
+
July 2020, doi: 10.1109/TPWRS.2019.2962024.
|
84
|
+
"""
|
85
|
+
|
86
|
+
def __init__(self, system, config):
|
87
|
+
DCOPF.__init__(self, system, config)
|
88
|
+
RTEDBase.__init__(self)
|
89
|
+
MPBase.__init__(self)
|
90
|
+
SRBase.__init__(self)
|
91
|
+
NSRBase.__init__(self)
|
92
|
+
|
93
|
+
self.config.add(OrderedDict((('t', 1),
|
94
|
+
)))
|
95
|
+
self.config.add_extra("_help",
|
96
|
+
t="time interval in hours",
|
97
|
+
)
|
98
|
+
self.config.add_extra("_tex",
|
99
|
+
t='T_{cfg}',
|
100
|
+
)
|
101
|
+
|
102
|
+
self.info = 'unit commitment'
|
103
|
+
self.type = 'DCUC'
|
104
|
+
|
105
|
+
# --- Data Section ---
|
106
|
+
# update timeslot model to UCTSlot
|
107
|
+
self.timeslot.info = 'Time slot for multi-period UC'
|
108
|
+
self.timeslot.model = 'UCTSlot'
|
109
|
+
# --- reserve cost ---
|
110
|
+
self.csu = RParam(info='startup cost',
|
111
|
+
name='csu', tex_name=r'c_{su}',
|
112
|
+
model='GCost', src='csu',
|
113
|
+
unit='$',)
|
114
|
+
self.csd = RParam(info='shutdown cost',
|
115
|
+
name='csd', tex_name=r'c_{sd}',
|
116
|
+
model='GCost', src='csd',
|
117
|
+
unit='$',)
|
118
|
+
# --- load ---
|
119
|
+
self.cdp = RParam(info='penalty for unserved load',
|
120
|
+
name='cdp', tex_name=r'c_{d,p}',
|
121
|
+
model='DCost', src='cdp',
|
122
|
+
no_parse=True,
|
123
|
+
unit=r'$/(p.u.*h)',)
|
124
|
+
self.dctrl = RParam(info='load controllability',
|
125
|
+
name='dctrl', tex_name=r'c_{trl,d}',
|
126
|
+
model='StaticLoad', src='ctrl',
|
127
|
+
expand_dims=1,
|
128
|
+
no_parse=True,)
|
129
|
+
# --- gen ---
|
130
|
+
self.td1 = RParam(info='minimum ON duration',
|
131
|
+
name='td1', tex_name=r't_{d1}',
|
132
|
+
model='StaticGen', src='td1',
|
133
|
+
unit='h',)
|
134
|
+
self.td2 = RParam(info='minimum OFF duration',
|
135
|
+
name='td2', tex_name=r't_{d2}',
|
136
|
+
model='StaticGen', src='td2',
|
137
|
+
unit='h',)
|
138
|
+
|
139
|
+
self.sd.info = 'zonal load factor for UC'
|
140
|
+
self.sd.model = 'UCTSlot'
|
141
|
+
|
142
|
+
self.ug.expand_dims = 1
|
143
|
+
self.amin.expand_dims = 1
|
144
|
+
self.amax.expand_dims = 1
|
145
|
+
|
146
|
+
# --- Model Section ---
|
147
|
+
# --- gen ---
|
148
|
+
# NOTE: extend pg to 2D matrix, where row for gen and col for timeslot
|
149
|
+
self.pg.horizon = self.timeslot
|
150
|
+
self.pg.info = '2D Gen power'
|
151
|
+
# TODO: havn't test non-controllability?
|
152
|
+
self.ctrle.u2 = self.tlv
|
153
|
+
self.ctrle.info = 'Reshaped controllability'
|
154
|
+
self.nctrle.u2 = self.tlv
|
155
|
+
self.nctrle.info = 'Reshaped non-controllability'
|
156
|
+
pmaxe = 'mul(mul(nctrl, pg0), ugd) + mul(mul(ctrl, pmax), ugd)'
|
157
|
+
self.pmaxe.e_str = pmaxe
|
158
|
+
self.pmaxe.horizon = self.timeslot
|
159
|
+
pmine = 'mul(mul(ctrl, pmin), ugd) + mul(mul(nctrl, pg0), ugd)'
|
160
|
+
self.pmine.e_str = pmine
|
161
|
+
self.pmine.horizon = self.timeslot
|
162
|
+
self.pglb.e_str = '-pg + pmine'
|
163
|
+
self.pgub.e_str = 'pg - pmaxe'
|
164
|
+
|
165
|
+
self.ugd = Var(info='commitment decision',
|
166
|
+
horizon=self.timeslot,
|
167
|
+
name='ugd', tex_name=r'u_{g,d}',
|
168
|
+
model='StaticGen', src='u',
|
169
|
+
boolean=True,)
|
170
|
+
self.vgd = Var(info='startup action',
|
171
|
+
horizon=self.timeslot,
|
172
|
+
name='vgd', tex_name=r'v_{g,d}',
|
173
|
+
model='StaticGen', src='u',
|
174
|
+
boolean=True,)
|
175
|
+
self.wgd = Var(info='shutdown action',
|
176
|
+
horizon=self.timeslot,
|
177
|
+
name='wgd', tex_name=r'w_{g,d}',
|
178
|
+
model='StaticGen', src='u',
|
179
|
+
boolean=True,)
|
180
|
+
self.zug = Var(info='Aux var, :math:`z_{ug} = u_{g,d} * p_g`',
|
181
|
+
horizon=self.timeslot,
|
182
|
+
name='zug', tex_name=r'z_{ug}',
|
183
|
+
model='StaticGen', pos=True,)
|
184
|
+
# NOTE: actions have two parts: initial status and the rest
|
185
|
+
self.actv = Constraint(name='actv', is_eq=True,
|
186
|
+
info='startup action',
|
187
|
+
e_str='ugd @ Mr - vgd[:, 1:]',)
|
188
|
+
self.actv0 = Constraint(name='actv0', is_eq=True,
|
189
|
+
info='initial startup action',
|
190
|
+
e_str='ugd[:, 0] - ug[:, 0] - vgd[:, 0]',)
|
191
|
+
self.actw = Constraint(name='actw', is_eq=True,
|
192
|
+
info='shutdown action',
|
193
|
+
e_str='-ugd @ Mr - wgd[:, 1:]',)
|
194
|
+
self.actw0 = Constraint(name='actw0', is_eq=True,
|
195
|
+
info='initial shutdown action',
|
196
|
+
e_str='-ugd[:, 0] + ug[:, 0] - wgd[:, 0]',)
|
197
|
+
|
198
|
+
self.prs.horizon = self.timeslot
|
199
|
+
self.prs.info = '2D Spinning reserve'
|
200
|
+
|
201
|
+
self.prns.horizon = self.timeslot
|
202
|
+
self.prns.info = '2D Non-spinning reserve'
|
203
|
+
|
204
|
+
# spinning reserve
|
205
|
+
self.prsb.e_str = 'mul(ugd, mul(pmax, tlv)) - zug - prs'
|
206
|
+
# spinning reserve requirement
|
207
|
+
self.rsr.e_str = '-gs@prs + dsr'
|
208
|
+
|
209
|
+
# non-spinning reserve
|
210
|
+
self.prnsb.e_str = 'mul(1-ugd, mul(pmax, tlv)) - prns'
|
211
|
+
# non-spinning reserve requirement
|
212
|
+
self.rnsr.e_str = '-gs@prns + dnsr'
|
213
|
+
|
214
|
+
# --- big M for ugd*pg ---
|
215
|
+
self.Mzug = NumOp(info='10 times of max of pmax as big M for zug',
|
216
|
+
name='Mzug', tex_name=r'M_{zug}',
|
217
|
+
u=self.pmax, fun=np.max,
|
218
|
+
rfun=np.dot, rargs=dict(b=10),
|
219
|
+
array_out=False,)
|
220
|
+
self.zuglb = Constraint(name='zuglb', info='zug lower bound',
|
221
|
+
is_eq=False, e_str='- zug + pg')
|
222
|
+
self.zugub = Constraint(name='zugub', info='zug upper bound',
|
223
|
+
is_eq=False, e_str='zug - pg - Mzug dot (1 - ugd)')
|
224
|
+
self.zugub2 = Constraint(name='zugub2', info='zug upper bound',
|
225
|
+
is_eq=False, e_str='zug - Mzug dot ugd')
|
226
|
+
|
227
|
+
# --- minimum ON/OFF duration ---
|
228
|
+
self.Con = MinDur(u=self.pg, u2=self.td1,
|
229
|
+
name='Con', tex_name=r'T_{on}',
|
230
|
+
info='minimum ON coefficient',)
|
231
|
+
self.don = Constraint(info='minimum online duration',
|
232
|
+
name='don', is_eq=False,
|
233
|
+
e_str='multiply(Con, vgd) - ugd')
|
234
|
+
self.Coff = MinDur(u=self.pg, u2=self.td2,
|
235
|
+
name='Coff', tex_name=r'T_{off}',
|
236
|
+
info='minimum OFF coefficient',)
|
237
|
+
self.doff = Constraint(info='minimum offline duration',
|
238
|
+
name='doff', is_eq=False,
|
239
|
+
e_str='multiply(Coff, wgd) - (1 - ugd)')
|
240
|
+
|
241
|
+
# --- line ---
|
242
|
+
self.plf.horizon = self.timeslot
|
243
|
+
self.plf.info = '2D Line flow'
|
244
|
+
self.plflb.e_str = '-Bf@aBus - Pfinj - mul(rate_a, tlv)'
|
245
|
+
self.plfub.e_str = 'Bf@aBus + Pfinj - mul(rate_a, tlv)'
|
246
|
+
self.alflb.e_str = '-CftT@aBus - amax@tlv'
|
247
|
+
self.alfub.e_str = 'CftT@aBus - amax@tlv'
|
248
|
+
|
249
|
+
# --- unserved load ---
|
250
|
+
self.pdu = Var(info='unserved demand',
|
251
|
+
name='pdu', tex_name=r'p_{d,u}',
|
252
|
+
model='StaticLoad', unit='p.u.',
|
253
|
+
horizon=self.timeslot,
|
254
|
+
nonneg=True,)
|
255
|
+
self.pdsp = NumOp(u=self.pds, fun=np.clip,
|
256
|
+
args=dict(a_min=0, a_max=None),
|
257
|
+
info='positive demand',
|
258
|
+
name='pdsp', tex_name=r'p_{d,s}^{+}',)
|
259
|
+
self.pdumax = Constraint(info='unserved demand upper bound',
|
260
|
+
name='pdumax', is_eq=False,
|
261
|
+
e_str='pdu - mul(pdsp, dctrl@tlv)')
|
262
|
+
# --- power balance ---
|
263
|
+
# NOTE: nodal balance is also contributed by unserved load
|
264
|
+
pb = 'Bbus@aBus + Pbusinj@tlv + Cl@(pds-pdu) + Csh@gsh@tlv - Cg@pg'
|
265
|
+
self.pb.e_str = pb
|
266
|
+
|
267
|
+
# --- objective ---
|
268
|
+
cost = 't**2 dot sum(c2 @ zug**2 + t dot c1 @ zug)'
|
269
|
+
cost += '+ sum(mul(ug, c0) @ tlv)'
|
270
|
+
_to_sum = 'csu * vgd + csd * wgd + csr @ prs + cnsr @ prns + cdp @ pdu'
|
271
|
+
cost += f' + t dot sum({_to_sum})'
|
272
|
+
self.obj.e_str = cost
|
273
|
+
|
274
|
+
def _initial_guess(self):
|
275
|
+
"""
|
276
|
+
Make initial guess for commitment decision with a priority list
|
277
|
+
defined by the weighted sum of generation cost and generator capacity.
|
278
|
+
|
279
|
+
If there are no offline generators, turn off the first 30% of the generators
|
280
|
+
on the priority list as initial guess.
|
281
|
+
"""
|
282
|
+
# check trigger condition
|
283
|
+
ug0 = self.system.PV.get(src='u', attr='v', idx=self.system.PV.idx.v)
|
284
|
+
if (ug0 == 0).any():
|
285
|
+
return True
|
286
|
+
else:
|
287
|
+
logger.warning('All generators are online at initial, make initial guess for commitment.')
|
288
|
+
|
289
|
+
gen = pd.DataFrame()
|
290
|
+
gen['idx'] = self.system.PV.idx.v
|
291
|
+
gen['pmax'] = self.system.PV.get(src='pmax', attr='v', idx=gen['idx'])
|
292
|
+
gen['bus'] = self.system.PV.get(src='bus', attr='v', idx=gen['idx'])
|
293
|
+
gen['zone'] = self.system.PV.get(src='zone', attr='v', idx=gen['idx'])
|
294
|
+
gcost_idx = self.system.GCost.find_idx(keys='gen', values=gen['idx'])
|
295
|
+
gen['c2'] = self.system.GCost.get(src='c2', attr='v', idx=gcost_idx)
|
296
|
+
gen['c1'] = self.system.GCost.get(src='c1', attr='v', idx=gcost_idx)
|
297
|
+
gen['c0'] = self.system.GCost.get(src='c0', attr='v', idx=gcost_idx)
|
298
|
+
gen['wsum'] = 0.8*gen['pmax'] + 0.05*gen['c2'] + 0.1*gen['c1'] + 0.05*gen['c0']
|
299
|
+
gen = gen.sort_values(by='wsum', ascending=True)
|
300
|
+
|
301
|
+
# Turn off 30% of the generators as initial guess
|
302
|
+
priority = gen['idx'].values
|
303
|
+
g_idx = priority[0:int(0.3*len(priority))]
|
304
|
+
ug0 = np.zeros_like(g_idx)
|
305
|
+
# NOTE: if number of generators is too small, turn off the first one
|
306
|
+
if len(g_idx) == 0:
|
307
|
+
g_idx = priority[0]
|
308
|
+
ug0 = 0
|
309
|
+
off_gen = f'{g_idx}'
|
310
|
+
else:
|
311
|
+
off_gen = ', '.join(g_idx)
|
312
|
+
self.system.StaticGen.set(src='u', idx=g_idx, attr='v', value=ug0)
|
313
|
+
logger.warning(f"As initial commitment guess, turn off StaticGen: {off_gen}")
|
314
|
+
return g_idx
|
315
|
+
|
316
|
+
def init(self, **kwargs):
|
317
|
+
self._initial_guess()
|
318
|
+
return super().init(**kwargs)
|
319
|
+
|
320
|
+
def dc2ac(self, **kwargs):
|
321
|
+
"""
|
322
|
+
AC conversion ``dc2ac`` is not implemented yet for
|
323
|
+
multi-period scheduling.
|
324
|
+
"""
|
325
|
+
return NotImplementedError
|
326
|
+
|
327
|
+
def unpack(self, **kwargs):
|
328
|
+
"""
|
329
|
+
Multi-period scheduling will not unpack results from
|
330
|
+
solver into devices.
|
331
|
+
|
332
|
+
# TODO: unpack first period results, and allow input
|
333
|
+
# to specify which period to unpack.
|
334
|
+
"""
|
335
|
+
return None
|
336
|
+
|
337
|
+
|
338
|
+
class UCDG(UC, DGBase):
|
339
|
+
"""
|
340
|
+
UC with distributed generation :ref:`DG`.
|
341
|
+
|
342
|
+
Note that UCDG only inlcudes DG output power. If ESD1 is included,
|
343
|
+
UCES should be used instead, otherwise there is no SOC.
|
344
|
+
"""
|
345
|
+
|
346
|
+
def __init__(self, system, config):
|
347
|
+
UC.__init__(self, system, config)
|
348
|
+
DGBase.__init__(self)
|
349
|
+
|
350
|
+
self.info = 'unit commitment with distributed generation'
|
351
|
+
self.type = 'DCUC'
|
352
|
+
|
353
|
+
# NOTE: extend vars to 2D
|
354
|
+
self.pgdg.horizon = self.timeslot
|
355
|
+
|
356
|
+
|
357
|
+
class UCES(UC, ESD1MPBase):
|
358
|
+
"""
|
359
|
+
UC with energy storage :ref:`ESD1`.
|
360
|
+
"""
|
361
|
+
|
362
|
+
def __init__(self, system, config):
|
363
|
+
UC.__init__(self, system, config)
|
364
|
+
ESD1MPBase.__init__(self)
|
365
|
+
|
366
|
+
self.info = 'unit commitment with energy storage'
|
367
|
+
self.type = 'DCUC'
|
368
|
+
|
369
|
+
self.pgdg.horizon = self.timeslot
|
370
|
+
self.SOC.horizon = self.timeslot
|
371
|
+
self.pce.horizon = self.timeslot
|
372
|
+
self.pde.horizon = self.timeslot
|
373
|
+
self.uce.horizon = self.timeslot
|
374
|
+
self.ude.horizon = self.timeslot
|
375
|
+
self.zce.horizon = self.timeslot
|
376
|
+
self.zde.horizon = self.timeslot
|
ams/shared.py
CHANGED
@@ -4,34 +4,62 @@ Shared constants and delayed imports.
|
|
4
4
|
This module is supplementary to the ``andes.shared`` module.
|
5
5
|
"""
|
6
6
|
import logging
|
7
|
+
import unittest
|
7
8
|
from functools import wraps
|
8
|
-
from datetime import datetime
|
9
9
|
from collections import OrderedDict
|
10
10
|
|
11
11
|
import cvxpy as cp
|
12
12
|
|
13
|
-
from andes.shared import pd
|
14
13
|
from andes.utils.lazyimport import LazyImport
|
15
14
|
|
15
|
+
from andes.system import System as adSystem
|
16
|
+
|
17
|
+
|
16
18
|
logger = logging.getLogger(__name__)
|
17
19
|
|
18
20
|
sps = LazyImport('import scipy.sparse as sps')
|
19
21
|
np = LazyImport('import numpy as np')
|
22
|
+
pd = LazyImport('import pandas as pd')
|
23
|
+
|
24
|
+
# --- an empty ANDES system ---
|
25
|
+
empty_adsys = adSystem(autogen_stale=False)
|
26
|
+
ad_models = list(empty_adsys.models.keys())
|
20
27
|
|
21
28
|
# --- NumPy constants ---
|
22
29
|
# NOTE: In NumPy 2.0, np.Inf and np.NaN are deprecated.
|
23
30
|
inf = np.inf
|
24
31
|
nan = np.nan
|
25
32
|
|
26
|
-
#
|
27
|
-
|
33
|
+
# --- misc constants ---
|
34
|
+
_prefix = r" - --------------> | " # NOQA
|
35
|
+
_max_length = 80 # NOQA
|
36
|
+
|
37
|
+
# NOTE: copyright
|
38
|
+
copyright_msg = 'Copyright (C) 2023-2024 Jinning Wang'
|
39
|
+
|
40
|
+
# NOTE: copied from CVXPY documentation, last checked on 2024/10/30, v1.5
|
41
|
+
mip_solvers = ['CBC', 'COPT', 'GLPK_MI', 'CPLEX', 'GUROBI',
|
28
42
|
'MOSEK', 'SCIP', 'XPRESS', 'SCIPY']
|
29
43
|
|
30
|
-
|
44
|
+
misocp_solvers = ['MOSEK', 'CPLEX', 'GUROBI', 'XPRESS', 'SCIP']
|
31
45
|
|
32
|
-
|
33
|
-
|
34
|
-
|
46
|
+
installed_solvers = cp.installed_solvers()
|
47
|
+
|
48
|
+
installed_mip_solvers = [s for s in installed_solvers if s in mip_solvers]
|
49
|
+
|
50
|
+
|
51
|
+
def require_MISOCP_solver(f):
|
52
|
+
"""
|
53
|
+
Decorator for functions that require MISOCP solver.
|
54
|
+
"""
|
55
|
+
|
56
|
+
@wraps(f)
|
57
|
+
def wrapper(*args, **kwargs):
|
58
|
+
if not any(s in misocp_solvers for s in installed_solvers):
|
59
|
+
raise ModuleNotFoundError("No MISOCP solver is available.")
|
60
|
+
return f(*args, **kwargs)
|
61
|
+
|
62
|
+
return wrapper
|
35
63
|
|
36
64
|
|
37
65
|
def require_MIP_solver(f):
|
@@ -41,13 +69,39 @@ def require_MIP_solver(f):
|
|
41
69
|
|
42
70
|
@wraps(f)
|
43
71
|
def wrapper(*args, **kwargs):
|
44
|
-
if not any(s in
|
72
|
+
if not any(s in mip_solvers for s in installed_solvers):
|
45
73
|
raise ModuleNotFoundError("No MIP solver is available.")
|
46
74
|
return f(*args, **kwargs)
|
47
75
|
|
48
76
|
return wrapper
|
49
77
|
|
50
78
|
|
79
|
+
def skip_unittest_without_MIP(f):
|
80
|
+
"""
|
81
|
+
Decorator for skipping tests that require MIP solver.
|
82
|
+
"""
|
83
|
+
def wrapper(*args, **kwargs):
|
84
|
+
if any(s in mip_solvers for s in installed_solvers):
|
85
|
+
pass
|
86
|
+
else:
|
87
|
+
raise unittest.SkipTest("No MIP solver is available.")
|
88
|
+
return f(*args, **kwargs)
|
89
|
+
return wrapper
|
90
|
+
|
91
|
+
|
92
|
+
def skip_unittest_without_MISOCP(f):
|
93
|
+
"""
|
94
|
+
Decorator for skipping tests that require MISOCP solver.
|
95
|
+
"""
|
96
|
+
def wrapper(*args, **kwargs):
|
97
|
+
if any(s in misocp_solvers for s in installed_solvers):
|
98
|
+
pass
|
99
|
+
else:
|
100
|
+
raise unittest.SkipTest("No MISOCP solver is available.")
|
101
|
+
return f(*args, **kwargs)
|
102
|
+
return wrapper
|
103
|
+
|
104
|
+
|
51
105
|
ppc_cols = OrderedDict([
|
52
106
|
('bus', ['bus_i', 'type', 'pd', 'qd', 'gs', 'bs', 'area', 'vm', 'va',
|
53
107
|
'baseKV', 'zone', 'vmax', 'vmin', 'lam_p', 'lam_q',
|
ams/system.py
CHANGED
@@ -17,14 +17,14 @@ from andes.variables import FileMan
|
|
17
17
|
from andes.utils.misc import elapsed
|
18
18
|
from andes.utils.tab import Tab
|
19
19
|
|
20
|
-
import ams
|
20
|
+
import ams
|
21
21
|
from ams.models.group import GroupBase
|
22
22
|
from ams.routines.type import TypeBase
|
23
23
|
from ams.models import file_classes
|
24
24
|
from ams.routines import all_routines
|
25
25
|
from ams.utils.paths import get_config_path
|
26
26
|
from ams.core.matprocessor import MatProcessor
|
27
|
-
from ams.
|
27
|
+
from ams.interface import to_andes
|
28
28
|
from ams.report import Report
|
29
29
|
|
30
30
|
logger = logging.getLogger(__name__)
|
@@ -229,10 +229,13 @@ class System(andes_System):
|
|
229
229
|
|
230
230
|
def _collect_group_data(self, items):
|
231
231
|
"""
|
232
|
-
Set the owner for routine attributes:
|
232
|
+
Set the owner for routine attributes: `RParam`, `Var`, `ExpressionCalc`, `Expression`,
|
233
|
+
and `RBaseService`.
|
233
234
|
"""
|
234
235
|
for item_name, item in items.items():
|
235
|
-
if item.model
|
236
|
+
if item.model is None:
|
237
|
+
continue
|
238
|
+
elif item.model in self.groups.keys():
|
236
239
|
item.is_group = True
|
237
240
|
item.owner = self.groups[item.model]
|
238
241
|
elif item.model in self.models.keys():
|
@@ -271,12 +274,18 @@ class System(andes_System):
|
|
271
274
|
type_instance = self.types[type_name]
|
272
275
|
type_instance.routines[vname] = rtn
|
273
276
|
# self.types[rtn.type].routines[vname] = rtn
|
274
|
-
# Collect
|
277
|
+
# Collect RParams
|
275
278
|
rparams = getattr(rtn, 'rparams')
|
276
279
|
self._collect_group_data(rparams)
|
277
|
-
# Collect routine
|
280
|
+
# Collect routine Vars
|
278
281
|
r_vars = getattr(rtn, 'vars')
|
279
282
|
self._collect_group_data(r_vars)
|
283
|
+
# Collect ExpressionCalcs
|
284
|
+
exprc = getattr(rtn, 'exprcs')
|
285
|
+
self._collect_group_data(exprc)
|
286
|
+
# Collect Expressions
|
287
|
+
expr = getattr(rtn, 'exprs')
|
288
|
+
self._collect_group_data(expr)
|
280
289
|
|
281
290
|
def import_groups(self):
|
282
291
|
"""
|
@@ -436,10 +445,10 @@ class System(andes_System):
|
|
436
445
|
|
437
446
|
# assign bus type as placeholder; 1=PQ, 2=PV, 3=ref, 4=isolated
|
438
447
|
if self.Bus.type.v.sum() == self.Bus.n: # if all type are PQ
|
439
|
-
self.Bus.
|
440
|
-
|
441
|
-
self.Bus.
|
442
|
-
|
448
|
+
self.Bus.alter(src='type', idx=self.PV.bus.v,
|
449
|
+
value=np.ones(self.PV.n))
|
450
|
+
self.Bus.alter(src='type', idx=self.Slack.bus.v,
|
451
|
+
value=np.ones(self.Slack.n))
|
443
452
|
|
444
453
|
# --- assign column and row names ---
|
445
454
|
self.mats.Cft.col_names = self.Line.idx.v
|
@@ -448,7 +457,7 @@ class System(andes_System):
|
|
448
457
|
self.mats.CftT.col_names = self.Bus.idx.v
|
449
458
|
self.mats.CftT.row_names = self.Line.idx.v
|
450
459
|
|
451
|
-
self.mats.Cg.col_names = self.StaticGen.
|
460
|
+
self.mats.Cg.col_names = self.StaticGen.get_all_idxes()
|
452
461
|
self.mats.Cg.row_names = self.Bus.idx.v
|
453
462
|
|
454
463
|
self.mats.Cl.col_names = self.PQ.idx.v
|
@@ -533,7 +542,11 @@ class System(andes_System):
|
|
533
542
|
|
534
543
|
raise NotImplementedError
|
535
544
|
|
536
|
-
def to_andes(self,
|
545
|
+
def to_andes(self, addfile=None,
|
546
|
+
setup=False, no_output=False,
|
547
|
+
default_config=True,
|
548
|
+
verify=False, tol=1e-3,
|
549
|
+
**kwargs):
|
537
550
|
"""
|
538
551
|
Convert the AMS system to an ANDES system.
|
539
552
|
|
@@ -543,30 +556,56 @@ class System(andes_System):
|
|
543
556
|
3. Power flow models are in the same shape as the AMS system.
|
544
557
|
4. Dynamic models, if any, are in the same shape as the AMS system.
|
545
558
|
|
559
|
+
This function is wrapped as the ``System`` class method ``to_andes()``.
|
560
|
+
Using the file conversion ``to_andes()`` will automatically
|
561
|
+
link the AMS system instance to the converted ANDES system instance
|
562
|
+
in the AMS system attribute ``dyn``.
|
563
|
+
|
564
|
+
It should be noted that detailed dynamic simualtion requires extra
|
565
|
+
dynamic models to be added to the ANDES system, which can be passed
|
566
|
+
through the ``addfile`` argument.
|
567
|
+
|
546
568
|
Parameters
|
547
569
|
----------
|
548
|
-
|
549
|
-
|
570
|
+
system : System
|
571
|
+
The AMS system to be converted to ANDES format.
|
550
572
|
addfile : str, optional
|
551
573
|
The additional file to be converted to ANDES dynamic mdoels.
|
552
|
-
|
553
|
-
|
574
|
+
setup : bool, optional
|
575
|
+
Whether to call `setup()` after the conversion. Default is True.
|
576
|
+
no_output : bool, optional
|
577
|
+
To ANDES system.
|
578
|
+
default_config : bool, optional
|
579
|
+
To ANDES system.
|
580
|
+
verify : bool
|
581
|
+
If True, the converted ANDES system will be verified with the source
|
582
|
+
AMS system using AC power flow.
|
583
|
+
tol : float
|
584
|
+
The tolerance of error.
|
554
585
|
|
555
586
|
Returns
|
556
587
|
-------
|
557
|
-
|
588
|
+
adsys : andes.system.System
|
558
589
|
The converted ANDES system.
|
559
590
|
|
560
591
|
Examples
|
561
592
|
--------
|
562
593
|
>>> import ams
|
563
594
|
>>> import andes
|
564
|
-
>>> sp = ams.load(ams.get_case('ieee14/
|
565
|
-
>>> sa = sp.to_andes(
|
566
|
-
...
|
567
|
-
|
595
|
+
>>> sp = ams.load(ams.get_case('ieee14/ieee14_uced.xlsx'), setup=True)
|
596
|
+
>>> sa = sp.to_andes(addfile=andes.get_case('ieee14/ieee14_full.xlsx'),
|
597
|
+
... setup=False, overwrite=True, no_output=True)
|
598
|
+
|
599
|
+
Notes
|
600
|
+
-----
|
601
|
+
1. Power flow models in the addfile will be skipped and only dynamic models will be used.
|
602
|
+
2. The addfile format is guessed based on the file extension. Currently only ``xlsx`` is supported.
|
603
|
+
3. Index in the addfile is automatically adjusted when necessary.
|
568
604
|
"""
|
569
|
-
return to_andes(self,
|
605
|
+
return to_andes(system=self, addfile=addfile,
|
606
|
+
setup=setup, no_output=no_output,
|
607
|
+
default_config=default_config,
|
608
|
+
verify=verify, tol=tol,
|
570
609
|
**kwargs)
|
571
610
|
|
572
611
|
def summary(self):
|
ams/utils/__init__.py
ADDED