Column Generation for VRP

Column Generation for VRP#

Ref. https://www.brainpad.co.jp/doors/contents/01_tech_2020-10-23-000003/

import numpy as np
import matplotlib.pyplot as plt
import pulp
# Problem parameters
num_customers = 10

coordinates_depot = np.array([0.5, 0.5])
coordinates_customers = np.random.rand(num_customers, 2)
coordinates = np.vstack((coordinates_depot, coordinates_customers))

weights = np.random.randint(5, 15, num_customers)

vehicle_capacity = 40
plt.scatter(coordinates[:, 0], coordinates[:, 1], c='r')
plt.scatter(coordinates_depot[0], coordinates_depot[1], c='b')
for i in range(num_customers):
    plt.annotate(i, (coordinates[i, 0], coordinates[i, 1]))

plt.xlim(0, 1)
plt.ylim(0, 1)
plt.tight_layout()
plt.show()
../../_images/814db81c2ffb2268727d66a89680f517aa290d8734096bf23f9bab21f1874fd5.png
# calculate costs
costs = np.zeros((num_customers + 1, num_customers + 1))
for i in range(num_customers + 1):
    for j in range(num_customers + 1):
        if i == j:
            continue
        costs[i, j] = np.linalg.norm(coordinates[i] - coordinates[j])
        costs[j, i] = costs[i, j]
def create_init_routes() -> list[tuple[int]]:
    # Create a route for each customer
    return [(0, i, 0) for i in range(1, num_customers + 1)]
def calculate_route_cost(route: tuple[int]) -> float:
    return sum(costs[route[i], route[i + 1]] for i in range(len(route) - 1))
def solve_dual_problem(routes: list[tuple[int]], route_costs: dict[tuple[int], float]) -> list[float]:
    problem = pulp.LpProblem("DualProblem", pulp.LpMaximize)

    # Variables
    y = pulp.LpVariable.dicts("y", range(num_customers), lowBound=0, cat=pulp.LpContinuous)

    # Objective function
    problem += pulp.lpSum([y[i] for i in range(num_customers)])

    # Constraints
    for route in routes:
        problem += pulp.lpSum([y[i - 1] for i in route[1:-1]]) <= route_costs[route]

    status = problem.solve(pulp.PULP_CBC_CMD(msg=0, timeLimit=60, threads=8))
    if status != 1:
        raise Exception("Dual problem not solved")

    return [0] + [y[i].varValue for i in range(num_customers)]
def solve_pricing_problem(benefits: list[float], big_M: int = 100) -> tuple[int]:
    problem = pulp.LpProblem("PricingProblem", pulp.LpMinimize)

    # Variables
    z = pulp.LpVariable.dicts(
        "z",
        ((i, j) for i in range(num_customers + 1) for j in range(num_customers + 1)),
        lowBound=0,
        upBound=1,
        cat=pulp.LpInteger
    )

    u = pulp.LpVariable.dicts("u", range(num_customers), lowBound=0, upBound=num_customers, cat=pulp.LpInteger)

    # Objective function
    problem += pulp.lpSum([(costs[i][j] - benefits[j]) * z[i, j] for i in range(num_customers + 1) for j in range(num_customers + 1) if i != j])

    # Constraints
    problem += pulp.lpSum([z[0, j] for j in range(1, num_customers + 1)]) == 1
    problem += pulp.lpSum([z[i, 0] for i in range(1, num_customers + 1)]) == 1

    for i in range(1, num_customers + 1):
        problem += pulp.lpSum([z[i, j] for j in range(num_customers + 1)]) <= 1

    for j in range(1, num_customers + 1):
        problem += pulp.lpSum([z[i, j] for i in range(num_customers + 1)]) <= 1

    for i in range(1, num_customers + 1):
        problem += pulp.lpSum([z[i, j] for j in range(num_customers + 1) if i != j]) == pulp.lpSum([z[j, i] for j in range(num_customers + 1) if i != j])

    for i in range(1, num_customers + 1):
        for j in range(1, num_customers + 1):
            problem += u[i - 1] + num_customers * (1 - z[i, j]) <= u[j - 1]

    problem += pulp.lpSum([weights[j - 1] * z[i, j] for i in range(num_customers + 1) for j in range(1, num_customers + 1) if i != j]) <= vehicle_capacity

    status = problem.solve(pulp.PULP_CBC_CMD(msg=0, timeLimit=60, threads=8))

    if status != 1:
        raise Exception("Pricing problem not solved")

    routes = [0]
    while True:
        for j in range(num_customers + 1):
            if z[routes[-1], j].varValue == 1:
                routes.append(j)
                break
        if routes[-1] == 0:
            break

    return tuple(routes)
def solve_master_problem(routes: list[tuple[int]], route_costs: dict[tuple[int], float]) -> tuple[list[tuple[int]], float]:
    problem = pulp.LpProblem("MasterProblem", pulp.LpMinimize)

    num_routes = len(routes)

    # Variables
    x = pulp.LpVariable.dicts("x", range(num_routes), lowBound=0, upBound=1, cat=pulp.LpInteger)

    # Objective function
    problem += pulp.lpSum([route_costs[route] * x[r] for r, route in enumerate(routes)])

    # Constraints
    for i in range(1, num_customers + 1):
        problem += pulp.lpSum([x[r] for r, route in enumerate(routes) if i in route]) == 1

    status = problem.solve(pulp.PULP_CBC_CMD(msg=0, timeLimit=60, threads=8))
    if status != 1:
        raise Exception("Master problem not solved")

    routes_selected = [routes[r] for r in range(num_routes) if x[r].varValue == 1]
    objective_value = pulp.value(problem.objective)
    return routes_selected, objective_value
max_iterations = 500

routes_hist = []
objective_values_hist = []
diff_hist = []

routes = create_init_routes()
route_costs = {route: calculate_route_cost(route) for route in routes}

selected_routes, objective_value = solve_master_problem(routes, route_costs)
objective_values_hist.append(objective_value)
print(f"Initial routes: {selected_routes} with objective value {objective_value}")

for t in range(max_iterations):
    print(f"Iteration {t}")
    benefits = solve_dual_problem(routes, route_costs)
    route_next = solve_pricing_problem(benefits)

    route_cost_next = calculate_route_cost(route_next)
    route_costs[route_next] = route_cost_next

    diff = route_cost_next - sum(benefits[i] for i in route_next)
    diff_hist.append(diff)
    print(f"difference: {diff}")

    if abs(diff) <= 1e-6:
        print("Finished!!!")
        break

    routes.append(route_next)
    routes_hist.append(routes)
    print(f"Route added: {route_next} with cost {route_cost_next}")

    # Solve the master problem
    selected_routes, objective_value = solve_master_problem(routes, route_costs)
    objective_values_hist.append(objective_value)
    print(f"Currnet routes: {selected_routes} with objective value {objective_value}")

# Solve the master problem
selected_routes, objective_value = solve_master_problem(routes, route_costs)
objective_values_hist.append(objective_value)
print(f"Selected routes: {selected_routes} with objective value {objective_value}")
Initial routes: [(0, 1, 0), (0, 2, 0), (0, 3, 0), (0, 4, 0), (0, 5, 0), (0, 6, 0), (0, 7, 0), (0, 8, 0), (0, 9, 0), (0, 10, 0)] with objective value 7.319097017790974
Iteration 0
difference: -2.1555108729741654
Route added: (0, 4, 8, 1, 2, 10, 0) with cost 1.8969801670258342
Currnet routes: [(0, 3, 0), (0, 5, 0), (0, 6, 0), (0, 7, 0), (0, 9, 0), (0, 4, 8, 1, 2, 10, 0)] with objective value 5.163586104037494
Iteration 1
difference: -1.4247160143311586
Route added: (0, 2, 5, 9, 6, 1, 0) with cost 2.5546680256688417
Currnet routes: [(0, 3, 0), (0, 5, 0), (0, 6, 0), (0, 7, 0), (0, 9, 0), (0, 4, 8, 1, 2, 10, 0)] with objective value 5.163586104037494
Iteration 2
difference: -1.6657925620295282
Route added: (0, 8, 4, 6, 9, 0) with cost 1.268314407970472
Currnet routes: [(0, 3, 0), (0, 5, 0), (0, 6, 0), (0, 7, 0), (0, 9, 0), (0, 4, 8, 1, 2, 10, 0)] with objective value 5.163586104037494
Iteration 3
difference: -1.2332135484064335
Route added: (0, 10, 2, 5, 3, 0) with cost 1.6664960515935665
Currnet routes: [(0, 1, 0), (0, 7, 0), (0, 8, 4, 6, 9, 0), (0, 10, 2, 5, 3, 0)] with objective value 4.009072513590214
Iteration 4
difference: -1.256706234177583
Route added: (0, 1, 4, 6, 7, 0) with cost 1.032940695822417
Currnet routes: [(0, 1, 0), (0, 7, 0), (0, 8, 4, 6, 9, 0), (0, 10, 2, 5, 3, 0)] with objective value 4.009072513590214
Iteration 5
difference: -0.9845332708492998
Route added: (0, 1, 8, 7, 5, 0) with cost 1.8583023291507001
Currnet routes: [(0, 1, 0), (0, 7, 0), (0, 8, 4, 6, 9, 0), (0, 10, 2, 5, 3, 0)] with objective value 4.009072513590214
Iteration 6
difference: -0.9308022088808507
Route added: (0, 9, 7, 8, 1, 10, 0) with cost 2.097264451119149
Currnet routes: [(0, 1, 0), (0, 7, 0), (0, 8, 4, 6, 9, 0), (0, 10, 2, 5, 3, 0)] with objective value 4.009072513590214
Iteration 7
difference: -0.8007210938119678
Route added: (0, 9, 7, 8, 1, 2, 0) with cost 2.2569604261880323
Currnet routes: [(0, 1, 0), (0, 7, 0), (0, 8, 4, 6, 9, 0), (0, 10, 2, 5, 3, 0)] with objective value 4.009072513590214
Iteration 8
difference: -0.6516175022806403
Route added: (0, 3, 5, 9, 7, 0) with cost 1.5236574777193599
Currnet routes: [(0, 1, 0), (0, 7, 0), (0, 8, 4, 6, 9, 0), (0, 10, 2, 5, 3, 0)] with objective value 4.009072513590214
Iteration 9
difference: -0.5656560068211813
Route added: (0, 7, 6, 8, 1, 0) with cost 1.1204331131788186
Currnet routes: [(0, 1, 0), (0, 7, 0), (0, 8, 4, 6, 9, 0), (0, 10, 2, 5, 3, 0)] with objective value 4.009072513590214
Iteration 10
difference: -0.4185834067434757
Route added: (0, 3, 9, 7, 1, 0) with cost 1.2566806932565244
Currnet routes: [(0, 1, 0), (0, 7, 0), (0, 8, 4, 6, 9, 0), (0, 10, 2, 5, 3, 0)] with objective value 4.009072513590214
Iteration 11
difference: -0.5609949009806914
Route added: (0, 10, 2, 5, 9, 7, 0) with cost 2.218572749019309
Currnet routes: [(0, 1, 0), (0, 7, 0), (0, 8, 4, 6, 9, 0), (0, 10, 2, 5, 3, 0)] with objective value 4.009072513590214
Iteration 12
difference: -0.3802903672748388
Route added: (0, 5, 9, 4, 7, 1, 0) with cost 1.8549488807251613
Currnet routes: [(0, 1, 0), (0, 7, 0), (0, 8, 4, 6, 9, 0), (0, 10, 2, 5, 3, 0)] with objective value 4.009072513590214
Iteration 13
difference: -0.35336051806508184
Route added: (0, 7, 4, 8, 1, 0) with cost 0.9744812619349181
Currnet routes: [(0, 1, 0), (0, 7, 0), (0, 8, 4, 6, 9, 0), (0, 10, 2, 5, 3, 0)] with objective value 4.009072513590214
Iteration 14
difference: -0.2750820195347248
Route added: (0, 9, 5, 2, 10, 1, 0) with cost 2.348009460465275
Currnet routes: [(0, 1, 0), (0, 7, 0), (0, 8, 4, 6, 9, 0), (0, 10, 2, 5, 3, 0)] with objective value 4.009072513590214
Iteration 15
difference: -0.16664809537668557
Route added: (0, 9, 3, 5, 10, 0) with cost 1.9143395946233142
Currnet routes: [(0, 1, 0), (0, 7, 0), (0, 8, 4, 6, 9, 0), (0, 10, 2, 5, 3, 0)] with objective value 4.009072513590214
Iteration 16
difference: -0.17424509150569767
Route added: (0, 9, 3, 2, 10, 0) with cost 1.6663603684943022
Currnet routes: [(0, 1, 0), (0, 7, 0), (0, 8, 4, 6, 9, 0), (0, 10, 2, 5, 3, 0)] with objective value 4.009072513590214
Iteration 17
difference: -0.13837662208835622
Route added: (0, 9, 6, 4, 7, 0) with cost 1.0819441479116438
Currnet routes: [(0, 1, 0), (0, 7, 0), (0, 8, 4, 6, 9, 0), (0, 10, 2, 5, 3, 0)] with objective value 4.009072513590214
Iteration 18
difference: -0.18176051988195985
Route added: (0, 9, 6, 8, 1, 0) with cost 1.3435472301180402
Currnet routes: [(0, 1, 0), (0, 7, 0), (0, 8, 4, 6, 9, 0), (0, 10, 2, 5, 3, 0)] with objective value 4.009072513590214
Iteration 19
difference: -0.27031183853678153
Route added: (0, 1, 4, 9, 3, 0) with cost 1.3428079414632184
Currnet routes: [(0, 1, 0), (0, 7, 0), (0, 8, 4, 6, 9, 0), (0, 10, 2, 5, 3, 0)] with objective value 4.009072513590214
Iteration 20
difference: -0.21078662972214723
Route added: (0, 9, 6, 8, 7, 0) with cost 1.2635911502778527
Currnet routes: [(0, 1, 0), (0, 7, 0), (0, 8, 4, 6, 9, 0), (0, 10, 2, 5, 3, 0)] with objective value 4.009072513590214
Iteration 21
difference: -0.1286863395303648
Route added: (0, 2, 1, 4, 6, 9, 0) with cost 2.178647980469635
Currnet routes: [(0, 1, 0), (0, 7, 0), (0, 8, 4, 6, 9, 0), (0, 10, 2, 5, 3, 0)] with objective value 4.009072513590214
Iteration 22
difference: -0.1948363272383613
Route added: (0, 9, 6, 4, 1, 0) with cost 1.2560548127616387
Currnet routes: [(0, 1, 0), (0, 7, 0), (0, 8, 4, 6, 9, 0), (0, 10, 2, 5, 3, 0)] with objective value 4.009072513590214
Iteration 23
difference: -0.13671430459924805
Route added: (0, 9, 6, 4, 1, 10, 0) with cost 2.018952005400752
Currnet routes: [(0, 1, 0), (0, 7, 0), (0, 8, 4, 6, 9, 0), (0, 10, 2, 5, 3, 0)] with objective value 4.009072513590214
Iteration 24
difference: -0.09304578088139004
Route added: (0, 6, 4, 8, 1, 0) with cost 1.10917710911861
Currnet routes: [(0, 3, 0), (0, 10, 2, 5, 9, 7, 0), (0, 6, 4, 8, 1, 0)] with objective value 3.702534247292998
Iteration 25
difference: -0.07113789087480704
Route added: (0, 9, 3, 5, 2, 0) with cost 2.057386749125193
Currnet routes: [(0, 3, 0), (0, 10, 2, 5, 9, 7, 0), (0, 6, 4, 8, 1, 0)] with objective value 3.702534247292998
Iteration 26
difference: -0.10418994288579642
Route added: (0, 9, 5, 2, 10, 0) with cost 1.9385249331142038
Currnet routes: [(0, 3, 0), (0, 10, 2, 5, 9, 7, 0), (0, 6, 4, 8, 1, 0)] with objective value 3.702534247292998
Iteration 27
difference: -0.010418116070323613
Route added: (0, 10, 1, 7, 6, 9, 0) with cost 2.0197898939296763
Currnet routes: [(0, 3, 0), (0, 10, 2, 5, 9, 7, 0), (0, 6, 4, 8, 1, 0)] with objective value 3.702534247292998
Iteration 28
difference: -0.046431626343242716
Route added: (0, 9, 3, 0) with cost 0.7658265636567573
Currnet routes: [(0, 3, 0), (0, 10, 2, 5, 9, 7, 0), (0, 6, 4, 8, 1, 0)] with objective value 3.702534247292998
Iteration 29
difference: -0.0941546403570861
Route added: (0, 7, 4, 8, 1, 2, 0) with cost 1.897074429642914
Currnet routes: [(0, 3, 0), (0, 10, 2, 5, 9, 7, 0), (0, 6, 4, 8, 1, 0)] with objective value 3.702534247292998
Iteration 30
difference: -0.14200854818574515
Route added: (0, 9, 5, 3, 0) with cost 1.243609661814255
Currnet routes: [(0, 3, 0), (0, 10, 2, 5, 9, 7, 0), (0, 6, 4, 8, 1, 0)] with objective value 3.702534247292998
Iteration 31
difference: -8.065081935626495e-09
Finished!!!
Selected routes: [(0, 3, 0), (0, 10, 2, 5, 9, 7, 0), (0, 6, 4, 8, 1, 0)] with objective value 3.702534247292998
plt.plot(objective_values_hist, 'o--')
plt.xlabel("Iteration")
plt.ylabel("Objective value")
plt.show()
../../_images/591a08eab6ed14801196a794d4dab0cd91f8aa0fb1e7c4810f5bc5203e309b1f.png
plt.plot(diff_hist, 'o--')
plt.hlines(0, 0, len(diff_hist), colors='r', linestyles='dashed')

plt.xlabel("Iteration")
plt.ylabel("Difference")
plt.show()
../../_images/315504dfde83e48b34bb43b1eb92babb57585349e944055a70c5f546489e6804.png
# show the selected routes
plt.scatter(coordinates[:, 0], coordinates[:, 1], c='r')
plt.scatter(coordinates_depot[0], coordinates_depot[1], c='b')

for i in range(num_customers):
    plt.annotate(i, (coordinates[i, 0], coordinates[i, 1]))

colorlist = ["r", "g", "b", "c", "m", "y", "k", "w"]

for r, route in enumerate(selected_routes):
    plt.plot(
        [coordinates[i, 0] for i in route],
        [coordinates[i, 1] for i in route],
        c=colorlist[r % len(colorlist)]
    )

plt.xlim(0, 1)
plt.ylim(0, 1)
plt.tight_layout()
plt.show()
../../_images/e42bf744ca141f9cb6cfc010fc200562a39aa940a8aafe93a5861655368cc71c.png