Search code examples
pythonschedulingpulp

Ramping constraint in linear scheduling program in PuLP


I am trying to write a LP in PuLP which minimizes the electricity costs of a nitrogen blending process. The produced nitrogen can be either directly used for the blending or can be stored and extracted at a later stage. With this flexibility, the blending process can be optimized based on the day ahead electricity costs.

I have created a dictionary with all the possible configurations for the plant with the corresponding electricity consumption, storage application and configuration of the three air separation units. From this dictionary, I then create a nested dictionary based on the forecasted nitrogen demand. Each primary key represents a timestage of 1 hour for the next day and the nested dictionary contains all the possible configurations for that timestage (which are the configurations which at least meet the forecasted demand of that hour).

So the nested dictionary has the following structure:

d = {t : { cfg_key : [production value (total production of ASUs in m3/hr), 
consumption value (MW), storage application (i.e. extraction or injection in m3/hr),(ASU 1, ASU 2, ASU 3)}}

e.g.: { 0 : { 135 : [(0, 0, 48000), 14.76, -115000,], 137 : [.....], ....etc. }}

if you need more details on this dictionary and how it was made, please ask I will add it. I've currently left it out to be more concise.

I then wrote the following LP in PuLP. This code selects one possible configuration for each timeslot, while meeting production and storage constraints.

from pulp import *

d = sols_cands
set_ASU = range(0,3)
M = 60000

#----Program Initialization--------------------------------------------------------------
lp_problem = LpProblem("SchedulingProgram", LpMinimize)

#----Variables---------------------------------------------------------------------------
var = {}       # Binary variable for each possible config to select

for t in d:
    var[t] = {}
    for cfg in d[t]:
        var[t][cfg] = LpVariable(f"x_{t}_{cfg}", 0, 1, LpBinary)

#----Objective function------------------------------------------------------------------
objective = sum([d[t][cfg][1] * var[t][cfg] * energy_prices[t] 
                      for t in var.keys() for cfg in var[t].keys()])

lp_problem.setObjective(objective)

#----Constraints-------------------------------------------------------------------------

# Constraint which ensures only one configuration is selected for each timeslot
for t in var.keys():
    lp_problem += sum([var[t][cfg] for cfg in var[t].keys()]) == 1

# Constraint to zero out the total storage utilization (extraction and injection) over the day
lp_problem += sum([d[t][cfg][2]*var[t][cfg] for t in var.keys() 
                       for cfg in var[t].keys()]) == 0
        

# Solve the LP problem
lp_problem.solve()

# Print the solution
for t in var.keys():
    for cfg in d[t].keys():
        if var[t][cfg].varValue == 1:
            print(f"Timeslot {t} : Energy Price {energy_prices[t]} \
                    : N2 Demand {n2_demand[t]} : Config {t} : {d[t][cfg]}")

print(f"Total energy costs are: {lp_problem.objective.value()}")
print(f"Status: {lp_problem.status}")

Then I've tried to add a constraint which limits the change in production of the ASUs to 9000 for sequential timeslots. I've added the following variable to track the status of the ASU and constraints.

ASU_status = {}    # Binary variable to track whether an ASU is on or off
for t in d:
    ASU_status[t] = {asu: LpVariable(f"z_{t}_{asu}", 0, 1, LpBinary) 
                            for asu in set_ASU}
    
for t in set_T:
    for cfg in d[t]:
        for asu in set_ASU:
            lp_problem += ASU_bin[t][asu] * M >= var[t][cfg]* d[t][cfg][0][asu]
        lp_problem += ASU_bin[t][asu] <= var[t][cfg]* d[t][cfg][0][asu]
          
        
for t in range(1,24):
    for cfg in d[t]:
        for p_cfg in d[t-1]:
            for asu in set_ASU:
                ASU_change = var[t][cfg] * d[t][cfg][0][asu] - var[t-1][p_cfg] * d[t-1][p_cfg][0][asu]
                lp_problem += ASU_change  <= ASU_status[t][asu] * 9000 + \
                                             (2 - ASU_status[t-1][asu] - ASU_status[t][asu])* M
                lp_problem += ASU_change >= - ASU_status[t][asu] * 9000 - \
                                              (2 - ASU_status[t-1][asu]- ASU_status[t][asu])* M

Part of config dictionary {config: [(ASU config), E-cons, Injection or extraction value]:

{0: [(0, 0, 0), 0.0, 0],
 1: [(0, 0, 0), 0.0, -10000],
 2: [(0, 0, 0), 0.0, -15000],
 3: [(0, 0, 0), 0.0, -20000],
 4: [(0, 0, 0), 0.0, -25000],
 5: [(0, 0, 0), 0.0, -30000],
 6: [(0, 0, 0), 0.0, -35000],
 7: [(0, 0, 0), 0.0, -40000],
 8: [(0, 0, 0), 0.0, -45000],
 9: [(0, 0, 0), 0.0, -50000],
 10: [(0, 0, 0), 0.0, -55000],
 11: [(0, 0, 0), 0.0, -60000],
 12: [(0, 0, 0), 0.0, -65000],
 13: [(0, 0, 0), 0.0, -70000],
 14: [(0, 0, 0), 0.0, -75000],
 15: [(0, 0, 0), 0.0, -80000],
 16: [(0, 0, 0), 0.0, -85000],
 17: [(0, 0, 0), 0.0, -90000],
 18: [(0, 0, 0), 0.0, -95000],
 19: [(0, 0, 0), 0.0, -100000],
 20: [(0, 0, 0), 0.0, -105000],
 21: [(0, 0, 0), 0.0, -110000],
 22: [(0, 0, 0), 0.0, -115000],
 23: [(0, 0, 0), 0.0, -120000],
 24: [(0, 0, 0), 0.0, -125000],
 25: [(0, 0, 0), 0.0, -130000],
 26: [(0, 0, 0), 0.0, -135000],
 27: [(0, 0, 0), 0.0, -140000],
 28: [(0, 0, 0), 0.0, -145000],
 29: [(0, 0, 0), 0.0, -150000],
 30: [(0, 0, 0), 0.0, -155000],
 31: [(0, 0, 0), 0.0, -160000],
 32: [(0, 0, 0), 0.0, -165000],
 33: [(0, 0, 0), 0.0, -170000],
 34: [(0, 0, 0), 0.0, -175000],
 35: [(0, 0, 0), 0.0, -180000],
 36: [(0, 0, 0), 0.0, -185000],
 37: [(0, 0, 0), 0.0, -190000],
 38: [(0, 0, 42000), 10.815, 0],
 39: [(0, 0, 42000), 10.815, -10000],
 40: [(0, 0, 42000), 10.815, -15000],
 41: [(0, 0, 42000), 10.815, -20000],
 42: [(0, 0, 42000), 10.815, -25000],
 43: [(0, 0, 42000), 10.815, -30000],
 44: [(0, 0, 42000), 10.815, -35000],
 45: [(0, 0, 42000), 10.815, -40000],
 46: [(0, 0, 42000), 10.815, -45000],
 47: [(0, 0, 42000), 10.815, -50000],
 48: [(0, 0, 42000), 10.815, -55000],
 49: [(0, 0, 42000), 10.815, -60000],
 50: [(0, 0, 42000), 10.815, -65000],
 51: [(0, 0, 42000), 10.815, -70000],
 52: [(0, 0, 42000), 10.815, -75000],
 53: [(0, 0, 42000), 10.815, -80000],
 54: [(0, 0, 42000), 10.815, -85000],
 55: [(0, 0, 42000), 10.815, -90000],
 56: [(0, 0, 42000), 10.815, -95000],
 57: [(0, 0, 42000), 10.815, -100000],
 58: [(0, 0, 42000), 10.815, -105000],
 59: [(0, 0, 42000), 10.815, -110000],
 60: [(0, 0, 42000), 10.815, -115000],
 61: [(0, 0, 42000), 10.815, -120000],
 62: [(0, 0, 42000), 10.815, -125000],
 63: [(0, 0, 42000), 10.815, -130000],
 64: [(0, 0, 42000), 10.815, -135000],
 65: [(0, 0, 42000), 10.815, -140000],
 66: [(0, 0, 42000), 10.815, -145000],
 67: [(0, 0, 44000), 11.293, 0],
 68: [(0, 0, 44000), 11.293, -10000],
 69: [(0, 0, 44000), 11.293, -15000],
 70: [(0, 0, 44000), 11.293, -20000],
 71: [(0, 0, 44000), 11.293, -25000],
 72: [(0, 0, 44000), 11.293, -30000],
 73: [(0, 0, 44000), 11.293, -35000],
 74: [(0, 0, 44000), 11.293, -40000],
 75: [(0, 0, 44000), 11.293, -45000],
 76: [(0, 0, 44000), 11.293, -50000],
 77: [(0, 0, 44000), 11.293, -55000],
 78: [(0, 0, 44000), 11.293, -60000],
 79: [(0, 0, 44000), 11.293, -65000],
 80: [(0, 0, 44000), 11.293, -70000],
 81: [(0, 0, 44000), 11.293, -75000],
 82: [(0, 0, 44000), 11.293, -80000],
 83: [(0, 0, 44000), 11.293, -85000],
 84: [(0, 0, 44000), 11.293, -90000],
 85: [(0, 0, 44000), 11.293, -95000],
 86: [(0, 0, 44000), 11.293, -100000],
 87: [(0, 0, 44000), 11.293, -105000],
 88: [(0, 0, 44000), 11.293, -110000],
 89: [(0, 0, 44000), 11.293, -115000],
 90: [(0, 0, 44000), 11.293, -120000],
 91: [(0, 0, 44000), 11.293, -125000],
 92: [(0, 0, 44000), 11.293, -130000],
 93: [(0, 0, 44000), 11.293, -135000],
 94: [(0, 0, 44000), 11.293, -140000],
 95: [(0, 0, 44000), 11.293, -145000],
 96: [(0, 0, 46000), 11.772, 0],
 97: [(0, 0, 46000), 11.772, -10000],
 98: [(0, 0, 46000), 11.772, -15000],
 99: [(0, 0, 46000), 11.772, -20000],
 100: [(0, 0, 46000), 11.772, -25000],
 101: [(0, 0, 46000), 11.772, -30000],
 102: [(0, 0, 46000), 11.772, -35000],
 103: [(0, 0, 46000), 11.772, -40000],
 104: [(0, 0, 46000), 11.772, -45000],
 105: [(0, 0, 46000), 11.772, -50000],
 106: [(0, 0, 46000), 11.772, -55000],
 107: [(0, 0, 46000), 11.772, -60000],
 108: [(0, 0, 46000), 11.772, -65000],
 109: [(0, 0, 46000), 11.772, -70000],
 110: [(0, 0, 46000), 11.772, -75000],
 111: [(0, 0, 46000), 11.772, -80000],
 112: [(0, 0, 46000), 11.772, -85000],
 113: [(0, 0, 46000), 11.772, -90000],
 114: [(0, 0, 46000), 11.772, -95000],
 115: [(0, 0, 46000), 11.772, -100000],
 116: [(0, 0, 46000), 11.772, -105000],
 117: [(0, 0, 46000), 11.772, -110000],
 118: [(0, 0, 46000), 11.772, -115000],
 119: [(0, 0, 46000), 11.772, -120000],
 120: [(0, 0, 46000), 11.772, -125000],
 121: [(0, 0, 46000), 11.772, -130000],
 122: [(0, 0, 46000), 11.772, -135000],
 123: [(0, 0, 46000), 11.772, -140000],
 124: [(0, 0, 48000), 12.25, 0],
 125: [(0, 0, 48000), 12.25, -10000],
 126: [(0, 0, 48000), 12.25, -15000],
 127: [(0, 0, 48000), 12.25, -20000],
 128: [(0, 0, 48000), 12.25, -25000],
 129: [(0, 0, 48000), 12.25, -30000],
 130: [(0, 0, 48000), 12.25, -35000],
 131: [(0, 0, 48000), 12.25, -40000],
 132: [(0, 0, 48000), 12.25, -45000],
 133: [(0, 0, 48000), 12.25, -50000],
 134: [(0, 0, 48000), 12.25, -55000],
 135: [(0, 0, 48000), 12.25, -60000],
 136: [(0, 0, 48000), 12.25, -65000],
 137: [(0, 0, 48000), 12.25, -70000],
 138: [(0, 0, 48000), 12.25, -75000],
 139: [(0, 0, 48000), 12.25, -80000],
 140: [(0, 0, 48000), 12.25, -85000],
 141: [(0, 0, 48000), 12.25, -90000],
 142: [(0, 0, 48000), 12.25, -95000],
 143: [(0, 0, 48000), 12.25, -100000],
 144: [(0, 0, 48000), 12.25, -105000],
 145: [(0, 0, 48000), 12.25, -110000],
 146: [(0, 0, 48000), 12.25, -115000],
 147: [(0, 0, 48000), 12.25, -120000],
 148: [(0, 0, 48000), 12.25, -125000],
 149: [(0, 0, 48000), 12.25, -130000],
 150: [(0, 0, 48000), 12.25, -135000],
 151: [(0, 0, 48000), 12.25, -140000],
 152: [(0, 0, 50000), 12.728, 0],
 153: [(0, 0, 50000), 12.728, -10000],
 154: [(0, 0, 50000), 12.728, -15000],
 155: [(0, 0, 50000), 12.728, -20000],
 156: [(0, 0, 50000), 12.728, -25000],
 157: [(0, 0, 50000), 12.728, -30000],
 158: [(0, 0, 50000), 12.728, -35000],
 159: [(0, 0, 50000), 12.728, -40000],
 160: [(0, 0, 50000), 12.728, -45000],
 161: [(0, 0, 50000), 12.728, -50000],
 162: [(0, 0, 50000), 12.728, -55000],
 163: [(0, 0, 50000), 12.728, -60000],
 164: [(0, 0, 50000), 12.728, -65000],
 165: [(0, 0, 50000), 12.728, -70000],
 166: [(0, 0, 50000), 12.728, -75000],
 167: [(0, 0, 50000), 12.728, -80000],
 168: [(0, 0, 50000), 12.728, -85000],
 169: [(0, 0, 50000), 12.728, -90000],
 170: [(0, 0, 50000), 12.728, -95000],
 171: [(0, 0, 50000), 12.728, -100000],
 172: [(0, 0, 50000), 12.728, -105000],
 173: [(0, 0, 50000), 12.728, -110000],
 174: [(0, 0, 50000), 12.728, -115000],
 175: [(0, 0, 50000), 12.728, -120000],
 176: [(0, 0, 50000), 12.728, -125000],
 177: [(0, 0, 50000), 12.728, -130000],
 178: [(0, 0, 50000), 12.728, -135000],
 179: [(0, 0, 50000), 12.728, -140000],
 180: [(0, 0, 52000), 13.207, 0],
 181: [(0, 0, 52000), 13.207, -10000],
 182: [(0, 0, 52000), 13.207, -15000],
 183: [(0, 0, 52000), 13.207, -20000],
 184: [(0, 0, 52000), 13.207, -25000],
 185: [(0, 0, 52000), 13.207, -30000],
 186: [(0, 0, 52000), 13.207, -35000],
 187: [(0, 0, 52000), 13.207, -40000],
 188: [(0, 0, 52000), 13.207, -45000],
 189: [(0, 0, 52000), 13.207, -50000],
 190: [(0, 0, 52000), 13.207, -55000],
 191: [(0, 0, 52000), 13.207, -60000],
 192: [(0, 0, 52000), 13.207, -65000],
 193: [(0, 0, 52000), 13.207, -70000],
 194: [(0, 0, 52000), 13.207, -75000],
 195: [(0, 0, 52000), 13.207, -80000],
 196: [(0, 0, 52000), 13.207, -85000],
 197: [(0, 0, 52000), 13.207, -90000],
 198: [(0, 0, 52000), 13.207, -95000],
 199: [(0, 0, 52000), 13.207, -100000],
 200: [(0, 0, 52000), 13.207, -105000],
 201: [(0, 0, 52000), 13.207, -110000],
 202: [(0, 0, 52000), 13.207, -115000],
 203: [(0, 0, 52000), 13.207, -120000],
 204: [(0, 0, 52000), 13.207, -125000],
 205: [(0, 0, 52000), 13.207, -130000],
 206: [(0, 0, 52000), 13.207, -135000],
 207: [(0, 0, 54000), 13.685, 0],
 208: [(0, 0, 54000), 13.685, -10000],
 209: [(0, 0, 54000), 13.685, -15000],
 210: [(0, 0, 54000), 13.685, -20000],
 211: [(0, 0, 54000), 13.685, -25000],
 212: [(0, 0, 54000), 13.685, -30000],
 213: [(0, 0, 54000), 13.685, -35000],
 214: [(0, 0, 54000), 13.685, -40000],
 215: [(0, 0, 54000), 13.685, -45000],
 216: [(0, 0, 54000), 13.685, -50000],
 217: [(0, 0, 54000), 13.685, -55000],
 218: [(0, 0, 54000), 13.685, -60000],
 219: [(0, 0, 54000), 13.685, -65000],
 220: [(0, 0, 54000), 13.685, -70000],
 221: [(0, 0, 54000), 13.685, -75000],
 222: [(0, 0, 54000), 13.685, -80000],
 223: [(0, 0, 54000), 13.685, -85000],
 224: [(0, 0, 54000), 13.685, -90000],
 225: [(0, 0, 54000), 13.685, -95000],
 226: [(0, 0, 54000), 13.685, -100000],
 227: [(0, 0, 54000), 13.685, -105000],
 228: [(0, 0, 54000), 13.685, -110000],
 229: [(0, 0, 54000), 13.685, -115000],
 230: [(0, 0, 54000), 13.685, -120000],
 231: [(0, 0, 54000), 13.685, -125000],
 232: [(0, 0, 54000), 13.685, -130000],
 233: [(0, 0, 54000), 13.685, -135000],
 234: [(0, 0, 56000), 14.163, 0],
 235: [(0, 0, 56000), 14.163, -10000],
 236: [(0, 0, 56000), 14.163, -15000],
 237: [(0, 0, 56000), 14.163, -20000],
 238: [(0, 0, 56000), 14.163, -25000],
 239: [(0, 0, 56000), 14.163, -30000],
 240: [(0, 0, 56000), 14.163, -35000],
 241: [(0, 0, 56000), 14.163, -40000],
 242: [(0, 0, 56000), 14.163, -45000],
 243: [(0, 0, 56000), 14.163, -50000],
 244: [(0, 0, 56000), 14.163, -55000],
 245: [(0, 0, 56000), 14.163, -60000],
 246: [(0, 0, 56000), 14.163, -65000],
 247: [(0, 0, 56000), 14.163, -70000],
 248: [(0, 0, 56000), 14.163, -75000],
 249: [(0, 0, 56000), 14.163, -80000],
 250: [(0, 0, 56000), 14.163, -85000],
 251: [(0, 0, 56000), 14.163, -90000],
 252: [(0, 0, 56000), 14.163, -95000],
 253: [(0, 0, 56000), 14.163, -100000],
 254: [(0, 0, 56000), 14.163, -105000],
 255: [(0, 0, 56000), 14.163, -110000],
 256: [(0, 0, 56000), 14.163, -115000],
 257: [(0, 0, 56000), 14.163, -120000],
 258: [(0, 0, 56000), 14.163, -125000],
 259: [(0, 0, 56000), 14.163, -130000],
 260: [(0, 0, 58000), 14.642, 0],
 261: [(0, 0, 58000), 14.642, -10000],
 262: [(0, 0, 58000), 14.642, -15000],
 263: [(0, 0, 58000), 14.642, -20000],
 264: [(0, 0, 58000), 14.642, -25000],
 265: [(0, 0, 58000), 14.642, -30000],
 266: [(0, 0, 58000), 14.642, -35000],
 267: [(0, 0, 58000), 14.642, -40000],
 268: [(0, 0, 58000), 14.642, -45000],
 269: [(0, 0, 58000), 14.642, -50000],
 270: [(0, 0, 58000), 14.642, -55000],
 271: [(0, 0, 58000), 14.642, -60000],
 272: [(0, 0, 58000), 14.642, -65000],
 273: [(0, 0, 58000), 14.642, -70000],
 274: [(0, 0, 58000), 14.642, -75000],
 275: [(0, 0, 58000), 14.642, -80000],
 276: [(0, 0, 58000), 14.642, -85000],
 277: [(0, 0, 58000), 14.642, -90000],
 278: [(0, 0, 58000), 14.642, -95000],
 279: [(0, 0, 58000), 14.642, -100000],
 280: [(0, 0, 58000), 14.642, -105000],
 281: [(0, 0, 58000), 14.642, -110000],
 282: [(0, 0, 58000), 14.642, -115000],
 283: [(0, 0, 58000), 14.642, -120000],
 284: [(0, 0, 58000), 14.642, -125000],
 285: [(0, 0, 58000), 14.642, -130000],
 286: [(0, 0, 58000), 18.212, 58000],
 287: [(0, 0, 60000), 15.12, 0],
 288: [(0, 0, 60000), 15.12, -10000],
 289: [(0, 0, 60000), 15.12, -15000],
 290: [(0, 0, 60000), 15.12, -20000],
 291: [(0, 0, 60000), 15.12, -25000],
 292: [(0, 0, 60000), 15.12, -30000],
 293: [(0, 0, 60000), 15.12, -35000],
 294: [(0, 0, 60000), 15.12, -40000],
 295: [(0, 0, 60000), 15.12, -45000],
 296: [(0, 0, 60000), 15.12, -50000],
 297: [(0, 0, 60000), 15.12, -55000],
 298: [(0, 0, 60000), 15.12, -60000],
 299: [(0, 0, 60000), 15.12, -65000],
 300: [(0, 0, 60000), 15.12, -70000],
 301: [(0, 0, 60000), 15.12, -75000],
 302: [(0, 0, 60000), 15.12, -80000],
 303: [(0, 0, 60000), 15.12, -85000],
 304: [(0, 0, 60000), 15.12, -90000],
 305: [(0, 0, 60000), 15.12, -95000],
 306: [(0, 0, 60000), 15.12, -100000],
 307: [(0, 0, 60000), 15.12, -105000],
 308: [(0, 0, 60000), 15.12, -110000],
 309: [(0, 0, 60000), 15.12, -115000],
 310: [(0, 0, 60000), 15.12, -120000],
 311: [(0, 0, 60000), 15.12, -125000],
 312: [(0, 0, 60000), 15.12, -130000],
 313: [(0, 0, 60000), 18.69, 58000],
 314: [(0, 0, 60000), 18.87, 60000],
 315: [(0, 42000, 42000), 21.63, 0],
 316: [(0, 42000, 42000), 21.63, -10000],
 317: [(0, 42000, 42000), 21.63, -15000],
 318: [(0, 42000, 42000), 21.63, -20000],
 319: [(0, 42000, 42000), 21.63, -25000],
 320: [(0, 42000, 42000), 21.63, -30000],
 321: [(0, 42000, 42000), 21.63, -35000],
 322: [(0, 42000, 42000), 21.63, -40000],
 323: [(0, 42000, 42000), 21.63, -45000],
 324: [(0, 42000, 42000), 21.63, -50000],
 325: [(0, 42000, 42000), 21.63, -55000],
 326: [(0, 42000, 42000), 21.63, -60000],
 327: [(0, 42000, 42000), 21.63, -65000],
 328: [(0, 42000, 42000), 21.63, -70000],
 329: [(0, 42000, 42000), 21.63, -75000],
 330: [(0, 42000, 42000), 21.63, -80000],
 331: [(0, 42000, 42000), 21.63, -85000],
 332: [(0, 42000, 42000), 21.63, -90000],
 333: [(0, 42000, 42000), 21.63, -95000],
 334: [(0, 42000, 42000), 21.63, -100000],
 335: [(0, 42000, 42000), 21.63, -105000],
 336: [(0, 42000, 42000), 25.2, 58000],
 337: [(0, 42000, 42000), 25.38, 60000],
 338: [(0, 42000, 42000), 25.56, 65000],
 339: [(0, 42000, 42000), 25.74, 70000],
 340: [(0, 42000, 42000), 25.92, 75000],
 341: [(0, 42000, 42000), 26.1, 80000],
 342: [(0, 42000, 44000), 22.108, 0],
 343: [(0, 42000, 44000), 22.108, -10000],
 344: [(0, 42000, 44000), 22.108, -15000],
 345: [(0, 42000, 44000), 22.108, -20000],
 346: [(0, 42000, 44000), 22.108, -25000],
 347: [(0, 42000, 44000), 22.108, -30000],
 348: [(0, 42000, 44000), 22.108, -35000],
 349: [(0, 42000, 44000), 22.108, -40000],
 350: [(0, 42000, 44000), 22.108, -45000],
 351: [(0, 42000, 44000), 22.108, -50000],
 352: [(0, 42000, 44000), 22.108, -55000],
 353: [(0, 42000, 44000), 22.108, -60000],
 354: [(0, 42000, 44000), 22.108, -65000],
 355: [(0, 42000, 44000), 22.108, -70000],
 356: [(0, 42000, 44000), 22.108, -75000],
 357: [(0, 42000, 44000), 22.108, -80000],
 358: [(0, 42000, 44000), 22.108, -85000],
 359: [(0, 42000, 44000), 22.108, -90000],
 360: [(0, 42000, 44000), 22.108, -95000],
 361: [(0, 42000, 44000), 22.108, -100000],
 362: [(0, 42000, 44000), 25.678, 58000],
 363: [(0, 42000, 44000), 25.858, 60000],
 364: [(0, 42000, 44000), 26.038, 65000],
 365: [(0, 42000, 44000), 26.218, 70000],
 366: [(0, 42000, 44000), 26.398, 75000],
 367: [(0, 42000, 44000), 26.578, 80000],
 368: [(0, 42000, 44000), 26.758, 85000],
 369: [(0, 44000, 44000), 22.586, 0],
 370: [(0, 44000, 44000), 22.586, -10000],
 371: [(0, 44000, 44000), 22.586, -15000],
 372: [(0, 44000, 44000), 22.586, -20000],
 373: [(0, 44000, 44000), 22.586, -25000],
 374: [(0, 44000, 44000), 22.586, -30000],
 375: [(0, 44000, 44000), 22.586, -35000],
 376: [(0, 44000, 44000), 22.586, -40000],
 377: [(0, 44000, 44000), 22.586, -45000],
 378: [(0, 44000, 44000), 22.586, -50000],
 379: [(0, 44000, 44000), 22.586, -55000],
 380: [(0, 44000, 44000), 22.586, -60000],
 381: [(0, 44000, 44000), 22.586, -65000],
 382: [(0, 44000, 44000), 22.586, -70000],
 383: [(0, 44000, 44000), 22.586, -75000],
 384: [(0, 44000, 44000), 22.586, -80000],
 385: [(0, 44000, 44000), 22.586, -85000],
 386: [(0, 44000, 44000), 22.586, -90000],
 387: [(0, 44000, 44000), 22.586, -95000],
 388: [(0, 44000, 44000), 22.586, -100000],
 389: [(0, 44000, 44000), 26.156, 58000],
 390: [(0, 44000, 44000), 26.336, 60000],
 391: [(0, 44000, 44000), 26.516, 65000],
 392: [(0, 44000, 44000), 26.696, 70000],
 393: [(0, 44000, 44000), 26.876, 75000],
 394: [(0, 44000, 44000), 27.056, 80000],
 395: [(0, 44000, 44000), 27.236, 85000],
 396: [(0, 42000, 48000), 23.065, 0],
 397: [(0, 42000, 48000), 23.065, -10000],
 398: [(0, 42000, 48000), 23.065, -15000],
 399: [(0, 42000, 48000), 23.065, -20000],
 400: [(0, 42000, 48000), 23.065, -25000],
 401: [(0, 42000, 48000), 23.065, -30000],
 402: [(0, 42000, 48000), 23.065, -35000],
 403: [(0, 42000, 48000), 23.065, -40000],
 404: [(0, 42000, 48000), 23.065, -45000],
 405: [(0, 42000, 48000), 23.065, -50000],
 406: [(0, 42000, 48000), 23.065, -55000],
 407: [(0, 42000, 48000), 23.065, -60000],
 408: [(0, 42000, 48000), 23.065, -65000],
 409: [(0, 42000, 48000), 23.065, -70000],
 410: [(0, 42000, 48000), 23.065, -75000],
 411: [(0, 42000, 48000), 23.065, -80000],
 412: [(0, 42000, 48000), 23.065, -85000],
 413: [(0, 42000, 48000), 23.065, -90000],
 414: [(0, 42000, 48000), 23.065, -95000],
 415: [(0, 42000, 48000), 23.065, -100000],
 416: [(0, 42000, 48000), 26.635, 58000],
 417: [(0, 42000, 48000), 26.815, 60000],
 418: [(0, 42000, 48000), 26.995, 65000],
 419: [(0, 42000, 48000), 27.175, 70000],
 420: [(0, 42000, 48000), 27.355, 75000],
 421: [(0, 42000, 48000), 27.535, 80000],
 422: [(0, 42000, 48000), 27.715, 85000],
 423: [(0, 42000, 48000), 27.895, 90000],
 424: [(0, 42000, 50000), 23.543, 0],
 425: [(0, 42000, 50000), 23.543, -10000],
 426: [(0, 42000, 50000), 23.543, -15000],
 427: [(0, 42000, 50000), 23.543, -20000],
 428: [(0, 42000, 50000), 23.543, -25000],
 429: [(0, 42000, 50000), 23.543, -30000],
 430: [(0, 42000, 50000), 23.543, -35000],
 431: [(0, 42000, 50000), 23.543, -40000],
 432: [(0, 42000, 50000), 23.543, -45000],
 433: [(0, 42000, 50000), 23.543, -50000],
 434: [(0, 42000, 50000), 23.543, -55000],
 435: [(0, 42000, 50000), 23.543, -60000],
 436: [(0, 42000, 50000), 23.543, -65000],
 437: [(0, 42000, 50000), 23.543, -70000],
 438: [(0, 42000, 50000), 23.543, -75000],
 439: [(0, 42000, 50000), 23.543, -80000],
 440: [(0, 42000, 50000), 23.543, -85000],
 441: [(0, 42000, 50000), 23.543, -90000],
 442: [(0, 42000, 50000), 23.543, -95000],
 443: [(0, 42000, 50000), 27.113, 58000],
 444: [(0, 42000, 50000), 27.293, 60000],
 445: [(0, 42000, 50000), 27.473, 65000],
 446: [(0, 42000, 50000), 27.653, 70000],
 447: [(0, 42000, 50000), 27.833, 75000],
 448: [(0, 42000, 50000), 28.013, 80000],

 

The current code I have is this:

import pulp
import matplotlib.pyplot as plt
import pickle
import pandas as pd
import numpy as np


#----DATA--------------------------------------------------------------------------------

time_steps = list(range(0,24))
asus = [0, 1, 2]

#----Nitrogen demand forecast------------------------------------------------------------
n2_demand = [121548, 121453, 121537, 121715, 119228, 118547, 118675, 115909, 108003, 103060, 100284, 99211, 99915, 103157, 102453, 
            106371, 107764, 117624, 123072, 123492, 120911, 113903, 107971, 107243]
energy_prices = [130.12, 128.01, 121.34, 114.94, 119.04, 132.98, 172.34, 190.85, 190, 176.92, 160.10, 151.80, 145.06, 132.50, 129.71,
                 128.53, 132.50, 165.21, 177.48, 191.34, 182.87, 163.08, 141.30, 131.18]
#----Import of configuration dictinary---------------------------------------------------
with open('saved_ConfigDict.pkl', 'rb') as f:                                       # Dictionary containing all the configurations
    cfg_d = pickle.load(f)


prob = pulp.LpProblem("Nitrogen_Optimization", pulp.LpMinimize)     


t_c = [ (t, c) for t in time_steps for c in cfg_d ]                                         # Time-Configurations combinations

t_a = [ (t, a) for t in time_steps for a in asus ]                                          # Time-ASU combinations

tot_prod = {c:sum(vals[0]) for c, vals in cfg_d.items()}                                # Total production value of the 3 ASUs

#----Variables---------------------------------------------------------------------------
run_cfg = pulp.LpVariable.dicts('run', t_c, cat='Binary')                                   # Run configuration c in timeslot t
asu_output_increase = pulp.LpVariable.dicts('increase_asu', t_a, cat='Binary')              # ASU a changes status in timeslot t
ASU_on = {(t, asu): pulp.LpVariable(cat='Binary', name=f'ASU_on_{asu}_{t}')
            for asu in asus for t in time_steps}

#----Objective---------------------------------------------------------------------------

obj = pulp.lpSum(cfg_d[c][1] * run_cfg[t, c] * energy_prices[t] for t, c in t_c)
prob.setObjective(obj)

#----Constraints-------------------------------------------------------------------------

for t in time_steps:

    
    tot_n2 = pulp.lpSum(tot_prod[c] * run_cfg[t, c] - cfg_d[c][2] * run_cfg[t, c] for c in cfg_d)      # Constraint 1: Ensure demand is met in each
    prob += tot_n2 >= n2_demand[t]                                                      # timestep 


    prob += sum(run_cfg[t, c] for c in cfg_d.keys()) == 1                                   # Constraint 2: Exactly one config is run in 
                                                                                            # each timestep 
ramp_limit = 9000
                                                                                               

    
                                                                 

    # Constraint 3: Capture and limit change in ASUs
for t in time_steps[1:]:
    for asu in asus:  
        prod_this_step = pulp.lpSum(cfg_d[c][0][asu] * run_cfg[t, c] for c in cfg_d)
        prod_last_step = pulp.lpSum(cfg_d[c][0][asu] * run_cfg[t-1, c] for c in cfg_d)
        ASU_change = prod_this_step - prod_last_step
        prob += prod_this_step <= M * ASU_on[t, asu]
        prob += prod_this_step >= 0.001 * ASU_on[t, asu]
        prob += prod_last_step <= M * ASU_on[t-1, asu]
        prob += prod_last_step >= 0.001 * ASU_on[t-1, asu]
                  
        prob += ASU_change <= ramp_limit + (2 - ASU_on[t, asu] - ASU_on[t-1, asu]) * M
        prob += ASU_change >= -ramp_limit - (2 - ASU_on[t, asu] - ASU_on[t-1, asu]) * M

prob += pulp.lpSum(run_cfg[t, c] * cfg_d[c][2] for t in time_steps for c in cfg_d) == 0            # Constraint 4: Limit the utilization of the storage

#----Results-----------------------------------------------------------------------------
prob.solve()

for v in prob.variables():
    if v.varValue > 0:
        print(v.name, "=", v.varValue)

Solution

  • Some slight variation of the below example should do it. Without a slice of your data, it is tough to fit your current construct exactly, but this has all the elements that you are looking for.

    To more clearly state what I mentioned in the comment, you must be repeating config information many many times in your data dictionary, which is a horrible practice. I note in your example above that configuration 1318 is used many times. Unless the configuration itself changes over time, pull it out.

    Anyhow, the below uses a binary variable to capture and limit change by ASU and (as seen in graphic) walks down and up through lesser production configurations to meet the spikes at start and end.

    CODE:

    # Nitrogen Production
    
    import pulp
    import matplotlib.pyplot as plt
    
    # DATA
    configs = { 'cfg-201' : (100, 0,   100),
                'cfg-73'  : (140, 40,  110),
                'cfg-108' : (50,  60,  60),
                'cfg-0'   : (0,   0,   0)}
    
    tot_prod = {cfg:sum(vals) for cfg, vals in configs.items()}  # a convenience...
    
    change_limit = 60  # max output change per ASU
    
    demand = {  1: 210,
                2: 0,
                3: 0,
                4: 0,
                5: 0,
                6: 0,
                7: 275}
    
    time_steps = list(demand.keys())
    
    # make set of all time-config combos.
    # note:  if there are restrictions to which combos are avail in particular times,
    #        you could just make this as a dictionary from data
    t_c = [ (t, c) for t in time_steps for c in configs ]
    
    asus = [0, 1, 2]
    t_a = [ (t, a) for t in time_steps for a in asus ]
    
    prob = pulp.LpProblem("gas_production", pulp.LpMinimize)
    
    # VARS
    run_config = pulp.LpVariable.dicts('run', t_c, cat='Binary')            # run config c in time t
    asu_output_increase = pulp.LpVariable.dicts('increase_asu', t_a, cat='Binary')  # asu a changes status in timeslot t
    
    # OBJ: minimize total production
    prob += pulp.lpSum(tot_prod[c] * run_config[t, c] for t, c in t_c)
    
    # CONSTRAINTS
    
    for t in time_steps:
    
        # 1. meet demand in all timesteps
        prob += pulp.lpSum(tot_prod[c] * run_config[t, c] for c in configs) >= demand[t]
    
        # 2. must run one of the configs (and only one) in each timestep
        prob += sum(run_config[t, c] for c in configs.keys()) == 1
    
    # constraints for 2nd step and beyond
    for t in time_steps[1:]:
    
        # 3. capture change in asu status, and limit it
        for asu in asus:
            prod_this_step = sum(configs[c][asu] * run_config[t, c] for c in configs)
            prod_last_step = sum(configs[c][asu] * run_config[t-1, c] for c in configs)
            prob += prod_this_step - prod_last_step <= asu_output_increase[t, asu] * change_limit
            prob += prod_last_step - prod_this_step <= (1 -  asu_output_increase[t, asu]) * change_limit
    
    
    prob.solve()
    # print(prob)
    for v in prob.variables():
        if v.varValue > 0:
            print(v.name, "=", v.varValue)
    
    plt.step(time_steps, [sum(tot_prod[c]*run_config[t, c].varValue for c in configs) for t in time_steps], where='mid', lw=3, label='supply')
    plt.bar(time_steps, [demand[t] for t in time_steps], color='gray', label='demand')
    plt.legend()
    

    OUTPUT

    Text output omitted... the status is "optimal"

    enter image description here