Why CVXPY

What is CVXPY

CVXPY is Python-based modelling language for convex optimization problems. While, usually writing an optimization prblem needs to be in restrictive standard form required by solvers, CVXPY lets the modeller to express an optimization problem in a natural way that follows the math. More than that, CVXPY supports mixed-integer convest programs. More importantluy, CVXPY allows the modeller to vectorize the model that fasten the preprocessing of the problem for the solver. Vectorized problems vs. scalar problems, avoids multiple nested for loops in the preprocessing which for large problems become extremely time consuming.

In this document, we tested the performance of CVXPY vs. Pyomo as the most popular Python optimization library for energy modellers. In order to test the performance in terms of time to build a unique optimization problem, we create a dummy multi node energy model with hourly time resolution and capacity expansion in Pyomo and CVXPY. We set the iteration limit of the solver equal to 1 to avoid the solver, solving the problem. As a result, we will test only the performance of the two libraries in terms of generating and loading the problem to the solver. Moreover, through this example, we show how defining a problem in CVXPY is close to mathematical language of the problem.

The data that are used for the test are available in github along with the jupyter notebook.

Pyomo Version

model = AbstractModel()

# Definition of sets
model.years = Set(initialize=lambda model: years)
model.hours = Set(initialize=lambda model: hours)
model.techs = Set(initialize=lambda model: techs)
model.regs = Set(initialize=lambda model: regions)

# Definition of Vars
model.production = Var(
    model.years, model.hours, model.techs, model.regs, domain=NonNegativeReals
)
model.new_capacity = Var(
    model.years, model.techs, model.regs, domain=NonNegativeReals
)
model.total_capacity = Var(
    model.years, model.techs, model.regs, domain=NonNegativeReals
)
# Definition of paramters
model.inv_cost = Param(
    model.years,
    model.techs,
    model.regs,
    initialize=lambda model, y, t, r: inv_cost.loc[y, t],
)
model.var_cost = Param(
    model.years,
    model.techs,
    model.regs,
    initialize=lambda model, y, t, r: var_cost.loc[y, t],
)

model.availability = Param(
    model.years,
    model.hours,
    model.techs,
    model.regs,
    initialize=lambda model, y, h, t, r: availability.loc[(y, h), t],
)
model.demand = Param(
    model.years,
    model.hours,
    model.regs,
    initialize=lambda model, y, h, r: demand.loc[(y, h), "demand"],
)

# Definition of rules
def tot_cap(model, y, t, r):
    if y == years[0]:
        return model.total_capacity[y, t, r] == model.new_capacity[y, t, r]
    return (
        model.total_capacity[y, t, r]
        == model.total_capacity[y - 1, t, r] + model.new_capacity[y, t, r]
    )

def demand_prod(model, y, h, r):
    return (
        sum(model.production[y, h, t, r] for t in model.techs)
        == model.demand[y, h, r]
    )

def availability_prod(model, y, h, t, r):
    return (
        model.production[y, h, t, r]
        <= model.total_capacity[y, t, r] * model.availability[y, h, t, r]
    )

def objective_function(model):

    tot_inv = sum(
        model.inv_cost[y, t, r] * model.new_capacity[y, t, r]
        for y in model.years
        for t in model.techs
        for r in model.regs
    )
    tot_var = sum(
        model.var_cost[y, t, r]
        * sum(model.production[y, h, t, r] for h in model.hours)
        for y in model.years
        for t in model.techs
        for r in model.regs
    )
    return tot_inv + tot_var

model.balance_rule = Constraint(
    model.years, model.hours, model.regs, rule=demand_prod
)
model.availability_rule = Constraint(
    model.years, model.hours, model.techs, model.regs, rule=availability_prod
)
model.capacity_rule = Constraint(model.years, model.techs, model.regs, rule=tot_cap)
model.OBJ = Objective(rule=objective_function, sense=minimize)

instance = model.create_instance()

opt = SolverFactory(solver)

CVXPY Version

constraints = []
tot_var_cost = 0
tot_inv_cost = 0

for rr in regions:
    # Creting variables
    production = cp.Variable(
        shape=(len(years) * len(hours), len(techs)),
        nonneg=True,
    )

    new_capacity = cp.Variable(shape=(len(years), len(techs)), nonneg=True)

    # meeting the demand
    constraints.append(
        cp.sum(production, axis=1) == demand.loc[years, "demand"].values
    )

    # total capacity -> cummulative sum of new capacity assuming
    # there is no disposed capacity
    if len(years) == 1:
        total_capacity = new_capacity
    else:
        total_capacity = cp.cumsum(new_capacity)

    # Production lower than the available capacity

    for yy, year in enumerate(years):
        constraints.append(
            production[yy * 8760 : (yy + 1) * 8760, :]
            <= cp.multiply(
                total_capacity[yy : yy + 1, :],
                availability.loc[(year, slice(None)), :].values,
            )
        )

        tot_var_cost += cp.sum(
            cp.multiply(
                cp.sum(production[yy * 8760 : (yy + 1) * 8760, :], axis=0),
                var_cost.loc[year, :].values,
            )
        )
    tot_inv_cost += cp.sum(cp.multiply(new_capacity, inv_cost.loc[years, :].values))

# Definition of objective function
objective = cp.Minimize(tot_var_cost + tot_inv_cost)

# Problem definition
problem = cp.Problem(objective=objective, constraints=constraints)
probblem.solve()

Results

Testing the two models for 2 nodes and changing the number of years, CVXPY always show a better performance in terms of time needed to generate the model. While for small problems, the difference is quiete negligible, for big problems, the difference is so considerable as shown in the figure. Moreover, writing an optimization problem in CVXPY is usually more transparent that can help energy modellers without professional coding skills understand the models and contribute to the development easier.

_images/test_speed.png