I have create a capacity expansion model using PYOMO, where there are 2 main generators gen1 and gen2 and one backup generator lost_load. The logic is pretty simple, gen1 and gen2 will run to fulfill the demand profile and they will get the money for that and there is some negative cost associated with gen1 and gen2 capacity. Here is the code:
import datetime
import pandas as pd
import numpy as np
from pyomo.environ import *
model = ConcreteModel()
np.random.seed(24)
load_profile = np.random.randint(90, 120, 24)
model.m_index = Set(initialize=list(range(len(load_profile))))
model.grid = Var(model.m_index, domain=NonNegativeReals)
# Gen1 variable
model.gen1_cap = Var(domain=NonNegativeReals)
model.gen1_use = Var(model.m_index, domain=NonNegativeReals)
# Gen2 variables
model.gen2_cap = Var(domain=NonNegativeReals)
model.gen2_use = Var(model.m_index, domain=NonNegativeReals)
# Load profile
model.load_profile = Param(model.m_index, initialize=dict(zip(model.m_index, load_profile)))
model.lost_load = Var(model.m_index, domain=NonNegativeReals)
# Objective function
def revenue(model):
total_revenue = sum(
model.gen1_use[m] * 5.2 +
model.gen2_use[m] * 6.1 +
model.lost_load[m] * -100
for m in model.m_index)
total_fixed_cost = model.gen1_cap * -45 + model.gen2_cap * -50
total_cost = total_revenue + total_fixed_cost
return total_cost
model.obj = Objective(rule=revenue, sense=maximize)
# When i=0
def energy_balance1(model, m):
return model.grid[m] <= model.gen1_use[m] + model.gen2_use[m] + model.lost_load[m]
model.energy_balance1 = Constraint(model.m_index, rule=energy_balance1)
def grid_limit(model, m):
return model.grid[m] == model.load_profile[m]
model.grid_limit = Constraint(model.m_index, rule=grid_limit)
def max_gen1(model, m):
eq = model.gen1_use[m] <= model.gen1_cap
return eq
model.max_gen1 = Constraint(model.m_index, rule=max_gen1)
def max_gen2(model, m):
eq = model.gen2_use[m] <= model.gen2_cap
return eq
model.max_gen2 = Constraint(model.m_index, rule=max_gen2)
Solver = SolverFactory('gurobi')
Solver.options['LogFile'] = "gurobiLog"
# Solver.options['MIPGap'] = 0.50
print('\nConnecting to Gurobi Server...')
results = Solver.solve(model)
if (results.solver.status == SolverStatus.ok):
if (results.solver.termination_condition == TerminationCondition.optimal):
print("\n\n***Optimal solution found***")
print('obj returned:', round(value(model.obj), 2))
else:
print("\n\n***No optimal solution found***")
if (results.solver.termination_condition == TerminationCondition.infeasible):
print("Infeasible solution")
exit()
else:
print("\n\n***Solver terminated abnormally***")
exit()
grid_use = []
gen1 = []
gen2 = []
lost_load = []
load = []
for i in range(len(load_profile)):
grid_use.append(value(model.grid[i]))
gen1.append(value(model.gen1_use[i]))
gen2.append(value(model.gen2_use[i]))
lost_load.append(value(model.lost_load[i]))
load.append(value(model.load_profile[i]))
print('gen1 capacity: ', value(model.gen1_cap))
print('gen2 capacity: ', value(model.gen2_cap))
pd.DataFrame({
'Grid': grid_use,
'Gen1': gen1,
'Gen2': gen2,
'Shortfall': lost_load,
'Load': load
}).to_excel('capacity expansion.xlsx')
This model will work if :
def energy_balance1(model, m):
return model.grid[m] == model.gen1_use[m] + model.gen2_use[m] + model.lost_load[m]
model.energy_balance1 = Constraint(model.m_index, rule=energy_balance1)
But if I set it to (<=) greater than equal to, it gives me an infeasible error. I want this condition because in actuality I want gen1 and gen2 to run at 100% capacity at all 24 instances. After all, in the objective function, we can see the positive cost associated with gen1_use and gen2_use. So to maximize the revenue they should run at 100% capacity even if the load profile is fulfilled. Here is the output I am getting:
optimal gen1 capacity: 9MW
optimal gen2 capacity: 108MW


Try something like this to get started. I avoided making capacity a variable by including several distinct generator choices, which seems very reasonable.
This also incorporates a binary "use" variable, which I think you need from the described problem.
CODE:
Output (a bit long)