科大讯飞–数字化车间智能排产调度挑战赛
本系列文章用于记录比赛中模型构建,算法设计,仅用于记录与学习。
系列文章将分为一下几个部分
- 分析问题,建立数学模型构建,并基于求解器验证
- 设计启发式规则求解车间调度问题
- 关键路径+VNS的混合算法求解车间调度问题
这三个部分也是我在解决这个问题过程中,求解方法的一个进化过程。第一次接触车间调度这类问题,涉及的内容也不会很深。下面就开始我们的第一部分内容:分析问题,构建数学模型,小规模样例验证。
启发式求解
由于求解器无法求解大规模的车间调度问题,本题中共40个产品,平均每种产品15个工序,36台机器,求解器无法在有效时间内得到可行解。在设计智能算法之前,想着先设计一种基于优先规则的启发式算法看看求解效果,后续也可以将该算法的求解结果作为初始解进一步求解。
这里我们使用的优先规则是机器最早开始时间+产品优先级。
- 机器最早开始时间,是指以机器角度出发寻找可加工的产品工序。只要有机器空闲就去寻找当前最早可加工的产品及其对应的工序,这样做的目的是使得机器尽可能少的空闲;
- 产品优先级,给不同产品设置优先级是因为同一台机器可对应多种产品的不同工序,方便机器在选择产品时有一定的优先顺序。
为什么要引入产品优先级。对数据进行分析以及多次实验后发现,产品生产的先后顺序对最早完工时间有一定的影响。后续实验中将通过实验证明。
1. 说明
这里需要说明几点:
- 在赛题中要求考虑工序B的准备时间,但在我们的启发式规则中先不考虑这一点(在这里影响不大);
- 工序B结束后必须立即开始工序C。由于我们是以机器的最早开工时间规则出发,可能存在工序B结束后工序C所对应的机器不处于空闲,而违反了这条约束。因此,在确定工序B的实际开工时间时,要同时考虑上一工序的结束时间、机器的最早开工时间、工序C对应机器的最早开工时间。(工序C只有一台机器加工)
t i = m a x ( 前 序 工 序 完 工 时 间 , 机 器 最 早 开 工 时 间 , 工 序 C 最 早 开 工 时 间 ) t_i = max(前序工序完工时间,机器最早开工时间,工序C最早开工时间) ti=max(前序工序完工时间,机器最早开工时间,工序C最早开工时间)
- 与上一篇一样,将每道工序都看作一个任务,因为工序D是按节生产所有一个工序D又可分为多个任务。如下表所示
- 由于工序A只有两台机器,又每个产品只有一个工序A且都是第一工序,因此可以首先将工序A对应的任务首先进行加工
index | route_id | route_No | name | equ_type | product_id | work_time |
---|---|---|---|---|---|---|
0 | A1 | 1 | 工序A | T-01 | P202201101 | 240 |
1 | A1 | 2 | 工序B | T-03 | P202201101 | 480 |
2 | A1 | 3 | 工序C | T-02 | P202201101 | 96 |
3 | A1 | 4 | 工序D | T-05 | P202201101 | 480 |
4 | A1 | 4 | 工序D | T-05 | P202201101 | 480 |
5 | A1 | 4 | 工序D | T-05 | P202201101 | 480 |
6 | A1 | 4 | 工序D | T-05 | P202201101 | 480 |
7 | A1 | 4 | 工序D | T-05 | P202201101 | 480 |
8 | A1 | 4 | 工序D | T-05 | P202201101 | 480 |
9 | A1 | 4 | 工序D | T-05 | P202201101 | 480 |
10 | A1 | 4 | 工序D | T-05 | P202201101 | 480 |
11 | A1 | 5 | 工序B | T-03 | P202201101 | 300 |
— | — | — | — | — | — | — |
24 | A1 | 13 | 工序D | T-05 | P202201101 | 480 |
25 | A1 | 13 | 工序D | T-05 | P202201101 | 480 |
26 | A1 | 13 | 工序D | T-05 | P202201101 | 480 |
27 | B1 | 1 | 工序A | T-01 | P202201102 | 240 |
28 | B1 | 2 | 工序B | T-03 | P202201102 | 200 |
29 | B1 | 3 | 工序C | T-02 | P202201102 | 48 |
2. 求解步骤
在求解之前,要准备一些属性记录任务与机器的状态。
- 机器的空闲开始时间。记录机器的最近一次的完工时间
- 任务的最早可开工时间。即前序工序的完工时间
- 任务状态。分为已完工、可开工、不可开工三种,主要用于机器寻找可加工任务
- 产品优先级。不同产品之间设计先后顺序,用于当机器对应多个可开工的任务时,选择优先级高的优先进行加工
求解步骤
步骤一:根据一定的规则(产品总工作时长、工序B的最早开工时间等)获得产品的优先级;
步骤二:初始化任务的初始状态,除了每个产品的工序A为可开工状态其余皆为不可开工状态;
步骤三:根据优先级对工序A对应的任务进行加工,并更新任务的状态及紧后工序的状态;
步骤四:对机器的空闲时间进行排序,取最早可开工机器k;
步骤五:根据机器k的空闲开始时间以及任务状态检索任务,存储为任务列表R;
步骤六:判断任务列表R是否为空,是则k=k+1,返回步骤五,否则进行下一步;
步骤七:根据任务的最早可加工时间进行排序,选择最早开始的任务进行加工,更新机器状态、任务状态及后续的工序状态;
1. 确定任务的开工时间及结束时间
2. 更新机器的释放时间
3. 更新当前任务的状态、开工时间、完工时间
4. 更新当前任务后续节点的最早开工时间,若当前任务为产品的最后一个工序则无须更新
步骤八:判断所有任务是否均已完工,是则结束,否则返回步骤四。
3. 结果对比
对于产品的优先级计算通过对数据进行观察,最终设计了两种规则,一个是产品总工时越长优先级越高;另一个是产品第二道工序越早开工优先级越高。
① 产品总工时越长优先级越高
② 产品第二道工序越早开工优先级越高
③ 随机赋予产品优先级(10次中取最好)
策略 | ① | ② | ①+② | ②+① | ③ |
---|---|---|---|---|---|
总时长 | 15434 | 15272 | 15204 | 14936 | 15484 |
评分 | 74.17 | 74.96 | 75.30 | 76.65 | 73.93 |
结果表明,先对产品进行优先排序对最终的总时长有一定的影响。
4. 代码
数据读取代码:此部分代码于上一篇的一样
import itertools
import pandas as pd
import numpy as np
import math
def result_process(product_info, route_info):
# 将产品信息划分为多个子任务
result = []
for i in range(len(product_info)):
route_id = product_info.iloc[i, 2]
route_temp = route_info[route_info['route_id'] == route_id].reset_index(drop=True)
route_temp['product_id'] = product_info.iloc[i, 0]
route_temp['work_time'] = 0
for j in range(len(route_temp)):
if route_temp.iloc[j, 4][-1] == 'h':
if route_temp.iloc[j, 2] == "工序C":
route_temp.loc[j, 'work_time'] = float(route_temp.iloc[j, 4][:-1]) * product_info.iloc[i, 1] * 60
else:
route_temp.loc[j, 'work_time'] = float(route_temp.iloc[j, 4][:-1]) * 60
else:
route_temp.loc[j, 'work_time'] = float(route_temp.iloc[j, 4][:3])
route_temp.loc[j, 'ready_time'] = float(route_temp.iloc[j, 4][4:7])
route_temp = route_temp.fillna(0)
route_D = route_temp[route_temp['name'] == '工序D']
for j in range(product_info.iloc[i, 1] - 1):
route_temp = pd.concat([route_temp, route_D], axis=0)
if i == 0:
result = route_temp
else:
result = pd.concat([result, route_temp], axis=0)
result = result.sort_values(['product_id', 'route_No']).reset_index(drop=True)
result = result.sort_values(['product_id', 'route_No']).reset_index(drop=True).reset_index()
return result
class Data:
def __init__(self):
self.order = []
self.equ_info = []
self.order_num = 0
self.pro_route = []
self.BC_order = []
self.equ_order = []
self.order_equ = []
self.conf_ = []
self.BB_order = []
self.equ_num = 0
self.order_order = []
self.ready_B = []
def pro_order(self,result):
# 获取每个任务的紧前任务
pro_route = {1: [0]}
BC = []
for i in range(2, len(result)):
if result.loc[i, 'product_id'] == result.loc[i - 1, 'product_id']:
if result.loc[i, 'route_No'] == result.loc[i - 1, 'route_No'] + 1:
temp_pro = []
count = 1
for j in range(i, 0, -1):
if result.loc[i, 'route_No'] == result.loc[i - count, 'route_No'] + 1:
temp_pro.append(i - count)
count += 1
else:
break
pro_route[i] = temp_pro
else:
pro_route[i] = pro_route[i - 1]
# 获取B、C的组合
if result.loc[i, 'name'] == '工序C' and result.loc[i - 1, 'name'] == '工序B':
BC.append([i - 1, i])
self.pro_route = pro_route
self.BC_order = BC
def conf_equ_order(self, result, equ_info):
# 构建任务与设备的资质矩阵、每个设备的子任务集合、每个子任务的设备集合
conf_ = np.zeros((len(result), len(equ_info)))
equ_order = {}
for j in range(len(equ_info)):
order = []
for i in range(len(result)):
if result.loc[i, 'equ_type'] == equ_info.loc[j, 'equ_type']:
conf_[i][j] = 1
order.append(i)
equ_order[j] = order
var_start = np.zeros(len(equ_info))
var_start[0] = 1
var_start[1] = 1
conf_ = np.insert(conf_, len(conf_), np.ones(len(equ_info)), axis=0)
self.conf_ = conf_
self.equ_order = equ_order
order_equ = {}
order_order = {}
for i in range(len(result)):
order_equ[i] = np.where(conf_[i][:] == 1)[0]
order_order[i] = equ_order[order_equ[i][0]]
self.order_equ = order_equ
self.order_order = order_order
def BB_index(self, result, product_info):
BB_order = {}
for i in range(len(product_info)):
p_id = product_info.loc[i, 'product_id']
df = result[(result['product_id'] == p_id) & (result['name'] == '工序B')]
df = df.reset_index(drop=True)
self.ready_B.append(df.loc[0, 'index'])
for j in range(1, len(df)):
ind = df.loc[j, 'index']
equ_type = df.loc[j, 'equ_type']
df1 = df[df['equ_type'] == equ_type]
arr = np.array(df1['index'])
arr = arr[arr < ind]
if len(arr) > 0:
BB_order[ind] = arr
else:
self.ready_B.append(df.loc[j, 'index'])
self.BB_order = BB_order
def readData(self, product_info):
route_info = pd.read_csv('工艺路线.csv', encoding="gbk")
equ_info = pd.read_csv('设备信息.csv', encoding="gbk")
self.equ_info = equ_info
# 0表示D分批,1表示D不分批
result = result_process(product_info, route_info)
# 获取每个任务的紧前任务
self.pro_order(result)
# # 构建任务与设备的资质矩阵、每个设备的子任务集合
self.conf_equ_order(result, equ_info)
# 同一个产品的BB工序序号
self.BB_index(result, product_info)
self.order_num = len(result)
self.order = result
self.equ_num = len(equ_info)
启发式代码
import pandas as pd
from Data import Data
import numpy as np
import time
from sklearn.utils import shuffle
"""初始化任务的状态"""
def init_(data):
flag = [1]
early_start = [0]
for i in range(1, len(data)):
# 每个产品的第一道工序状态设置为“可开工”,其余设置为“不可开工”
if data.loc[i, 'product_id'] == data.loc[i - 1, 'product_id']:
flag.append(0)
early_start.append(-1)
else:
flag.append(1)
early_start.append(0)
data['flag'] = np.array(flag)
data['early_start'] = np.array(early_start)
data['end'] = -1
data['equ_name'] = ""
return data
"""产品优先级"""
def priority_(res, flag):
product_id = np.unique(np.array(res['product_id']))
total_time = {}
gx2_wt = []
index = []
# 记录产品的总加工时长,及第二道工序的最早开工时间
for p in product_id:
df = res[res['product_id'] == p]
total_time[p] = sum(df['work_time'])
index.append(df.iloc[0, 0])
gx2_wt.append(df.iloc[1, 9])
temp = pd.DataFrame.from_dict(total_time, orient='index')
temp['id'] = index
temp['gx2_wt'] = gx2_wt
"""
flag = 1,只对总时长进行排序
flag = 2,只对工序B进行排序
flag = 3,先对总时长进行排序,后对工序B进行排序
flag = 4,先对工序B进行排序,再对总时长进行排序
flag = 5,随机赋予优先级
"""
if flag == 1:
temp = temp.sort_values(by=[0], ascending=False).reset_index()
elif flag == 2:
temp = temp.sort_values(by='gx2_wt').reset_index()
elif flag == 3:
temp = temp.sort_values(by='gx2_wt').sort_values(by=[0], ascending=False).reset_index()
elif flag == 4:
temp = temp.sort_values(by=[0], ascending=False).sort_values(by='gx2_wt').reset_index()
else:
temp = shuffle(temp).reset_index()
return temp
"""根据优先级对工序A对应的任务进行加工,并更新任务的状态及紧后工序的状态"""
def gx_A(res, pri, equ_index):
m0 = m1 = 0
m0_name = 'Z-1001'
m1_name = 'Z-1002'
# 根据工序2的优先级给产品中的工序A排序
for p in range(len(pri)):
ind = pri.loc[p, 'id']
if m0 <= m1:
m0 = m0 + res.loc[ind, 'work_time']
# 更新当前任务的状态信息
res.loc[ind, 'flag'] = 2
res.loc[ind, 'end'] = m0
res.loc[ind, 'equ_name'] = m0_name
# 更新紧后工序的状态信息
res.loc[ind + 1, 'flag'] = 1
res.loc[ind + 1, 'early_start'] = m0
equ_index[m0_name].append(ind)
else:
m1 = m1 + res.loc[ind, 'work_time']
res.loc[ind, 'flag'] = 2
res.loc[ind, 'end'] = m1
res.loc[ind, 'equ_name'] = m1_name
res.loc[ind + 1, 'flag'] = 1
res.loc[ind + 1, 'early_start'] = m1
equ_index[m1_name].append(ind)
return res, equ_index
"""启发式规则:机器最早开工时间"""
def machine_early_start_time(res, equ_release_time, equ_index):
equ_num = len(equ_release_time)
res_num = len(res)
# 根据设备的最早开始时间进行车间调度
while True:
# 遍历设备,选择最早开始加工的设备
equ_release_time = equ_release_time.sort_values(by=['release_time'])
for e in range(equ_num):
equ_name = equ_release_time.index[e]
if equ_name == 'Y-2045':
continue
equ_type = equ_release_time.iloc[e, 0]
es = equ_release_time.iloc[e, 1]
# 筛选出可加工工序
df_pro = res[(res['equ_type'] == equ_type) & (res['flag'] == 1)]
# 选择优先级最高的工序进行加工
if len(df_pro) > 0:
# 对任务的最早开加工时间进行排序,选择最早开始的任务
df = df_pro.sort_values(by=['early_start'])
ind = df.index[0]
equ_index[equ_name].append(ind)
# 计算释放时间
release_t = es + df.iloc[0, 9] if es >= df.iloc[0, 11] else df.iloc[0, 9] + df.iloc[0, 11]
# 判断是否在最后一个工序
if ind != res_num - 1:
# 判断前后两工序是否是同一个产品的工序
if res.loc[ind, 'product_id'] == res.loc[ind + 1, 'product_id']:
# 判断当前工序是否为工序B
if df.iloc[0, 3] == '工序B':
# 如果是工序B,释放时间要与工序C的设备有关
es_y = equ_release_time.loc['Y-2045', 'release_time']
if es_y > release_t:
release_t = es_y
equ_release_time.loc[equ_name, 'release_time'] = release_t
# 更新工序信息
res.loc[ind, 'flag'] = 2
res.loc[ind, 'end'] = release_t
res.loc[ind, 'equ_name'] = equ_name
# 更新工序C的信息
equ_index['Y-2045'].append(ind + 1)
res.loc[ind + 1, 'early_start'] = release_t
release_t = release_t + res.loc[ind + 1, 'work_time']
res.loc[ind + 1, 'flag'] = 2
res.loc[ind + 1, 'end'] = release_t
res.loc[ind + 1, 'equ_name'] = 'Y-2045'
equ_release_time.loc['Y-2045', 'release_time'] = release_t
# 更新后续的工序
count = 2
while True:
res.loc[ind + count, 'flag'] = 1
res.loc[ind + count, 'early_start'] = release_t
count += 1
if ind + count >= res_num:
break
if res.loc[ind + count, 'name'] != "工序D":
break
else:
# 不是工序B则是工序D
# 更新设备的空闲开始时间
equ_release_time.loc[equ_name, 'release_time'] = release_t
# 更新工序信息
res.loc[ind, 'flag'] = 2
res.loc[ind, 'end'] = release_t
res.loc[ind, 'equ_name'] = equ_name
p_id = res.loc[ind, 'product_id']
next_route_no = res.loc[ind, 'route_No']
temp_df = res[(res['product_id'] == p_id) & (res['route_No'] == next_route_no)]
# 工序D比较特殊,只有当当前工序D对应的所有任务都完工,才更新紧后工序
if sum(np.array(temp_df['flag'])) >= len(temp_df) * 2:
n_df = res[(res['product_id'] == p_id) & (res['route_No'] == next_route_no + 1)]
if len(n_df) > 0:
n_ind = n_df.index[0]
res.loc[n_ind, 'flag'] = 1
res.loc[n_ind, 'early_start'] = max(np.array(temp_df['end']))
break
else:
# 更新设备的空闲开始时间
equ_release_time.loc[equ_name, 'release_time'] = release_t
# 更新工序信息
res.loc[ind, 'flag'] = 2
res.loc[ind, 'end'] = release_t
res.loc[ind, 'equ_name'] = equ_name
else:
# 更新设备的空闲开始时间
equ_release_time.loc[equ_name, 'release_time'] = release_t
# 更新工序信息
res.loc[ind, 'flag'] = 2
res.loc[ind, 'end'] = release_t
res.loc[ind, 'equ_name'] = equ_name
if sum(np.array(res['flag'])) >= len(res) * 2:
break
return res, equ_release_time, equ_index
def heuri_():
product = pd.read_csv('产品信息.csv')
ori_data = Data()
ori_data.readData(product)
data = ori_data.order
equ = pd.read_csv('设备信息.csv', encoding="gbk")
equ_index = {}
equ_release_time = {}
equ_num = len(equ)
for e in range(equ_num):
equ_index[equ.iloc[e, 0]] = []
equ_release_time[equ.iloc[e, 0]] = [equ.iloc[e, 1], 0]
equ_release_time = pd.DataFrame.from_dict(equ_release_time, orient='index')
equ_release_time.columns = ['type', 'release_time']
# 任务数据初始化
res = init_(data)
res_num = len(res)
# 产品优先级
# 1. 总工时越长优先级越高
# pri = priority_(res, 1)
# 2. 工序B越早开始优先级越高
# pri = priority_(res, 2)
# pri = priority_(res, 3)
# pri = priority_(res, 4)
# 3. 随机赋予优先级
pri = priority_(res, 5)
# 根据优先级对工序A对应的任务进行加工,并更新任务的状态及紧后工序的状态;
res, equ_index = gx_A(res, pri, equ_index)
# 启发式:从机器角度出发,以最早可加工的机器选择可加工的工序
res, equ_release_time, equ_index = machine_early_start_time(res, equ_release_time, equ_index)
print(max(res['end']))
return equ_index, res
if __name__ == '__main__':
st = time.time()
equ_, res_ = heuri_()
print(time.time() - st)
写到这里,启发式规则求解基本已经完成,最终的结果应该是七十多分,但离最优解还有一定的距离,还有优化空闲。因此,后续以启发式规则为初始解,设计关键路径+VNS的智能算法进行求解。