openTEPES 4.18.7__py3-none-any.whl → 4.18.8__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.
- openTEPES/__init__.py +1 -1
- openTEPES/openTEPES.py +3 -4
- openTEPES/openTEPES_InputData.py +209 -109
- openTEPES/openTEPES_Main.py +17 -7
- openTEPES/openTEPES_ModelFormulation.py +101 -60
- openTEPES/openTEPES_OutputResults.py +280 -140
- openTEPES/openTEPES_ProblemSolving.py +30 -29
- openTEPES/sSEP/oT_Data_RampReserveDown_sSEP.csv +8737 -0
- openTEPES/sSEP/oT_Data_RampReserveUp_sSEP.csv +8737 -0
- {opentepes-4.18.7.dist-info → opentepes-4.18.8.dist-info}/METADATA +6 -5
- {opentepes-4.18.7.dist-info → opentepes-4.18.8.dist-info}/RECORD +14 -12
- {opentepes-4.18.7.dist-info → opentepes-4.18.8.dist-info}/WHEEL +0 -0
- {opentepes-4.18.7.dist-info → opentepes-4.18.8.dist-info}/entry_points.txt +0 -0
- {opentepes-4.18.7.dist-info → opentepes-4.18.8.dist-info}/licenses/LICENSE +0 -0
openTEPES/openTEPES_Main.py
CHANGED
|
@@ -660,7 +660,7 @@
|
|
|
660
660
|
# For more information on this, and how to apply and follow the GNU AGPL, see
|
|
661
661
|
# <https://www.gnu.org/licenses/>.
|
|
662
662
|
|
|
663
|
-
# Open Generation, Storage, and Transmission Operation and Expansion Planning Model with RES and ESS (openTEPES) -
|
|
663
|
+
# Open Generation, Storage, and Transmission Operation and Expansion Planning Model with RES and ESS (openTEPES) - January 19, 2026
|
|
664
664
|
# simplicity and transparency in power systems planning
|
|
665
665
|
|
|
666
666
|
# Developed by
|
|
@@ -681,13 +681,20 @@
|
|
|
681
681
|
|
|
682
682
|
#%% libraries
|
|
683
683
|
import argparse
|
|
684
|
+
import platform
|
|
685
|
+
import psutil
|
|
684
686
|
import os
|
|
685
687
|
import time
|
|
686
688
|
# import pkg_resources
|
|
687
689
|
from .openTEPES import openTEPES_run
|
|
688
690
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
+
|
|
692
|
+
GREEN = "\033[32m"
|
|
693
|
+
BLUE = "\033[34m"
|
|
694
|
+
RESET = "\033[0m"
|
|
695
|
+
|
|
696
|
+
print(GREEN + 'Open Generation, Storage, and Transmission Operation and Expansion Planning Model with RES and ESS (openTEPES) - Version 4.18.8 - January 19, 2026' + RESET)
|
|
697
|
+
print(BLUE + '#### Academic research license - for non-commercial use only ####' + RESET + '\n')
|
|
691
698
|
|
|
692
699
|
parser = argparse.ArgumentParser(description='Introducing main parameters...')
|
|
693
700
|
parser.add_argument('--case', type=str, default=None)
|
|
@@ -698,7 +705,7 @@ parser.add_argument('--result', type=str, default=None)
|
|
|
698
705
|
|
|
699
706
|
DIR = os.path.dirname(__file__)
|
|
700
707
|
CASE = '9n'
|
|
701
|
-
SOLVER = '
|
|
708
|
+
SOLVER = 'highs' # 'gams', 'appsi_highs', 'gurobi'
|
|
702
709
|
RESULT = 'Yes'
|
|
703
710
|
LOG = 'No'
|
|
704
711
|
|
|
@@ -746,12 +753,15 @@ def main():
|
|
|
746
753
|
print('Total time ... {} s'.format(ElapsedTime))
|
|
747
754
|
path_to_write_time = os.path.join(args.dir,args.case,f'openTEPES_time_{CASE}.log')
|
|
748
755
|
with open(path_to_write_time, 'w') as f:
|
|
749
|
-
f.write('Elapsed time '+str(ElapsedTime)+' s')
|
|
756
|
+
f.write(('Elapsed time ' + str(ElapsedTime) + ' s on ' + platform.node() + ' [' + platform.system() + ' ' +
|
|
757
|
+
platform.release() + '; ' + str(psutil.cpu_count(logical=False)) + ' ' + platform.processor() +
|
|
758
|
+
' physical cores at ' + str(round(psutil.cpu_freq().current/1000,2)) + ' GHz; ' +
|
|
759
|
+
str(round(psutil.virtual_memory().total/(1024**3),1)) + ' GB]\n'))
|
|
750
760
|
# Final message
|
|
751
761
|
print('End of the run ************')
|
|
752
|
-
print('
|
|
762
|
+
print(BLUE + '#### Academic research license - for non-commercial use only ####' + RESET)
|
|
753
763
|
|
|
754
764
|
return model
|
|
755
765
|
|
|
756
766
|
if __name__ == '__main__':
|
|
757
|
-
model = main()
|
|
767
|
+
model = main()
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Open Generation, Storage, and Transmission Operation and Expansion Planning Model with RES and ESS (openTEPES) -
|
|
2
|
+
Open Generation, Storage, and Transmission Operation and Expansion Planning Model with RES and ESS (openTEPES) - January 15, 2026
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import time
|
|
@@ -9,7 +9,10 @@ import pandas as pd
|
|
|
9
9
|
from collections import defaultdict
|
|
10
10
|
from pyomo.environ import Constraint, Objective, minimize, Set, RangeSet, Param
|
|
11
11
|
|
|
12
|
+
# from line_profiler import profile
|
|
12
13
|
|
|
14
|
+
|
|
15
|
+
# @profile
|
|
13
16
|
def TotalObjectiveFunction(OptModel, mTEPES, pIndLogConsole):
|
|
14
17
|
print('Total cost o.f. model formulation ****')
|
|
15
18
|
|
|
@@ -28,6 +31,7 @@ def TotalObjectiveFunction(OptModel, mTEPES, pIndLogConsole):
|
|
|
28
31
|
print('Total fixed and variable costs ... ', round(GeneratingTime), 's')
|
|
29
32
|
|
|
30
33
|
|
|
34
|
+
# @profile
|
|
31
35
|
def InvestmentModelFormulation(OptModel, mTEPES, pIndLogConsole):
|
|
32
36
|
print('Investment model formulation ****')
|
|
33
37
|
|
|
@@ -108,6 +112,7 @@ def InvestmentModelFormulation(OptModel, mTEPES, pIndLogConsole):
|
|
|
108
112
|
print('Gen&transm investment o.f./constraints ... ', round(GeneratingTime), 's')
|
|
109
113
|
|
|
110
114
|
|
|
115
|
+
# @profile
|
|
111
116
|
def GenerationOperationModelFormulationObjFunct(OptModel, mTEPES, pIndLogConsole, p, sc, st):
|
|
112
117
|
print('Generation oper o.f. model formulation ****')
|
|
113
118
|
|
|
@@ -145,22 +150,22 @@ def GenerationOperationModelFormulationObjFunct(OptModel, mTEPES, pIndLogConsole
|
|
|
145
150
|
h2n[nd].append(hp)
|
|
146
151
|
|
|
147
152
|
def eTotalGCost(OptModel,n):
|
|
148
|
-
return OptModel.vTotalGCost[p,sc,n] == (
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
153
|
+
return OptModel.vTotalGCost[p,sc,n] == (mTEPES.pLoadLevelDuration[p,sc,n]() * sum(mTEPES.pLinearVarCost [p,sc,n,nr] * OptModel.vTotalOutput [p,sc,n,nr] +
|
|
154
|
+
mTEPES.pConstantVarCost[p,sc,n,nr] * OptModel.vCommitment [p,sc,n,nr] for nr in mTEPES.nr if (p,nr) in mTEPES.pnr) +
|
|
155
|
+
mTEPES.pLoadLevelWeight [p,sc,n]() * sum(mTEPES.pStartUpCost [ nr] * OptModel.vStartUp [p,sc,n,nr] +
|
|
156
|
+
mTEPES.pShutDownCost [ nr] * OptModel.vShutDown [p,sc,n,nr] for nr in mTEPES.nr if (p,nr) in mTEPES.pnr) +
|
|
157
|
+
mTEPES.pLoadLevelWeight [p,sc,n]() * sum(mTEPES.pOperReserveCost[ nr] * OptModel.vReserveUp [p,sc,n,nr] +
|
|
158
|
+
mTEPES.pOperReserveCost[ nr] * OptModel.vReserveDown [p,sc,n,nr] for nr in mTEPES.nr if (p,nr) in mTEPES.pnr and mTEPES.pIndOperReserveGen[nr] == 0) +
|
|
159
|
+
mTEPES.pLoadLevelWeight [p,sc,n]() * sum(mTEPES.pOperReserveCost[ eh] * OptModel.vESSReserveUp [p,sc,n,eh] +
|
|
160
|
+
mTEPES.pOperReserveCost[ eh] * OptModel.vESSReserveDown [p,sc,n,eh] for eh in mTEPES.eh if (p,eh) in mTEPES.peh and mTEPES.pIndOperReserveCon[eh] == 0) +
|
|
161
|
+
mTEPES.pLoadLevelDuration[p,sc,n]() * sum(mTEPES.pLinearVarCost [p,sc,n,bo] * OptModel.vTotalOutputHeat[p,sc,n,bo] for bo in mTEPES.bo if (p,bo) in mTEPES.pbo) +
|
|
162
|
+
mTEPES.pLoadLevelDuration[p,sc,n]() * sum(mTEPES.pLinearOMCost [ re] * OptModel.vTotalOutput [p,sc,n,re] for re in mTEPES.re if (p,re) in mTEPES.pre) )
|
|
158
163
|
setattr(OptModel, f'eTotalGCost_{p}_{sc}_{st}', Constraint(mTEPES.n, rule=eTotalGCost, doc='system variable generation operation cost [MEUR]'))
|
|
159
164
|
|
|
160
165
|
# the small tolerance pEpsilon=1e-5 is added to avoid pumping/charging with curtailment/spillage
|
|
161
166
|
pEpsilon = 1e-5
|
|
162
167
|
def eTotalCCost(OptModel,n):
|
|
163
|
-
return OptModel.vTotalCCost [p,sc,n] ==
|
|
168
|
+
return OptModel.vTotalCCost [p,sc,n] == mTEPES.pLoadLevelDuration[p,sc,n]() * sum((mTEPES.pLinearVarCost[p,sc,n,eh]+pEpsilon) * OptModel.vESSTotalCharge[p,sc,n,eh] for eh in mTEPES.eh if (p,eh) in mTEPES.peh)
|
|
164
169
|
setattr(OptModel, f'eTotalCCost_{p}_{sc}_{st}', Constraint(mTEPES.n, rule=eTotalCCost, doc='system variable consumption operation cost [MEUR]'))
|
|
165
170
|
|
|
166
171
|
def eTotalECost(OptModel,n):
|
|
@@ -172,40 +177,40 @@ def GenerationOperationModelFormulationObjFunct(OptModel, mTEPES, pIndLogConsole
|
|
|
172
177
|
|
|
173
178
|
def eTotalEmissionArea(OptModel,n,ar):
|
|
174
179
|
if mTEPES.pEmission[p,ar] < math.inf and sum(mTEPES.pEmissionRate[g] for g in mTEPES.g if (ar, g) in mTEPES.a2g and (p, g) in mTEPES.pg):
|
|
175
|
-
return OptModel.vTotalEmissionArea[p,sc,n,ar] == (
|
|
176
|
-
+
|
|
180
|
+
return OptModel.vTotalEmissionArea[p,sc,n,ar] == (mTEPES.pLoadLevelDuration[p,sc,n]() * sum(mTEPES.pEmissionRate[nr] * 1e-3 * OptModel.vTotalOutput [p,sc,n,nr] for nr in mTEPES.nr if (ar,nr) in mTEPES.a2g and (p,nr) in mTEPES.pnr) #1e-3 to change from tCO2/MWh to MtCO2/GWh
|
|
181
|
+
+ mTEPES.pLoadLevelDuration[p,sc,n]() * sum(mTEPES.pEmissionRate[bo] * 1e-3 * OptModel.vTotalOutputHeat[p,sc,n,bo] for bo in mTEPES.bo if (ar,bo) in mTEPES.a2g and (p,bo) in mTEPES.pbo)) #1e-3 to change from tCO2/MWh to MtCO2/GWh
|
|
177
182
|
else:
|
|
178
183
|
return Constraint.Skip
|
|
179
184
|
setattr(OptModel, f'eTotalEmissionArea_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.ar, rule=eTotalEmissionArea, doc='area total emission [MtCO2 eq]'))
|
|
180
185
|
|
|
181
186
|
def eTotalECostArea(OptModel,n,ar):
|
|
182
187
|
if sum(mTEPES.pEmissionVarCost[p,sc,n,g] for g in mTEPES.g if (ar,g) in mTEPES.a2g and (p,g) in mTEPES.pg):
|
|
183
|
-
return OptModel.vTotalECostArea[p,sc,n,ar] == (
|
|
184
|
-
|
|
188
|
+
return OptModel.vTotalECostArea[p,sc,n,ar] == (mTEPES.pLoadLevelDuration[p,sc,n]() * (sum(mTEPES.pEmissionVarCost[p,sc,n, g] * OptModel.vTotalOutput [p,sc,n, g] for g in mTEPES.g if (ar, g) in mTEPES.a2g and (p, g) in mTEPES.pg )
|
|
189
|
+
+ sum(mTEPES.pEmissionVarCost[p,sc,n,bo] * OptModel.vTotalOutputHeat[p,sc,n,bo] for bo in mTEPES.bo if (ar,bo) in mTEPES.a2g and (p,bo) in mTEPES.pbo)))
|
|
185
190
|
else:
|
|
186
191
|
return Constraint.Skip
|
|
187
192
|
setattr(OptModel, f'eTotalECostArea_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.ar, rule=eTotalECostArea, doc='area emission cost [MEUR]'))
|
|
188
193
|
|
|
189
194
|
def eTotalRESEnergyArea(OptModel,n,ar):
|
|
190
195
|
if mTEPES.pRESEnergy[p,ar] and st == mTEPES.Last_st:
|
|
191
|
-
return OptModel.vTotalRESEnergyArea[p,sc,n,ar] ==
|
|
196
|
+
return OptModel.vTotalRESEnergyArea[p,sc,n,ar] == mTEPES.pLoadLevelDuration[p,sc,n]() * sum(OptModel.vTotalOutput[p,sc,n,re] for re in mTEPES.re if (ar,re) in mTEPES.a2g and (p,re) in mTEPES.pre)
|
|
192
197
|
else:
|
|
193
198
|
return Constraint.Skip
|
|
194
199
|
setattr(OptModel, f'eTotalRESEnergyArea_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.ar, rule=eTotalRESEnergyArea, doc='area RES energy [GWh]'))
|
|
195
200
|
|
|
196
201
|
def eTotalNCost(OptModel,n):
|
|
197
|
-
return OptModel.vTotalNCost[p,sc,n] ==
|
|
202
|
+
return OptModel.vTotalNCost[p,sc,n] == mTEPES.pLoadLevelDuration[p,sc,n]() * pEpsilon * sum(OptModel.vLineLosses[p,sc,n,ni,nf,cc] for ni,nf,cc in mTEPES.ll if (p,ni,nf,cc) in mTEPES.pll)
|
|
198
203
|
setattr(OptModel, f'eTotalNCost_{p}_{sc}_{st}', Constraint(mTEPES.n, rule=eTotalNCost, doc='system variable network operation cost [MEUR]'))
|
|
199
204
|
|
|
200
205
|
def eTotalRCost(OptModel,n):
|
|
201
206
|
if mTEPES.pIndHydrogen == 0 and mTEPES.pIndHeat == 0:
|
|
202
|
-
return OptModel.vTotalRCost[p,sc,n] ==
|
|
207
|
+
return OptModel.vTotalRCost[p,sc,n] == mTEPES.pLoadLevelDuration[p,sc,n]() * mTEPES.pENSCost * sum(OptModel.vENS[p,sc,n,nd] for nd in mTEPES.nd)
|
|
203
208
|
elif mTEPES.pIndHydrogen == 1 and mTEPES.pIndHeat == 0:
|
|
204
|
-
return OptModel.vTotalRCost[p,sc,n] ==
|
|
209
|
+
return OptModel.vTotalRCost[p,sc,n] == mTEPES.pLoadLevelDuration[p,sc,n]() * mTEPES.pENSCost * sum(OptModel.vENS[p,sc,n,nd] for nd in mTEPES.nd) + mTEPES.pH2NSCost * sum(OptModel.vH2NS[p,sc,n,nd] for nd in mTEPES.nd if sum(1 for el in e2n[nd]) + sum(1 for nf,cc in lout[nd]) + sum(1 for ni,cc in lin[nd]))
|
|
205
210
|
elif mTEPES.pIndHydrogen == 0 and mTEPES.pIndHeat == 1:
|
|
206
|
-
return OptModel.vTotalRCost[p,sc,n] ==
|
|
211
|
+
return OptModel.vTotalRCost[p,sc,n] == mTEPES.pLoadLevelDuration[p,sc,n]() * mTEPES.pENSCost * sum(OptModel.vENS[p,sc,n,nd] for nd in mTEPES.nd) + mTEPES.pLoadLevelDuration[p,sc,n]() * mTEPES.pHeatNSCost * sum(OptModel.vHeatNS[p,sc,n,nd] for nd in mTEPES.nd if sum(1 for ch in c2n[nd]) + sum(1 for hp in h2n[nd]) + sum(1 for nf,cc in lout[nd]) + sum(1 for ni,cc in lin[nd]))
|
|
207
212
|
elif mTEPES.pIndHydrogen == 1 and mTEPES.pIndHeat == 1:
|
|
208
|
-
return OptModel.vTotalRCost[p,sc,n] ==
|
|
213
|
+
return OptModel.vTotalRCost[p,sc,n] == mTEPES.pLoadLevelDuration[p,sc,n]() * mTEPES.pENSCost * sum(OptModel.vENS[p,sc,n,nd] for nd in mTEPES.nd) + mTEPES.pH2NSCost * sum(OptModel.vH2NS[p,sc,n,nd] for nd in mTEPES.nd if sum(1 for el in e2n[nd]) + sum(1 for nf,cc in lout[nd]) + sum(1 for ni,cc in lin[nd])) + mTEPES.pLoadLevelDuration[p,sc,n]() * mTEPES.pHeatNSCost * sum(OptModel.vHeatNS[p,sc,n,nd] for nd in mTEPES.nd if sum(1 for ch in c2n[nd]) + sum(1 for hp in h2n[nd]) + sum(1 for nf,cc in lout[nd]) + sum(1 for ni,cc in lin[nd]))
|
|
209
214
|
setattr(OptModel, f'eTotalRCost_{p}_{sc}_{st}', Constraint(mTEPES.n, rule=eTotalRCost, doc='system reliability cost [MEUR]'))
|
|
210
215
|
|
|
211
216
|
GeneratingTime = time.time() - StartTime
|
|
@@ -213,13 +218,14 @@ def GenerationOperationModelFormulationObjFunct(OptModel, mTEPES, pIndLogConsole
|
|
|
213
218
|
print('Operation cost o.f. ... ', round(GeneratingTime), 's')
|
|
214
219
|
|
|
215
220
|
|
|
221
|
+
# @profile
|
|
216
222
|
def GenerationOperationModelFormulationInvestment(OptModel, mTEPES, pIndLogConsole, p, sc, st):
|
|
217
223
|
print('Investment & operation var constraints ****')
|
|
218
224
|
|
|
219
225
|
StartTime = time.time()
|
|
220
226
|
|
|
221
227
|
def eInstallGenComm(OptModel,n,gc):
|
|
222
|
-
if gc in mTEPES.nr and gc not in mTEPES.eh and gc not in mTEPES.bc and (mTEPES.pMinPowerElec[p,sc,n,gc] or mTEPES.pConstantVarCost[p,sc,n,gc]):
|
|
228
|
+
if gc in mTEPES.nr and gc not in mTEPES.eh and gc not in mTEPES.bc and (p,gc) in mTEPES.pgc and (mTEPES.pMinPowerElec[p,sc,n,gc] or mTEPES.pConstantVarCost[p,sc,n,gc]):
|
|
223
229
|
if mTEPES.pMustRun[gc] == 0:
|
|
224
230
|
return OptModel.vCommitment[p,sc,n,gc] <= OptModel.vGenerationInvest[p,gc]
|
|
225
231
|
else:
|
|
@@ -232,8 +238,11 @@ def GenerationOperationModelFormulationInvestment(OptModel, mTEPES, pIndLogConso
|
|
|
232
238
|
print('eInstallGenComm ... ', len(getattr(OptModel, f'eInstallGenComm_{p}_{sc}_{st}')), ' rows')
|
|
233
239
|
|
|
234
240
|
def eInstallESSComm(OptModel,n,ec):
|
|
235
|
-
if mTEPES.
|
|
236
|
-
|
|
241
|
+
if (p,ec) in mTEPES.pec:
|
|
242
|
+
if mTEPES.pIndBinStorInvest[ec]:
|
|
243
|
+
return OptModel.vCommitment[p,sc,n,ec] <= OptModel.vGenerationInvest[p,ec]
|
|
244
|
+
else:
|
|
245
|
+
return Constraint.Skip
|
|
237
246
|
else:
|
|
238
247
|
return Constraint.Skip
|
|
239
248
|
setattr(OptModel, f'eInstallESSComm_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.ec, rule=eInstallESSComm, doc='commitment if ESS unit [p.u.]'))
|
|
@@ -359,6 +368,7 @@ def GenerationOperationModelFormulationInvestment(OptModel, mTEPES, pIndLogConso
|
|
|
359
368
|
print('Generating operation & investment ... ', round(GeneratingTime), 's')
|
|
360
369
|
|
|
361
370
|
|
|
371
|
+
# @profile
|
|
362
372
|
def GenerationOperationModelFormulationDemand(OptModel, mTEPES, pIndLogConsole, p, sc, st):
|
|
363
373
|
print('Inertia, oper resr, demand constraints ****')
|
|
364
374
|
|
|
@@ -417,7 +427,6 @@ def GenerationOperationModelFormulationDemand(OptModel, mTEPES, pIndLogConsole,
|
|
|
417
427
|
if (sum(1 for nr in n2a[ar] if (mTEPES.pIndOperReserveGen[nr] == 0 or mTEPES.pIndOperReserveCon[nr] == 0 ))
|
|
418
428
|
+ sum(1 for eh in e2a[ar] if (mTEPES.pIndOperReserveGen[eh] == 0 or mTEPES.pIndOperReserveCon[eh] == 0 )) == 0):
|
|
419
429
|
return Constraint.Skip
|
|
420
|
-
|
|
421
430
|
return sum(OptModel.vReserveUp [p,sc,n,nr] for nr in n2a[ar] if mTEPES.pIndOperReserveGen[nr] == 0 and (p,nr) in mTEPES.pnr) + sum(OptModel.vESSReserveUp [p,sc,n,eh] for eh in e2a[ar] if mTEPES.pIndOperReserveCon[eh] == 0 and (p,eh) in mTEPES.peh) == mTEPES.pOperReserveUp[p,sc,n,ar]
|
|
422
431
|
|
|
423
432
|
setattr(OptModel, f'eOperReserveUp_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.ar, rule=eOperReserveUp, doc='up operating reserve [GW]'))
|
|
@@ -433,7 +442,6 @@ def GenerationOperationModelFormulationDemand(OptModel, mTEPES, pIndLogConsole,
|
|
|
433
442
|
if (sum(1 for nr in n2a[ar] if (mTEPES.pIndOperReserveGen[nr] == 0 or mTEPES.pIndOperReserveCon[nr] == 0 ))
|
|
434
443
|
+ sum(1 for eh in e2a[ar] if (mTEPES.pIndOperReserveGen[eh] == 0 or mTEPES.pIndOperReserveCon[eh] == 0 )) == 0):
|
|
435
444
|
return Constraint.Skip
|
|
436
|
-
|
|
437
445
|
return sum(OptModel.vReserveDown[p,sc,n,nr] for nr in n2a[ar] if mTEPES.pIndOperReserveGen[nr] == 0 and (p,nr) in mTEPES.pnr) + sum(OptModel.vESSReserveDown[p,sc,n,eh] for eh in e2a[ar] if mTEPES.pIndOperReserveCon[eh] == 0 and (p,eh) in mTEPES.peh) == mTEPES.pOperReserveDw[p,sc,n,ar]
|
|
438
446
|
|
|
439
447
|
setattr(OptModel, f'eOperReserveDw_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.ar, rule=eOperReserveDw, doc='down operating reserve [GW]'))
|
|
@@ -589,6 +597,7 @@ def GenerationOperationModelFormulationDemand(OptModel, mTEPES, pIndLogConsole,
|
|
|
589
597
|
print('Generating inertia/reserves/balance ... ', round(GeneratingTime), 's')
|
|
590
598
|
|
|
591
599
|
|
|
600
|
+
# @profile
|
|
592
601
|
def GenerationOperationModelFormulationStorage(OptModel, mTEPES, pIndLogConsole, p, sc, st):
|
|
593
602
|
print('Storage scheduling constraints ****')
|
|
594
603
|
|
|
@@ -643,14 +652,14 @@ def GenerationOperationModelFormulationStorage(OptModel, mTEPES, pIndLogConsole,
|
|
|
643
652
|
if (p,es) in mTEPES.pes and (mTEPES.pTotalMaxCharge[es] or mTEPES.pTotalEnergyInflows[es]):
|
|
644
653
|
if (p,sc,st,n) in mTEPES.s2n and mTEPES.n.ord(n) == mTEPES.pStorageTimeStep[es]:
|
|
645
654
|
if es not in mTEPES.ec:
|
|
646
|
-
return mTEPES.pIniInventory[p,sc,n,es]() + sum(mTEPES.pDuration[p,sc,n2]()*(mTEPES.pEnergyInflows[p,sc,n2,es]() - OptModel.vEnergyOutflows[p,sc,n2,es] - OptModel.vTotalOutput[p,sc,n2,es] / math.sqrt(mTEPES.pEfficiency[es]) + math.sqrt(mTEPES.pEfficiency[es]) *OptModel.vESSTotalCharge[p,sc,n2,es]) for n2 in list(mTEPES.n2)[mTEPES.n.ord(n)-mTEPES.pStorageTimeStep[es]:mTEPES.n.ord(n)]) == OptModel.vESSInventory[p,sc,n,es] + OptModel.vESSSpillage[p,sc,n,es]
|
|
655
|
+
return mTEPES.pIniInventory[p,sc,n,es]() + sum(mTEPES.pDuration[p,sc,n2]()*(mTEPES.pEnergyInflows[p,sc,n2,es]() - OptModel.vEnergyOutflows[p,sc,n2,es] - OptModel.vTotalOutput[p,sc,n2,es] / math.sqrt(mTEPES.pEfficiency[es]) + math.sqrt(mTEPES.pEfficiency[es]) * OptModel.vESSTotalCharge[p,sc,n2,es]) for n2 in list(mTEPES.n2)[mTEPES.n.ord(n)-mTEPES.pStorageTimeStep[es]:mTEPES.n.ord(n)]) == OptModel.vESSInventory[p,sc,n,es] + OptModel.vESSSpillage[p,sc,n,es]
|
|
647
656
|
else:
|
|
648
|
-
return OptModel.vIniInventory[p,sc,n,es] + sum(mTEPES.pDuration[p,sc,n2]()*(OptModel.vEnergyInflows[p,sc,n2,es] - OptModel.vEnergyOutflows[p,sc,n2,es] - OptModel.vTotalOutput[p,sc,n2,es] / math.sqrt(mTEPES.pEfficiency[es]) + math.sqrt(mTEPES.pEfficiency[es]) *OptModel.vESSTotalCharge[p,sc,n2,es]) for n2 in list(mTEPES.n2)[mTEPES.n.ord(n)-mTEPES.pStorageTimeStep[es]:mTEPES.n.ord(n)]) == OptModel.vESSInventory[p,sc,n,es] + OptModel.vESSSpillage[p,sc,n,es]
|
|
657
|
+
return OptModel.vIniInventory[p,sc,n,es] + sum(mTEPES.pDuration[p,sc,n2]()*(OptModel.vEnergyInflows[p,sc,n2,es] - OptModel.vEnergyOutflows[p,sc,n2,es] - OptModel.vTotalOutput[p,sc,n2,es] / math.sqrt(mTEPES.pEfficiency[es]) + math.sqrt(mTEPES.pEfficiency[es]) * OptModel.vESSTotalCharge[p,sc,n2,es]) for n2 in list(mTEPES.n2)[mTEPES.n.ord(n)-mTEPES.pStorageTimeStep[es]:mTEPES.n.ord(n)]) == OptModel.vESSInventory[p,sc,n,es] + OptModel.vESSSpillage[p,sc,n,es]
|
|
649
658
|
elif (p,sc,st,n) in mTEPES.s2n and mTEPES.n.ord(n) > mTEPES.pStorageTimeStep[es]:
|
|
650
659
|
if es not in mTEPES.ec:
|
|
651
|
-
return OptModel.vESSInventory[p,sc,mTEPES.n.prev(n,mTEPES.pStorageTimeStep[es]),es] + sum(mTEPES.pDuration[p,sc,n2]()*(mTEPES.pEnergyInflows[p,sc,n2,es]() - OptModel.vEnergyOutflows[p,sc,n2,es] - OptModel.vTotalOutput[p,sc,n2,es] / math.sqrt(mTEPES.pEfficiency[es]) + math.sqrt(mTEPES.pEfficiency[es]) *OptModel.vESSTotalCharge[p,sc,n2,es]) for n2 in list(mTEPES.n2)[mTEPES.n.ord(n)-mTEPES.pStorageTimeStep[es]:mTEPES.n.ord(n)]) == OptModel.vESSInventory[p,sc,n,es] + OptModel.vESSSpillage[p,sc,n,es]
|
|
660
|
+
return OptModel.vESSInventory[p,sc,mTEPES.n.prev(n,mTEPES.pStorageTimeStep[es]),es] + sum(mTEPES.pDuration[p,sc,n2]()*(mTEPES.pEnergyInflows[p,sc,n2,es]() - OptModel.vEnergyOutflows[p,sc,n2,es] - OptModel.vTotalOutput[p,sc,n2,es] / math.sqrt(mTEPES.pEfficiency[es]) + math.sqrt(mTEPES.pEfficiency[es]) * OptModel.vESSTotalCharge[p,sc,n2,es]) for n2 in list(mTEPES.n2)[mTEPES.n.ord(n)-mTEPES.pStorageTimeStep[es]:mTEPES.n.ord(n)]) == OptModel.vESSInventory[p,sc,n,es] + OptModel.vESSSpillage[p,sc,n,es]
|
|
652
661
|
else:
|
|
653
|
-
return OptModel.vESSInventory[p,sc,mTEPES.n.prev(n,mTEPES.pStorageTimeStep[es]),es] + sum(mTEPES.pDuration[p,sc,n2]()*(OptModel.vEnergyInflows[p,sc,n2,es] - OptModel.vEnergyOutflows[p,sc,n2,es] - OptModel.vTotalOutput[p,sc,n2,es] / math.sqrt(mTEPES.pEfficiency[es]) + math.sqrt(mTEPES.pEfficiency[es]) *OptModel.vESSTotalCharge[p,sc,n2,es]) for n2 in list(mTEPES.n2)[mTEPES.n.ord(n)-mTEPES.pStorageTimeStep[es]:mTEPES.n.ord(n)]) == OptModel.vESSInventory[p,sc,n,es] + OptModel.vESSSpillage[p,sc,n,es]
|
|
662
|
+
return OptModel.vESSInventory[p,sc,mTEPES.n.prev(n,mTEPES.pStorageTimeStep[es]),es] + sum(mTEPES.pDuration[p,sc,n2]()*(OptModel.vEnergyInflows[p,sc,n2,es] - OptModel.vEnergyOutflows[p,sc,n2,es] - OptModel.vTotalOutput[p,sc,n2,es] / math.sqrt(mTEPES.pEfficiency[es]) + math.sqrt(mTEPES.pEfficiency[es]) * OptModel.vESSTotalCharge[p,sc,n2,es]) for n2 in list(mTEPES.n2)[mTEPES.n.ord(n)-mTEPES.pStorageTimeStep[es]:mTEPES.n.ord(n)]) == OptModel.vESSInventory[p,sc,n,es] + OptModel.vESSSpillage[p,sc,n,es]
|
|
654
663
|
else:
|
|
655
664
|
return Constraint.Skip
|
|
656
665
|
else:
|
|
@@ -694,7 +703,6 @@ def GenerationOperationModelFormulationStorage(OptModel, mTEPES, pIndLogConsole,
|
|
|
694
703
|
# Check if generator is available in the period and has variable charging capacity
|
|
695
704
|
if (p,eh) not in mTEPES.peh or mTEPES.pMaxCharge2ndBlock[p,sc,n,eh] == 0.0:
|
|
696
705
|
return Constraint.Skip
|
|
697
|
-
|
|
698
706
|
# Hydro units have commitment while ESS units are implicitly always committed
|
|
699
707
|
if eh not in mTEPES.h:
|
|
700
708
|
# ESS units only need this constraint when they can offer operating reserves and the systems demands reserves
|
|
@@ -723,9 +731,7 @@ def GenerationOperationModelFormulationStorage(OptModel, mTEPES, pIndLogConsole,
|
|
|
723
731
|
# Skip if the ESS cannot consume at variable power
|
|
724
732
|
if not mTEPES.pMaxCharge2ndBlock[p,sc,n,eh]:
|
|
725
733
|
return Constraint.Skip
|
|
726
|
-
|
|
727
734
|
return OptModel.vCharge2ndBlock[p,sc,n,eh] - OptModel.vESSReserveUp[p,sc,n,eh] >= 0.0
|
|
728
|
-
|
|
729
735
|
setattr(OptModel, f'eMinCharge_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.eh, rule=eMinCharge, doc='min charge of an ESS [p.u.]'))
|
|
730
736
|
|
|
731
737
|
if pIndLogConsole == 1:
|
|
@@ -740,15 +746,13 @@ def GenerationOperationModelFormulationStorage(OptModel, mTEPES, pIndLogConsole,
|
|
|
740
746
|
|
|
741
747
|
# Generators with consumption capability cannot be consuming and generating simultaneously
|
|
742
748
|
def eChargeDischarge(OptModel,n,eh):
|
|
743
|
-
# Check if generator is
|
|
749
|
+
# Check if generator is available in the period
|
|
744
750
|
if (p,eh) not in mTEPES.peh:
|
|
745
751
|
return Constraint.Skip
|
|
746
752
|
# Constraint only relevant to generators which can consume and generate power
|
|
747
753
|
if mTEPES.pMaxPower2ndBlock [p,sc,n,eh] == 0.0 or mTEPES.pMaxCharge2ndBlock[p,sc,n,eh] == 0.0:
|
|
748
754
|
return Constraint.Skip
|
|
749
|
-
|
|
750
755
|
#Hydro generators can have binary commitment, energy modeled ESS do not have commitment
|
|
751
|
-
|
|
752
756
|
# ESS Generator
|
|
753
757
|
if eh not in mTEPES.h:
|
|
754
758
|
return ((OptModel.vOutput2ndBlock[p,sc,n,eh] + mTEPES.pUpReserveActivation * OptModel.vReserveUp [p,sc,n,eh]) / mTEPES.pMaxPower2ndBlock [p,sc,n,eh] +
|
|
@@ -756,22 +760,19 @@ def GenerationOperationModelFormulationStorage(OptModel, mTEPES, pIndLogConsole,
|
|
|
756
760
|
# Hydro Generator
|
|
757
761
|
else:
|
|
758
762
|
return OptModel.vCommitment[p,sc,n,eh] + OptModel.vCommitmentCons[p,sc,n,eh] <= 1.0
|
|
759
|
-
|
|
760
763
|
setattr(OptModel, f'eChargeDischarge_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.eh, rule=eChargeDischarge, doc='incompatibility between charge and discharge [p.u.]'))
|
|
761
764
|
|
|
762
765
|
if pIndLogConsole == 1:
|
|
763
766
|
print('eChargeDischarge ... ', len(getattr(OptModel, f'eChargeDischarge_{p}_{sc}_{st}')), ' rows')
|
|
764
767
|
|
|
765
768
|
def eESSTotalCharge(OptModel,n,eh):
|
|
766
|
-
# Check if generator is
|
|
769
|
+
# Check if generator is available in the period
|
|
767
770
|
if (p,eh) not in mTEPES.peh:
|
|
768
771
|
return Constraint.Skip
|
|
769
772
|
# Constraint only applies to generators with charging capabilities
|
|
770
773
|
if mTEPES.pMaxCharge2ndBlock[p,sc,n,eh] == 0.0:
|
|
771
774
|
return Constraint.Skip
|
|
772
|
-
|
|
773
775
|
# Hydro generators can have binary commitment, energy modeled ESS do not have commitment
|
|
774
|
-
|
|
775
776
|
# ESS Generator
|
|
776
777
|
if eh not in mTEPES.h:
|
|
777
778
|
# Check minimum charge to avoid dividing by 0. Dividing by MinCharge is more numerically stable
|
|
@@ -786,7 +787,6 @@ def GenerationOperationModelFormulationStorage(OptModel, mTEPES, pIndLogConsole,
|
|
|
786
787
|
return OptModel.vESSTotalCharge[p,sc,n,eh] == OptModel.vCharge2ndBlock[p,sc,n,eh] + mTEPES.pDwReserveActivation * OptModel.vESSReserveDown[p,sc,n,eh] - mTEPES.pUpReserveActivation * OptModel.vESSReserveUp[p,sc,n,eh]
|
|
787
788
|
else:
|
|
788
789
|
return OptModel.vESSTotalCharge[p,sc,n,eh] / mTEPES.pMinCharge[p,sc,n,eh] == OptModel.vCommitmentCons[p,sc,n,eh] + (OptModel.vCharge2ndBlock[p,sc,n,eh] + mTEPES.pDwReserveActivation * OptModel.vESSReserveDown[p,sc,n,eh] - mTEPES.pUpReserveActivation * OptModel.vESSReserveUp[p,sc,n,eh]) / mTEPES.pMinCharge[p,sc,n,eh]
|
|
789
|
-
|
|
790
790
|
setattr(OptModel, f'eESSTotalCharge_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.eh, rule=eESSTotalCharge, doc='total charge of an ESS unit [GW]'))
|
|
791
791
|
|
|
792
792
|
if pIndLogConsole == 1:
|
|
@@ -839,6 +839,7 @@ def GenerationOperationModelFormulationStorage(OptModel, mTEPES, pIndLogConsole,
|
|
|
839
839
|
if pIndLogConsole == 1:
|
|
840
840
|
print('Generating storage operation ... ', round(GeneratingTime), 's')
|
|
841
841
|
|
|
842
|
+
# @profile
|
|
842
843
|
def GenerationOperationModelFormulationReservoir(OptModel, mTEPES, pIndLogConsole, p, sc, st):
|
|
843
844
|
print('Reservoir scheduling constraints ****')
|
|
844
845
|
|
|
@@ -897,9 +898,9 @@ def GenerationOperationModelFormulationReservoir(OptModel, mTEPES, pIndLogConsol
|
|
|
897
898
|
|
|
898
899
|
# Avoid division by 0 if turbine has no minimum power
|
|
899
900
|
if mTEPES.pMinPowerElec[p,sc,n,h] == 0.0:
|
|
900
|
-
return OptModel.vOutput2ndBlock[p,sc,n,h] + OptModel.vReserveUp[p,sc,n,h] <= sum(OptModel.vReservoirVolume[p,sc,n,rs] - mTEPES.pMinVolume[p,sc,n,rs] for rs in mTEPES.rs if (rs,
|
|
901
|
+
return OptModel.vOutput2ndBlock[p,sc,n,h] + OptModel.vReserveUp[p,sc,n,h] <= sum(OptModel.vReservoirVolume[p,sc,n,rs] - mTEPES.pMinVolume[p,sc,n,rs] for rs in mTEPES.rs if (rs,h) in mTEPES.r2h) * mTEPES.pProductionFunctionHydro[h] / mTEPES.pDuration[p,sc,n]()
|
|
901
902
|
else:
|
|
902
|
-
return (OptModel.vOutput2ndBlock[p,sc,n,h] + OptModel.vReserveUp[p,sc,n,h]) / mTEPES.pMinPowerElec[p,sc,n,h] + OptModel.vCommitment[p,sc,n,h] <= sum(OptModel.vReservoirVolume[p,sc,n,rs] - mTEPES.pMinVolume[p,sc,n,rs] for rs in mTEPES.rs if (rs,
|
|
903
|
+
return (OptModel.vOutput2ndBlock[p,sc,n,h] + OptModel.vReserveUp[p,sc,n,h]) / mTEPES.pMinPowerElec[p,sc,n,h] + OptModel.vCommitment[p,sc,n,h] <= sum(OptModel.vReservoirVolume[p,sc,n,rs] - mTEPES.pMinVolume[p,sc,n,rs] for rs in mTEPES.rs if (rs,h) in mTEPES.r2h) * mTEPES.pProductionFunctionHydro[h] / mTEPES.pDuration[p,sc,n]() / mTEPES.pMinPowerElec[p,sc,n,h]
|
|
903
904
|
|
|
904
905
|
setattr(OptModel, f'eTrbReserveUpIfUpstream_{p}_{sc}_{st}', Constraint(mTEPES.nhc, rule=eTrbReserveUpIfUpstream, doc='up operating reserve if energy available [GW]'))
|
|
905
906
|
|
|
@@ -1049,6 +1050,7 @@ def GenerationOperationModelFormulationReservoir(OptModel, mTEPES, pIndLogConsol
|
|
|
1049
1050
|
print('Generating reservoir operation ... ', round(GeneratingTime), 's')
|
|
1050
1051
|
|
|
1051
1052
|
|
|
1053
|
+
# @profile
|
|
1052
1054
|
def GenerationOperationModelFormulationCommitment(OptModel, mTEPES, pIndLogConsole, p, sc, st):
|
|
1053
1055
|
print('Unit commitment constraints ****')
|
|
1054
1056
|
|
|
@@ -1062,10 +1064,20 @@ def GenerationOperationModelFormulationCommitment(OptModel, mTEPES, pIndLogConso
|
|
|
1062
1064
|
|
|
1063
1065
|
def eMaxOutput2ndBlock(OptModel,n,nr):
|
|
1064
1066
|
if (p,nr) in mTEPES.pnr and (nr not in mTEPES.es or (nr in mTEPES.es and (mTEPES.pTotalMaxCharge[nr] or mTEPES.pTotalEnergyInflows[nr]))) and sum(mTEPES.pOperReserveUp[p,sc,n,ar] for ar in a2n[nr]):
|
|
1065
|
-
if mTEPES.
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1067
|
+
if mTEPES.pIndRampReserves == 0 or sum(mTEPES.pRampReserveUp[p,sc,n,ar] for ar in mTEPES.ar) == 0.0:
|
|
1068
|
+
if mTEPES.pMaxPower2ndBlock[p,sc,n,nr] and mTEPES.pIndOperReserveGen[nr] != 1 and n != mTEPES.n.last():
|
|
1069
|
+
return (OptModel.vOutput2ndBlock[p,sc,n,nr] + OptModel.vReserveUp [p,sc,n,nr] ) / mTEPES.pMaxPower2ndBlock[p,sc,n,nr] <= OptModel.vCommitment[p,sc,n,nr] - OptModel.vStartUp[p,sc,n,nr] - OptModel.vShutDown[p,sc,mTEPES.n.next(n),nr]
|
|
1070
|
+
elif mTEPES.pMaxPower2ndBlock[p,sc,n,nr] and mTEPES.pIndOperReserveGen[nr] != 1 and n == mTEPES.n.last():
|
|
1071
|
+
return (OptModel.vOutput2ndBlock[p,sc,n,nr] + OptModel.vReserveUp [p,sc,n,nr] ) / mTEPES.pMaxPower2ndBlock[p,sc,n,nr] <= OptModel.vCommitment[p,sc,n,nr] - OptModel.vStartUp[p,sc,n,nr]
|
|
1072
|
+
else:
|
|
1073
|
+
return Constraint.Skip
|
|
1074
|
+
elif mTEPES.pIndRampReserves == 1 and sum(mTEPES.pRampReserveUp[p,sc,n,ar] for ar in mTEPES.ar) > 0.0:
|
|
1075
|
+
if mTEPES.pMaxPower2ndBlock[p,sc,n,nr] and mTEPES.pIndOperReserveGen[nr] != 1 and n != mTEPES.n.last():
|
|
1076
|
+
return (OptModel.vOutput2ndBlock[p,sc,n,nr] + OptModel.vReserveUp [p,sc,n,nr] + OptModel.vRampReserveUp [p,sc,n,nr]) / mTEPES.pMaxPower2ndBlock[p,sc,n,nr] <= OptModel.vCommitment[p,sc,n,nr] - OptModel.vStartUp[p,sc,n,nr] - OptModel.vShutDown[p,sc,mTEPES.n.next(n),nr]
|
|
1077
|
+
elif mTEPES.pMaxPower2ndBlock[p,sc,n,nr] and mTEPES.pIndOperReserveGen[nr] != 1 and n == mTEPES.n.last():
|
|
1078
|
+
return (OptModel.vOutput2ndBlock[p,sc,n,nr] + OptModel.vReserveUp [p,sc,n,nr] + OptModel.vRampReserveUp [p,sc,n,nr]) / mTEPES.pMaxPower2ndBlock[p,sc,n,nr] <= OptModel.vCommitment[p,sc,n,nr] - OptModel.vStartUp[p,sc,n,nr]
|
|
1079
|
+
else:
|
|
1080
|
+
return Constraint.Skip
|
|
1069
1081
|
else:
|
|
1070
1082
|
return Constraint.Skip
|
|
1071
1083
|
else:
|
|
@@ -1077,8 +1089,10 @@ def GenerationOperationModelFormulationCommitment(OptModel, mTEPES, pIndLogConso
|
|
|
1077
1089
|
|
|
1078
1090
|
def eMinOutput2ndBlock(OptModel,n,nr):
|
|
1079
1091
|
if (p,nr) in mTEPES.pnr and (nr not in mTEPES.es or (nr in mTEPES.es and (mTEPES.pTotalMaxCharge[nr] or mTEPES.pTotalEnergyInflows[nr]))) and sum(mTEPES.pOperReserveDw[p,sc,n,ar] for ar in a2n[nr]):
|
|
1080
|
-
if mTEPES.pMaxPower2ndBlock[p,sc,n,nr] and mTEPES.pIndOperReserveGen[nr] != 1:
|
|
1081
|
-
return OptModel.vOutput2ndBlock[p,sc,n,nr] - OptModel.vReserveDown[p,sc,n,nr]
|
|
1092
|
+
if mTEPES.pMaxPower2ndBlock[p,sc,n,nr] and mTEPES.pIndOperReserveGen[nr] != 1 and (mTEPES.pIndRampReserves == 0 or sum(mTEPES.pRampReserveDw[p,sc,n,ar] for ar in mTEPES.ar) == 0.0):
|
|
1093
|
+
return OptModel.vOutput2ndBlock[p,sc,n,nr] - OptModel.vReserveDown[p,sc,n,nr] >= 0.0
|
|
1094
|
+
if mTEPES.pMaxPower2ndBlock[p,sc,n,nr] and mTEPES.pIndOperReserveGen[nr] != 1 and mTEPES.pIndRampReserves == 1 and sum(mTEPES.pRampReserveDw[p,sc,n,ar] for ar in mTEPES.ar) > 0.0:
|
|
1095
|
+
return OptModel.vOutput2ndBlock[p,sc,n,nr] - OptModel.vReserveDown[p,sc,n,nr] - OptModel.vRampReserveDw[p,sc,n,nr] >= 0.0
|
|
1082
1096
|
else:
|
|
1083
1097
|
return Constraint.Skip
|
|
1084
1098
|
else:
|
|
@@ -1140,10 +1154,9 @@ def GenerationOperationModelFormulationCommitment(OptModel, mTEPES, pIndLogConso
|
|
|
1140
1154
|
# Skip if the generator is not part of the exclusive group
|
|
1141
1155
|
if nr not in mTEPES.GeneratorsInYearlyGroup[group]:
|
|
1142
1156
|
return Constraint.Skip
|
|
1143
|
-
# Skip if there are one or
|
|
1157
|
+
# Skip if there are one or fewer generators in the group
|
|
1144
1158
|
if len(mTEPES.GeneratorsInYearlyGroup[group] & {nr for (p,nr) in mTEPES.pnr}) <= 1:
|
|
1145
1159
|
return Constraint.Skip
|
|
1146
|
-
|
|
1147
1160
|
return OptModel.vCommitment[p,sc,n,nr] <= OptModel.vMaxCommitmentYearly[p,sc,nr,group]
|
|
1148
1161
|
|
|
1149
1162
|
setattr(OptModel, f'eMaxCommitmentYearly_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.ExclusiveGroupsYearly, mTEPES.nr, rule=eMaxCommitmentYearly, doc='maximum of all the commitments [p.u.]'))
|
|
@@ -1164,7 +1177,6 @@ def GenerationOperationModelFormulationCommitment(OptModel, mTEPES, pIndLogConso
|
|
|
1164
1177
|
# Skip if there are one or less generators in the group
|
|
1165
1178
|
if len(mTEPES.GeneratorsInYearlyGroup[group] & {nr for (p, nr) in mTEPES.pnr}) <= 1:
|
|
1166
1179
|
return Constraint.Skip
|
|
1167
|
-
|
|
1168
1180
|
return OptModel.vTotalOutput[p,sc,n,nr]/mTEPES.pMaxPowerElec[p,sc,n,nr] <= OptModel.vMaxCommitmentYearly[p,sc,nr,group]
|
|
1169
1181
|
|
|
1170
1182
|
setattr(OptModel, f'eMaxCommitGenYearly_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.ExclusiveGroupsYearly,mTEPES.nr, rule=eMaxCommitGenYearly, doc='maximum of all the capacity factors'))
|
|
@@ -1193,10 +1205,9 @@ def GenerationOperationModelFormulationCommitment(OptModel, mTEPES, pIndLogConso
|
|
|
1193
1205
|
# Skip if the generator is not part of the exclusive group
|
|
1194
1206
|
if nr not in mTEPES.GeneratorsInHourlyGroup[group]:
|
|
1195
1207
|
return Constraint.Skip
|
|
1196
|
-
# Skip if there are one or
|
|
1208
|
+
# Skip if there are one or fewer generators in the group
|
|
1197
1209
|
if len(mTEPES.GeneratorsInHourlyGroup[group] & {nr for (p,nr) in mTEPES.pnr}) <= 1:
|
|
1198
1210
|
return Constraint.Skip
|
|
1199
|
-
|
|
1200
1211
|
return OptModel.vCommitment[p,sc,n,nr] <= OptModel.vMaxCommitmentHourly[p,sc,n,nr,group]
|
|
1201
1212
|
|
|
1202
1213
|
setattr(OptModel, f'eMaxCommitmentHourly_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.ExclusiveGroupsHourly, mTEPES.nr, rule=eMaxCommitmentHourly, doc='maximum of all the commitments [p.u.]'))
|
|
@@ -1217,7 +1228,6 @@ def GenerationOperationModelFormulationCommitment(OptModel, mTEPES, pIndLogConso
|
|
|
1217
1228
|
# Skip if there are one or less generators in the group
|
|
1218
1229
|
if len(mTEPES.GeneratorsInHourlyGroup[group] & {nr for (p,nr) in mTEPES.pnr}) <= 1:
|
|
1219
1230
|
return Constraint.Skip
|
|
1220
|
-
|
|
1221
1231
|
return OptModel.vTotalOutput[p,sc,n,nr]/mTEPES.pMaxPowerElec[p,sc,n,nr] <= OptModel.vMaxCommitmentHourly[p,sc,n,nr,group]
|
|
1222
1232
|
|
|
1223
1233
|
setattr(OptModel, f'eMaxCommitGenHourly_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.ExclusiveGroupsHourly,mTEPES.nr, rule=eMaxCommitGenHourly, doc='maximum of all the capacity factors'))
|
|
@@ -1226,14 +1236,13 @@ def GenerationOperationModelFormulationCommitment(OptModel, mTEPES, pIndLogConso
|
|
|
1226
1236
|
print('eMaxCommitGenHourly ... ', len(getattr(OptModel, f'eMaxCommitGenHourly_{p}_{sc}_{st}')), ' rows')
|
|
1227
1237
|
|
|
1228
1238
|
def eExclusiveGensHourly(OptModel,n,group):
|
|
1229
|
-
# Skip if there are one or
|
|
1239
|
+
# Skip if there are one or fewer generators in the group
|
|
1230
1240
|
# This is written in a different way to the rest of the code to avoid variable shadowing due to comprehension
|
|
1231
1241
|
if len(mTEPES.GeneratorsInHourlyGroup[group] & {gen for (period, gen) in mTEPES.pnr if period == p}) <= 1:
|
|
1232
1242
|
return Constraint.Skip
|
|
1233
|
-
|
|
1234
1243
|
return sum(OptModel.vMaxCommitmentHourly[p,sc,n,nr,group] + (OptModel.vCommitmentCons[p,sc,n,nr] if nr in mTEPES.h else 0) for nr in mTEPES.GeneratorsInHourlyGroup[group] if (p, nr) in mTEPES.pnr ) <= 1
|
|
1235
1244
|
|
|
1236
|
-
setattr(OptModel, f'eExclusiveGensHourly_{p}_{sc}_{st}', Constraint(mTEPES.n,mTEPES.ExclusiveGroupsHourly, rule=eExclusiveGensHourly, doc='mutually exclusive generators'))
|
|
1245
|
+
setattr(OptModel, f'eExclusiveGensHourly_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.ExclusiveGroupsHourly, rule=eExclusiveGensHourly, doc='mutually exclusive generators'))
|
|
1237
1246
|
|
|
1238
1247
|
if pIndLogConsole == 1:
|
|
1239
1248
|
print('eExclusiveGensHourly ... ', len(getattr(OptModel, f'eExclusiveGensHourly_{p}_{sc}_{st}')), ' rows')
|
|
@@ -1241,11 +1250,34 @@ def GenerationOperationModelFormulationCommitment(OptModel, mTEPES, pIndLogConso
|
|
|
1241
1250
|
GeneratingTime = time.time() - StartTime
|
|
1242
1251
|
if pIndLogConsole == 1:
|
|
1243
1252
|
print('Generating generation commitment ... ', round(GeneratingTime), 's')
|
|
1253
|
+
|
|
1254
|
+
|
|
1255
|
+
# @profile
|
|
1244
1256
|
def GenerationOperationModelFormulationRampMinTime(OptModel, mTEPES, pIndLogConsole, p, sc, st):
|
|
1245
1257
|
print('Ramp and min up/down time constraints ****')
|
|
1246
1258
|
|
|
1247
1259
|
StartTime = time.time()
|
|
1248
1260
|
|
|
1261
|
+
def eSystemRampUp(OptModel,n):
|
|
1262
|
+
if mTEPES.pIndRampReserves == 1 and sum(mTEPES.pRampReserveUp[p,sc,n,ar] for ar in mTEPES.ar):
|
|
1263
|
+
return sum(OptModel.vRampReserveUp[p,sc,n,nr] for nr in mTEPES.nr if (p,nr) in mTEPES.pnr and (nr not in mTEPES.es or (nr in mTEPES.es and (mTEPES.pTotalMaxCharge[nr] or mTEPES.pTotalEnergyInflows[nr])))) / mTEPES.pDuration[p,sc,n]() >= sum(mTEPES.pRampReserveUp[p,sc,n,ar] for ar in mTEPES.ar)
|
|
1264
|
+
else:
|
|
1265
|
+
return Constraint.Skip
|
|
1266
|
+
setattr(OptModel, f'eSystemRampUp_{p}_{sc}_{st}', Constraint(mTEPES.n, rule=eSystemRampUp, doc='minimum system ramp up [p.u.]'))
|
|
1267
|
+
|
|
1268
|
+
if pIndLogConsole == 1:
|
|
1269
|
+
print('eSystemRampUp ... ', len(getattr(OptModel, f'eSystemRampUp_{p}_{sc}_{st}')), ' rows')
|
|
1270
|
+
|
|
1271
|
+
def eSystemRampDw(OptModel,n):
|
|
1272
|
+
if mTEPES.pIndRampReserves == 1 and sum(mTEPES.pRampReserveDw[p,sc,n,ar] for ar in mTEPES.ar):
|
|
1273
|
+
return sum(OptModel.vRampReserveDw[p,sc,n,nr] for nr in mTEPES.nr if (p,nr) in mTEPES.pnr and (nr not in mTEPES.es or (nr in mTEPES.es and (mTEPES.pTotalMaxCharge[nr] or mTEPES.pTotalEnergyInflows[nr])))) / mTEPES.pDuration[p,sc,n]() >= sum(mTEPES.pRampReserveDw[p,sc,n,ar] for ar in mTEPES.ar)
|
|
1274
|
+
else:
|
|
1275
|
+
return Constraint.Skip
|
|
1276
|
+
setattr(OptModel, f'eSystemRampDw_{p}_{sc}_{st}', Constraint(mTEPES.n, rule=eSystemRampDw, doc='minimum system ramp down [p.u.]'))
|
|
1277
|
+
|
|
1278
|
+
if pIndLogConsole == 1:
|
|
1279
|
+
print('eSystemRampDw ... ', len(getattr(OptModel, f'eSystemRampDw_{p}_{sc}_{st}')), ' rows')
|
|
1280
|
+
|
|
1249
1281
|
def eRampUp(OptModel,n,nr):
|
|
1250
1282
|
if (p,nr) in mTEPES.pnr and (nr not in mTEPES.es or (nr in mTEPES.es and (mTEPES.pTotalMaxCharge[nr] or mTEPES.pTotalEnergyInflows[nr]))):
|
|
1251
1283
|
if mTEPES.pRampUp[nr] and mTEPES.pIndBinGenRamps() == 1 and mTEPES.pRampUp[nr]*mTEPES.pDuration[p,sc,n]() < mTEPES.pMaxPower2ndBlock[p,sc,n,nr] and mTEPES.pDuration[p,sc,n]():
|
|
@@ -1434,6 +1466,7 @@ def GenerationOperationModelFormulationRampMinTime(OptModel, mTEPES, pIndLogCons
|
|
|
1434
1466
|
print('Generating ramps & minimum time ... ', round(GeneratingTime), 's')
|
|
1435
1467
|
|
|
1436
1468
|
|
|
1469
|
+
# @profile
|
|
1437
1470
|
def NetworkSwitchingModelFormulation(OptModel, mTEPES, pIndLogConsole, p, sc, st):
|
|
1438
1471
|
print('Network switching model constraints ****')
|
|
1439
1472
|
|
|
@@ -1499,6 +1532,7 @@ def NetworkSwitchingModelFormulation(OptModel, mTEPES, pIndLogConsole, p, sc, st
|
|
|
1499
1532
|
print('Switching minimum on/off state ... ', round(GeneratingTime), 's')
|
|
1500
1533
|
|
|
1501
1534
|
|
|
1535
|
+
# @profile
|
|
1502
1536
|
def NetworkOperationModelFormulation(OptModel, mTEPES, pIndLogConsole, p, sc, st):
|
|
1503
1537
|
print('Network operation model constraints ****')
|
|
1504
1538
|
|
|
@@ -1631,6 +1665,8 @@ def NetworkOperationModelFormulation(OptModel, mTEPES, pIndLogConsole, p, sc, st
|
|
|
1631
1665
|
if pIndLogConsole == 1:
|
|
1632
1666
|
print('Generating network constraints ... ', round(GeneratingTime), 's')
|
|
1633
1667
|
|
|
1668
|
+
|
|
1669
|
+
# @profile
|
|
1634
1670
|
def NetworkCycles(mTEPES, pIndLogConsole):
|
|
1635
1671
|
print('Network Cycles Detection ****')
|
|
1636
1672
|
|
|
@@ -1702,6 +1738,8 @@ def NetworkCycles(mTEPES, pIndLogConsole):
|
|
|
1702
1738
|
if pIndLogConsole == 1:
|
|
1703
1739
|
print('Cycles detection ... ', round(CyclesDetectionTime), 's')
|
|
1704
1740
|
|
|
1741
|
+
|
|
1742
|
+
# @profile
|
|
1705
1743
|
def CycleConstraints(OptModel, mTEPES, pIndLogConsole, p, sc, st):
|
|
1706
1744
|
print('Network cycle constraints ****')
|
|
1707
1745
|
|
|
@@ -1759,6 +1797,8 @@ def CycleConstraints(OptModel, mTEPES, pIndLogConsole, p, sc, st):
|
|
|
1759
1797
|
if pIndLogConsole == 1:
|
|
1760
1798
|
print('Generating cycle flow constraints ... ', round(CycleFlowTime), 's')
|
|
1761
1799
|
|
|
1800
|
+
|
|
1801
|
+
# @profile
|
|
1762
1802
|
def NetworkH2OperationModelFormulation(OptModel, mTEPES, pIndLogConsole, p, sc, st):
|
|
1763
1803
|
print('Hydrogen scheduling constraints ****')
|
|
1764
1804
|
|
|
@@ -1785,7 +1825,7 @@ def NetworkH2OperationModelFormulation(OptModel, mTEPES, pIndLogConsole, p, sc,
|
|
|
1785
1825
|
|
|
1786
1826
|
def eBalanceH2(OptModel,n,nd):
|
|
1787
1827
|
if sum(1 for el in e2n[nd]) + sum(1 for nf,cc in lout[nd]) + sum(1 for ni,cc in lin[nd]):
|
|
1788
|
-
return (
|
|
1828
|
+
return (mTEPES.pDuration[p,sc,n]()*sum(OptModel.vESSTotalCharge[p,sc,n,el]/mTEPES.pProductionFunctionH2[el] for el in e2n[nd]) - mTEPES.pDuration[p,sc,n]()*sum(OptModel.vTotalOutputHeat[p,sc,n,hh]*mTEPES.pProductionFunctionH2ToHeat[hh] for hh in b2n[nd] if (p,hh) in mTEPES.phh and hh in mTEPES.hh) + OptModel.vH2NS[p,sc,n,nd] -
|
|
1789
1829
|
sum(OptModel.vFlowH2[p,sc,n,nd,nf,cc] for nf,cc in lout[nd] if (p,nd,nf,cc) in mTEPES.ppa) + sum(OptModel.vFlowH2[p,sc,n,ni,nd,cc] for ni,cc in lin[nd] if (p,ni,nd,cc) in mTEPES.ppa)) == mTEPES.pDemandH2[p,sc,n,nd]*mTEPES.pDuration[p,sc,n]()
|
|
1790
1830
|
else:
|
|
1791
1831
|
return Constraint.Skip
|
|
@@ -1799,6 +1839,7 @@ def NetworkH2OperationModelFormulation(OptModel, mTEPES, pIndLogConsole, p, sc,
|
|
|
1799
1839
|
print('Generating hydrogen operation ... ', round(GeneratingTime), 's')
|
|
1800
1840
|
|
|
1801
1841
|
|
|
1842
|
+
# @profile
|
|
1802
1843
|
def NetworkHeatOperationModelFormulation(OptModel, mTEPES, pIndLogConsole, p, sc, st):
|
|
1803
1844
|
print('Heat scheduling constraints ****')
|
|
1804
1845
|
|