diff --git a/Decode.py b/Decode.py index e6ff57d..75ed9f2 100644 --- a/Decode.py +++ b/Decode.py @@ -1,9 +1,7 @@ import numpy as np - from Job import Job from Machine import Machine_Time_window - class Decode: def __init__(self, J, Processing_time, M_num): """ @@ -16,7 +14,8 @@ class Decode: self.J = J self.Machines = [] # 存储机器类 self.Scheduled = [] # 已经排产过的工序 - self.fitness = 0 # 适应度 + self.fitness = 0 # 适应度(最大完工时间) + self.Machine_Load = np.zeros(M_num, dtype=int) # 机器总负载 self.Machine_State = np.zeros(M_num, dtype=int) # 在机器上加工的工件是哪个 self.Jobs = [] # 存储工件类 for j in range(M_num): @@ -68,11 +67,11 @@ class Decode: for le_i in range(len(M_Tlen)): # 当前空格时间比加工时间大可插入 if M_Tlen[le_i] >= P_t: - # 当前空格开始时间比该工件上一工序结束时间大可插入该空格,以空格开始时间为这一工序开始 + # 当前空格开始时间比该工件上一工序结束时间大可插入该空格 if M_Tstart[le_i] >= last_O_end: ealiest_start = M_Tstart[le_i] break - # 当前空格开始时间比该工件上一工序结束时间小但空格可满足插入该工序,以该工序的上一工序的结束为开始 + # 当前空格开始时间比该工件上一工序结束时间小但空格可满足插入 if M_Tstart[le_i] < last_O_end and M_Tend[le_i] - last_O_end >= P_t: ealiest_start = last_O_end break @@ -85,19 +84,40 @@ class Decode: """ :param CHS: 种群基因 :param Len_Chromo: MS与OS的分解线 - :return: 适应度,即最大加工时间 + :return: 双目标值 [最大加工时间, 负载标准差] """ + # 重置状态 + self.fitness = 0 + self.Machine_Load = np.zeros(self.M_num, dtype=int) + for machine in self.Machines: + machine.assigned_task = [] + machine.O_start = [] + machine.O_end = [] + machine.End_time = 0 + for job in self.Jobs: + job.Processed = [] + job.J_start = [] + job.J_end = [] + job.J_machine = [] + job.Last_Processing_Machine = None + job.Last_Processing_end_time = 0 + MS = list(CHS[0:Len_Chromo]) OS = list(CHS[Len_Chromo:2 * Len_Chromo]) Needed_Matrix = self.Order_Matrix(MS) - JM = Needed_Matrix[0] + JM, TM = Needed_Matrix[0], Needed_Matrix[1] for i in OS: Job = i O_num = self.Jobs[Job].Current_Processed() # 现在加工的工序 - Machine = JM[Job][O_num] # 用基因的OS部分的工件序号以及工序序号索引机器顺序矩阵的机器序号 + Machine = JM[Job][O_num] # 确定加工机器 + Process_time = TM[Job][O_num] # 确定加工时间 Para = self.Earliest_Start(Job, O_num, Machine) self.Jobs[Job]._Input(Para[0], Para[5], Para[1]) # 工件完成该工序 if Para[5] > self.fitness: self.fitness = Para[5] self.Machines[Machine]._Input(Job, Para[0], Para[2], Para[3]) # 机器完成该工件该工序 - return self.fitness + self.Machine_Load[Machine] += Process_time # 累加机器负载 + + # 计算负载均衡度(标准差) + load_std = np.std(self.Machine_Load) + return [self.fitness, load_std] \ No newline at end of file diff --git a/Encode.py b/Encode.py index 4f27a1e..eeebfc4 100644 --- a/Encode.py +++ b/Encode.py @@ -1,14 +1,12 @@ import random - import numpy as np - class Encode: def __init__(self, Matrix, Pop_size, J, J_num, M_num): """ :param Matrix: 机器加工时间矩阵 :param Pop_size: 种群数量 - :param J: 各工件对应的工序数 + :param J: 各工件对应的工序数字典 :param J_num: 工件数 :param M_num: 机器数 """ @@ -54,10 +52,10 @@ class Encode: for i in range(self.GS_num): Machine_time = np.zeros(self.M_num, dtype=int) # 步骤1 生成一个整型数组,长度为机器数,且初始化每个元素为0 random.shuffle(OS_list) # 生成工序排序部分 - OS[i] = np.array(OS_list) # 随机打乱后将其赋值给OS的某一行(因为有一个种群,第i则是赋值在OS的第i行,以此生成完整的OS) + OS[i] = np.array(OS_list) # 随机打乱后将其赋值给OS的某一行 GJ_list = [i_1 for i_1 in range(self.J_num)] # 生成工件集 - random.shuffle(GJ_list) # 随机打乱工件集,为的是下一步可以随机抽出第一个工件 - for g in GJ_list: # 选择第一个工件(由于上一步已经打乱工件集,抽出第一个也是“随机”) + random.shuffle(GJ_list) # 随机打乱工件集 + for g in GJ_list: # 选择第一个工件 h = self.Matrix[g] # h为第一个工件包含的工序对应的时间矩阵 for j in range(len(h)): # 从此工件的第一个工序开始 D = h[j] # D为第一个工件的第一个工序对应的时间矩阵 @@ -70,11 +68,11 @@ class Encode: for Machine_add in List_Machine_weizhi: # 将机器时间数组对应位置和工序可选机器的时间相加 Machine_Select.append(Machine_time[Machine_add] + D[Machine_add]) Min_time = min(Machine_Select) # 选出时间最小的机器 - K = Machine_Select.index(Min_time) # 第一次出现最小时间的位置,确定最小负荷为哪个机器,即为该工序可选择的机器里的第K个机器,并非Mk - I = List_Machine_weizhi[K] # 所有机器里的第I个机器,即Mi + K = Machine_Select.index(Min_time) # 第一次出现最小时间的位置 + I = List_Machine_weizhi[K] # 所有机器里的第I个机器 Machine_time[I] += Min_time # 相应的机器位置加上最小时间 site = self.Site(g, j) # 定位每个工件的每道工序的位置 - MS[i][site] = K # 即将每个工序选择的第K个机器赋值到每个工件的每道工序的位置上去 即生成MS的染色体 + MS[i][site] = K # 即将每个工序选择的第K个机器赋值到对应位置 CHS1 = np.hstack((MS, OS)) # 将MS和OS整合为一个矩阵 return CHS1 @@ -85,11 +83,10 @@ class Encode: OS = self.CHS_Matrix(self.LS_num) for i in range(self.LS_num): random.shuffle(OS_list) # (随机打乱)生成工序排序部分 - OS[i] = np.array(OS_list) # 随机打乱后将其赋值给OS的某一行(因为有一个种群,第i则是赋值在OS的第i行,以此生成完整的OS) + OS[i] = np.array(OS_list) GJ_List = [i_1 for i_1 in range(self.J_num)] # 生成工件集 for g in GJ_List: # 选择第一个工件(注意:不用随机打乱了) - Machine_time = np.zeros(self.M_num, - dtype=int) # 设置一个整型数组 并初始化每一个元素为0,由于局部初始化,每个工件的所有工序结束后都要重新初始化,所以和全局初始化不同,此步骤应放在此处 + Machine_time = np.zeros(self.M_num, dtype=int) # 局部初始化,每个工件重新初始化 h = self.Matrix[g] # h为第一个工件包含的工序对应的时间矩阵 for j in range(len(h)): # 从选择的工件的第一个工序开始 D = h[j] # 此工件第一个工序对应的机器加工时间矩阵 @@ -102,11 +99,11 @@ class Encode: for Machine_add in List_Machine_weizhi: # 将机器时间数组对应位置和工序可选机器的时间相加 Machine_Select.append(Machine_time[Machine_add] + D[Machine_add]) Min_time = min(Machine_Select) # 选出这些时间里最小的 - K = Machine_Select.index(Min_time) # 第一次出现最小时间的位置,确定最小负荷为哪个机器,即为该工序可选择的机器里的第K个机器,并非Mk - I = List_Machine_weizhi[K] # 所有机器里的第I个机器,即Mi + K = Machine_Select.index(Min_time) # 第一次出现最小时间的位置 + I = List_Machine_weizhi[K] # 所有机器里的第I个机器 Machine_time[I] += Min_time site = self.Site(g, j) # 定位每个工件的每道工序的位置 - MS[i][site] = K # 即将每个工序选择的第K个机器赋值到每个工件的每道工序的位置上去 + MS[i][site] = K # 即将每个工序选择的第K个机器赋值到对应位置 CHS1 = np.hstack((MS, OS)) # 将MS和OS整合为一个矩阵 return CHS1 @@ -128,9 +125,9 @@ class Encode: Useing_Machine = D[k] if Useing_Machine != 9999: List_Machine_weizhi.append(k) - number = random.choice(List_Machine_weizhi) # 从可选择的机器编号中随机选择一个(此编号就是机器编号) - K = List_Machine_weizhi.index(number) # 即为该工序可选择的机器里的第K个机器,并非Mk + number = random.choice(List_Machine_weizhi) # 从可选择的机器编号中随机选择一个 + K = List_Machine_weizhi.index(number) # 即为该工序可选择的机器里的第K个机器 site = self.Site(g, j) # 定位每个工件的每道工序的位置 - MS[i][site] = K # 即将每个工序选择的第K个机器赋值到每个工件的每道工序的位置上去 + MS[i][site] = K # 即将每个工序选择的第K个机器赋值到对应位置 CHS1 = np.hstack((MS, OS)) - return CHS1 + return CHS1 \ No newline at end of file diff --git a/GA.py b/GA.py index 0c9f003..edf71d2 100644 --- a/GA.py +++ b/GA.py @@ -9,12 +9,12 @@ from Instance import * class GA(): def __init__(self): - self.Pop_size = 300 # 种群数量 + self.Pop_size = 500 # 种群数量 self.Pc = 0.8 # 交叉概率 self.Pm = 0.3 # 变异概率 self.Pv = 0.5 # 选择何种方式进行交叉的概率阈值 self.Pw = 0.95 # 选择何种方式进行变异的概率阈值 - self.Max_Itertions = 20 # 最大迭代次数 + self.Max_Itertions = 100 # 最大迭代次数 # 适应度 def fitness(self, CHS, J, Processing_time, M_num, Len): @@ -27,7 +27,7 @@ class GA(): # 机器部分交叉 def machine_cross(self, CHS1, CHS2, T0): """ - :param CHS1: 机器选择部分的基因1 + :param CHS1: 机器选 择部分的基因1 :param CHS2: 机器选择部分的基因2 :param T0: 工序总数 :return: 交叉后的机器选择部分的基因 @@ -142,7 +142,7 @@ class GA(): for i in range(len(A)): for j in range(len(A[i])): OS[Site[j]] = A[i][j] - C_I = np.hstack((MS, OS)) + C_I = np.hstack((MS, OS)) # 水平堆叠,即合成 A_CHS.append(C_I) Fit = [] for i in range(len(A_CHS)): diff --git a/Job.py b/Job.py index 129b3c0..e2889ab 100644 --- a/Job.py +++ b/Job.py @@ -30,4 +30,4 @@ class Job: self.Processed.append(1) self.J_start.append(W_Eailiest) self.J_end.append(End_time) - self.J_machine.append(Machine) + self.J_machine.append(Machine) \ No newline at end of file diff --git a/Machine.py b/Machine.py index f69c24f..da50a47 100644 --- a/Machine.py +++ b/Machine.py @@ -50,4 +50,4 @@ class Machine_Time_window: self.O_start.sort() self.O_end.append(M_Ealiest + P_t) self.O_end.sort() - self.End_time = self.O_end[-1] + self.End_time = self.O_end[-1] \ No newline at end of file diff --git a/NSGA2.py b/NSGA2.py new file mode 100644 index 0000000..4ef06da --- /dev/null +++ b/NSGA2.py @@ -0,0 +1,105 @@ +import random + +class NSGA2: + def __init__(self, pop_size, obj_num): + self.pop_size = pop_size + self.obj_num = obj_num + + def fast_non_dominated_sort(self, pop_obj): + """快速非支配排序""" + pop_size = len(pop_obj) + dominated = [[] for _ in range(pop_size)] # 被支配个体列表,_通常用作占位符变量,表示不关心这个变量的具体值 + rank = [0] * pop_size # 个体的非支配等级 + n = [0] * pop_size # 支配该个体的个体数量 + + # 计算每个个体的支配关系 + for p in range(pop_size): + for q in range(pop_size): + if p != q: + # p支配q + if all(pop_obj[p][i] <= pop_obj[q][i] for i in range(self.obj_num)) and \ + any(pop_obj[p][i] < pop_obj[q][i] for i in range(self.obj_num)): + dominated[p].append(q) + # q支配p + elif all(pop_obj[q][i] <= pop_obj[p][i] for i in range(self.obj_num)) and \ + any(pop_obj[q][i] < pop_obj[p][i] for i in range(self.obj_num)): + n[p] += 1 + + # 找到等级为0的个体 + if n[p] == 0: + rank[p] = 0 + + # 计算其他等级 + current_rank = 0 + while True: + next_rank = [] + for p in range(pop_size): + if rank[p] == current_rank: + for q in dominated[p]: + n[q] -= 1 + if n[q] == 0 and rank[q] == 0: + rank[q] = current_rank + 1 + next_rank.append(q) + if not next_rank: + break + current_rank += 1 + + return rank + + def crowding_distance(self, pop_obj, rank): + """计算拥挤度距离""" + pop_size = len(pop_obj) + distance = [0.0] * pop_size + max_rank = max(rank) + + # 对每个等级的个体计算拥挤度 + for r in range(max_rank + 1): + # 获取当前等级的个体索引 + current_indices = [i for i in range(pop_size) if rank[i] == r] + if len(current_indices) <= 1: + continue + + # 对每个目标函数进行排序 + for m in range(self.obj_num): + # 按目标m的值排序 + sorted_indices = sorted(current_indices, key=lambda x: pop_obj[x][m]) + min_val = pop_obj[sorted_indices[0]][m] + max_val = pop_obj[sorted_indices[-1]][m] + + # 边界个体的拥挤度设为无穷大 + distance[sorted_indices[0]] = float('inf') + distance[sorted_indices[-1]] = float('inf') + + # 计算中间个体的拥挤度 + for i in range(1, len(sorted_indices) - 1): + if max_val - min_val == 0: + continue + distance[sorted_indices[i]] += (pop_obj[sorted_indices[i + 1]][m] - pop_obj[sorted_indices[i - 1]][m]) / (max_val - min_val) + + return distance + + def selection(self, pop, pop_obj): + """选择操作:基于非支配排序和拥挤度的锦标赛选择""" + pop_size = len(pop) + rank = self.fast_non_dominated_sort(pop_obj) + distance = self.crowding_distance(pop_obj, rank) + selected = [] + + for _ in range(pop_size): + # 随机选择两个个体进行锦标赛 + i = random.randint(0, pop_size - 1) + j = random.randint(0, pop_size - 1) + + # 优先选择等级低(更优)的个体 + if rank[i] < rank[j]: + selected.append(pop[i]) + elif rank[i] > rank[j]: + selected.append(pop[j]) + # 等级相同则选择拥挤度大的个体 + else: + if distance[i] > distance[j]: + selected.append(pop[i]) + else: + selected.append(pop[j]) + + return selected \ No newline at end of file diff --git a/main.py b/main.py index fc689c2..bce2691 100644 --- a/main.py +++ b/main.py @@ -1,11 +1,11 @@ import random import matplotlib.pyplot as plt import numpy as np - from Decode import Decode from Encode import Encode from GA import GA from Instance import * +from NSGA2 import NSGA2 # 绘制甘特图 @@ -31,65 +31,112 @@ def Gantt(Machines): if __name__ == '__main__': - Optimal_fit = 9999 # 最佳适应度(初始化) - Optimal_CHS = 0 # 最佳适应度对应的基因个体(初始化) + # 初始化参数 g = GA() e = Encode(Processing_time, g.Pop_size, J, J_num, M_num) CHS1 = e.Global_initial() CHS2 = e.Random_initial() CHS3 = e.Local_initial() C = np.vstack((CHS1, CHS2, CHS3)) - Best_fit = [] # 记录适应度在迭代过程中的变化,便于绘图 + + # 双目标优化相关 + nsga2 = NSGA2(g.Pop_size, 2) # 2个优化目标 + Optimal_solutions = [] # 存储非支配解 + Optimal_fit_values = [] # 存储非支配解的目标值 + for i in range(g.Max_Itertions): - print("iter_{} start!".format(i)) + print(f"iter_{i} start!") Fit = g.fitness(C, J, Processing_time, M_num, O_num) - Best = C[Fit.index(min(Fit))] - best_fitness = min(Fit) - if best_fitness < Optimal_fit: - Optimal_fit = best_fitness - Optimal_CHS = Best - print('iter_{}: new best_fitness = {}'.format(i, best_fitness)) - Best_fit.append(Optimal_fit) # 始终记录当前最优适应度 - for j in range(len(C)): - Cafter = [] + + # 非支配排序 + rank = nsga2.fast_non_dominated_sort(Fit) + current_non_dominated = [C[j] for j in range(len(C)) if rank[j] == 0] + current_non_dominated_fit = [Fit[j] for j in range(len(C)) if rank[j] == 0] + + # 更新全局非支配解 + Optimal_solutions.extend(current_non_dominated) + Optimal_fit_values.extend(current_non_dominated_fit) + + # 对全局解重新筛选非支配解 + if Optimal_solutions: + rank_all = nsga2.fast_non_dominated_sort(Optimal_fit_values) + # 保留等级为0的解 + Optimal_solutions = [Optimal_solutions[j] for j in range(len(Optimal_solutions)) if rank_all[j] == 0] + Optimal_fit_values = [Optimal_fit_values[j] for j in range(len(Optimal_fit_values)) if rank_all[j] == 0] + # 控制解的数量,过多时保留拥挤度高的解 + if len(Optimal_solutions) > g.Pop_size: + distance = nsga2.crowding_distance(Optimal_fit_values, rank_all) + # 按拥挤度排序并保留前Pop_size个解 + sorted_indices = sorted(range(len(distance)), key=lambda k: distance[k], reverse=True) + Optimal_solutions = [Optimal_solutions[j] for j in sorted_indices[:g.Pop_size]] + Optimal_fit_values = [Optimal_fit_values[j] for j in sorted_indices[:g.Pop_size]] + + # 选择操作(基于NSGA-II) + selected = nsga2.selection(C, Fit) + + # 交叉变异操作 + new_pop = [] + for j in range(len(selected)): if random.random() < g.Pc: - N_i = random.choice(np.arange(len(C))) + # 选择另一个个体进行交叉 + mate_idx = random.randint(0, len(selected) - 1) if random.random() < g.Pv: - Cross = g.machine_cross(C[j], C[N_i], O_num) + offspring1, offspring2 = g.machine_cross(selected[j], selected[mate_idx], O_num) else: - Cross = g.operation_cross(C[j], C[N_i], O_num, J_num) - Cafter.append(Cross[0]) - Cafter.append(Cross[1]) - Cafter.append(C[j]) + offspring1, offspring2 = g.operation_cross(selected[j], selected[mate_idx], O_num, J_num) + new_pop.append(offspring1) + new_pop.append(offspring2) + else: + new_pop.append(selected[j]) + + # 变异操作 if random.random() < g.Pm: if random.random() < g.Pw: - Variance = g.machine_variation(C[j], Processing_time, O_num, J) + mutated = g.machine_variation(selected[j], Processing_time, O_num, J) else: - Variance = g.operation_variation(C[j], O_num, J_num, J, Processing_time, M_num) - Cafter.append(Variance) - if Cafter != []: - Fit = g.fitness(Cafter, J, Processing_time, M_num, O_num) - C[j] = Cafter[Fit.index(min(Fit))] + mutated = g.operation_variation(selected[j], O_num, J_num, J, Processing_time, M_num) + new_pop.append(mutated) - # 迭代结束后输出最终结果 + # 保持种群规模 + if len(new_pop) > g.Pop_size: + # 对新种群进行筛选 + new_fit = g.fitness(new_pop, J, Processing_time, M_num, O_num) + new_rank = nsga2.fast_non_dominated_sort(new_fit) + new_distance = nsga2.crowding_distance(new_fit, new_rank) + # 按等级和拥挤度排序 + sorted_indices = sorted(range(len(new_pop)), key=lambda k: (new_rank[k], -new_distance[k])) + C = [new_pop[j] for j in sorted_indices[:g.Pop_size]] + else: + C = new_pop + + # 输出结果 print("\n=== 优化结果 ===") - print("最优适应度 (最少完成时间):", Optimal_fit) + print(f"非支配解数量: {len(Optimal_solutions)}") + print("非支配解目标值 (最大完工时间, 负载标准差):") + for fit in Optimal_fit_values: + print(f"({fit[0]}, {fit[1]:.2f})") - # 解码最优解并绘制甘特图 - d = Decode(J, Processing_time, M_num) - final_fitness = d.decode(Optimal_CHS, O_num) - print("解码验证适应度:", final_fitness) + # 选择一个折中解绘制甘特图(例如Cmax最小的解) + if Optimal_solutions: + # 找到Cmax最小的解 + cmax_values = [fit[0] for fit in Optimal_fit_values] + best_idx = cmax_values.index(min(cmax_values)) + Optimal_CHS = Optimal_solutions[best_idx] + # 解码并绘图 + d = Decode(J, Processing_time, M_num) + final_fitness = d.decode(Optimal_CHS, O_num) + print(f"\n选中解的目标值: (最大完工时间: {final_fitness[0]}, 负载标准差: {final_fitness[1]:.2f})") + Gantt(d.Machines) - # 绘制最优甘特图 - Gantt(d.Machines) - - # 绘制收敛曲线 - x = np.linspace(0, g.Max_Itertions, g.Max_Itertions) - plt.figure() - plt.plot(x, Best_fit, '-k') - plt.title('The maximum completion time of each iteration') - plt.ylabel('Cmax') - plt.xlabel('Iteration') - plt.grid(True) - plt.savefig('最大完成时间的优化过程.png') - plt.show() \ No newline at end of file + # 绘制帕累托前沿 + if Optimal_fit_values: + plt.figure() + cmax = [fit[0] for fit in Optimal_fit_values] + load_std = [fit[1] for fit in Optimal_fit_values] + plt.scatter(cmax, load_std, color='red') + plt.title('ParetoFront') + plt.xlabel('maxtime') + plt.ylabel('standard deviation') + plt.grid(True) + plt.savefig('pareto.png') + plt.show() \ No newline at end of file diff --git a/优化后排程方案的甘特图.png b/优化后排程方案的甘特图.png index 241626b..c95e2ec 100644 Binary files a/优化后排程方案的甘特图.png and b/优化后排程方案的甘特图.png differ