diff --git a/SMO/benchmarks.py b/SMO/benchmarks.py deleted file mode 100644 index 9ec6a7e..0000000 --- a/SMO/benchmarks.py +++ /dev/null @@ -1,34 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Python code of Spider-Monkey Optimization (SMO) -Coded by: Mukesh Saraswat (emailid: saraswatmukesh@gmail.com), Himanshu Mittal (emailid: himanshu.mittal224@gmail.com) and Raju Pal (emailid: raju3131.pal@gmail.com) -The code template used is similar to code given at link: https://github.com/himanshuRepo/CKGSA-in-Python - and C++ version of the SMO at link: http://smo.scrs.in/ - -Reference: Jagdish Chand Bansal, Harish Sharma, Shimpi Singh Jadon, and Maurice Clerc. "Spider monkey optimization algorithm for numerical optimization." Memetic computing 6, no. 1, 31-47, 2014. -@link: http://smo.scrs.in/ - --- Benchmark.py: Defining the benchmark function along its range lower bound, upper bound and dimensions - -Code compatible: - -- Python: 2.* or 3.* -""" - -import numpy -import math - - -# define the function blocks -def F1(x): - s = numpy.sum(x ** 2); - return s - - -# define the function parameters -def getFunctionDetails(): - # [name, lb, ub, dim, acc_err, obj_val] - param = ["F1", -100, 100, 30, 1.0e-5, 0] - return param - - - diff --git a/SMO/main.py b/SMO/main.py deleted file mode 100644 index 9302aae..0000000 --- a/SMO/main.py +++ /dev/null @@ -1,79 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Python code of Spider-Monkey Optimization (SMO) -Coded by: Mukesh Saraswat (emailid: saraswatmukesh@gmail.com), Himanshu Mittal (emailid: himanshu.mittal224@gmail.com) and Raju Pal (emailid: raju3131.pal@gmail.com) -The code template used is similar to code given at link: https://github.com/himanshuRepo/CKGSA-in-Python - and C++ version of the SMO at link: http://smo.scrs.in/ - -Reference: Jagdish Chand Bansal, Harish Sharma, Shimpi Singh Jadon, and Maurice Clerc. "Spider monkey optimization algorithm for numerical optimization." Memetic computing 6, no. 1, 31-47, 2014. -@link: http://smo.scrs.in/ - --- Main.py: Calling the Spider-Monkey Optimization (SMO) Algorithm - for minimizing of an objective Function - -Code compatible: - -- Python: 2.* or 3.* -""" -import smo -import benchmarks -import csv -import numpy -import time -import math - - -def selector(func_details, popSize, Iter, succ_rate, mean_feval): - function_name = func_details[0] - lb = func_details[1] - ub = func_details[2] - dim = func_details[3] - acc_err = func_details[4] - obj_val = func_details[5] - - - x, succ_rate, mean_feval = smo.main(getattr(benchmarks, function_name), lb, ub, dim, popSize, Iter, acc_err, - obj_val, succ_rate, mean_feval) - return x, succ_rate, mean_feval - - -# Select number of repetitions for each experiment. -# To obtain meaningful statistical results, usually 30 independent runs are executed for each algorithm. -NumOfRuns = 2 - -# Select general parameters for all optimizers (population size, number of iterations) -PopulationSize = 10 -Iterations = 500 - -mean_error = 0 -total_feval = 0 -mean1 = 0 -var = 0 -sd = 0 -mean_feval = 0 -succ_rate = 0 -GlobalMins = numpy.zeros(NumOfRuns) - -for k in range(0, NumOfRuns): - - func_details = benchmarks.getFunctionDetails() - print("Run: {}".format(k + 1)) - x, succ_rate, mean_feval = selector(func_details, PopulationSize, Iterations, succ_rate, mean_feval) - mean_error = mean_error + x.error; - mean1 = mean1 + x.convergence[-1] - total_feval = total_feval + x.feval - GlobalMins[k] = x.convergence[-1] - - -mean1 = mean1 / NumOfRuns; -mean_error = mean_error / NumOfRuns -if (succ_rate > 0): - mean_feval = mean_feval / succ_rate -total_feval = total_feval / NumOfRuns -for k in range(NumOfRuns): - var = var + math.pow((GlobalMins[k] - mean1), 2) -var = var / NumOfRuns -sd = math.sqrt(var) - -print( - "Values after executing SMO: \n Mean Error:{} \n Mean Function eval:{} \n Total Function eval:{} \n Variance:{} \n STD:{}".format( - mean_error, mean_feval, total_feval, var, sd)) diff --git a/SMO/smo.py b/SMO/smo.py deleted file mode 100644 index 7b8651b..0000000 --- a/SMO/smo.py +++ /dev/null @@ -1,348 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Python code of Spider-Monkey Optimization (SMO) -Coded by: Mukesh Saraswat (emailid: saraswatmukesh@gmail.com), Himanshu Mittal (emailid: himanshu.mittal224@gmail.com) and Raju Pal (emailid: raju3131.pal@gmail.com) -The code template used is similar to code given at link: https://github.com/himanshuRepo/CKGSA-in-Python - and C++ version of the SMO at link: http://smo.scrs.in/ - -Reference: Jagdish Chand Bansal, Harish Sharma, Shimpi Singh Jadon, and Maurice Clerc. "Spider monkey optimization algorithm for numerical optimization." Memetic computing 6, no. 1, 31-47, 2014. -@link: http://smo.scrs.in/ - --- smo.py: Performing the Spider-Monkey Optimization (SMO) Algorithm - -Code compatible: - -- Python: 2.* or 3.* -""" - -from __future__ import division -import time -import random -import numpy -import math -from solution import solution - - -class SMO(): - def __init__(self, objf1, lb1, ub1, dim1, PopSize1, acc_err1, iters1): - self.PopSize = PopSize1 - self.dim = dim1 - self.acc_err = acc_err1 - self.lb = lb1 - self.ub = ub1 - self.objf = objf1 - self.pos = numpy.zeros((PopSize1, dim1)) - self.fun_val = numpy.zeros(PopSize1) - self.fitness = numpy.zeros(PopSize1) - self.gpoint = numpy.zeros((PopSize1, 2)) - self.prob = numpy.zeros(PopSize1) - self.LocalLimit = dim1 * PopSize1; - self.GlobalLimit = PopSize1; - self.fit = numpy.zeros(PopSize1) - self.MinCost = numpy.zeros(iters1) - self.Bestpos = numpy.zeros(dim1) - self.group = 0 - self.func_eval = 0 - self.part = 1 - self.max_part = 5 - self.cr = 0.1 - - # ====== Function: CalculateFitness() ========= # - def CalculateFitness(self, fun1): - if fun1 >= 0: - result = (1 / (fun1 + 1)) - else: - result = (1 + math.fabs(fun1)) - return result - - # ================ X X X ===================== # - - # ==================================== Function: Initialization() ============================================ # - def initialize(self): - global GlobalMin, GlobalLeaderPosition, GlobalLimitCount, LocalMin, LocalLimitCount, LocalLeaderPosition - S_max = int(self.PopSize / 2) - LocalMin = numpy.zeros(S_max) - LocalLeaderPosition = numpy.zeros((S_max, self.dim)) - LocalLimitCount = numpy.zeros(S_max) - for i in range(self.PopSize): - for j in range(self.dim): - if type(self.ub) == int: - self.pos[i, j] = random.random() * (self.ub - self.lb) + self.lb - else: - self.pos[i, j] = random.random() * (self.ub[j] - self.lb[j]) + self.lb[j] - # Calculate objective function for each particle - for i in range(self.PopSize): - # Performing the bound checking - self.pos[i, :] = numpy.clip(self.pos[i, :], self.lb, self.ub) - self.fun_val[i] = self.objf(self.pos[i, :]) - self.func_eval += 1 - self.fitness[i] = self.CalculateFitness(self.fun_val[i]) - - # Initialize Global Leader Learning - GlobalMin = self.fun_val[0] - GlobalLeaderPosition = self.pos[0, :] - GlobalLimitCount = 0 - - # Initialize Local Leader Learning - for k in range(self.group): - LocalMin[k] = self.fun_val[int(self.gpoint[k, 0])] - LocalLimitCount[k] = 0 - LocalLeaderPosition[k, :] = self.pos[int(self.gpoint[k, 0]), :] - - # ============================================ X X X ======================================================= # - - # =========== Function: CalculateProbabilities() ============ # - def CalculateProbabilities(self): - maxfit = self.fitness[0]; - i = 1 - while (i < self.PopSize): - if (self.fitness[i] > maxfit): - maxfit = self.fitness[i]; - i += 1 - for i in range(self.PopSize): - self.prob[i] = (0.9 * (self.fitness[i] / maxfit)) + 0.1; - - # ========================== X X X ======================== # - - # ================= Function: create_group() ================ # - def create_group(self): - g = 0 - lo = 0 - while (lo < self.PopSize): - hi = lo + int(self.PopSize / self.part) - self.gpoint[g, 0] = lo - self.gpoint[g, 1] = hi - if ((self.PopSize - hi) < (int(self.PopSize / self.part))): - self.gpoint[g, 1] = (self.PopSize - 1) - g = g + 1 - lo = hi + 1 - self.group = g - - # ========================== X X X ======================== # - - # ================= Function: LocalLearning() ================ # - def LocalLearning(self): - global LocalMin, LocalLimitCount, LocalLeaderPosition - S_max = int(self.PopSize / 2) - OldMin = numpy.zeros(S_max) - for k in range(self.group): - OldMin[k] = LocalMin[k] - - for k in range(self.group): - i = int(self.gpoint[k, 0]) - while (i <= int(self.gpoint[k, 1])): - if (self.fun_val[i] < LocalMin[k]): - LocalMin[k] = self.fun_val[i] - LocalLeaderPosition[k, :] = self.pos[i, :] - i = i + 1 - - for k in range(self.group): - if (math.fabs(OldMin[k] - LocalMin[k]) < self.acc_err): - LocalLimitCount[k] = LocalLimitCount[k] + 1 - else: - LocalLimitCount[k] = 0 - - # ========================== X X X ======================== # - - # ================= Function: GlobalLearning() ================ # - def GlobalLearning(self): - global GlobalMin, GlobalLeaderPosition, GlobalLimitCount - G_trial = GlobalMin - for i in range(self.PopSize): - if (self.fun_val[i] < GlobalMin): - GlobalMin = self.fun_val[i] - GlobalLeaderPosition = self.pos[i, :] - - if (math.fabs(G_trial - GlobalMin) < self.acc_err): - GlobalLimitCount = GlobalLimitCount + 1 - else: - GlobalLimitCount = 0 - - # ========================== X X X ======================== # - - # ================= Function: LocalLeaderPhase() ================ # - def LocalLeaderPhase(self, k): - global LocalLeaderPosition - new_position = numpy.zeros((1, self.dim)) - lo = int(self.gpoint[k, 0]) - hi = int(self.gpoint[k, 1]) - i = lo - while (i <= hi): - while True: - PopRand = int((random.random() * (hi - lo) + lo)) - if (PopRand != i): - break - for j in range(self.dim): - if (random.random() >= self.cr): - new_position[0, j] = self.pos[i, j] + (LocalLeaderPosition[k, j] - self.pos[i, j]) * ( - random.random()) + (self.pos[PopRand, j] - self.pos[i, j]) * (random.random() - 0.5) * 2 - else: - new_position[0, j] = self.pos[i, j] - new_position = numpy.clip(new_position, self.lb, self.ub) - - ObjValSol = self.objf(new_position) - self.func_eval += 1 - FitnessSol = self.CalculateFitness(ObjValSol) - if (FitnessSol > self.fitness[i]): - self.pos[i, :] = new_position - self.fun_val[i] = ObjValSol - self.fitness[i] = FitnessSol - i += 1 - - # ========================== X X X ======================== # - - # ================= Function: GlobalLeaderPhase() ================ # - def GlobalLeaderPhase(self, k): - global GlobalLeaderPosition - new_position = numpy.zeros((1, self.dim)) - lo = int(self.gpoint[k, 0]) - hi = int(self.gpoint[k, 1]) - i = lo; - l = lo; - while (l < hi): - if (random.random() < self.prob[i]): - l += 1 - while True: - PopRand = int(random.random() * (hi - lo) + lo) - if (PopRand != i): - break - param2change = int(random.random() * self.dim) - new_position = self.pos[i, :] - new_position[param2change] = self.pos[i, param2change] + ( - GlobalLeaderPosition[param2change] - self.pos[i, param2change]) * (random.random()) + ( - self.pos[PopRand, param2change] - self.pos[ - i, param2change]) * (random.random() - 0.5) * 2 - new_position = numpy.clip(new_position, self.lb, self.ub) - ObjValSol = self.objf(new_position) - self.func_eval += 1 - FitnessSol = self.CalculateFitness(ObjValSol) - if (FitnessSol > self.fitness[i]): - self.pos[i, :] = new_position - self.fun_val[i] = ObjValSol - self.fitness[i] = FitnessSol - i += 1; - if (i == hi): - i = lo; - - # ========================== X X X ======================== # - - # ================= Function: GlobalLeaderDecision() ================ # - def GlobalLeaderDecision(self): - global GlobalLimitCount - if (GlobalLimitCount > self.GlobalLimit): - GlobalLimitCount = 0 - if (self.part < self.max_part): - self.part = self.part + 1 - self.create_group() - self.LocalLearning() - else: - self.part = 1 - self.create_group() - self.LocalLearning() - - # ========================== X X X ======================== # - - # ================= Function: LocalLeaderDecision() ================ # - def LocalLeaderDecision(self): - global GlobalLeaderPosition, LocalLimitCount, LocalLeaderPosition - for k in range(self.group): - if (LocalLimitCount[k] > self.LocalLimit): - i = self.gpoint[k, 0] - while (i <= int(self.gpoint[k, 1])): - for j in range(self.dim): - if (random.random() >= self.cr): - if type(self.ub) == int: - self.pos[i, j] = random.random() * (self.ub - self.lb) + self.lb - else: - self.pos[i, j] = random.random() * (self.ub[j] - self.lb[j]) + self.lb[j] - else: - self.pos[i, j] = self.pos[i, j] + ( - GlobalLeaderPosition[j] - self.pos[i, j]) * random.random() + ( - self.pos[i, j] - LocalLeaderPosition[k, j]) * random.random() - self.pos[i, :] = numpy.clip(self.pos[i, :], self.lb, self.ub) - self.fun_val[i] = self.objf(self.pos[i, :]) - self.func_eval += 1 - self.fitness[i] = self.CalculateFitness(self.fun_val[i]) - i += 1 - LocalLimitCount[k] = 0 - # ========================== X X X ======================== # - - -# ==================================== Main() ===================================== # -def main(objf1, lb1, ub1, dim1, PopSize1, iters, acc_err1, obj_val, succ_rate, mean_feval): - smo = SMO(objf1, lb1, ub1, dim1, PopSize1, acc_err1, iters) - s = solution() - print("SMO is optimizing \"" + smo.objf.__name__ + "\"") - timerStart = time.time() - s.startTime = time.strftime("%Y-%m-%d-%H-%M-%S") - - # =========================== Calling: initialize() =========================== # - smo.initialize() - - # ========================== Calling: GlobalLearning() ======================== # - smo.GlobalLearning() - - # ========================= Calling: LocalLearning() ========================== # - smo.LocalLearning() - - # ========================== Calling: create_group() ========================== # - smo.create_group() - - # ================================= Looping ================================== # - for l in range(iters): - for k in range(smo.group): - # ==================== Calling: LocalLeaderPhase() =================== # - smo.LocalLeaderPhase(k) - - # =================== Calling: CalculateProbabilities() ================== # - smo.CalculateProbabilities() - - for k in range(smo.group): - # ==================== Calling: GlobalLeaderPhase() ================== # - smo.GlobalLeaderPhase(k) - - # ======================= Calling: GlobalLearning() ====================== # - smo.GlobalLearning() - - # ======================= Calling: LocalLearning() ======================= # - smo.LocalLearning() - - # ================== Calling: LocalLeaderDecision() ====================== # - smo.LocalLeaderDecision() - - # ===================== Calling: GlobalLeaderDecision() ================== # - smo.GlobalLeaderDecision() - - # ======================= Updating: 'cr' parameter ======================= # - smo.cr = smo.cr + (0.4 / iters) - - # ====================== Saving the best individual ====================== # - smo.MinCost[l] = GlobalMin - gBestScore = GlobalMin - - # ================ Displaying the fitness of each iteration ============== # - if (l % 1 == 0): - print(['At iteration ' + str(l + 1) + ' the best fitness is ' + str(gBestScore)]); - - # ====================== Checking: acc_error ============================ # - if (math.fabs(GlobalMin - obj_val) <= smo.acc_err): - succ_rate += 1 - mean_feval = mean_feval + smo.func_eval - break - # ========================= XXX Ending of Loop XXX ========================== # - - # =========================== XX Result saving XX =========================== # - error1 = math.fabs(GlobalMin - obj_val) - timerEnd = time.time() - s.endTime = time.strftime("%Y-%m-%d-%H-%M-%S") - s.executionTime = timerEnd - timerStart - s.convergence = smo.MinCost - s.optimizer = "SMO" - s.error = error1 - s.feval = smo.func_eval - s.objfname = smo.objf.__name__ - - return s, succ_rate, mean_feval - - # ================================ X X X =================================== # - - diff --git a/SMO/solution.py b/SMO/solution.py deleted file mode 100644 index 34ed4b2..0000000 --- a/SMO/solution.py +++ /dev/null @@ -1,33 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Python code of Spider-Monkey Optimization (SMO) -Coded by: Mukesh Saraswat (emailid: saraswatmukesh@gmail.com), Himanshu Mittal (emailid: himanshu.mittal224@gmail.com) and Raju Pal (emailid: raju3131.pal@gmail.com) -The code template used is similar to code given at link: https://github.com/himanshuRepo/CKGSA-in-Python - and C++ version of the SMO at link: http://smo.scrs.in/ - -Reference: Jagdish Chand Bansal, Harish Sharma, Shimpi Singh Jadon, and Maurice Clerc. "Spider monkey optimization algorithm for numerical optimization." Memetic computing 6, no. 1, 31-47, 2014. -@link: http://smo.scrs.in/ - --- solution.py: Defining the solution variable for saving the output variables - -Code compatible: - -- Python: 2.* or 3.* -""" - -class solution: - def __init__(self): - self.best = 0 - self.bestIndividual=[] - self.convergence = [] - self.optimizer="" - self.objfname="" - self.startTime=0 - self.endTime=0 - self.executionTime=0 - self.lb=0 - self.ub=0 - self.dim=0 - self.popnum=0 - self.error =0 - self.feval=0 - self.maxiers=0 diff --git a/base_optimizer/optimizer_aggregation.py b/base_optimizer/optimizer_aggregation.py new file mode 100644 index 0000000..4e383df --- /dev/null +++ b/base_optimizer/optimizer_aggregation.py @@ -0,0 +1,220 @@ +from base_optimizer.optimizer_common import * + +from ortools.sat.python import cp_model +from collections import defaultdict + + +@timer_wrapper +def optimizer_aggregation(component_data, pcb_data): + # === phase 0: data preparation === + M = 1000 # a sufficient large number + a, b = 1, 6 # coefficient + K, I, J, L = max_head_index, 0, 0, 0 # the maximum number of heads, component types, nozzle types and batch level + + component_list, nozzle_list = defaultdict(int), defaultdict(int) + cpidx_2_part, nzidx_2_nozzle = {}, {} + for _, data in pcb_data.iterrows(): + part = data['part'] + if part not in cpidx_2_part.values(): + cpidx_2_part[len(cpidx_2_part)] = part + + component_list[part] += 1 + + idx = component_data[component_data['part'] == part].index.tolist()[0] + nozzle = component_data.loc[idx]['nz'] + if nozzle not in nzidx_2_nozzle.values(): + nzidx_2_nozzle[len(nzidx_2_nozzle)] = nozzle + nozzle_list[nozzle] += 1 + + I, J = len(component_list.keys()), len(nozzle_list.keys()) + L = I + 1 + HC = [[M for _ in range(J)] for _ in range(I)] # the handing class when component i is handled by nozzle type j + # represent the nozzle-component compatibility + for i in range(I): + for _, item in enumerate(cpidx_2_part.items()): + index, part = item + cp_idx = component_data[component_data['part'] == part].index.tolist()[0] + nozzle = component_data.loc[cp_idx]['nz'] + + for j in range(J): + if nzidx_2_nozzle[j] == nozzle: + HC[index][j] = 0 + + # === phase 1: mathematical model solver === + model = cp_model.CpModel() + solver = cp_model.CpSolver() + + # === Decision Variables === + # the number of components of type i that are placed by nozzle type j on placement head k + X = {} + for i in range(I): + for j in range(J): + for k in range(K): + X[i, j, k] = model.NewIntVar(0, component_list[cpidx_2_part[i]], 'X_{}_{}_{}'.format(i, j, k)) + + # the total number of nozzle changes on placement head k + N = {} + for k in range(K): + N[k] = model.NewIntVar(0, J, 'N_{}'.format(k)) + + # the largest workload of all placement heads + WL = model.NewIntVar(0, len(pcb_data), 'WL') + + # whether batch Xijk is placed on level l + Z = {} + for i in range(I): + for j in range(J): + for l in range(L): + for k in range(K): + Z[i, j, l, k] = model.NewBoolVar('Z_{}_{}_{}_{}'.format(i, j, l, k)) + + # Dlk := 2 if a change of nozzles in the level l + 1 on placement head k + # Dlk := 1 if there are no batches placed on levels higher than l + D = {} + for l in range(L): + for k in range(K): + D[l, k] = model.NewIntVar(0, 2, 'D_{}_{}'.format(l, k)) + + D_abs = {} + for l in range(L): + for j in range(J): + for k in range(K): + D_abs[l, j, k] = model.NewIntVar(0, M, 'D_abs_{}_{}_{}'.format(l, j, k)) + + # == Objective function === + model.Minimize(a * WL + b * sum(N[k] for k in range(K))) + + # === Constraint === + for i in range(I): + model.Add(sum(X[i, j, k] for j in range(J) for k in range(K)) == component_list[cpidx_2_part[i]]) + + for k in range(K): + model.Add(sum(X[i, j, k] for i in range(I) for j in range(J)) <= WL) + + for i in range(I): + for j in range(J): + for k in range(K): + model.Add(X[i, j, k] <= M * sum(Z[i, j, l, k] for l in range(L))) + + for i in range(I): + for j in range(J): + for k in range(K): + model.Add(sum(Z[i, j, l, k] for l in range(L)) <= 1) + + for i in range(I): + for j in range(J): + for k in range(K): + model.Add(sum(Z[i, j, l, k] for l in range(L)) <= X[i, j, k]) + + for k in range(K): + for l in range(L - 1): + model.Add(sum(Z[i, j, l, k] for j in range(J) for i in range(I)) >= sum( + Z[i, j, l + 1, k] for j in range(J) for i in range(I))) + + for l in range(I): + for k in range(K): + model.Add(sum(Z[i, j, l, k] for i in range(I) for j in range(J)) <= 1) + + for l in range(L - 1): + for j in range(J): + for k in range(K): + model.AddAbsEquality(D_abs[l, j, k], + sum(Z[i, j, l, k] for i in range(I)) - sum(Z[i, j, l + 1, k] for i in range(I))) + + for k in range(K): + for l in range(L): + model.Add(D[l, k] == sum(D_abs[l, j, k] for j in range(J))) + + for k in range(K): + model.Add(N[k] == sum(D[l, k] for l in range(L)) - 1) + + for l in range(L): + for k in range(K): + model.Add(0 >= sum(HC[i][j] * Z[i, j, l, k] for i in range(I) for j in range(J))) + + # === Main Process === + component_result, cycle_result = [], [] + feeder_slot_result, placement_result, head_sequence = [], [], [] + solver.parameters.max_time_in_seconds = 20.0 + + status = solver.Solve(model) + if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: + print('total cost = {}'.format(solver.ObjectiveValue())) + + # convert cp model solution to standard output + model_cycle_result, model_component_result = [], [] + for l in range(L): + model_component_result.append([None for _ in range(K)]) + model_cycle_result.append([0 for _ in range(K)]) + for k in range(K): + for i in range(I): + for j in range(J): + if solver.BooleanValue(Z[i, j, l, k]) != 0: + model_component_result[-1][k] = cpidx_2_part[i] + model_cycle_result[-1][k] = solver.Value(X[i, j, k]) + + # remove redundant term + if sum(model_cycle_result[-1]) == 0: + model_component_result.pop() + model_cycle_result.pop() + + head_component_index = [0 for _ in range(max_head_index)] + while True: + head_cycle = [] + for head, index in enumerate(head_component_index): + head_cycle.append(model_cycle_result[index][head]) + + if len([cycle for cycle in head_cycle if cycle > 0]) == 0: + break + + component_result.append([None for _ in range(max_head_index)]) + min_cycle = min([cycle for cycle in head_cycle if cycle > 0]) + for head, index in enumerate(head_component_index): + if model_cycle_result[index][head] != 0: + component_result[-1][head] = model_component_result[index][head] + else: + continue + + model_cycle_result[index][head] -= min_cycle + if model_cycle_result[index][head] == 0 and index + 1 < len(model_cycle_result): + head_component_index[head] += 1 + + cycle_result.append(min_cycle) + + part_2_index = {} + for index, data in component_data.iterrows(): + part_2_index[data['part']] = index + + for cycle in range(len(component_result)): + for head in range(max_head_index): + part = component_result[cycle][head] + component_result[cycle][head] = -1 if part is None else part_2_index[part] + + feeder_slot_result = feeder_assignment(component_data, pcb_data, component_result, cycle_result) + + # === phase 2: heuristic method === + mount_point_pos = defaultdict(list) + for pcb_idx, data in pcb_data.iterrows(): + part = data['part'] + part_index = component_data[component_data['part'] == part].index.tolist()[0] + mount_point_pos[part_index].append([data['x'], data['y'], pcb_idx]) + + for index_ in mount_point_pos.keys(): + mount_point_pos[index_].sort(key=lambda x: (x[1], x[0])) + + for cycle_idx, _ in enumerate(cycle_result): + for _ in range(cycle_result[cycle_idx]): + placement_result.append([-1 for _ in range(max_head_index)]) + for head in range(max_head_index): + if component_result[cycle_idx][head] == -1: + continue + index_ = component_result[cycle_idx][head] + + placement_result[-1][head] = mount_point_pos[index_][-1][2] + mount_point_pos[index_].pop() + head_sequence.append(dynamic_programming_cycle_path(pcb_data, placement_result[-1], feeder_slot_result[cycle_idx])) + + else: + warnings.warn('No solution found!', UserWarning) + + return component_result, cycle_result, feeder_slot_result, placement_result, head_sequence diff --git a/base_optimizer/optimizer_celldivision.py b/base_optimizer/optimizer_celldivision.py new file mode 100644 index 0000000..f7fb9a4 --- /dev/null +++ b/base_optimizer/optimizer_celldivision.py @@ -0,0 +1,204 @@ +from base_optimizer.optimizer_common import * +from result_analysis import * + + +def convert_cell_2_result(pcb_data, component_data, component_cell, population): + assert component_cell['points'].sum() == len(pcb_data) + head_assignment = [[] for _ in range(max_head_index)] + + wl = [0 for _ in range(max_head_index)] # workload + + e1, e2, e3 = 1, 0.5, 1. / 6 + + component_result, cycle_result, feeder_slot_result = [], [], [] + for index in population: + if component_cell.loc[index]['points'] == 0: + continue + # 元胞对应的元件类型和贴装点数 + component_type, component_points = component_cell.loc[index, 'index'], component_cell.loc[index, 'points'] + + nozzle_change, maxwl = [0 for _ in range(max_head_index)], [0 for _ in range(max_head_index)] + for head in range(max_head_index): + if head_assignment[head]: + assigned_part = head_assignment[head][-1][0] + if component_data.loc[assigned_part]['nz'] != component_data.loc[component_type]['nz']: + nozzle_change[head] = 1 + wl1 = wl.copy() + wl1[head] += component_points + maxwl[head] = max(wl1) + e1 * nozzle_change[head] + + awl, wl2 = min(maxwl), wl.copy() + for idx, val in enumerate(maxwl): + if val > awl: + wl2[idx] += e3 + head_ = wl2.index(min(wl2)) + wl[head_] += component_points + head_assignment[head_].append([component_type, component_points]) + + head_assignment_counter = [0 for _ in range(max_head_index)] + while True: + assigned_part, assigned_cycle = [-1 for _ in range(max_head_index)], [0 for _ in range(max_head_index)] + for head in range(max_head_index): + counter = head_assignment_counter[head] + + if head_assignment[head] and head_assignment[head][counter][1] > 0: + assigned_part[head] = head_assignment[head][counter][0] + assigned_cycle[head] = head_assignment[head][counter][1] + + nonzero_cycle = [cycle for cycle in assigned_cycle if cycle > 0] + if not nonzero_cycle: + break + + cycle = min(nonzero_cycle) + cycle_result.append(cycle) + component_result.append(assigned_part) + + for head in range(max_head_index): + counter = head_assignment_counter[head] + + if head_assignment[head] and head_assignment[head][counter][1] > 0: + head_assignment[head][counter][1] -= cycle_result[-1] + if head_assignment[head][counter][1] == 0 and counter < len(head_assignment[head]) - 1: + head_assignment_counter[head] += 1 + + feeder_slot_result = feeder_assignment(component_data, pcb_data, component_result, cycle_result) + return component_result, cycle_result, feeder_slot_result + + +@timer_wrapper +def optimizer_celldivision(pcb_data, component_data, hinter=True): + # Crossover method: Two-point crossover + # Mutation method: Swap + # Parent selection method: Roulette wheel + # Termination condition: 20 successive non-improvement iterations + population_size = 40 # 种群规模 + crossover_rate, mutation_rate = .6, .02 + golden_section = 0.618 + + # 获取元件元胞 + point_num = len(pcb_data) + component_cell = pd.DataFrame({'index': np.arange(len(component_data)), 'points': np.zeros(len(component_data), dtype=int)}) + for point_cnt in range(point_num): + part = pcb_data.loc[point_cnt, 'fdr'].split(' ', 1)[1] + index = np.where(component_data['part'].values == part) + component_cell.loc[index[0], 'points'] += 1 + component_cell = component_cell[~component_cell['points'].isin([0])] + + # component_cell.sort_values(by = "points" , inplace = True, ascending = False) + best_population, best_component_cell = [], [] + min_pop_val = float('inf') # 最优种群价值 + Div, Imp = 0, 0 + while True: + # randomly generate permutations + generation_ = np.array(component_cell.index) + pop_generation = [] + for _ in range(population_size): + np.random.shuffle(generation_) + pop_generation.append(generation_.tolist()) + + pop_val = [] + for pop in range(population_size): + component_result, cycle_result, feeder_slot_result = convert_cell_2_result(pcb_data, component_data, + component_cell, + pop_generation[pop]) + pop_val.append( + component_assign_evaluate(component_data, component_result, cycle_result, feeder_slot_result)) + + # 初始化随机生成种群 + Upit = int(1.5 * np.sqrt(len(component_cell))) + + while Div < Upit: + if hinter: + print('----- current div : ' + str(Div) + ' , total div : ' + str(Upit) + ' -----') + + # 选择 + new_pop_generation, new_pop_val = [], [] + top_k_index = get_top_k_value(pop_val, int(population_size * 0.3)) + for index in top_k_index: + new_pop_generation.append(pop_generation[index]) + new_pop_val.append(pop_val[index]) + index = [i for i in range(population_size)] + + select_index = random.choices(index, weights=pop_val, k=population_size - int(population_size * 0.3)) + for index in select_index: + new_pop_generation.append(pop_generation[index]) + new_pop_val.append(pop_val[index]) + pop_generation, pop_val = new_pop_generation, new_pop_val + + # 交叉 + for pop in range(population_size): + if pop % 2 == 0 and np.random.random() < crossover_rate: + index1, index2 = roulette_wheel_selection(pop_val), -1 + while True: + index2 = roulette_wheel_selection(pop_val) + if index1 != index2: + break + # 两点交叉算子 + pop_generation[index1], pop_generation[index2] = partially_mapped_crossover(pop_generation[index1], + pop_generation[index2]) + + if np.random.random() < mutation_rate: + index_ = roulette_wheel_selection(pop_val) + swap_mutation(pop_generation[index_]) + + # 将元件元胞分配到各个吸杆上,计算价值函数 + for pop in range(population_size): + component_result, cycle_result, feeder_slot_result = convert_cell_2_result(pcb_data, component_data, + component_cell, + pop_generation[pop]) + pop_val[pop] = component_assign_evaluate(component_data, component_result, cycle_result, + feeder_slot_result) + assert(pop_val[pop] > 0) + + if min(pop_val) < min_pop_val: + min_pop_val = min(pop_val) + best_population = copy.deepcopy(pop_generation[np.argmin(pop_val)]) + best_component_cell = copy.deepcopy(component_cell) + Div, Imp = 0, 1 + else: + Div += 1 + + if Imp == 1: + Div, Imp = 0, 0 + # Section: cell division operation + if hinter: + print(' ------------- cell division operation ------------- ') + division_component_cell = pd.DataFrame() + for idx, rows in component_cell.iterrows(): + if component_cell.loc[idx, 'points'] <= 1: + division_component_cell = pd.concat([division_component_cell, pd.DataFrame([rows])], + ignore_index=True) + else: + division_component_cell = pd.concat([division_component_cell, pd.DataFrame([rows] * 2)], + ignore_index=True) + + rows_counter = len(division_component_cell) + division_points = int(max(np.ceil(division_component_cell.loc[rows_counter - 2, + 'points'] * golden_section), 1)) + # 避免出现空元胞的情形 + if division_points == 0 or division_points == division_component_cell.loc[ + rows_counter - 2, 'points']: + division_component_cell.loc[rows_counter - 2, 'points'] = 1 + else: + division_component_cell.loc[rows_counter - 2, 'points'] = division_points + + division_component_cell.loc[rows_counter - 1, 'points'] -= division_component_cell.loc[ + rows_counter - 2, 'points'] + + if division_component_cell.loc[rows_counter - 2, 'points'] == 0 or division_component_cell.loc[ + rows_counter - 1, 'points'] == 0: + raise ValueError + + component_cell = division_component_cell + + # 完成分裂后重新生成染色体组 + generation_ = np.array(range(len(component_cell))) + pop_generation = [] + for _ in range(population_size): + np.random.shuffle(generation_) + pop_generation.append(generation_.tolist()) + else: + break + + assert(len(best_component_cell) == len(best_population)) + return convert_cell_2_result(pcb_data, component_data, best_component_cell, best_population) diff --git a/base_optimizer/optimizer_common.py b/base_optimizer/optimizer_common.py new file mode 100644 index 0000000..95a1d68 --- /dev/null +++ b/base_optimizer/optimizer_common.py @@ -0,0 +1,954 @@ +import copy +import time +import math +import random +import argparse +import os +import warnings +import copy + +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt + +from functools import wraps +from collections import defaultdict +from tqdm import tqdm + +# 整线参数 +max_machine_index = 3 + +# 时间参数 +T_pp, T_tr, T_nc = 2, 5, 25 + +# 机器参数 +max_head_index, max_slot_index = 6, 120 +interval_ratio = 2 +slot_interval = 15 +head_interval = slot_interval * interval_ratio +head_nozzle = ['' for _ in range(max_head_index)] # 头上已经分配吸嘴 + +# 位置信息 +slotf1_pos, slotr1_pos = [-31.267, 44.], [807., 810.545] # F1(前基座最左侧)、R1(后基座最右侧)位置 +fix_camera_pos = [269.531, 694.823] # 固定相机位置 +anc_marker_pos = [336.457, 626.230] # ANC基准点位置 +stopper_pos = [635.150, 124.738] # 止档块位置 + +# 算法权重参数 +e_nz_change, e_gang_pick = 4, 0.6 + +# 电机参数 +head_rotary_velocity = 8e-5 # 贴装头R轴旋转时间 +x_max_velocity, y_max_velocity = 1.4, 1.2 +x_max_acceleration, y_max_acceleration = x_max_velocity / 0.079, y_max_velocity / 0.079 + +# 不同种类供料器宽度 +feeder_width = {'SM8': (7.25, 7.25), 'SM12': (7.00, 20.00), 'SM16': (7.00, 22.00), + 'SM24': (7.00, 29.00), 'SM32': (7.00, 44.00)} + +# 可用吸嘴数量限制 +nozzle_limit = {'CN065': 6, 'CN040': 6, 'CN220': 6, 'CN400': 6, 'CN140': 6} + + +def axis_moving_time(distance, axis=0): + distance = abs(distance) * 1e-3 + Lamax = x_max_velocity ** 2 / x_max_acceleration if axis == 0 else y_max_velocity ** 2 / y_max_acceleration + Tmax = x_max_velocity / x_max_acceleration if axis == 0 else y_max_velocity / y_max_acceleration + if axis == 0: + return 2 * math.sqrt(distance / x_max_acceleration) if distance < Lamax else 2 * Tmax + ( + distance - Lamax) / x_max_velocity + else: + return 2 * math.sqrt(distance / y_max_acceleration) if distance < Lamax else 2 * Tmax + ( + distance - Lamax) / y_max_velocity + + +def head_rotary_time(angle): + while -180 > angle > 180: + if angle > 180: + angle -= 360 + else: + angle += 360 + return abs(angle) * head_rotary_velocity + + +def find_commonpart(head_group, feeder_group): + feeder_group_len = len(feeder_group) + + max_length, max_common_part = -1, [] + for offset in range(-max_head_index + 1, feeder_group_len - 1): + # offset: head_group相对于feeder_group的偏移量 + length, common_part = 0, [] + for hd_index in range(max_head_index): + fd_index = hd_index + offset + if fd_index < 0 or fd_index >= feeder_group_len: + common_part.append(-1) + continue + + if head_group[hd_index] == feeder_group[fd_index] and head_group[hd_index] != -1: + length += 1 + common_part.append(head_group[hd_index]) + else: + common_part.append(-1) + if length > max_length: + max_length = length + max_common_part = common_part + + return max_common_part + + +def timer_wrapper(func): + @wraps(func) + def measure_time(*args, **kwargs): + start_time = time.time() + result = func(*args, **kwargs) + + print("function {} running time : {} s".format(func.__name__, time.time() - start_time)) + return result + + return measure_time + + +def feeder_assignment(component_data, pcb_data, component_result, cycle_result): + # Section: 供料器分配结果 + feeder_slot_result, feeder_group_result = [], [] + feeder_limit = defaultdict(int) + for component in range(len(component_data)): + feeder_limit[component] = component_data.loc[component]['feeder-limit'] + + for component_cycle in component_result: + new_feeder_group = [] + for component in component_cycle: + if component == -1 or feeder_limit[component] == 0 or new_feeder_group.count(component) >= feeder_limit[component]: + new_feeder_group.append(-1) + else: + new_feeder_group.append(component) + + if len(new_feeder_group) == 0: + continue + + while sum(i >= 0 for i in new_feeder_group) != 0: + max_common_part, index = [], -1 + max_common_length = -1 + for feeder_index in range(len(feeder_group_result)): + common_part = find_commonpart(new_feeder_group, feeder_group_result[feeder_index]) + if sum(i > 0 for i in common_part) > max_common_length: + max_common_length = sum(i > 0 for i in common_part) + max_common_part, index = common_part, feeder_index + + new_feeder_length = 0 + for feeder in new_feeder_group: + if feeder != -1 and feeder_limit[feeder] > 0: + new_feeder_length += 1 + + if new_feeder_length > max_common_length: + # 新分配供料器 + feeder_group_result.append([]) + for feeder_index in range(len(new_feeder_group)): + feeder = new_feeder_group[feeder_index] + if feeder != -1 and feeder_limit[feeder] > 0: + feeder_group_result[-1].append(feeder) + new_feeder_group[feeder_index] = -1 + feeder_limit[feeder] -= 1 + else: + feeder_group_result[-1].append(-1) + else: + # 使用旧供料器 + for feeder_index, feeder_part in enumerate(max_common_part): + if feeder_part != -1: + new_feeder_group[feeder_index] = -1 + + # 去除多余的元素 + for feeder_group in feeder_group_result: + while len(feeder_group) > 0 and feeder_group[0] == -1: + feeder_group.pop(0) + + while len(feeder_group) > 0 and feeder_group[-1] == -1: + feeder_group.pop(-1) + + # 确定供料器组的安装位置 + point_num = len(pcb_data) + component_pos = [[] for _ in range(len(component_data))] + for point_cnt in range(point_num): + part = pcb_data.loc[point_cnt, 'part'] + index = np.where(component_data['part'].values == part)[0] + component_pos[index[0]].append(pcb_data.loc[point_cnt, 'x'] + stopper_pos[0]) + + # 元件使用的头 + CT_Head = defaultdict(list) + for component_cycle in component_result: + for head, component in enumerate(component_cycle): + if component == -1: + continue + if component not in CT_Head: + CT_Head[component] = [head, head] + CT_Head[component][0] = min(CT_Head[component][0], head) + CT_Head[component][1] = max(CT_Head[component][1], head) + + # 供料器组分配的优先顺序 + feeder_assign_sequence = [] + for i in range(len(feeder_group_result)): + for j in range(len(feeder_group_result)): + if j in feeder_assign_sequence: + continue + + if len(feeder_assign_sequence) == i: + feeder_assign_sequence.append(j) + else: + seq = feeder_assign_sequence[-1] + if cycle_result[seq] * len([k for k in feeder_group_result[seq] if k >= 0]) < cycle_result[j] * len( + [k for k in feeder_group_result[seq] if k >= 0]): + feeder_assign_sequence.pop(-1) + feeder_assign_sequence.append(j) + + # TODO: 暂未考虑机械限位 + feeder_group_slot = [-1] * len(feeder_group_result) + feeder_lane_state = [0] * max_slot_index # 0表示空,1表示已占有 + for index in feeder_assign_sequence: + feeder_group = feeder_group_result[index] + best_slot = [] + for cp_index, component in enumerate(feeder_group): + if component == -1: + continue + best_slot.append(round((sum(component_pos[component]) / len(component_pos[component]) - slotf1_pos[ + 0]) / slot_interval) + 1 - cp_index * interval_ratio) + best_slot = round(sum(best_slot) / len(best_slot)) + + search_dir, step = 0, 0 # dir: 1-向右, 0-向左 + left_out_range, right_out_range = False, False + while True: + assign_slot = best_slot + step if search_dir else best_slot - step + # 出现越界,反向搜索 + if assign_slot + (len(feeder_group) - 1) * interval_ratio >= max_slot_index / 2: + right_out_range = True + search_dir = 0 + step += 1 + elif assign_slot < 0: + left_out_range = True + search_dir = 1 + step += 1 + else: + if left_out_range or right_out_range: + step += 1 # 单向搜索 + else: + search_dir = 1 - search_dir # 双向搜索 + if search_dir == 0: + step += 1 + + assign_available = True + + # === 分配对应槽位 === + for slot in range(assign_slot, assign_slot + interval_ratio * len(feeder_group), interval_ratio): + feeder_index = int((slot - assign_slot) / interval_ratio) + pick_part = feeder_group[feeder_index] + if feeder_lane_state[slot] == 1 and pick_part != -1: + assign_available = False + break + + if pick_part != -1 and (slot - CT_Head[pick_part][0] * interval_ratio <= 0 or + slot + (max_head_index - CT_Head[pick_part][1] - 1) * interval_ratio > max_slot_index // 2): + assign_available = False + break + + if assign_available: + for idx, part in enumerate(feeder_group): + if part != -1: + feeder_lane_state[assign_slot + idx * interval_ratio] = 1 + feeder_group_slot[index] = assign_slot + break + + if feeder_group_slot[index] == -1: + raise Exception('feeder assign error!') + + # 按照最大匹配原则,确定各元件周期拾取槽位 + for component_cycle in component_result: + feeder_slot_result.append([-1] * max_head_index) + head_index = [head for head, component in enumerate(component_cycle) if component >= 0] + while head_index: + max_overlap_counter = 0 + overlap_feeder_group_index, overlap_feeder_group_offset = -1, -1 + for feeder_group_idx, feeder_group in enumerate(feeder_group_result): + # offset 头1 相对于 供料器组第一个元件的偏移量 + for offset in range(-max_head_index + 1, max_head_index + len(feeder_group)): + overlap_counter = 0 + for head in head_index: + if 0 <= head + offset < len(feeder_group) and component_cycle[head] == \ + feeder_group[head + offset]: + overlap_counter += 1 + + if overlap_counter > max_overlap_counter: + max_overlap_counter = overlap_counter + overlap_feeder_group_index, overlap_feeder_group_offset = feeder_group_idx, offset + + feeder_group = feeder_group_result[overlap_feeder_group_index] + head_index_cpy = copy.deepcopy(head_index) + + for idx, head in enumerate(head_index_cpy): + if 0 <= head + overlap_feeder_group_offset < len(feeder_group) and component_cycle[head] == \ + feeder_group[head + overlap_feeder_group_offset]: + feeder_slot_result[-1][head] = feeder_group_slot[overlap_feeder_group_index] + interval_ratio * ( + head + overlap_feeder_group_offset) + head_index.remove(head) + + return feeder_slot_result + + +def dynamic_programming_cycle_path(pcb_data, cycle_placement, assigned_feeder): + head_sequence = [] + num_pos = sum([placement != -1 for placement in cycle_placement]) + 1 + + pos, head_set = [], [] + feeder_set = set() + for head, feeder in enumerate(assigned_feeder): + if feeder == -1: + continue + + head_set.append(head) + placement = cycle_placement[head] + if feeder != -1 and placement == -1: + print(assigned_feeder) + print(cycle_placement) + + pos.append([pcb_data.loc[placement]['x'] - head * head_interval + stopper_pos[0], + pcb_data.loc[placement]['y'] + stopper_pos[1]]) + + feeder_set.add(feeder - head * interval_ratio) + + pos.insert(0, [slotf1_pos[0] + ((min(list(feeder_set)) + max(list(feeder_set))) / 2 - 1) * slot_interval, + slotf1_pos[1]]) + + def get_distance(pos_1, pos_2): + return math.sqrt((pos_1[0] - pos_2[0]) ** 2 + (pos_1[1] - pos_2[1]) ** 2) + + # 各节点之间的距离 + dist = [[get_distance(pos_1, pos_2) for pos_2 in pos] for pos_1 in pos] + + min_dist = [[np.inf for _ in range(num_pos)] for s in range(1 << num_pos)] + min_path = [[[] for _ in range(num_pos)] for s in range(1 << num_pos)] + + # 状压dp搜索 + for s in range(1, 1 << num_pos, 2): + # 考虑节点集合s必须包括节点0 + if not (s & 1): + continue + for j in range(1, num_pos): + # 终点j需在当前考虑节点集合s内 + if not (s & (1 << j)): + continue + if s == int((1 << j) | 1): + # 若考虑节点集合s仅含节点0和节点j,dp边界,赋予初值 + # print('j:', j) + min_path[s][j] = [j] + min_dist[s][j] = dist[0][j] + + # 枚举下一个节点i,更新 + for i in range(1, num_pos): + # 下一个节点i需在考虑节点集合s外 + if s & (1 << i): + continue + if min_dist[s][j] + dist[j][i] < min_dist[s | (1 << i)][i]: + min_path[s | (1 << i)][i] = min_path[s][j] + [i] + min_dist[s | (1 << i)][i] = min_dist[s][j] + dist[j][i] + + ans_dist = float('inf') + ans_path = [] + # 求最终最短哈密顿回路 + for i in range(1, num_pos): + if min_dist[(1 << num_pos) - 1][i] + dist[i][0] < ans_dist: + # 更新,回路化 + ans_path = min_path[s][i] + ans_dist = min_dist[(1 << num_pos) - 1][i] + dist[i][0] + + for parent in ans_path: + head_sequence.append(head_set[parent - 1]) + + start_head, end_head = head_sequence[0], head_sequence[-1] + if pcb_data.loc[cycle_placement[start_head]]['x'] - start_head * head_interval > \ + pcb_data.loc[cycle_placement[end_head]]['x'] - end_head * head_interval: + head_sequence = list(reversed(head_sequence)) + return head_sequence + + +@timer_wrapper +def greedy_placement_route_generation(component_data, pcb_data, component_result, cycle_result, feeder_slot_result): + placement_result, head_sequence_result = [], [] + mount_point_index = [[] for _ in range(len(component_data))] + mount_point_pos = [[] for _ in range(len(component_data))] + + for i in range(len(pcb_data)): + part = pcb_data.loc[i]['part'] + component_index = component_data[component_data['part'] == part].index.tolist()[0] + # 记录贴装点序号索引和对应的位置坐标 + mount_point_index[component_index].append(i) + mount_point_pos[component_index].append([pcb_data.loc[i]['x'], pcb_data.loc[i]['y']]) + + search_dir = 1 # 0:自左向右搜索 1:自右向左搜索 + for cycle_set in range(len(component_result)): + floor_cycle, ceil_cycle = sum(cycle_result[:cycle_set]), sum(cycle_result[:(cycle_set + 1)]) + for cycle in range(floor_cycle, ceil_cycle): + # search_dir = 1 - search_dir + assigned_placement = [-1] * max_head_index + max_pos = [max(mount_point_pos[component_index], key=lambda x: x[0]) for component_index in + range(len(mount_point_pos)) if len(mount_point_pos[component_index]) > 0][0][0] + min_pos = [min(mount_point_pos[component_index], key=lambda x: x[0]) for component_index in + range(len(mount_point_pos)) if len(mount_point_pos[component_index]) > 0][0][0] + point2head_range = min(math.floor((max_pos - min_pos) / head_interval) + 1, max_head_index) + + # 最近邻确定 + way_point = None + head_range = range(max_head_index - 1, -1, -1) if search_dir else range(max_head_index) + for head_counter, head in enumerate(head_range): + if component_result[cycle_set][head] == -1: + continue + + component_index = component_result[cycle_set][head] + if way_point is None or head_counter % point2head_range == 0: + index = 0 + if way_point is None: + if search_dir: + index = np.argmax(mount_point_pos[component_index], axis=0)[0] + else: + index = np.argmin(mount_point_pos[component_index], axis=0)[0] + else: + for next_head in head_range: + component_index = component_result[cycle_set][next_head] + if assigned_placement[next_head] == -1 and component_index != -1: + num_points = len(mount_point_pos[component_index]) + index = np.argmin( + [abs(mount_point_pos[component_index][i][0] - way_point[0]) * .1 + abs( + mount_point_pos[component_index][i][1] - way_point[1]) for i in + range(num_points)]) + head = next_head + break + # index = np.argmax(mount_point_pos[component_index], axis=0)[0] + assigned_placement[head] = mount_point_index[component_index][index] + + # 记录路标点 + way_point = mount_point_pos[component_index][index] + way_point[0] += (max_head_index - head - 1) * head_interval if search_dir else -head * head_interval + + mount_point_index[component_index].pop(index) + mount_point_pos[component_index].pop(index) + else: + head_index, point_index = -1, -1 + min_cheby_distance, min_euler_distance = float('inf'), float('inf') + for next_head in range(max_head_index): + if assigned_placement[next_head] != -1 or component_result[cycle_set][next_head] == -1: + continue + next_comp_index = component_result[cycle_set][next_head] + for counter in range(len(mount_point_pos[next_comp_index])): + if search_dir: + delta_x = abs(mount_point_pos[next_comp_index][counter][0] - way_point[0] + + (max_head_index - next_head - 1) * head_interval) + else: + delta_x = abs(mount_point_pos[next_comp_index][counter][0] - way_point[0] + - next_head * head_interval) + + delta_y = abs(mount_point_pos[next_comp_index][counter][1] - way_point[1]) + + euler_distance = pow(axis_moving_time(delta_x, 0), 2) + pow(axis_moving_time(delta_y, 1), 2) + cheby_distance = max(axis_moving_time(delta_x, 0), + axis_moving_time(delta_y, 1)) + 5e-2 * euler_distance + if cheby_distance < min_cheby_distance or (abs(cheby_distance - min_cheby_distance) < 1e-9 + and euler_distance < min_euler_distance): + # if euler_distance < min_euler_distance: + min_cheby_distance, min_euler_distance = cheby_distance, euler_distance + head_index, point_index = next_head, counter + + component_index = component_result[cycle_set][head_index] + assert (0 <= head_index < max_head_index) + + assigned_placement[head_index] = mount_point_index[component_index][point_index] + way_point = mount_point_pos[component_index][point_index] + way_point[0] += (max_head_index - head_index - 1) * head_interval if search_dir \ + else -head_index * head_interval + + mount_point_index[component_index].pop(point_index) + mount_point_pos[component_index].pop(point_index) + + placement_result.append(assigned_placement) # 各个头上贴装的元件类型 + head_sequence_result.append( + dynamic_programming_cycle_path(pcb_data, assigned_placement, feeder_slot_result[cycle_set])) + + return placement_result, head_sequence_result + + +@timer_wrapper +def beam_search_for_route_generation(component_data, pcb_data, component_result, cycle_result, feeder_slot_result): + beam_width = 4 # 集束宽度 + base_points = [float('inf'), float('inf')] + + mount_point_index = [[] for _ in range(len(component_data))] + mount_point_pos = [[] for _ in range(len(component_data))] + + for i in range(len(pcb_data)): + part = pcb_data.loc[i]['part'] + component_index = component_data[component_data['part'] == part].index.tolist()[0] + + # 记录贴装点序号索引和对应的位置坐标 + mount_point_index[component_index].append(i) + mount_point_pos[component_index].append([pcb_data.loc[i]['x'], pcb_data.loc[i]['y']]) + + # 记录最左下角坐标 + if mount_point_pos[component_index][-1][0] < base_points[0]: + base_points[0] = mount_point_pos[component_index][-1][0] + if mount_point_pos[component_index][-1][1] < base_points[1]: + base_points[1] = mount_point_pos[component_index][-1][1] + + beam_placement_sequence, beam_head_sequence = [], [] + beam_mount_point_index, beam_mount_point_pos = [], [] + + for beam_counter in range(beam_width): + beam_mount_point_index.append(copy.deepcopy(mount_point_index)) + beam_mount_point_pos.append(copy.deepcopy(mount_point_pos)) + + beam_placement_sequence.append([]) + beam_head_sequence.append([]) + + beam_distance = [0 for _ in range(beam_width)] # 记录当前集束搜索点的点数 + def argpartition(list, kth): + if kth < len(list): + return np.argpartition(list, kth) + else: + index, indexes = 0, [] + while len(indexes) < kth: + indexes.append(index) + index += 1 + if index >= len(list): + index = 0 + return np.array(indexes) + + with tqdm(total=100) as pbar: + search_dir = 0 + pbar.set_description('route schedule') + for cycle_set in range(len(component_result)): + floor_cycle, ceil_cycle = sum(cycle_result[:cycle_set]), sum(cycle_result[:(cycle_set + 1)]) + for cycle in range(floor_cycle, ceil_cycle): + search_dir = 1 - search_dir + beam_way_point = None + for beam_counter in range(beam_width): + beam_placement_sequence[beam_counter].append([-1 for _ in range(max_head_index)]) + + head_range = range(max_head_index - 1, -1, -1) if search_dir else range(max_head_index) + for head in head_range: + component_index = component_result[cycle_set][head] + if component_index == -1: + continue + + if beam_way_point is None: + # 首个贴装点的选取,距离基准点最近的beam_width个点 + beam_way_point = [[0, 0]] * beam_width + + for beam_counter in range(beam_width): + if search_dir: + index = np.argmax(beam_mount_point_pos[beam_counter][component_index], axis=0)[0] + else: + index = np.argmin(beam_mount_point_pos[beam_counter][component_index], axis=0)[0] + + beam_placement_sequence[beam_counter][-1][head] = beam_mount_point_index[beam_counter][component_index][index] + + beam_way_point[beam_counter] = beam_mount_point_pos[beam_counter][component_index][index] + beam_way_point[beam_counter][0] += (max_head_index - head - 1) * head_interval if \ + search_dir else -head * head_interval + + beam_mount_point_index[beam_counter][component_index].pop(index) + beam_mount_point_pos[beam_counter][component_index].pop(index) + else: + # 后续贴装点 + search_beam_distance = [] + search_beam_index = [0] * (beam_width ** 2) + for beam_counter in range(beam_width ** 2): + search_beam_distance.append(beam_distance[beam_counter // beam_width]) + + for beam_counter in range(beam_width): + # 对于集束beam_counter + 1最近的beam_width个点 + num_points = len(beam_mount_point_pos[beam_counter][component_index]) + + dist = [] + for i in range(num_points): + if search_dir: + delta_x = axis_moving_time( + beam_mount_point_pos[beam_counter][component_index][i][0] - + beam_way_point[beam_counter][0] + (max_head_index - head - 1) * head_interval, + 0) + else: + delta_x = axis_moving_time( + beam_mount_point_pos[beam_counter][component_index][i][0] - + beam_way_point[beam_counter][0] - head * head_interval, 0) + + delta_y = axis_moving_time(beam_mount_point_pos[beam_counter][component_index][i][1] - + beam_way_point[beam_counter][1], 1) + + dist.append(max(delta_x, delta_y)) + + indexes = argpartition(dist, kth=beam_width)[:beam_width] + + # 记录中间信息 + for i, index in enumerate(indexes): + search_beam_distance[i + beam_counter * beam_width] += dist[index] + search_beam_index[i + beam_counter * beam_width] = index + + indexes = np.argsort(search_beam_distance) + + beam_mount_point_pos_cpy = copy.deepcopy(beam_mount_point_pos) + beam_mount_point_index_cpy = copy.deepcopy(beam_mount_point_index) + + beam_placement_sequence_cpy = copy.deepcopy(beam_placement_sequence) + beam_head_sequence_cpy = copy.deepcopy(beam_head_sequence) + beam_counter = 0 + assigned_placement = [] + + for i, index in enumerate(indexes): + # 拷贝原始集束数据 + beam_mount_point_pos[beam_counter] = copy.deepcopy(beam_mount_point_pos_cpy[index // beam_width]) + beam_mount_point_index[beam_counter] = copy.deepcopy(beam_mount_point_index_cpy[index // beam_width]) + beam_placement_sequence[beam_counter] = copy.deepcopy(beam_placement_sequence_cpy[index // beam_width]) + beam_head_sequence[beam_counter] = copy.deepcopy(beam_head_sequence_cpy[index // beam_width]) + + # 更新各集束最新扫描的的贴装点 + component_index = component_result[cycle_set][head] + + beam_placement_sequence[beam_counter][-1][head] = \ + beam_mount_point_index[beam_counter][component_index][search_beam_index[index]] + + if beam_placement_sequence[beam_counter][ + -1] in assigned_placement and beam_width - beam_counter < len(indexes) - i: + continue + + assigned_placement.append(beam_placement_sequence[beam_counter][-1]) + + # 更新参考基准点 + beam_way_point[beam_counter] = beam_mount_point_pos[beam_counter][component_index][search_beam_index[index]] + beam_way_point[beam_counter][0] += (max_head_index - head - 1) * head_interval if \ + search_dir else -head * head_interval + + # 更新各集束贴装路径长度,移除各集束已分配的贴装点 + beam_distance[beam_counter] = search_beam_distance[index] + + beam_mount_point_pos[beam_counter][component_index].pop(search_beam_index[index]) + beam_mount_point_index[beam_counter][component_index].pop(search_beam_index[index]) + + beam_counter += 1 + + if beam_counter >= beam_width: + break + assert(beam_counter >= beam_width) + + # 更新头贴装顺序 + for beam_counter in range(beam_width): + beam_head_sequence[beam_counter].append( + dynamic_programming_cycle_path(pcb_data, beam_placement_sequence[beam_counter][-1], + feeder_slot_result[cycle_set])) + + pbar.update(1 / sum(cycle_result) * 100) + + index = np.argmin(beam_distance) + return beam_placement_sequence[index], beam_head_sequence[index] + + +def optimal_nozzle_assignment(component_data, pcb_data): + # === Nozzle Assignment === + # number of points for nozzle & number of heads for nozzle + nozzle_points, nozzle_assigned_counter = defaultdict(int), defaultdict(int) + if len(pcb_data) == 0: + return nozzle_assigned_counter + for _, step in pcb_data.iterrows(): + part = step['part'] + idx = component_data[component_data['part'] == part].index.tolist()[0] + nozzle = component_data.loc[idx]['nz'] + + nozzle_assigned_counter[nozzle] = 0 + nozzle_points[nozzle] += 1 + + assert len(nozzle_points.keys()) <= max_head_index + total_points, available_head = len(pcb_data), max_head_index + # S1: set of nozzle types which are sufficient to assign one nozzle to the heads + # S2: temporary nozzle set + # S3: set of nozzle types which already have the maximum reasonable nozzle amounts. + S1, S2, S3 = [], [], [] + + for nozzle in nozzle_points.keys(): # Phase 1 + if nozzle_points[nozzle] * max_head_index < total_points: + nozzle_assigned_counter[nozzle] = 1 + available_head -= 1 + total_points -= nozzle_points[nozzle] + + S1.append(nozzle) + else: + S2.append(nozzle) + + available_head_ = available_head # Phase 2 + for nozzle in S2: + nozzle_assigned_counter[nozzle] = math.floor(available_head * nozzle_points[nozzle] / total_points) + available_head_ = available_head_ - nozzle_assigned_counter[nozzle] + + S2.sort(key=lambda x: nozzle_points[x] / (nozzle_assigned_counter[x] + 1e-10), reverse=True) + while available_head_ > 0: + nozzle = S2[0] + nozzle_assigned_counter[nozzle] += 1 + + S2.remove(nozzle) + S3.append(nozzle) + available_head_ -= 1 + + phase_iteration = len(S2) - 1 + while phase_iteration > 0: # Phase 3 + nozzle_i_val, nozzle_j_val = 0, 0 + nozzle_i, nozzle_j = None, None + for nozzle in S2: + if nozzle_i is None or nozzle_points[nozzle] / nozzle_assigned_counter[nozzle] > nozzle_i_val: + nozzle_i_val = nozzle_points[nozzle] / nozzle_assigned_counter[nozzle] + nozzle_i = nozzle + + if nozzle_assigned_counter[nozzle] > 1: + if nozzle_j is None or nozzle_points[nozzle] / (nozzle_assigned_counter[nozzle] - 1) < nozzle_j_val: + nozzle_j_val = nozzle_points[nozzle] / (nozzle_assigned_counter[nozzle] - 1) + nozzle_j = nozzle + + if nozzle_i and nozzle_j and nozzle_points[nozzle_j] / (nozzle_assigned_counter[nozzle_j] - 1) < \ + nozzle_points[nozzle_i] / nozzle_assigned_counter[nozzle_i]: + nozzle_assigned_counter[nozzle_j] -= 1 + nozzle_assigned_counter[nozzle_i] += 1 + S2.remove(nozzle_i) + S3.append(nozzle_i) + else: + break + + return nozzle_assigned_counter + + +# === 遗传算法公用函数 === +def sigma_scaling(pop_val, c: float): + # function: f' = max(f - (avg(f) - c · sigma(f), 0) + avg_val = sum(pop_val) / len(pop_val) + sigma_val = math.sqrt(sum(abs(v - avg_val) for v in pop_val) / len(pop_val)) + + for idx, val in enumerate(pop_val): + pop_val[idx] = max(val - (avg_val - c * sigma_val), 0) + return pop_val + + +def directed_edge_recombination_crossover(c, individual1, individual2): + assert len(individual1) == len(individual2) + left_edge_list, right_edge_list = defaultdict(list), defaultdict(list) + + for index in range(len(individual1) - 1): + elem1, elem2 = individual1[index], individual1[index + 1] + right_edge_list[elem1].append(elem2) + left_edge_list[elem2].append(elem1) + + for index in range(len(individual2) - 1): + elem1, elem2 = individual2[index], individual2[index + 1] + right_edge_list[elem1].append(elem2) + left_edge_list[elem2].append(elem1) + + offspring = [] + while len(offspring) != len(individual1): + while True: + center_element = np.random.choice(individual1) + if center_element not in offspring: # 避免重复选取 + break + direction, candidate = 1, [center_element] + parent = center_element + for edge_list in left_edge_list.values(): + while parent in edge_list: + edge_list.remove(parent) + + for edge_list in right_edge_list.values(): + while parent in edge_list: + edge_list.remove(parent) + + while True: + max_len, max_len_neighbor = -1, 0 + if direction == 1: + if len(right_edge_list[parent]) == 0: + direction, parent = -1, center_element + continue + for neighbor in right_edge_list[parent]: + if max_len < len(right_edge_list[neighbor]): + max_len_neighbor = neighbor + max_len = len(right_edge_list[neighbor]) + candidate.append(max_len_neighbor) + parent = max_len_neighbor + elif direction == -1: + if len(left_edge_list[parent]) == 0: + direction, parent = 0, center_element + continue + for neighbor in left_edge_list[parent]: + if max_len < len(left_edge_list[neighbor]): + max_len_neighbor = neighbor + max_len = len(left_edge_list[neighbor]) + candidate.insert(0, max_len_neighbor) + parent = max_len_neighbor + else: + break + + # 移除重复元素 + for edge_list in left_edge_list.values(): + while max_len_neighbor in edge_list: + edge_list.remove(max_len_neighbor) + + for edge_list in right_edge_list.values(): + while max_len_neighbor in edge_list: + edge_list.remove(max_len_neighbor) + + offspring += candidate + + return offspring + + +def partially_mapped_crossover(parent1, parent2): + range_ = np.random.randint(0, len(parent1), 2) # 前闭后开 + range_ = sorted(range_) + + parent1_cpy, parent2_cpy = [-1 for _ in range(len(parent1))], [-1 for _ in range(len(parent2))] + + parent1_cpy[range_[0]: range_[1] + 1] = copy.deepcopy(parent2[range_[0]: range_[1] + 1]) + parent2_cpy[range_[0]: range_[1] + 1] = copy.deepcopy(parent1[range_[0]: range_[1] + 1]) + + for index in range(len(parent1)): + if range_[0] <= index <= range_[1]: + continue + + cur_ptr, cur_elem = 0, parent1[index] + while True: + parent1_cpy[index] = cur_elem + if parent1_cpy.count(cur_elem) == 1: + break + parent1_cpy[index] = -1 + + if cur_ptr == 0: + cur_ptr, cur_elem = 1, parent2[index] + else: + index_ = parent1_cpy.index(cur_elem) + cur_elem = parent2[index_] + + for index in range(len(parent2)): + if range_[0] <= index <= range_[1]: + continue + + cur_ptr, cur_elem = 0, parent2[index] + while True: + parent2_cpy[index] = cur_elem + if parent2_cpy.count(cur_elem) == 1: + break + parent2_cpy[index] = -1 + + if cur_ptr == 0: + cur_ptr, cur_elem = 1, parent1[index] + else: + index_ = parent2_cpy.index(cur_elem) + cur_elem = parent1[index_] + + return parent1_cpy, parent2_cpy + + +def cycle_crossover(parent1, parent2): + offspring1, offspring2 = [-1 for _ in range(len(parent1))], [-1 for _ in range(len(parent2))] + + idx = 0 + while True: + if offspring1[idx] != -1: + break + offspring1[idx] = parent1[idx] + idx = parent1.index(parent2[idx]) + + for idx, gene in enumerate(offspring1): + if gene == -1: + offspring1[idx] = parent2[idx] + + idx = 0 + while True: + if offspring2[idx] != -1: + break + offspring2[idx] = parent2[idx] + idx = parent2.index(parent1[idx]) + + for idx, gene in enumerate(offspring2): + if gene == -1: + offspring2[idx] = parent1[idx] + + return offspring1, offspring2 + + +def swap_mutation(parent): + range_ = np.random.randint(0, len(parent), 2) + parent[range_[0]], parent[range_[1]] = parent[range_[1]], parent[range_[0]] + return parent + + +def constraint_swap_mutation(component_points, individual): + offspring = individual.copy() + + idx, component_index = 0, random.randint(0, len(component_points) - 1) + for points in component_points.values(): + if component_index == 0: + while True: + index1, index2 = random.sample(range(points + max_machine_index - 2), 2) + if offspring[idx + index1] != offspring[idx + index2]: + break + + clip = offspring[idx: idx + points + max_machine_index - 1].copy() + avl_machine = 0 + for idx_, gene in enumerate(clip): + if gene == 0 and (idx_ == 0 or clip[idx_ - 1] != 0): + avl_machine += 1 + + clip[index1], clip[index2] = clip[index2], clip[index1] + for idx_, gene in enumerate(clip): + if gene == 0 and (idx_ == 0 or clip[idx_ - 1] != 0): + avl_machine -= 1 + + if avl_machine != 0: + return offspring + + offspring[idx + index1], offspring[idx + index2] = offspring[idx + index2], offspring[idx + index1] + break + + component_index -= 1 + idx += (points + max_machine_index - 1) + + return offspring + + +def random_selective(data, possibility): # 依概率选择随机数 + assert len(data) == len(possibility) and len(data) > 0 + + sum_val = sum(possibility) + possibility = [p / sum_val for p in possibility] + + random_val = random.random() + for idx, val in enumerate(possibility): + random_val -= val + if random_val <= 0: + break + return data[idx] + + +def insert_mutation(parent): + pos, val = np.random.randint(0, len(parent), 1), parent[-1] + parent[pos: len(parent) - 1] = parent[pos + 1:] + parent[pos] = val + return parent + + +def roulette_wheel_selection(pop_eval): + # Roulette wheel + random_val = np.random.random() * sum(pop_eval) + for idx, val in enumerate(pop_eval): + random_val -= val + if random_val <= 0: + return idx + return len(pop_eval) - 1 + + +def get_top_k_value(pop_val, k: int, reverse=True): + res = [] + pop_val_cpy = copy.deepcopy(pop_val) + pop_val_cpy.sort(reverse=reverse) + + for i in range(min(len(pop_val_cpy), k)): + for j in range(len(pop_val)): + if abs(pop_val_cpy[i] - pop_val[j]) < 1e-9 and j not in res: + res.append(j) + break + return res diff --git a/base_optimizer/optimizer_feederpriority.py b/base_optimizer/optimizer_feederpriority.py new file mode 100644 index 0000000..2dbb071 --- /dev/null +++ b/base_optimizer/optimizer_feederpriority.py @@ -0,0 +1,720 @@ +from base_optimizer.optimizer_common import * + + +@timer_wrapper +def feeder_allocate(component_data, pcb_data, feeder_data, nozzle_pattern, figure=False): + + feeder_points, feeder_division_points = defaultdict(int), defaultdict(int) # 供料器贴装点数 + mount_center_pos = defaultdict(int) + + feeder_limit, feeder_arrange = defaultdict(int), defaultdict(int) + part_nozzle = defaultdict(str) + + feeder_base = [-2] * max_slot_index # 已安装在供料器基座上的元件(-2: 未分配,-1: 占用状态) + feeder_base_points = [0] * max_slot_index # 供料器基座结余贴装点数量 + + for data in pcb_data.iterrows(): + pos, part = data[1]['x'] + stopper_pos[0], data[1]['part'] + + part_index = component_data[component_data['part'] == part].index.tolist()[0] + if part not in component_data: + feeder_limit[part_index] = component_data.loc[part_index]['feeder-limit'] + feeder_arrange[part_index] = 0 + + feeder_points[part_index] += 1 + mount_center_pos[part_index] += ((pos - mount_center_pos[part_index]) / feeder_points[part_index]) + part_nozzle[part_index] = component_data.loc[part_index]['nz'] + + for part_index, points in feeder_points.items(): + feeder_division_points[part_index] = max(points // feeder_limit[part_index], 1) + + nozzle_component, nozzle_component_points = defaultdict(list), defaultdict(list) + for part, nozzle in part_nozzle.items(): + for _ in range(feeder_limit[part]): + nozzle_component[nozzle].append(part) + nozzle_component_points[nozzle].append(feeder_points[part]) + + if feeder_data is not None: + for _, feeder in feeder_data.iterrows(): + slot, part = feeder['slot'], feeder['part'] + part_index = component_data[component_data['part'] == part].index.tolist()[0] + + # 供料器基座分配位置和对应贴装点数 + feeder_base[slot], feeder_base_points[slot] = part_index, feeder_division_points[part_index] + + feeder_type = component_data.loc[part_index]['fdr'] + extra_width = feeder_width[feeder_type][0] + feeder_width[feeder_type][1] - slot_interval + while extra_width > 0: + slot += 1 + feeder_base[slot] = -1 + extra_width -= slot_interval + + feeder_limit[part_index] -= 1 + feeder_arrange[part_index] += 1 + if feeder_limit[part_index] < 0: + info = 'the number of arranged feeder for [' + part + '] exceeds the quantity limit' + raise ValueError(info) + + for nozzle, components in nozzle_component.items(): + if part_index in components: + index_ = components.index(part_index) + + nozzle_component[nozzle].pop(index_) + nozzle_component_points[nozzle].pop(index_) + break + + nozzle_assigned_counter = optimal_nozzle_assignment(component_data, pcb_data) + head_assign_indexes = list(range(max_head_index)) + nozzle_pattern, optimal_nozzle_pattern, optimal_nozzle_points = [], None, 0 + # nozzle_pattern = ['CN220', 'CN065','CN065','CN065','CN065','CN220'] + + # 先排序 + nozzle_pattern_list = [] + for nozzle, counter in nozzle_assigned_counter.items(): + nozzle_pattern_list.append([nozzle, sum(nozzle_component_points[nozzle]) // counter]) + nozzle_pattern_list.sort(key=lambda x: x[1], reverse=True) + + # 后确定吸嘴分配模式 + head_index = [3, 2, 4, 1, 5, 0] + nozzle_pattern = [0] * max_head_index + for nozzle, _ in nozzle_pattern_list: + counter = nozzle_assigned_counter[nozzle] + while counter: + nozzle_pattern[head_index[0]] = nozzle + counter -= 1 + head_index.pop(0) + + while True: + best_assign, best_assign_points = [], [] + best_assign_slot, best_assign_value = -1, -np.Inf + best_nozzle_component, best_nozzle_component_points = None, None + for slot in range(1, max_slot_index // 2 - (max_head_index - 1) * interval_ratio + 1): + nozzle_assigned_counter_cpy = copy.deepcopy(nozzle_assigned_counter) + + feeder_assign, feeder_assign_points = [], [] + tmp_feeder_limit, tmp_feeder_points = feeder_limit.copy(), feeder_points.copy() + tmp_nozzle_component, tmp_nozzle_component_points = copy.deepcopy(nozzle_component), copy.deepcopy( + nozzle_component_points) + + # 记录扫描到的已安装的供料器元件类型 + for head in range(max_head_index): + feeder_assign.append(feeder_base[slot + head * interval_ratio]) + + if scan_part := feeder_assign[-1] >= 0: + nozzle = part_nozzle[scan_part] + feeder_assign_points.append(feeder_base_points[slot + head * interval_ratio]) + if feeder_assign_points[-1] <= 0: + feeder_assign[-1], feeder_assign_points[-1] = -1, 0 + elif nozzle in nozzle_assigned_counter_cpy.keys(): + nozzle_assigned_counter_cpy[nozzle] -= 1 + if nozzle_assigned_counter_cpy[nozzle] == 0: + nozzle_assigned_counter_cpy.pop(nozzle) + else: + feeder_assign_points.append(0) + + if -2 not in feeder_assign: # 无可用槽位 + if sum(feeder_assign_points) > optimal_nozzle_points: + optimal_nozzle_points = sum(feeder_assign_points) + optimal_nozzle_pattern = [''] * max_head_index + for head in range(max_head_index): + optimal_nozzle_pattern[head] = part_nozzle[feeder_assign[head]] + continue + + assign_part_stack, assign_part_stack_points = [], [] + for idx in head_assign_indexes: + if feeder_assign[idx] != -2: + continue + + if len(nozzle_pattern) == 0: # 吸嘴匹配模式为空,优先分配元件,根据分配元件倒推吸嘴匹配模式 + nozzle_assign = '' + max_points, max_nozzle_points = 0, 0 + for nozzle in nozzle_assigned_counter_cpy.keys(): + if len(tmp_nozzle_component[nozzle]) == 0: + continue + part = max(tmp_nozzle_component[nozzle], + key=lambda x: tmp_feeder_points[x] / tmp_feeder_limit[x] if + tmp_feeder_points[x] != 0 else 0) + index_ = tmp_nozzle_component[nozzle].index(part) + if max_points < tmp_nozzle_component_points[nozzle][index_]: + max_points, nozzle_assign = tmp_nozzle_component_points[nozzle][index_], nozzle + else: + # 吸嘴匹配模式非空,按对应吸嘴类型进行元件分配 + nozzle_assign = nozzle_pattern[idx] + + if len(tmp_nozzle_component[nozzle_assign]) == 0: + # 当前头对应吸嘴类型无可用元件,将计划分配的元件压入堆栈 + part = max(tmp_feeder_points.keys(), + key=lambda x: tmp_feeder_points[x] / tmp_feeder_limit[x] if tmp_feeder_limit[ + x] != 0 else 0) + for nozzle, component_list in tmp_nozzle_component.items(): + if part in component_list: + nozzle_assign = nozzle + + assign_part_stack.append(part) + assign_part_stack_points.append(feeder_division_points[part]) + break + else: + # 当前头对应吸嘴类型有可用元件,直接分配对应类型的元件 + index_ = tmp_nozzle_component[nozzle_assign].index(max(tmp_nozzle_component[nozzle_assign], + key=lambda x: tmp_feeder_points[x] / + tmp_feeder_limit[x] if + tmp_feeder_limit[x] != 0 else 0)) + + part = tmp_nozzle_component[nozzle_assign][index_] + + feeder_type = component_data.loc[part]['fdr'] + extra_width, extra_slot = feeder_width[feeder_type][0] + feeder_width[feeder_type][1] - slot_interval, 1 + slot_overlap = False + while extra_width > 0: + slot_ = slot + idx * interval_ratio + extra_slot + if feeder_base[slot_] != -2 or slot_ > max_slot_index // 2: + slot_overlap = True + break + extra_width -= slot_interval + extra_slot += 1 + + # 可用供料器数目充足且不存在和已有供料器的占位冲突 + if tmp_feeder_limit[part] > 0 and not slot_overlap: + feeder_assign[idx], feeder_assign_points[idx] = part, feeder_division_points[part] + extra_width, extra_head = feeder_width[feeder_type][0] + feeder_width[feeder_type][ + 1] - head_interval, 1 + while extra_width > 0 and idx + extra_head < max_head_index: + feeder_assign[idx + extra_head] = -1 + extra_head += 1 + extra_width -= head_interval + else: + part = -1 # 存在位置冲突的元件,不占用可用供料器数 + + # 更新吸嘴匹配模式的吸嘴数 + if nozzle_assign in nozzle_assigned_counter_cpy.keys(): + nozzle_assigned_counter_cpy[nozzle_assign] -= 1 + if nozzle_assigned_counter_cpy[nozzle_assign] == 0: + nozzle_assigned_counter_cpy.pop(nozzle_assign) + + if part >= 0 and tmp_feeder_limit[part] == 0: + continue + + if part in tmp_nozzle_component[nozzle_assign]: + part_index = tmp_nozzle_component[nozzle_assign].index(part) + + tmp_nozzle_component[nozzle_assign].pop(part_index) + tmp_nozzle_component_points[nozzle_assign].pop(part_index) + + tmp_feeder_limit[part] -= 1 + tmp_feeder_points[part] -= feeder_division_points[part] + + # 元件堆栈出栈,首先分配吸嘴类型一致的头 + if nozzle_pattern: + for head, feeder in enumerate(feeder_assign): + if feeder != -2: + continue + for idx, part in enumerate(assign_part_stack): + + feeder_type = component_data.loc[part]['fdr'] + extra_width, extra_slot = feeder_width[feeder_type][0] + feeder_width[feeder_type][ + 1] - slot_interval, 1 + + slot_overlap = False + while extra_width > 0: + slot_ = slot + head * interval_ratio + extra_slot + if feeder_base[slot_] != -2 or slot_ > max_slot_index // 2: + slot_overlap = True + break + extra_width -= slot_interval + extra_slot += 1 + + if component_data.loc[part]['nz'] == nozzle_pattern[head] and not slot_overlap: + feeder_assign[head], feeder_assign_points[head] = assign_part_stack[idx], \ + assign_part_stack_points[idx] + + assign_part_stack.pop(idx) + assign_part_stack_points.pop(idx) + break + + # 元件堆栈,然后分配元件堆栈中未分配的其它元件 + for head in head_assign_indexes: + if feeder_assign[head] != -2 or len(assign_part_stack) == 0: + continue + part, points = assign_part_stack[0], assign_part_stack_points[0] + + feeder_type = component_data.loc[part]['fdr'] + extra_width, extra_slot = feeder_width[feeder_type][0] + feeder_width[feeder_type][1] - slot_interval, 1 + + slot_overlap = False + while extra_width > 0: + slot_ = slot + head * interval_ratio + extra_slot + if feeder_base[slot_] != -2 or slot_ > max_slot_index // 2: + slot_overlap = True + break + extra_width -= slot_interval + extra_slot += 1 + + if not slot_overlap: + feeder_assign[head], feeder_assign_points[head] = part, points + extra_width, extra_head = feeder_width[feeder_type][0] + feeder_width[feeder_type][ + 1] - head_interval, 1 + while extra_width > 0 and head + extra_head < max_head_index: + feeder_assign[head + extra_head] = -1 + extra_head += 1 + extra_width -= head_interval + else: + # 返还由于机械限位无法分配的,压入元件堆栈中的元素 + nozzle = component_data.loc[part]['nz'] + tmp_nozzle_component[nozzle].insert(0, part) + tmp_nozzle_component_points[nozzle].insert(0, points) + + assign_part_stack.pop(0) + assign_part_stack_points.pop(0) + + # 仍然存在由于机械限位,无法进行分配的在堆栈中的元件 + while assign_part_stack: + part, points = assign_part_stack[0], assign_part_stack_points[0] + nozzle = component_data.loc[part]['nz'] + + tmp_nozzle_component[nozzle].insert(0, part) + tmp_nozzle_component_points[nozzle].insert(0, points) + + assign_part_stack.pop(0) + assign_part_stack_points.pop(0) + + nozzle_change_counter, average_slot = 0, [] + for head, feeder_ in enumerate(feeder_assign): + if feeder_ < 0: + continue + average_slot.append( + (mount_center_pos[feeder_] - slotf1_pos[0]) / slot_interval + 1 - head * interval_ratio) + if nozzle_pattern and component_data.loc[feeder_]['nz'] != nozzle_pattern[head]: + nozzle_change_counter += 1 + + if len(average_slot) == 0: + continue + + average_slot = sum(average_slot) / len(average_slot) + assign_value = 0 + feeder_assign_points_cpy = feeder_assign_points.copy() + while True: + points_filter = list(filter(lambda x: x > 0, feeder_assign_points_cpy)) + if not points_filter: + break + assign_value += e_gang_pick * min(points_filter) * (len(points_filter) - 1) + for head, _ in enumerate(feeder_assign_points_cpy): + if feeder_assign_points_cpy[head] == 0: + continue + feeder_assign_points_cpy[head] -= min(points_filter) + + assign_value -= 1e2 * e_nz_change * nozzle_change_counter + 1e-5 * abs(slot - average_slot) + + if assign_value >= best_assign_value and sum(feeder_assign_points) != 0: + + best_assign_value = assign_value + best_assign = feeder_assign.copy() + best_assign_points = feeder_assign_points.copy() + best_assign_slot = slot + best_nozzle_component, best_nozzle_component_points = tmp_nozzle_component, tmp_nozzle_component_points + + if not best_assign_points: + break + + if len(nozzle_pattern) == 0: + nozzle_pattern = [''] * max_head_index + for idx, part in enumerate(best_assign): + if part < 0: + continue + + # 新安装的供料器 + if feeder_base[best_assign_slot + idx * interval_ratio] != part: + # 除去分配给最大化同时拾取周期的项,保留结余项 + feeder_base_points[best_assign_slot + idx * interval_ratio] += ( + feeder_division_points[part] - min(filter(lambda x: x > 0, best_assign_points))) + + feeder_points[part] -= feeder_division_points[part] + feeder_limit[part] -= 1 + feeder_arrange[part] += 1 + + if feeder_limit[part] == 0: + feeder_division_points[part] = 0 + for nozzle, components in nozzle_component.items(): + if part in components: + index_ = components.index(part) + + nozzle_component[nozzle].pop(index_) + nozzle_component_points[nozzle].pop(index_) + break + feeder_division_points[part] = 0 + else: + # 已有的供料器 + feeder_base_points[best_assign_slot + idx * interval_ratio] -= min( + filter(lambda x: x > 0, best_assign_points)) + + # 更新供料器基座信息 + feeder_base[best_assign_slot + idx * interval_ratio] = part + + feeder_type, extra_slot = component_data.loc[part]['fdr'], 0 + extra_width = feeder_width[feeder_type][0] + feeder_width[feeder_type][1] - slot_interval + while extra_width > 0: + extra_slot += 1 + if feeder_base[best_assign_slot + idx * interval_ratio + extra_slot] == -2: + feeder_base[best_assign_slot + idx * interval_ratio + extra_slot] = -1 # 标记槽位已占用 + extra_width -= slot_interval + + # 更新吸嘴信息 + nozzle_pattern[idx] = component_data.loc[part]['nz'] + + # 更新头分配的先后顺序 + head_assign_indexes = np.array(best_assign_points).argsort().tolist() + + nozzle_component, nozzle_component_points = copy.deepcopy(best_nozzle_component), copy.deepcopy( + best_nozzle_component_points) + + if sum(best_assign_points) > optimal_nozzle_points: + optimal_nozzle_points = sum(best_assign_points) + optimal_nozzle_pattern = nozzle_pattern.copy() + + assert not list(filter(lambda x: x < 0, feeder_limit.values())) # 分配供料器数目在限制范围内 + + # 若所有供料器均安装在基座上,重新对基座进行扫描,确定最优吸嘴模式(有序) + if not optimal_nozzle_points: + feeder_base, feeder_base_points = [-2] * max_slot_index, [0] * max_slot_index + for _, feeder in feeder_data.iterrows(): + slot, part = feeder['slot'], feeder['part'] + part_index = component_data[component_data['part'] == part].index.tolist()[0] + + # 供料器基座分配位置和对应贴装点数 + feeder_base[slot], feeder_base_points[slot] = part_index, feeder_division_points[part_index] + + # 前基座 TODO: 后基座 + for slot in range(max_slot_index // 2 - (max_head_index - 1) * interval_ratio): + sum_scan_points = 0 + for head in range(max_head_index): + sum_scan_points += feeder_base_points[slot + head * interval_ratio] + + if sum_scan_points > optimal_nozzle_points: + optimal_nozzle_pattern = ['' for _ in range(max_head_index)] + for head in range(max_head_index): + if part := feeder_base[slot + head * interval_ratio] == -2: + continue + optimal_nozzle_pattern[head] = part_nozzle[part] + + # 更新供料器占位信息 + for _, data in feeder_data.iterrows(): + feeder_base[data['slot']] = -1 + + for slot, feeder in enumerate(feeder_base): + if feeder < 0: + continue + part = component_data.loc[feeder]['part'] + + feeder_data.loc[len(feeder_data.index)] = [slot, part, 0] + + if figure: + # 绘制供料器位置布局 + for slot in range(max_slot_index // 2): + plt.scatter(slotf1_pos[0] + slot_interval * slot, slotf1_pos[1], marker='x', s=12, color='black', alpha=0.5) + plt.text(slotf1_pos[0] + slot_interval * slot, slotf1_pos[1] - 45, slot + 1, ha='center', va='bottom', + size=8) + + feeder_assign_range = [] + for feeder in feeder_data.iterrows(): + slot, part = feeder[1]['slot'], feeder[1]['part'] + part_index = component_data[component_data['part'] == part].index.tolist()[0] + feeder_type = component_data.loc[part_index]['fdr'] + width = feeder_width[feeder_type][0] + feeder_width[feeder_type][1] + start = slotf1_pos[0] + slot_interval * (slot - 1) - slot_interval / 2 + end = slotf1_pos[0] + slot_interval * (slot - 1) - slot_interval / 2 + width + + rec_x = [start, end, end, start] + rec_y = [slotf1_pos[1] - 40, slotf1_pos[1] - 40, slotf1_pos[1] + 10, slotf1_pos[1] + 10] + + c = 'red' if feeder[1]['arg'] == 0 else 'black' # 黑色表示已分配,红色表示新分配 + plt.text(slotf1_pos[0] + slot_interval * (slot - 1), slotf1_pos[1] + 12, + part + ': ' + str(feeder_points[part_index]), ha='center', size=7, rotation=90, color=c) + + plt.fill(rec_x, rec_y, facecolor='yellow', alpha=0.4) + + feeder_assign_range.append([start, end]) + + # 记录重叠区间 + feeder_assign_range.sort(key=lambda x: x[0]) + for i in range(1, len(feeder_assign_range)): + if feeder_assign_range[i][0] < feeder_assign_range[i - 1][1]: + start, end = feeder_assign_range[i][0], feeder_assign_range[i - 1][1] + + rec_x = [start, end, end, start] + rec_y = [slotf1_pos[1] - 40, slotf1_pos[1] - 40, slotf1_pos[1] + 10, slotf1_pos[1] + 10] + plt.fill(rec_x, rec_y, facecolor='red') + + plt.plot([slotf1_pos[0] - slot_interval / 2, slotf1_pos[0] + slot_interval * (max_slot_index // 2 - 1 + 0.5)], + [slotf1_pos[1] + 10, slotf1_pos[1] + 10], color='black') + plt.plot([slotf1_pos[0] - slot_interval / 2, slotf1_pos[0] + slot_interval * (max_slot_index // 2 - 1 + 0.5)], + [slotf1_pos[1] - 40, slotf1_pos[1] - 40], color='black') + + for counter in range(max_slot_index // 2 + 1): + pos = slotf1_pos[0] + (counter - 0.5) * slot_interval + plt.plot([pos, pos], [slotf1_pos[1] + 10, slotf1_pos[1] - 40], color='black', linewidth=1) + + plt.ylim(-10, 100) + plt.show() + + return optimal_nozzle_pattern + + +@timer_wrapper +def feeder_base_scan(component_data, pcb_data, feeder_data, nozzle_pattern): + feeder_assign_check = set() + for feeder in feeder_data.iterrows(): + feeder_assign_check.add(feeder[1]['part']) + + component_points = [0] * len(component_data) + for step in pcb_data.iterrows(): + part = step[1]['part'] + part_index = component_data[component_data['part'] == part].index.tolist()[0] + + component_points[part_index] += 1 + nozzle_type = component_data.loc[part_index]['nz'] + if nozzle_type not in nozzle_limit.keys() or nozzle_limit[nozzle_type] <= 0: + info = 'there is no available nozzle [' + nozzle_type + '] for the assembly process' + raise ValueError(info) + + assert len(feeder_assign_check) == len(component_points) - component_points.count(0) # 所有供料器均已分配槽位 + feeder_part = [-1] * max_slot_index + for feeder in feeder_data.iterrows(): + part, slot = feeder[1]['part'], feeder[1]['slot'] + part_index = component_data[component_data['part'] == part].index.tolist() + if len(part_index) != 1: + print('unregistered component: ', part, ' in slot', slot) + continue + part_index = part_index[0] + feeder_part[slot] = part_index + + component_result, cycle_result, feeder_slot_result = [], [], [] # 贴装点索引和拾取槽位优化结果 + + nozzle_mode = [nozzle_pattern] # 吸嘴匹配模式 + with tqdm(total=len(pcb_data)) as pbar: + pbar.set_description('feeder scan process') + pbar_prev = 0 + value_increment_base = 0 + while True: + # === 周期内循环 === + assigned_part = [-1 for _ in range(max_head_index)] # 当前扫描到的头分配元件信息 + assigned_cycle = [0 for _ in range(max_head_index)] # 当前扫描到的元件最大分配次数 + assigned_slot = [-1 for _ in range(max_head_index)] # 当前扫描到的供料器分配信息 + + best_assigned_eval_func = -float('inf') + nozzle_insert_cycle = 0 + for cycle_index, nozzle_cycle in enumerate(nozzle_mode): + scan_eval_func_list = [] # 若干次扫描得到的最优解 + # nozzle_cycle 吸嘴模式下,已扫描到的最优结果 + cur_scan_part = [-1 for _ in range(max_head_index)] + cur_scan_cycle = [0 for _ in range(max_head_index)] + cur_scan_slot = [-1 for _ in range(max_head_index)] + cur_nozzle_limit = copy.deepcopy(nozzle_limit) + + while True: + best_scan_part, best_scan_cycle = [-1 for _ in range(max_head_index)], [-1 for _ in + range(max_head_index)] + best_scan_slot = [-1 for _ in range(max_head_index)] + best_scan_nozzle_limit = copy.deepcopy(cur_nozzle_limit) + + scan_eval_func, search_break = -float('inf'), True + + # 前供料器基座扫描 + for slot in range(1, max_slot_index // 2 - (max_head_index - 1) * interval_ratio + 1): + scan_cycle, scan_part, scan_slot = cur_scan_cycle.copy(), cur_scan_part.copy(), cur_scan_slot.copy() + scan_nozzle_limit = copy.deepcopy(cur_nozzle_limit) + + # 预扫描确定各类型元件拾取数目(前瞻) + preview_scan_part = defaultdict(int) + for head in range(max_head_index): + part = feeder_part[slot + head * interval_ratio] + + # 贴装头和拾取槽位满足对应关系 + if scan_part[head] == -1 and part != -1 and component_points[part] > 0 and scan_part.count( + part) < component_points[part]: + preview_scan_part[part] += 1 + + component_counter = 0 + for head in range(max_head_index): + part = feeder_part[slot + head * interval_ratio] + # 1.匹配条件满足: 贴装头和拾取槽位满足对应关系 + if scan_part[head] == -1 and part != -1 and component_points[part] > 0 and scan_part.count( + part) < component_points[part]: + # 2.匹配条件满足:不超过可用吸嘴数的限制 + nozzle = component_data.loc[part]['nz'] + if scan_nozzle_limit[nozzle] <= 0: + continue + + # 3.增量条件满足: 引入新的元件类型不会使代价函数的值减少(前瞻) + if scan_cycle.count(0) == max_head_index: + gang_pick_change = component_points[part] + else: + prev_cycle = min(filter(lambda x: x > 0, scan_cycle)) + # 同时拾取数的提升 + gang_pick_change = min(prev_cycle, component_points[part] // preview_scan_part[part]) + + # 4.拾取移动距离条件满足: 邻近元件进行同时抓取,降低移动路径长度 + # reference_slot = -1 + # for head_, slot_ in enumerate(scan_slot): + # if slot_ != -1: + # reference_slot = slot_ - head_ * interval_ratio + # if reference_slot != -1 and abs(reference_slot - slot) > (max_head_index - 1) * interval_ratio: + # continue + + # 5.同时拾取的增量 和 吸嘴更换次数比较 + prev_nozzle_change = 0 + if cycle_index + 1 < len(nozzle_mode): + prev_nozzle_change = 2 * (nozzle_cycle[head] != nozzle_mode[cycle_index + 1][head]) + + # 避免首个周期吸杆占用率低的问题 + if nozzle_cycle[head] == '': + nozzle_change = 0 + else: + nozzle_change = 2 * (nozzle != nozzle_cycle[head]) + + if cycle_index + 1 < len(nozzle_mode): + nozzle_change += 2 * (nozzle != nozzle_mode[cycle_index + 1][head]) + nozzle_change -= prev_nozzle_change + + val = e_gang_pick * gang_pick_change - e_nz_change * nozzle_change + if val < value_increment_base: + continue + + component_counter += 1 + + scan_part[head] = part + scan_cycle[head] = component_points[part] // preview_scan_part[part] + scan_slot[head] = slot + head * interval_ratio + + scan_nozzle_limit[nozzle] -= 1 + + nozzle_counter = 0 # 吸嘴更换次数 + # 上一周期 + for head, nozzle in enumerate(nozzle_cycle): + if scan_part[head] == -1: + continue + if component_data.loc[scan_part[head]]['nz'] != nozzle and nozzle != '': + nozzle_counter += 2 + + # 下一周期(额外增加的吸嘴更换次数) + if cycle_index + 1 < len(nozzle_mode): + for head, nozzle in enumerate(nozzle_mode[cycle_index + 1]): + if scan_part[head] == -1: + continue + prev_counter, new_counter = 0, 0 + if nozzle_cycle[head] != nozzle and nozzle_cycle[head] != '' and nozzle != '': + prev_counter += 2 + if component_data.loc[scan_part[head]]['nz'] != nozzle and nozzle != '': + new_counter += 2 + nozzle_counter += new_counter - prev_counter + else: + for head, nozzle in enumerate(nozzle_mode[0]): + if scan_part[head] == -1: + continue + prev_counter, new_counter = 0, 0 + if nozzle_cycle[head] != nozzle and nozzle_cycle[head] != '' and nozzle != '': + prev_counter += 2 + if component_data.loc[scan_part[head]]['nz'] != nozzle and nozzle != '': + new_counter += 2 + nozzle_counter += new_counter - prev_counter + + if component_counter == 0: # 当前情形下未扫描到任何元件 + continue + + search_break = False + + scan_part_head = defaultdict(list) + for head, part in enumerate(scan_part): + if part == -1: + continue + scan_part_head[part].append(head) + + for part, heads in scan_part_head.items(): + part_cycle = component_points[part] // len(heads) + for head in heads: + scan_cycle[head] = part_cycle + + # 计算扫描后的代价函数,记录扫描后的最优解 + # 短期收益 + cycle = min(filter(lambda x: x > 0, scan_cycle)) + gang_pick_counter, gang_pick_slot_set = 0, set() + for head, pick_slot in enumerate(scan_slot): + gang_pick_slot_set.add(pick_slot - head * interval_ratio) + + eval_func_short_term = e_gang_pick * (max_head_index - scan_slot.count(-1) - len( + gang_pick_slot_set)) * cycle - e_nz_change * nozzle_counter + + # 长期收益 + gang_pick_slot_dict = defaultdict(list) + for head, pick_slot in enumerate(scan_slot): + if pick_slot == -1: + continue + gang_pick_slot_dict[pick_slot - head * interval_ratio].append(scan_cycle[head]) + + eval_func_long_term = 0 + for pick_cycle in gang_pick_slot_dict.values(): + while pick_cycle: + min_cycle = min(pick_cycle) + eval_func_long_term += e_gang_pick * (len(pick_cycle) - 1) * min(pick_cycle) + pick_cycle = list(map(lambda c: c - min_cycle, pick_cycle)) + pick_cycle = list(filter(lambda c: c > 0, pick_cycle)) + eval_func_long_term -= e_nz_change * nozzle_counter + + ratio = 0.5 + eval_func = (1 - ratio) * eval_func_short_term + ratio * eval_func_long_term + if eval_func >= scan_eval_func: + scan_eval_func = eval_func + best_scan_part, best_scan_cycle = scan_part.copy(), scan_cycle.copy() + best_scan_slot = scan_slot.copy() + + best_scan_nozzle_limit = copy.deepcopy(scan_nozzle_limit) + + if search_break: + break + + scan_eval_func_list.append(scan_eval_func) + + cur_scan_part = best_scan_part.copy() + cur_scan_slot = best_scan_slot.copy() + cur_scan_cycle = best_scan_cycle.copy() + + cur_nozzle_limit = copy.deepcopy(best_scan_nozzle_limit) + + if len(scan_eval_func_list) != 0: + if sum(scan_eval_func_list) >= best_assigned_eval_func: + best_assigned_eval_func = sum(scan_eval_func_list) + + assigned_part = cur_scan_part.copy() + assigned_slot = cur_scan_slot.copy() + assigned_cycle = cur_scan_cycle.copy() + + nozzle_insert_cycle = cycle_index + + # 从供料器基座中移除对应数量的贴装点 + nonzero_cycle = [cycle for cycle in assigned_cycle if cycle > 0] + if not nonzero_cycle: + value_increment_base -= max_head_index + continue + + for head, slot in enumerate(assigned_slot): + if assigned_part[head] == -1: + continue + component_points[feeder_part[slot]] -= min(nonzero_cycle) + + component_result.insert(nozzle_insert_cycle, assigned_part) + cycle_result.insert(nozzle_insert_cycle, min(nonzero_cycle)) + feeder_slot_result.insert(nozzle_insert_cycle, assigned_slot) + + # 更新吸嘴匹配模式 + cycle_nozzle = nozzle_mode[nozzle_insert_cycle].copy() + for head, component in enumerate(assigned_part): + if component == -1: + continue + cycle_nozzle[head] = component_data.loc[component]['nz'] + + nozzle_mode.insert(nozzle_insert_cycle + 1, cycle_nozzle) + + pbar.update(len(pcb_data) - sum(component_points) - pbar_prev) + pbar_prev = len(pcb_data) - sum(component_points) + if sum(component_points) == 0: + break + + return component_result, cycle_result, feeder_slot_result diff --git a/base_optimizer/optimizer_hybridgenetic.py b/base_optimizer/optimizer_hybridgenetic.py new file mode 100644 index 0000000..609be78 --- /dev/null +++ b/base_optimizer/optimizer_hybridgenetic.py @@ -0,0 +1,564 @@ +import copy +import random + +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd + +from base_optimizer.optimizer_common import * +from collections import defaultdict + + +def dynamic_programming_cycle_path(cycle_placement, cycle_points): + head_sequence = [] + num_pos = sum([placement != -1 for placement in cycle_placement]) + 1 + + pos, head_set = [], [] + average_pos_x, counter = 0, 1 + for head, placement in enumerate(cycle_placement): + if placement == -1: + continue + head_set.append(head) + pos.append([cycle_points[head][0], cycle_points[head][1]]) + average_pos_x = average_pos_x + (pos[-1][0] - average_pos_x) / counter + + counter += 1 + + pos.insert(0, [average_pos_x, slotf1_pos[1]]) + + def get_distance(pos_1, pos_2): + return math.sqrt((pos_1[0] - pos_2[0]) ** 2 + (pos_1[1] - pos_2[1]) ** 2) + + # 各节点之间的距离 + dist = [[get_distance(pos_1, pos_2) for pos_2 in pos] for pos_1 in pos] + + min_dist = [[np.inf for i in range(num_pos)] for s in range(1 << num_pos)] + min_path = [[[] for i in range(num_pos)] for s in range(1 << num_pos)] + + # 状压dp搜索 + for s in range(1, 1 << num_pos, 2): + # 考虑节点集合s必须包括节点0 + if not (s & 1): + continue + for j in range(1, num_pos): + # 终点i需在当前考虑节点集合s内 + if not (s & (1 << j)): + continue + if s == int((1 << j) | 1): + # 若考虑节点集合s仅含节点0和节点j,dp边界,赋予初值 + # print('j:', j) + min_path[s][j] = [j] + min_dist[s][j] = dist[0][j] + + # 枚举下一个节点i,更新 + for i in range(1, num_pos): + # 下一个节点i需在考虑节点集合s外 + if s & (1 << i): + continue + if min_dist[s][j] + dist[j][i] < min_dist[s | (1 << i)][i]: + min_path[s | (1 << i)][i] = min_path[s][j] + [i] + min_dist[s | (1 << i)][i] = min_dist[s][j] + dist[j][i] + + ans_dist = np.inf + ans_path = [] + # 求最终最短哈密顿回路 + for i in range(1, num_pos): + if min_dist[(1 << num_pos) - 1][i] + dist[i][0] < ans_dist: + # 更新,回路化 + ans_path = min_path[s][i] + ans_dist = min_dist[(1 << num_pos) - 1][i] + dist[i][0] + + for element in ans_path: + head_sequence.append(head_set[element - 1]) + + return head_sequence + + +def pickup_group_combination(component_nozzle, designated_nozzle, supply, supply_cycle, demand, demand_cycle): + + combination, combination_cycle = demand.copy(), demand_cycle.copy() + supply_cpy = supply.copy() + + while True: + supply_cpy_bits = max_head_index - supply_cpy.count(None) + if supply_cpy_bits == 0: + break + max_match_offset, max_match_counter = 0, 0 + supply_cpy_index = [idx for idx, part in enumerate(supply_cpy) if part] # 加快搜索速度 + for offset in range(-supply_cpy_index[-1], max_head_index - supply_cpy_index[0]): + match_counter = 0 + for idx, part in enumerate(supply_cpy): + if 0 <= idx + offset < max_head_index: + if part is None: + continue + if combination[idx + offset] is None and designated_nozzle[idx + offset] == designated_nozzle[idx]: + match_counter += 1 + if match_counter > max_match_counter: + max_match_counter = match_counter + max_match_offset = offset + if match_counter == supply_cpy_bits: + break + + for idx, part in enumerate(supply_cpy): + if 0 <= idx + max_match_offset < max_head_index: + if part is None: + continue + + if demand[idx + max_match_offset] is None: + combination[idx + max_match_offset] = part + combination_cycle[idx + max_match_offset] = supply_cycle[idx] + supply_cpy[idx] = None + + return combination, combination_cycle + + +def cal_individual_val(component_nozzle, component_point_pos, designated_nozzle, pickup_group, pickup_group_cycle, + pair_group, feeder_part_arrange, individual): + + place_time, pick_time = 0.234, 0.4 + x_moving_speed, y_moving_speed = 300, 300 # mm/s + + prev_pair_index = None + sequenced_pickup_group, sequenced_pickup_cycle = [], [] + for gene in individual: + pickup = pickup_group[gene] + + pair_index = None + for idx, pair in enumerate(pair_group): + if gene in pair: + pair_index = idx + break + + if pair_index is not None and pair_index == prev_pair_index: + for idx, component in enumerate(pickup): + sequenced_pickup_group[-1][idx] = component + else: + sequenced_pickup_group.append(pickup.copy()) + sequenced_pickup_cycle.append(pickup_group_cycle[gene]) + + V = [float('inf') for _ in range(len(sequenced_pickup_group) + 1)] # Node Value + V[0] = 0 + V_SNode = [-1 for _ in range(len(sequenced_pickup_group) + 1)] + + nozzle_assigned_heads = defaultdict(int) + for nozzle in designated_nozzle: + nozzle_assigned_heads[nozzle] += 1 + + pickup_result, pickup_cycle_result = [[] for _ in range(len(V))], [[] for _ in range(len(V))] + component_point_index = defaultdict(int) + for i in range(1, len(V)): + cost, t0 = 0, 0 + load = defaultdict(int) + Pd, Pd_cycle = [None for _ in range(max_head_index)], [0 for _ in range(max_head_index)] # demand pickup + j = i + while j < len(V): + Ps, Ps_cycle = sequenced_pickup_group[j - 1], [sequenced_pickup_cycle[j - 1] for _ in + range(max_head_index)] # supply pickup and its cycle + for part in Ps: + if part: + load[component_nozzle[part]] += 1 + + is_combinable = True + for nozzle, counter in load.items(): + if counter > nozzle_assigned_heads[nozzle]: + is_combinable = False + + if is_combinable: + cost = cost - t0 + # combine sequenced pickup ρb and ps into ρu(union pickup) + Pu, Pu_cycle = pickup_group_combination(component_nozzle, designated_nozzle, Ps, Ps_cycle, Pd, Pd_cycle) + + # decide the placement cluster and sequencing of pickup ρu + pickup_action_counter, place_action_counter = 0, max_head_index - Pu.count(None) + right_most_slot, left_most_slot = 0, max_slot_index // 2 # most left and right pickup slot + + # === TODO: 机械限位、后槽位分配未处理 === + for head in range(max_head_index): + if not Pu[head]: + continue + assert Pu[head] in feeder_part_arrange.keys() + for slot in feeder_part_arrange[Pu[head]]: + left_most_slot = min(slot - head * interval_ratio, left_most_slot) + right_most_slot = max(slot - head * interval_ratio, right_most_slot) + + # calculate forward, backward, pick and place traveling time + t_FW, t_BW, t_PL, t_PU = 0, 0, 0, 0 + cycle = 0 + while cycle < max(Pu_cycle): + mount_points = [] + for head, part in enumerate(Pu): + if part is None or cycle > Pu_cycle[head]: + continue + idx = component_point_index[part] + mount_points.append([component_point_pos[part][idx][0] - head * head_interval + stopper_pos[0], + component_point_pos[part][idx][0] + stopper_pos[1]]) + assert len(mount_points) > 0 + + # calculate cycle moving distance + mount_points.sort(key=lambda x: x[0]) + t_FW += max( + abs(slotf1_pos[0] + (left_most_slot - 1) * slot_interval - mount_points[0][0]) / x_moving_speed, + abs(slotf1_pos[1] - mount_points[0][1]) / y_moving_speed) + t_BW += max( + abs(slotf1_pos[0] + (right_most_slot - 1) * slot_interval - mount_points[-1][0]) / x_moving_speed, + abs(slotf1_pos[1] - mount_points[-1][1]) / y_moving_speed) + # pick up moving time + t_PU += (right_most_slot - left_most_slot) * slot_interval / x_moving_speed + # place moving time + for idx_points in range(len(mount_points) - 1): + t_PL += max(abs(mount_points[idx_points][0] - mount_points[idx_points + 1][0]) / x_moving_speed, + abs(mount_points[idx_points][1] - mount_points[idx_points + 1][1]) / y_moving_speed) + cycle += 1 + + t0 = t_FW + (t_PL + place_action_counter * place_time) + t_BW + cost += (t_PU + pickup_action_counter * pick_time) + t0 + + if V[i - 1] + cost < V[j]: + pickup_result[j], pickup_cycle_result[j] = Pu, Pu_cycle + V_SNode[j] = i - 1 + V[j] = V[i - 1] + cost + + Pd, Pd_cycle = Pu, Pu_cycle + j += 1 + else: + break + + node = len(V) - 1 + while True: + prev_node = V_SNode[node] + if prev_node == -1: + break + for k in range(prev_node + 1, node): + pickup_result[k], pickup_cycle_result[k] = [], [] + node = prev_node + return V[-1], pickup_result, pickup_cycle_result + + +def convert_individual_2_result(component_data, component_point_pos, designated_nozzle, pickup_group, pickup_group_cycle, + pair_group, feeder_lane, individual): + component_result, cycle_result, feeder_slot_result = [], [], [] + placement_result, head_sequence_result = [], [] + + # === 记录不同元件对应的槽位 === + feeder_part_arrange = defaultdict(list) + for slot in range(1, max_slot_index // 2 + 1): + if feeder_lane[slot]: + feeder_part_arrange[feeder_lane[slot]].append(slot) + + # === 记录不同元件的注册吸嘴类型 === + component_nozzle = defaultdict(str) + for pickup in pickup_group: + for part in pickup: + if part is None or part in component_nozzle.keys(): + continue + component_nozzle[part] = component_data[component_data['part'] == part]['nz'].tolist()[0] + + # initial result + _, pickup_result, pickup_cycle_result = cal_individual_val(component_nozzle, component_point_pos, designated_nozzle, + pickup_group, pickup_group_cycle, + pair_group, feeder_part_arrange, individual) + + for idx, pickup in enumerate(pickup_result): + while pickup and max(pickup_cycle_result[idx]) != 0: + cycle = min([cycle_ for cycle_ in pickup_cycle_result[idx] if cycle_ > 0]) + feeder_part_arrange_index = defaultdict(int) + component_result.append([-1 for _ in range(max_head_index)]) + feeder_slot_result.append([-1 for _ in range(max_head_index)]) + cycle_result.append(cycle) + for head, part in enumerate(pickup): + if part is None or pickup_cycle_result[idx][head] == 0: + continue + + part_index = component_data[component_data['part'] == part].index.tolist()[0] + component_result[-1][head] = part_index + feeder_slot_result[-1][head] = feeder_part_arrange[part][feeder_part_arrange_index[part]] + feeder_part_arrange_index[part] += 1 + if feeder_part_arrange_index[part] >= len(feeder_part_arrange[part]): + feeder_part_arrange_index[part] = 0 + + pickup_cycle_result[idx][head] -= cycle + + component_point_index = defaultdict(int) + for cycle_set in range(len(cycle_result)): + for cycle in range(cycle_result[cycle_set]): + placement_result.append([-1 for _ in range(max_head_index)]) + mount_point = [[0, 0] for _ in range(max_head_index)] + for head in range(max_head_index): + part_index = component_result[cycle_set][head] + if part_index == -1: + continue + + part = component_data.iloc[part_index]['part'] + point_info = component_point_pos[part][component_point_index[part]] + + placement_result[-1][head] = point_info[2] + mount_point[head] = point_info[0:2] + + component_point_index[part] += 1 + head_sequence_result.append(dynamic_programming_cycle_path(placement_result[-1], mount_point)) + + return component_result, cycle_result, feeder_slot_result, placement_result, head_sequence_result + + +@timer_wrapper +def optimizer_hybrid_genetic(pcb_data, component_data, hinter=True): + random.seed(0) + np.random.seed(0) + nozzle_assigned_counter = optimal_nozzle_assignment(component_data, pcb_data) + + # nozzle assignment result: + designated_nozzle = [''] * max_head_index + head_index = 0 + for nozzle, num in nozzle_assigned_counter.items(): + while num > 0: + designated_nozzle[head_index] = nozzle + head_index += 1 + num -= 1 + + # === component assignment === + component_points, nozzle_components = defaultdict(int), defaultdict(list) # 元件贴装点数,吸嘴-元件对应关系 + component_feeder_limit, component_divided_points = defaultdict(int), defaultdict(list) + for step in pcb_data.iterrows(): + part = step[1]['part'] + idx = component_data[component_data['part'] == part].index.tolist()[0] + nozzle = component_data.loc[idx]['nz'] + + component_feeder_limit[part] = component_data.loc[idx]['feeder-limit'] + component_points[part] += 1 + if nozzle_components[nozzle].count(part) < component_feeder_limit[part]: + nozzle_components[nozzle].append(part) + + for part, feeder_limit in component_feeder_limit.items(): + for _ in range(feeder_limit): + component_divided_points[part].append(component_points[part] // feeder_limit) + + for part, divided_points in component_divided_points.items(): + index = 0 + while sum(divided_points) < component_points[part]: + divided_points[index] += 1 + index += 1 + + CT_Group, CT_Points = [], [] # CT: Component Type + while sum(len(nozzle_components[nozzle]) for nozzle in nozzle_components.keys()) != 0: + + CT_Group.append([None for _ in range(max_head_index)]) + CT_Points.append([0 for _ in range(max_head_index)]) + + for head_index in range(max_head_index): + nozzle = designated_nozzle[head_index] # 分配的吸嘴 + if len(nozzle_components[nozzle]) == 0: # 无可用元件 + continue + + max_points, designated_part = 0, None + for part in nozzle_components[nozzle]: + if component_points[part] > max_points: + max_points = component_points[part] + designated_part = part + + component_points[designated_part] -= component_divided_points[designated_part][-1] + + CT_Group[-1][head_index] = designated_part + CT_Points[-1][head_index] = component_divided_points[designated_part][-1] + + component_divided_points[designated_part].pop() + nozzle_components[nozzle].remove(designated_part) + + # === assign CT group to feeder slot === + point_num = len(pcb_data) + component_point_pos = defaultdict(list) + for point_cnt in range(point_num): + part = pcb_data.loc[point_cnt, 'part'] + component_point_pos[part].append( + [pcb_data.loc[point_cnt, 'x'] + stopper_pos[0], pcb_data.loc[point_cnt, 'y'] + stopper_pos[1], point_cnt]) + + for pos_list in component_point_pos.values(): + pos_list.sort(key=lambda x: (x[0], x[1])) + + CT_Group_slot = [-1] * len(CT_Group) + feeder_lane = [None] * max_slot_index # 供料器基座上已分配的元件类型 + CT_Head = defaultdict(list) + for pickup in CT_Group: + for head, CT in enumerate(pickup): + if CT is None: + continue + if CT not in CT_Head: + CT_Head[CT] = [head, head] + CT_Head[CT][0] = min(CT_Head[CT][0], head) + CT_Head[CT][1] = max(CT_Head[CT][1], head) + + for CTIdx, pickup in enumerate(CT_Group): + best_slot = [] + for cp_index, component in enumerate(pickup): + if component is None: + continue + best_slot.append(round((sum(pos[0] for pos in component_point_pos[component]) / len( + component_point_pos[component]) - slotf1_pos[0]) / slot_interval) + 1 - cp_index * interval_ratio) + best_slot = round(sum(best_slot) / len(best_slot)) + + search_dir, step = 0, 0 # dir: 1-向右, 0-向左 + prev_assign_available = True + while True: + assign_slot = best_slot + step if search_dir else best_slot - step + if assign_slot + (len(pickup) - 1) * interval_ratio >= max_slot_index / 2 or assign_slot < 0: + if not prev_assign_available: + raise Exception('feeder assign error!') + + # prev_assign_available = False + search_dir = 1 - search_dir + if search_dir == 1: + step += 1 + continue + + prev_assign_available = True + assign_available = True + + # 分配对应槽位 + for slot in range(assign_slot, assign_slot + interval_ratio * len(pickup), interval_ratio): + pickup_index = int((slot - assign_slot) / interval_ratio) + pick_part = pickup[pickup_index] + + # 检查槽位占用情况 + if feeder_lane[slot] is not None and pick_part is not None: + assign_available = False + break + + # 检查机械限位冲突 + if pick_part is not None and (slot - CT_Head[pick_part][0] * interval_ratio <= 0 or + slot + (max_head_index - CT_Head[pick_part][1] - 1) * interval_ratio > max_slot_index // 2): + assign_available = False + break + + if assign_available: + for idx, component in enumerate(pickup): + if component is not None: + feeder_lane[assign_slot + idx * interval_ratio] = component + CT_Group_slot[CTIdx] = assign_slot + break + + search_dir = 1 - search_dir + if search_dir == 1: + step += 1 + + # === Initial Pickup Group === + initial_pickup, initial_pickup_cycle = [], [] + for index, CT in enumerate(CT_Group): + while True: + if CT_Points[index].count(0) == max_head_index: + break + min_element = min([Points for Points in CT_Points[index] if Points > 0]) + + initial_pickup.append(copy.deepcopy(CT_Group[index])) + initial_pickup_cycle.append(min_element) + for head in range(max_head_index): + if CT_Points[index][head] >= min_element: + CT_Points[index][head] -= min_element + if CT_Points[index][head] == 0: + CT_Group[index][head] = None + + # pickup partition rule + partition_probability = 0.1 + pickup_group, pair_group = [], [] # pair_group: pickups from same initial group + pickup_group_cycle = [] + for idx, Pickup in enumerate(initial_pickup): + pickup_num = len([element for element in Pickup if element is not None]) + if 2 <= pickup_num <= max_head_index / 3 or ( + max_head_index / 3 <= pickup_num <= max_head_index / 2 and np.random.rand() < partition_probability): + # partitioned into single component pickups + # or partition the potentially inefficient initial pickups with a small probability + pair_index = [] + for index, CT in enumerate(Pickup): + if CT is not None: + pair_index.append(len(pickup_group)) + pickup_group.append([None for _ in range(max_head_index)]) + pickup_group[-1][index] = CT + pickup_group_cycle.append(initial_pickup_cycle[idx]) + pair_group.append(pair_index) + else: + pickup_group.append(Pickup) + pickup_group_cycle.append(initial_pickup_cycle[idx]) + + # basic parameter + # crossover rate & mutation rate: 80% & 10% + # population size: 200 + # the number of generation: 500 + crossover_rate, mutation_rate = 0.8, 0.1 + population_size, n_generations = 200, 500 + + # initial solution + population = [] + for _ in range(population_size): + pop_permutation = list(range(len(pickup_group))) + np.random.shuffle(pop_permutation) + population.append(pop_permutation) + + best_individual, best_pop_val = [], [] + + # === 记录不同元件对应的槽位 === + feeder_part_arrange = defaultdict(list) + for slot in range(1, max_slot_index // 2 + 1): + if feeder_lane[slot]: + feeder_part_arrange[feeder_lane[slot]].append(slot) + + # === 记录不同元件的注册吸嘴类型 === + component_nozzle = defaultdict(str) + for pickup in pickup_group: + for part in pickup: + if part is None or part in component_nozzle.keys(): + continue + component_nozzle[part] = component_data[component_data['part'] == part]['nz'].tolist()[0] + + with tqdm(total=n_generations) as pbar: + pbar.set_description('hybrid genetic process') + + for _ in range(n_generations): + # calculate fitness value + pop_val = [] + for pop_idx, individual in enumerate(population): + val, _, _ = cal_individual_val(component_nozzle, component_point_pos, designated_nozzle, pickup_group, + pickup_group_cycle, pair_group, feeder_part_arrange, individual) + pop_val.append(val) + + idx = np.argmin(pop_val) + if len(best_pop_val) == 0 or pop_val[idx] < best_pop_val[-1]: + best_individual = copy.deepcopy(population[idx]) + best_pop_val.append(pop_val[idx]) + + # min-max convert + max_val = 1.5 * max(pop_val) + pop_val = list(map(lambda v: max_val - v, pop_val)) + + # crossover and mutation + c = 0 + new_population = [] + for pop in range(population_size): + if pop % 2 == 0 and np.random.random() < crossover_rate: + index1, index2 = roulette_wheel_selection(pop_val), -1 + while True: + index2 = roulette_wheel_selection(pop_val) + if index1 != index2: + break + # 两点交叉算子 + offspring1 = directed_edge_recombination_crossover(c, population[index1], population[index2]) + c += 1 + offspring2 = directed_edge_recombination_crossover(c, population[index2], population[index1]) + c += 1 + + if np.random.random() < mutation_rate: + swap_mutation(offspring1) + + if np.random.random() < mutation_rate: + swap_mutation(offspring2) + + new_population.append(offspring1) + new_population.append(offspring2) + + # selection + top_k_index = get_top_k_value(pop_val, population_size - len(new_population)) + for index in top_k_index: + new_population.append(population[index]) + + population = new_population + pbar.update(1) + + return convert_individual_2_result(component_data, component_point_pos, designated_nozzle, pickup_group, + pickup_group_cycle, pair_group, feeder_lane, best_individual) diff --git a/base_optimizer/optimizer_scanbased.py b/base_optimizer/optimizer_scanbased.py new file mode 100644 index 0000000..d6a9cc3 --- /dev/null +++ b/base_optimizer/optimizer_scanbased.py @@ -0,0 +1,166 @@ +import itertools +from base_optimizer.optimizer_common import * + + +@timer_wrapper +def optimizer_scanbased(component_data, pcb_data, hinter): + + population_size = 200 # 种群规模 + crossover_rate, mutation_rate = .4, .02 + n_generation = 5 + + component_points = [0] * len(component_data) + for i in range(len(pcb_data)): + part = pcb_data.loc[i]['part'] + part_index = component_data[component_data['part'] == part].index.tolist()[0] + + component_points[part_index] += 1 + nozzle_type = component_data.loc[part_index]['nz'] + if nozzle_type not in nozzle_limit.keys() or nozzle_limit[nozzle_type] <= 0: + info = 'there is no available nozzle [' + nozzle_type + '] for the assembly process' + raise ValueError(info) + + # randomly generate permutations + generation_ = np.array([i for i in range(max_slot_index // 2)]) # 仅考虑前基座 + pop_individual, pop_val = [], [] + for _ in range(population_size): + np.random.shuffle(generation_) + pop_individual.append(generation_.tolist()) + + _, cycle_result, feeder_slot_result = convert_individual_2_result(component_points, pop_individual[-1]) + + pop_val.append(feeder_arrange_evaluate(feeder_slot_result, cycle_result)) + + # todo: 过程写的有问题,暂时不想改 + with tqdm(total=n_generation) as pbar: + pbar.set_description('hybrid genetic process') + for _ in range(n_generation): + # 交叉 + for pop in range(population_size): + if pop % 2 == 0 and np.random.random() < crossover_rate: + index1, index2 = roulette_wheel_selection(pop_val), -1 + while True: + index2 = roulette_wheel_selection(pop_val) + if index1 != index2: + break + + # 两点交叉算子 + offspring1, offspring2 = cycle_crossover(pop_individual[index1], pop_individual[index2]) + + _, cycle_result, feeder_slot_result = convert_individual_2_result(component_points, offspring1) + pop_val.append(feeder_arrange_evaluate(feeder_slot_result, cycle_result)) + pop_individual.append(offspring1) + + _, cycle_result, feeder_slot_result = convert_individual_2_result(component_points, offspring2) + pop_val.append(feeder_arrange_evaluate(feeder_slot_result, cycle_result)) + pop_individual.append(offspring2) + + sigma_scaling(pop_val, 1) + + # 变异 + if np.random.random() < mutation_rate: + index_ = roulette_wheel_selection(pop_val) + offspring = swap_mutation(pop_individual[index_]) + _, cycle_result, feeder_slot_result = convert_individual_2_result(component_points, offspring) + + pop_val.append(feeder_arrange_evaluate(feeder_slot_result, cycle_result)) + pop_individual.append(offspring) + + sigma_scaling(pop_val, 1) + + new_population, new_popval = [], [] + for index in get_top_k_value(pop_val, population_size): + new_population.append(pop_individual[index]) + new_popval.append(pop_val[index]) + + pop_individual, pop_val = new_population, new_popval + + # select the best individual + pop = np.argmin(pop_val) + component_result, cycle_result, feeder_slot_result = convert_individual_2_result(component_points, pop_individual[pop]) + + placement_result, head_sequence = greedy_placement_route_generation(component_data, pcb_data, component_result, + cycle_result, feeder_slot_result) + + return component_result, cycle_result, feeder_slot_result, placement_result, head_sequence + + +def convert_individual_2_result(component_points, pop): + component_result, cycle_result, feeder_slot_result = [], [], [] + + feeder_part = [-1] * (max_slot_index // 2) # 已安装在供料器基座上的元件(0: 未分配) + feeder_base_points = [0] * (max_slot_index // 2) # 供料器基座结余贴装点数量 + + # 将基因信息转换为供料器基座安装结果 + for idx, gene in enumerate(pop): + if idx >= len(component_points): + break + feeder_part[gene], feeder_base_points[gene] = idx, component_points[idx] + + # TODO: 暂时未考虑可用吸嘴数的限制 + # for _ in range(math.ceil(sum(component_points) / max_head_index)): + while True: + # === 周期内循环 === + assigned_part = [-1 for _ in range(max_head_index)] # 当前扫描到的头分配元件信息 + assigned_slot = [-1 for _ in range(max_head_index)] # 当前扫描到的供料器分配信息 + + prev_scan_slot = len(feeder_part) // 2 # 前一轮扫描的位置 + while True: + best_scan_part, best_scan_slot = [-1 for _ in range(max_head_index)], [-1 for _ in range(max_head_index)] + best_slot_index = -1 + for slot in range(max_slot_index // 2 - (max_head_index - 1) * interval_ratio): + scan_part, scan_slot = assigned_part.copy(), assigned_slot.copy() + + for head in range(max_head_index): + part = feeder_part[slot + head * interval_ratio] + + # 贴装头和拾取槽位满足对应关系 + if scan_part[head] == -1 and part != -1 and feeder_base_points[slot + head * interval_ratio] > 0: + scan_part[head], scan_slot[head] = part, slot + head * interval_ratio + 1 + + if scan_part.count(-1) < best_scan_part.count(-1) or (scan_part.count(-1) == best_scan_part.count(-1) + and abs(slot - prev_scan_slot) < + abs(best_slot_index - prev_scan_slot)): + best_slot_index = slot + best_scan_part, best_scan_slot = scan_part.copy(), scan_slot.copy() + + assigned_points = 0 + for idx, slot in enumerate(best_scan_slot): + if slot != -1 and assigned_slot[idx] == -1: + feeder_base_points[slot - 1] -= 1 + assigned_points += 1 + + assigned_part, assigned_slot = best_scan_part.copy(), best_scan_slot.copy() + prev_scan_slot = best_slot_index + + if assigned_part.count(-1) == 0 or assigned_points == 0: + break + + if len(cycle_result) == 0 or component_result[-1] != assigned_part: + cycle_result.append(1) + component_result.append(assigned_part) + feeder_slot_result.append(assigned_slot) + else: + cycle_result[-1] += 1 + + if sum(feeder_base_points) == 0: + break + + return component_result, cycle_result, feeder_slot_result + + +def feeder_arrange_evaluate(feeder_slot_result, cycle_result): + assert len(feeder_slot_result) == len(cycle_result) + arrange_val = 0 + for cycle, feeder_slot in enumerate(feeder_slot_result): + pick_slot = set() + for head, slot in enumerate(feeder_slot): + pick_slot.add(slot - head * interval_ratio) + + arrange_val += len(pick_slot) * t_pick * cycle_result[cycle] + pick_slot = list(pick_slot) + pick_slot.sort() + arrange_val += axis_moving_time(pick_slot[0] - pick_slot[-1]) * cycle_result[cycle] + + return arrange_val + diff --git a/dataloader.py b/dataloader.py index 00befae..e73e200 100644 --- a/dataloader.py +++ b/dataloader.py @@ -1,11 +1,9 @@ import random -from optimizer_common import * +from base_optimizer.optimizer_common import * -def load_data(filename: str, load_cp_data=True, load_feeder_data=True, component_register=False): - # 锁定随机数种子 - random.seed(0) +def load_data(filename: str, default_feeder_limit=1, load_cp_data=True, load_feeder_data=True, cp_auto_register=False): # 读取PCB数据 filename = 'data/' + filename @@ -28,44 +26,43 @@ def load_data(filename: str, load_cp_data=True, load_feeder_data=True, component # pcb_data["x"] = pcb_data["x"].apply(lambda x: -x) # 注册元件检查 - component_data = None - if load_cp_data: - part_feeder_assign = defaultdict(set) - part_col = ["part", "desc", "fdr", "nz", 'camera', 'group', 'feeder-limit'] - try: - component_data = pd.DataFrame(pd.read_csv(filepath_or_buffer='component.txt', sep='\t', header=None), columns=part_col) - except: + part_feeder_assign = defaultdict(set) + part_col = ["part", "desc", "fdr", "nz", 'camera', 'group', 'feeder-limit'] + try: + if load_cp_data: + component_data = pd.DataFrame(pd.read_csv(filepath_or_buffer='component.txt', sep='\t', header=None), + columns=part_col) + else: component_data = pd.DataFrame(columns=part_col) - for _, data in pcb_data.iterrows(): - part, nozzle = data.part, data.nz.split(' ')[1] - slot = data['fdr'].split(' ')[0] + except: + component_data = pd.DataFrame(columns=part_col) - if part not in component_data['part'].values: - if not component_register: - raise Exception("unregistered component: " + component_data['part'].values) - else: - component_data = pd.concat([component_data, - pd.DataFrame([part, '', 'SM8', nozzle, '飞行相机1', 'CHIP-Rect', 0], - index=part_col).T], ignore_index=True) - # warning_info = 'register component ' + part + ' with default feeder type' - # warnings.warn(warning_info, UserWarning) - part_index = component_data[component_data['part'] == part].index.tolist()[0] - part_feeder_assign[part].add(slot) + for _, data in pcb_data.iterrows(): + part, nozzle = data.part, data.nz.split(' ')[1] + slot = data['fdr'].split(' ')[0] - if nozzle != 'A' and component_data.loc[part_index]['nz'] != nozzle: - warning_info = 'the nozzle type of component ' + part + ' is not consistent with the pcb data' - warnings.warn(warning_info, UserWarning) + if part not in component_data['part'].values: + if not cp_auto_register: + raise Exception("unregistered component: " + component_data['part'].values) + else: + component_data = pd.concat([component_data, pd.DataFrame( + [part, '', 'SM8', nozzle, '飞行相机1', 'CHIP-Rect', default_feeder_limit], index=part_col).T], + ignore_index=True) + # warning_info = 'register component ' + part + ' with default feeder type' + # warnings.warn(warning_info, UserWarning) + part_index = component_data[component_data['part'] == part].index.tolist()[0] + part_feeder_assign[part].add(slot) - for idx, data in component_data.iterrows(): - if data['fdr'][0:3] == 'SME': # 电动供料器和气动供料器参数一致 - component_data.at[idx, 'fdr'] = data['fdr'][0:2] + data['fdr'][3:] + if nozzle != 'A' and component_data.loc[part_index]['nz'] != nozzle: + warning_info = 'the nozzle type of component ' + part + ' is not consistent with the pcb data' + warnings.warn(warning_info, UserWarning) - for part, slots in part_feeder_assign.items(): - part_index = component_data[component_data['part'] == part].index.tolist()[0] - component_data.at[part_index, 'feeder-limit'] = max(len(slots), component_data.at[part_index, 'feeder-limit']) + for idx, data in component_data.iterrows(): + if data['fdr'][0:3] == 'SME': # 电动供料器和气动供料器参数一致 + component_data.at[idx, 'fdr'] = data['fdr'][0:2] + data['fdr'][3:] # 读取供料器基座数据 - feeder_data = pd.DataFrame(columns=range(3)) + feeder_data = pd.DataFrame(columns=['slot', 'part', 'arg']) # arg表示是否为预分配,不表示分配数目 if load_feeder_data: for data in pcb_data.iterrows(): fdr = data[1]['fdr'] @@ -75,7 +72,6 @@ def load_data(filename: str, load_cp_data=True, load_feeder_data=True, component slot = int(slot[1:]) if slot[0] == 'F' else int(slot[1:]) + max_slot_index // 2 feeder_data = pd.concat([feeder_data, pd.DataFrame([slot, part, 1]).T]) - feeder_data.columns = ['slot', 'part', 'arg'] # arg表示是否为预分配,不表示分配数目 feeder_data.drop_duplicates(subset='slot', inplace=True, ignore_index=True) # 随机移除部分已安装的供料器 if load_feeder_data == 2: @@ -83,7 +79,7 @@ def load_data(filename: str, load_cp_data=True, load_feeder_data=True, component feeder_data.drop(index=drop_index, inplace=True) feeder_data.sort_values(by='slot', ascending=True, inplace=True, ignore_index=True) - else: - feeder_data.columns = ['slot', 'part', 'arg'] # 同上 + # plt.scatter(pcb_data["x"], pcb_data["y"]) + # plt.show() return pcb_data, component_data, feeder_data diff --git a/optimizer.py b/optimizer.py index 283044b..5cfa5ec 100644 --- a/optimizer.py +++ b/optimizer.py @@ -1,305 +1,166 @@ import math -import random import matplotlib.pyplot as plt +import pandas as pd + +from base_optimizer.optimizer_aggregation import * +from base_optimizer.optimizer_scanbased import * +from base_optimizer.optimizer_celldivision import * +from base_optimizer.optimizer_hybridgenetic import * +from base_optimizer.optimizer_feederpriority import * -from optimizer_common import * from dataloader import * - -def get_top_k_value(pop_val, k: int): - res = [] - pop_val_cpy = copy.deepcopy(pop_val) - pop_val_cpy.sort(reverse=True) - - for i in range(min(len(pop_val_cpy), k)): - for j in range(len(pop_val)): - if abs(pop_val_cpy[i] - pop_val[j]) < 1e-9 and j not in res: - res.append(j) - break - return res +from optimizer_genetic import * +from optimizer_heuristic import * -def swap_mutation(component_points, individual): - offspring = individual.copy() +def optimizer(pcb_data, component_data, assembly_line_optimizer, single_machine_optimizer): + assignment_result = assemblyline_optimizer_genetic(pcb_data, component_data) - idx, component_index = 0, random.randint(0, len(component_points) - 1) - for points in component_points.values(): - if component_index == 0: - index1 = random.randint(0, points + max_machine_index - 2) - while True: - index2 = random.randint(0, points + max_machine_index - 2) - if index1 != index2 and offspring[idx + index1] != offspring[idx + index2]: - break - offspring[idx + index1], offspring[idx + index2] = offspring[idx + index2], offspring[idx + index1] - break + # assignment_result = [[0, 0, 0, 0, 216, 0, 0], [0, 0, 0, 0, 216, 0, 0], [36, 24, 12, 12, 0, 36, 12]] + placement_points, placement_time = [], [] + partial_pcb_data, partial_component_data = defaultdict(pd.DataFrame), defaultdict(pd.DataFrame) + for machine_index in range(max_machine_index): + partial_pcb_data[machine_index] = pd.DataFrame(columns=pcb_data.columns) + partial_component_data[machine_index] = component_data.copy(deep=True) + placement_points.append(sum(assignment_result[machine_index])) - component_index -= 1 - idx += (points + max_machine_index - 1) + # averagely assign available feeder + for part_index, data in component_data.iterrows(): + feeder_limit = data['feeder-limit'] + feeder_points = [assignment_result[machine_index][part_index] for machine_index in range(max_machine_index)] - return offspring - - -def roulette_wheel_selection(pop_eval): - # Roulette wheel - random_val = np.random.random() - for idx, val in enumerate(pop_eval): - random_val -= val - if random_val <= 0: - return idx - return len(pop_eval) - 1 - - -def random_selective(data, possibility): # 依概率选择随机数 - assert len(data) == len(possibility) and len(data) > 0 - - sum_val = sum(possibility) - possibility = [p / sum_val for p in possibility] - - random_val = random.random() - for idx, val in enumerate(possibility): - random_val -= val - if random_val <= 0: - break - return data[idx] - - -def selective_initialization(component_points, population_size): - population = [] # population initialization - - for _ in range(population_size): - individual = [] - for points in component_points.values(): - if points == 0: + for machine_index in range(max_machine_index): + if feeder_points[machine_index] == 0: continue - avl_machine_num = random.randint(1, min(max_machine_index, points)) # 可用机器数 - selective_possibility = [] - for p in range(1, avl_machine_num + 1): - selective_possibility.append(pow(2, avl_machine_num - p + 1)) + arg_feeder = max(math.floor(feeder_points[machine_index] / sum(feeder_points) * data['feeder-limit']), 1) - sel_machine_num = random_selective([p + 1 for p in range(avl_machine_num)], selective_possibility) # 选择的机器数 - sel_machine_set = random.sample([p for p in range(avl_machine_num)], sel_machine_num) + partial_component_data[machine_index].loc[part_index]['feeder-limit'] = arg_feeder + feeder_limit -= arg_feeder - sel_machine_points = [1 for _ in range(sel_machine_num)] - for p in range(sel_machine_num - 1): - if points == sum(sel_machine_points): - break - assign_points = random.randint(1, points - sum(sel_machine_points)) - sel_machine_points[p] += assign_points + for machine_index in range(max_machine_index): + if feeder_limit <= 0: + break - if sum(sel_machine_points) < points: - sel_machine_points[-1] += (points - sum(sel_machine_points)) + if feeder_points[machine_index] == 0: + continue + partial_component_data[machine_index].loc[part_index]['feeder-limit'] += 1 + feeder_limit -= 1 - # code component allocation into chromosome - for p in range(max_machine_index): - if p in sel_machine_set: - individual += [0 for _ in range(sel_machine_points[0])] - sel_machine_points.pop(0) - individual.append(1) - individual.pop(-1) - - population.append(individual) - return population - - -def selective_crossover(mother, father, non_decelerating=True): - assert len(mother) == len(father) - - offspring1, offspring2 = mother.copy(), father.copy() - one_counter, feasible_cutline = 0, [] - for idx in range(len(mother) - 1): - if mother[idx] == 1: - one_counter += 1 - if father[idx] == 1: - one_counter -= 1 - - # first constraint: the total number of “1”s (the number of partitions) in the chromosome is unchanged - if one_counter != 0 or idx == 0 or idx == len(mother) - 2: - continue - - # the selected cutline should guarantee there are the same or a larger number unassigned machine - # for each component type - n_bro, n_new = 0, 0 - if mother[idx] and mother[idx + 1]: - n_bro += 1 - if father[idx] and father[idx + 1]: - n_bro += 1 - if mother[idx] and father[idx + 1]: - n_new += 1 - if father[idx] and mother[idx + 1]: - n_new += 1 - - # non_decelerating or accelerating crossover - if (non_decelerating and n_bro <= n_new) or n_bro < n_new: - feasible_cutline.append(idx) - - if len(feasible_cutline) == 0: - return offspring1, offspring2 - - cutline_idx = feasible_cutline[random.randint(0, len(feasible_cutline) - 1)] - offspring1, offspring2 = mother[:cutline_idx + 1] + father[cutline_idx + 1:], father[:cutline_idx + 1] + mother[ - cutline_idx + 1:] - return offspring1, offspring2 - - -def cal_individual_val(component_points, component_nozzle, individual): - idx, objective_val = 0, [0] - machine_component_points = [[] for _ in range(max_machine_index)] - - # decode the component allocation - for points in component_points.values(): - component_gene = individual[idx: idx + points + max_machine_index - 1] - machine_idx, component_counter = 0, 0 - for gene in component_gene: - if gene: - machine_component_points[machine_idx].append(component_counter) - machine_idx += 1 - component_counter = 0 + component_machine_index = [0 for _ in range(len(component_data))] + pcb_data = pcb_data.sort_values(by="x", ascending=False) + for _, data in pcb_data.iterrows(): + part = data['part'] + part_index = component_data[component_data['part'] == part].index.tolist()[0] + while True: + machine_index = component_machine_index[part_index] + if assignment_result[machine_index][part_index] == 0: + component_machine_index[part_index] += 1 + machine_index += 1 else: - component_counter += 1 - machine_component_points[-1].append(component_counter) - idx += (points + max_machine_index - 1) - - for machine_idx in range(max_machine_index): - nozzle_points = defaultdict(int) - for idx, nozzle in component_nozzle.items(): - if component_points[idx] == 0: - continue - nozzle_points[nozzle] += machine_component_points[machine_idx][idx] - - machine_points = sum(machine_component_points[machine_idx]) # num of placement points - if machine_points == 0: - continue - ul = math.ceil(len(nozzle_points) * 1.0 / max_head_index) - 1 # num of nozzle set - - # assignments of nozzles to heads - wl = 0 # num of workload - total_heads = (1 + ul) * max_head_index - len(nozzle_points) - nozzle_heads = defaultdict(int) - for nozzle in nozzle_points.keys(): - nozzle_heads[nozzle] = math.floor(nozzle_points[nozzle] * 1.0 / machine_points * total_heads) - nozzle_heads[nozzle] += 1 - - total_heads = (1 + ul) * max_head_index - for heads in nozzle_heads.values(): - total_heads -= heads - - for nozzle in nozzle_heads.keys(): # TODO:有利于减少周期的方法 - if total_heads == 0: break - nozzle_heads[nozzle] += 1 - total_heads -= 1 + assignment_result[machine_index][part_index] -= 1 + partial_pcb_data[machine_index] = pd.concat([partial_pcb_data[machine_index], pd.DataFrame(data).T]) - # averagely assign placements to heads - heads_placement = [] - for nozzle in nozzle_heads.keys(): - points = math.floor(nozzle_points[nozzle] / nozzle_heads[nozzle]) + for machine_index, data in partial_pcb_data.items(): + data = data.reset_index(drop=True) + if len(data) == 0: + continue - heads_placement += [[nozzle, points] for _ in range(nozzle_heads[nozzle])] - nozzle_points[nozzle] -= (nozzle_heads[nozzle] * points) - for idx in range(len(heads_placement) - 1, -1, -1): - if nozzle_points[nozzle] <= 0: - break - nozzle_points[nozzle] -= 1 - heads_placement[idx][1] += 1 - heads_placement = sorted(heads_placement, key=lambda x: x[1], reverse=True) + placement_time.append(base_optimizer(machine_index + 1, data, partial_component_data[machine_index], + feeder_data=pd.DataFrame(columns=['slot', 'part', 'arg']), + method=single_machine_optimizer, hinter=True)) - # every max_head_index heads in the non-decreasing order are grouped together as nozzle set - for idx in range(len(heads_placement) // max_head_index): - wl += heads_placement[idx][1] - objective_val.append(T_pp * machine_points + T_tr * wl + T_nc * ul) + average_time, standard_deviation_time = sum(placement_time) / max_machine_index, 0 + for machine_index in range(max_machine_index): + print('assembly time for machine ' + str(machine_index + 1) + ': ' + str( + placement_time[machine_index]) + ' s, ' + 'total placements: ' + str(placement_points[machine_index])) + standard_deviation_time += pow(placement_time[machine_index] - average_time, 2) + standard_deviation_time /= max_machine_index + standard_deviation_time = math.sqrt(standard_deviation_time) - return max(objective_val), machine_component_points + print('finial assembly time: ' + str(max(placement_time)) + 's, standard deviation: ' + str(standard_deviation_time)) -@timer_wrapper -def optimizer(pcb_data, component_data): - # basic parameter - # crossover rate & mutation rate: 80% & 10% - # population size: 200 - # the number of generation: 500 - crossover_rate, mutation_rate = 0.8, 0.1 - population_size, n_generations = 200, 500 +# todo: 不同类型元件的组装时间差异 +def base_optimizer(machine_index, pcb_data, component_data, feeder_data=None, method='', hinter=False): + if method == 'cell_division': # 基于元胞分裂的遗传算法 + component_result, cycle_result, feeder_slot_result = optimizer_celldivision(pcb_data, component_data, False) + placement_result, head_sequence = greedy_placement_route_generation(component_data, pcb_data, component_result, + cycle_result, feeder_slot_result) + elif method == 'feeder_priority': # 基于基座扫描的供料器优先算法 + # 第1步:分配供料器位置 + nozzle_pattern = feeder_allocate(component_data, pcb_data, feeder_data, False) + # 第2步:扫描供料器基座,确定元件拾取的先后顺序 + component_result, cycle_result, feeder_slot_result = feeder_base_scan(component_data, pcb_data, feeder_data, + nozzle_pattern) - # the number of placement points and nozzle type of component - component_points, component_nozzle = defaultdict(int), defaultdict(str) - for data in pcb_data.iterrows(): - part_index = component_data[component_data['part'] == data[1]['part']].index.tolist()[0] - nozzle = component_data.loc[part_index]['nz'] + # 第3步:贴装路径规划 + placement_result, head_sequence = greedy_placement_route_generation(component_data, pcb_data, component_result, + cycle_result, feeder_slot_result) + # placement_result, head_sequence = beam_search_for_route_generation(component_data, pcb_data, component_result, + # cycle_result, feeder_slot_result) - component_points[part_index] += 1 - component_nozzle[part_index] = nozzle + elif method == 'hybrid_genetic': # 基于拾取组的混合遗传算法 + component_result, cycle_result, feeder_slot_result, placement_result, head_sequence = optimizer_hybrid_genetic( + pcb_data, component_data, False) - # population initialization - best_popval = [] - population = selective_initialization(component_points, population_size) - with tqdm(total=n_generations) as pbar: - pbar.set_description('genetic process for PCB assembly') + elif method == 'aggregation': # 基于batch-level的整数规划 + 启发式算法 + component_result, cycle_result, feeder_slot_result, placement_result, head_sequence = optimizer_aggregation( + component_data, pcb_data) + elif method == 'scan_based': + component_result, cycle_result, feeder_slot_result, placement_result, head_sequence = optimizer_scanbased( + component_data, pcb_data, False) + else: + raise 'method is not existed' - new_population, new_pop_val = [], [] - for _ in range(n_generations): - # calculate fitness value - pop_val = [] - for individual in population: - val, _ = cal_individual_val(component_points, component_nozzle, individual) - pop_val.append(val) + if hinter: + optimization_assign_result(component_data, pcb_data, component_result, cycle_result, feeder_slot_result, + nozzle_hinter=False, component_hinter=False, feeder_hinter=False) - best_popval.append(min(pop_val)) - # min-max convert - max_val = max(pop_val) - pop_val = list(map(lambda v: max_val - v, pop_val)) + print('----- Placement machine ' + str(machine_index) + ' ----- ') + print('-Cycle counter: {}'.format(sum(cycle_result))) - sum_pop_val = sum(pop_val) - pop_val = [v / sum_pop_val for v in pop_val] + total_nozzle_change_counter, total_pick_counter = 0, 0 + assigned_nozzle = ['' if idx == -1 else component_data.loc[idx]['nz'] for idx in component_result[0]] - select_index = get_top_k_value(pop_val, population_size - len(new_pop_val)) - population = [population[idx] for idx in select_index] - pop_val = [pop_val[idx] for idx in select_index] + for cycle in range(len(cycle_result)): + pick_slot = set() + for head in range(max_head_index): + if (idx := component_result[cycle][head]) == -1: + continue - population += new_population - for individual in new_population: - val, _ = cal_individual_val(component_points, component_nozzle, individual) - pop_val.append(val) + nozzle = component_data.loc[idx]['nz'] + if nozzle != assigned_nozzle[head]: + if assigned_nozzle[head] != '': + total_nozzle_change_counter += 1 + assigned_nozzle[head] = nozzle - # crossover and mutation - new_population = [] - for pop in range(population_size): - if pop % 2 == 0 and np.random.random() < crossover_rate: - index1 = roulette_wheel_selection(pop_val) - while True: - index2 = roulette_wheel_selection(pop_val) - if index1 != index2: - break + pick_slot.add(feeder_slot_result[cycle][head] - head * interval_ratio) + total_pick_counter += len(pick_slot) * cycle_result[cycle] - offspring1, offspring2 = selective_crossover(population[index1], population[index2]) - if np.random.random() < mutation_rate: - offspring1 = swap_mutation(component_points, offspring1) + print('-Nozzle change counter: {}'.format(total_nozzle_change_counter)) + print('-Pick operation counter: {}'.format(total_pick_counter)) + print('------------------------------ ') - if np.random.random() < mutation_rate: - offspring1 = swap_mutation(component_points, offspring1) - - new_population.append(offspring1) - new_population.append(offspring2) - - pbar.update(1) - - best_individual = population[np.argmin(pop_val)] - val, result = cal_individual_val(component_points, component_nozzle, best_individual) - print(result) - - plt.plot(best_popval) - plt.show() - # TODO: 计算实际的PCB整线组装时间 + # 估算贴装用时 + return placement_time_estimate(component_data, pcb_data, component_result, cycle_result, feeder_slot_result, + placement_result, head_sequence, False) -if __name__ == '__main__': +def main(): # warnings.simplefilter('ignore') # 参数解析 parser = argparse.ArgumentParser(description='assembly line optimizer implementation') - parser.add_argument('--filename', default='PCB.txt', type=str, help='load pcb data') + parser.add_argument('--filename', default='PCB1 - FL19-30W.txt', type=str, help='load pcb data') parser.add_argument('--auto_register', default=1, type=int, help='register the component according the pcb data') - + parser.add_argument('--base_optimizer', default='feeder_priority', type=str, + help='base optimizer for single machine') + parser.add_argument('--assembly_optimizer', default='genetic', type=str, help='optimizer for PCB Assembly Line') + parser.add_argument('--feeder_limit', default=2, type=int, + help='the upper feeder limit for each type of component') params = parser.parse_args() # 结果输出显示所有行和列 @@ -307,10 +168,13 @@ if __name__ == '__main__': pd.set_option('display.max_rows', None) # 加载PCB数据 - pcb_data, component_data, _ = load_data(params.filename, component_register=params.auto_register) # 加载PCB数据 + pcb_data, component_data, _ = load_data(params.filename, default_feeder_limit=params.feeder_limit, + cp_auto_register=params.auto_register) # 加载PCB数据 - optimizer(pcb_data, component_data) + optimizer(pcb_data, component_data, params.assembly_optimizer, params.base_optimizer) +if __name__ == '__main__': + main() diff --git a/optimizer_common.py b/optimizer_common.py deleted file mode 100644 index 59be942..0000000 --- a/optimizer_common.py +++ /dev/null @@ -1,78 +0,0 @@ -import copy -import time -import math -import argparse - -import warnings -import pandas as pd -import numpy as np -import matplotlib.pyplot as plt - -from functools import wraps -from collections import defaultdict -from typing import List, Counter -from tqdm import tqdm - -# 机器参数 -max_head_index, max_slot_index = 6, 120 # 暂时默认所有机器参数相同 -max_machine_index = 3 -interval_ratio = 2 -slot_interval = 15 -head_interval = slot_interval * interval_ratio -head_nozzle = ['' for _ in range(max_head_index)] # 头上已经分配吸嘴 - -# 位置信息 -slotf1_pos, slotr1_pos = [-31.267, 44.], [807., 810.545] # F1(前基座最左侧)、R1(后基座最右侧)位置 -fix_camera_pos = [269.531, 694.823] # 固定相机位置 -anc_marker_pos = [336.457, 626.230] # ANC基准点位置 -stopper_pos = [635.150, 124.738] # 止档块位置 - -# 时间参数 -T_pp, T_tr, T_nc = 2, 5, 25 - -# 电机参数 -head_rotary_velocity = 8e-5 # 贴装头R轴旋转时间 -x_max_velocity, y_max_velocity = 1.4, 1.2 -x_max_acceleration, y_max_acceleration = x_max_velocity / 0.079, y_max_velocity / 0.079 - -# 不同种类供料器宽度 -feeder_width = {'SM8': (7.25, 7.25), 'SM12': (7.00, 20.00), 'SM16': (7.00, 22.00), - 'SM24': (7.00, 29.00), 'SM32': (7.00, 44.00)} - -# 可用吸嘴数量限制 -nozzle_limit = {'CN065': 6, 'CN040': 6, 'CN220': 6, 'CN400': 6, 'CN140': 6} - - -def axis_moving_time(distance, axis=0): - distance = abs(distance) * 1e-3 - Lamax = x_max_velocity ** 2 / x_max_acceleration if axis == 0 else y_max_velocity ** 2 / y_max_acceleration - Tmax = x_max_velocity / x_max_acceleration if axis == 0 else y_max_velocity / y_max_acceleration - if axis == 0: - return 2 * math.sqrt(distance / x_max_acceleration) if distance < Lamax else 2 * Tmax + ( - distance - Lamax) / x_max_velocity - else: - return 2 * math.sqrt(distance / y_max_acceleration) if distance < Lamax else 2 * Tmax + ( - distance - Lamax) / y_max_velocity - - -def head_rotary_time(angle): - while -180 > angle > 180: - if angle > 180: - angle -= 360 - else: - angle += 360 - return abs(angle) * head_rotary_velocity - - -def timer_wrapper(func): - @wraps(func) - def measure_time(*args, **kwargs): - start_time = time.time() - result = func(*args, **kwargs) - - print("function {} running time : {} s".format(func.__name__, time.time() - start_time)) - return result - - return measure_time - - diff --git a/optimizer_genetic.py b/optimizer_genetic.py new file mode 100644 index 0000000..d25e679 --- /dev/null +++ b/optimizer_genetic.py @@ -0,0 +1,288 @@ +import matplotlib.pyplot as plt + +from base_optimizer.optimizer_common import * + + +def selective_initialization(component_points, component_feeders, population_size): + population = [] # population initialization + + for _ in range(population_size): + individual = [] + for part_index, points in component_points.items(): + if points == 0: + continue + # 可用机器数 + avl_machine_num = random.randint(1, min(max_machine_index, component_feeders[part_index], points)) + + selective_possibility = [] + for p in range(1, avl_machine_num + 1): + selective_possibility.append(pow(2, avl_machine_num - p + 1)) + + sel_machine_num = random_selective([p + 1 for p in range(avl_machine_num)], selective_possibility) # 选择的机器数 + sel_machine_set = random.sample([p for p in range(max_machine_index)], sel_machine_num) + + sel_machine_points = [1 for _ in range(sel_machine_num)] + for p in range(sel_machine_num - 1): + if points == sum(sel_machine_points): + break + assign_points = random.randint(1, points - sum(sel_machine_points)) + sel_machine_points[p] += assign_points + + if sum(sel_machine_points) < points: + sel_machine_points[-1] += (points - sum(sel_machine_points)) + + # code component allocation into chromosome + for p in range(max_machine_index): + if p in sel_machine_set: + individual += [0 for _ in range(sel_machine_points[0])] + sel_machine_points.pop(0) + individual.append(1) + individual.pop(-1) + + population.append(individual) + return population + + +def selective_crossover(component_points, component_feeders, mother, father, non_decelerating=True): + assert len(mother) == len(father) + + offspring1, offspring2 = mother.copy(), father.copy() + one_counter, feasible_cut_line = 0, [] + + idx = 0 + for part_index, points in component_points.items(): + one_counter = 0 + + idx_, mother_cut_line, father_cut_line = 0, [-1], [-1] + for idx_, gene in enumerate(mother[idx: idx + points + max_machine_index - 1]): + if gene: + mother_cut_line.append(idx_) + mother_cut_line.append(idx_ + 1) + + for idx_, gene in enumerate(father[idx: idx + points + max_machine_index - 1]): + if gene: + father_cut_line.append(idx_) + father_cut_line.append(idx_ + 1) + + for offset in range(points + max_machine_index - 1): + if mother[idx + offset] == 1: + one_counter += 1 + if father[idx + offset] == 1: + one_counter -= 1 + + # first constraint: the total number of '1's (the number of partitions) in the chromosome is unchanged + if one_counter != 0 or offset == 0 or offset == points + max_machine_index - 2: + continue + + # the selected cut-line should guarantee there are the same or a larger number unassigned machine + # for each component type + n_bro, n_new = 0, 0 + if mother[idx + offset] and mother[idx + offset + 1]: + n_bro += 1 + if father[idx + offset] and father[idx + offset + 1]: + n_bro += 1 + if mother[idx + offset] and father[idx + offset + 1]: + n_new += 1 + if father[idx + offset] and mother[idx + offset + 1]: + n_new += 1 + + # second constraint: non_decelerating or accelerating crossover + if n_new < n_bro or (n_new == n_bro and not non_decelerating): + continue + + # third constraint (customized constraint): + # no more than the maximum number of available machine for each component type + new_mother_cut_line, new_father_cut_line = [], [] + for idx_ in range(max_machine_index + 1): + if mother_cut_line[idx_] <= offset: + new_mother_cut_line.append(mother_cut_line[idx_]) + else: + new_father_cut_line.append(mother_cut_line[idx_]) + + if father_cut_line[idx_] <= offset: + new_father_cut_line.append(father_cut_line[idx_]) + else: + new_mother_cut_line.append(father_cut_line[idx_]) + + sorted(new_mother_cut_line, reverse=False) + sorted(new_father_cut_line, reverse=False) + n_mother_machine, n_father_machine = 0, 0 + + for idx_ in range(max_machine_index): + if new_mother_cut_line[idx_ + 1] - new_mother_cut_line[idx_]: + n_mother_machine += 1 + + if new_father_cut_line[idx_ + 1] - new_father_cut_line[idx_]: + n_father_machine += 1 + + if n_mother_machine > component_feeders[part_index] or n_father_machine > component_feeders[part_index]: + continue + + feasible_cut_line.append(idx + offset) + + idx += (points + max_machine_index - 1) + + if len(feasible_cut_line) == 0: + return offspring1, offspring2 + + cut_line_idx = feasible_cut_line[random.randint(0, len(feasible_cut_line) - 1)] + offspring1, offspring2 = mother[:cut_line_idx + 1] + father[cut_line_idx + 1:], father[:cut_line_idx + 1] + mother[ + cut_line_idx + 1:] + return offspring1, offspring2 + + +def cal_individual_val(component_points, component_nozzle, individual): + idx, objective_val = 0, [] + machine_component_points = [[] for _ in range(max_machine_index)] + + # decode the component allocation + for points in component_points.values(): + component_gene = individual[idx: idx + points + max_machine_index - 1] + machine_idx, component_counter = 0, 0 + for gene in component_gene: + if gene: + machine_component_points[machine_idx].append(component_counter) + machine_idx += 1 + component_counter = 0 + else: + component_counter += 1 + machine_component_points[-1].append(component_counter) + idx += (points + max_machine_index - 1) + + for machine_idx in range(max_machine_index): + nozzle_points = defaultdict(int) + for idx, nozzle in component_nozzle.items(): + if component_points[idx] == 0: + continue + nozzle_points[nozzle] += machine_component_points[machine_idx][idx] + + machine_points = sum(machine_component_points[machine_idx]) # num of placement points + if machine_points == 0: + continue + ul = math.ceil(len(nozzle_points) * 1.0 / max_head_index) - 1 # num of nozzle set + + # assignments of nozzles to heads + wl = 0 # num of workload + total_heads = (1 + ul) * max_head_index - len(nozzle_points) + nozzle_heads = defaultdict(int) + for nozzle in nozzle_points.keys(): + nozzle_heads[nozzle] = math.floor(nozzle_points[nozzle] * 1.0 / machine_points * total_heads) + nozzle_heads[nozzle] += 1 + + total_heads = (1 + ul) * max_head_index + for heads in nozzle_heads.values(): + total_heads -= heads + + for nozzle in sorted(nozzle_heads, key=lambda x: nozzle_points[x] / nozzle_heads[x], reverse=True): + if total_heads == 0: + break + nozzle_heads[nozzle] += 1 + total_heads -= 1 + + # averagely assign placements to heads + heads_placement = [] + for nozzle in nozzle_heads.keys(): + points = math.floor(nozzle_points[nozzle] / nozzle_heads[nozzle]) + + heads_placement += [[nozzle, points] for _ in range(nozzle_heads[nozzle])] + nozzle_points[nozzle] -= (nozzle_heads[nozzle] * points) + for idx in range(len(heads_placement) - 1, -1, -1): + if nozzle_points[nozzle] <= 0: + break + nozzle_points[nozzle] -= 1 + heads_placement[idx][1] += 1 + heads_placement = sorted(heads_placement, key=lambda x: x[1], reverse=True) + + # every max_head_index heads in the non-decreasing order are grouped together as nozzle set + for idx in range(len(heads_placement) // max_head_index): + wl += heads_placement[idx][1] + objective_val.append(T_pp * machine_points + T_tr * wl + T_nc * ul) + + return objective_val, machine_component_points + + +def assemblyline_optimizer_genetic(pcb_data, component_data): + # basic parameter + # crossover rate & mutation rate: 80% & 10% + # population size: 200 + # the number of generation: 500 + crossover_rate, mutation_rate = 0.8, 0.1 + population_size, n_generations = 200, 500 + + # the number of placement points, the number of available feeders, and nozzle type of component respectively + component_points, component_feeders, component_nozzle = defaultdict(int), defaultdict(int), defaultdict(str) + for data in pcb_data.iterrows(): + part_index = component_data[component_data['part'] == data[1]['part']].index.tolist()[0] + nozzle = component_data.loc[part_index]['nz'] + + component_points[part_index] += 1 + component_feeders[part_index] = component_data.loc[part_index]['feeder-limit'] + component_nozzle[part_index] = nozzle + + # population initialization + best_popval = [] + population = selective_initialization(component_points, component_feeders, population_size) + with tqdm(total=n_generations) as pbar: + pbar.set_description('genetic algorithm process for PCB assembly line balance') + + new_population, new_pop_val = [], [] + for _ in range(n_generations): + # calculate fitness value + pop_val = [] + for individual in population: + val, assigned_points = cal_individual_val(component_points, component_nozzle, individual) + pop_val.append(max(val)) + + best_popval.append(min(pop_val)) + + select_index = get_top_k_value(pop_val, population_size - len(new_pop_val), reverse=False) + population = [population[idx] for idx in select_index] + pop_val = [pop_val[idx] for idx in select_index] + + population += new_population + for individual in new_population: + val, _ = cal_individual_val(component_points, component_nozzle, individual) + pop_val.append(max(val)) + + # min-max convert + max_val = max(pop_val) + pop_val = list(map(lambda v: max_val - v, pop_val)) + sum_pop_val = sum(pop_val) + 1e-10 + pop_val = [v / sum_pop_val + 1e-3 for v in pop_val] + + # crossover and mutation + new_population = [] + for pop in range(population_size): + if pop % 2 == 0 and np.random.random() < crossover_rate: + index1 = roulette_wheel_selection(pop_val) + while True: + index2 = roulette_wheel_selection(pop_val) + if index1 != index2: + break + + offspring1, offspring2 = selective_crossover(component_points, component_feeders, + population[index1], population[index2]) + + if np.random.random() < mutation_rate: + offspring1 = constraint_swap_mutation(component_points, offspring1) + + if np.random.random() < mutation_rate: + offspring1 = constraint_swap_mutation(component_points, offspring1) + + new_population.append(offspring1) + new_population.append(offspring2) + + pbar.update(1) + + best_individual = population[np.argmax(pop_val)] + _, assignment_result = cal_individual_val(component_points, component_nozzle, best_individual) + + # available feeder check + for part_index, data in component_data.iterrows(): + feeder_limit = data['feeder-limit'] + for machine_index in range(max_machine_index): + if assignment_result[machine_index][part_index]: + feeder_limit -= 1 + assert feeder_limit >= 0 + + return assignment_result diff --git a/optimizer_heuristic.py b/optimizer_heuristic.py new file mode 100644 index 0000000..3ca559f --- /dev/null +++ b/optimizer_heuristic.py @@ -0,0 +1,16 @@ +from base_optimizer.optimizer_common import * + + +# TODO: 需要考虑贴装点分布位置的限制 +def assembly_time_estimator(pcb_data, component_data, assignment): + return 0 + + +def assemblyline_optimizer_heuristic(pcb_data, component_data): + assignment_result = [] + + + # for machine_index in range(max_machine_index): + # assembly_time_estimator(pcb_data, component_data, assignment_result[machine_index]) + + return assignment_result diff --git a/result_analysis.py b/result_analysis.py index 2c6b71f..d7d9866 100644 --- a/result_analysis.py +++ b/result_analysis.py @@ -1,4 +1,5 @@ -from optimizer_common import * +from base_optimizer.optimizer_common import * + # 绘制各周期从供料器周期拾取的元件位置 def pickup_cycle_schematic(feeder_slot_result, cycle_result): @@ -6,10 +7,10 @@ def pickup_cycle_schematic(feeder_slot_result, cycle_result): plt.rcParams['axes.unicode_minus'] = False # 解决保存图像是负号'-'显示为方块的问题 # data bar_width = .7 - feeder_part = np.zeros((int)(max_slot_index / 2), dtype = np.int) + feeder_part = np.zeros(int(max_slot_index / 2), dtype=np.int) for cycle in range(len(feeder_slot_result)): label_str = '周期' + str(cycle + 1) - cur_feeder_part = np.zeros((int)(max_slot_index / 2), dtype = np.int) + cur_feeder_part = np.zeros(int(max_slot_index / 2), dtype=np.int) for slot in feeder_slot_result[cycle]: if slot > 0: cur_feeder_part[slot] += cycle_result[cycle] @@ -38,7 +39,7 @@ def placement_route_schematic(pcb_data, component_result, cycle_result, feeder_s mount_pos = [] for head in head_sequence[cycle]: index = placement_result[cycle][head] - plt.text(pos_x[index], pos_y[index] + 0.1, 'HD%d' % (head + 1), ha='center', va = 'bottom', size = 10) + plt.text(pos_x[index], pos_y[index] + 0.1, 'HD%d' % (head + 1), ha='center', va='bottom', size=10) plt.plot([pos_x[index], pos_x[index] - head * head_interval], [pos_y[index], pos_y[index]], linestyle='-.', color='black', linewidth=1) mount_pos.append([pos_x[index] - head * head_interval, pos_y[index]]) @@ -48,7 +49,8 @@ def placement_route_schematic(pcb_data, component_result, cycle_result, feeder_s # 绘制贴装路径 for i in range(len(mount_pos) - 1): - plt.plot([mount_pos[i][0], mount_pos[i + 1][0]], [mount_pos[i][1], mount_pos[i + 1][1]], color='blue', linewidth=1) + plt.plot([mount_pos[i][0], mount_pos[i + 1][0]], [mount_pos[i][1], mount_pos[i + 1][1]], color='blue', + linewidth=1) draw_x, draw_y = [], [] for c in range(cycle, len(placement_result)): @@ -65,8 +67,8 @@ def placement_route_schematic(pcb_data, component_result, cycle_result, feeder_s # 绘制供料器位置布局 for slot in range(max_slot_index // 2): - plt.scatter(slotf1_pos[0] + slot_interval * slot, slotf1_pos[1], marker = 'x', s = 12, color = 'green') - plt.text(slotf1_pos[0] + slot_interval * slot, slotf1_pos[1] - 50, slot + 1, ha = 'center', va = 'bottom', size = 8) + plt.scatter(slotf1_pos[0] + slot_interval * slot, slotf1_pos[1], marker='x', s=12, color='green') + plt.text(slotf1_pos[0] + slot_interval * slot, slotf1_pos[1] - 50, slot + 1, ha='center', va='bottom', size=8) feeder_part, feeder_counter = {}, {} placement_cycle = 0 @@ -84,7 +86,8 @@ def placement_route_schematic(pcb_data, component_result, cycle_result, feeder_s placement_cycle += cycle_result[cycle_] for slot, part in feeder_part.items(): - plt.text(slotf1_pos[0] + slot_interval * (slot - 1), slotf1_pos[1] + 15, part + ': ' + str(feeder_counter[slot]), ha = 'center', size = 7, rotation = 90) + plt.text(slotf1_pos[0] + slot_interval * (slot - 1), slotf1_pos[1] + 15, + part + ': ' + str(feeder_counter[slot]), ha='center', size=7, rotation=90) plt.plot([slotf1_pos[0] - slot_interval / 2, slotf1_pos[0] + slot_interval * (max_slot_index // 2 - 1 + 0.5)], [slotf1_pos[1] + 10, slotf1_pos[1] + 10], color = 'black') @@ -93,7 +96,7 @@ def placement_route_schematic(pcb_data, component_result, cycle_result, feeder_s for counter in range(max_slot_index // 2 + 1): pos = slotf1_pos[0] + (counter - 0.5) * slot_interval - plt.plot([pos, pos], [slotf1_pos[1] + 10, slotf1_pos[1] - 40], color='black', linewidth = 1) + plt.plot([pos, pos], [slotf1_pos[1] + 10, slotf1_pos[1] - 40], color='black', linewidth=1) # 绘制拾取路径 pick_slot = [] @@ -130,7 +133,8 @@ def placement_route_schematic(pcb_data, component_result, cycle_result, feeder_s plt.show() -def save_placement_route_figure(file_name, pcb_data, component_result, cycle_result, feeder_slot_result, placement_result, head_sequence): +def save_placement_route_figure(file_name, pcb_data, component_result, cycle_result, feeder_slot_result, + placement_result, head_sequence): path = 'result/' + file_name[:file_name.find('.')] if not os.path.exists(path): os.mkdir(path) @@ -150,8 +154,8 @@ def save_placement_route_figure(file_name, pcb_data, component_result, cycle_res for head in head_sequence[cycle]: index = placement_result[cycle][head] plt.text(pos_x[index], pos_y[index] + 0.1, 'HD%d' % (head + 1), ha='center', va='bottom', size=10) - plt.plot([pos_x[index], pos_x[index] - head * head_interval], [pos_y[index], pos_y[index]], linestyle='-.', - color='black', linewidth=1) + plt.plot([pos_x[index], pos_x[index] - head * head_interval], [pos_y[index], pos_y[index]], + linestyle='-.', color='black', linewidth=1) mount_pos.append([pos_x[index] - head * head_interval, pos_y[index]]) plt.plot(mount_pos[-1][0], mount_pos[-1][1], marker='^', color='red', markerfacecolor='white') @@ -196,10 +200,12 @@ def save_placement_route_figure(file_name, pcb_data, component_result, cycle_res plt.text(slotf1_pos[0] + slot_interval * (slot - 1), slotf1_pos[1] + 15, part + ': ' + str(feeder_counter[slot]), ha='center', size=7, rotation=90) - plt.plot([slotf1_pos[0] - slot_interval / 2, slotf1_pos[0] + slot_interval * (max_slot_index // 2 - 1 + 0.5)], - [slotf1_pos[1] + 10, slotf1_pos[1] + 10], color='black') - plt.plot([slotf1_pos[0] - slot_interval / 2, slotf1_pos[0] + slot_interval * (max_slot_index // 2 - 1 + 0.5)], - [slotf1_pos[1] - 40, slotf1_pos[1] - 40], color='black') + plt.plot( + [slotf1_pos[0] - slot_interval / 2, slotf1_pos[0] + slot_interval * (max_slot_index // 2 - 1 + 0.5)], + [slotf1_pos[1] + 10, slotf1_pos[1] + 10], color='black') + plt.plot( + [slotf1_pos[0] - slot_interval / 2, slotf1_pos[0] + slot_interval * (max_slot_index // 2 - 1 + 0.5)], + [slotf1_pos[1] - 40, slotf1_pos[1] - 40], color='black') for counter in range(max_slot_index // 2 + 1): pos = slotf1_pos[0] + (counter - 0.5) * slot_interval @@ -217,12 +223,13 @@ def save_placement_route_figure(file_name, pcb_data, component_result, cycle_res pick_slot = list(set(pick_slot)) pick_slot = sorted(pick_slot) - plt.plot([mount_pos[0][0], slotf1_pos[0] + slot_interval * (pick_slot[0] - 1)], [mount_pos[0][1], slotf1_pos[1]], + plt.plot([mount_pos[0][0], slotf1_pos[0] + slot_interval * (pick_slot[0] - 1)], + [mount_pos[0][1], slotf1_pos[1]], color='blue', linewidth=1) + plt.plot([mount_pos[-1][0], slotf1_pos[0] + slot_interval * (pick_slot[-1] - 1)], + [mount_pos[-1][1], slotf1_pos[1]], color='blue', linewidth=1) + plt.plot([slotf1_pos[0] + slot_interval * (pick_slot[0] - 1), + slotf1_pos[0] + slot_interval * (pick_slot[-1] - 1)], [slotf1_pos[1], slotf1_pos[1]], color='blue', linewidth=1) - plt.plot([mount_pos[-1][0], slotf1_pos[0] + slot_interval * (pick_slot[-1] - 1)], [mount_pos[-1][1], slotf1_pos[1]], - color='blue', linewidth=1) - plt.plot([slotf1_pos[0] + slot_interval * (pick_slot[0] - 1), slotf1_pos[0] + slot_interval * (pick_slot[-1] - 1)], - [slotf1_pos[1], slotf1_pos[1]], color='blue', linewidth=1) plt.savefig(path + '/cycle_{}'.format(cycle + 1))