title: How to use gurobi to plan production and minimize the times of changeovers
body: <p>Suppose we have two lines: <code>L1</code> and <code>L2</code>.</p>
  <p>Suppose we have 4 product types: <code>A1</code>, <code>A2</code>, <code>B1</code> and <code>B2</code>.</p>
  <p>Suppose the line capacity for both of the lines are 24 hours.</p>
  <p>Suppose the changeover cost is</p>
  <pre><code>                   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
  </code></pre>
  <p>Suppose the daily demand is</p>
  <pre><code>'A1':14 hours,
  'A2':10 hours,
  'B1':12 hours,
  'B2':12 hours,
  </code></pre>
  <p>How could we use gurobi to come up with the best plan</p>
  <pre><code>L1: A1 -&gt;cost 0-&gt; A1 (14 hours) -&gt;cost 1-&gt; A2 (10 hours)
  L2: A2 -&gt;cost 4-&gt; B1 (12 hours) -&gt;cost 1-&gt; B2 (12 hours)
  Total changeover cost = 0 + 1 + 4 + 1 = 6
  </code></pre>
  <p>As a comparison, a suboptimal plan could be</p>
  <pre><code>L1: A1 -&gt;cost 0-&gt; A1 (14 hours) -&gt;cost 4-&gt; B1 (10 hours)
  L2: A2 -&gt;cost 0-&gt; A2 (10 hours) -&gt;cost 4-&gt; B2 (12 hours) -&gt;cost 1-&gt; B1(2 hours)
  Total changeover cost = 0 + 4 + 0 + 4 + 1 = 9
  </code></pre>
  <p>As a graphical illustration:</p>
  <p><a href="https://i.stack.imgur.com/VdPLE.png" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/VdPLE.png" alt="enter image description here" /></a></p>
  <p>My code does not work as expected:</p>
  <pre><code>##############################################################################
  ##################  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')
  
  hours = model.addVars(LINES, PRODUCTS)
  
  flags = model.addVars(LINES, PRODUCTS, vtype = GRB.BINARY)
  
  paths = model.addVars(LINES, PRODUCTS, PRODUCTS, vtype = GRB.BINARY)
  
  # 1. Meet demand
  meet_demand = model.addConstrs(
      (
       sum(hours[line, product] for line in LINES) == DEMAND[product] for product in PRODUCTS
      ),
      name = 'meet_demand_for_each_product'
  )
  
  # 2. Line capacity
  capacity = model.addConstrs(
      (
       sum(hours[line, product] for product in PRODUCTS) &lt;= LINE_CAPACITY for line in LINES
      ),
      name = 'line_capacity'
  )
  
  # 3. Constraints between flags and hours
  for line in LINES:
      for product in PRODUCTS:
          model.addGenConstrIndicator(flags[line, product],
                                    0,
                                    hours[line, product] == 0
                                    
                                    )
          model.addGenConstrIndicator(flags[line, product],
                                    1,
                                    hours[line, product] &gt;= 0
                                    )
  
  # 4. Path
  
  
  # 1. sum == N
  path_1 = model.addConstrs(
      (
      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 -&gt; B -&gt; A
  path_2 = model.addConstrs(
      (
      paths[line, p1, p2] +  paths[line, p2, p1] &lt;= 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
  
  path_3_1 = model.addConstrs(
      (
      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
  path_3_2 = model.addConstrs(
      (
      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
  
  path_4_1 = model.addConstrs(
      (
      paths[line, p1, p2] &lt;= flags[line, p1] for p1 in PRODUCTS for p2 in PRODUCTS for line in LINES if p1 != LAST_PRODUCTION[line] and p1 != p2
      ),
      name = 'shade_for_nonproduction_nonlast_types_row'
  )
  
  path_4_2 = model.addConstrs(
      (
      paths[line, p1, p2] &lt;= flags[line, p2] for p1 in PRODUCTS for p2 in PRODUCTS for line in LINES if p2 != LAST_PRODUCTION[line] and p1 != p2
      ),
      name = 'shade_for_nonproduction_nonlast_types_column'
  )
  
  
  
  #6. Column sum &lt;= 1
  
  path_6 = model.addConstrs(
      (
      sum(paths[line, p1, p2] for p1 in PRODUCTS) &lt;= 1 for p2 in PRODUCTS for line in LINES
      ),
      name = 'column_sum_less_or_equal_to_1'
  )
  
  
  
  #7 Row sum
    # Non last type
  path_7_1 = model.addConstrs(
      (
       sum(paths[line, p1, p2] for p2 in PRODUCTS if p1 != p2) &lt;= 1 for p1 in PRODUCTS  for line in LINES
      ),
      name = 'row_sum_less_or_equal_to_1'
  )
    
    # Last type    
  path_7_2_1 = model.addConstrs(
      (
        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'
  )
      
  
  path_7_2_2 = model.addConstrs(
      (
        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]) &gt;=1)
      ),
      name = 'row_sum_less_or_equal_to_1'
  )
  
  
  
  # There has to be way out from the last type 
  path_8 = model.addConstrs(
      (
          sum(paths[line, p1, p2] for p2 in PRODUCTS) &gt;= 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(&quot;MIPGap&quot;, 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 &gt; 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 == &quot;L1&quot;:
          line1.loc[p1, p2] = np.round(paths[line, p1, p2].x,1)
      else:
          pass
  line1.sort_index()
  line1.to_csv(SAVE_FOLDER + &quot;L1.csv&quot;)
  
  # 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 == &quot;L2&quot;:
          line2.loc[p1, p2] = np.round(paths[line, p1, p2].x,1)
      else:
          pass
  line2.sort_index(inplace = True)
  line2.to_csv(SAVE_FOLDER + &quot;L2.csv&quot;)
  </code></pre>