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.
Files changed (70) hide show
  1. openTEPES/9n_PTDF/oT_Data_Demand_9n_PTDF.csv +8737 -0
  2. openTEPES/9n_PTDF/oT_Data_Duration_9n_PTDF.csv +8737 -0
  3. openTEPES/9n_PTDF/oT_Data_Emission_9n_PTDF.csv +2 -0
  4. openTEPES/9n_PTDF/oT_Data_EnergyInflows_9n_PTDF.csv +8737 -0
  5. openTEPES/9n_PTDF/oT_Data_EnergyOutflows_9n_PTDF.csv +8737 -0
  6. openTEPES/9n_PTDF/oT_Data_Generation_9n_PTDF.csv +17 -0
  7. openTEPES/9n_PTDF/oT_Data_Inertia_9n_PTDF.csv +8737 -0
  8. openTEPES/9n_PTDF/oT_Data_Network_9n_PTDF.csv +14 -0
  9. openTEPES/9n_PTDF/oT_Data_NodeLocation_9n_PTDF.csv +10 -0
  10. openTEPES/9n_PTDF/oT_Data_OperatingReserveDown_9n_PTDF.csv +8737 -0
  11. openTEPES/9n_PTDF/oT_Data_OperatingReserveUp_9n_PTDF.csv +8737 -0
  12. openTEPES/9n_PTDF/oT_Data_Option_9n_PTDF.csv +2 -0
  13. openTEPES/9n_PTDF/oT_Data_Parameter_9n_PTDF.csv +2 -0
  14. openTEPES/9n_PTDF/oT_Data_Period_9n_PTDF.csv +2 -0
  15. openTEPES/9n_PTDF/oT_Data_RESEnergy_9n_PTDF.csv +2 -0
  16. openTEPES/9n_PTDF/oT_Data_ReserveMargin_9n_PTDF.csv +2 -0
  17. openTEPES/9n_PTDF/oT_Data_Scenario_9n_PTDF.csv +2 -0
  18. openTEPES/9n_PTDF/oT_Data_Stage_9n_PTDF.csv +2 -0
  19. openTEPES/9n_PTDF/oT_Data_VariableEmissionCost_9n_PTDF.csv +8737 -0
  20. openTEPES/9n_PTDF/oT_Data_VariableFuelCost_9n_PTDF.csv +8737 -0
  21. openTEPES/9n_PTDF/oT_Data_VariableMaxConsumption_9n_PTDF.csv +8737 -0
  22. openTEPES/9n_PTDF/oT_Data_VariableMaxEnergy_9n_PTDF.csv +8737 -0
  23. openTEPES/9n_PTDF/oT_Data_VariableMaxGeneration_9n_PTDF.csv +8737 -0
  24. openTEPES/9n_PTDF/oT_Data_VariableMaxStorage_9n_PTDF.csv +8737 -0
  25. openTEPES/9n_PTDF/oT_Data_VariableMinConsumption_9n_PTDF.csv +8737 -0
  26. openTEPES/9n_PTDF/oT_Data_VariableMinEnergy_9n_PTDF.csv +8737 -0
  27. openTEPES/9n_PTDF/oT_Data_VariableMinGeneration_9n_PTDF.csv +8737 -0
  28. openTEPES/9n_PTDF/oT_Data_VariableMinStorage_9n_PTDF.csv +8737 -0
  29. openTEPES/9n_PTDF/oT_Data_VariablePTDF_9n_PTDF.csv +8740 -0
  30. openTEPES/9n_PTDF/oT_Data_VariableTTCBck_9n_PTDF.csv +8739 -0
  31. openTEPES/9n_PTDF/oT_Data_VariableTTCFrw_9n_PTDF.csv +8739 -0
  32. openTEPES/9n_PTDF/oT_Dict_AreaToRegion_9n_PTDF.csv +2 -0
  33. openTEPES/9n_PTDF/oT_Dict_Area_9n_PTDF.csv +2 -0
  34. openTEPES/9n_PTDF/oT_Dict_Circuit_9n_PTDF.csv +3 -0
  35. openTEPES/9n_PTDF/oT_Dict_Generation_9n_PTDF.csv +17 -0
  36. openTEPES/9n_PTDF/oT_Dict_Line_9n_PTDF.csv +3 -0
  37. openTEPES/9n_PTDF/oT_Dict_LoadLevel_9n_PTDF.csv +8737 -0
  38. openTEPES/9n_PTDF/oT_Dict_NodeToZone_9n_PTDF.csv +10 -0
  39. openTEPES/9n_PTDF/oT_Dict_Node_9n_PTDF.csv +10 -0
  40. openTEPES/9n_PTDF/oT_Dict_Period_9n_PTDF.csv +2 -0
  41. openTEPES/9n_PTDF/oT_Dict_Region_9n_PTDF.csv +31 -0
  42. openTEPES/9n_PTDF/oT_Dict_Scenario_9n_PTDF.csv +2 -0
  43. openTEPES/9n_PTDF/oT_Dict_Stage_9n_PTDF.csv +2 -0
  44. openTEPES/9n_PTDF/oT_Dict_Storage_9n_PTDF.csv +3 -0
  45. openTEPES/9n_PTDF/oT_Dict_Technology_9n_PTDF.csv +7 -0
  46. openTEPES/9n_PTDF/oT_Dict_ZoneToArea_9n_PTDF.csv +10 -0
  47. openTEPES/9n_PTDF/oT_Dict_Zone_9n_PTDF.csv +10 -0
  48. openTEPES/RTS-GMLC_6y/oT_Dict_AreaToRegion_RTS-GMLC_6y.csv +4 -4
  49. openTEPES/RTS-GMLC_6y/oT_Dict_Area_RTS-GMLC_6y.csv +4 -4
  50. openTEPES/RTS-GMLC_6y/oT_Dict_Circuit_RTS-GMLC_6y.csv +5 -5
  51. openTEPES/RTS-GMLC_6y/oT_Dict_Line_RTS-GMLC_6y.csv +3 -3
  52. openTEPES/RTS-GMLC_6y/oT_Dict_NodeToZone_RTS-GMLC_6y.csv +74 -74
  53. openTEPES/RTS-GMLC_6y/oT_Dict_Region_RTS-GMLC_6y.csv +2 -2
  54. openTEPES/RTS-GMLC_6y/oT_Dict_Scenario_RTS-GMLC_6y.csv +2 -2
  55. openTEPES/RTS-GMLC_6y/oT_Dict_Storage_RTS-GMLC_6y.csv +3 -3
  56. openTEPES/RTS-GMLC_6y/oT_Dict_Technology_RTS-GMLC_6y.csv +10 -10
  57. openTEPES/RTS-GMLC_6y/oT_Dict_ZoneToArea_RTS-GMLC_6y.csv +22 -22
  58. openTEPES/RTS-GMLC_6y/oT_Dict_Zone_RTS-GMLC_6y.csv +22 -22
  59. openTEPES/__init__.py +1 -1
  60. openTEPES/openTEPES.py +137 -65
  61. openTEPES/openTEPES_InputData.py +419 -234
  62. openTEPES/openTEPES_Main.py +2 -2
  63. openTEPES/openTEPES_ModelFormulation.py +469 -180
  64. openTEPES/openTEPES_OutputResults.py +305 -223
  65. openTEPES/openTEPES_ProblemSolving.py +68 -56
  66. {opentepes-4.18.3.dist-info → openTEPES-4.18.5.dist-info}/METADATA +17 -18
  67. {opentepes-4.18.3.dist-info → openTEPES-4.18.5.dist-info}/RECORD +70 -23
  68. {opentepes-4.18.3.dist-info → openTEPES-4.18.5.dist-info}/WHEEL +1 -1
  69. {opentepes-4.18.3.dist-info → openTEPES-4.18.5.dist-info}/LICENSE +0 -0
  70. {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) - March 18, 2025
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.pLoadLevelDuration[p,sc,n]() * mTEPES.pStartUpCost [ nr] * OptModel.vStartUp [p,sc,n,nr] +
151
- mTEPES.pLoadLevelDuration[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.pLoadLevelDuration[p,sc,n]() * mTEPES.pOperReserveCost[ nr] * OptModel.vReserveUp [p,sc,n,nr] +
153
- mTEPES.pLoadLevelDuration[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.pIndOperReserve[nr] == 0) +
154
- sum(mTEPES.pLoadLevelDuration[p,sc,n]() * mTEPES.pOperReserveCost[ eh] * OptModel.vESSReserveUp [p,sc,n,eh] +
155
- mTEPES.pLoadLevelDuration[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.pIndOperReserve[eh] == 0) +
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 mTEPES.pOperReserveUp[p,sc,n,ar]:
407
- if sum(1 for nr in n2a[ar] if mTEPES.pIndOperReserve[nr] == 0) + sum(1 for eh in e2a[ar] if mTEPES.pIndOperReserve[eh] == 0):
408
- return sum(OptModel.vReserveUp [p,sc,n,nr] for nr in n2a[ar] if mTEPES.pIndOperReserve[nr] == 0 and (p,nr) in mTEPES.pnr) + sum(OptModel.vESSReserveUp [p,sc,n,eh] for eh in e2a[ar] if mTEPES.pIndOperReserve[eh] == 0 and (p,eh) in mTEPES.peh) == mTEPES.pOperReserveUp[p,sc,n,ar]
409
- else:
410
- return Constraint.Skip
411
- else:
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 mTEPES.pOperReserveDw[p,sc,n,ar]:
420
- if sum(1 for nr in n2a[ar] if mTEPES.pIndOperReserve[nr] == 0) + sum(1 for eh in e2a[ar] if mTEPES.pIndOperReserve[eh] == 0):
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
- if mTEPES.pMinRatioDwUp and sum(mTEPES.pOperReserveUp[p,sc,n,ar] for ar in a2n[nr]) and sum(mTEPES.pOperReserveDw[p,sc,n,ar] for ar in a2n[nr]) and mTEPES.pMaxPower2ndBlock[p,sc,n,nr] and mTEPES.pIndOperReserve[nr] == 0:
433
- return OptModel.vReserveDown[p,sc,n,nr] >= OptModel.vReserveUp[p,sc,n,nr] * mTEPES.pMinRatioDwUp
434
- else:
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
- if mTEPES.pMaxRatioDwUp < 1.0 and sum(mTEPES.pOperReserveUp[p,sc,n,ar] for ar in a2n[nr]) and sum(mTEPES.pOperReserveDw[p,sc,n,ar] for ar in a2n[nr]) and mTEPES.pMaxPower2ndBlock[p,sc,n,nr] and mTEPES.pIndOperReserve[nr] == 0:
443
- return OptModel.vReserveDown[p,sc,n,nr] <= OptModel.vReserveUp[p,sc,n,nr] * mTEPES.pMaxRatioDwUp
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
- if mTEPES.pMinRatioDwUp and sum(mTEPES.pOperReserveUp[p,sc,n,ar] for ar in a2e[eh]) and sum(mTEPES.pOperReserveDw[p,sc,n,ar] for ar in a2e[eh]) and mTEPES.pMaxPower2ndBlock[p,sc,n,eh] and mTEPES.pIndOperReserve[eh] == 0:
453
- return OptModel.vESSReserveDown[p,sc,n,eh] >= OptModel.vESSReserveUp[p,sc,n,eh] * mTEPES.pMinRatioDwUp
454
- else:
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
- if mTEPES.pMaxRatioDwUp < 1.0 and sum(mTEPES.pOperReserveUp[p,sc,n,ar] for ar in a2e[eh]) and sum(mTEPES.pOperReserveDw[p,sc,n,ar] for ar in a2e[eh]) and mTEPES.pMaxPower2ndBlock[p,sc,n,eh] and mTEPES.pIndOperReserve[eh] == 0:
463
- return OptModel.vESSReserveDown[p,sc,n,eh] <= OptModel.vESSReserveUp[p,sc,n,eh] * mTEPES.pMaxRatioDwUp
464
- else:
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
- if mTEPES.pIndOperReserve[es] == 0 and (p,es) in mTEPES.pes:
473
- if sum(mTEPES.pOperReserveUp[p,sc,n,ar] for ar in a2e[es]) and (mTEPES.pTotalMaxCharge[es] or mTEPES.pTotalEnergyInflows[es]) and mTEPES.pMaxPower2ndBlock [p,sc,n,es] and mTEPES.pDuration[p,sc,n]():
474
- return OptModel.vReserveUp [p,sc,n,es] <= OptModel.vESSInventory[p,sc,n,es] / mTEPES.pDuration[p,sc,n]()
475
- else:
476
- return Constraint.Skip
477
- else:
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 eReserveDwIfEnergy(OptModel,n,es):
485
- if mTEPES.pIndOperReserve[es] == 0 and (p,es) in mTEPES.pes:
486
- if sum(mTEPES.pOperReserveDw[p,sc,n,ar] for ar in a2e[es]) and (mTEPES.pTotalMaxCharge[es] or mTEPES.pTotalEnergyInflows[es]) and mTEPES.pMaxPower2ndBlock [p,sc,n,es] and mTEPES.pDuration[p,sc,n]():
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
- def eESSReserveUpIfEnergy(OptModel,n,es):
498
- if mTEPES.pIndOperReserve[es] == 0 and (p,es) in mTEPES.pes:
499
- if sum(mTEPES.pOperReserveUp[p,sc,n,ar] for ar in a2e[es]) and (mTEPES.pTotalMaxCharge[es] or mTEPES.pTotalEnergyInflows[es]) and mTEPES.pMaxCharge2ndBlock[p,sc,n,es] and mTEPES.pDuration[p,sc,n]():
500
- return OptModel.vESSReserveUp [p,sc,n,es] <= (mTEPES.pMaxStorage[p,sc,n,es] - OptModel.vESSInventory[p,sc,n,es]) / mTEPES.pDuration[p,sc,n]()
501
- else:
502
- return Constraint.Skip
503
- else:
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
- def eESSReserveDwIfEnergy(OptModel,n,es):
511
- if mTEPES.pIndOperReserve[es] == 0 and (p,es) in mTEPES.pes:
512
- if sum(mTEPES.pOperReserveDw[p,sc,n,ar] for ar in a2e[es]) and (mTEPES.pTotalMaxCharge[es] or mTEPES.pTotalEnergyInflows[es]) and mTEPES.pMaxCharge2ndBlock[p,sc,n,es] and mTEPES.pDuration[p,sc,n]():
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 Constraint.Skip
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 mTEPES.pIndOperReserve[eh] == 0 and (p,eh) in mTEPES.peh:
643
- if sum(mTEPES.pOperReserveDw[p,sc,n,ar] for ar in a2e[eh]) and mTEPES.pMaxCharge[p,sc,n,eh]:
644
- return (OptModel.vCharge2ndBlock[p,sc,n,eh] + OptModel.vESSReserveDown[p,sc,n,eh]) / mTEPES.pMaxCharge2ndBlock[p,sc,n,eh] <= 1.0
645
- else:
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 Constraint.Skip
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 mTEPES.pIndOperReserve[eh] == 0 and (p,eh) in mTEPES.peh:
656
- if sum(mTEPES.pOperReserveUp[p,sc,n,ar] for ar in a2e[eh]) and mTEPES.pMaxCharge[p,sc,n,eh]:
657
- return OptModel.vCharge2ndBlock[p,sc,n,eh] - OptModel.vESSReserveUp [p,sc,n,eh] >= 0.0
658
- else:
659
- return Constraint.Skip
660
- else:
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 (p,eh) in mTEPES.peh:
676
- if mTEPES.pMaxPower2ndBlock [p,sc,n,eh] and mTEPES.pMaxCharge2ndBlock[p,sc,n,eh]:
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 (p,eh) in mTEPES.peh:
690
- if mTEPES.pMaxCharge[p,sc,n,eh] and mTEPES.pMaxCharge2ndBlock[p,sc,n,eh]:
691
- if mTEPES.pMinCharge[p,sc,n,eh] == 0.0:
692
- 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]
693
- else:
694
- 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]
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 Constraint.Skip
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
- return Constraint.Skip
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 eTrbReserveUpIfEnergy(OptModel,n,h):
789
- if mTEPES.pIndOperReserve[h] == 0 and (p,h) in mTEPES.ph:
790
- if sum(mTEPES.pOperReserveUp[p,sc,n,ar] for ar in a2h[h]) and mTEPES.pMaxPower2ndBlock [p,sc,n,h] and mTEPES.pDuration[p,sc,n]():
791
- return OptModel.vReserveUp [p,sc,n,h] <= sum( OptModel.vReservoirVolume[p,sc,n,rs] for rs in mTEPES.rs if (rs,h) in mTEPES.r2h) / mTEPES.pDuration[p,sc,n]() * mTEPES.pProductionFunctionHydro[h]
792
- else:
793
- return Constraint.Skip
794
- else:
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
- setattr(OptModel, f'eTrbReserveUpIfEnergy_{p}_{sc}_{st}', Constraint(mTEPES.nhc, rule=eTrbReserveUpIfEnergy, doc='up operating reserve if energy available [GW]'))
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('eTrbReserveUpIfEnergy ... ', len(getattr(OptModel, f'eTrbReserveUpIfEnergy_{p}_{sc}_{st}')), ' rows')
901
+ print('eTrbReserveUpIfUpstream ... ', len(getattr(OptModel, f'eTrbReserveUpIfUpstream_{p}_{sc}_{st}')), ' rows')
800
902
 
801
- def eTrbReserveDwIfEnergy(OptModel,n,h):
802
- if mTEPES.pIndOperReserve[h] == 0 and (p,h) in mTEPES.ph:
803
- if sum(mTEPES.pOperReserveDw[p,sc,n,ar] for ar in a2h[h]) and mTEPES.pMaxPower2ndBlock [p,sc,n,h] and mTEPES.pDuration[p,sc,n]():
804
- return OptModel.vReserveDown [p,sc,n,h] <= (sum(mTEPES.pMaxVolume[p,sc,n,rs] - OptModel.vReservoirVolume[p,sc,n,rs] for rs in mTEPES.rs if (rs,h) in mTEPES.r2h)) / mTEPES.pDuration[p,sc,n]() * mTEPES.pProductionFunctionHydro[h]
805
- else:
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
- setattr(OptModel, f'eTrbReserveDwIfEnergy_{p}_{sc}_{st}', Constraint(mTEPES.nhc, rule=eTrbReserveDwIfEnergy, doc='down operating reserve if energy available [GW]'))
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('eTrbReserveDwIfEnergy ... ', len(getattr(OptModel, f'eTrbReserveDwIfEnergy_{p}_{sc}_{st}')), ' rows')
931
+ print('eTrbReserveUpIfDownstream ... ', len(getattr(OptModel, f'eTrbReserveUpIfDownstream_{p}_{sc}_{st}')), ' rows')
813
932
 
814
- def ePmpReserveUpIfEnergy(OptModel,n,h):
815
- if mTEPES.pIndOperReserve[h] == 0 and sum(1 for rs in mTEPES.rs if (h,rs) in mTEPES.p2r) and (p,h) in mTEPES.ph:
816
- if sum(mTEPES.pOperReserveUp[p,sc,n,ar] for ar in a2h[h]) and mTEPES.pMaxCharge2ndBlock[p,sc,n,h] and mTEPES.pDuration[p,sc,n]():
817
- return OptModel.vESSReserveUp [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.p2r)) / mTEPES.pDuration[p,sc,n]() * mTEPES.pProductionFunctionHydro[h]
818
- else:
819
- return Constraint.Skip
820
- else:
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
- setattr(OptModel, f'ePmpReserveUpIfEnergy_{p}_{sc}_{st}', Constraint(mTEPES.np2c, rule=ePmpReserveUpIfEnergy, doc='up operating reserve if energy available [GW]'))
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('ePmpReserveUpIfEnergy ... ', len(getattr(OptModel, f'ePmpReserveUpIfEnergy_{p}_{sc}_{st}')), ' rows')
965
+ print('ePmpReserveDwIfUpstream ... ', len(getattr(OptModel, f'ePmpReserveDwIfUpstream_{p}_{sc}_{st}')), ' rows')
826
966
 
827
- def ePmpReserveDwIfEnergy(OptModel,n,h):
828
- if mTEPES.pIndOperReserve[h] == 0 and sum(1 for rs in mTEPES.rs if (rs,h) in mTEPES.r2p) and (p,h) in mTEPES.ph:
829
- if sum(mTEPES.pOperReserveDw[p,sc,n,ar] for ar in a2h[h]) and mTEPES.pMaxCharge2ndBlock[p,sc,n,h] and mTEPES.pDuration[p,sc,n]():
830
- return OptModel.vESSReserveDown[p,sc,n,h] <= sum( OptModel.vReservoirVolume[p,sc,n,rs] for rs in mTEPES.rs if (rs,h) in mTEPES.r2p) / mTEPES.pDuration[p,sc,n]() * mTEPES.pProductionFunctionHydro[h]
831
- else:
832
- return Constraint.Skip
833
- else:
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
- setattr(OptModel, f'ePmpReserveDwIfEnergy_{p}_{sc}_{st}', Constraint(mTEPES.npc, rule=ePmpReserveDwIfEnergy, doc='down operating reserve if energy available [GW]'))
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('ePmpReserveDwIfEnergy ... ', len(getattr(OptModel, f'ePmpReserveDwIfEnergy_{p}_{sc}_{st}')), ' rows')
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 eMaxCommitment(OptModel,n,nr):
971
- if len(mTEPES.g2g) and (p,nr) in mTEPES.pnr:
972
- if sum(1 for g in mTEPES.nr if (nr,g) in mTEPES.g2g or (g,nr) in mTEPES.g2g):
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
- setattr(OptModel, f'eMaxCommitment_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.nr, rule=eMaxCommitment, doc='maximum of all the commitments [p.u.]'))
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('eMaxCommitment ... ', len(getattr(OptModel, f'eMaxCommitment_{p}_{sc}_{st}')), ' rows')
1146
+ print('eMaxCommitmentYearly ... ', len(getattr(OptModel, f'eMaxCommitmentYearly_{p}_{sc}_{st}')), ' rows')
982
1147
 
983
- def eMaxCommitGen(OptModel,n,g):
984
- if len(mTEPES.g2g) and (p,g) in mTEPES.pg:
985
- if sum(1 for gg in mTEPES.g if (g,gg) in mTEPES.g2g or (gg,g) in mTEPES.g2g) and mTEPES.pMaxPowerElec[p,sc,n,g]:
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
- setattr(OptModel, f'eMaxCommitGen_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.g, rule=eMaxCommitGen, doc='maximum of all the capacity factors'))
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('eMaxCommitGen ... ', len(getattr(OptModel, f'eMaxCommitGen_{p}_{sc}_{st}')), ' rows')
1167
+ print('eMaxCommitGenYearly ... ', len(getattr(OptModel, f'eMaxCommitGenYearly_{p}_{sc}_{st}')), ' rows')
995
1168
 
996
- def eExclusiveGens(OptModel,n,g):
997
- if len(mTEPES.g2g) and (p,g) in mTEPES.pg:
998
- if sum(1 for gg in mTEPES.g if (gg,g) in mTEPES.g2g):
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
- setattr(OptModel, f'eExclusiveGens_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.g, rule=eExclusiveGens, doc='mutually exclusive generators'))
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('eExclusiveGens ... ', len(getattr(OptModel, f'eExclusiveGens_{p}_{sc}_{st}')), ' rows')
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.pLineNTCMax[ni,nf,cc] >= - OptModel.vLineCommit[p,sc,n,ni,nf,cc]
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.pLineNTCMax[ni,nf,cc] <= OptModel.vLineCommit[p,sc,n,ni,nf,cc]
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 len(mTEPES.ll):
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 len(mTEPES.ll):
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,p,n,cyc,nii,nff,cc):
1428
- return (sum(OptModel.vFlowElec[sc,p,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) -
1429
- sum(OptModel.vFlowElec[sc,p,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[sc,p,n,nii,nff,cc]
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,p,n,cyc,nii,nff,cc):
1436
- return (sum(OptModel.vFlowElec[sc,p,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) -
1437
- sum(OptModel.vFlowElec[sc,p,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[sc,p,n,nii,nff,cc]
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,p,n,ni,nf,cc,c2):
1444
- if cc < c2 and (ni,nf,cc) in mTEPES.lea and (ni,nf,c2) in mTEPES.lca:
1445
- return (OptModel.vFlowElec[sc,p,n,ni,nf,cc] - OptModel.vFlowElec[sc,p,n,ni,nf,c2] * mTEPES.pLineX[ni,nf,c2] / mTEPES.pLineX[ni,nf,cc]) / max(mTEPES.pLineNTCBck[ni,nf,cc],mTEPES.pLineNTCFrw[ni,nf,cc]) <= 1 - OptModel.vLineCommit[sc,p,n,ni,nf,c2]
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,p,n,ni,nf,cc,c2):
1454
- if cc < c2 and (ni,nf,cc) in mTEPES.lea and (ni,nf,c2) in mTEPES.lca:
1455
- return (OptModel.vFlowElec[sc,p,n,ni,nf,cc] - OptModel.vFlowElec[sc,p,n,ni,nf,c2] * mTEPES.pLineX[ni,nf,c2] / mTEPES.pLineX[ni,nf,cc]) / max(mTEPES.pLineNTCBck[ni,nf,cc],mTEPES.pLineNTCFrw[ni,nf,cc]) >= - 1 + OptModel.vLineCommit[sc,p,n,ni,nf,c2]
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.]'))