openTEPES 4.18.3__py3-none-any.whl → 4.18.5__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/9n_PTDF/oT_Data_Demand_9n_PTDF.csv +8737 -0
- openTEPES/9n_PTDF/oT_Data_Duration_9n_PTDF.csv +8737 -0
- openTEPES/9n_PTDF/oT_Data_Emission_9n_PTDF.csv +2 -0
- openTEPES/9n_PTDF/oT_Data_EnergyInflows_9n_PTDF.csv +8737 -0
- openTEPES/9n_PTDF/oT_Data_EnergyOutflows_9n_PTDF.csv +8737 -0
- openTEPES/9n_PTDF/oT_Data_Generation_9n_PTDF.csv +17 -0
- openTEPES/9n_PTDF/oT_Data_Inertia_9n_PTDF.csv +8737 -0
- openTEPES/9n_PTDF/oT_Data_Network_9n_PTDF.csv +14 -0
- openTEPES/9n_PTDF/oT_Data_NodeLocation_9n_PTDF.csv +10 -0
- openTEPES/9n_PTDF/oT_Data_OperatingReserveDown_9n_PTDF.csv +8737 -0
- openTEPES/9n_PTDF/oT_Data_OperatingReserveUp_9n_PTDF.csv +8737 -0
- openTEPES/9n_PTDF/oT_Data_Option_9n_PTDF.csv +2 -0
- openTEPES/9n_PTDF/oT_Data_Parameter_9n_PTDF.csv +2 -0
- openTEPES/9n_PTDF/oT_Data_Period_9n_PTDF.csv +2 -0
- openTEPES/9n_PTDF/oT_Data_RESEnergy_9n_PTDF.csv +2 -0
- openTEPES/9n_PTDF/oT_Data_ReserveMargin_9n_PTDF.csv +2 -0
- openTEPES/9n_PTDF/oT_Data_Scenario_9n_PTDF.csv +2 -0
- openTEPES/9n_PTDF/oT_Data_Stage_9n_PTDF.csv +2 -0
- openTEPES/9n_PTDF/oT_Data_VariableEmissionCost_9n_PTDF.csv +8737 -0
- openTEPES/9n_PTDF/oT_Data_VariableFuelCost_9n_PTDF.csv +8737 -0
- openTEPES/9n_PTDF/oT_Data_VariableMaxConsumption_9n_PTDF.csv +8737 -0
- openTEPES/9n_PTDF/oT_Data_VariableMaxEnergy_9n_PTDF.csv +8737 -0
- openTEPES/9n_PTDF/oT_Data_VariableMaxGeneration_9n_PTDF.csv +8737 -0
- openTEPES/9n_PTDF/oT_Data_VariableMaxStorage_9n_PTDF.csv +8737 -0
- openTEPES/9n_PTDF/oT_Data_VariableMinConsumption_9n_PTDF.csv +8737 -0
- openTEPES/9n_PTDF/oT_Data_VariableMinEnergy_9n_PTDF.csv +8737 -0
- openTEPES/9n_PTDF/oT_Data_VariableMinGeneration_9n_PTDF.csv +8737 -0
- openTEPES/9n_PTDF/oT_Data_VariableMinStorage_9n_PTDF.csv +8737 -0
- openTEPES/9n_PTDF/oT_Data_VariablePTDF_9n_PTDF.csv +8740 -0
- openTEPES/9n_PTDF/oT_Data_VariableTTCBck_9n_PTDF.csv +8739 -0
- openTEPES/9n_PTDF/oT_Data_VariableTTCFrw_9n_PTDF.csv +8739 -0
- openTEPES/9n_PTDF/oT_Dict_AreaToRegion_9n_PTDF.csv +2 -0
- openTEPES/9n_PTDF/oT_Dict_Area_9n_PTDF.csv +2 -0
- openTEPES/9n_PTDF/oT_Dict_Circuit_9n_PTDF.csv +3 -0
- openTEPES/9n_PTDF/oT_Dict_Generation_9n_PTDF.csv +17 -0
- openTEPES/9n_PTDF/oT_Dict_Line_9n_PTDF.csv +3 -0
- openTEPES/9n_PTDF/oT_Dict_LoadLevel_9n_PTDF.csv +8737 -0
- openTEPES/9n_PTDF/oT_Dict_NodeToZone_9n_PTDF.csv +10 -0
- openTEPES/9n_PTDF/oT_Dict_Node_9n_PTDF.csv +10 -0
- openTEPES/9n_PTDF/oT_Dict_Period_9n_PTDF.csv +2 -0
- openTEPES/9n_PTDF/oT_Dict_Region_9n_PTDF.csv +31 -0
- openTEPES/9n_PTDF/oT_Dict_Scenario_9n_PTDF.csv +2 -0
- openTEPES/9n_PTDF/oT_Dict_Stage_9n_PTDF.csv +2 -0
- openTEPES/9n_PTDF/oT_Dict_Storage_9n_PTDF.csv +3 -0
- openTEPES/9n_PTDF/oT_Dict_Technology_9n_PTDF.csv +7 -0
- openTEPES/9n_PTDF/oT_Dict_ZoneToArea_9n_PTDF.csv +10 -0
- openTEPES/9n_PTDF/oT_Dict_Zone_9n_PTDF.csv +10 -0
- openTEPES/RTS-GMLC_6y/oT_Dict_AreaToRegion_RTS-GMLC_6y.csv +4 -4
- openTEPES/RTS-GMLC_6y/oT_Dict_Area_RTS-GMLC_6y.csv +4 -4
- openTEPES/RTS-GMLC_6y/oT_Dict_Circuit_RTS-GMLC_6y.csv +5 -5
- openTEPES/RTS-GMLC_6y/oT_Dict_Line_RTS-GMLC_6y.csv +3 -3
- openTEPES/RTS-GMLC_6y/oT_Dict_NodeToZone_RTS-GMLC_6y.csv +74 -74
- openTEPES/RTS-GMLC_6y/oT_Dict_Region_RTS-GMLC_6y.csv +2 -2
- openTEPES/RTS-GMLC_6y/oT_Dict_Scenario_RTS-GMLC_6y.csv +2 -2
- openTEPES/RTS-GMLC_6y/oT_Dict_Storage_RTS-GMLC_6y.csv +3 -3
- openTEPES/RTS-GMLC_6y/oT_Dict_Technology_RTS-GMLC_6y.csv +10 -10
- openTEPES/RTS-GMLC_6y/oT_Dict_ZoneToArea_RTS-GMLC_6y.csv +22 -22
- openTEPES/RTS-GMLC_6y/oT_Dict_Zone_RTS-GMLC_6y.csv +22 -22
- openTEPES/__init__.py +1 -1
- openTEPES/openTEPES.py +137 -65
- openTEPES/openTEPES_InputData.py +419 -234
- openTEPES/openTEPES_Main.py +2 -2
- openTEPES/openTEPES_ModelFormulation.py +469 -180
- openTEPES/openTEPES_OutputResults.py +305 -223
- openTEPES/openTEPES_ProblemSolving.py +68 -56
- {opentepes-4.18.3.dist-info → openTEPES-4.18.5.dist-info}/METADATA +17 -18
- {opentepes-4.18.3.dist-info → openTEPES-4.18.5.dist-info}/RECORD +70 -23
- {opentepes-4.18.3.dist-info → openTEPES-4.18.5.dist-info}/WHEEL +1 -1
- {opentepes-4.18.3.dist-info → openTEPES-4.18.5.dist-info}/LICENSE +0 -0
- {opentepes-4.18.3.dist-info → openTEPES-4.18.5.dist-info}/entry_points.txt +0 -0
|
@@ -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) - June 23, 2025
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import time
|
|
@@ -147,12 +147,12 @@ def GenerationOperationModelFormulationObjFunct(OptModel, mTEPES, pIndLogConsole
|
|
|
147
147
|
def eTotalGCost(OptModel,n):
|
|
148
148
|
return OptModel.vTotalGCost[p,sc,n] == (sum(mTEPES.pLoadLevelDuration[p,sc,n]() * mTEPES.pLinearVarCost [p,sc,n,nr] * OptModel.vTotalOutput [p,sc,n,nr] +
|
|
149
149
|
mTEPES.pLoadLevelDuration[p,sc,n]() * mTEPES.pConstantVarCost[p,sc,n,nr] * OptModel.vCommitment [p,sc,n,nr] +
|
|
150
|
-
mTEPES.
|
|
151
|
-
mTEPES.
|
|
152
|
-
sum(mTEPES.
|
|
153
|
-
mTEPES.
|
|
154
|
-
sum(mTEPES.
|
|
155
|
-
mTEPES.
|
|
150
|
+
mTEPES.pLoadLevelWeight [p,sc,n]() * mTEPES.pStartUpCost [ nr] * OptModel.vStartUp [p,sc,n,nr] +
|
|
151
|
+
mTEPES.pLoadLevelWeight [p,sc,n]() * mTEPES.pShutDownCost [ nr] * OptModel.vShutDown [p,sc,n,nr] for nr in mTEPES.nr if (p,nr) in mTEPES.pnr) +
|
|
152
|
+
sum(mTEPES.pLoadLevelWeight [p,sc,n]() * mTEPES.pOperReserveCost[ nr] * OptModel.vReserveUp [p,sc,n,nr] +
|
|
153
|
+
mTEPES.pLoadLevelWeight [p,sc,n]() * 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) +
|
|
154
|
+
sum(mTEPES.pLoadLevelWeight [p,sc,n]() * mTEPES.pOperReserveCost[ eh] * OptModel.vESSReserveUp [p,sc,n,eh] +
|
|
155
|
+
mTEPES.pLoadLevelWeight [p,sc,n]() * 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) +
|
|
156
156
|
sum(mTEPES.pLoadLevelDuration[p,sc,n]() * mTEPES.pLinearVarCost [p,sc,n,bo] * OptModel.vTotalOutputHeat[p,sc,n,bo] for bo in mTEPES.bo if (p,bo) in mTEPES.pbo) +
|
|
157
157
|
sum(mTEPES.pLoadLevelDuration[p,sc,n]() * mTEPES.pLinearOMCost [ re] * OptModel.vTotalOutput [p,sc,n,re] for re in mTEPES.re if (p,re) in mTEPES.pre) )
|
|
158
158
|
setattr(OptModel, f'eTotalGCost_{p}_{sc}_{st}', Constraint(mTEPES.n, rule=eTotalGCost, doc='system variable generation operation cost [MEUR]'))
|
|
@@ -403,118 +403,163 @@ def GenerationOperationModelFormulationDemand(OptModel, mTEPES, pIndLogConsole,
|
|
|
403
403
|
print('eSystemInertia ... ', len(getattr(OptModel, f'eSystemInertia_{p}_{sc}_{st}')), ' rows')
|
|
404
404
|
|
|
405
405
|
def eOperReserveUp(OptModel,n,ar):
|
|
406
|
-
if
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
406
|
+
# Skip if there are no upward operating reserves
|
|
407
|
+
if not mTEPES.pOperReserveUp[p,sc,n,ar]:
|
|
408
|
+
return Constraint.Skip
|
|
409
|
+
# Skip if there are no generators in this area which can provide reserves
|
|
410
|
+
if (sum(1 for nr in n2a[ar] if (mTEPES.pIndOperReserveGen[nr] == 0 or mTEPES.pIndOperReserveCon[nr] == 0 ))
|
|
411
|
+
+ sum(1 for eh in e2a[ar] if (mTEPES.pIndOperReserveGen[eh] == 0 or mTEPES.pIndOperReserveCon[eh] == 0 )) == 0):
|
|
412
412
|
return Constraint.Skip
|
|
413
|
+
|
|
414
|
+
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]
|
|
415
|
+
|
|
413
416
|
setattr(OptModel, f'eOperReserveUp_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.ar, rule=eOperReserveUp, doc='up operating reserve [GW]'))
|
|
414
417
|
|
|
415
418
|
if pIndLogConsole == 1:
|
|
416
419
|
print('eOperReserveUp ... ', len(getattr(OptModel, f'eOperReserveUp_{p}_{sc}_{st}')), ' rows')
|
|
417
420
|
|
|
418
421
|
def eOperReserveDw(OptModel,n,ar):
|
|
419
|
-
if
|
|
420
|
-
|
|
421
|
-
return sum(OptModel.vReserveDown[p,sc,n,nr] for nr in n2a[ar] if mTEPES.pIndOperReserve[nr] == 0 and (p,nr) in mTEPES.pnr) + sum(OptModel.vESSReserveDown[p,sc,n,eh] for eh in e2a[ar] if mTEPES.pIndOperReserve[eh] == 0 and (p,eh) in mTEPES.peh) == mTEPES.pOperReserveDw[p,sc,n,ar]
|
|
422
|
-
else:
|
|
423
|
-
return Constraint.Skip
|
|
424
|
-
else:
|
|
422
|
+
# Skip if there are no downward operating reserves
|
|
423
|
+
if not mTEPES.pOperReserveDw[p,sc,n,ar]:
|
|
425
424
|
return Constraint.Skip
|
|
425
|
+
# Skip if there are no generators in this area which can provide reserves
|
|
426
|
+
if (sum(1 for nr in n2a[ar] if (mTEPES.pIndOperReserveGen[nr] == 0 or mTEPES.pIndOperReserveCon[nr] == 0 ))
|
|
427
|
+
+ sum(1 for eh in e2a[ar] if (mTEPES.pIndOperReserveGen[eh] == 0 or mTEPES.pIndOperReserveCon[eh] == 0 )) == 0):
|
|
428
|
+
return Constraint.Skip
|
|
429
|
+
|
|
430
|
+
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]
|
|
431
|
+
|
|
426
432
|
setattr(OptModel, f'eOperReserveDw_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.ar, rule=eOperReserveDw, doc='down operating reserve [GW]'))
|
|
427
433
|
|
|
428
434
|
if pIndLogConsole == 1:
|
|
429
435
|
print('eOperReserveDw ... ', len(getattr(OptModel, f'eOperReserveDw_{p}_{sc}_{st}')), ' rows')
|
|
430
436
|
|
|
431
437
|
def eReserveMinRatioDwUp(OptModel,n,nr):
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
438
|
+
# Skip if there is no minimum up/down reserve ratio
|
|
439
|
+
if not mTEPES.pMinRatioDwUp:
|
|
440
|
+
return Constraint.Skip
|
|
441
|
+
# Skip if no reserves are needed in the Area where the generator is located
|
|
442
|
+
if sum(mTEPES.pOperReserveUp[p,sc,n,ar] + mTEPES.pOperReserveDw[p,sc,n,ar] for ar in a2n[nr]) == 0:
|
|
435
443
|
return Constraint.Skip
|
|
444
|
+
# Skip if generator cannot provide reserves while generating power
|
|
445
|
+
if mTEPES.pMaxPower2ndBlock[p,sc,n,nr] == 0 or mTEPES.pIndOperReserveGen[nr]:
|
|
446
|
+
return Constraint.Skip
|
|
447
|
+
|
|
448
|
+
return OptModel.vReserveDown[p,sc,n,nr] >= OptModel.vReserveUp[p,sc,n,nr] * mTEPES.pMinRatioDwUp
|
|
449
|
+
|
|
436
450
|
setattr(OptModel, f'eReserveMinRatioDwUp_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.nr, rule=eReserveMinRatioDwUp, doc='minimum ratio down to up operating reserve [GW]'))
|
|
437
451
|
|
|
438
452
|
if pIndLogConsole == 1:
|
|
439
453
|
print('eReserveMinRatioDwUp ... ', len(getattr(OptModel, f'eReserveMinRatioDwUp_{p}_{sc}_{st}')), ' rows')
|
|
440
454
|
|
|
441
455
|
def eReserveMaxRatioDwUp(OptModel,n,nr):
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
else:
|
|
456
|
+
# Skip if there is no maximum up/down reserve ratio
|
|
457
|
+
if mTEPES.pMaxRatioDwUp >= 1.0:
|
|
445
458
|
return Constraint.Skip
|
|
459
|
+
# Skip if no reserves are needed in the Area where the generator is located
|
|
460
|
+
if sum(mTEPES.pOperReserveUp[p,sc,n,ar] + mTEPES.pOperReserveDw[p,sc,n,ar] for ar in a2n[nr]) == 0:
|
|
461
|
+
return Constraint.Skip
|
|
462
|
+
# Skip if generator cannot provide reserves while generating power
|
|
463
|
+
if mTEPES.pMaxPower2ndBlock[p,sc,n,nr] == 0 or mTEPES.pIndOperReserveGen[nr]:
|
|
464
|
+
return Constraint.Skip
|
|
465
|
+
|
|
466
|
+
return OptModel.vReserveDown[p,sc,n,nr] <= OptModel.vReserveUp[p,sc,n,nr] * mTEPES.pMaxRatioDwUp
|
|
467
|
+
|
|
446
468
|
setattr(OptModel, f'eReserveMaxRatioDwUp_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.nr, rule=eReserveMaxRatioDwUp, doc='maximum ratio down to up operating reserve [GW]'))
|
|
447
469
|
|
|
448
470
|
if pIndLogConsole == 1:
|
|
449
471
|
print('eReserveMaxRatioDwUp ... ', len(getattr(OptModel, f'eReserveMaxRatioDwUp_{p}_{sc}_{st}')), ' rows')
|
|
450
472
|
|
|
451
473
|
def eRsrvMinRatioDwUpESS(OptModel,n,eh):
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
474
|
+
# Skip if there is no minimum up/down reserve ratio
|
|
475
|
+
if not mTEPES.pMinRatioDwUp:
|
|
476
|
+
return Constraint.Skip
|
|
477
|
+
# Skip if no reserves are needed in the Area where the generator is located
|
|
478
|
+
if sum(mTEPES.pOperReserveUp[p,sc,n,ar] + mTEPES.pOperReserveDw[p,sc,n,ar] for ar in a2n[nr]) == 0:
|
|
479
|
+
return Constraint.Skip
|
|
480
|
+
# Skip if generator cannot provide reserves while generating power
|
|
481
|
+
if mTEPES.pMaxPower2ndBlock[p,sc,n,nr] == 0 or mTEPES.pIndOperReserveCon[nr]:
|
|
455
482
|
return Constraint.Skip
|
|
483
|
+
|
|
484
|
+
return OptModel.vESSReserveDown[p,sc,n,eh] >= OptModel.vESSReserveUp[p,sc,n,eh] * mTEPES.pMinRatioDwUp
|
|
456
485
|
setattr(OptModel, f'eRsrvMinRatioDwUpESS_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.eh, rule=eRsrvMinRatioDwUpESS, doc='minimum ratio down to up operating reserve [GW]'))
|
|
457
486
|
|
|
458
487
|
if pIndLogConsole == 1:
|
|
459
488
|
print('eRsrvMinRatioDwUpESS ... ', len(getattr(OptModel, f'eRsrvMinRatioDwUpESS_{p}_{sc}_{st}')), ' rows')
|
|
460
489
|
|
|
461
490
|
def eRsrvMaxRatioDwUpESS(OptModel,n,eh):
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
491
|
+
# Skip if there is no maximum up/down reserve ratio
|
|
492
|
+
if mTEPES.pMaxRatioDwUp >= 1.0:
|
|
493
|
+
return Constraint.Skip
|
|
494
|
+
# Skip if no reserves are needed in the Area where the generator is located
|
|
495
|
+
if sum(mTEPES.pOperReserveUp[p,sc,n,ar] + mTEPES.pOperReserveDw[p,sc,n,ar] for ar in a2n[nr]) == 0:
|
|
465
496
|
return Constraint.Skip
|
|
497
|
+
# Skip if generator cannot provide reserves while generating power
|
|
498
|
+
if mTEPES.pMaxPower2ndBlock[p,sc,n,nr] == 0 or mTEPES.pIndOperReserveCon[nr]:
|
|
499
|
+
return Constraint.Skip
|
|
500
|
+
|
|
501
|
+
return OptModel.vESSReserveDown[p,sc,n,eh] <= OptModel.vESSReserveUp[p,sc,n,eh] * mTEPES.pMaxRatioDwUp
|
|
502
|
+
|
|
466
503
|
setattr(OptModel, f'eRsrvMaxRatioDwUpESS_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.eh, rule=eRsrvMaxRatioDwUpESS, doc='maximum ratio down to up operating reserve [GW]'))
|
|
467
504
|
|
|
468
505
|
if pIndLogConsole == 1:
|
|
469
506
|
print('eRsrvMaxRatioDwUpESS ... ', len(getattr(OptModel, f'eRsrvMaxRatioDwUpESS_{p}_{sc}_{st}')), ' rows')
|
|
470
507
|
|
|
508
|
+
|
|
471
509
|
def eReserveUpIfEnergy(OptModel,n,es):
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
510
|
+
# When ESS units offer operating reserves, they must be able to provide the corresponding energy
|
|
511
|
+
# This means they must have enough stored energy to provide all reserves if they were to have 100% activation
|
|
512
|
+
|
|
513
|
+
# Skip if generator is not available in the period
|
|
514
|
+
if (p,es) not in mTEPES.pes:
|
|
515
|
+
return Constraint.Skip
|
|
516
|
+
# Skip if generator cannot provide operating reserves while generating power
|
|
517
|
+
if mTEPES.pIndOperReserveGen[es] or mTEPES.pMaxPower2ndBlock [p,sc,n,es] == 0:
|
|
518
|
+
return Constraint.Skip
|
|
519
|
+
# Skip if no upward reserves are needed in the area where the generator is located
|
|
520
|
+
if sum(mTEPES.pOperReserveUp[p,sc,n,ar] for ar in a2e[es]) == 0:
|
|
521
|
+
return Constraint.Skip
|
|
522
|
+
# Skip if the ESS has no charging capabilities and receives no Inflows
|
|
523
|
+
if (mTEPES.pTotalMaxCharge[es] and mTEPES.pTotalEnergyInflows[es]) == 0:
|
|
478
524
|
return Constraint.Skip
|
|
525
|
+
# Skip if the duration of the LoadLevel is 0
|
|
526
|
+
if not mTEPES.pDuration[p,sc,n]():
|
|
527
|
+
return Constraint.Skip
|
|
528
|
+
|
|
529
|
+
# Avoid division by 0 if unit has no minimum power
|
|
530
|
+
if mTEPES.pMinPowerElec[p,sc,n,es] == 0:
|
|
531
|
+
return ((OptModel.vOutput2ndBlock[p,sc,n,es] + OptModel.vReserveUp[p,sc,n,es]) ) / math.sqrt(mTEPES.pEfficiency[es])<= (OptModel.vESSInventory[p,sc,n,es] - mTEPES.pMinStorage[p,sc,n,es]) / mTEPES.pDuration[p,sc,n]()
|
|
532
|
+
else:
|
|
533
|
+
return ((OptModel.vOutput2ndBlock[p,sc,n,es] + OptModel.vReserveUp[p,sc,n,es]) / mTEPES.pMinPowerElec[p,sc,n,es] + 1) / math.sqrt(mTEPES.pEfficiency[es]) <= (OptModel.vESSInventory[p,sc,n,es] - mTEPES.pMinStorage[p,sc,n,es]) / mTEPES.pDuration[p,sc,n]() / mTEPES.pMinPowerElec[p,sc,n,es]
|
|
534
|
+
|
|
479
535
|
setattr(OptModel, f'eReserveUpIfEnergy_{p}_{sc}_{st}', Constraint(mTEPES.nesc, rule=eReserveUpIfEnergy, doc='up operating reserve if energy available [GW]'))
|
|
480
536
|
|
|
481
537
|
if pIndLogConsole == 1:
|
|
482
538
|
print('eReserveUpIfEnergy ... ', len(getattr(OptModel, f'eReserveUpIfEnergy_{p}_{sc}_{st}')), ' rows')
|
|
483
539
|
|
|
484
|
-
def
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
return OptModel.vReserveDown[p,sc,n,es] <= (mTEPES.pMaxStorage[p,sc,n,es] - OptModel.vESSInventory[p,sc,n,es]) / mTEPES.pDuration[p,sc,n]()
|
|
488
|
-
else:
|
|
489
|
-
return Constraint.Skip
|
|
490
|
-
else:
|
|
491
|
-
return Constraint.Skip
|
|
492
|
-
setattr(OptModel, f'eReserveDwIfEnergy_{p}_{sc}_{st}', Constraint(mTEPES.nesc, rule=eReserveDwIfEnergy, doc='down operating reserve if energy available [GW]'))
|
|
493
|
-
|
|
494
|
-
if pIndLogConsole == 1:
|
|
495
|
-
print('eReserveDwIfEnergy ... ', len(getattr(OptModel, f'eReserveDwIfEnergy_{p}_{sc}_{st}')), ' rows')
|
|
540
|
+
def eESSReserveDwIfEnergy(OptModel,n,es):
|
|
541
|
+
# When ESS units offer operating reserves, they must be able to provide the corresponding energy
|
|
542
|
+
# This means they must have enough stored energy to provide all reserves if they were to have 100% activation
|
|
496
543
|
|
|
497
|
-
|
|
498
|
-
if
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
544
|
+
# Skip if generator is not available in the period
|
|
545
|
+
if (p,es) not in mTEPES.pes:
|
|
546
|
+
return Constraint.Skip
|
|
547
|
+
# Skip if generator cannot provide operating reserves while generating power
|
|
548
|
+
if mTEPES.pIndOperReserveGen[es] or mTEPES.pMaxPower2ndBlock [p,sc,n,es] == 0:
|
|
549
|
+
return Constraint.Skip
|
|
550
|
+
# Skip if no upward reserves are needed in the area where the generator is located
|
|
551
|
+
if sum(mTEPES.pOperReserveUp[p,sc,n,ar] for ar in a2e[es]) == 0:
|
|
552
|
+
return Constraint.Skip
|
|
553
|
+
# Skip if the duration of the LoadLevel is 0
|
|
554
|
+
if not mTEPES.pDuration[p,sc,n]():
|
|
504
555
|
return Constraint.Skip
|
|
505
|
-
setattr(OptModel, f'eESSReserveUpIfEnergy_{p}_{sc}_{st}', Constraint(mTEPES.nesc, rule=eESSReserveUpIfEnergy, doc='up operating reserve if energy available [GW]'))
|
|
506
|
-
|
|
507
|
-
if pIndLogConsole == 1:
|
|
508
|
-
print('eESSReserveUpIfEnergy ... ', len(getattr(OptModel, f'eESSReserveUpIfEnergy_{p}_{sc}_{st}')), ' rows')
|
|
509
556
|
|
|
510
|
-
|
|
511
|
-
if mTEPES.
|
|
512
|
-
|
|
513
|
-
return OptModel.vESSReserveDown[p,sc,n,es] <= OptModel.vESSInventory[p,sc,n,es] / mTEPES.pDuration[p,sc,n]()
|
|
514
|
-
else:
|
|
515
|
-
return Constraint.Skip
|
|
557
|
+
# Avoid division by 0 if unit has no minimum power
|
|
558
|
+
if mTEPES.pMinCharge[p,sc,n,es] == 0:
|
|
559
|
+
return OptModel.vCharge2ndBlock[p,sc,n,es] + OptModel.vESSReserveDown[p,sc,n,es] * math.sqrt(mTEPES.pEfficiency[es]) <= (mTEPES.pMaxStorage[p,sc,n,es] - OptModel.vESSInventory[p,sc,n,es]) / mTEPES.pDuration[p,sc,n]()
|
|
516
560
|
else:
|
|
517
|
-
return
|
|
561
|
+
return ((OptModel.vCharge2ndBlock[p,sc,n,es] + OptModel.vESSReserveDown[p,sc,n,es]) / mTEPES.pMinCharge[p,sc,n,es] + 1) * math.sqrt(mTEPES.pEfficiency[es]) <= (mTEPES.pMaxStorage[p,sc,n,es] - OptModel.vESSInventory[p,sc,n,es]) / mTEPES.pDuration[p,sc,n]() / mTEPES.pMinCharge[p,sc,n,es]
|
|
562
|
+
|
|
518
563
|
setattr(OptModel, f'eESSReserveDwIfEnergy_{p}_{sc}_{st}', Constraint(mTEPES.nesc, rule=eESSReserveDwIfEnergy, doc='down operating reserve if energy available [GW]'))
|
|
519
564
|
|
|
520
565
|
if pIndLogConsole == 1:
|
|
@@ -591,14 +636,14 @@ def GenerationOperationModelFormulationStorage(OptModel, mTEPES, pIndLogConsole,
|
|
|
591
636
|
if (p,es) in mTEPES.pes and (mTEPES.pTotalMaxCharge[es] or mTEPES.pTotalEnergyInflows[es]):
|
|
592
637
|
if (p,sc,st,n) in mTEPES.s2n and mTEPES.n.ord(n) == mTEPES.pStorageTimeStep[es]:
|
|
593
638
|
if es not in mTEPES.ec:
|
|
594
|
-
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] + 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]
|
|
639
|
+
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]
|
|
595
640
|
else:
|
|
596
|
-
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] + 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]
|
|
641
|
+
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]
|
|
597
642
|
elif (p,sc,st,n) in mTEPES.s2n and mTEPES.n.ord(n) > mTEPES.pStorageTimeStep[es]:
|
|
598
643
|
if es not in mTEPES.ec:
|
|
599
|
-
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] + 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]
|
|
644
|
+
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]
|
|
600
645
|
else:
|
|
601
|
-
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] + 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]
|
|
646
|
+
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]
|
|
602
647
|
else:
|
|
603
648
|
return Constraint.Skip
|
|
604
649
|
else:
|
|
@@ -639,26 +684,42 @@ def GenerationOperationModelFormulationStorage(OptModel, mTEPES, pIndLogConsole,
|
|
|
639
684
|
print('eMaxShiftTime ... ', len(getattr(OptModel, f'eMaxShiftTime_{p}_{sc}_{st}')), ' rows')
|
|
640
685
|
|
|
641
686
|
def eMaxCharge(OptModel,n,eh):
|
|
642
|
-
if
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
687
|
+
# Check if generator is available in the period and has variable charging capacity
|
|
688
|
+
if (p,eh) not in mTEPES.peh or mTEPES.pMaxCharge2ndBlock[p,sc,n,eh] == 0:
|
|
689
|
+
return Constraint.Skip
|
|
690
|
+
|
|
691
|
+
# Hydro units have commitment while ESS units are implicitly always committed
|
|
692
|
+
if eh not in mTEPES.h:
|
|
693
|
+
# ESS units only need this constraint when they can offer operating reserves and the systems demands reserves
|
|
694
|
+
if mTEPES.pIndOperReserveCon[eh] != 0 or not sum(mTEPES.pOperReserveDw[p,sc,n,ar] for ar in a2e[eh]):
|
|
646
695
|
return Constraint.Skip
|
|
696
|
+
# ESS case equation
|
|
697
|
+
return (OptModel.vCharge2ndBlock[p, sc, n, eh] + OptModel.vESSReserveDown[p, sc, n, eh]) / mTEPES.pMaxCharge2ndBlock[p, sc, n, eh] <= 1.0
|
|
698
|
+
# Hydro case equation
|
|
699
|
+
|
|
647
700
|
else:
|
|
648
|
-
return
|
|
701
|
+
return (OptModel.vCharge2ndBlock[p, sc, n, eh] + OptModel.vESSReserveDown[p, sc, n, eh]) / mTEPES.pMaxCharge2ndBlock[p, sc, n, eh] <= OptModel.vCommitmentCons[p, sc, n, eh]
|
|
649
702
|
setattr(OptModel, f'eMaxCharge_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.eh, rule=eMaxCharge, doc='max charge of an ESS [p.u.]'))
|
|
650
703
|
|
|
651
704
|
if pIndLogConsole == 1:
|
|
652
705
|
print('eMaxCharge ... ', len(getattr(OptModel, f'eMaxCharge_{p}_{sc}_{st}')), ' rows')
|
|
653
706
|
|
|
654
|
-
def eMinCharge(OptModel,n,eh):
|
|
655
|
-
if
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
707
|
+
def eMinCharge(OptModel, n, eh):
|
|
708
|
+
# Skip if ESS is not available in the period
|
|
709
|
+
if (p,eh) not in mTEPES.peh:
|
|
710
|
+
return Constraint.Skip
|
|
711
|
+
# Skip if ESS cannot provide reserves while consuming power
|
|
712
|
+
if mTEPES.pIndOperReserveCon[eh]:
|
|
713
|
+
return Constraint.Skip
|
|
714
|
+
# Skip if no reserves are demanded in the area where the ESS is located
|
|
715
|
+
if sum(mTEPES.pOperReserveUp[p, sc, n, ar] for ar in a2e[eh]) == 0:
|
|
661
716
|
return Constraint.Skip
|
|
717
|
+
# Skip if the ESS cannot consume at variable power
|
|
718
|
+
if not mTEPES.pMaxCharge2ndBlock[p, sc, n, eh]:
|
|
719
|
+
return Constraint.Skip
|
|
720
|
+
|
|
721
|
+
return OptModel.vCharge2ndBlock[p, sc, n, eh] - OptModel.vESSReserveUp[p, sc, n, eh] >= 0.0
|
|
722
|
+
|
|
662
723
|
setattr(OptModel, f'eMinCharge_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.eh, rule=eMinCharge, doc='min charge of an ESS [p.u.]'))
|
|
663
724
|
|
|
664
725
|
if pIndLogConsole == 1:
|
|
@@ -671,31 +732,55 @@ def GenerationOperationModelFormulationStorage(OptModel, mTEPES, pIndLogConsole,
|
|
|
671
732
|
# return Constraint.Skip
|
|
672
733
|
# OptModel.eChargeDischarge = Constraint(mTEPES.n, mTEPES.eh, rule=eChargeDischarge, doc='incompatibility between charge and discharge [p.u.]')
|
|
673
734
|
|
|
735
|
+
# Generators with consumption capability cannot be consuming and generating simultaneously
|
|
674
736
|
def eChargeDischarge(OptModel,n,eh):
|
|
675
|
-
if
|
|
676
|
-
|
|
677
|
-
return ((OptModel.vOutput2ndBlock[p,sc,n,eh] + mTEPES.pUpReserveActivation * OptModel.vReserveUp [p,sc,n,eh]) / mTEPES.pMaxPower2ndBlock [p,sc,n,eh] +
|
|
678
|
-
(OptModel.vCharge2ndBlock[p,sc,n,eh] + mTEPES.pDwReserveActivation * OptModel.vESSReserveDown[p,sc,n,eh]) / mTEPES.pMaxCharge2ndBlock[p,sc,n,eh] <= 1.0)
|
|
679
|
-
else:
|
|
680
|
-
return Constraint.Skip
|
|
681
|
-
else:
|
|
737
|
+
# Check if generator is avaiable in the period
|
|
738
|
+
if (p,eh) not in mTEPES.peh:
|
|
682
739
|
return Constraint.Skip
|
|
740
|
+
# Constraint only relevant to generators which can consume and generate power
|
|
741
|
+
if mTEPES.pMaxPower2ndBlock [p,sc,n,eh] == 0 or mTEPES.pMaxCharge2ndBlock[p,sc,n,eh] == 0:
|
|
742
|
+
return Constraint.Skip
|
|
743
|
+
|
|
744
|
+
#Hydro generators can have binary commitment, energy modeled ESS do not have commitment
|
|
745
|
+
|
|
746
|
+
# ESS Generator
|
|
747
|
+
if eh not in mTEPES.h:
|
|
748
|
+
return ((OptModel.vOutput2ndBlock[p,sc,n,eh] + mTEPES.pUpReserveActivation * OptModel.vReserveUp [p,sc,n,eh]) / mTEPES.pMaxPower2ndBlock [p,sc,n,eh] +
|
|
749
|
+
(OptModel.vCharge2ndBlock[p,sc,n,eh] + mTEPES.pDwReserveActivation * OptModel.vESSReserveDown[p,sc,n,eh]) / mTEPES.pMaxCharge2ndBlock[p,sc,n,eh] <= 1.0)
|
|
750
|
+
# Hydro Generator
|
|
751
|
+
else:
|
|
752
|
+
return OptModel.vCommitment[p,sc,n,eh] + OptModel.vCommitmentCons[p,sc,n,eh] <= 1.0
|
|
753
|
+
|
|
683
754
|
setattr(OptModel, f'eChargeDischarge_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.eh, rule=eChargeDischarge, doc='incompatibility between charge and discharge [p.u.]'))
|
|
684
755
|
|
|
685
756
|
if pIndLogConsole == 1:
|
|
686
757
|
print('eChargeDischarge ... ', len(getattr(OptModel, f'eChargeDischarge_{p}_{sc}_{st}')), ' rows')
|
|
687
758
|
|
|
688
759
|
def eESSTotalCharge(OptModel,n,eh):
|
|
689
|
-
if
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
760
|
+
# Check if generator is avaiable in the period
|
|
761
|
+
if (p,eh) not in mTEPES.peh:
|
|
762
|
+
return Constraint.Skip
|
|
763
|
+
# Constraint only applies to generators with charging capabilities
|
|
764
|
+
if mTEPES.pMaxCharge2ndBlock[p,sc,n,eh] == 0:
|
|
765
|
+
return Constraint.Skip
|
|
766
|
+
|
|
767
|
+
# Hydro generators can have binary commitment, energy modeled ESS do not have commitment
|
|
768
|
+
|
|
769
|
+
# ESS Generator
|
|
770
|
+
if eh not in mTEPES.h:
|
|
771
|
+
# Check minimum charge to avoid dividing by 0. Dividing by MinCharge is more numerically stable
|
|
772
|
+
if mTEPES.pMinCharge[p,sc,n,eh] == 0.0:
|
|
773
|
+
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]
|
|
695
774
|
else:
|
|
696
|
-
return
|
|
775
|
+
return OptModel.vESSTotalCharge[p,sc,n,eh] / mTEPES.pMinCharge[p,sc,n,eh] == 1.0 + (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]
|
|
776
|
+
# Hydro generator
|
|
697
777
|
else:
|
|
698
|
-
|
|
778
|
+
# Check minimum charge to avoid dividing by 0. Dividing by MinCharge is more numerically stable
|
|
779
|
+
if mTEPES.pMinCharge[p,sc,n,eh] == 0.0:
|
|
780
|
+
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]
|
|
781
|
+
else:
|
|
782
|
+
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]
|
|
783
|
+
|
|
699
784
|
setattr(OptModel, f'eESSTotalCharge_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.eh, rule=eESSTotalCharge, doc='total charge of an ESS unit [GW]'))
|
|
700
785
|
|
|
701
786
|
if pIndLogConsole == 1:
|
|
@@ -785,57 +870,132 @@ def GenerationOperationModelFormulationReservoir(OptModel, mTEPES, pIndLogConsol
|
|
|
785
870
|
if pIndLogConsole == 1:
|
|
786
871
|
print('eMinVolume2Comm ... ', len(getattr(OptModel, f'eMinVolume2Comm_{p}_{sc}_{st}')), ' rows')
|
|
787
872
|
|
|
788
|
-
def
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
873
|
+
def eTrbReserveUpIfUpstream(OptModel,n,h):
|
|
874
|
+
# There must be enough water upstream of the turbine to produce all the possible offered power (scheduled + reserves)
|
|
875
|
+
|
|
876
|
+
# Skip if generator is not available in the period
|
|
877
|
+
if (p,h) not in mTEPES.ph:
|
|
878
|
+
return Constraint.Skip
|
|
879
|
+
# Skip if turbine cannot provide reserves
|
|
880
|
+
if mTEPES.pIndOperReserveGen[h]:
|
|
881
|
+
return Constraint.Skip
|
|
882
|
+
# Skip if no reserves are demanded in the area where the turbine is located
|
|
883
|
+
if sum(mTEPES.pOperReserveUp[p, sc, n, ar] for ar in a2h[h]) == 0:
|
|
884
|
+
return Constraint.Skip
|
|
885
|
+
# Skip if turbine cannot generate at variable power
|
|
886
|
+
if mTEPES.pMaxPower2ndBlock [p,sc,n,h] == 0:
|
|
887
|
+
return Constraint.Skip
|
|
888
|
+
# Skip if LoalLevel has duration 0
|
|
889
|
+
if mTEPES.pDuration[p, sc, n]() == 0:
|
|
795
890
|
return Constraint.Skip
|
|
796
|
-
|
|
891
|
+
|
|
892
|
+
# Avoid division by 0 if turbine has no minimum power
|
|
893
|
+
if mTEPES.pMinPowerElec[p, sc, n, h] == 0:
|
|
894
|
+
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]()
|
|
895
|
+
else:
|
|
896
|
+
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]
|
|
897
|
+
|
|
898
|
+
setattr(OptModel, f'eTrbReserveUpIfUpstream_{p}_{sc}_{st}', Constraint(mTEPES.nhc, rule=eTrbReserveUpIfUpstream, doc='up operating reserve if energy available [GW]'))
|
|
797
899
|
|
|
798
900
|
if pIndLogConsole == 1:
|
|
799
|
-
print('
|
|
901
|
+
print('eTrbReserveUpIfUpstream ... ', len(getattr(OptModel, f'eTrbReserveUpIfUpstream_{p}_{sc}_{st}')), ' rows')
|
|
800
902
|
|
|
801
|
-
def
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
return Constraint.Skip
|
|
807
|
-
else:
|
|
903
|
+
def eTrbReserveUpIfDownstream(OptModel,n,h):
|
|
904
|
+
#There must be enough spare reservoir capacity downstream of a turbine to fit all the possible water (scheduled + reserves)
|
|
905
|
+
|
|
906
|
+
# Skip if generator is not available in the period
|
|
907
|
+
if (p,h) not in mTEPES.ph:
|
|
808
908
|
return Constraint.Skip
|
|
809
|
-
|
|
909
|
+
# Skip if turbine cannot provide reserves
|
|
910
|
+
if mTEPES.pIndOperReserveGen[h]:
|
|
911
|
+
return Constraint.Skip
|
|
912
|
+
# Skip if no reserves are demanded in the area where the turbine is located
|
|
913
|
+
if sum(mTEPES.pOperReserveUp[p, sc, n, ar] for ar in a2h[h]) == 0:
|
|
914
|
+
return Constraint.Skip
|
|
915
|
+
# Skip if turbine cannot generate at variable power
|
|
916
|
+
if mTEPES.pMaxPower2ndBlock [p,sc,n,h] == 0:
|
|
917
|
+
return Constraint.Skip
|
|
918
|
+
# Skip if LoalLevel has duration 0
|
|
919
|
+
if mTEPES.pDuration[p, sc, n]() == 0:
|
|
920
|
+
return Constraint.Skip
|
|
921
|
+
|
|
922
|
+
# Avoid division by 0 if turbine has no minimum power
|
|
923
|
+
if mTEPES.pMinPowerElec[p, sc, n, h] == 0:
|
|
924
|
+
return OptModel.vOutput2ndBlock[p, sc, n, h] + OptModel.vReserveUp[p, sc, n, h] <= sum(mTEPES.pMaxVolume[p, sc, n, rs] - OptModel.vReservoirVolume[p, sc, n, rs] for rs in mTEPES.rs if (h,rs) in mTEPES.h2r) * mTEPES.pProductionFunctionHydro[h] / mTEPES.pDuration[p, sc, n]()
|
|
925
|
+
else:
|
|
926
|
+
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(mTEPES.pMaxVolume[p, sc, n, rs] - OptModel.vReservoirVolume[p, sc, n, rs] for rs in mTEPES.rs if (h,rs) in mTEPES.h2r) * mTEPES.pProductionFunctionHydro[h] / mTEPES.pDuration[p, sc, n]() / mTEPES.pMinPowerElec[p, sc, n, h]
|
|
927
|
+
|
|
928
|
+
setattr(OptModel, f'eTrbReserveUpIfDownstream_{p}_{sc}_{st}', Constraint(mTEPES.nhc, rule=eTrbReserveUpIfDownstream, doc='up operating reserve if energy available [GW]'))
|
|
810
929
|
|
|
811
930
|
if pIndLogConsole == 1:
|
|
812
|
-
print('
|
|
931
|
+
print('eTrbReserveUpIfDownstream ... ', len(getattr(OptModel, f'eTrbReserveUpIfDownstream_{p}_{sc}_{st}')), ' rows')
|
|
813
932
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
933
|
+
|
|
934
|
+
def ePmpReserveDwIfUpstream(OptModel,n,h):
|
|
935
|
+
# There must be enough reservoir capacity upstream to store all the possible water (scheduled + reserves)
|
|
936
|
+
|
|
937
|
+
# Skip if pump is not available in the period
|
|
938
|
+
if (p, h) not in mTEPES.ph:
|
|
939
|
+
return Constraint.Skip
|
|
940
|
+
# Skip if pump cannot provide reserves
|
|
941
|
+
if mTEPES.pIndOperReserveCon[h]:
|
|
942
|
+
return Constraint.Skip
|
|
943
|
+
# Skip if pump is not connected to any reservoir
|
|
944
|
+
if sum(1 for rs in mTEPES.rs if (h,rs) in mTEPES.p2r) == 0:
|
|
945
|
+
return Constraint.Skip
|
|
946
|
+
# Skip if no reserves are demanded in the area where the turbine is located
|
|
947
|
+
if sum(mTEPES.pOperReserveDw[p, sc, n, ar] for ar in a2h[h]) == 0:
|
|
948
|
+
return Constraint.Skip
|
|
949
|
+
# Skip if pump cannot consume at variable power
|
|
950
|
+
if mTEPES.pMaxCharge2ndBlock[p, sc, n, h] == 0:
|
|
951
|
+
return Constraint.Skip
|
|
952
|
+
# Skip if LoalLevel has duration 0
|
|
953
|
+
if mTEPES.pDuration[p, sc, n]() == 0:
|
|
821
954
|
return Constraint.Skip
|
|
822
|
-
|
|
955
|
+
|
|
956
|
+
# Avoid dividing by 0 if pump has no minimum charge
|
|
957
|
+
if mTEPES.pMinCharge[p,sc,n,h] == 0:
|
|
958
|
+
return (OptModel.vCharge2ndBlock[p,sc,n,h] + OptModel.vESSReserveDown[p,sc,n,h] ) * mTEPES.pEfficiency[h] <= sum(mTEPES.pMaxVolume[p,sc,n,rs] - OptModel.vReservoirVolume[p,sc,n,rs] for rs in mTEPES.rs if (h,rs) in mTEPES.p2r) * mTEPES.pProductionFunctionHydro[h] / mTEPES.pDuration[p,sc,n]()
|
|
959
|
+
else:
|
|
960
|
+
return ((OptModel.vCharge2ndBlock[p,sc,n,h] + OptModel.vESSReserveDown[p,sc,n,h]) / mTEPES.pMinCharge[p,sc,n,h] + OptModel.vCommitmentCons[p,sc,n,h]) * mTEPES.pEfficiency[h] <= sum(mTEPES.pMaxVolume[p,sc,n,rs] - OptModel.vReservoirVolume[p,sc,n,rs] for rs in mTEPES.rs if (h,rs) in mTEPES.p2r) * mTEPES.pProductionFunctionHydro[h] / mTEPES.pDuration[p,sc,n]() / mTEPES.pMinCharge[p,sc,n,h]
|
|
961
|
+
|
|
962
|
+
setattr(OptModel, f'ePmpReserveDwIfUpstream_{p}_{sc}_{st}', Constraint(mTEPES.npc, rule=ePmpReserveDwIfUpstream, doc='down operating reserve if energy available [GW]'))
|
|
823
963
|
|
|
824
964
|
if pIndLogConsole == 1:
|
|
825
|
-
print('
|
|
965
|
+
print('ePmpReserveDwIfUpstream ... ', len(getattr(OptModel, f'ePmpReserveDwIfUpstream_{p}_{sc}_{st}')), ' rows')
|
|
826
966
|
|
|
827
|
-
def
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
967
|
+
def ePmpReserveDwIfDownstream(OptModel, n, h):
|
|
968
|
+
# There must be enough water downstream for the pump to draw from in case it needs to operate at the maximum capacity offered (scheduled + reserves)
|
|
969
|
+
|
|
970
|
+
# Skip if pump is not available in the period
|
|
971
|
+
if (p, h) not in mTEPES.ph:
|
|
972
|
+
return Constraint.Skip
|
|
973
|
+
# Skip if pump cannot provide reserves
|
|
974
|
+
if mTEPES.pIndOperReserveCon[h]:
|
|
975
|
+
return Constraint.Skip
|
|
976
|
+
# Skip if pump is not connected to any reservoir
|
|
977
|
+
if sum(1 for rs in mTEPES.rs if (h, rs) in mTEPES.p2r) == 0:
|
|
978
|
+
return Constraint.Skip
|
|
979
|
+
# Skip if no reserves are demanded in the area where the turbine is located
|
|
980
|
+
if sum(mTEPES.pOperReserveDw[p, sc, n, ar] for ar in a2h[h]) == 0:
|
|
981
|
+
return Constraint.Skip
|
|
982
|
+
# Skip if pump cannot consume at variable power
|
|
983
|
+
if mTEPES.pMaxCharge2ndBlock[p, sc, n, h] == 0:
|
|
834
984
|
return Constraint.Skip
|
|
835
|
-
|
|
985
|
+
# Skip if LoalLevel has duration 0
|
|
986
|
+
if mTEPES.pDuration[p, sc, n]() == 0:
|
|
987
|
+
return Constraint.Skip
|
|
988
|
+
|
|
989
|
+
# Avoid dividing by 0 if pump has no minimum charge
|
|
990
|
+
if mTEPES.pMinCharge[p, sc, n, h] == 0:
|
|
991
|
+
return (OptModel.vCharge2ndBlock[p, sc, n, h] + OptModel.vESSReserveDown[p, sc, n, h] ) * mTEPES.pEfficiency[h] <= sum(OptModel.vReservoirVolume[p, sc, n, rs] - mTEPES.pMinVolume[p, sc, n, rs] for rs in mTEPES.rs if (rs, h) in mTEPES.r2p) * mTEPES.pProductionFunctionHydro[h] / mTEPES.pDuration[p, sc, n]()
|
|
992
|
+
else:
|
|
993
|
+
return ((OptModel.vCharge2ndBlock[p, sc, n, h] + OptModel.vESSReserveDown[p, sc, n, h]) / mTEPES.pMinCharge[p, sc, n, h] + OptModel.vCommitmentCons[p, sc, n, h]) * mTEPES.pEfficiency[h] <= sum(OptModel.vReservoirVolume[p, sc, n, rs] - mTEPES.pMinVolume[p, sc, n, rs] for rs in mTEPES.rs if (rs, h) in mTEPES.r2p) * mTEPES.pProductionFunctionHydro[h] / mTEPES.pDuration[p, sc, n]() / mTEPES.pMinCharge[p, sc, n, h]
|
|
994
|
+
|
|
995
|
+
setattr(OptModel, f'ePmpReserveDwIfDownstream_{p}_{sc}_{st}', Constraint(mTEPES.npc, rule=ePmpReserveDwIfDownstream, doc='down operating reserve if energy available [GW]'))
|
|
836
996
|
|
|
837
997
|
if pIndLogConsole == 1:
|
|
838
|
-
print('
|
|
998
|
+
print('ePmpReserveDwIfDownstream ... ', len(getattr(OptModel, f'ePmpReserveDwIfDownstream_{p}_{sc}_{st}')), ' rows')
|
|
839
999
|
|
|
840
1000
|
def eHydroInventory(OptModel,n,rs):
|
|
841
1001
|
if (p,rs) in mTEPES.prs and sum(1 for h in mTEPES.h if (rs,h) in mTEPES.r2h or (h,rs) in mTEPES.h2r or (rs,h) in mTEPES.r2p or (h,rs) in mTEPES.p2r):
|
|
@@ -967,50 +1127,114 @@ def GenerationOperationModelFormulationCommitment(OptModel, mTEPES, pIndLogConso
|
|
|
967
1127
|
if pIndLogConsole == 1:
|
|
968
1128
|
print('eStableStates ... ', len(getattr(OptModel, f'eStableStates_{p}_{sc}_{st}')), ' rows')
|
|
969
1129
|
|
|
970
|
-
def
|
|
971
|
-
if
|
|
972
|
-
|
|
973
|
-
return OptModel.vCommitment[p,sc,n,nr] <= OptModel.vMaxCommitment[p,sc,nr]
|
|
974
|
-
else:
|
|
975
|
-
return Constraint.Skip
|
|
976
|
-
else:
|
|
1130
|
+
def eMaxCommitmentYearly(OptModel,n,group,nr):
|
|
1131
|
+
# Skip if generator not available on period
|
|
1132
|
+
if (p,nr) not in mTEPES.pnr:
|
|
977
1133
|
return Constraint.Skip
|
|
978
|
-
|
|
1134
|
+
# Skip if the generator is not part of the exclusive group
|
|
1135
|
+
if nr not in mTEPES.GeneratorsInYearlyGroup[group]:
|
|
1136
|
+
return Constraint.Skip
|
|
1137
|
+
# Skip if there are one or less generators in the group
|
|
1138
|
+
if len(mTEPES.GeneratorsInYearlyGroup[group] & {nr for (p,nr) in mTEPES.pnr}) <= 1:
|
|
1139
|
+
return Constraint.Skip
|
|
1140
|
+
|
|
1141
|
+
return OptModel.vCommitment[p,sc,n,nr] <= OptModel.vMaxCommitmentYearly[p,sc,nr,group]
|
|
1142
|
+
|
|
1143
|
+
setattr(OptModel, f'eMaxCommitmentYearly_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.ExclusiveGroupsYearly, mTEPES.nr, rule=eMaxCommitmentYearly, doc='maximum of all the commitments [p.u.]'))
|
|
979
1144
|
|
|
980
1145
|
if pIndLogConsole == 1:
|
|
981
|
-
print('
|
|
1146
|
+
print('eMaxCommitmentYearly ... ', len(getattr(OptModel, f'eMaxCommitmentYearly_{p}_{sc}_{st}')), ' rows')
|
|
982
1147
|
|
|
983
|
-
def
|
|
984
|
-
if
|
|
985
|
-
|
|
986
|
-
return OptModel.vTotalOutput[p,sc,n,g]/mTEPES.pMaxPowerElec[p,sc,n,g] <= OptModel.vMaxCommitment[p,sc,g]
|
|
987
|
-
else:
|
|
988
|
-
return Constraint.Skip
|
|
989
|
-
else:
|
|
1148
|
+
def eMaxCommitGenYearly(OptModel,n,group,nr):
|
|
1149
|
+
# Skip if generator not available on period
|
|
1150
|
+
if (p,nr) not in mTEPES.pnr:
|
|
990
1151
|
return Constraint.Skip
|
|
991
|
-
|
|
1152
|
+
# Skip if the generator is not part of the exclusive group
|
|
1153
|
+
if nr not in mTEPES.GeneratorsInYearlyGroup[group]:
|
|
1154
|
+
return Constraint.Skip
|
|
1155
|
+
# Avoid division by 0. If Maximum power is 0 this equation is not needed anyways
|
|
1156
|
+
if mTEPES.pMaxPowerElec[p, sc, n, nr] == 0:
|
|
1157
|
+
return Constraint.Skip
|
|
1158
|
+
# Skip if there are one or less generators in the group
|
|
1159
|
+
if len(mTEPES.GeneratorsInYearlyGroup[group] & {nr for (p, nr) in mTEPES.pnr}) <= 1:
|
|
1160
|
+
return Constraint.Skip
|
|
1161
|
+
|
|
1162
|
+
return OptModel.vTotalOutput[p,sc,n,nr]/mTEPES.pMaxPowerElec[p,sc,n,nr] <= OptModel.vMaxCommitmentYearly[p,sc,nr,group]
|
|
1163
|
+
|
|
1164
|
+
setattr(OptModel, f'eMaxCommitGenYearly_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.ExclusiveGroupsYearly,mTEPES.nr, rule=eMaxCommitGenYearly, doc='maximum of all the capacity factors'))
|
|
992
1165
|
|
|
993
1166
|
if pIndLogConsole == 1:
|
|
994
|
-
print('
|
|
1167
|
+
print('eMaxCommitGenYearly ... ', len(getattr(OptModel, f'eMaxCommitGenYearly_{p}_{sc}_{st}')), ' rows')
|
|
995
1168
|
|
|
996
|
-
def
|
|
997
|
-
if
|
|
998
|
-
|
|
999
|
-
return OptModel.vMaxCommitment[p,sc,g] + sum(OptModel.vMaxCommitment[p,sc,gg] for gg in mTEPES.g if (gg,g) in mTEPES.g2g) <= 1
|
|
1000
|
-
else:
|
|
1001
|
-
return Constraint.Skip
|
|
1002
|
-
else:
|
|
1169
|
+
def eExclusiveGensYearly(OptModel,group):
|
|
1170
|
+
# Skip if there are one or less generators in the group
|
|
1171
|
+
if len(mTEPES.GeneratorsInYearlyGroup[group] & {nr for (p,nr) in mTEPES.pnr}) <= 1:
|
|
1003
1172
|
return Constraint.Skip
|
|
1004
|
-
|
|
1173
|
+
return sum(OptModel.vMaxCommitmentYearly[p, sc, nr, group] + (OptModel.vCommitmentCons[p, sc, nr] if nr in mTEPES.h else 0) for nr in mTEPES.GeneratorsInYearlyGroup[group] if (p, nr) in mTEPES.pnr ) <= 1
|
|
1174
|
+
setattr(OptModel, f'eExclusiveGensYearly_{p}_{sc}_{st}', Constraint(mTEPES.ExclusiveGroupsYearly, rule=eExclusiveGensYearly, doc='mutually exclusive generators'))
|
|
1005
1175
|
|
|
1006
1176
|
if pIndLogConsole == 1:
|
|
1007
|
-
print('
|
|
1177
|
+
print('eExclusiveGensYearly ... ', len(getattr(OptModel, f'eExclusiveGensYearly_{p}_{sc}_{st}')), ' rows')
|
|
1008
1178
|
|
|
1009
1179
|
GeneratingTime = time.time() - StartTime
|
|
1010
1180
|
if pIndLogConsole == 1:
|
|
1011
1181
|
print('Generating generation commitment ... ', round(GeneratingTime), 's')
|
|
1012
1182
|
|
|
1183
|
+
def eMaxCommitmentHourly(OptModel,n,group,nr):
|
|
1184
|
+
# Skip if generator not available on period
|
|
1185
|
+
if (p,nr) not in mTEPES.pnr:
|
|
1186
|
+
return Constraint.Skip
|
|
1187
|
+
# Skip if the generator is not part of the exclusive group
|
|
1188
|
+
if nr not in mTEPES.GeneratorsInHourlyGroup[group]:
|
|
1189
|
+
return Constraint.Skip
|
|
1190
|
+
# Skip if there are one or less generators in the group
|
|
1191
|
+
if len(mTEPES.GeneratorsInHourlyGroup[group] & {nr for (p,nr) in mTEPES.pnr}) <= 1:
|
|
1192
|
+
return Constraint.Skip
|
|
1193
|
+
|
|
1194
|
+
return OptModel.vCommitment[p,sc,n,nr] <= OptModel.vMaxCommitmentHourly[p,sc,n,nr,group]
|
|
1195
|
+
|
|
1196
|
+
setattr(OptModel, f'eMaxCommitmentHourly_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.ExclusiveGroupsHourly, mTEPES.nr, rule=eMaxCommitmentHourly, doc='maximum of all the commitments [p.u.]'))
|
|
1013
1197
|
|
|
1198
|
+
if pIndLogConsole == 1:
|
|
1199
|
+
print('eMaxCommitmentHourly ... ', len(getattr(OptModel, f'eMaxCommitmentHourly_{p}_{sc}_{st}')), ' rows')
|
|
1200
|
+
|
|
1201
|
+
def eMaxCommitGenHourly(OptModel,n,group,nr):
|
|
1202
|
+
# Skip if generator not available on period
|
|
1203
|
+
if (p,nr) not in mTEPES.pnr:
|
|
1204
|
+
return Constraint.Skip
|
|
1205
|
+
# Skip if the generator is not part of the exclusive group
|
|
1206
|
+
if nr not in mTEPES.GeneratorsInHourlyGroup[group]:
|
|
1207
|
+
return Constraint.Skip
|
|
1208
|
+
# Avoid division by 0. If Maximum power is 0 this equation is not needed anyways
|
|
1209
|
+
if mTEPES.pMaxPowerElec[p,sc,n,nr] == 0:
|
|
1210
|
+
return Constraint.Skip
|
|
1211
|
+
# Skip if there are one or less generators in the group
|
|
1212
|
+
if len(mTEPES.GeneratorsInHourlyGroup[group] & {nr for (p,nr) in mTEPES.pnr}) <= 1:
|
|
1213
|
+
return Constraint.Skip
|
|
1214
|
+
|
|
1215
|
+
return OptModel.vTotalOutput[p,sc,n,nr]/mTEPES.pMaxPowerElec[p,sc,n,nr] <= OptModel.vMaxCommitmentHourly[p,sc,n,nr,group]
|
|
1216
|
+
|
|
1217
|
+
setattr(OptModel, f'eMaxCommitGenHourly_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.ExclusiveGroupsHourly,mTEPES.nr, rule=eMaxCommitGenHourly, doc='maximum of all the capacity factors'))
|
|
1218
|
+
|
|
1219
|
+
if pIndLogConsole == 1:
|
|
1220
|
+
print('eMaxCommitGenHourly ... ', len(getattr(OptModel, f'eMaxCommitGenHourly_{p}_{sc}_{st}')), ' rows')
|
|
1221
|
+
|
|
1222
|
+
def eExclusiveGensHourly(OptModel,n,group):
|
|
1223
|
+
# Skip if there are one or less generators in the group
|
|
1224
|
+
# This is written in a different way to the rest of the code to avoid variable shadowing due to comprehension
|
|
1225
|
+
if len(mTEPES.GeneratorsInHourlyGroup[group] & {gen for (period, gen) in mTEPES.pnr if period == p}) <= 1:
|
|
1226
|
+
return Constraint.Skip
|
|
1227
|
+
|
|
1228
|
+
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
|
|
1229
|
+
|
|
1230
|
+
setattr(OptModel, f'eExclusiveGensHourly_{p}_{sc}_{st}', Constraint(mTEPES.n,mTEPES.ExclusiveGroupsHourly, rule=eExclusiveGensHourly, doc='mutually exclusive generators'))
|
|
1231
|
+
|
|
1232
|
+
if pIndLogConsole == 1:
|
|
1233
|
+
print('eExclusiveGensHourly ... ', len(getattr(OptModel, f'eExclusiveGensHourly_{p}_{sc}_{st}')), ' rows')
|
|
1234
|
+
|
|
1235
|
+
GeneratingTime = time.time() - StartTime
|
|
1236
|
+
if pIndLogConsole == 1:
|
|
1237
|
+
print('Generating generation commitment ... ', round(GeneratingTime), 's')
|
|
1014
1238
|
def GenerationOperationModelFormulationRampMinTime(OptModel, mTEPES, pIndLogConsole, p, sc, st):
|
|
1015
1239
|
print('Ramp and min up/down time constraints ****')
|
|
1016
1240
|
|
|
@@ -1276,7 +1500,7 @@ def NetworkOperationModelFormulation(OptModel, mTEPES, pIndLogConsole, p, sc, st
|
|
|
1276
1500
|
|
|
1277
1501
|
def eNetCapacity1(OptModel,n,ni,nf,cc):
|
|
1278
1502
|
if mTEPES.pIndBinSingleNode() == 0 and ((ni,nf,cc) in mTEPES.lc or mTEPES.pIndBinLineSwitch[ni,nf,cc] == 1):
|
|
1279
|
-
return OptModel.vFlowElec[p,sc,n,ni,nf,cc] / mTEPES.
|
|
1503
|
+
return OptModel.vFlowElec[p,sc,n,ni,nf,cc] / mTEPES.pMaxNTCMax[p,sc,n,ni,nf,cc] >= - OptModel.vLineCommit[p,sc,n,ni,nf,cc]
|
|
1280
1504
|
else:
|
|
1281
1505
|
return Constraint.Skip
|
|
1282
1506
|
setattr(OptModel, f'eNetCapacity1_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.la, rule=eNetCapacity1, doc='maximum flow by existing network capacity [p.u.]'))
|
|
@@ -1286,7 +1510,7 @@ def NetworkOperationModelFormulation(OptModel, mTEPES, pIndLogConsole, p, sc, st
|
|
|
1286
1510
|
|
|
1287
1511
|
def eNetCapacity2(OptModel,n,ni,nf,cc):
|
|
1288
1512
|
if mTEPES.pIndBinSingleNode() == 0 and ((ni,nf,cc) in mTEPES.lc or mTEPES.pIndBinLineSwitch[ni,nf,cc] == 1):
|
|
1289
|
-
return OptModel.vFlowElec[p,sc,n,ni,nf,cc] / mTEPES.
|
|
1513
|
+
return OptModel.vFlowElec[p,sc,n,ni,nf,cc] / mTEPES.pMaxNTCMax[p,sc,n,ni,nf,cc] <= OptModel.vLineCommit[p,sc,n,ni,nf,cc]
|
|
1290
1514
|
else:
|
|
1291
1515
|
return Constraint.Skip
|
|
1292
1516
|
setattr(OptModel, f'eNetCapacity2_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.la, rule=eNetCapacity2, doc='maximum flow by existing network capacity [p.u.]'))
|
|
@@ -1295,7 +1519,7 @@ def NetworkOperationModelFormulation(OptModel, mTEPES, pIndLogConsole, p, sc, st
|
|
|
1295
1519
|
print('eNetCapacity2 ... ', len(getattr(OptModel, f'eNetCapacity2_{p}_{sc}_{st}')), ' rows')
|
|
1296
1520
|
|
|
1297
1521
|
def eKirchhoff2ndLaw1(OptModel,n,ni,nf,cc):
|
|
1298
|
-
if mTEPES.pIndBinSingleNode() == 0 and mTEPES.pElecNetPeriodIni[ni,nf,cc] <= p and mTEPES.pElecNetPeriodFin[ni,nf,cc] >= p and mTEPES.pLineX[ni,nf,cc]:
|
|
1522
|
+
if mTEPES.pIndBinSingleNode() == 0 and mTEPES.pElecNetPeriodIni[ni,nf,cc] <= p and mTEPES.pElecNetPeriodFin[ni,nf,cc] >= p and mTEPES.pLineX[ni,nf,cc] and mTEPES.pIndPTDF == 0:
|
|
1299
1523
|
if (ni,nf,cc) in mTEPES.lca:
|
|
1300
1524
|
return OptModel.vFlowElec[p,sc,n,ni,nf,cc] / mTEPES.pBigMFlowBck[ni,nf,cc]() - (OptModel.vTheta[p,sc,n,ni] - OptModel.vTheta[p,sc,n,nf]) / mTEPES.pLineX[ni,nf,cc] / mTEPES.pBigMFlowBck[ni,nf,cc]() * mTEPES.pSBase >= - 1 + OptModel.vLineCommit[p,sc,n,ni,nf,cc]
|
|
1301
1525
|
else:
|
|
@@ -1308,7 +1532,7 @@ def NetworkOperationModelFormulation(OptModel, mTEPES, pIndLogConsole, p, sc, st
|
|
|
1308
1532
|
print('eKirchhoff2ndLaw1 ... ', len(getattr(OptModel, f'eKirchhoff2ndLaw1_{p}_{sc}_{st}')), ' rows')
|
|
1309
1533
|
|
|
1310
1534
|
def eKirchhoff2ndLaw2(OptModel,n,ni,nf,cc):
|
|
1311
|
-
if mTEPES.pIndBinSingleNode() == 0 and mTEPES.pElecNetPeriodIni[ni,nf,cc] <= p and mTEPES.pElecNetPeriodFin[ni,nf,cc] >= p and mTEPES.pLineX[ni,nf,cc]:
|
|
1535
|
+
if mTEPES.pIndBinSingleNode() == 0 and mTEPES.pElecNetPeriodIni[ni,nf,cc] <= p and mTEPES.pElecNetPeriodFin[ni,nf,cc] >= p and mTEPES.pLineX[ni,nf,cc] and mTEPES.pIndPTDF == 0:
|
|
1312
1536
|
return OptModel.vFlowElec[p,sc,n,ni,nf,cc] / mTEPES.pBigMFlowFrw[ni,nf,cc]() - (OptModel.vTheta[p,sc,n,ni] - OptModel.vTheta[p,sc,n,nf]) / mTEPES.pLineX[ni,nf,cc] / mTEPES.pBigMFlowFrw[ni,nf,cc]() * mTEPES.pSBase <= 1 - OptModel.vLineCommit[p,sc,n,ni,nf,cc]
|
|
1313
1537
|
else:
|
|
1314
1538
|
return Constraint.Skip
|
|
@@ -1318,7 +1542,7 @@ def NetworkOperationModelFormulation(OptModel, mTEPES, pIndLogConsole, p, sc, st
|
|
|
1318
1542
|
print('eKirchhoff2ndLaw2 ... ', len(getattr(OptModel, f'eKirchhoff2ndLaw2_{p}_{sc}_{st}')), ' rows')
|
|
1319
1543
|
|
|
1320
1544
|
def eLineLosses1(OptModel,n,ni,nf,cc):
|
|
1321
|
-
if mTEPES.pIndBinSingleNode() == 0 and mTEPES.pIndBinNetLosses() and
|
|
1545
|
+
if mTEPES.pIndBinSingleNode() == 0 and mTEPES.pIndBinNetLosses() and mTEPES.ll and mTEPES.pIndPTDF == 0:
|
|
1322
1546
|
return OptModel.vLineLosses[p,sc,n,ni,nf,cc] >= - 0.5 * mTEPES.pLineLossFactor[ni,nf,cc] * OptModel.vFlowElec[p,sc,n,ni,nf,cc]
|
|
1323
1547
|
else:
|
|
1324
1548
|
return Constraint.Skip
|
|
@@ -1328,7 +1552,7 @@ def NetworkOperationModelFormulation(OptModel, mTEPES, pIndLogConsole, p, sc, st
|
|
|
1328
1552
|
print('eLineLosses1 ... ', len(getattr(OptModel, f'eLineLosses1_{p}_{sc}_{st}')), ' rows')
|
|
1329
1553
|
|
|
1330
1554
|
def eLineLosses2(OptModel,n,ni,nf,cc):
|
|
1331
|
-
if mTEPES.pIndBinSingleNode() == 0 and mTEPES.pIndBinNetLosses() and
|
|
1555
|
+
if mTEPES.pIndBinSingleNode() == 0 and mTEPES.pIndBinNetLosses() and mTEPES.ll and mTEPES.pIndPTDF == 0:
|
|
1332
1556
|
return OptModel.vLineLosses[p,sc,n,ni,nf,cc] >= 0.5 * mTEPES.pLineLossFactor[ni,nf,cc] * OptModel.vFlowElec[p,sc,n,ni,nf,cc]
|
|
1333
1557
|
else:
|
|
1334
1558
|
return Constraint.Skip
|
|
@@ -1337,11 +1561,70 @@ def NetworkOperationModelFormulation(OptModel, mTEPES, pIndLogConsole, p, sc, st
|
|
|
1337
1561
|
if pIndLogConsole == 1:
|
|
1338
1562
|
print('eLineLosses2 ... ', len(getattr(OptModel, f'eLineLosses2_{p}_{sc}_{st}')), ' rows')
|
|
1339
1563
|
|
|
1564
|
+
# nodes to generators (g2n)
|
|
1565
|
+
g2n = defaultdict(list)
|
|
1566
|
+
for nd,g in mTEPES.n2g:
|
|
1567
|
+
g2n[nd].append(g)
|
|
1568
|
+
e2n = defaultdict(list)
|
|
1569
|
+
for nd,eh in mTEPES.nd*mTEPES.eh:
|
|
1570
|
+
if (nd,eh) in mTEPES.n2g:
|
|
1571
|
+
e2n[nd].append(eh)
|
|
1572
|
+
|
|
1573
|
+
def eNetPosition(OptModel,n,nd):
|
|
1574
|
+
if mTEPES.pIndBinSingleNode() == 0 and mTEPES.pIndPTDF == 1:
|
|
1575
|
+
"""Net position NP_n = Σ P_g in node n − demand"""
|
|
1576
|
+
return (OptModel.vNetPosition[p,sc,n,nd] == sum(OptModel.vTotalOutput[p,sc,n,g] for g in g2n[nd] if (p,g) in mTEPES.pg) - sum(OptModel.vESSTotalCharge[p,sc,n,eh] for eh in e2n[nd] if (p,eh) in mTEPES.peh) + OptModel.vENS[p,sc,n,nd] - mTEPES.pDemandElec[p,sc,n,nd])
|
|
1577
|
+
else:
|
|
1578
|
+
return Constraint.Skip
|
|
1579
|
+
setattr(OptModel, f'eNetPosition_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.nd, rule=eNetPosition, doc='net position [GW]'))
|
|
1580
|
+
|
|
1581
|
+
def eFlowBasedCalcu1(OptModel,n,ni,nf,cc):
|
|
1582
|
+
if mTEPES.pIndBinSingleNode() == 0 and mTEPES.pElecNetPeriodIni[ni,nf,cc] <= p and mTEPES.pElecNetPeriodFin[ni,nf,cc] >= p and mTEPES.pIndPTDF == 1 and mTEPES.pIndBinLinePTDF[ni,nf,cc] == 1:
|
|
1583
|
+
if (ni,nf,cc) in mTEPES.lca:
|
|
1584
|
+
return OptModel.vFlowElec[p,sc,n,ni,nf,cc] - sum(mTEPES.pPTDF[p,sc,n,ni,nf,cc,nd] * OptModel.vNetPosition[p,sc,n,nd] for nd in mTEPES.nd if (p,sc,n,ni,nf,cc,nd) in mTEPES.psnland) >= - 1 + OptModel.vLineCommit[p,sc,n,ni,nf,cc]
|
|
1585
|
+
else:
|
|
1586
|
+
return OptModel.vFlowElec[p,sc,n,ni,nf,cc] - sum(mTEPES.pPTDF[p,sc,n,ni,nf,cc,nd] * OptModel.vNetPosition[p,sc,n,nd] for nd in mTEPES.nd if (p,sc,n,ni,nf,cc,nd) in mTEPES.psnland) == 0
|
|
1587
|
+
else:
|
|
1588
|
+
return Constraint.Skip
|
|
1589
|
+
setattr(OptModel, f'eFlowBasedCalcu1_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.la, rule=eFlowBasedCalcu1, doc='flow based calculation [p.u.]'))
|
|
1590
|
+
|
|
1591
|
+
if pIndLogConsole == 1:
|
|
1592
|
+
print('eFlowBasedCalcu1 ... ', len(getattr(OptModel, f'eFlowBasedCalcu1_{p}_{sc}_{st}')), ' rows')
|
|
1593
|
+
|
|
1594
|
+
def eFlowBasedCalcu2(OptModel,n,ni,nf,cc):
|
|
1595
|
+
if mTEPES.pIndBinSingleNode() == 0 and mTEPES.pElecNetPeriodIni[ni,nf,cc] <= p and mTEPES.pElecNetPeriodFin[ni,nf,cc] >= p and mTEPES.pIndPTDF == 1 and mTEPES.pIndBinLinePTDF[ni,nf,cc] == 1:
|
|
1596
|
+
return OptModel.vFlowElec[p,sc,n,ni,nf,cc] - sum(mTEPES.pPTDF[p,sc,n,ni,nf,cc,nd] * OptModel.vNetPosition[p,sc,n,nd] for nd in mTEPES.nd if (p,sc,n,ni,nf,cc,nd) in mTEPES.psnland) <= 1 - OptModel.vLineCommit[p,sc,n,ni,nf,cc]
|
|
1597
|
+
else:
|
|
1598
|
+
return Constraint.Skip
|
|
1599
|
+
setattr(OptModel, f'eFlowBasedCalcu2_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.lca, rule=eFlowBasedCalcu2, doc='flow based calculation [p.u.]'))
|
|
1600
|
+
|
|
1601
|
+
if pIndLogConsole == 1:
|
|
1602
|
+
print('eFlowBasedCalcu2 ... ', len(getattr(OptModel, f'eFlowBasedCalcu2_{p}_{sc}_{st}')), ' rows')
|
|
1603
|
+
|
|
1604
|
+
# def eSecurityMargingTTCFrw(OptModel,n,ni,nf,cc):
|
|
1605
|
+
# if mTEPES.pIndBinSingleNode() == 0 and mTEPES.pElecNetPeriodIni[ni,nf,cc] <= p and mTEPES.pElecNetPeriodFin[ni,nf,cc] >= p and mTEPES.pIndBinLinePTDF[ni,nf,cc] == 1 and mTEPES.pIndPTDF == 1:
|
|
1606
|
+
# return OptModel.vFlowElec[p,sc,n,ni,nf,cc] <= mTEPES.pVariableTTCFrw[p,sc,n,ni,nf,cc]
|
|
1607
|
+
# else:
|
|
1608
|
+
# return Constraint.Skip
|
|
1609
|
+
# setattr(OptModel, f'eSecurityMargingTTCFrw_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.lca, rule=eSecurityMargingTTCFrw, doc='security margin TTC for flow based calculation [p.u.]'))
|
|
1610
|
+
#
|
|
1611
|
+
# if pIndLogConsole == 1:
|
|
1612
|
+
# print('eSecurityMargingTTCFrw... ', len(getattr(OptModel, f'eSecurityMargingTTCFrw_{p}_{sc}_{st}')), ' rows')
|
|
1613
|
+
#
|
|
1614
|
+
# def eSecurityMargingTTCBck(OptModel,n,ni,nf,cc):
|
|
1615
|
+
# if mTEPES.pIndBinSingleNode() == 0 and mTEPES.pElecNetPeriodIni[ni,nf,cc] <= p and mTEPES.pElecNetPeriodFin[ni,nf,cc] >= p and mTEPES.pIndBinLinePTDF[ni,nf,cc] == 1 and mTEPES.pIndPTDF == 1:
|
|
1616
|
+
# return OptModel.vFlowElec[p,sc,n,ni,nf,cc] >= - mTEPES.pVariableTTCBck[p,sc,n,ni,nf,cc]
|
|
1617
|
+
# else:
|
|
1618
|
+
# return Constraint.Skip
|
|
1619
|
+
# setattr(OptModel, f'eSecurityMargingTTCBck_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.lca, rule=eSecurityMargingTTCBck, doc='security margin TTC for flow based calculation [p.u.]'))
|
|
1620
|
+
#
|
|
1621
|
+
# if pIndLogConsole == 1:
|
|
1622
|
+
# print('eSecurityMargingTTCBck... ', len(getattr(OptModel, f'eSecurityMargingTTCBck_{p}_{sc}_{st}')), ' rows')
|
|
1623
|
+
|
|
1340
1624
|
GeneratingTime = time.time() - StartTime
|
|
1341
1625
|
if pIndLogConsole == 1:
|
|
1342
1626
|
print('Generating network constraints ... ', round(GeneratingTime), 's')
|
|
1343
1627
|
|
|
1344
|
-
|
|
1345
1628
|
def NetworkCycles(mTEPES, pIndLogConsole):
|
|
1346
1629
|
print('Network Cycles Detection ****')
|
|
1347
1630
|
|
|
@@ -1424,25 +1707,31 @@ def CycleConstraints(OptModel, mTEPES, pIndLogConsole, p, sc, st):
|
|
|
1424
1707
|
|
|
1425
1708
|
#%% cycle Kirchhoff's second law with some candidate lines
|
|
1426
1709
|
# this equation is formulated for every AC candidate line included in the cycle
|
|
1427
|
-
def eCycleKirchhoff2ndLawCnd1(OptModel,sc,
|
|
1428
|
-
|
|
1429
|
-
|
|
1710
|
+
def eCycleKirchhoff2ndLawCnd1(OptModel,p,sc,n,cyc,nii,nff,cc):
|
|
1711
|
+
if mTEPES.pIndPTDF == 0:
|
|
1712
|
+
return (sum(OptModel.vFlowElec[p,sc,n,ni,nf,cc] * mTEPES.pLineX[ni,nf,cc] / mTEPES.pSBase for ni,nf in list(zip(mTEPES.ncd[cyc], mTEPES.ncd[cyc][1:] + mTEPES.ncd[cyc][:1])) for cc in mTEPES.cc if (ni,nf,cc) in mTEPES.uctc) -
|
|
1713
|
+
sum(OptModel.vFlowElec[p,sc,n,ni,nf,cc] * mTEPES.pLineX[ni,nf,cc] / mTEPES.pSBase for nf,ni in list(zip(mTEPES.ncd[cyc], mTEPES.ncd[cyc][1:] + mTEPES.ncd[cyc][:1])) for cc in mTEPES.cc if (ni,nf,cc) in mTEPES.uctc) ) / mTEPES.pBigMTheta[cyc,nii,nff,cc] <= 1 - OptModel.vLineCommit[p,sc,n,nii,nff,cc]
|
|
1714
|
+
else:
|
|
1715
|
+
return Constraint.Skip
|
|
1430
1716
|
setattr(OptModel, f'eCycleKirchhoff2ndLawCnd1_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.lcac, rule=eCycleKirchhoff2ndLawCnd1, doc='cycle flow for with some AC candidate lines [rad]'))
|
|
1431
1717
|
|
|
1432
1718
|
if pIndLogConsole == 1:
|
|
1433
1719
|
print('eCycleKirchhoff2ndLC1 ... ', len(getattr(OptModel, f'eCycleKirchhoff2ndLawCnd1_{p}_{sc}_{st}')), ' rows')
|
|
1434
1720
|
|
|
1435
|
-
def eCycleKirchhoff2ndLawCnd2(OptModel,sc,
|
|
1436
|
-
|
|
1437
|
-
|
|
1721
|
+
def eCycleKirchhoff2ndLawCnd2(OptModel,p,sc,n,cyc,nii,nff,cc):
|
|
1722
|
+
if mTEPES.pIndPTDF == 0:
|
|
1723
|
+
return (sum(OptModel.vFlowElec[p,sc,n,ni,nf,cc] * mTEPES.pLineX[ni,nf,cc] / mTEPES.pSBase for ni,nf in list(zip(mTEPES.ncd[cyc], mTEPES.ncd[cyc][1:] + mTEPES.ncd[cyc][:1])) for cc in mTEPES.cc if (ni,nf,cc) in mTEPES.uctc) -
|
|
1724
|
+
sum(OptModel.vFlowElec[p,sc,n,ni,nf,cc] * mTEPES.pLineX[ni,nf,cc] / mTEPES.pSBase for nf,ni in list(zip(mTEPES.ncd[cyc], mTEPES.ncd[cyc][1:] + mTEPES.ncd[cyc][:1])) for cc in mTEPES.cc if (ni,nf,cc) in mTEPES.uctc) ) / mTEPES.pBigMTheta[cyc,nii,nff,cc] >= - 1 + OptModel.vLineCommit[p,sc,n,nii,nff,cc]
|
|
1725
|
+
else:
|
|
1726
|
+
return Constraint.Skip
|
|
1438
1727
|
setattr(OptModel, f'eCycleKirchhoff2ndLawCnd2_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.lcac, rule=eCycleKirchhoff2ndLawCnd2, doc='cycle flow for with some AC candidate lines [rad]'))
|
|
1439
1728
|
|
|
1440
1729
|
if pIndLogConsole == 1:
|
|
1441
1730
|
print('eCycleKirchhoff2ndLC2 ... ', len(getattr(OptModel, f'eCycleKirchhoff2ndLawCnd2_{p}_{sc}_{st}')), ' rows')
|
|
1442
1731
|
|
|
1443
|
-
def eFlowParallelCandidate1(OptModel,sc,
|
|
1444
|
-
if cc < c2 and (ni,nf,cc) in mTEPES.lea and (ni,nf,c2) in mTEPES.lca:
|
|
1445
|
-
return (OptModel.vFlowElec[sc,
|
|
1732
|
+
def eFlowParallelCandidate1(OptModel,p,sc,n,ni,nf,cc,c2):
|
|
1733
|
+
if (cc < c2 and (ni,nf,cc) in mTEPES.lea and (ni,nf,c2) in mTEPES.lca) and mTEPES.pIndPTDF == 0:
|
|
1734
|
+
return (OptModel.vFlowElec[p,sc,n,ni,nf,cc] - OptModel.vFlowElec[p,sc,n,ni,nf,c2] * mTEPES.pLineX[ni,nf,c2] / mTEPES.pLineX[ni,nf,cc]) / max(mTEPES.pMaxNTCBck[p,sc,n,ni,nf,cc],mTEPES.pMaxNTCFrw[p,sc,n,ni,nf,cc]) <= 1 - OptModel.vLineCommit[p,sc,n,ni,nf,c2]
|
|
1446
1735
|
else:
|
|
1447
1736
|
return Constraint.Skip
|
|
1448
1737
|
setattr(OptModel, f'eFlowParallelCandidate1_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.pct, mTEPES.cc, mTEPES.c2, rule=eFlowParallelCandidate1, doc='unitary flow for each AC candidate parallel circuit [p.u.]'))
|
|
@@ -1450,9 +1739,9 @@ def CycleConstraints(OptModel, mTEPES, pIndLogConsole, p, sc, st):
|
|
|
1450
1739
|
if pIndLogConsole == 1:
|
|
1451
1740
|
print('eFlowParallelCnddate1 ... ', len(getattr(OptModel, f'eFlowParallelCandidate1_{p}_{sc}_{st}')), ' rows')
|
|
1452
1741
|
|
|
1453
|
-
def eFlowParallelCandidate2(OptModel,sc,
|
|
1454
|
-
if cc < c2 and (ni,nf,cc) in mTEPES.lea and (ni,nf,c2) in mTEPES.lca:
|
|
1455
|
-
return (OptModel.vFlowElec[sc,
|
|
1742
|
+
def eFlowParallelCandidate2(OptModel,p,sc,n,ni,nf,cc,c2):
|
|
1743
|
+
if (cc < c2 and (ni,nf,cc) in mTEPES.lea and (ni,nf,c2) in mTEPES.lca) and mTEPES.pIndPTDF == 0:
|
|
1744
|
+
return (OptModel.vFlowElec[p,sc,n,ni,nf,cc] - OptModel.vFlowElec[p,sc,n,ni,nf,c2] * mTEPES.pLineX[ni,nf,c2] / mTEPES.pLineX[ni,nf,cc]) / max(mTEPES.pMaxNTCBck[p,sc,n,ni,nf,cc],mTEPES.pMaxNTCFrw[p,sc,n,ni,nf,cc]) >= - 1 + OptModel.vLineCommit[p,sc,n,ni,nf,c2]
|
|
1456
1745
|
else:
|
|
1457
1746
|
return Constraint.Skip
|
|
1458
1747
|
setattr(OptModel, f'eFlowParallelCandidate2_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.pct, mTEPES.cc, mTEPES.c2, rule=eFlowParallelCandidate2, doc='unitary flow for each AC candidate parallel circuit [p.u.]'))
|