背景介绍

车间调度问题(Job Shop Scheduling Problem, JSSP)是一类经典的组合优化问题,它在制造业和生产管理中有着广泛的应用。JSSP 的目标是对车间中的一系列作业进行排程,以使得作业在不同机器上的加工顺序是最优的,达到某种特定的优化目标,比如最小化总的作业完成时间(即使得总完工时间尽可能短),或者最小化延迟时间、最大化产量等。车间调度问题通常被描述为一系列作业集合 J 和一系列机器集合 M,每个作业由一系列操作组成,每个操作需要在特定的机器上加工一定时间。作业调度的规则如下:
(1)每个作业的操作需要按照特定的顺序进行。
(2)同一时刻,一台机器只能加工一个操作。
(3)操作一旦开始,在完成之前不能中断。
(4)所有作业的开始加工时间不得早于零。
(5)同一时刻,一个工件只能在一台机器上进行加工

车间调度问题可以被更进一步分类,如:
(a)开放式车间调度(Open Shop Scheduling):作业的操作可以在任意顺序进行。
(b)流水式车间调度(Flow Shop Scheduling):所有作业的操作必须按照相同的顺序进行。
(c)柔性车间调度(Flexible Job Shop Scheduling):每个操作可以在多台不同但功能相似的机器上加工。

车间调度问题是 NP-难问题,这意味着没有已知的多项式时间的算法可以保证找到每个实例的最优解。因此,对于实际应用中的车间调度问题,通常会运用启发式算法和元启发式算法来寻求近似的最优解,比如遗传算法、粒子群优化、蚁群算法和禁忌搜索等。

车间调度问题的研究对于提高生产效率、降低成本、提高企业的市场竞争力等都具有重要意义。在实际应用中,调度方案通常还需要考虑各种现实限制,例如机器故障、交货期限、工人的工作时间等。因此,车间调度问题在研究和工业领域都是一个非常活跃和挑战性的领域。
在这里插入图片描述

问题简介

如下表所示为某车间调度问题数据,该问题工包含5个工件,每个工件4道工序,共有6台机器,其中工序1只能由机器1完成,工序2可由机器2或机器4完成,工序3可由机器3或者机器5完成,工序4只能由机器6完成,表中数据为各工件的各项工序在机器上加工所需的时间。
在这里插入图片描述
上述问题可以描述成一个简单的柔性车间调度问题,问题包含以下基本假设:
(1) 同一时刻同一台机器只能加工一个工件。
(2) 每个工件在某一时刻只能在一台机器上加工,不能中断操作。
(3) 同一工件有先后顺序之分,不同工件则没有。
(4) 不同的工件具有相同的优先级。
(5)T=0时刻是开始时刻,此时所有工件尚未开始加工。
(6)所有工件的所有工序都要完成。

问题的目标函数是最小化完工时间,我们需要通过算法手段决策工件的工序在哪台机器上执行以及机器的具体执行顺序,下图为上述问题工序的加工顺序图。
在这里插入图片描述

算法介绍

本文拟采用遗传算法对该问题进行求解,实际上只要定义了编码、解码以及适应度函数,基本所有启发算法都可以代入使用,方便起见,本文选择相对简单的遗传算法来对该问题进行求解,着重介绍下编码、解码、适应度的计算以及交叉和变异。方便起见,下文提到的所有索引都从0开始,机器0代表机器1,机器1代表机器2,以此类推。
编码方式
通常来说,遗传算法中每个个体代表一个完整的解决方案,对于车间调度问题而言,若要表达完整的解决方案,编码中最少应该包含每个机器的加工工件以及加工顺序两部分信息。简单起见,本文的编码方式如下,工包含两部分信息,其中order部分为各机器的加工顺序信息,machine为各工件每道工序选择的加工机器信息。

								{'order': [[2, 1, 4, 0, 3],
								  [4, 3, 1, 0, 2],
								  [0, 1, 4, 2, 3],
								  [1, 2, 0, 4, 3],
								  [2, 1, 4, 0, 3],
								  [1, 2, 4, 0, 3]],
								 'machine': [[0, 1, 4, 5],
								  [0, 1, 4, 5],
								  [0, 3, 4, 5],
								  [0, 3, 4, 5],
								  [0, 3, 2, 5]]}

例如其中[2, 1, 4, 0, 3]表示第一台机器的加工顺序为2->1->4->0->3,以此类推,后面的5个list分别为其余5台机器的加工顺序。看到这里可能会有疑问,前面不是提到了某些工序只需要在多台机器中的任意一台进行加工即可吗?order中的信息看起来所有工件都会在所有机器上进行加工?machine中的信息就是用于解决这个问题的,machine的长度为5,代表5个工件的使用机器情况,例如[0,1,4,5]表示工件0的四道工序分别在机器0、1、4、5上进行加工。

解码方式
以编码方式中提到的个体为例,由于工序0只有一台机器,因此机器0的加工顺序为[[2, 1, 4, 0, 3],工序2的可加工机器为1和3,从machine信息中可以看出来,使用机器1加工的是[0,1],使用机器3加工的是[2,3,4],再结合order中的顺序,可以得到每台机器上加工的工件及其加工顺序为:

			[[2, 1, 4, 0, 3], [1, 0], [4], [2, 4, 3], [2, 1, 0, 3], [1, 2, 4, 0, 3]]

以第一台机器为例,可以计算出每个工件的开始及结束加工时间如下,其中t1为开始加工时间,t2为结束加工时间。

							{2: {'t1': 0, 't2': 5},
							   1: {'t1': 5, 't2': 14},
							   4: {'t1': 14, 't2': 20},
							   0: {'t1': 20, 't2': 27},
							   3: {'t1': 27, 't2': 38}}

同理,可以获得其余每台机器上的加工时间,用如下所示的字典表示,其中第一个key为工序编号,第2个key为机器编号,第3个key为工件编号,例如time_table[0][0][2]={‘t1’: 0, ‘t2’: 5}表示第一道工序,工件2在机器0上进行加工,开始时间是0,结束时间是5。

								{0: {0: {2: {'t1': 0, 't2': 5},
								   1: {'t1': 5, 't2': 14},
								   4: {'t1': 14, 't2': 20},
								   0: {'t1': 20, 't2': 27},
								   3: {'t1': 27, 't2': 38}}},
								 1: {1: {1: {'t1': 14, 't2': 20}, 0: {'t1': 27, 't2': 32}},
								  3: {2: {'t1': 5, 't2': 11},
								   4: {'t1': 20, 't2': 33},
								   3: {'t1': 38, 't2': 48}}},
								 2: {2: {4: {'t1': 33, 't2': 44}},
								  4: {2: {'t1': 11, 't2': 20},
								   1: {'t1': 20, 't2': 31},
								   0: {'t1': 32, 't2': 39},
								   3: {'t1': 48, 't2': 56}}},
								 3: {5: {1: {'t1': 31, 't2': 37},
								   2: {'t1': 37, 't2': 45},
								   4: {'t1': 45, 't2': 54},
								   0: {'t1': 54, 't2': 61},
								   3: {'t1': 61, 't2': 71}}}}

适应度计算
在上一节解码过程中,我们首先根据order和machine的信息获得了每台机器上加工的工件以及具体的加工顺序,然后按照离散时间仿真的思路(略复杂)能够获得每个工件的在每道工序的具体加工时间以及结束时间及其对应的机器,那么最大完工时间只需要在上述time table取所有工件的最大t2即可,我们定义适应度为最大完工时间的倒数,这样适应度永远大于0,且适应度越大,最大完工时间越短。

种群交叉
交叉方式分为两部分,第一部分是order部分的交叉,第二部分是machine部分的交叉将父代和母代的加工顺序组合分配给子代,例如假设父代和母代的order信息如下:

									父代:[[2, 1, 4, 0, 3],
									  [4, 3, 1, 0, 2],
									  [0, 1, 4, 2, 3],
									  [1, 2, 0, 4, 3],
									  [2, 1, 4, 0, 3],
									  [1, 2, 4, 0, 3]]
									
									母代:[[1, 2, 3, 0, 4],
									  [3, 4, 2, 0, 1],
									  [1, 0, 2, 4, 3],
									  [2, 1, 3, 0, 4],
									  [2, 1, 4, 3, 0],
									  [3, 1, 4, 0, 2]]

相当于父代和母代总共有12个排列顺序,随机选择6个分配给孩子1,随机选择6个分配给孩子2,可以获得如下两个子代,第二部分是machine的交叉,过程与第一部分类似。

									子代1:[[2, 1, 4, 0, 3],
									  [4, 3, 1, 0, 2],
									  [0, 1, 4, 2, 3],
									  [2, 1, 3, 0, 4],
									  [2, 1, 4, 3, 0],
									  [3, 1, 4, 0, 2]]
									
									子代2:[[1, 2, 3, 0, 4],
									  [3, 4, 2, 0, 1],
									  [1, 0, 2, 4, 3],
									  [1, 2, 0, 4, 3],
									  [2, 1, 4, 0, 3],
									  [1, 2, 4, 0, 3]]

个体变异
变异也有两种情况,第一种是order部分的变异,一部分是machine部分的变异。首先并非所有个体都要参与变异,一般有一个变异参数,对于交叉生成的个体,如果生成的随机数小于这个参数则进行变异。order部分的变异首先生成一个随机数n,该随机数指定order要变异的次数,其次生成3n个随机数,每3个为一组,例如生成的随机数是1,2,3表示对机器1的加工顺序2和加工顺序3进行调换,例如n=1,且生成的随机数为0、0、1,那么子代1将变为:

									子代1:[[1, 2, 4, 0, 3],
									  [4, 3, 1, 0, 2],
									  [0, 1, 4, 2, 3],
									  [2, 1, 3, 0, 4],
									  [2, 1, 4, 3, 0],
									  [3, 1, 4, 0, 2]]

接下来是machine的变异,machine的变异首先生成一个随机数m,表示machine部分进行变异的次数,其次生成2m个随机数,每2为一组,例如生成的随机数2,1,子代的machine从子代a变成子代b,工件2的工序1使用的机器由3变为1。

							子代a: [[0, 1, 4, 5],
										  [0, 1, 4, 5],
										  [0, 3, 4, 5],
										  [0, 3, 4, 5],
										  [0, 3, 2, 5]]
						
							子代b:[[0, 1, 4, 5],
										  [0, 1, 4, 5],
										  [0, 1, 4, 5],
										  [0, 3, 4, 5],
										  [0, 3, 2, 5]]

代码实现

数据处理

# 工件数量
workpiece_num=5

# 工序数量
process_num=4

# 机器数量
machine_num=6

# 加工时间
process_time_dict={}
process_time_dict[0]=[7,5,9,9,7,7]
process_time_dict[1]=[9,6,8,8,11,6]
process_time_dict[2]=[5,8,6,6,9,8]
process_time_dict[3]=[11,12,12,10,8,10]
process_time_dict[4]=[6,9,11,13,9,9]

# 工序对应的机器
process_machin_dict={0:[0],1:[1,3],2:[2,4],3:[5]}

种群交叉

'''
种群交叉
'''
def cross(father,mother):
    child1={}
    child2={}
    
    order1=[]
    order2=[]
    
    for i in range(machine_num):
        if randint(0,1):
            order1.append(father['order'][i])
            order2.append(mother['order'][i])
        else:
            order1.append(mother['order'][i])
            order2.append(father['order'][i])
    child1['order']=order1
    child2['order']=order2
    
            
    machine1=[]
    machine2=[]
    
    for j in range(workpiece_num):
        if randint(0,1):
            machine1.append(father['machine'][j])
            machine2.append(mother['machine'][j])
        else:
            machine1.append(mother['machine'][j])
            machine2.append(father['machine'][j])
    child1['machine']=machine1
    child2['machine']=machine2
    
    return child1,child2

个体变异

'''
个体变异
'''
def mutate(solution):
    solution_copy=deepcopy(solution)
    
    # order调整
    swap_num=randint(0,machine_num)
    for i in range(swap_num):
        idx=randint(0,machine_num-1)
        s1,s2=randint(0,workpiece_num-1),randint(0,workpiece_num-1)
        temp=solution_copy['order'][idx][s1]
        solution_copy['order'][idx][s1]=solution_copy['order'][idx][s2]
        solution_copy['order'][idx][s2]=temp
        
    # machine调整
    change_num=randint(0,workpiece_num)
    for j in range(change_num):
        idx=randint(0,process_num-1)
        solution_copy['machine'][j][idx]=random.choice(process_machin_dict[idx])
        
    if get_fitness(solution_copy)>get_fitness(solution):
        return solution_copy
    return solution

可视化

'''
绘制甘特图
'''
def plot_gantt(solution):
    # 给定的数据
    time_table,machine_order=resolve_solution(deepcopy(solution))
    data={}
    for k in time_table:
        for i in time_table[k]:
            data[i]=time_table[k][i]

    # 创建颜色绘图
    colors = list(mcolors.TABLEAU_COLORS.keys())  # 预定义的颜色
    color_map = {}

    fig, ax = plt.subplots(figsize=[20,10])
    for machine, jobs in data.items():
        for job, times in jobs.items():
            start_time = times['t1']
            end_time = times['t2']
            duration = end_time - start_time

            # 检查工件是否已经分配了颜色
            if job not in color_map:
                color_map[job] = colors.pop(0)

            # 绘制甘特图的条形块
            ax.broken_barh([(start_time, duration)], (machine - 0.4, 0.8), facecolors=color_map[job])

    # 设置y轴
    ax.set_yticks(range(len(data)))
    ax.set_yticklabels(['Machine ' + str(i) for i in range(machine_num)])

    # 设置x轴
    ax.set_xlabel('Time')
    ax.set_xlim(0, max(time['t2'] for machine in data.values() for time in machine.values()))

    # 绘制图例
    patches = [plt.Rectangle((0,0),1,1, color=mcolors.TABLEAU_COLORS[color]) for color in color_map.values()]
    labels = [f'Workpiece {i}' for i in color_map.keys()]
    ax.legend(patches, labels, loc='upper right')

    # 在条形上添加任务编号
    for machine, jobs in data.items():
        for job, times in jobs.items():
            start_time = times['t1']
            duration = times['t2'] - times['t1']
            ax.text(x=start_time + duration/2, y=machine, s=f'{job}', color='white', va='center', ha='center',
                    bbox=dict(boxstyle='round,pad=0.1', facecolor=color_map[job], edgecolor='none'))

    # 显示网格
    ax.grid(False)

    # 展示图表
    plt.show()

结果展示

收敛过程
在这里插入图片描述

甘特图
在这里插入图片描述

本文小结

本文对车间调度问题进行了简单介绍,并以一个简单柔性车间调度问题为例,使用遗传算法对该问题进行了求解,并且详细描述了使用遗传算法求解过程中的编码、解码、交叉以及变异等操作。另外,本文给出了遗传算法求解车间调度问题的主体python代码,最后展示了算法求解的结果,包括结果收敛过程以及甘特图,验证了算法的有效性。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐