openTEPES 4.18.4__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 +21 -15
  61. openTEPES/openTEPES_InputData.py +383 -214
  62. openTEPES/openTEPES_Main.py +2 -2
  63. openTEPES/openTEPES_ModelFormulation.py +413 -156
  64. openTEPES/openTEPES_OutputResults.py +228 -166
  65. openTEPES/openTEPES_ProblemSolving.py +30 -28
  66. {opentepes-4.18.4.dist-info → openTEPES-4.18.5.dist-info}/METADATA +15 -16
  67. {opentepes-4.18.4.dist-info → openTEPES-4.18.5.dist-info}/RECORD +70 -23
  68. {opentepes-4.18.4.dist-info → openTEPES-4.18.5.dist-info}/WHEEL +1 -1
  69. {opentepes-4.18.4.dist-info → openTEPES-4.18.5.dist-info}/LICENSE +0 -0
  70. {opentepes-4.18.4.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) - April 05, 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
@@ -150,9 +150,9 @@ def GenerationOperationModelFormulationObjFunct(OptModel, mTEPES, pIndLogConsole
150
150
  mTEPES.pLoadLevelWeight [p,sc,n]() * mTEPES.pStartUpCost [ nr] * OptModel.vStartUp [p,sc,n,nr] +
151
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
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.pIndOperReserve[nr] == 0) +
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
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.pIndOperReserve[eh] == 0) +
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]:
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):
425
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:
435
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:
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:
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:
445
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:
478
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:
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:
@@ -646,7 +691,7 @@ def GenerationOperationModelFormulationStorage(OptModel, mTEPES, pIndLogConsole,
646
691
  # Hydro units have commitment while ESS units are implicitly always committed
647
692
  if eh not in mTEPES.h:
648
693
  # ESS units only need this constraint when they can offer operating reserves and the systems demands reserves
649
- if mTEPES.pIndOperReserve[eh] != 0 or not sum(mTEPES.pOperReserveDw[p,sc,n,ar] for ar in a2e[eh]):
694
+ if mTEPES.pIndOperReserveCon[eh] != 0 or not sum(mTEPES.pOperReserveDw[p,sc,n,ar] for ar in a2e[eh]):
650
695
  return Constraint.Skip
651
696
  # ESS case equation
652
697
  return (OptModel.vCharge2ndBlock[p, sc, n, eh] + OptModel.vESSReserveDown[p, sc, n, eh]) / mTEPES.pMaxCharge2ndBlock[p, sc, n, eh] <= 1.0
@@ -660,13 +705,21 @@ def GenerationOperationModelFormulationStorage(OptModel, mTEPES, pIndLogConsole,
660
705
  print('eMaxCharge ... ', len(getattr(OptModel, f'eMaxCharge_{p}_{sc}_{st}')), ' rows')
661
706
 
662
707
  def eMinCharge(OptModel, n, eh):
663
- if mTEPES.pIndOperReserve[eh] == 0 and (p, eh) in mTEPES.peh:
664
- if sum(mTEPES.pOperReserveUp[p, sc, n, ar] for ar in a2e[eh]) and mTEPES.pMaxCharge[p, sc, n, eh]:
665
- return OptModel.vCharge2ndBlock[p, sc, n, eh] - OptModel.vESSReserveUp[p, sc, n, eh] >= 0.0
666
- else:
667
- return Constraint.Skip
668
- else:
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]:
669
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:
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
+
670
723
  setattr(OptModel, f'eMinCharge_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.eh, rule=eMinCharge, doc='min charge of an ESS [p.u.]'))
671
724
 
672
725
  if pIndLogConsole == 1:
@@ -817,57 +870,132 @@ def GenerationOperationModelFormulationReservoir(OptModel, mTEPES, pIndLogConsol
817
870
  if pIndLogConsole == 1:
818
871
  print('eMinVolume2Comm ... ', len(getattr(OptModel, f'eMinVolume2Comm_{p}_{sc}_{st}')), ' rows')
819
872
 
820
- def eTrbReserveUpIfEnergy(OptModel,n,h):
821
- if mTEPES.pIndOperReserve[h] == 0 and (p,h) in mTEPES.ph:
822
- 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]():
823
- 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]
824
- else:
825
- return Constraint.Skip
826
- 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:
827
890
  return Constraint.Skip
828
- 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]'))
829
899
 
830
900
  if pIndLogConsole == 1:
831
- print('eTrbReserveUpIfEnergy ... ', len(getattr(OptModel, f'eTrbReserveUpIfEnergy_{p}_{sc}_{st}')), ' rows')
901
+ print('eTrbReserveUpIfUpstream ... ', len(getattr(OptModel, f'eTrbReserveUpIfUpstream_{p}_{sc}_{st}')), ' rows')
832
902
 
833
- def eTrbReserveDwIfEnergy(OptModel,n,h):
834
- if mTEPES.pIndOperReserve[h] == 0 and (p,h) in mTEPES.ph:
835
- 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]():
836
- 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]
837
- else:
838
- return Constraint.Skip
839
- 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:
908
+ return Constraint.Skip
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:
840
914
  return Constraint.Skip
841
- setattr(OptModel, f'eTrbReserveDwIfEnergy_{p}_{sc}_{st}', Constraint(mTEPES.nhc, rule=eTrbReserveDwIfEnergy, doc='down operating reserve if energy available [GW]'))
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]'))
842
929
 
843
930
  if pIndLogConsole == 1:
844
- print('eTrbReserveDwIfEnergy ... ', len(getattr(OptModel, f'eTrbReserveDwIfEnergy_{p}_{sc}_{st}')), ' rows')
931
+ print('eTrbReserveUpIfDownstream ... ', len(getattr(OptModel, f'eTrbReserveUpIfDownstream_{p}_{sc}_{st}')), ' rows')
845
932
 
846
- def ePmpReserveUpIfEnergy(OptModel,n,h):
847
- 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:
848
- 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]():
849
- 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]
850
- else:
851
- return Constraint.Skip
852
- 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:
853
954
  return Constraint.Skip
854
- 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]'))
855
963
 
856
964
  if pIndLogConsole == 1:
857
- print('ePmpReserveUpIfEnergy ... ', len(getattr(OptModel, f'ePmpReserveUpIfEnergy_{p}_{sc}_{st}')), ' rows')
965
+ print('ePmpReserveDwIfUpstream ... ', len(getattr(OptModel, f'ePmpReserveDwIfUpstream_{p}_{sc}_{st}')), ' rows')
858
966
 
859
- def ePmpReserveDwIfEnergy(OptModel,n,h):
860
- 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:
861
- 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]():
862
- 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]
863
- else:
864
- return Constraint.Skip
865
- 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:
866
972
  return Constraint.Skip
867
- setattr(OptModel, f'ePmpReserveDwIfEnergy_{p}_{sc}_{st}', Constraint(mTEPES.npc, rule=ePmpReserveDwIfEnergy, doc='down operating reserve if energy available [GW]'))
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:
984
+ return Constraint.Skip
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]'))
868
996
 
869
997
  if pIndLogConsole == 1:
870
- print('ePmpReserveDwIfEnergy ... ', len(getattr(OptModel, f'ePmpReserveDwIfEnergy_{p}_{sc}_{st}')), ' rows')
998
+ print('ePmpReserveDwIfDownstream ... ', len(getattr(OptModel, f'ePmpReserveDwIfDownstream_{p}_{sc}_{st}')), ' rows')
871
999
 
872
1000
  def eHydroInventory(OptModel,n,rs):
873
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):
@@ -999,50 +1127,114 @@ def GenerationOperationModelFormulationCommitment(OptModel, mTEPES, pIndLogConso
999
1127
  if pIndLogConsole == 1:
1000
1128
  print('eStableStates ... ', len(getattr(OptModel, f'eStableStates_{p}_{sc}_{st}')), ' rows')
1001
1129
 
1002
- def eMaxCommitment(OptModel,n,nr):
1003
- if len(mTEPES.g2g) and (p,nr) in mTEPES.pnr:
1004
- if sum(1 for g in mTEPES.nr if (nr,g) in mTEPES.g2g or (g,nr) in mTEPES.g2g):
1005
- return OptModel.vCommitment[p,sc,n,nr] <= OptModel.vMaxCommitment[p,sc,nr]
1006
- else:
1007
- return Constraint.Skip
1008
- else:
1130
+ def eMaxCommitmentYearly(OptModel,n,group,nr):
1131
+ # Skip if generator not available on period
1132
+ if (p,nr) not in mTEPES.pnr:
1133
+ return Constraint.Skip
1134
+ # Skip if the generator is not part of the exclusive group
1135
+ if nr not in mTEPES.GeneratorsInYearlyGroup[group]:
1009
1136
  return Constraint.Skip
1010
- setattr(OptModel, f'eMaxCommitment_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.nr, rule=eMaxCommitment, doc='maximum of all the commitments [p.u.]'))
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.]'))
1011
1144
 
1012
1145
  if pIndLogConsole == 1:
1013
- print('eMaxCommitment ... ', len(getattr(OptModel, f'eMaxCommitment_{p}_{sc}_{st}')), ' rows')
1146
+ print('eMaxCommitmentYearly ... ', len(getattr(OptModel, f'eMaxCommitmentYearly_{p}_{sc}_{st}')), ' rows')
1014
1147
 
1015
- def eMaxCommitGen(OptModel,n,g):
1016
- if len(mTEPES.g2g) and (p,g) in mTEPES.pg:
1017
- 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]:
1018
- return OptModel.vTotalOutput[p,sc,n,g]/mTEPES.pMaxPowerElec[p,sc,n,g] <= OptModel.vMaxCommitment[p,sc,g]
1019
- else:
1020
- return Constraint.Skip
1021
- else:
1148
+ def eMaxCommitGenYearly(OptModel,n,group,nr):
1149
+ # Skip if generator not available on period
1150
+ if (p,nr) not in mTEPES.pnr:
1151
+ return Constraint.Skip
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:
1022
1160
  return Constraint.Skip
1023
- setattr(OptModel, f'eMaxCommitGen_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.g, rule=eMaxCommitGen, doc='maximum of all the capacity factors'))
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'))
1024
1165
 
1025
1166
  if pIndLogConsole == 1:
1026
- print('eMaxCommitGen ... ', len(getattr(OptModel, f'eMaxCommitGen_{p}_{sc}_{st}')), ' rows')
1167
+ print('eMaxCommitGenYearly ... ', len(getattr(OptModel, f'eMaxCommitGenYearly_{p}_{sc}_{st}')), ' rows')
1027
1168
 
1028
- def eExclusiveGens(OptModel,g):
1029
- if len(mTEPES.g2g) and (p,g) in mTEPES.pg:
1030
- if sum(1 for gg in mTEPES.g if (gg,g) in mTEPES.g2g):
1031
- return OptModel.vMaxCommitment[p,sc,g] + sum(OptModel.vMaxCommitment[p,sc,gg] for gg in mTEPES.g if (gg,g) in mTEPES.g2g) <= 1
1032
- else:
1033
- return Constraint.Skip
1034
- 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:
1035
1172
  return Constraint.Skip
1036
- setattr(OptModel, f'eExclusiveGens_{p}_{sc}_{st}', Constraint(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'))
1037
1175
 
1038
1176
  if pIndLogConsole == 1:
1039
- print('eExclusiveGens ... ', len(getattr(OptModel, f'eExclusiveGens_{p}_{sc}_{st}')), ' rows')
1177
+ print('eExclusiveGensYearly ... ', len(getattr(OptModel, f'eExclusiveGensYearly_{p}_{sc}_{st}')), ' rows')
1040
1178
 
1041
1179
  GeneratingTime = time.time() - StartTime
1042
1180
  if pIndLogConsole == 1:
1043
1181
  print('Generating generation commitment ... ', round(GeneratingTime), 's')
1044
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.]'))
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'))
1045
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')
1046
1238
  def GenerationOperationModelFormulationRampMinTime(OptModel, mTEPES, pIndLogConsole, p, sc, st):
1047
1239
  print('Ramp and min up/down time constraints ****')
1048
1240
 
@@ -1308,7 +1500,7 @@ def NetworkOperationModelFormulation(OptModel, mTEPES, pIndLogConsole, p, sc, st
1308
1500
 
1309
1501
  def eNetCapacity1(OptModel,n,ni,nf,cc):
1310
1502
  if mTEPES.pIndBinSingleNode() == 0 and ((ni,nf,cc) in mTEPES.lc or mTEPES.pIndBinLineSwitch[ni,nf,cc] == 1):
1311
- 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]
1312
1504
  else:
1313
1505
  return Constraint.Skip
1314
1506
  setattr(OptModel, f'eNetCapacity1_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.la, rule=eNetCapacity1, doc='maximum flow by existing network capacity [p.u.]'))
@@ -1318,7 +1510,7 @@ def NetworkOperationModelFormulation(OptModel, mTEPES, pIndLogConsole, p, sc, st
1318
1510
 
1319
1511
  def eNetCapacity2(OptModel,n,ni,nf,cc):
1320
1512
  if mTEPES.pIndBinSingleNode() == 0 and ((ni,nf,cc) in mTEPES.lc or mTEPES.pIndBinLineSwitch[ni,nf,cc] == 1):
1321
- 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]
1322
1514
  else:
1323
1515
  return Constraint.Skip
1324
1516
  setattr(OptModel, f'eNetCapacity2_{p}_{sc}_{st}', Constraint(mTEPES.n, mTEPES.la, rule=eNetCapacity2, doc='maximum flow by existing network capacity [p.u.]'))
@@ -1327,7 +1519,7 @@ def NetworkOperationModelFormulation(OptModel, mTEPES, pIndLogConsole, p, sc, st
1327
1519
  print('eNetCapacity2 ... ', len(getattr(OptModel, f'eNetCapacity2_{p}_{sc}_{st}')), ' rows')
1328
1520
 
1329
1521
  def eKirchhoff2ndLaw1(OptModel,n,ni,nf,cc):
1330
- 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:
1331
1523
  if (ni,nf,cc) in mTEPES.lca:
1332
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]
1333
1525
  else:
@@ -1340,7 +1532,7 @@ def NetworkOperationModelFormulation(OptModel, mTEPES, pIndLogConsole, p, sc, st
1340
1532
  print('eKirchhoff2ndLaw1 ... ', len(getattr(OptModel, f'eKirchhoff2ndLaw1_{p}_{sc}_{st}')), ' rows')
1341
1533
 
1342
1534
  def eKirchhoff2ndLaw2(OptModel,n,ni,nf,cc):
1343
- 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:
1344
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]
1345
1537
  else:
1346
1538
  return Constraint.Skip
@@ -1350,7 +1542,7 @@ def NetworkOperationModelFormulation(OptModel, mTEPES, pIndLogConsole, p, sc, st
1350
1542
  print('eKirchhoff2ndLaw2 ... ', len(getattr(OptModel, f'eKirchhoff2ndLaw2_{p}_{sc}_{st}')), ' rows')
1351
1543
 
1352
1544
  def eLineLosses1(OptModel,n,ni,nf,cc):
1353
- 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:
1354
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]
1355
1547
  else:
1356
1548
  return Constraint.Skip
@@ -1360,7 +1552,7 @@ def NetworkOperationModelFormulation(OptModel, mTEPES, pIndLogConsole, p, sc, st
1360
1552
  print('eLineLosses1 ... ', len(getattr(OptModel, f'eLineLosses1_{p}_{sc}_{st}')), ' rows')
1361
1553
 
1362
1554
  def eLineLosses2(OptModel,n,ni,nf,cc):
1363
- 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:
1364
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]
1365
1557
  else:
1366
1558
  return Constraint.Skip
@@ -1369,11 +1561,70 @@ def NetworkOperationModelFormulation(OptModel, mTEPES, pIndLogConsole, p, sc, st
1369
1561
  if pIndLogConsole == 1:
1370
1562
  print('eLineLosses2 ... ', len(getattr(OptModel, f'eLineLosses2_{p}_{sc}_{st}')), ' rows')
1371
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
+
1372
1624
  GeneratingTime = time.time() - StartTime
1373
1625
  if pIndLogConsole == 1:
1374
1626
  print('Generating network constraints ... ', round(GeneratingTime), 's')
1375
1627
 
1376
-
1377
1628
  def NetworkCycles(mTEPES, pIndLogConsole):
1378
1629
  print('Network Cycles Detection ****')
1379
1630
 
@@ -1456,25 +1707,31 @@ def CycleConstraints(OptModel, mTEPES, pIndLogConsole, p, sc, st):
1456
1707
 
1457
1708
  #%% cycle Kirchhoff's second law with some candidate lines
1458
1709
  # this equation is formulated for every AC candidate line included in the cycle
1459
- def eCycleKirchhoff2ndLawCnd1(OptModel,sc,p,n,cyc,nii,nff,cc):
1460
- 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) -
1461
- 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
1462
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]'))
1463
1717
 
1464
1718
  if pIndLogConsole == 1:
1465
1719
  print('eCycleKirchhoff2ndLC1 ... ', len(getattr(OptModel, f'eCycleKirchhoff2ndLawCnd1_{p}_{sc}_{st}')), ' rows')
1466
1720
 
1467
- def eCycleKirchhoff2ndLawCnd2(OptModel,sc,p,n,cyc,nii,nff,cc):
1468
- 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) -
1469
- 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
1470
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]'))
1471
1728
 
1472
1729
  if pIndLogConsole == 1:
1473
1730
  print('eCycleKirchhoff2ndLC2 ... ', len(getattr(OptModel, f'eCycleKirchhoff2ndLawCnd2_{p}_{sc}_{st}')), ' rows')
1474
1731
 
1475
- def eFlowParallelCandidate1(OptModel,sc,p,n,ni,nf,cc,c2):
1476
- if cc < c2 and (ni,nf,cc) in mTEPES.lea and (ni,nf,c2) in mTEPES.lca:
1477
- 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]
1478
1735
  else:
1479
1736
  return Constraint.Skip
1480
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.]'))
@@ -1482,9 +1739,9 @@ def CycleConstraints(OptModel, mTEPES, pIndLogConsole, p, sc, st):
1482
1739
  if pIndLogConsole == 1:
1483
1740
  print('eFlowParallelCnddate1 ... ', len(getattr(OptModel, f'eFlowParallelCandidate1_{p}_{sc}_{st}')), ' rows')
1484
1741
 
1485
- def eFlowParallelCandidate2(OptModel,sc,p,n,ni,nf,cc,c2):
1486
- if cc < c2 and (ni,nf,cc) in mTEPES.lea and (ni,nf,c2) in mTEPES.lca:
1487
- 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]
1488
1745
  else:
1489
1746
  return Constraint.Skip
1490
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.]'))