openTEPES 4.18.0__py3-none-any.whl → 4.18.1__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 -3
- openTEPES/openTEPES_InputData.py +59 -43
- openTEPES/openTEPES_Main.py +2 -2
- openTEPES/openTEPES_ModelFormulation.py +35 -20
- openTEPES/openTEPES_OutputResults.py +208 -199
- {opentepes-4.18.0.dist-info → opentepes-4.18.1.dist-info}/METADATA +1 -1
- {opentepes-4.18.0.dist-info → opentepes-4.18.1.dist-info}/RECORD +11 -11
- {opentepes-4.18.0.dist-info → opentepes-4.18.1.dist-info}/LICENSE +0 -0
- {opentepes-4.18.0.dist-info → opentepes-4.18.1.dist-info}/WHEEL +0 -0
- {opentepes-4.18.0.dist-info → opentepes-4.18.1.dist-info}/entry_points.txt +0 -0
openTEPES/__init__.py
CHANGED
|
@@ -14,7 +14,7 @@ Open Generation, Storage, and Transmission Operation and Expansion Planning Mode
|
|
|
14
14
|
>>> import openTEPES as oT
|
|
15
15
|
>>> oT.routine("9n", "C:\\Users\\UserName\\Documents\\GitHub\\openTEPES", "glpk")
|
|
16
16
|
"""
|
|
17
|
-
__version__ = "4.18.
|
|
17
|
+
__version__ = "4.18.1"
|
|
18
18
|
|
|
19
19
|
from .openTEPES_Main import main
|
|
20
20
|
from .openTEPES import *
|
openTEPES/openTEPES.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Open Generation, Storage, and Transmission Operation and Expansion Planning Model with RES and ESS (openTEPES) - December
|
|
2
|
+
Open Generation, Storage, and Transmission Operation and Expansion Planning Model with RES and ESS (openTEPES) - December 19, 2024
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
# import dill as pickle
|
|
@@ -39,8 +39,8 @@ def openTEPES_run(DirName, CaseName, SolverName, pIndOutputResults, pIndLogConso
|
|
|
39
39
|
idxDict['y' ] = 1
|
|
40
40
|
|
|
41
41
|
#%% model declaration
|
|
42
|
-
mTEPES = ConcreteModel('Open Generation, Storage, and Transmission Operation and Expansion Planning Model with RES and ESS (openTEPES) - Version 4.18.
|
|
43
|
-
print( 'Open Generation, Storage, and Transmission Operation and Expansion Planning Model with RES and ESS (openTEPES) - Version 4.18.
|
|
42
|
+
mTEPES = ConcreteModel('Open Generation, Storage, and Transmission Operation and Expansion Planning Model with RES and ESS (openTEPES) - Version 4.18.1 - December 19, 2024')
|
|
43
|
+
print( 'Open Generation, Storage, and Transmission Operation and Expansion Planning Model with RES and ESS (openTEPES) - Version 4.18.1 - December 19, 2024', file=open(f'{_path}/openTEPES_version_{CaseName}.log','w'))
|
|
44
44
|
|
|
45
45
|
pIndOutputResults = [j for i,j in idxDict.items() if i == pIndOutputResults][0]
|
|
46
46
|
pIndLogConsole = [j for i,j in idxDict.items() if i == pIndLogConsole ][0]
|
openTEPES/openTEPES_InputData.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Open Generation, Storage, and Transmission Operation and Expansion Planning Model with RES and ESS (openTEPES) - December
|
|
2
|
+
Open Generation, Storage, and Transmission Operation and Expansion Planning Model with RES and ESS (openTEPES) - December 10, 2024
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import datetime
|
|
@@ -68,6 +68,7 @@ def InputData(DirName, CaseName, mTEPES, pIndLogConsole):
|
|
|
68
68
|
|
|
69
69
|
try:
|
|
70
70
|
dfDemandHeat = pd.read_csv(f'{_path}/oT_Data_DemandHeat_' f'{CaseName}.csv', header=0, index_col=[0,1,2])
|
|
71
|
+
dfReserveMarginHeat = pd.read_csv(f'{_path}/oT_Data_ReserveMarginHeat_' f'{CaseName}.csv', header=0, index_col=[0,1 ])
|
|
71
72
|
dfNetworkHeat = pd.read_csv(f'{_path}/oT_Data_NetworkHeat_' f'{CaseName}.csv', header=0, index_col=[0,1,2])
|
|
72
73
|
pIndHeat = 1
|
|
73
74
|
except:
|
|
@@ -117,6 +118,7 @@ def InputData(DirName, CaseName, mTEPES, pIndLogConsole):
|
|
|
117
118
|
|
|
118
119
|
if pIndHeat == 1:
|
|
119
120
|
dfDemandHeat.fillna (0.0, inplace=True)
|
|
121
|
+
dfReserveMarginHeat.fillna(0.0, inplace=True)
|
|
120
122
|
dfNetworkHeat.fillna (0.0, inplace=True)
|
|
121
123
|
|
|
122
124
|
dfReserveMargin = dfReserveMargin.where (dfReserveMargin > 0.0, 0.0)
|
|
@@ -146,7 +148,7 @@ def InputData(DirName, CaseName, mTEPES, pIndLogConsole):
|
|
|
146
148
|
|
|
147
149
|
# show some statistics of the data
|
|
148
150
|
if pIndLogConsole == 1:
|
|
149
|
-
print('Reserve margin
|
|
151
|
+
print('Reserve margin electricity \n', dfReserveMargin.describe (), '\n')
|
|
150
152
|
print('Maximum CO2 emission \n', dfEmission.describe (), '\n')
|
|
151
153
|
print('Minimum RES energy \n', dfRESEnergy.describe (), '\n')
|
|
152
154
|
print('Electricity demand \n', dfDemand.describe (), '\n')
|
|
@@ -181,6 +183,7 @@ def InputData(DirName, CaseName, mTEPES, pIndLogConsole):
|
|
|
181
183
|
|
|
182
184
|
if pIndHeat == 1:
|
|
183
185
|
print('Heat demand \n', dfDemandHeat.describe (), '\n')
|
|
186
|
+
print('Reserve margin heat \n', dfReserveMarginHeat.describe (), '\n')
|
|
184
187
|
print('Heat pipe network \n', dfNetworkHeat.describe (), '\n')
|
|
185
188
|
|
|
186
189
|
#%% reading the sets
|
|
@@ -329,6 +332,7 @@ def InputData(DirName, CaseName, mTEPES, pIndLogConsole):
|
|
|
329
332
|
pDemandH2 = dfDemandHydrogen [mTEPES.nd] # hydrogen demand [tH2/h]
|
|
330
333
|
|
|
331
334
|
if pIndHeat == 1:
|
|
335
|
+
pReserveMarginHeat = dfReserveMarginHeat ['ReserveMargin'] # minimum adequacy reserve margin [p.u.]
|
|
332
336
|
pDemandHeat = dfDemandHeat [mTEPES.nd] * 1e-3 # heat demand [GW]
|
|
333
337
|
|
|
334
338
|
if pTimeStep > 1:
|
|
@@ -563,8 +567,8 @@ def InputData(DirName, CaseName, mTEPES, pIndLogConsole):
|
|
|
563
567
|
mTEPES.la = Set(doc='all real electric lines' , initialize=[ln for ln in mTEPES.ln if pLineX [ln] != 0.0 and pLineNTCFrw[ln] > 0.0 and pLineNTCBck[ln] > 0.0 and pElecNetPeriodIni[ln] <= mTEPES.p.last() and pElecNetPeriodFin[ln] >= mTEPES.p.first()])
|
|
564
568
|
mTEPES.ls = Set(doc='all real switch electric lines' , initialize=[la for la in mTEPES.la if pIndBinLineSwitch [la] ])
|
|
565
569
|
mTEPES.lc = Set(doc='candidate electric lines' , initialize=[la for la in mTEPES.la if pNetFixedCost [la] > 0.0])
|
|
566
|
-
mTEPES.cd = Set(doc='
|
|
567
|
-
mTEPES.ed = Set(doc='
|
|
570
|
+
mTEPES.cd = Set(doc='candidate DC electric lines' , initialize=[la for la in mTEPES.la if pNetFixedCost [la] > 0.0 and pLineType[la] == 'DC'])
|
|
571
|
+
mTEPES.ed = Set(doc='existing DC electric lines' , initialize=[la for la in mTEPES.la if pNetFixedCost [la] == 0.0 and pLineType[la] == 'DC'])
|
|
568
572
|
mTEPES.ll = Set(doc='loss electric lines' , initialize=[la for la in mTEPES.la if pLineLossFactor [la] > 0.0 and pIndBinNetLosses > 0 ])
|
|
569
573
|
mTEPES.rf = Set(doc='reference node' , initialize=[pReferenceNode])
|
|
570
574
|
mTEPES.gq = Set(doc='gen reactive units' , initialize=[gg for gg in mTEPES.gg if pRMaxReactivePower [gg] > 0.0 and pElecGenPeriodIni[gg] <= mTEPES.p.last() and pElecGenPeriodFin[gg] >= mTEPES.p.first()])
|
|
@@ -1048,30 +1052,32 @@ def InputData(DirName, CaseName, mTEPES, pIndLogConsole):
|
|
|
1048
1052
|
|
|
1049
1053
|
# small values are converted to 0
|
|
1050
1054
|
pDemandElecPeak = pd.Series([0.0 for p,ar in mTEPES.par], index=mTEPES.par)
|
|
1055
|
+
pDemandHeatPeak = pd.Series([0.0 for p,ar in mTEPES.par], index=mTEPES.par)
|
|
1051
1056
|
for p,ar in mTEPES.par:
|
|
1052
1057
|
# values < 1e-5 times the maximum demand for each area (an area is related to operating reserves procurement, i.e., country) are converted to 0
|
|
1053
1058
|
pDemandElecPeak[p,ar] = pDemandElec.loc[p,:,:][[nd for nd in d2a[ar]]].sum(axis=1).max()
|
|
1054
|
-
|
|
1059
|
+
pEpsilonElec = pDemandElecPeak[p,ar]*1e-5
|
|
1055
1060
|
|
|
1056
1061
|
# these parameters are in GW
|
|
1057
|
-
pDemandElecPos [pDemandElecPos [[nd for nd in d2a[ar]]] <
|
|
1058
|
-
pDemandElecNeg [pDemandElecNeg [[nd for nd in d2a[ar]]] > -
|
|
1059
|
-
pSystemInertia [pSystemInertia [[ ar ]] <
|
|
1060
|
-
pOperReserveUp [pOperReserveUp [[ ar ]] <
|
|
1061
|
-
pOperReserveDw [pOperReserveDw [[ ar ]] <
|
|
1062
|
+
pDemandElecPos [pDemandElecPos [[nd for nd in d2a[ar]]] < pEpsilonElec] = 0.0
|
|
1063
|
+
pDemandElecNeg [pDemandElecNeg [[nd for nd in d2a[ar]]] > -pEpsilonElec] = 0.0
|
|
1064
|
+
pSystemInertia [pSystemInertia [[ ar ]] < pEpsilonElec] = 0.0
|
|
1065
|
+
pOperReserveUp [pOperReserveUp [[ ar ]] < pEpsilonElec] = 0.0
|
|
1066
|
+
pOperReserveDw [pOperReserveDw [[ ar ]] < pEpsilonElec] = 0.0
|
|
1067
|
+
|
|
1062
1068
|
if len(g2a[ar]):
|
|
1063
|
-
pMinPowerElec [pMinPowerElec [[g for g in g2a[ar]]] <
|
|
1064
|
-
pMaxPowerElec [pMaxPowerElec [[g for g in g2a[ar]]] <
|
|
1065
|
-
pMinCharge [pMinCharge [[es for es in e2a[ar]]] <
|
|
1066
|
-
pMaxCharge [pMaxCharge [[eh for eh in g2a[ar]]] <
|
|
1067
|
-
pEnergyInflows [pEnergyInflows [[es for es in e2a[ar]]] <
|
|
1068
|
-
pEnergyOutflows[pEnergyOutflows[[es for es in e2a[ar]]] <
|
|
1069
|
+
pMinPowerElec [pMinPowerElec [[g for g in g2a[ar]]] < pEpsilonElec] = 0.0
|
|
1070
|
+
pMaxPowerElec [pMaxPowerElec [[g for g in g2a[ar]]] < pEpsilonElec] = 0.0
|
|
1071
|
+
pMinCharge [pMinCharge [[es for es in e2a[ar]]] < pEpsilonElec] = 0.0
|
|
1072
|
+
pMaxCharge [pMaxCharge [[eh for eh in g2a[ar]]] < pEpsilonElec] = 0.0
|
|
1073
|
+
pEnergyInflows [pEnergyInflows [[es for es in e2a[ar]]] < pEpsilonElec] = 0.0
|
|
1074
|
+
pEnergyOutflows[pEnergyOutflows[[es for es in e2a[ar]]] < pEpsilonElec] = 0.0
|
|
1069
1075
|
# these parameters are in GWh
|
|
1070
|
-
pMinStorage [pMinStorage [[es for es in e2a[ar]]] <
|
|
1071
|
-
pMaxStorage [pMaxStorage [[es for es in e2a[ar]]] <
|
|
1072
|
-
pIniInventory [pIniInventory [[es for es in e2a[ar]]] <
|
|
1076
|
+
pMinStorage [pMinStorage [[es for es in e2a[ar]]] < pEpsilonElec] = 0.0
|
|
1077
|
+
pMaxStorage [pMaxStorage [[es for es in e2a[ar]]] < pEpsilonElec] = 0.0
|
|
1078
|
+
pIniInventory [pIniInventory [[es for es in e2a[ar]]] < pEpsilonElec] = 0.0
|
|
1073
1079
|
|
|
1074
|
-
# pInitialInventory.update(pd.Series([0.0 for es in e2a[ar] if pInitialInventory[es] <
|
|
1080
|
+
# pInitialInventory.update(pd.Series([0.0 for es in e2a[ar] if pInitialInventory[es] < pEpsilonElec], index=[es for es in e2a[ar] if pInitialInventory[es] < pEpsilonElec], dtype='float64'))
|
|
1075
1081
|
|
|
1076
1082
|
# merging positive and negative values of the demand
|
|
1077
1083
|
pDemandElec = pDemandElecPos.where(pDemandElecNeg >= 0.0, pDemandElecNeg)
|
|
@@ -1091,22 +1097,24 @@ def InputData(DirName, CaseName, mTEPES, pIndLogConsole):
|
|
|
1091
1097
|
pMaxCapacity = pMaxPowerElec.where(pMaxPowerElec > pMaxCharge, pMaxCharge)
|
|
1092
1098
|
|
|
1093
1099
|
if len(g2a[ar]):
|
|
1094
|
-
pMaxPower2ndBlock [pMaxPower2ndBlock [[g for g in g2a[ar]]] <
|
|
1095
|
-
pMaxCharge2ndBlock[pMaxCharge2ndBlock[[g for g in g2a[ar]]] <
|
|
1100
|
+
pMaxPower2ndBlock [pMaxPower2ndBlock [[g for g in g2a[ar]]] < pEpsilonElec] = 0.0
|
|
1101
|
+
pMaxCharge2ndBlock[pMaxCharge2ndBlock[[g for g in g2a[ar]]] < pEpsilonElec] = 0.0
|
|
1096
1102
|
|
|
1097
|
-
pLineNTCFrw.update(pd.Series([0.0 for ni,nf,cc in mTEPES.la if pLineNTCFrw[ni,nf,cc] <
|
|
1098
|
-
pLineNTCBck.update(pd.Series([0.0 for ni,nf,cc in mTEPES.la if pLineNTCBck[ni,nf,cc] <
|
|
1103
|
+
pLineNTCFrw.update(pd.Series([0.0 for ni,nf,cc in mTEPES.la if pLineNTCFrw[ni,nf,cc] < pEpsilonElec], index=[(ni,nf,cc) for ni,nf,cc in mTEPES.la if pLineNTCFrw[ni,nf,cc] < pEpsilonElec], dtype='float64'))
|
|
1104
|
+
pLineNTCBck.update(pd.Series([0.0 for ni,nf,cc in mTEPES.la if pLineNTCBck[ni,nf,cc] < pEpsilonElec], index=[(ni,nf,cc) for ni,nf,cc in mTEPES.la if pLineNTCBck[ni,nf,cc] < pEpsilonElec], dtype='float64'))
|
|
1099
1105
|
pLineNTCMax = pLineNTCFrw.where(pLineNTCFrw > pLineNTCBck, pLineNTCBck)
|
|
1100
1106
|
|
|
1101
1107
|
if pIndHydrogen == 1:
|
|
1102
|
-
pDemandH2[pDemandH2[[nd for nd in d2a[ar]]] <
|
|
1103
|
-
pH2PipeNTCFrw.update(pd.Series([0.0 for ni,nf,cc in mTEPES.pa if pH2PipeNTCFrw[ni,nf,cc] <
|
|
1104
|
-
pH2PipeNTCBck.update(pd.Series([0.0 for ni,nf,cc in mTEPES.pa if pH2PipeNTCBck[ni,nf,cc] <
|
|
1108
|
+
pDemandH2[pDemandH2[[nd for nd in d2a[ar]]] < pEpsilonElec] = 0.0
|
|
1109
|
+
pH2PipeNTCFrw.update(pd.Series([0.0 for ni,nf,cc in mTEPES.pa if pH2PipeNTCFrw[ni,nf,cc] < pEpsilonElec], index=[(ni,nf,cc) for ni,nf,cc in mTEPES.pa if pH2PipeNTCFrw[ni,nf,cc] < pEpsilonElec], dtype='float64'))
|
|
1110
|
+
pH2PipeNTCBck.update(pd.Series([0.0 for ni,nf,cc in mTEPES.pa if pH2PipeNTCBck[ni,nf,cc] < pEpsilonElec], index=[(ni,nf,cc) for ni,nf,cc in mTEPES.pa if pH2PipeNTCBck[ni,nf,cc] < pEpsilonElec], dtype='float64'))
|
|
1105
1111
|
|
|
1106
1112
|
if pIndHeat == 1:
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1113
|
+
pDemandHeatPeak[p,ar] = pDemandHeat.loc[p,:,:][[nd for nd in d2a[ar]]].sum(axis=1).max()
|
|
1114
|
+
pEpsilonHeat = pDemandHeatPeak[p,ar]*1e-5
|
|
1115
|
+
pDemandHeat [pDemandHeat [[nd for nd in d2a[ar]]] < pEpsilonHeat] = 0.0
|
|
1116
|
+
pHeatPipeNTCFrw.update(pd.Series([0.0 for ni,nf,cc in mTEPES.ha if pHeatPipeNTCFrw[ni,nf,cc] < pEpsilonHeat], index=[(ni,nf,cc) for ni,nf,cc in mTEPES.ha if pHeatPipeNTCFrw[ni,nf,cc] < pEpsilonHeat], dtype='float64'))
|
|
1117
|
+
pHeatPipeNTCBck.update(pd.Series([0.0 for ni,nf,cc in mTEPES.ha if pHeatPipeNTCBck[ni,nf,cc] < pEpsilonHeat], index=[(ni,nf,cc) for ni,nf,cc in mTEPES.ha if pHeatPipeNTCBck[ni,nf,cc] < pEpsilonHeat], dtype='float64'))
|
|
1110
1118
|
|
|
1111
1119
|
# drop generators not g or es or eh or ch
|
|
1112
1120
|
pMinPowerElec = pMinPowerElec.loc [:,mTEPES.g ]
|
|
@@ -1135,17 +1143,18 @@ def InputData(DirName, CaseName, mTEPES, pIndLogConsole):
|
|
|
1135
1143
|
# computation of the power to heat ratio of the CHP units
|
|
1136
1144
|
# heat ratio of boiler units is fixed to 1.0
|
|
1137
1145
|
pPower2HeatRatio = pd.Series([1.0 if ch in mTEPES.bo else (pRatedMaxPowerElec[ch]-pRatedMinPowerElec[ch])/(pRatedMaxPowerHeat[ch]-pRatedMinPowerHeat[ch]) for ch in mTEPES.ch], index=mTEPES.ch)
|
|
1138
|
-
pMinPowerHeat = pd.DataFrame([[pMinPowerElec [ch][p,sc,n]/pPower2HeatRatio[ch]
|
|
1139
|
-
pMaxPowerHeat = pd.DataFrame([[pMaxPowerElec [ch][p,sc,n]/pPower2HeatRatio[ch]
|
|
1140
|
-
pMinPowerHeat.update(pd.DataFrame([[pRatedMinPowerHeat[bo]
|
|
1141
|
-
pMaxPowerHeat.update(pd.DataFrame([[pRatedMaxPowerHeat[bo]
|
|
1142
|
-
pMaxPowerHeat.update(pd.DataFrame([[pMaxCharge[hp][p,sc,n]/pProductionFunctionHeat[hp]
|
|
1146
|
+
pMinPowerHeat = pd.DataFrame([[pMinPowerElec [ch][p,sc,n]/pPower2HeatRatio[ch] for ch in mTEPES.ch] for p,sc,n in mTEPES.psn], index=pd.MultiIndex.from_tuples(mTEPES.psn), columns=mTEPES.ch)
|
|
1147
|
+
pMaxPowerHeat = pd.DataFrame([[pMaxPowerElec [ch][p,sc,n]/pPower2HeatRatio[ch] for ch in mTEPES.ch] for p,sc,n in mTEPES.psn], index=pd.MultiIndex.from_tuples(mTEPES.psn), columns=mTEPES.ch)
|
|
1148
|
+
pMinPowerHeat.update(pd.DataFrame([[pRatedMinPowerHeat[bo] for bo in mTEPES.bo] for p,sc,n in mTEPES.psn], index=pd.MultiIndex.from_tuples(mTEPES.psn), columns=mTEPES.bo))
|
|
1149
|
+
pMaxPowerHeat.update(pd.DataFrame([[pRatedMaxPowerHeat[bo] for bo in mTEPES.bo] for p,sc,n in mTEPES.psn], index=pd.MultiIndex.from_tuples(mTEPES.psn), columns=mTEPES.bo))
|
|
1150
|
+
pMaxPowerHeat.update(pd.DataFrame([[pMaxCharge[hp][p,sc,n]/pProductionFunctionHeat[hp] for hp in mTEPES.hp] for p,sc,n in mTEPES.psn], index=pd.MultiIndex.from_tuples(mTEPES.psn), columns=mTEPES.hp))
|
|
1143
1151
|
|
|
1144
1152
|
# drop values not par, p, or ps
|
|
1145
1153
|
pReserveMargin = pReserveMargin.loc [mTEPES.par]
|
|
1146
1154
|
pEmission = pEmission.loc [mTEPES.par]
|
|
1147
1155
|
pRESEnergy = pRESEnergy.loc [mTEPES.par]
|
|
1148
1156
|
pDemandElecPeak = pDemandElecPeak.loc[mTEPES.par]
|
|
1157
|
+
pDemandHeatPeak = pDemandHeatPeak.loc[mTEPES.par]
|
|
1149
1158
|
pPeriodWeight = pPeriodWeight.loc [mTEPES.p ]
|
|
1150
1159
|
pScenProb = pScenProb.loc [mTEPES.ps ]
|
|
1151
1160
|
|
|
@@ -1208,6 +1217,7 @@ def InputData(DirName, CaseName, mTEPES, pIndLogConsole):
|
|
|
1208
1217
|
pH2PipeUpInvest = pH2PipeUpInvest.loc [mTEPES.pc]
|
|
1209
1218
|
|
|
1210
1219
|
if pIndHeat == 1:
|
|
1220
|
+
pReserveMarginHeat = pReserveMarginHeat.loc [mTEPES.par]
|
|
1211
1221
|
# drop generators not hp
|
|
1212
1222
|
pProductionFunctionHeat = pProductionFunctionHeat.loc [mTEPES.hp]
|
|
1213
1223
|
# drop generators not hh
|
|
@@ -1402,6 +1412,7 @@ def InputData(DirName, CaseName, mTEPES, pIndLogConsole):
|
|
|
1402
1412
|
pMinPowerHeat = filter_rows(pMinPowerHeat, mTEPES.psnch)
|
|
1403
1413
|
pMaxPowerHeat = filter_rows(pMaxPowerHeat, mTEPES.psnch)
|
|
1404
1414
|
|
|
1415
|
+
mTEPES.pReserveMarginHeat = Param(mTEPES.par, initialize=pReserveMarginHeat.to_dict() , within=NonNegativeReals, doc='Adequacy reserve margin' )
|
|
1405
1416
|
mTEPES.pRatedMaxPowerHeat = Param(mTEPES.gg, initialize=pRatedMaxPowerHeat.to_dict() , within=NonNegativeReals, doc='Rated maximum heat' )
|
|
1406
1417
|
mTEPES.pMinPowerHeat = Param(mTEPES.psnch, initialize=pMinPowerHeat.to_dict() , within=NonNegativeReals, doc='Minimum heat power' )
|
|
1407
1418
|
mTEPES.pMaxPowerHeat = Param(mTEPES.psnch, initialize=pMaxPowerHeat.to_dict() , within=NonNegativeReals, doc='Maximum heat power' )
|
|
@@ -1442,17 +1453,18 @@ def InputData(DirName, CaseName, mTEPES, pIndLogConsole):
|
|
|
1442
1453
|
mTEPES.pDemandH2Abs = Param(mTEPES.psnnd, initialize=pDemandH2Abs.to_dict(), within=NonNegativeReals, doc='Hydrogen demand' )
|
|
1443
1454
|
|
|
1444
1455
|
if pIndHeat == 1:
|
|
1445
|
-
pDemandHeat
|
|
1446
|
-
pDemandHeatAbs
|
|
1456
|
+
pDemandHeat = filter_rows(pDemandHeat , mTEPES.psnnd)
|
|
1457
|
+
pDemandHeatAbs = filter_rows(pDemandHeatAbs , mTEPES.psnnd)
|
|
1447
1458
|
|
|
1448
|
-
mTEPES.
|
|
1449
|
-
mTEPES.
|
|
1459
|
+
mTEPES.pDemandHeatPeak = Param(mTEPES.par, initialize=pDemandHeatPeak.to_dict(), within=NonNegativeReals, doc='Peak heat demand' )
|
|
1460
|
+
mTEPES.pDemandHeat = Param(mTEPES.psnnd, initialize=pDemandHeat.to_dict() , within=NonNegativeReals, doc='Heat demand per hour' )
|
|
1461
|
+
mTEPES.pDemandHeatAbs = Param(mTEPES.psnnd, initialize=pDemandHeatAbs.to_dict(), within=NonNegativeReals, doc='Heat demand' )
|
|
1450
1462
|
|
|
1451
|
-
mTEPES.pLoadLevelDuration = Param(mTEPES.psn, initialize=0
|
|
1463
|
+
mTEPES.pLoadLevelDuration = Param(mTEPES.psn, initialize=0 , within=NonNegativeIntegers, doc='Load level duration', mutable=True)
|
|
1452
1464
|
for p,sc,n in mTEPES.psn:
|
|
1453
1465
|
mTEPES.pLoadLevelDuration[p,sc,n] = mTEPES.pLoadLevelWeight[p,sc,n]() * mTEPES.pDuration[p,sc,n]()
|
|
1454
1466
|
|
|
1455
|
-
mTEPES.pPeriodProb = Param(mTEPES.ps, initialize=0.0
|
|
1467
|
+
mTEPES.pPeriodProb = Param(mTEPES.ps, initialize=0.0 , within=NonNegativeReals, doc='Period probability', mutable=True)
|
|
1456
1468
|
for p,sc in mTEPES.ps:
|
|
1457
1469
|
# periods and scenarios are going to be solved together with their weight and probability
|
|
1458
1470
|
mTEPES.pPeriodProb[p,sc] = mTEPES.pPeriodWeight[p] * mTEPES.pScenProb[p,sc]
|
|
@@ -2427,8 +2439,12 @@ def SettingUpVariables(OptModel, mTEPES):
|
|
|
2427
2439
|
|
|
2428
2440
|
# detecting reserve margin infeasibility
|
|
2429
2441
|
for p,ar in mTEPES.p*mTEPES.ar:
|
|
2430
|
-
if
|
|
2431
|
-
raise
|
|
2442
|
+
if sum(mTEPES.pRatedMaxPowerElec[g] * mTEPES.pAvailability[g]() / (1.0-mTEPES.pEFOR[g]) for g in mTEPES.g if (p,g) in mTEPES.pg and (ar,g) in mTEPES.a2g) < mTEPES.pDemandElecPeak[p,ar] * mTEPES.pReserveMargin[p,ar]:
|
|
2443
|
+
raise ValueError('### Electricity reserve margin infeasibility ', p, ar, sum(mTEPES.pRatedMaxPowerElec[g] * mTEPES.pAvailability[g]() / (1.0-mTEPES.pEFOR[g]) for g in mTEPES.g if (p,g) in mTEPES.pg and (ar,g) in mTEPES.a2g), mTEPES.pDemandElecPeak[p,ar] * mTEPES.pReserveMargin[p,ar])
|
|
2444
|
+
|
|
2445
|
+
if mTEPES.pIndHeat == 1:
|
|
2446
|
+
if sum(mTEPES.pRatedMaxPowerHeat[g] * mTEPES.pAvailability[g]() / (1.0-mTEPES.pEFOR[g]) for g in mTEPES.g if (p,g) in mTEPES.pg and (ar,g) in mTEPES.a2g) < mTEPES.pDemandHeatPeak[p,ar] * mTEPES.pReserveMarginHeat[p,ar]:
|
|
2447
|
+
raise ValueError('### Heat reserve margin infeasibility ', p, ar, sum(mTEPES.pRatedMaxPowerHeat[g] * mTEPES.pAvailability[g]() / (1.0-mTEPES.pEFOR[g]) for g in mTEPES.g if (p,g) in mTEPES.pg and (ar,g) in mTEPES.a2g), mTEPES.pDemandHeatPeak[p,ar] * mTEPES.pReserveMargin[p,ar])
|
|
2432
2448
|
|
|
2433
2449
|
DetectInfeasibilities(mTEPES)
|
|
2434
2450
|
|
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) - December
|
|
663
|
+
# Open Generation, Storage, and Transmission Operation and Expansion Planning Model with RES and ESS (openTEPES) - December 19, 2024
|
|
664
664
|
# simplicity and transparency in power systems planning
|
|
665
665
|
|
|
666
666
|
# Developed by
|
|
@@ -685,7 +685,7 @@ import time
|
|
|
685
685
|
# import pkg_resources
|
|
686
686
|
from .openTEPES import openTEPES_run
|
|
687
687
|
|
|
688
|
-
print('\033[1;32mOpen Generation, Storage, and Transmission Operation and Expansion Planning Model with RES and ESS (openTEPES) - Version 4.18.
|
|
688
|
+
print('\033[1;32mOpen Generation, Storage, and Transmission Operation and Expansion Planning Model with RES and ESS (openTEPES) - Version 4.18.1 - December 19, 2024\033[0m')
|
|
689
689
|
print('\033[34m#### Academic research license - for non-commercial use only ####\033[0m \n')
|
|
690
690
|
|
|
691
691
|
parser = argparse.ArgumentParser(description='Introducing main parameters...')
|
|
@@ -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) - December 11, 2024
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import time
|
|
@@ -291,17 +291,32 @@ def GenerationOperationModelFormulationInvestment(OptModel, mTEPES, pIndLogConso
|
|
|
291
291
|
if pIndLogConsole == 1:
|
|
292
292
|
print('eUninstallGenCap ... ', len(getattr(OptModel, f'eUninstallGenCap_{p}_{sc}_{st}')), ' rows')
|
|
293
293
|
|
|
294
|
-
def
|
|
294
|
+
def eAdequacyReserveMarginElec(OptModel,ar):
|
|
295
295
|
if mTEPES.pReserveMargin[p,ar] and st == mTEPES.Last_st and sum(1 for gc in mTEPES.gc if (ar,gc) in mTEPES.a2g) and sum(mTEPES.pRatedMaxPowerElec[g] * mTEPES.pAvailability[g]() / (1.0-mTEPES.pEFOR[g]) for g in mTEPES.g if (ar,g) in mTEPES.a2g and g not in (mTEPES.gc or mTEPES.gd)) <= mTEPES.pDemandElecPeak[p,ar] * mTEPES.pReserveMargin[p,ar]:
|
|
296
296
|
return ((sum( mTEPES.pRatedMaxPowerElec[g ] * mTEPES.pAvailability[g ]() / (1.0-mTEPES.pEFOR[g ]) for g in mTEPES.g if (p,g ) in mTEPES.pg and (ar,g ) in mTEPES.a2g and g not in (mTEPES.gc or mTEPES.gd)) +
|
|
297
297
|
sum( OptModel.vGenerationInvest[p,gc] * mTEPES.pRatedMaxPowerElec[gc] * mTEPES.pAvailability[gc]() / (1.0-mTEPES.pEFOR[gc]) for gc in mTEPES.gc if (p,gc) in mTEPES.pgc and (ar,gc) in mTEPES.a2g ) +
|
|
298
298
|
sum((1-OptModel.vGenerationRetire[p,gd]) * mTEPES.pRatedMaxPowerElec[gd] * mTEPES.pAvailability[gd]() / (1.0-mTEPES.pEFOR[gd]) for gd in mTEPES.gd if (p,gd) in mTEPES.pgd and (ar,gd) in mTEPES.a2g ) ) >= mTEPES.pDemandElecPeak[p,ar] * mTEPES.pReserveMargin[p,ar])
|
|
299
299
|
else:
|
|
300
300
|
return Constraint.Skip
|
|
301
|
-
setattr(OptModel, f'
|
|
301
|
+
setattr(OptModel, f'eAdequacyReserveMarginElec_{p}_{sc}_{st}', Constraint(mTEPES.ar, rule=eAdequacyReserveMarginElec, doc='electricity system adequacy reserve margin [p.u.]'))
|
|
302
302
|
|
|
303
303
|
if pIndLogConsole == 1:
|
|
304
|
-
print('
|
|
304
|
+
print('eAdeqReserveMarginElec... ', len(getattr(OptModel, f'eAdequacyReserveMarginElec_{p}_{sc}_{st}')), ' rows')
|
|
305
|
+
|
|
306
|
+
def eAdequacyReserveMarginHeat(OptModel,ar):
|
|
307
|
+
if mTEPES.pIndHeat == 1:
|
|
308
|
+
if mTEPES.pReserveMarginHeat[p,ar] and st == mTEPES.Last_st and sum(1 for gc in mTEPES.gc if (ar,gc) in mTEPES.a2g) and sum(mTEPES.pRatedMaxPowerHeat[g] * mTEPES.pAvailability[g]() / (1.0-mTEPES.pEFOR[g]) for g in mTEPES.g if (ar,g) in mTEPES.a2g and g not in (mTEPES.gc or mTEPES.gd)) <= mTEPES.pDemandHeatPeak[p,ar] * mTEPES.pReserveMarginHeat[p,ar]:
|
|
309
|
+
return ((sum( mTEPES.pRatedMaxPowerHeat[g ] * mTEPES.pAvailability[g ]() / (1.0-mTEPES.pEFOR[g ]) for g in mTEPES.g if (p,g ) in mTEPES.pg and (ar,g ) in mTEPES.a2g and g not in (mTEPES.gc or mTEPES.gd)) +
|
|
310
|
+
sum( OptModel.vGenerationInvest[p,gc] * mTEPES.pRatedMaxPowerHeat[gc] * mTEPES.pAvailability[gc]() / (1.0-mTEPES.pEFOR[gc]) for gc in mTEPES.gc if (p,gc) in mTEPES.pgc and (ar,gc) in mTEPES.a2g ) +
|
|
311
|
+
sum((1-OptModel.vGenerationRetire[p,gd]) * mTEPES.pRatedMaxPowerHeat[gd] * mTEPES.pAvailability[gd]() / (1.0-mTEPES.pEFOR[gd]) for gd in mTEPES.gd if (p,gd) in mTEPES.pgd and (ar,gd) in mTEPES.a2g ) ) >= mTEPES.pDemandHeatPeak[p,ar] * mTEPES.pReserveMarginHeat[p,ar])
|
|
312
|
+
else:
|
|
313
|
+
return Constraint.Skip
|
|
314
|
+
else:
|
|
315
|
+
return Constraint.Skip
|
|
316
|
+
setattr(OptModel, f'eAdequacyReserveMarginHeat_{p}_{sc}_{st}', Constraint(mTEPES.ar, rule=eAdequacyReserveMarginHeat, doc='heat system adequacy reserve margin [p.u.]'))
|
|
317
|
+
|
|
318
|
+
if pIndLogConsole == 1:
|
|
319
|
+
print('eAdeqReserveMarginHeat... ', len(getattr(OptModel, f'eAdequacyReserveMarginHeat_{p}_{sc}_{st}')), ' rows')
|
|
305
320
|
|
|
306
321
|
def eMaxSystemEmission(OptModel,ar):
|
|
307
322
|
if mTEPES.pEmission[p,ar] < math.inf and st == mTEPES.Last_st and sum(mTEPES.pEmissionVarCost[p,sc,na,nr] for na,nr in mTEPES.na*mTEPES.nr if (ar,nr) in mTEPES.a2g):
|
|
@@ -1493,26 +1508,26 @@ def NetworkHeatOperationModelFormulation(OptModel, mTEPES, pIndLogConsole, p, sc
|
|
|
1493
1508
|
lout[ni].append((nf,cc))
|
|
1494
1509
|
|
|
1495
1510
|
# nodes to CHPs (c2n)
|
|
1496
|
-
|
|
1497
|
-
for nd,
|
|
1498
|
-
if (nd,
|
|
1499
|
-
|
|
1511
|
+
chp2n = defaultdict(list)
|
|
1512
|
+
for nd,chp in mTEPES.nd*mTEPES.chp:
|
|
1513
|
+
if (nd,chp) in mTEPES.n2g:
|
|
1514
|
+
chp2n[nd].append(chp)
|
|
1500
1515
|
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1516
|
+
def eEnergy2Heat(OptModel,n,chp):
|
|
1517
|
+
if (p,chp) in mTEPES.pchp and chp not in mTEPES.bo and chp in mTEPES.ch:
|
|
1518
|
+
return OptModel.vTotalOutputHeat[p,sc,n,chp] == OptModel.vTotalOutput[p,sc,n,chp] / mTEPES.pPower2HeatRatio[chp]
|
|
1519
|
+
elif (p,chp) in mTEPES.pchp and chp in mTEPES.hp:
|
|
1520
|
+
return OptModel.vTotalOutputHeat[p,sc,n,chp] == OptModel.vESSTotalCharge[p,sc,n,chp] / mTEPES.pProductionFunctionHeat[chp]
|
|
1521
|
+
else:
|
|
1522
|
+
return Constraint.Skip
|
|
1523
|
+
setattr(OptModel, f'eEnergy2Heat_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.chp, rule=eEnergy2Heat, doc='Energy to heat conversion [Tcal/h]'))
|
|
1506
1524
|
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
for nd,hp in mTEPES.nd*mTEPES.hp:
|
|
1510
|
-
if (nd,hp) in mTEPES.n2g:
|
|
1511
|
-
h2n[nd].append(hp)
|
|
1525
|
+
if pIndLogConsole == 1:
|
|
1526
|
+
print('eEnergy2Heat ... ', len(getattr(OptModel, f'eEnergy2Heat_{p}_{sc}_{st}')), ' rows')
|
|
1512
1527
|
|
|
1513
1528
|
def eBalanceHeat(OptModel,n,nd):
|
|
1514
|
-
if sum(1 for ch in
|
|
1515
|
-
return (sum(OptModel.
|
|
1529
|
+
if sum(1 for ch in chp2n[nd]) + sum(1 for nf,cc in lout[nd]) + sum(1 for ni,cc in lin[nd]):
|
|
1530
|
+
return (sum(OptModel.vTotalOutputHeat[p,sc,n,chp] for chp in chp2n[nd] if (p,chp) in mTEPES.pchp) + OptModel.vHeatNS[p,sc,n,nd] -
|
|
1516
1531
|
sum(OptModel.vFlowHeat[p,sc,n,nd,nf,cc] for nf,cc in lout[nd] if (p,nd,nf,cc) in mTEPES.pha) + sum(OptModel.vFlowHeat[p,sc,n,ni,nd,cc] for ni,cc in lin[nd] if (p,ni,nd,cc) in mTEPES.pha)) == mTEPES.pDemandHeat[p,sc,n,nd]
|
|
1517
1532
|
else:
|
|
1518
1533
|
return Constraint.Skip
|