# How to use gurobi to plan production and minimize the times of changeovers

Solution for How to use gurobi to plan production and minimize the times of changeovers
is Given Below:

Suppose we have two lines: `L1` and `L2`.

Suppose we have 4 product types: `A1`, `A2`, `B1` and `B2`.

Suppose the line capacity for both of the lines are 24 hours.

Suppose the changeover cost is

``````                   to
A1  A2  B1  B2
A1   0   1   4   4
from   A2   1   0   4   4
B1   4   4   0   1
B2   4   4   1   0
``````

Suppose the daily demand is

``````'A1':14 hours,
'A2':10 hours,
'B1':12 hours,
'B2':12 hours,
``````

How could we use gurobi to come up with the best plan

``````L1: A1 ->cost 0-> A1 (14 hours) ->cost 1-> A2 (10 hours)
L2: A2 ->cost 4-> B1 (12 hours) ->cost 1-> B2 (12 hours)
Total changeover cost = 0 + 1 + 4 + 1 = 6
``````

As a comparison, a suboptimal plan could be

``````L1: A1 ->cost 0-> A1 (14 hours) ->cost 4-> B1 (10 hours)
L2: A2 ->cost 0-> A2 (10 hours) ->cost 4-> B2 (12 hours) ->cost 1-> B1(2 hours)
Total changeover cost = 0 + 4 + 0 + 4 + 1 = 9
``````

As a graphical illustration: My code does not work as expected:

``````##############################################################################
##################  Production Scheduling with changeovers  ##################
##############################################################################

import os
import time
START_TIME = time.time()
import numpy as np
import pandas as pd
import gurobipy as gp
from gurobipy import GRB, quicksum, max_, and_, or_
from pathlib import Path
from matplotlib import pyplot as plt
from pathlib import Path

###############################   Inputs   ###################################

PRODUCTS = set(['A1','A2','B1','B2'])

LINES = set(['L1', 'L2'])

LAST_PRODUCTION = {
'L1':'A1',
'L2':'A2',
}

DEMAND = {
'A1':14,
'A2':10,
'B1':12,
'B2':12,
}

#   CHANGEOVER_COST
#     A1  A2  B1  B2
# A1   0   1   4   4
# A2   1   0   4   4
# B1   4   4   0   1
# B2   4   4   1   0

CHANGEOVER_COST = {

('A1', 'A1'):0,
('A1', 'A2'):1,
('A1', 'B1'):4,
('A1', 'B2'):4,

('A2', 'A1'):1,
('A2', 'A2'):0,
('A2', 'B1'):4,
('A2', 'B2'):4,

('B1', 'A1'):4,
('B1', 'A2'):4,
('B1', 'B1'):0,
('B1', 'B2'):1,

('B2', 'A1'):4,
('B2', 'A2'):4,
('B2', 'B1'):1,
('B2', 'B2'):0

}

LINE_CAPACITY = 24

###############################   Model   ###################################

model = gp.Model('production_scheduling_with_changeover')

flags = model.addVars(LINES, PRODUCTS, vtype = GRB.BINARY)

paths = model.addVars(LINES, PRODUCTS, PRODUCTS, vtype = GRB.BINARY)

# 1. Meet demand
(
sum(hours[line, product] for line in LINES) == DEMAND[product] for product in PRODUCTS
),
name="meet_demand_for_each_product"
)

# 2. Line capacity
(
sum(hours[line, product] for product in PRODUCTS) <= LINE_CAPACITY for line in LINES
),
name="line_capacity"
)

# 3. Constraints between flags and hours
for line in LINES:
for product in PRODUCTS:
0,
hours[line, product] == 0

)
1,
hours[line, product] >= 0
)

# 4. Path

# 1. sum == N
(
sum(paths[line, p1, p2]  for p1 in PRODUCTS for p2 in PRODUCTS) == sum(flags[line, product] for product in PRODUCTS) for line in LINES
),
name="total_paths"
)

# 2. no A -> B -> A
(
paths[line, p1, p2] +  paths[line, p2, p1] <= 1 for p1 in PRODUCTS for p2 in PRODUCTS for line in LINES if p1 != p2
),
name="no_A_to_B_to_A"
)

#3. Diagonal
# Non-last type

(
paths[line, p, p] == 0 for p in PRODUCTS  for line in LINES if p != LAST_PRODUCTION[line]
),
name="diagonal_0_for_non_last_type"
)

# Last type
(
paths[line, p, p] == flags[line, p] for p in PRODUCTS for line in LINES if p == LAST_PRODUCTION[line]
),
name="diagonal_for_last_type"
)

#4. Set 0 for non_production and non_last types

(
paths[line, p1, p2] <= flags[line, p1] for p1 in PRODUCTS for p2 in PRODUCTS for line in LINES if p1 != LAST_PRODUCTION[line] and p1 != p2
),
)

(
paths[line, p1, p2] <= flags[line, p2] for p1 in PRODUCTS for p2 in PRODUCTS for line in LINES if p2 != LAST_PRODUCTION[line] and p1 != p2
),
)

#6. Column sum <= 1

(
sum(paths[line, p1, p2] for p1 in PRODUCTS) <= 1 for p2 in PRODUCTS for line in LINES
),
name="column_sum_less_or_equal_to_1"
)

#7 Row sum
# Non last type
(
sum(paths[line, p1, p2] for p2 in PRODUCTS if p1 != p2) <= 1 for p1 in PRODUCTS  for line in LINES
),
name="row_sum_less_or_equal_to_1"
)

# Last type
(
sum(paths[line, LAST_PRODUCTION[line], p2] for p2 in PRODUCTS if LAST_PRODUCTION[line] != p2) == 0
for line in LINES if and_(sum(flags[line, p] for p in PRODUCTS if p != LAST_PRODUCTION[line]) ==0)
),
name="row_sum_less_or_equal_to_0"
)

(
sum(paths[line, LAST_PRODUCTION[line], p2] for p2 in PRODUCTS if LAST_PRODUCTION[line] != p2) == 1
for line in LINES if and_(sum(flags[line, p] for p in PRODUCTS if p != LAST_PRODUCTION[line]) >=1)
),
name="row_sum_less_or_equal_to_1"
)

# There has to be way out from the last type
(
sum(paths[line, p1, p2] for p2 in PRODUCTS) >= paths[line, LAST_PRODUCTION[line], p1] for p1 in PRODUCTS
for line in LINES if  p1 != LAST_PRODUCTION[line]
),
name="The 1st to product from last has to have its next type"
)

# Minimize the changeover cost
obj = sum(paths[line, p1, p2]*CHANGEOVER_COST[p1, p2] for p1 in PRODUCTS for p2 in PRODUCTS for line in LINES)

model.setObjective(obj, GRB.MINIMIZE)

model.setParam("MIPGap", 0.01)

model.optimize()

print('Time', time.time() - START_TIME)

#####################################################################################################
###############################   info extraction from model   ######################################
#####################################################################################################

SAVE_FOLDER = 'C:/daten/'

rows = LINES.copy()
columns = PRODUCTS.copy()

plan = pd.DataFrame(columns = columns, index = rows, data = 0)
indicator = pd.DataFrame(columns = columns, index = rows, data = 0)

plan.sort_index(inplace = True)
indicator.sort_index(inplace = True)

for line, product in hours.keys():
#print(line, product, hours[line, product].x)
if (abs(hours[line, product].x > 1e-6)):
plan.loc[line, product] = np.round(hours[line, product].x,1)
indicator.loc[line, product] = np.round(flags[line, product].x,1)

plan_transposed = plan.transpose().sort_index()
plan_transposed_2 = plan_transposed#plan_transposed[(plan_transposed.T != 0).any()]
plan_transposed_2.to_csv(SAVE_FOLDER + 'hours.csv')

indicator_transposed = indicator.transpose().sort_index()
indicator_transposed_2 = indicator_transposed[(indicator_transposed.T != 0).any()]
indicator_transposed_2.to_csv(SAVE_FOLDER + 'flags.csv')

# Line #1 Path
rows = PRODUCTS.copy()
columns = PRODUCTS.copy()
line1 = pd.DataFrame(columns = columns, index = rows, data = 0)
for line, p1, p2 in paths.keys():
if line == "L1":
line1.loc[p1, p2] = np.round(paths[line, p1, p2].x,1)
else:
pass
line1.sort_index()
line1.to_csv(SAVE_FOLDER + "L1.csv")

# Line #2 Path
rows = PRODUCTS.copy()
columns = PRODUCTS.copy()
line2 = pd.DataFrame(columns = columns, index = rows, data = 0)
for line, p1, p2 in paths.keys():
if line == "L2":
line2.loc[p1, p2] = np.round(paths[line, p1, p2].x,1)
else:
pass
line2.sort_index(inplace = True)
line2.to_csv(SAVE_FOLDER + "L2.csv")
``````