diff --git a/opt/hyper_heuristic.py b/opt/hyper_heuristic.py new file mode 100644 index 0000000..5db4237 --- /dev/null +++ b/opt/hyper_heuristic.py @@ -0,0 +1,539 @@ +from opt.predictor import NeuralPredictor +from opt.utils import * +from core.interface import * +from core.common import * + +os.environ['KMP_DUPLICATE_LIB_OK'] = 'True' + + +class Heuristic: + @staticmethod + def apply(cp_points, cp_nozzle, cp_assign, config: defaultdict[int]): + return -1 + + +class LeastPoints(Heuristic): + @staticmethod + def apply(cp_points, cp_nozzle, cp_assign, config: defaultdict[int]): + machine_index, machine_points = [], [] + for index in config.keys(): + if len(cp_assign[index]) == 0: + return index + machine_index.append(index) + machine_points.append(sum([cp_points[cp_idx] for cp_idx in cp_assign[index]])) + + return machine_index[np.argmin(machine_points)] + + +class LeastNzTypes(Heuristic): + @staticmethod + def apply(cp_points, cp_nozzle, cp_assign, config: defaultdict[int]): + machine_index, machine_nozzle = [], [] + for index in config.keys(): + if len(cp_assign[index]) == 0: + return index + machine_index.append(index) + machine_nozzle.append([cp_nozzle[cp_idx] for cp_idx in cp_assign[index]]) + index = np.argmin( + [len(set(nozzle)) + 1e-5 * sum(cp_points[c] for c in cp_assign[machine_idx]) for machine_idx, nozzle in + enumerate(machine_nozzle)]) + + return machine_index[index] + + +class LeastCpTypes(Heuristic): + @staticmethod + def apply(cp_points, cp_nozzle, cp_assign, config: defaultdict[int]): + machine_index, machine_types = [], [] + for index in config.keys(): + machine_index.append(index) + machine_types.append(len(cp_assign[index]) + 1e-5 * sum(cp_points[cp] for cp in cp_assign[index])) + + return machine_index[np.argmin(machine_types)] + + +class LeastCpNzRatio(Heuristic): + @staticmethod + def apply(cp_points, cp_nozzle, cp_assign, config: defaultdict[int]): + machine_index, machine_nz_type, machine_cp_type = [], [], [] + for index in config.keys(): + if len(cp_assign[index]) == 0: + return index + machine_index.append(index) + machine_nz_type.append(set(cp_nozzle[cp_idx] for cp_idx in cp_assign[index])) + machine_cp_type.append(len(cp_assign[index])) + + min_idx = np.argmin([(machine_cp_type[idx] + 1e-5 * sum( + cp_points[c] for c in cp_assign[machine_index[idx]])) / (len(machine_nz_type[idx]) + 1e-5) for idx in + range(len(machine_index))]) + + return machine_index[min_idx] + + +def nozzle_assignment(cp_points, cp_nozzle, cp_assign, head_num): + nozzle_points = defaultdict(int) + + for cp_idx in cp_assign: + nozzle_points[cp_nozzle[cp_idx]] += cp_points[cp_idx] + + while len(nozzle_points.keys()) > head_num: + del nozzle_points[min(nozzle_points.items(), key=lambda x: x[1])[0]] + + sum_points = sum(nozzle_points.values()) + nozzle_points = defaultdict(int, {k: v for k, v in nozzle_points.items() if v / sum_points >= 0.8 / head_num}) + nozzle_heads = defaultdict(int, {k: 1 for k in nozzle_points.keys()}) + + while sum(nozzle_heads.values()) != head_num: + max_cycle_nozzle = None + for nozzle, head_cnt in nozzle_heads.items(): + if max_cycle_nozzle is None or nozzle_points[nozzle] / head_cnt > nozzle_points[max_cycle_nozzle] / \ + nozzle_heads[max_cycle_nozzle]: + max_cycle_nozzle = nozzle + + assert max_cycle_nozzle is not None + nozzle_heads[max_cycle_nozzle] += 1 + return nozzle_heads, nozzle_points + + +class LeastCycle(Heuristic): + @staticmethod + def apply(cp_points, cp_nozzle, cp_assign, config: defaultdict[int]): + machine_index, machine_cycle = [], [] + for index, head_num in config.items(): + assign_component = cp_assign[index] + if len(assign_component) == 0: + return index + + nozzle_heads, nozzle_points = nozzle_assignment(cp_points, cp_nozzle, assign_component, head_num) + machine_index.append(index) + machine_cycle.append( + max(nozzle_points[nozzle] / head for nozzle, head in nozzle_heads.items()) + 1e-5 * sum( + cp_points[c] for c in cp_assign[index])) + + return machine_index[np.argmin(machine_cycle)] + + +class LeastNzChange(Heuristic): + @staticmethod + def apply(cp_points, cp_nozzle, cp_assign, config: defaultdict[int]): + machine_index, machine_nozzle_change = [], [] + for index, head_num in config.items(): + assign_component = cp_assign[index] + if len(assign_component) == 0: + return index + + heads_points = [] + nozzle_heads, nozzle_points = nozzle_assignment(cp_points, cp_nozzle, assign_component, head_num) + for nozzle, head in nozzle_heads.items(): + for _ in range(head): + heads_points.append(nozzle_points[nozzle] / nozzle_heads[nozzle]) + machine_index.append(index) + machine_nozzle_change.append(np.std(heads_points) + 1e-5 * sum(cp_points[c] for c in cp_assign[index])) + + return machine_index[np.argmin(machine_nozzle_change)] + + +class LeastPickup(Heuristic): + @staticmethod + def apply(cp_points, cp_nozzle, cp_assign, config: defaultdict[int]): + machine_index, machine_pick_up = [], [] + for index, head_num in config.items(): + assign_component = cp_assign[index] + if len(assign_component) == 0: + return index + nozzle_heads, nozzle_points = nozzle_assignment(cp_points, cp_nozzle, assign_component, head_num) + + nozzle_level, nozzle_counter = defaultdict(int), defaultdict(int) + level_points = defaultdict(int) + + for cp_idx in sorted(assign_component, key=lambda x: cp_points[x], reverse=True): + nozzle, points = cp_nozzle[cp_idx], cp_points[cp_idx] + if nozzle not in nozzle_heads.keys(): + continue + if nozzle_counter[nozzle] and nozzle_counter[nozzle] % nozzle_heads[nozzle] == 0: + nozzle_level[nozzle] += 1 + level = nozzle_level[nozzle] + level_points[level] = max(level_points[level], points) + nozzle_counter[nozzle] += 1 + machine_index.append(index) + machine_pick_up.append(sum(points for points in level_points.values()) + 1e-5 * sum( + cp_points[idx] for idx in cp_assign[index])) + + return machine_index[np.argmin(machine_pick_up)] + + +class HyperHeuristicOpt(BaseOpt): + def __init__(self, machine_num, part_data, step_data, feeder_data=None): + super().__init__(None, part_data, step_data, feeder_data) + self.line_config = [MachineConfig() for _ in range(machine_num)] + # self.base_opt = FeederPriorityOpt + self.base_opt = CellDivisionOpt + self.heuristic_map = { + 'p': LeastPoints, + 'n': LeastNzTypes, + 'c': LeastCpTypes, + 'r': LeastCpNzRatio, + 'k': LeastCycle, + 'g': LeastNzChange, + 'u': LeastPickup, + } + self.machine_num = machine_num + self.predictor = NeuralPredictor() + + self.cp_feeders = defaultdict(int) + self.cp_nozzle = defaultdict(str) + self.cp_points = defaultdict(int) + self.cp_index = defaultdict(int) + + part_points = defaultdict(int) + for _, data in self.step_data.iterrows(): + part_points[data.part] += 1 + + division_part = [] + for _, data in self.part_data.iterrows(): + division_part.extend([part_points[data.part] / data.fdn for _ in range(data.fdn)]) + division_points = sum(division_part) / len(division_part) + + idx = 0 + for cp_idx, data in self.part_data.iterrows(): + self.cp_feeders[cp_idx] = 1 + + division_data = copy.deepcopy(data) + division_data['points'] = part_points[data.part] + feeder_limit, total_points = division_data.fdn, division_data.points + if feeder_limit != 1: + feeder_limit = round(min(max(total_points // division_points * 1.5, feeder_limit), total_points)) + # feeder_limit = total_points # 小规模数据启用 + surplus_points = total_points % feeder_limit + for _ in range(feeder_limit): + division_data.fdn, division_data.points = 1, math.floor(total_points / feeder_limit) + if surplus_points: + division_data.points += 1 + surplus_points -= 1 + + self.cp_points[idx], self.cp_nozzle[idx] = division_data.points, division_data.nz + self.cp_index[idx] = cp_idx + idx += 1 + + self.board_width = self.step_data['x'].max() - self.step_data['x'].min() + self.board_height = self.step_data['y'].max() - self.step_data['y'].min() + + def generate_pattern(self): + """ + Generates a random pattern. + :return: The generated pattern string. + """ + return "".join([random.choice(list(self.heuristic_map.keys())) + for _ in range(random.randrange(1, len(self.cp_points)))]) + + def convertor(self, component_list, individual): + component_num = len(self.cp_feeders.keys()) + + cp_assign = [[] for _ in range(self.machine_num)] + component_machine_assign = [[0 for _ in range(self.machine_num)] for _ in range(component_num)] + machine_assign_counter = [0 for _ in range(self.machine_num)] + + for idx, div_cp_idx in enumerate(component_list): + h = individual[idx % len(individual)] + cp_idx = self.cp_index[div_cp_idx] + if self.cp_points[cp_idx] == 0: + continue + machine_config = defaultdict(int) # 可被分配的机器索引-贴片头数 + if sum(component_machine_assign[cp_idx][:]) < self.cp_feeders[cp_idx]: + for machine_index in range(self.machine_num): + if component_machine_assign[cp_idx][machine_index] or machine_assign_counter[machine_index] < \ + self.predictor.max_placement_points: + machine_config[machine_index] = self.line_config[machine_index].head_num + machine_index = self.heuristic_map[h].apply(self.cp_points, self.cp_nozzle, cp_assign, machine_config) + else: + for machine_index in range(self.machine_num): + if component_machine_assign[cp_idx][machine_index]: + machine_config[machine_index] = self.line_config[machine_index].head_num + machine_index = self.heuristic_map[h].apply(self.cp_points, self.cp_nozzle, cp_assign, machine_config) + + cp_assign[machine_index].append(div_cp_idx) + + if component_machine_assign[cp_idx][machine_index] == 0: + machine_assign_counter[machine_index] += 1 + component_machine_assign[cp_idx][machine_index] = 1 + + return cp_assign + + def crossover(self, parent1, parent2): + """ + Attempt to perform crossover between two chromosomes. + :param parent1: The first parent. + :param parent2: The second parent. + :return: The two individuals after crossover has been performed. + """ + point1, point2 = random.randrange(len(parent1)), random.randrange(len(parent2)) + substr1, substr2 = parent1[point1:], parent2[point2:] + offspring1, offspring2 = "".join((parent1[:point1], substr2)), "".join((parent2[:point2], substr1)) + return offspring1[:len(self.cp_points)], offspring2[:len(self.cp_points)] + + def mutation(self, individual): + """ + Attempts to mutate the individual by replacing a random heuristic in the chromosome by a generated pattern. + :param individual: The individual to mutate. + :return: The mutated individual. + """ + pattern = list(individual) + mutation_point = random.randrange(len(pattern)) + pattern[mutation_point] = self.generate_pattern() + return ''.join(pattern)[:len(self.cp_points)] + + def initialize(self, population_size): + return [self.generate_pattern() for _ in range(population_size)] + + def cal_ind_val(self, component_list, individual): + machine_cp_assign = self.convertor(component_list, individual) + component_number = len(self.cp_feeders) + + machine_cp_points = [[0 for _ in range(component_number)] for _ in range(self.machine_num)] + for machine_idx in range(self.machine_num): + for idx in machine_cp_assign[machine_idx]: + machine_cp_points[machine_idx][self.cp_index[idx]] += self.cp_points[idx] + machine_cp_feeders = [[0 for _ in range(component_number)] for _ in range(self.machine_num)] + + for cp_idx in range(component_number): + if self.cp_points[cp_idx] == 0: + continue + feeder_nums = self.cp_feeders[cp_idx] + for machine_idx in range(self.machine_num): + if machine_cp_points[machine_idx][cp_idx]: + machine_cp_feeders[machine_idx][cp_idx] = 1 + feeder_nums -= 1 + while feeder_nums > 0: + assign_machine = None + for machine_idx in range(self.machine_num): + if machine_cp_points[machine_idx][cp_idx] == 0: + continue + if assign_machine is None: + assign_machine = machine_idx + continue + if machine_cp_points[assign_machine][cp_idx] / machine_cp_feeders[assign_machine][cp_idx] \ + < machine_cp_points[machine_idx][cp_idx] / machine_cp_feeders[machine_idx][cp_idx]: + assign_machine = machine_idx + machine_cp_feeders[assign_machine][cp_idx] += 1 + feeder_nums -= 1 + nozzle_type = defaultdict(str) + for idx, cp_idx in self.cp_index.items(): + nozzle_type[cp_idx] = self.cp_nozzle[idx] + + obj = [] + for machine_idx in range(self.machine_num): + div_cp_points, div_cp_nozzle = defaultdict(int), defaultdict(str) + idx = 0 + for cp_idx in range(component_number): + total_points = machine_cp_points[machine_idx][cp_idx] + if total_points == 0: + continue + div_index = 0 + div_points = [total_points // machine_cp_feeders[machine_idx][cp_idx] for _ in + range(machine_cp_feeders[machine_idx][cp_idx])] + while sum(div_points) < total_points: + div_points[div_index] += 1 + div_index += 1 + + for points in div_points: + div_cp_points[idx] = points + div_cp_nozzle[idx] = nozzle_type[cp_idx] + idx += 1 + obj.append(self.predictor.eval(div_cp_points, div_cp_nozzle, + self.board_width, self.board_height, self.line_config[machine_idx])) + + return obj + + def evaluate(self, assignment): + partial_step_data, partial_part_data = defaultdict(pd.DataFrame), defaultdict(pd.DataFrame) + for machine_index in range(self.machine_num): + partial_step_data[machine_index] = pd.DataFrame(columns=self.step_data.columns) + partial_part_data[machine_index] = self.part_data.copy(deep=True) + partial_part_data[machine_index]['points'] = 0 + + # averagely assign available feeder + for part_index, data in self.part_data.iterrows(): + feeder_limit = data.fdn + feeder_points = [assignment[machine_index][part_index] for machine_index in range(self.machine_num)] + if sum(feeder_points) == 0: + continue + + for machine_index in range(self.machine_num): + partial_part_data[machine_index].loc[part_index, 'points'] = 0 + + for machine_index in range(self.machine_num): + if feeder_points[machine_index] == 0: + continue + + partial_part_data[machine_index].loc[part_index, 'fdn'] = 1 + feeder_limit -= 1 + + while feeder_limit: + assign_machine = None + for machine_index in range(self.machine_num): + if feeder_limit <= 0: + break + + if feeder_points[machine_index] == 0: + continue + + if assign_machine is None or feeder_points[machine_index] / \ + partial_part_data[machine_index].loc[part_index].fdn > feeder_points[ + assign_machine] / partial_part_data[assign_machine].loc[part_index].fdn: + assign_machine = machine_index + + assert assign_machine is not None + partial_part_data[assign_machine].loc[part_index, 'fdn'] += 1 + + feeder_limit -= 1 + + for machine_index in range(self.machine_num): + if feeder_points[machine_index] > 0: + assert partial_part_data[machine_index].loc[part_index].fdn > 0 # assignment[machine_index][part_index] + + # === assign placements === + part2idx = defaultdict(int) + for idx, data in self.part_data.iterrows(): + part2idx[data.part] = idx + + machine_average_pos = [[0, 0] for _ in range(self.machine_num)] + machine_step_counter = [0 for _ in range(self.machine_num)] + part_step_data = defaultdict(list) + for _, data in self.step_data.iterrows(): + part_step_data[part2idx[data.part]].append(data) + + multiple_component_index = [] + for part_index in range(len(self.part_data)): + machine_assign_set = [] + for machine_index in range(self.machine_num): + if assignment[machine_index][part_index]: + machine_assign_set.append(machine_index) + + if len(machine_assign_set) == 1: + for data in part_step_data[part_index]: + machine_index = machine_assign_set[0] + + machine_average_pos[machine_index][0] += data.x + machine_average_pos[machine_index][1] += data.y + + machine_step_counter[machine_index] += 1 + + partial_part_data[machine_index].loc[part_index, 'points'] += 1 + partial_step_data[machine_index] = pd.concat( + [partial_step_data[machine_index], pd.DataFrame(data).T]) + + elif len(machine_assign_set) > 1: + multiple_component_index.append(part_index) + + for machine_index in range(self.machine_num): + if machine_step_counter[machine_index] == 0: + continue + machine_average_pos[machine_index][0] /= machine_step_counter[machine_index] + machine_average_pos[machine_index][1] /= machine_step_counter[machine_index] + + for part_index in multiple_component_index: + for data in part_step_data[part_index]: + idx = -1 + min_dist = None + for machine_index in range(self.machine_num): + if partial_part_data[machine_index].loc[part_index, 'points'] >= assignment[machine_index][part_index]: + continue + dist = (data.x - machine_average_pos[machine_index][0]) ** 2 + ( + data.y - machine_average_pos[machine_index][1]) ** 2 + if min_dist is None or dist < min_dist: + min_dist, idx = dist, machine_index + + assert idx >= 0 + machine_step_counter[idx] += 1 + machine_average_pos[idx][0] += (1 - 1 / machine_step_counter[idx]) * machine_average_pos[idx][0] \ + + data.x / machine_step_counter[idx] + machine_average_pos[idx][1] += (1 - 1 / machine_step_counter[idx]) * machine_average_pos[idx][1] \ + + data.y / machine_step_counter[idx] + + partial_part_data[idx].loc[part_index, 'points'] += 1 + partial_step_data[idx] = pd.concat([partial_step_data[idx], pd.DataFrame(data).T]) + + obj, result = [], [] + for machine_index in range(self.machine_num): + rows = partial_part_data[machine_index]['points'] != 0 + partial_part_data[machine_index] = partial_part_data[machine_index][rows] + + opt = self.base_opt(self.line_config[machine_index], partial_part_data[machine_index], + partial_step_data[machine_index]) + opt.optimize(hinter=False) + info = evaluation(self.line_config[machine_index], partial_part_data[machine_index], + partial_step_data[machine_index], opt.result) + obj.append(info.total_time) + result.append(opt.result) + + return max(obj), result + + def optimize(self): + # genetic-based hyper-heuristic + crossover_rate, mutation_rate = 0.6, 0.1 + population_size, total_generation = 20, 50 + group_size = 10 + + best_val = np.inf + + component_list = list(range(len(self.cp_points))) + with tqdm(total=total_generation * group_size) as pbar: + pbar.set_description('hyper-heuristic algorithm process for PCB assembly line balance') + for _ in range(group_size): + random.shuffle(component_list) + new_population = [] + population = self.initialize(population_size) + + # calculate fitness value + pop_val = [max(self.cal_ind_val(component_list, individual)) for individual in population] + + for _ in range(total_generation): + population += new_population + for individual in new_population: + pop_val.append(max(self.cal_ind_val(component_list, individual))) + + select_index = GenOpe.get_top_kth(pop_val, population_size, reverse=False) + population = [population[idx] for idx in select_index] + pop_val = [pop_val[idx] for idx in select_index] + + # min-max convert + max_val = max(pop_val) + sel_pop_val = list(map(lambda v: max_val - v, pop_val)) + sum_pop_val = sum(sel_pop_val) + 1e-10 + sel_pop_val = [v / sum_pop_val + 1e-3 for v in sel_pop_val] + + # crossover and mutation + new_population = [] + for pop in range(population_size): + if pop % 2 == 0 and np.random.random() < crossover_rate: + index1 = GenOpe.roulette_wheel_selection(sel_pop_val) + while True: + index2 = GenOpe.roulette_wheel_selection(sel_pop_val) + if index1 != index2: + break + + offspring1, offspring2 = self.crossover(population[index1], population[index2]) + + if np.random.random() < mutation_rate: + offspring1 = self.mutation(offspring1) + + if np.random.random() < mutation_rate: + offspring2 = self.mutation(offspring2) + + new_population.append(offspring1) + new_population.append(offspring2) + + pbar.update(1) + + machine_assign = self.convertor(component_list, population[0]) + + assignment_result = [[0 for _ in range(len(self.part_data))] for _ in range(self.machine_num)] + for machine_idx in range(self.machine_num): + for idx in machine_assign[machine_idx]: + assignment_result[machine_idx][self.cp_index[idx]] += self.cp_points[idx] + + val, res = self.evaluate(assignment_result) + if best_val is None or val < best_val: + best_val = val + self.result = res + diff --git a/opt/param.pth b/opt/param.pth new file mode 100644 index 0000000..fe0c812 Binary files /dev/null and b/opt/param.pth differ diff --git a/opt/predictor.py b/opt/predictor.py new file mode 100644 index 0000000..96bfa6b --- /dev/null +++ b/opt/predictor.py @@ -0,0 +1,314 @@ +from core.common import * +from opt.smm.basis import BaseOpt + +import torch + + +class Predictor: + def __init__(self): + pass + + @staticmethod + def training(self, params): + pass + + @staticmethod + def testing(self, params): + pass + + @staticmethod + def predict(self, cp_points, cp_nozzle, board_width=None, board_height=None): + pass + + +class Net(torch.nn.Module): + def __init__(self, input_size, hidden_size=1000, output_size=1): + super(Net, self).__init__() + self.fc1 = torch.nn.Linear(input_size, hidden_size) + self.relu = torch.nn.ReLU() # 激活函数 + self.fc2 = torch.nn.Linear(hidden_size, hidden_size) + # self.relu1 = torch.nn.ReLU() # 激活函数 + self.fc3 = torch.nn.Linear(hidden_size, output_size) + + def forward(self, x): + x = self.fc1(x) + # x = self.relu(x) + x = self.fc2(x) + x = self.relu(x) + x = self.fc3(x) + return x + + +class NeuralPredictor(Predictor, BaseOpt): + def __init__(self): + super().__init__() + self.min_placement_points = 10 + self.max_placement_points = 1000 + + self.max_component_types = 30 + self.default_feeder_limit = 1 + self.max_nozzle_types = 4 + + self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + + self.net = Net(input_size=self.get_feature(), output_size=1).to(self.device) + self.net_file = 'opt/param.pth' + try: + self.net.load_state_dict(torch.load(self.net_file, map_location=self.device)) + except: + warnings.warn('the parameters of neural net model load failed', UserWarning) + + def init_weights(self): + for m in self.net.modules(): + if isinstance(m, torch.nn.Linear): + torch.nn.init.xavier_uniform_(m.weight) + torch.nn.init.zeros_(m.bias) + + def subobjective(self, cp_points, cp_nozzle, config): + if len(cp_points.keys()) or sum(cp_points.values()) == 0: + return 0, 0, 0, 0 + nozzle_heads, nozzle_points = defaultdict(int), defaultdict(int) + for idx, points in cp_points.items(): + if points == 0: + continue + nozzle = cp_nozzle[idx] + nozzle_points[nozzle] += points + nozzle_heads[nozzle] = 1 + + anc_round_counter = 0 + while sum(nozzle_heads.values()) != config.head_num: + max_cycle_nozzle = None + + for nozzle, head_num in nozzle_heads.items(): + if max_cycle_nozzle is None or nozzle_points[nozzle] / head_num > nozzle_points[max_cycle_nozzle] / \ + nozzle_heads[max_cycle_nozzle]: + max_cycle_nozzle = nozzle + assert max_cycle_nozzle is not None + nozzle_heads[max_cycle_nozzle] += 1 + + head_nozzle_assignment, min_cost = None, None + + # generate initial nozzle group + nozzle_group = [] + # averagely assign for the same type of nozzles, and generate nozzle group + nozzle_points_cpy = copy.deepcopy(nozzle_points) + for nozzle, heads in nozzle_heads.items(): + points = nozzle_points_cpy[nozzle] // heads + for _ in range(heads): + nozzle_group.append([nozzle, points]) + nozzle_points_cpy[nozzle] -= heads * points + + for idx, [nozzle, _] in enumerate(nozzle_group): + if nozzle_points_cpy[nozzle]: + nozzle_group[idx][1] += 1 + nozzle_points_cpy[nozzle] -= 1 + + while True: + # assign nozzle group to each head + nozzle_group.sort(key=lambda x: -x[1]) + + tmp_head_nozzle_assignment = [] + head_total_points = [0 for _ in range(config.head_num)] + for idx, nozzle_item in enumerate(nozzle_group): + if idx < config.head_num: + tmp_head_nozzle_assignment.append([nozzle_item.copy()]) + head_total_points[idx] += nozzle_item[1] + else: + min_head = np.argmin(head_total_points) + tmp_head_nozzle_assignment[min_head].append(nozzle_item.copy()) + head_total_points[min_head] += nozzle_item[1] + + cost = config.cycle_time * max(head_total_points) + for head in range(config.head_num): + for cycle in range(len(tmp_head_nozzle_assignment[head])): + if cycle + 1 == len(tmp_head_nozzle_assignment[head]): + if tmp_head_nozzle_assignment[head][cycle][0] != tmp_head_nozzle_assignment[head][-1][0]: + cost += self.nozzle_change_weight + else: + if tmp_head_nozzle_assignment[head][cycle][0] != tmp_head_nozzle_assignment[head][cycle + 1][0]: + cost += self.nozzle_change_weight + + while True: + min_head, max_head = np.argmin(head_total_points), np.argmax(head_total_points) + min_head_nozzle, max_head_nozzle = tmp_head_nozzle_assignment[min_head][-1][0], \ + tmp_head_nozzle_assignment[max_head][-1][0] + if min_head_nozzle == max_head_nozzle: + break + + min_head_list, max_head_list = [min_head], [max_head] + minmax_head_points = 0 + for head in range(config.head_num): + if head in min_head_list or head in max_head_list: + minmax_head_points += head_total_points[head] + continue + + # the max/min heads with the sum nozzle type + if tmp_head_nozzle_assignment[head][-1][0] == tmp_head_nozzle_assignment[min_head][-1][0]: + min_head_list.append(head) + minmax_head_points += head_total_points[head] + + if tmp_head_nozzle_assignment[head][-1][0] == tmp_head_nozzle_assignment[max_head][-1][0]: + max_head_list.append(head) + minmax_head_points += head_total_points[head] + + # todo: restriction of available nozzle + # the reduction of cycles is not offset the cost of nozzle change + average_points = minmax_head_points // (len(min_head_list) + len(max_head_list)) + reminder_points = minmax_head_points % (len(min_head_list) + len(max_head_list)) + max_cycle = average_points + (1 if reminder_points > 0 else 0) + for head in range(config.head_num): + if head in min_head_list or head in max_head_list: + continue + max_cycle = max(max_cycle, head_total_points[head]) + + nozzle_change_counter = 0 + for head in min_head_list: + if tmp_head_nozzle_assignment[head][0] == tmp_head_nozzle_assignment[head][-1]: + nozzle_change_counter += 2 + else: + nozzle_change_counter += 1 + + if self.cycle_weight * (max(head_total_points) - max_cycle) < self.nozzle_change_weight * nozzle_change_counter: + break + + cost -= self.cycle_weight * (max(head_total_points) - max_cycle) - self.nozzle_change_weight * nozzle_change_counter + + required_points = 0 # 待均摊的贴装点数较多的吸嘴类型 + for head in min_head_list: + points = average_points - head_total_points[head] + tmp_head_nozzle_assignment[head].append([max_head_nozzle, points]) + head_total_points[head] = average_points + required_points += points + + for head in max_head_list: + tmp_head_nozzle_assignment[head][-1][1] -= required_points // len(max_head_list) + head_total_points[head] -= required_points // len(max_head_list) + + required_points -= (required_points // len(max_head_list)) * len(max_head_list) + + for head in max_head_list: + if required_points <= 0: + break + tmp_head_nozzle_assignment[head][-1][1] -= 1 + head_total_points[head] -= 1 + required_points -= 1 + + if min_cost is None or cost < min_cost: + min_cost = cost + head_nozzle_assignment = copy.deepcopy(tmp_head_nozzle_assignment) + else: + break + + # 在吸嘴组中增加一个吸嘴 + idx, nozzle = 0, nozzle_group[0][0] + for idx, [nozzle_, _] in enumerate(nozzle_group): + if nozzle_ != nozzle: + break + + average_points, remainder_points = nozzle_points[nozzle] // (idx + 1), nozzle_points[nozzle] % (idx + 1) + nozzle_group.append([nozzle, 0]) + for idx, [nozzle_, _] in enumerate(nozzle_group): + if nozzle_ == nozzle: + nozzle_group[idx][1] = average_points + (1 if remainder_points > 0 else 0) + remainder_points -= 1 + + cycle_counter, nozzle_change_counter = 0, 0 + for head in range(config.head_num): + head_cycle_counter = 0 + for cycle in range(len(head_nozzle_assignment[head])): + if cycle + 1 == len(head_nozzle_assignment[head]): + if head_nozzle_assignment[head][0][0] != head_nozzle_assignment[head][-1][0]: + nozzle_change_counter += 1 + else: + if head_nozzle_assignment[head][cycle][0] != head_nozzle_assignment[head][cycle + 1][0]: + nozzle_change_counter += 1 + head_cycle_counter += head_nozzle_assignment[head][cycle][1] + cycle_counter = max(cycle_counter, head_cycle_counter) + + # === 元件拾取次数预估 === + cp_info = [] + for idx, points in cp_points.items(): + if points == 0: + continue + feeder_limit = 1 # todo: 暂时仅考虑一种吸嘴的情形 + reminder_points = points % feeder_limit + for _ in range(feeder_limit): + cp_info.append([idx, points // feeder_limit + (1 if reminder_points > 0 else 0), cp_nozzle[idx]]) + reminder_points -= 1 + + cp_info.sort(key=lambda x: -x[1]) + + nozzle_level, nozzle_counter = defaultdict(int), defaultdict(int) + level_points = defaultdict(int) + for info in cp_info: + nozzle = info[2] + if nozzle_counter[nozzle] and nozzle_counter[nozzle] % nozzle_heads[nozzle] == 0: + nozzle_level[nozzle] += 1 + level = nozzle_level[nozzle] + level_points[level] = max(level_points[level], info[1]) + nozzle_counter[nozzle] += 1 + + pickup_counter = sum(points for points in level_points.values()) + + return cycle_counter, nozzle_change_counter, anc_round_counter, pickup_counter + + def encode(self, cp_points: defaultdict[str], cp_nozzle: defaultdict[str], board_width, board_height, config): + assert len(cp_points.keys()) == len(cp_nozzle.keys()) + + # === general info === + total_points = sum(points for points in cp_points.values()) + total_component_types, total_nozzle_types = len(cp_points.keys()), len(set(cp_nozzle.values())) + + data = [total_points, total_component_types, total_nozzle_types] + data.extend([board_width, board_height]) + + # === heuristic info === + cycle, nozzle_change, anc_move, pickup = self.subobjective(cp_points, cp_nozzle, config) + data.extend([cycle, nozzle_change, anc_move, pickup]) + + # === nozzle info === + nozzle_points = defaultdict(int) + for cp_idx, nozzle in cp_nozzle.items(): + nozzle_points[cp_nozzle[cp_idx]] += cp_points[cp_idx] # points for different nozzle type + nozzle_items = [[nozzle, points] for nozzle, points in nozzle_points.items()] + nozzle_items = sorted(nozzle_items, key=lambda x: x[1], reverse=True) + + nz2idx = defaultdict(int) + nozzle_slice = [0 for _ in range(self.max_nozzle_types)] + for idx, [nozzle, points] in enumerate(nozzle_items): + nz2idx[nozzle] = idx if idx < self.max_nozzle_types else self.max_nozzle_types - 1 + nozzle_slice[idx if idx < self.max_nozzle_types else -1] += points + data.extend(nozzle_slice) + + # === part info === + part_data_slice = defaultdict(list) + for idx in range(self.max_nozzle_types): + part_data_slice[idx] = [] + + cp_items = [[component, points] for component, points in cp_points.items()] + cp_items = sorted(cp_items, key=lambda x: (x[1], nz2idx[cp_nozzle[x[0]]] * 0.1 + x[1]), reverse=True) + for component, points in cp_items: + nozzle = cp_nozzle[component] + part_data_slice[nz2idx[nozzle]].append(points) + + data_slice = [0 for _ in range(self.max_nozzle_types)] + for idx, part_list in part_data_slice.items(): + data_slice[idx] = len(part_list) + data.extend(data_slice) + + for idx in range(self.max_nozzle_types): + if len(part_data_slice[idx]) <= self.max_component_types: + part_data_slice[idx].extend([0 for _ in range(self.max_component_types - len(part_data_slice[idx]))]) + else: + part_data_slice[idx] = part_data_slice[idx][:self.max_component_types] + data.extend(part_data_slice[idx]) + + return data + + def get_feature(self): + return (self.max_component_types + 2) * self.max_nozzle_types + 5 + 4 + + def eval(self, cp_points, cp_nozzle, board_width, board_height, config): + encoding = np.array(self.encode(cp_points, cp_nozzle, board_width, board_height, config)) + encoding = torch.from_numpy(encoding.reshape((-1, np.shape(encoding)[0]))).float().to(self.device) + return self.net(encoding)[0, 0].item() diff --git a/opt/smm/aggregation.py b/opt/smm/aggregation.py new file mode 100644 index 0000000..c898f07 --- /dev/null +++ b/opt/smm/aggregation.py @@ -0,0 +1,172 @@ +from opt.smm.basis import * +from opt.utils import * +from opt.smm.solver import * + + +class Aggregation(BaseOpt): + def __init__(self, config, part_data, step_data, feeder_data=pd.DataFrame(columns=['slot', 'part'])): + super().__init__(config, part_data, step_data, feeder_data) + self.feeder_assigner = FeederAssignOpt(config, part_data, step_data) + + def optimize(self, hinter=True): + # === phase 0: data preparation === + M = 1000 # a sufficient large number + a, b = 1, 6 # coefficient + + part_list, nozzle_list = defaultdict(int), defaultdict(int) + cpidx_2_part, nzidx_2_nozzle = {}, {} + for _, data in self.step_data.iterrows(): + part = data.part + if part not in cpidx_2_part.values(): + cpidx_2_part[len(cpidx_2_part)] = part + + part_list[part] += 1 + + idx = self.part_data[self.part_data['part'] == part].index.tolist()[0] + nozzle = self.part_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(part_list.keys()), len(nozzle_list.keys()) # the maximum number of part types and nozzle types + L = I + 1 # the maximum number of batch level + K = self.config.head_num # the maximum number of heads + HC = [[M for _ in range(J)] for _ in range(I)] # represent the nozzle-part compatibility + + for i in range(I): + for _, item in enumerate(cpidx_2_part.items()): + index, part = item + cp_idx = self.part_data[self.part_data['part'] == part].index.tolist()[0] + nozzle = self.part_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 === + mdl = Model('SMT') + # mdl.setParam('OutputFlag', hinter) + + # === Decision Variables === + # the largest workload of all placement heads + WL = mdl.addVar(vtype=GRB.INTEGER, lb=0, ub=len(self.step_data), name='WL') + + # the number of parts of type i that are placed by nozzle type j on placement head k + X = mdl.addVars(I, J, K, vtype=GRB.INTEGER, ub=max(part_list.values()), name='X') + + # the total number of nozzle changes on placement head k + N = mdl.addVars(K, vtype=GRB.INTEGER, name='N') + + # whether batch Xijk is placed on level l + Z = mdl.addVars(I, J, L, K, vtype=GRB.BINARY, name='Z') + + # 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 + # Dlk := 0 otherwise + D = mdl.addVars(L, K, vtype=GRB.BINARY, name='D') + D_plus = mdl.addVars(L, J, K, vtype=GRB.INTEGER, name='D_plus') + D_minus = mdl.addVars(L, J, K, vtype=GRB.INTEGER, name='D_minus') + + # == Objective function === + mdl.setObjective(a * WL + b * quicksum(N[k] for k in range(K)), GRB.MINIMIZE) + + # === Constraint === + mdl.addConstrs( + quicksum(X[i, j, k] for j in range(J) for k in range(K)) == part_list[cpidx_2_part[i]] for i in range(I)) + + mdl.addConstrs(quicksum(X[i, j, k] for i in range(I) for j in range(J)) <= WL for k in range(K)) + + mdl.addConstrs( + X[i, j, k] <= M * quicksum(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)) + + mdl.addConstrs( + quicksum(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)) + mdl.addConstrs( + quicksum(Z[i, j, l, k] for l in range(L)) <= X[i, j, k] for i in range(I) for j in range(J) for k in + range(K)) + + mdl.addConstrs(quicksum(Z[i, j, l, k] for j in range(J) for i in range(I)) >= quicksum( + Z[i, j, l + 1, k] for j in range(J) for i in range(I)) for k in range(K) for l in range(L - 1)) + + mdl.addConstrs( + quicksum(Z[i, j, l, k] for i in range(I) for j in range(J)) <= 1 for k in range(K) for l in range(L)) + mdl.addConstrs(D_plus[l, j, k] - D_minus[l, j, k] == quicksum(Z[i, j, l, k] for i in range(I)) - quicksum( + Z[i, j, l + 1, k] for i in range(I)) for l in range(L - 1) for j in range(J) for k in range(K)) + + mdl.addConstrs( + D[l, k] == quicksum((D_plus[l, j, k] + D_minus[l, j, k]) for j in range(J)) for k in range(K) for l in + range(L)) + + # mdl.addConstrs(2 * N[k] == quicksum(D[l, k] for l in range(L)) - 1 for k in range(K)) + # mdl.addConstrs( + # 0 >= quicksum(HC[i][j] * Z[i, j, l, k] for i in range(I) for j in range(J)) for l in range(L) for k in + # range(K)) + + # === Main Process === + mdl.TimeLimit = 100 + mdl.optimize() + if mdl.Status == GRB.OPTIMAL or mdl.Status == GRB.TIME_LIMIT: + print('total cost = {}'.format(mdl.objval)) + + # convert cp model solution to standard output + model_cycle_result, model_part_result = [], [] + for l in range(L): + model_part_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 abs(Z[i, j, l, k].x - 1) <= 1e-3: + model_part_result[-1][k] = cpidx_2_part[i] + model_cycle_result[-1][k] = round(X[i, j, k].x) + + # remove redundant term + if sum(model_cycle_result[-1]) == 0: + model_part_result.pop() + model_cycle_result.pop() + + head_part_index = [0 for _ in range(self.config.head_num)] + while True: + head_cycle = [] + for head, index in enumerate(head_part_index): + head_cycle.append(model_cycle_result[index][head]) + + if len([cycle for cycle in head_cycle if cycle > 0]) == 0: + break + + self.result.part.append([None for _ in range(self.config.head_num)]) + min_cycle = min([cycle for cycle in head_cycle if cycle > 0]) + for head, index in enumerate(head_part_index): + if model_cycle_result[index][head] != 0: + self.result.part[-1][head] = model_part_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_part_index[head] += 1 + + self.result.cycle.append(min_cycle) + + part_2_index = {} + for index, data in self.part_data.iterrows(): + part_2_index[data['part']] = index + + for cycle in range(len(self.result.part)): + for head in range(self.config.head_num): + part = self.result.part[cycle][head] + self.result.part[cycle][head] = -1 if part is None else part_2_index[part] + + self.result.slot = self.feeder_assigner.do(self.result.part, self.result.cycle) + # === phase 2: heuristic method === + self.result.point, self.result.sequence = self.path_planner.greedy_level_placing(self.result.part, + self.result.cycle, + self.result.slot) + else: + warnings.warn('No solution found!', UserWarning) + + + + + diff --git a/opt/smm/basis.py b/opt/smm/basis.py new file mode 100644 index 0000000..8b65130 --- /dev/null +++ b/opt/smm/basis.py @@ -0,0 +1,363 @@ +from data.type import OptResult +from opt.smm.path_plan import PathPlanOpt +from collections import defaultdict + +import numpy as np +import copy + + +class BaseOpt: + def __init__(self, config, part_data, step_data, feeder_data=None): + self.part_data = part_data + self.step_data = step_data + self.feeder_data = feeder_data + self.config = config + + self.result = OptResult() + self.path_planner = PathPlanOpt(config, part_data, step_data) + + self.cycle_weight = 1 + self.nozzle_change_weight = 1 + self.pickup_weight = 1 + self.place_weight = 1 + self.move_weight = 1 + + +class FeederAssignOpt: + def __init__(self, config, part_data, step_data, feeder_data=None): + self.part_data = part_data + self.step_data = step_data + self.feeder_data = feeder_data + self.config = config + + def find_commonpart(self, head_group, feeder_group): + feeder_group_len = len(feeder_group) + + max_length, max_common_part = -1, [] + for offset in range(-self.config.head_num + 1, feeder_group_len - 1): + # offset: head_group相对于feeder_group的偏移量 + length, common_part = 0, [] + for hd_index in range(self.config.head_num): + 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 do(self, part_result, cycle_result): + slot_result, feeder_group = [], [] + feeder_limit = {idx: data.fdn for idx, data in self.part_data.iterrows()} + + for part_cycle in part_result: + new_feeder_group = [] + for part in part_cycle: + if part == -1 or feeder_limit[part] == 0 or new_feeder_group.count(part) >= feeder_limit[part]: + new_feeder_group.append(-1) + else: + new_feeder_group.append(part) + + 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)): + common_part = self.find_commonpart(new_feeder_group, feeder_group[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.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[-1].append(feeder) + new_feeder_group[feeder_index] = -1 + feeder_limit[feeder] -= 1 + else: + feeder_group[-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 group in feeder_group: + while len(group) > 0 and group[0] == -1: + group.pop(0) + + while len(group) > 0 and group[-1] == -1: + group.pop(-1) + + # 确定供料器组的安装位置 + part_pos = defaultdict(list) + for _, data in self.step_data.iterrows(): + idx = self.part_data[self.part_data['part'].values == data.part].index.tolist()[0] + part_pos[idx].append(data.x + self.config.stopper_pos.x) + + # 元件使用的头 + CT_Head = defaultdict(list) + for part_cycle in part_result: + for head, part in enumerate(part_cycle): + if part == -1: + continue + if part not in CT_Head: + CT_Head[part] = [head, head] + CT_Head[part][0] = min(CT_Head[part][0], head) + CT_Head[part][1] = max(CT_Head[part][1], head) + + # 供料器组分配的优先顺序 + feeder_assign_sequence = [] + for i in range(len(feeder_group)): + for j in range(len(feeder_group)): + 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[seq] if k >= 0]) < cycle_result[j] * len( + [k for k in feeder_group[seq] if k >= 0]): + feeder_assign_sequence.pop(-1) + feeder_assign_sequence.append(j) + + # TODO: 暂未考虑机械限位 + feeder_group_slot = [-1] * len(feeder_group) + feeder_lane_state = [0] * self.config.slot_num # 0表示空,1表示已占有 + intv_ratio = self.config.head_intv // self.config.slot_intv + for index in feeder_assign_sequence: + group = feeder_group[index] + best_slot = [] + for cp_index, part in enumerate(group): + if part == -1: + continue + best_slot.append(round((sum(part_pos[part]) / len(part_pos[part]) - self.config.slotf1_pos.x) + / self.config.slot_intv) + 1 - cp_index * intv_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(group) - 1) * intv_ratio >= self.config.slot_num / 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 + intv_ratio * len(group), intv_ratio): + pick_part = group[(slot - assign_slot) // intv_ratio] + 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] * intv_ratio <= 0 or + slot + (self.config.head_num - CT_Head[pick_part][1] - 1) * + intv_ratio > self.config.slot_num // 2): + assign_available = False + break + + if assign_available: + for idx, part in enumerate(group): + if part != -1: + feeder_lane_state[assign_slot + idx * intv_ratio] = 1 + feeder_group_slot[index] = assign_slot + break + + if feeder_group_slot[index] == -1: + raise Exception('feeder assign error!') + + # 按照最大匹配原则,确定各元件周期拾取槽位 + for part_cycle in part_result: + slot_result.append([-1] * self.config.head_num) + head_index = [head for head, component in enumerate(part_cycle) if component >= 0] + while head_index: + max_overlap_counter = 0 + overlap_feeder_group_index, overlap_feeder_group_offset = -1, -1 + for index, group in enumerate(feeder_group): + # offset 头1 相对于 供料器组第一个元件的偏移量 + for offset in range(-self.config.head_num + 1, self.config.head_num + len(group)): + overlap_counter = 0 + for head in head_index: + if 0 <= head + offset < len(group) and part_cycle[head] == 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 = index, offset + + group = feeder_group[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(group) and part_cycle[head] == \ + group[head + overlap_feeder_group_offset]: + slot_result[-1][head] = feeder_group_slot[overlap_feeder_group_index] + intv_ratio * ( + head + overlap_feeder_group_offset) + head_index.remove(head) + + return slot_result + + +class GenOpe: + @staticmethod + def roulette_wheel_selection(pop_eval): + 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 + + @staticmethod + def get_top_kth(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 + + @staticmethod + def partially_mapped_crossover(parent1, parent2): + size = len(parent1) + start, end = sorted(np.random.randint(0, size, 2)) + + def create_child(primary_parent, secondary_parent): + child = [-1] * size + child[start:end + 1] = copy.deepcopy(secondary_parent[start:end + 1]) + + for i in range(size): + if start <= i <= end: + continue + + cur_ptr, cur_elem = 0, primary_parent[i] + while True: + child[i] = cur_elem + if child.count(cur_elem) == 1: + break + child[i] = -1 + + if cur_ptr == 0: + cur_ptr, cur_elem = 1, secondary_parent[i] + else: + index_ = child.index(cur_elem) + cur_elem = secondary_parent[index_] + + return child + + return create_child(parent1, parent2), create_child(parent2, parent1) + + @staticmethod + 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 + + @staticmethod + def directed_edge_recombine_crossover(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 + diff --git a/opt/smm/cell_division.py b/opt/smm/cell_division.py new file mode 100644 index 0000000..df952a6 --- /dev/null +++ b/opt/smm/cell_division.py @@ -0,0 +1,237 @@ +from opt.smm.basis import * +from opt.utils import * + + +class CellDivisionOpt(BaseOpt): + def __init__(self, config, part_data, step_data, feeder_data=None): + super().__init__(config, part_data, step_data, feeder_data) + + self.feeder_assigner = FeederAssignOpt(config, part_data, step_data) + + self.e_gang_pick = 0.6 + self.e_nz_change = 4 + + def evaluate(self, part_result, cycle_result, slot_result) -> float: + nozzle_change_counter = 0 + for head in range(self.config.head_num): + nozzle = '' + for cycle in range(len(part_result)): + component_index = part_result[cycle][head] + if component_index == -1: + continue + + if cycle != 0 and nozzle != self.part_data.loc[component_index, 'nz']: + nozzle_change_counter += 1 + nozzle = self.part_data.loc[component_index, 'nz'] + + gang_pick_counter = 0 + for cycle, feeder_slot in enumerate(slot_result): + pick_slot = defaultdict(int) + for head, slot in enumerate(feeder_slot): + if slot == -1: + continue + pick_slot[slot - head * (self.config.head_intv / self.config.slot_intv)] += 1 + for _ in pick_slot.values(): + gang_pick_counter += cycle_result[cycle] + + return sum(cycle_result) + self.e_nz_change * nozzle_change_counter + self.e_gang_pick * gang_pick_counter + + def convertor(self, part_cell, population): + assert part_cell['points'].sum() == len(self.step_data) + head_num = self.config.head_num + head_assignment = [[] for _ in range(head_num)] + + wl = [0 for _ in range(head_num)] # workload + + e1, e2, e3 = 1, 2, 1. / 6 + + part_result, cycle_result, feeder_slot_result = [], [], [] + for index in population: + if part_cell.loc[index]['points'] == 0: + continue + # 鍏冭優瀵瑰簲鐨勫厓浠剁被鍨嬪拰璐磋鐐规暟 + part_type, part_points = int(part_cell.loc[index, 'index']), int(part_cell.loc[index, 'points']) + + nozzle_change, maxwl = [0 for _ in range(head_num)], [0 for _ in range(head_num)] + for head in range(head_num): + if head_assignment[head]: + assigned_part = head_assignment[head][-1][0] + if self.part_data.loc[assigned_part]['nz'] != self.part_data.loc[part_type]['nz']: + nozzle_change[head] = 1 + wl1 = wl.copy() + wl1[head] += part_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_] += part_points + head_assignment[head_].append([part_type, part_points]) + + head_assignment_counter = [0 for _ in range(head_num)] + while True: + assigned_part, assigned_cycle = [-1 for _ in range(head_num)], [0 for _ in range(head_num)] + for head in range(head_num): + 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_result.append(min(nonzero_cycle)) + part_result.append(assigned_part) + + for head in range(head_num): + 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 + + slot_result = self.feeder_assigner.do(part_result, cycle_result) + return part_result, cycle_result, slot_result + + def optimize(self, 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 + + # 鑾峰彇鍏冧欢鍏冭優 + part_points = defaultdict(int) + for _, data in self.step_data.iterrows(): + part_points[data.part] += 1 + feeder_num = sum(self.part_data['fdn']) + part_cell = pd.DataFrame({'index': np.arange(feeder_num), 'points': np.zeros(feeder_num, dtype=int)}) + cell_index = 0 + for part_index, data in self.part_data.iterrows(): + total_points, div_points = part_points[data.part], math.ceil(part_points[data.part] / data.fdn) + for _ in range(data.fdn): + part_cell.loc[cell_index, 'index'] = part_index + part_cell.loc[cell_index, 'points'] = min(div_points, total_points) + total_points -= div_points + cell_index += 1 + + part_cell = part_cell[~part_cell['points'].isin([0])] + + # part_cell.sort_values(by = "points" , inplace = True, ascending = False) + best_population, best_part_cell = [], [] + min_pop_val = float('inf') # 鏈浼樼缇や环鍊 + Div, Imp = 0, 0 + while True: + # randomly generate permutations + generation_ = np.array(part_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): + part_result, cycle_result, slot_result = self.convertor(part_cell, pop_generation[pop]) + pop_val.append(self.evaluate(part_result, cycle_result, slot_result)) + + # 鍒濆鍖栭殢鏈虹敓鎴愮缇 + Upit = int(1.5 * np.sqrt(len(part_cell))) + + while Div < Upit: + if hinter: + print('----- current div : ' + str(Div) + ' , total div : ' + str(Upit) + ' -----') + + # 閫夋嫨 + new_pop_generation, new_pop_val = [], [] + top_k_index = GenOpe.get_top_kth(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 = GenOpe.roulette_wheel_selection(pop_val), -1 + while True: + index2 = GenOpe.roulette_wheel_selection(pop_val) + if index1 != index2: + break + # 涓ょ偣浜ゅ弶绠楀瓙 + pop_generation[index1], pop_generation[index2] = GenOpe.partially_mapped_crossover(pop_generation[index1], + pop_generation[index2]) + + if np.random.random() < mutation_rate: + index_ = GenOpe.roulette_wheel_selection(pop_val) + GenOpe.swap_mutation(pop_generation[index_]) + + # 灏嗗厓浠跺厓鑳炲垎閰嶅埌鍚勪釜鍚告潌涓婏紝璁$畻浠峰煎嚱鏁 + for pop in range(population_size): + part_result, cycle_result, slot_result = self.convertor(part_cell, pop_generation[pop]) + pop_val[pop] = self.evaluate(part_result, cycle_result, 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_part_cell = copy.deepcopy(part_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 ------------- ') + div_part_cell = pd.DataFrame() + for idx, rows in part_cell.iterrows(): + if part_cell.loc[idx, 'points'] <= 1: + div_part_cell = pd.concat([div_part_cell, pd.DataFrame([rows])], ignore_index=True) + else: + div_part_cell = pd.concat([div_part_cell, pd.DataFrame([rows] * 2)], ignore_index=True) + + rows_counter = len(div_part_cell) + div_points = int(max(np.ceil(div_part_cell.loc[rows_counter - 2, 'points'] * golden_section), 1)) + + # 閬垮厤鍑虹幇绌哄厓鑳炵殑鎯呭舰 + if div_points == 0 or div_points == div_part_cell.loc[rows_counter - 2, 'points']: + div_part_cell.loc[rows_counter - 2, 'points'] = 1 + else: + div_part_cell.loc[rows_counter - 2, 'points'] = div_points + + div_part_cell.loc[rows_counter - 1, 'points'] -= div_part_cell.loc[rows_counter - 2, 'points'] + + if div_part_cell.loc[rows_counter - 2, 'points'] == 0 or \ + div_part_cell.loc[rows_counter - 1, 'points'] == 0: + raise ValueError + + part_cell = div_part_cell + + # 瀹屾垚鍒嗚鍚庨噸鏂扮敓鎴愭煋鑹蹭綋缁 + generation_ = np.array(range(len(part_cell))) + pop_generation = [] + for _ in range(population_size): + np.random.shuffle(generation_) + pop_generation.append(generation_.tolist()) + else: + break + + assert len(best_part_cell) == len(best_population) + self.result.part, self.result.cycle, self.result.slot = self.convertor(best_part_cell, best_population) + self.result.point, self.result.sequence = self.path_planner.scan_based(self.result.part, + self.result.cycle, self.result.slot) diff --git a/opt/smm/feeder_priority.py b/opt/smm/feeder_priority.py new file mode 100644 index 0000000..38b6940 --- /dev/null +++ b/opt/smm/feeder_priority.py @@ -0,0 +1,791 @@ +from opt.smm.basis import * +from opt.utils import * + + +class FeederPriorityOpt(BaseOpt): + def __init__(self, config, part_data, step_data, feeder_data=pd.DataFrame(columns=['slot', 'part'])): + super().__init__(config, part_data, step_data, feeder_data) + + self.e_gang_pick = 0.6 + self.e_nz_change = 4 + + def optimize(self, hinter=True): + self.feeder_priority_assignment(hinter=hinter) + self.result.point, self.result.sequence = self.path_planner.scan_based(self.result.part, self.result.cycle, + self.result.slot) + + def feeder_priority_assignment(self, hinter=True): + feeder_allocate_val = np.inf + nozzle_pattern_list = self.feeder_nozzle_pattern() + pbar = tqdm(total=len(nozzle_pattern_list), desc='feeder priority process') if hinter else None + + # 绗1姝ワ細纭畾鍚稿槾鍒嗛厤妯″紡 + allocated_feeder_data = copy.deepcopy(self.feeder_data) + for nozzle_pattern in nozzle_pattern_list: + feeder_data = copy.deepcopy(allocated_feeder_data) + # 绗2姝ワ細鍒嗛厤渚涙枡鍣ㄤ綅缃 + self.feeder_allocate(feeder_data, nozzle_pattern, figure=False) + # 绗3姝ワ細鎵弿渚涙枡鍣ㄥ熀搴э紝纭畾鍏冧欢鎷惧彇鐨勫厛鍚庨『搴 + result = OptResult() + result.part, result.cycle, result.slot = self.feeder_base_scan(feeder_data) + info = evaluation(self.config, self.part_data, self.step_data, result) + val = self.cycle_weight * info.cycle_counter + self.nozzle_change_weight * info.nozzle_change_counter + \ + self.pickup_weight * info.pickup_counter + self.move_weight * info.pickup_distance + if val < feeder_allocate_val: + feeder_allocate_val = val + self.result, self.feeder_data = result, feeder_data + if pbar: + pbar.update(1) + + return self.result.part, self.result.cycle, self.result.slot + + def feeder_nozzle_pattern(self): + nozzle_pattern_list = [] + nozzle_points = defaultdict(int) + head_num = self.config.head_num + + part_nozzle = defaultdict(str) + for _, data in self.part_data.iterrows(): + part_nozzle[data.part] = data.nz + + for _, data in self.step_data.iterrows(): + nozzle_points[part_nozzle[data.part]] += 1 + + while len(nozzle_points.keys()) > head_num: + del nozzle_points[min(nozzle_points.items(), key=lambda x: x[1])[0]] + + sum_points = sum(nozzle_points.values()) + nozzle_points = defaultdict(int, {k: v for k, v in nozzle_points.items() if v / sum_points >= 0.8 / head_num}) + + head_assign_indexes = [int(head_num // 2 + pow(-1, h + 1) * (math.ceil(h / 2) - 1 / 2) + + math.ceil((head_num + 1) % 2) / 2) - 1 for h in range(1, head_num + 1)] + while len(nozzle_points): + nozzle_heads, nozzle_indices = defaultdict(int), defaultdict(str), + min_points_nozzle = None + for idx, (nozzle, points) in enumerate(nozzle_points.items()): + nozzle_heads[nozzle], nozzle_indices[idx] = 1, nozzle + if min_points_nozzle is None or points < nozzle_points[min_points_nozzle]: + min_points_nozzle = nozzle + + while sum(nozzle_heads.values()) != head_num: + max_cycle = None + + for nozzle, head_cnt in nozzle_heads.items(): + if max_cycle is None or nozzle_points[nozzle] / head_cnt > nozzle_points[max_cycle] / \ + nozzle_heads[max_cycle]: + max_cycle = nozzle + elif nozzle_points[nozzle] / head_cnt == nozzle_points[max_cycle] / nozzle_heads[max_cycle]: + if head_cnt > nozzle_heads[max_cycle]: + max_cycle = nozzle + + assert max_cycle is not None + nozzle_heads[max_cycle] += 1 + + num_permu = reduce(lambda x, y: x * y, range(1, len(nozzle_indices.keys()) + 1)) + num_permu = num_permu // 2 if len(nozzle_indices.keys()) > 3 else num_permu + for permu in itertools.permutations(nozzle_indices.keys()): + if (num_permu := num_permu - 1) < 0: + break + nozzle_pattern_list.append([]) + for idx in permu: + for _ in range(nozzle_heads[nozzle_indices[idx]]): + nozzle_pattern_list[-1].append(nozzle_indices[idx]) + + if len(nozzle_points.keys()) > 1: + nozzle_average_points = [] + for nozzle, head in nozzle_heads.items(): + nozzle_average_points.append([nozzle, head, nozzle_points[nozzle] / head]) + + nozzle_average_points = sorted(nozzle_average_points, key=lambda x: -x[2]) + idx = 0 + nozzle_pattern_list.append(['' for _ in range(head_num)]) + for nozzle, head, _ in nozzle_average_points: + for _ in range(head): + nozzle_pattern_list[-1][head_assign_indexes[idx]] = nozzle + idx += 1 + + idx = 1 + nozzle_pattern_list.append(['' for _ in range(head_num)]) + for nozzle, head, _ in nozzle_average_points: + for _ in range(head): + nozzle_pattern_list[-1][head_assign_indexes[-idx]] = nozzle + idx += 1 + + nozzle_points.pop(min_points_nozzle) + return nozzle_pattern_list + + def feeder_allocate(self, feeder_data, nozzle_pattern, figure=False): + head_num, slot_num = self.config.head_num, self.config.slot_num + slot_intv, head_intv = self.config.slot_intv, self.config.head_intv + intv_ratio = round(head_intv / slot_intv) + feeder_points, feeder_division_points = defaultdict(int), defaultdict(int) # 渚涙枡鍣ㄨ创瑁呯偣鏁 + feeder_center_pos = defaultdict(float) + + feeder_limit, feeder_arrange = defaultdict(int), defaultdict(int) + part_nozzle = defaultdict(str) + + feeder_base = [-2] * slot_num # 宸插畨瑁呭湪渚涙枡鍣ㄥ熀搴т笂鐨勫厓浠讹紙-2: 鏈垎閰嶏紝-1: 鍗犵敤鐘舵侊級 + feeder_base_points = [0] * slot_num # 渚涙枡鍣ㄥ熀搴х粨浣欒创瑁呯偣鏁伴噺 + part_index = defaultdict(int) + for idx, data in self.part_data.iterrows(): + part_index[data.part] = idx + + feeder_limit[idx] = data.fdn + feeder_arrange[idx] = 0 + + for _, data in self.step_data.iterrows(): + pos, part = data.x + self.config.stopper_pos.x, data.part + + index = part_index[part] + + feeder_points[index] += 1 + feeder_center_pos[index] += ((pos - feeder_center_pos[index]) / feeder_points[index]) + part_nozzle[index] = self.part_data.loc[index].nz + + for index, points in feeder_points.items(): + feeder_division_points[index] = points // feeder_limit[index] + + nozzle_part, nozzle_part_points = defaultdict(list), defaultdict(list) + for part, nozzle in part_nozzle.items(): + for _ in range(feeder_limit[part]): + nozzle_part[nozzle].append(part) + nozzle_part_points[nozzle].append(feeder_points[part]) + + if feeder_data is not None: + for _, feeder in feeder_data.iterrows(): + slot, part = feeder.slot, feeder.part + index = part_index[part] + + # 渚涙枡鍣ㄥ熀搴у垎閰嶄綅缃拰瀵瑰簲璐磋鐐规暟 + feeder_base[slot], feeder_base_points[slot] = index, feeder_division_points[index] + + feeder_type = self.part_data.loc[index].fdr + extra_width = feeder_width[feeder_type][0] + feeder_width[feeder_type][1] - slot_intv + while extra_width > 0: + slot += 1 + feeder_base[slot] = -1 + extra_width -= slot_intv + + feeder_limit[index] -= 1 + feeder_arrange[index] += 1 + if feeder_limit[index] < 0: + info = 'the number of arranged feeder for [' + part + '] exceeds the quantity limit' + raise ValueError(info) + + for nozzle, part in nozzle_part.items(): + if index in part: + index_ = part.index(index) + + nozzle_part[nozzle].pop(index_) + nozzle_part_points[nozzle].pop(index_) + break + + head_assign_indexes = [int(head_num // 2 + pow(-1, h + 1) * (math.ceil(h / 2) - 1 / 2) + + math.ceil((head_num + 1) % 2) / 2) - 1 for h in range(1, head_num + 1)] + assert len(nozzle_pattern) == head_num + while True: + best_assign, best_assign_points = [], [] + best_assign_slot, best_assign_value = -1, -np.inf + best_nozzle_part, best_nozzle_part_points = None, None + for slot in range(1, slot_num // 2 - (head_num - 1) * intv_ratio + 1): + feeder_assign, feeder_assign_points = [], [] + tmp_feeder_limit, tmp_feeder_points = feeder_limit.copy(), feeder_points.copy() + tmp_nozzle_part, tmp_nozzle_part_points = copy.deepcopy(nozzle_part), copy.deepcopy( + nozzle_part_points) + + # 璁板綍鎵弿鍒扮殑宸插畨瑁呯殑渚涙枡鍣ㄥ厓浠剁被鍨 + for head in range(head_num): + feeder_assign.append(feeder_base[slot + head * intv_ratio]) + + if feeder_assign[-1] >= 0: + feeder_assign_points.append(feeder_base_points[slot + head * intv_ratio]) + if feeder_assign_points[-1] <= 0: + feeder_assign[-1], feeder_assign_points[-1] = -1, 0 + else: + feeder_assign_points.append(0) + + if -2 not in feeder_assign: + continue + + assign_part_stack, assign_part_stack_points = [], [] + for idx in head_assign_indexes: + if feeder_assign[idx] != -2: + continue + + # 鍚稿槾鍖归厤妯″紡闈炵┖锛屾寜瀵瑰簲鍚稿槾绫诲瀷杩涜鍏冧欢鍒嗛厤 + nozzle_assign = nozzle_pattern[idx] + + if len(tmp_nozzle_part[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, part_list in tmp_nozzle_part.items(): + if part in part_list: + nozzle_assign = nozzle + + assign_part_stack.append(part) + assign_part_stack_points.append(feeder_division_points[part]) + break + else: + # 褰撳墠澶村搴斿惛鍢寸被鍨嬫湁鍙敤鍏冧欢锛岀洿鎺ュ垎閰嶅搴旂被鍨嬬殑鍏冧欢 + index_ = tmp_nozzle_part[nozzle_assign].index(max(tmp_nozzle_part[nozzle_assign], + key=lambda x: tmp_feeder_points[x] / + tmp_feeder_limit[x] if + tmp_feeder_limit[x] != 0 else 0)) + + part = tmp_nozzle_part[nozzle_assign][index_] + + feeder_type = self.part_data.loc[part].fdr + extra_width, extra_slot = feeder_width[feeder_type][0] + feeder_width[feeder_type][1] - slot_intv, 1 + slot_overlap = False + while extra_width > 0: + slot_ = slot + idx * intv_ratio + extra_slot + if feeder_base[slot_] != -2 or slot_ > slot_num // 2: + slot_overlap = True + break + if idx + extra_slot // 2 < head_num and feeder_assign[idx + extra_slot // 2] >= 0: + slot_overlap = True + break + extra_width -= slot_intv + 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_intv, 1 + while extra_width > 0 and idx + extra_head < head_num: + feeder_assign[idx + extra_head] = -1 + extra_head += 1 + extra_width -= head_intv + else: + part = -1 # 瀛樺湪浣嶇疆鍐茬獊鐨勫厓浠讹紝涓嶅崰鐢ㄥ彲鐢ㄤ緵鏂欏櫒鏁 + + if part >= 0 and tmp_feeder_limit[part] == 0: + continue + + if part in tmp_nozzle_part[nozzle_assign]: + index = tmp_nozzle_part[nozzle_assign].index(part) + + tmp_nozzle_part[nozzle_assign].pop(index) + tmp_nozzle_part_points[nozzle_assign].pop(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 = self.part_data.loc[part].fdr + extra_width, extra_slot = feeder_width[feeder_type][0] + feeder_width[feeder_type][ + 1] - slot_intv, 1 + + slot_overlap = False + while extra_width > 0: + slot_ = slot + head * intv_ratio + extra_slot + if feeder_base[slot_] != -2 or slot_ > slot_num // 2: + slot_overlap = True + break + extra_width -= slot_intv + extra_slot += 1 + + if self.part_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 = self.part_data.loc[part].fdr + extra_width = feeder_width[feeder_type][0] + feeder_width[feeder_type][1] - slot_intv + extra_slot = 1 + + slot_overlap = False + while extra_width > 0: + slot_ = slot + head * intv_ratio + extra_slot + if feeder_base[slot_] != -2 or slot_ > slot_num // 2: + slot_overlap = True + break + extra_width -= slot_intv + extra_slot += 1 + + if not slot_overlap: + feeder_assign[head], feeder_assign_points[head] = part, points + extra_width = feeder_width[feeder_type][0] + feeder_width[feeder_type][1] - head_intv + extra_head = 1 + while extra_width > 0 and head + extra_head < head_num: + feeder_assign[head + extra_head] = -1 + extra_head += 1 + extra_width -= head_intv + else: + # 杩旇繕鐢变簬鏈烘闄愪綅鏃犳硶鍒嗛厤鐨勶紝鍘嬪叆鍏冧欢鍫嗘爤涓殑鍏冪礌 + nozzle = self.part_data.loc[part].nz + tmp_nozzle_part[nozzle].insert(0, part) + tmp_nozzle_part_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 = self.part_data.loc[part].nz + + tmp_nozzle_part[nozzle].insert(0, part) + tmp_nozzle_part_points[nozzle].insert(0, points) + + assign_part_stack.pop(0) + assign_part_stack_points.pop(0) + + nozzle_change_counter = 0 + average_slot, average_head = [], [] + for head, feeder_ in enumerate(feeder_assign): + if feeder_ < 0: + continue + average_slot.append((feeder_center_pos[feeder_] - self.config.slotf1_pos.x) / slot_intv + 1) + average_head.append(head) + if nozzle_pattern and self.part_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) - sum(average_head) / len(average_head) * intv_ratio + 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 += self.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 * self.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_part, best_nozzle_part_points = \ + tmp_nozzle_part.copy(), tmp_nozzle_part_points.copy() + + if not best_assign_points: + break + + for idx, part in enumerate(best_assign): + if part < 0: + continue + # 鏂板畨瑁呯殑渚涙枡鍣 + if feeder_base[best_assign_slot + idx * intv_ratio] != part: + # 闄ゅ幓鍒嗛厤缁欐渶澶у寲鍚屾椂鎷惧彇鍛ㄦ湡鐨勯」锛屼繚鐣欑粨浣欓」 + feeder_base_points[best_assign_slot + idx * intv_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, part_list in nozzle_part.items(): + if part in part_list: + index_ = part_list.index(part) + + nozzle_part[nozzle].pop(index_) + nozzle_part_points[nozzle].pop(index_) + break + feeder_division_points[part] = 0 + else: + # 宸叉湁鐨勪緵鏂欏櫒 + feeder_base_points[best_assign_slot + idx * intv_ratio] -= min( + filter(lambda x: x > 0, best_assign_points)) + + # 鏇存柊渚涙枡鍣ㄥ熀搴т俊鎭 + feeder_base[best_assign_slot + idx * intv_ratio] = part + + feeder_type, extra_slot = self.part_data.loc[part].fdr, 0 + extra_width = feeder_width[feeder_type][0] + feeder_width[feeder_type][1] - slot_intv + while extra_width > 0: + extra_slot += 1 + if feeder_base[best_assign_slot + idx * intv_ratio + extra_slot] == -2: + feeder_base[best_assign_slot + idx * intv_ratio + extra_slot] = -1 # 鏍囪妲戒綅宸插崰鐢 + else: + assert 'feeder allocation conflict' + extra_width -= slot_intv + + # 鏇存柊鍚稿槾淇℃伅 + nozzle_pattern[idx] = self.part_data.loc[part].nz + + # 鏇存柊澶村垎閰嶇殑鍏堝悗椤哄簭 + head_assign_indexes = np.array(best_assign_points).argsort().tolist() + + nozzle_part, nozzle_part_points = copy.deepcopy(best_nozzle_part), copy.deepcopy( + best_nozzle_part_points) + + assert not list(filter(lambda x: x < 0, feeder_limit.values())) # 鍒嗛厤渚涙枡鍣ㄦ暟鐩湪闄愬埗鑼冨洿鍐 + + # 鏇存柊渚涙枡鍣ㄥ崰浣嶄俊鎭 + for _, data in feeder_data.iterrows(): + feeder_base[data.slot] = -1 + + for slot, feeder in enumerate(feeder_base): + if feeder < 0: + continue + part = self.part_data.loc[feeder].part + + feeder_data.loc[len(feeder_data.index)] = [slot, part] + + if figure: + slotf1_pos = self.config.slotf1_pos + # 缁樺埗渚涙枡鍣ㄤ綅缃竷灞 + for slot in range(slot_num // 2): + plt.scatter(slotf1_pos.x + slot_intv * slot, slotf1_pos.y, marker='x', s=12, color='black', alpha=0.5) + plt.text(slotf1_pos.x + slot_intv * slot, slotf1_pos.y - 45, str(slot + 1), ha='center', va='bottom', + size=8) + + feeder_assign_range = [] + for _, feeder in feeder_data.iterrows(): + index = self.part_data[self.part_data.part == feeder.part].index.tolist()[0] + feeder_type = self.part_data.loc[index].fdr + width = feeder_width[feeder_type][0] + feeder_width[feeder_type][1] + start = slotf1_pos.x + slot_intv * (feeder.slot - 1) - slot_intv / 2 + end = slotf1_pos.x + slot_intv * (feeder.slot - 1) - slot_intv / 2 + width + + rec_x = [start, end, end, start] + rec_y = [slotf1_pos.y - 40, slotf1_pos.y - 40, slotf1_pos.y + 10, slotf1_pos.y + 10] + + c = 'red' if feeder.arg == 0 else 'black' # 榛戣壊琛ㄧず宸插垎閰嶏紝绾㈣壊琛ㄧず鏂板垎閰 + plt.text(slotf1_pos.x + slot_intv * (feeder.slot - 1), slotf1_pos.y + 12, + feeder.part + ': ' + str(feeder_points[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.y - 40, slotf1_pos.y - 40, slotf1_pos.y + 10, slotf1_pos.y + 10] + plt.fill(rec_x, rec_y, facecolor='red') + + plt.plot([slotf1_pos.x - slot_intv / 2, slotf1_pos.x + slot_intv * (slot_num // 2 - 1 + 0.5)], + [slotf1_pos.y + 10, slotf1_pos.y + 10], color='black') + plt.plot([slotf1_pos.x - slot_intv / 2, slotf1_pos.x + slot_intv * (slot_num // 2 - 1 + 0.5)], + [slotf1_pos.y - 40, slotf1_pos.y - 40], color='black') + + for counter in range(slot_num // 2 + 1): + pos = slotf1_pos.x + (counter - 0.5) * slot_intv + plt.plot([pos, pos], [slotf1_pos.y + 10, slotf1_pos.y - 40], color='black', linewidth=1) + + plt.ylim(-10, 100) + plt.show() + + def feeder_base_scan(self, feeder_data): + feeder_assign_check = set() + for _, feeder in feeder_data.iterrows(): + feeder_assign_check.add(feeder.part) + + part_index, part_points = defaultdict(int), defaultdict(int) + for idx, data in self.part_data.iterrows(): + part_index[data.part] = idx + for _, data in self.step_data.iterrows(): + part_points[part_index[data.part]] += 1 + + # assert len(feeder_assign_check) == len(part_points.values()) - list(part_points.values()).count(0) # 鎵鏈変緵鏂欏櫒鍧囧凡鍒嗛厤妲戒綅 + + mount_center_slot = defaultdict(float) + for _, data in self.step_data.iterrows(): + idx = part_index[data.part] + mount_center_slot[idx] += (data.x - mount_center_slot[idx]) + + for idx, pos in mount_center_slot.items(): + mount_center_slot[idx] = (pos / part_points[idx] + self.config.stopper_pos.x - + self.config.slotf1_pos.x) / self.config.slot_intv + 1 + + head_num, slot_num = self.config.head_num, self.config.slot_num + intv_ratio = round(self.config.head_intv / self.config.slot_intv) + feeder_part = [-1] * slot_num + for _, data in feeder_data.iterrows(): + part_index = self.part_data[self.part_data.part == data.part].index.tolist() + if len(part_index) != 1: + print('unregistered component: ', data.part, ' in slot', data.slot) + continue + part_index = part_index[0] + feeder_part[data.slot] = part_index + + part_result, cycle_result, slot_result = [], [], [] # 璐磋鐐圭储寮曞拰鎷惧彇妲戒綅浼樺寲缁撴灉 + + sum_nozzle_points, nozzle_pattern = -1, None + for slot in range(slot_num // 2 - (head_num - 1) * intv_ratio): + cur_nozzle_points, cur_nozzle_pattern = 0, ['' for _ in range(head_num)] + for head in range(head_num): + if (part := feeder_part[slot + head * intv_ratio]) == -1: + continue + cur_nozzle_pattern[head] = self.part_data.loc[part].nz + cur_nozzle_points += part_points[part] + if cur_nozzle_points > sum_nozzle_points: + sum_nozzle_points = cur_nozzle_points + nozzle_pattern = cur_nozzle_pattern + + nozzle_mode, nozzle_mode_cycle = [nozzle_pattern], [0] # 鍚稿槾鍖归厤妯″紡 + + value_increment_base = 0 + while True: + # === 鍛ㄦ湡鍐呭惊鐜 === + assigned_part = [-1 for _ in range(head_num)] # 褰撳墠鎵弿鍒扮殑澶村垎閰嶅厓浠朵俊鎭 + assigned_cycle = [0 for _ in range(head_num)] # 褰撳墠鎵弿鍒扮殑鍏冧欢鏈澶у垎閰嶆鏁 + assigned_slot = [-1 for _ in range(head_num)] # 褰撳墠鎵弿鍒扮殑渚涙枡鍣ㄥ垎閰嶄俊鎭 + + 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(head_num)] + cur_scan_cycle = [0 for _ in range(head_num)] + cur_scan_slot = [-1 for _ in range(head_num)] + cur_nozzle_limit = copy.deepcopy(nozzle_limit) + + while True: + best_scan_part = [-1 for _ in range(head_num)] + best_scan_cycle = [0 for _ in range(head_num)] + best_scan_slot = [-1 for _ in range(head_num)] + + best_scan_nozzle_limit = copy.deepcopy(cur_nozzle_limit) + scan_eval_func, search_break = -float('inf'), True + + # 鍓嶄緵鏂欏櫒鍩哄骇鎵弿 + for slot in range(1, slot_num // 2 - (head_num - 1) * intv_ratio + 1): + if sum(feeder_part[slot: slot + head_num * intv_ratio: intv_ratio]) == -head_num: + continue + + 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(head_num): + part = feeder_part[slot + head * intv_ratio] + + # 璐磋澶村拰鎷惧彇妲戒綅婊¤冻瀵瑰簲鍏崇郴 + if scan_part[head] == -1 and part != -1 and part_points[part] > 0 and scan_part.count( + part) < part_points[part]: + preview_scan_part[part] += 1 + + part_counter = 0 + for head in range(head_num): + part = feeder_part[slot + head * intv_ratio] + # 1.鍖归厤鏉′欢婊¤冻: 璐磋澶村拰鎷惧彇妲戒綅婊¤冻瀵瑰簲鍏崇郴 + if scan_part[head] == -1 and part != -1 and part_points[part] > 0 and scan_part.count( + part) < part_points[part]: + # 2.鍖归厤鏉′欢婊¤冻锛氫笉瓒呰繃鍙敤鍚稿槾鏁扮殑闄愬埗 + nozzle = self.part_data.loc[part].nz + if scan_nozzle_limit[nozzle] <= 0: + continue + + # 3.澧為噺鏉′欢婊¤冻: 寮曞叆鏂扮殑鍏冧欢绫诲瀷涓嶄細浣夸唬浠峰嚱鏁扮殑鍊煎噺灏(鍓嶇灮) + if scan_cycle.count(0) == head_num: + gang_pick_change = part_points[part] + else: + prev_cycle = min(filter(lambda x: x > 0, scan_cycle)) + # 鍚屾椂鎷惧彇鏁扮殑鎻愬崌 + gang_pick_change = min(prev_cycle, part_points[part] // preview_scan_part[part]) + + # 4.鎷惧彇绉诲姩璺濈鏉′欢婊¤冻: 閭昏繎鍏冧欢杩涜鍚屾椂鎶撳彇锛岄檷浣庣Щ鍔ㄨ矾寰勯暱搴 + # reference_slot = -1 + # for head_, slot_ in enumerate(scan_slot): + # if slot_ != -1: + # reference_slot = slot_ - head_ * intv_ratio + # if reference_slot != -1 and abs(reference_slot - slot) > (head_num - 1) * intv_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]) + + # 閬垮厤棣栦釜鍛ㄦ湡鍚告潌鍗犵敤鐜囦綆鐨勯棶棰 + 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 = self.e_gang_pick * gang_pick_change - self.e_nz_change * nozzle_change + if val < value_increment_base: + continue + part_counter += 1 + + scan_part[head] = part + scan_cycle[head] = part_points[part] // preview_scan_part[part] + scan_slot[head] = slot + head * intv_ratio + + scan_nozzle_limit[nozzle] -= 1 + + nozzle_counter = 0 # 鍚稿槾鏇存崲娆℃暟 + # 涓婁竴鍛ㄦ湡 + for head, nozzle in enumerate(nozzle_cycle): + if scan_part[head] == -1: + continue + if self.part_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 self.part_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 self.part_data.loc[scan_part[head]].nz != nozzle and nozzle != '': + new_counter += 2 + nozzle_counter += new_counter - prev_counter + + if part_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 = part_points[part] // len(heads) + for head in heads: + scan_cycle[head] = part_cycle + + # 璁$畻鎵弿鍚庣殑浠d环鍑芥暟,璁板綍鎵弿鍚庣殑鏈浼樿В + # 鐭湡鏀剁泭 + 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 * intv_ratio) + + eval_func_short_term = self.e_gang_pick * (head_num - scan_slot.count(-1) - len( + gang_pick_slot_set)) * cycle - self.e_nz_change * nozzle_counter + + # 闀挎湡鏀剁泭 + gang_pick_slot_dict = defaultdict(list) + for head, pick_slot in enumerate(scan_slot): + gang_pick_slot_dict[pick_slot - head * intv_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 += self.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 -= self.e_nz_change * nozzle_counter + + # 鎷惧彇杩囩▼涓殑绉诲姩璺緞 + pick_slot_set = set() + for head, pick_slot in enumerate(scan_slot): + if pick_slot == -1: + continue + pick_slot_set.add(pick_slot - head * intv_ratio) + + slot_offset = 0 + for head, part in enumerate(scan_part): + if part == -1: + continue + slot_offset += abs(scan_slot[head] - mount_center_slot[part]) + + ratio = 0.5 + eval_func = (1 - ratio) * eval_func_short_term + ratio * eval_func_long_term - 1e-5 * ( + max(pick_slot_set) - min(pick_slot_set)) - 1e-5 * slot_offset + 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) and 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 -= head_num + continue + + for head, slot in enumerate(assigned_slot): + if assigned_part[head] == -1: + continue + part_points[feeder_part[slot]] -= min(nonzero_cycle) + + insert_cycle = sum([nozzle_mode_cycle[c] for c in range(nozzle_insert_cycle + 1)]) + + part_result.insert(insert_cycle, assigned_part) + cycle_result.insert(insert_cycle, min(nonzero_cycle)) + slot_result.insert(insert_cycle, assigned_slot) + + # 鏇存柊鍚稿槾鍖归厤妯″紡 + cycle_nozzle = nozzle_mode[nozzle_insert_cycle].copy() + for head, part in enumerate(assigned_part): + if part == -1: + continue + cycle_nozzle[head] = self.part_data.loc[part].nz + + if cycle_nozzle == nozzle_mode[nozzle_insert_cycle]: + nozzle_mode_cycle[nozzle_insert_cycle] += 1 + elif nozzle_insert_cycle + 1 < len(nozzle_mode) and cycle_nozzle == nozzle_mode[nozzle_insert_cycle + 1]: + nozzle_mode_cycle[nozzle_insert_cycle + 1] += 1 + else: + nozzle_mode.insert(nozzle_insert_cycle + 1, cycle_nozzle) + nozzle_mode_cycle.insert(nozzle_insert_cycle + 1, 1) + + if sum(part_points.values()) == 0: + break + + return part_result, cycle_result, slot_result diff --git a/opt/smm/hybrid_genetic.py b/opt/smm/hybrid_genetic.py new file mode 100644 index 0000000..1a37fc4 --- /dev/null +++ b/opt/smm/hybrid_genetic.py @@ -0,0 +1,535 @@ +from opt.smm.basis import * +from opt.utils import * + + +class HybridGeneticOpt(BaseOpt): + def __init__(self, config, part_data, step_data, feeder_data=None): + super().__init__(config, part_data, step_data, feeder_data) + + self.part_nozzle = defaultdict(str) + self.part_point_pos = defaultdict(list) + + self.designated_nozzle = [''] * self.config.head_num + self.designated_slot = [None] * self.config.slot_num + + self.pickup_group = [] # pair_group: pickups from same initial group + self.pickup_group_cycle = [] + self.pair_group = [] + self.feeder_part_arrange = defaultdict(list) + + def pickup_group_combine(self, supply, supply_cycle, demand, demand_cycle): + + combination, combination_cycle = demand.copy(), demand_cycle.copy() + supply_cpy = supply.copy() + + while True: + supply_cpy_bits = self.config.head_num - 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], self.config.head_num - supply_cpy_index[0]): + match_counter = 0 + for idx, part in enumerate(supply_cpy): + if 0 <= idx + offset < self.config.head_num: + if part is None: + continue + if combination[idx + offset] is None and self.designated_nozzle[idx + offset] == self.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 < self.config.head_num: + 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 + + if max_match_counter == 0: + break + + return combination, combination_cycle + + def cal_ind_val(self, 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 = self.pickup_group[gene] + + pair_index = None + for idx, pair in enumerate(self.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(self.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 self.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) + + intv_ratio = self.config.head_intv // self.config.head_intv + for i in range(1, len(V)): + cost, t0 = 0, 0 + load = defaultdict(int) + Pd, Pd_cycle = [None for _ in range(self.config.head_num)], [0 for _ in range(self.config.head_num)] # demand pickup + j = i + while j < len(V): + Ps, Ps_cycle = sequenced_pickup_group[j - 1], [sequenced_pickup_cycle[j - 1] for _ in + range(self.config.head_num)] # supply pickup and its cycle + for part in Ps: + if part: + load[self.part_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 = self.pickup_group_combine(Ps, Ps_cycle, Pd, Pd_cycle) + + # decide the placement cluster and sequencing of pickup 蟻u + pickup_action_counter, place_action_counter = 0, self.config.head_num - Pu.count(None) + right_most_slot, left_most_slot = 0, self.config.slot_num // 2 # most left and right pickup slot + + # === TODO: 鏈烘闄愪綅銆佸悗妲戒綅鍒嗛厤鏈鐞 === + for head in range(self.config.head_num): + if not Pu[head]: + continue + assert Pu[head] in self.feeder_part_arrange.keys() + for slot in self.feeder_part_arrange[Pu[head]]: + left_most_slot = min(slot - head * intv_ratio, left_most_slot) + right_most_slot = max(slot - head * intv_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(Point(self.part_point_pos[part][idx].x - head * self.config.head_intv + + self.config.stopper_pos.x, + self.part_point_pos[part][idx].y + self.config.stopper_pos.y)) + assert len(mount_points) > 0 + + # calculate cycle moving distance + mount_points.sort(key=lambda p: p.x) + slotf1_pos = self.config.slotf1_pos + t_FW += max( + abs(slotf1_pos.x + (left_most_slot - 1) * self.config.slot_intv - mount_points[0].x) / x_moving_speed, + abs(slotf1_pos.y - mount_points[0].y) / y_moving_speed) + t_BW += max( + abs(slotf1_pos.x + (right_most_slot - 1) * self.config.slot_intv - mount_points[-1].x) / x_moving_speed, + abs(slotf1_pos.y - mount_points[-1].y) / y_moving_speed) + # pick up moving time + t_PU += (right_most_slot - left_most_slot) * self.config.slot_intv / x_moving_speed + # place moving time + for idx_points in range(len(mount_points) - 1): + t_PL += max(abs(mount_points[idx_points].x - mount_points[idx_points + 1].x) / x_moving_speed, + abs(mount_points[idx_points].y - mount_points[idx_points + 1].y) / 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 convertor(self, individual): + + part_result, cycle_result, feeder_slot_result = [], [], [] + # initial result + _, pickup_result, pickup_cycle_result = self.cal_ind_val(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) + part_result.append([-1 for _ in range(self.config.head_num)]) + feeder_slot_result.append([-1 for _ in range(self.config.head_num)]) + cycle_result.append(cycle) + for head, part in enumerate(pickup): + if part is None or pickup_cycle_result[idx][head] == 0: + continue + + part_result[-1][head] = self.part_data[self.part_data['part'] == part].index.tolist()[0] + feeder_slot_result[-1][head] = self.feeder_part_arrange[part][feeder_part_arrange_index[part]] + feeder_part_arrange_index[part] += 1 + if feeder_part_arrange_index[part] >= len(self.feeder_part_arrange[part]): + feeder_part_arrange_index[part] = 0 + + pickup_cycle_result[idx][head] -= cycle + + return part_result, cycle_result, feeder_slot_result + + def optimal_nozzle_assignment(self): + if len(self.step_data) == 0: + return defaultdict(int) + + # === Nozzle Assignment === + # number of points for nozzle & number of heads for nozzle + nozzle_points, nozzle_assigned_counter = defaultdict(int), defaultdict(int) + + for _, data in self.step_data.iterrows(): + idx = self.part_data[self.part_data['part'] == data['part']].index.tolist()[0] + nozzle = self.part_data.loc[idx]['nz'] + + nozzle_assigned_counter[nozzle] = 0 + nozzle_points[nozzle] += 1 + + assert len(nozzle_points.keys()) <= self.config.head_num + total_points, available_head = len(self.step_data), self.config.head_num + # 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] * self.config.head_num < 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 + + @timer_wrapper + def optimize(self): + nozzle_assigned_counter = self.optimal_nozzle_assignment() + + # nozzle assignment result: + self.designated_nozzle = [''] * self.config.head_num + head_index = 0 + for nozzle, num in nozzle_assigned_counter.items(): + while num > 0: + self.designated_nozzle[head_index] = nozzle + head_index += 1 + num -= 1 + + # === component assignment === + part_points, nozzle_components = defaultdict(int), defaultdict(list) # 鍏冧欢璐磋鐐规暟锛屽惛鍢-鍏冧欢瀵瑰簲鍏崇郴 + component_feeder_limit, component_divided_points = defaultdict(int), defaultdict(list) + for _, data in self.step_data.iterrows(): + part = data['part'] + idx = self.part_data[self.part_data['part'] == part].index.tolist()[0] + nozzle = self.part_data.loc[idx]['nz'] + + component_feeder_limit[part] = self.part_data.loc[idx].fdn + part_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(part_points[part] // feeder_limit) + + for part, divided_points in component_divided_points.items(): + index = 0 + while sum(divided_points) < part_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(self.config.head_num)]) + CT_Points.append([0 for _ in range(self.config.head_num)]) + + for head_index in range(self.config.head_num): + nozzle = self.designated_nozzle[head_index] # 鍒嗛厤鐨勫惛鍢 + if len(nozzle_components[nozzle]) == 0: # 鏃犲彲鐢ㄥ厓浠 + continue + + max_points, designated_part = 0, None + for part in nozzle_components[nozzle]: + if part_points[part] > max_points: + max_points = part_points[part] + designated_part = part + + part_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 === + for _, data in self.step_data.iterrows(): + self.part_point_pos[data.part].append(Point(data.x + self.config.stopper_pos.x, + data.y + self.config.stopper_pos.y)) + + for pos_list in self.part_point_pos.values(): + pos_list.sort(key=lambda p: (p.x, p.y)) + + CT_Group_slot = [-1] * len(CT_Group) + feeder_lane = [None] * self.config.slot_num # 渚涙枡鍣ㄥ熀搴т笂宸插垎閰嶇殑鍏冧欢绫诲瀷 + 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) + + intv_ratio = self.config.head_intv // self.config.slot_intv + for CTIdx, pickup in enumerate(CT_Group): + best_slot = [] + for cp_index, part in enumerate(pickup): + if part is None: + continue + best_slot.append(round((sum(p.x for p in self.part_point_pos[part]) / len( + self.part_point_pos[part]) - self.config.slotf1_pos.x) / self.config.slot_intv) + 1 - cp_index * intv_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) * intv_ratio >= self.config.slot_num / 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 + intv_ratio * len(pickup), intv_ratio): + pickup_index = int((slot - assign_slot) / intv_ratio) + pick_part = pickup[pickup_index] + + # 妫鏌ユЫ浣嶅崰鐢ㄦ儏鍐 + if feeder_lane[slot] and pick_part: + assign_available = False + break + + # 妫鏌ユ満姊伴檺浣嶅啿绐 + if pick_part and (slot - CT_Head[pick_part][0] * intv_ratio <= 0 or slot + ( + self.config.head_num - CT_Head[pick_part][1] - 1) * intv_ratio > self.config.slot_num // 2): + assign_available = False + break + + if assign_available: + for idx, part in enumerate(pickup): + if part: + feeder_lane[assign_slot + idx * intv_ratio] = part + 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) == self.config.head_num: + 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(self.config.head_num): + 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 + for idx, Pickup in enumerate(initial_pickup): + pickup_num = len([element for element in Pickup if element is not None]) + if 2 <= pickup_num <= self.config.head_num / 3 or ( + self.config.head_num / 3 <= pickup_num <= self.config.head_num / 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(self.pickup_group)) + self.pickup_group.append([None for _ in range(self.config.head_num)]) + self.pickup_group[-1][index] = CT + self.pickup_group_cycle.append(initial_pickup_cycle[idx]) + self.pair_group.append(pair_index) + else: + self.pickup_group.append(Pickup) + self.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(self.pickup_group))) + np.random.shuffle(pop_permutation) + population.append(pop_permutation) + + best_individual, best_pop_val = [], [] + + # === 璁板綍涓嶅悓鍏冧欢瀵瑰簲鐨勬Ы浣 === + self.feeder_part_arrange = defaultdict(list) + for slot in range(1, self.config.slot_num // 2 + 1): + if feeder_lane[slot]: + self.feeder_part_arrange[feeder_lane[slot]].append(slot) + + # === 璁板綍涓嶅悓鍏冧欢鐨勬敞鍐屽惛鍢寸被鍨 === + self.part_nozzle = defaultdict(str) + for pickup in self.pickup_group: + for part in pickup: + if part is None or part in self.part_nozzle.keys(): + continue + self.part_nozzle[part] = self.part_data[self.part_data['part'] == part]['nz'].tolist()[0] + + with tqdm(total=n_generations) as pbar: + pbar.set_description('hybrid genetic process') + # calculate fitness value + pop_val = [self.cal_ind_val(individual)[0] for individual in population] # val is related to assembly time + + for _ in range(n_generations): + # min-max convert + max_val = 1.5 * max(pop_val) + convert_pop_val = list(map(lambda v: max_val - v, pop_val)) + + # crossover and mutation + c = 0 + new_population, new_pop_val = [], [] + for pop in range(population_size): + if pop % 2 == 0 and np.random.random() < crossover_rate: + index1, index2 = GenOpe.roulette_wheel_selection(convert_pop_val), -1 + while True: + index2 = GenOpe.roulette_wheel_selection(convert_pop_val) + if index1 != index2: + break + # 涓ょ偣浜ゅ弶绠楀瓙 + offspring1 = GenOpe.directed_edge_recombine_crossover(population[index1], population[index2]) + offspring2 = GenOpe.directed_edge_recombine_crossover(population[index2], population[index1]) + + if np.random.random() < mutation_rate: + GenOpe.swap_mutation(offspring1) + + if np.random.random() < mutation_rate: + GenOpe.swap_mutation(offspring2) + + new_population.append(offspring1) + new_population.append(offspring2) + + new_pop_val.append(self.cal_ind_val(offspring1)[0]) + new_pop_val.append(self.cal_ind_val(offspring2)[0]) + + # generate next generation + top_k_index = GenOpe.get_top_kth(pop_val, population_size - len(new_population), reverse=False) + for index in top_k_index: + new_population.append(population[index]) + new_pop_val.append(pop_val[index]) + + population = new_population + pop_val = new_pop_val + pbar.update(1) + + best_individual = population[np.argmin(pop_val)] + self.result.part, self.result.cycle, self.result.slot = self.convertor(best_individual) + self.result.point, self.result.sequence = self.path_planner.greedy_cluster(self.result.part, self.result.cycle, + self.result.slot) + + + diff --git a/opt/smm/path_plan.py b/opt/smm/path_plan.py new file mode 100644 index 0000000..1ea8df4 --- /dev/null +++ b/opt/smm/path_plan.py @@ -0,0 +1,290 @@ +import copy + +import numpy as np + +from opt.utils import axis_moving_time +from core.common import * +from data.type import Point + + +class PathPlanOpt: + def __init__(self, config, part_data, step_data): + self.part_data = part_data + self.step_data = step_data + self.config = config + + def dynamic_programming_cycle_path(self, cycle_point, cycle_slot): + head_sequence = [] + num_pos = sum([placement != -1 for placement in cycle_point]) + 1 + intv_ratio = self.config.head_intv // self.config.slot_intv + + pos, head_set = [], [] + feeder_set = set() + for head, slot in enumerate(cycle_slot): + if slot == -1: + continue + + head_set.append(head) + placement = cycle_point[head] + pos.append([self.step_data.loc[placement]['x'] - head * self.config.head_intv + self.config.stopper_pos.x, + self.step_data.loc[placement]['y'] + self.config.stopper_pos.y, + self.step_data.loc[placement]['r'], head]) + + feeder_set.add(slot - head * intv_ratio) + + pos.insert(0, [self.config.slotf1_pos.x + ((min(list(feeder_set)) + max(list(feeder_set))) / 2 - 1) * + self.config.slot_intv, self.config.slotf1_pos.y, None, 0]) + + def get_distance(pos_1, pos_2): + # 拾取起始与终止位置 或 非同轴 + if pos_1[2] is None or pos_2[2] is None or pos_1[3] + (1 if pos_1[3] % 2 == 0 else -1) != pos_2[3]: + return max(axis_moving_time(pos_1[0] - pos_2[0], 0), axis_moving_time(pos_1[1] - pos_2[1], 1)) + else: + return max(axis_moving_time(pos_1[0] - pos_2[0], 0), axis_moving_time(pos_1[1] - pos_2[1], 1), + axis_moving_time(pos_1[2] - pos_2[2], 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 self.step_data.loc[cycle_point[start_head]]['x'] - start_head * self.config.head_intv > \ + self.step_data.loc[cycle_point[end_head]]['x'] - end_head * self.config.head_intv: + head_sequence = list(reversed(head_sequence)) + return ans_dist, head_sequence + + def scan_based(self, part_result, cycle_result, slot_result): + point_result, sequence_result = [], [] + + class Mount: + def __init__(self): + self.pos = [] + self.angle = [] + + self.part = [] + self.step = [] + + def pop(self, index): + self.pos.pop(index) + self.angle.pop(index) + + self.part.pop(index) + self.step.pop(index) + + all_points = Mount() + + for step_index, data in self.step_data.iterrows(): + part_index = self.part_data[self.part_data.part == data.part].index.tolist()[0] + + # 记录贴装点序号索引和对应的位置坐标 + all_points.pos.append(Point(data.x + self.config.stopper_pos.x, data.y + self.config.stopper_pos.y)) + all_points.angle.append(data.r) + all_points.part.append(part_index) + all_points.step.append(step_index) + + head_num = self.config.head_num + left_boundary, right_boundary = min(all_points.pos, key=lambda p: p.x).x, \ + max(all_points.pos, key=lambda p: p.x).x + search_step = max((right_boundary - left_boundary) / head_num / 2, 0) + + ref_pos_y = min(all_points.pos, key=lambda p: p.y).y + for cycle_index, component_cycle in enumerate(part_result): + for _ in range(cycle_result[cycle_index]): + min_dist = np.inf + tmp_assigned_point, tmp_assigned_head_seq = [], [] + + tmp_all_points = Mount() + for search_dir in range(3): # 不同的搜索方向,贴装头和起始点的选取方法各不相同 + if search_dir == 0: + # 从左向右搜索 + search_points = np.arange(left_boundary, (left_boundary + right_boundary) / 2, search_step) + head_range = list(range(head_num)) + elif search_dir == 1: + # 从右向左搜索 + search_points = np.arange(right_boundary + 1e-3, (left_boundary + right_boundary) / 2, -search_step) + head_range = list(range(head_num - 1, -1, -1)) + else: + # 从中间向两边搜索 + search_points = np.arange(left_boundary, right_boundary, search_step / 2) + head_range, head_index = [], (head_num - 1) // 2 + while head_index >= 0: + if 2 * head_index != head_num - 1: + head_range.append(head_num - 1 - head_index) + head_range.append(head_index) + head_index -= 1 + + for start_points in search_points: + cur_all_points = copy.deepcopy(all_points) + + assigned_point = [-1] * head_num + assigned_mount_point, assigned_mount_angle = [Point(0, 0)] * head_num, [0] * head_num + head_counter, point_index = 0, -1 + for head_index in head_range: + if head_counter == 0: + part_index = part_result[cycle_index][head_index] + + if part_index == -1: + continue + + min_horizontal_distance = np.inf + for index, part in enumerate(cur_all_points.part): + if part != part_result[cycle_index][head_index]: + continue + + horizontal_distance = abs(cur_all_points.pos[index].x - start_points) + 0 * abs( + cur_all_points.pos[index].y - ref_pos_y) + + if horizontal_distance < min_horizontal_distance: + min_horizontal_distance = horizontal_distance + point_index = index + else: + point_index = -1 + min_cheby_distance = np.inf + + for index, part in enumerate(cur_all_points.part): + if part != part_result[cycle_index][head_index]: + continue + point_pos = [Point(cur_all_points.pos[index].x - head_index * self.config.head_intv, + cur_all_points.pos[index].y)] + + cheby_distance, euler_distance = 0, 0 + for next_head in range(head_num): + if assigned_point[next_head] == -1: + continue + point_pos.append(Point(assigned_mount_point[next_head].x - next_head * head_num, + assigned_mount_point[next_head].y)) + + point_pos = sorted(point_pos, key=lambda p: p.x) + for mount_seq in range(len(point_pos) - 1): + delta_x = axis_moving_time( + point_pos[mount_seq].x - point_pos[mount_seq + 1].x, 0) + delta_y = axis_moving_time( + point_pos[mount_seq].y - point_pos[mount_seq + 1].y, 1) + cheby_distance += max(delta_x, delta_y) + euler_distance += math.sqrt(delta_x ** 2 + delta_y ** 2) + + # cheby_distance += 0.01 * euler_distance + if cheby_distance < min_cheby_distance: + min_cheby_distance, min_euler_distance = cheby_distance, euler_distance + point_index = index + + if point_index == -1: + continue + + head_counter += 1 + + assigned_point[head_index] = all_points.step[point_index] + assigned_mount_point[head_index] = all_points.pos[point_index] + assigned_mount_angle[head_index] = all_points.angle[point_index] + + cur_all_points.pop(point_index) + + dist, head_seq = self.dynamic_programming_cycle_path(assigned_point, slot_result[cycle_index]) + + if min_dist is None or dist < min_dist: + tmp_all_points = cur_all_points + tmp_assigned_point, tmp_assigned_head_seq = assigned_point, head_seq + min_dist = dist + + all_points = tmp_all_points + point_result.append(tmp_assigned_point) + sequence_result.append(tmp_assigned_head_seq) + + return point_result, sequence_result + + def greedy_cluster(self, part_result, cycle_result, slot_result): + point_result, sequence_result = [], [] + + # === assign CT group to feeder slot === + component_point_pos = defaultdict(list) + + for idx, data in self.step_data.iterrows(): + component_point_pos[data.part].append([data.x + self.config.stopper_pos.x, + data.y + self.config.stopper_pos.y, idx]) + + for pos_list in component_point_pos.values(): + pos_list.sort(key=lambda x: (x[0], x[1])) + + component_point_index = defaultdict(int) + for cycle_set in range(len(cycle_result)): + for cycle in range(cycle_result[cycle_set]): + point_result.append([-1 for _ in range(self.config.head_num)]) + for head in range(self.config.head_num): + part_index = part_result[cycle_set][head] + if part_index == -1: + continue + + part = self.part_data.loc[part_index]['part'] + point_info = component_point_pos[part][component_point_index[part]] + + point_result[-1][head] = point_info[2] + # mount_point[head] = point_info[0:2] + + component_point_index[part] += 1 + sequence_result.append( + self.dynamic_programming_cycle_path(point_result[-1], slot_result[cycle_set])[1]) + return point_result, sequence_result + + def greedy_level_placing(self, part_result, cycle_result, slot_result): + point_result, sequence_result = [], [] + part_indices = defaultdict(int) + for part_idx, data in self.part_data.iterrows(): + part_indices[data.part] = part_idx + + mount_point_pos = defaultdict(list) + for pcb_idx, data in self.step_data.iterrows(): + mount_point_pos[part_indices[data.part]].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]): + point_result.append([-1 for _ in range(self.config.head_num)]) + for head in range(self.config.head_num): + if part_result[cycle_idx][head] == -1: + continue + index_ = part_result[cycle_idx][head] + point_result[-1][head] = mount_point_pos[index_][-1][2] + mount_point_pos[index_].pop() + sequence_result.append(self.dynamic_programming_cycle_path(point_result[-1], slot_result[cycle_idx])[1]) + return point_result, sequence_result + diff --git a/opt/smm/two_phase.py b/opt/smm/two_phase.py new file mode 100644 index 0000000..be70194 --- /dev/null +++ b/opt/smm/two_phase.py @@ -0,0 +1,484 @@ +import copy + +from opt.smm.basis import * +from opt.utils import * +from opt.smm.solver import * + + +class TwoPhaseOpt(BaseOpt): + def __init__(self, config, part_data, step_data, feeder_data=pd.DataFrame(columns=['slot', 'part'])): + super().__init__(config, part_data, step_data, feeder_data) + self.feeder_assigner = FeederAssignOpt(config, part_data, step_data) + self.reduction = True + self.partition = True + self.initial = False + + def optimize(self, hinter=True): + # data preparation: convert data to index + part_list, nozzle_list = defaultdict(int), defaultdict(int) + part_feeder = defaultdict(int) + cpidx_2_part, nzidx_2_nozzle, cpidx_2_nzidx = {}, {}, {} + arg_slot_rng = None if len(self.feeder_data) == 0 else [self.feeder_data.iloc[0].slot, self.feeder_data.iloc[-1].slot] + for idx, data in self.part_data.iterrows(): + part, nozzle = data.part, data.nz + + cpidx_2_part[idx] = part + nz_key = [key for key, val in nzidx_2_nozzle.items() if val == nozzle] + + nz_idx = len(nzidx_2_nozzle) if len(nz_key) == 0 else nz_key[0] + nzidx_2_nozzle[nz_idx] = nozzle + + part_list[part] = 0 + part_feeder[part] = data.fdn + cpidx_2_nzidx[idx] = nz_idx + + for _, data in self.step_data.iterrows(): + idx = self.part_data[self.part_data.part == data.part].index.tolist()[0] + nozzle = self.part_data.loc[idx].nz + + nozzle_list[nozzle] += 1 + part_list[data.part] += 1 + + part_feederbase = defaultdict(int) + if self.feeder_data is not None: + for _, data in self.feeder_data.iterrows(): + idx = -1 + for idx, part_ in cpidx_2_part.items(): + if data.part == part_: + break + assert idx != -1 + part_feederbase[idx] = data.slot # part index - slot + + ratio = 1 if self.reduction else 2 + I, J = len(cpidx_2_part.keys()), len(nzidx_2_nozzle.keys()) + # === determine the hyper-parameter of L === + # first phase: calculate the number of heads for each type of nozzle + nozzle_heads = defaultdict(int) + for nozzle in nozzle_list.keys(): + nozzle_heads[nozzle] = 1 + + head_num = self.config.head_num + while sum(nozzle_heads.values()) != head_num: + max_cycle_nozzle = None + + for nozzle, head_num in nozzle_heads.items(): + if max_cycle_nozzle is None or nozzle_list[nozzle] / head_num > nozzle_list[max_cycle_nozzle] / \ + nozzle_heads[max_cycle_nozzle]: + max_cycle_nozzle = nozzle + + assert max_cycle_nozzle is not None + nozzle_heads[max_cycle_nozzle] += 1 + + nozzle_comp_points = defaultdict(list) + for part, points in part_list.items(): + idx = self.part_data[self.part_data.part == part].index.tolist()[0] + nozzle = self.part_data.loc[idx].nz + nozzle_comp_points[nozzle].append([part, points]) + + level = 1 if len(part_list) == 1 or len(part_list) % head_num == 0 else 2 + part_assignment, cycle_assignment = [], [] + + def aux_func(info): + return max(map(lambda points: max([p[1] for p in points]), info)) + + pre_objbst, pre_changetime = None, None + + def terminate_condition(mdl, where): + if where == GRB.Callback.MIP: + objbst, objbnd = mdl.cbGet(GRB.Callback.MIP_OBJBST), mdl.cbGet(GRB.Callback.MIP_OBJBND) + changetime = mdl.cbGet(GRB.Callback.RUNTIME) + nonlocal pre_objbst, pre_changetime + # condition: value change + if abs(objbst - 1e+100) > 1: # 閬垮厤鏈壘鍒板彲琛岃В鎻愬墠閫鍑 + if pre_objbst and abs(pre_objbst - objbst) < 1e-3: + if pre_changetime and changetime - pre_changetime > 90 * (1 - objbnd / objbst): + mdl.terminate() + else: + pre_changetime = changetime + + pre_objbst = objbst + + def recursive_assign(assign_points, nozzle_compo_points, cur_level, total_level) -> int: + def func(points): + return map(lambda points: max([p[1] for p in points]), points) + + if cur_level > total_level and sum(func(nozzle_compo_points.values())) == 0: + return 0 + elif assign_points <= 0 and cur_level == 1: + return -1 # backtrack + elif assign_points <= 0 or cur_level > total_level: + return 1 # fail + + nozzle_compo_points_cpy = copy.deepcopy(nozzle_compo_points) + prev_assign = 0 + for part in part_assignment[cur_level - 1]: + if part != -1: + prev_assign += 1 + + head_idx = 0 + for nozzle, head in nozzle_heads.items(): + while head: + min_idx = -1 + for idx, (part, points) in enumerate(nozzle_compo_points_cpy[nozzle]): + if points >= assign_points and ( + min_idx == -1 or points < nozzle_compo_points_cpy[nozzle][min_idx][1]): + min_idx = idx + part_assignment[cur_level - 1][head_idx] = -1 if min_idx == -1 else \ + nozzle_compo_points_cpy[nozzle][min_idx][0] + if min_idx != -1: + nozzle_compo_points_cpy[nozzle][min_idx][1] -= assign_points + head -= 1 + head_idx += 1 + + cycle_assignment[cur_level - 1] = assign_points + for part in part_assignment[cur_level - 1]: + if part != -1: + prev_assign -= 1 + + if prev_assign == 0: + res = 1 + else: + points = min(len(self.step_data) // head_num + 1, aux_func(nozzle_compo_points_cpy.values())) + res = recursive_assign(points, nozzle_compo_points_cpy, cur_level + 1, total_level) + if res == 0: + return 0 + elif res == 1: + # All cycles have been completed, but there are still points left to be allocated + return recursive_assign(assign_points - 1, nozzle_compo_points, cur_level, total_level) + + # second phase: (greedy) recursive search to assign points for each cycle set and obtain an initial solution + while True: + part_assignment = [[-1 for _ in range(head_num)] for _ in range(level)] + cycle_assignment = [-1 for _ in range(level)] + points = min(len(self.step_data) // head_num + 1, max(part_list.values())) + if recursive_assign(points, nozzle_comp_points, 1, level) == 0: + break + level += 1 + + L = len(cycle_assignment) if self.partition else len(self.step_data) + S = ratio * sum(part_feeder.values()) if len(self.feeder_data) == 0 else arg_slot_rng[-1] - arg_slot_rng[ + 0] + 1 # the available feeder num + M = len(self.step_data) # a sufficiently large number (number of placement points) + HC = [[0 for _ in range(J)] for _ in range(I)] + for i in range(I): + for j in range(J): + HC[i][j] = 1 if cpidx_2_nzidx[i] == j else 0 + + mdl = Model('SMT') + mdl.setParam('Seed', 0) + mdl.setParam('OutputFlag', hinter) # set whether output the debug information + mdl.setParam('TimeLimit', 3600) + mdl.setParam('PoolSearchMode', 2) + mdl.setParam('PoolSolutions', 100) + mdl.setParam('PoolGap', 1e-4) + mdl.setParam("Heuristics", 0.5) + + # Use only if other methods, including exploring the tree with the default settings, do not yield a viable solution + # mdl.setParam("ZeroObjNodes", 100) + + # === Decision Variables === + H = head_num + x = mdl.addVars(I, S, H, L, vtype=GRB.BINARY, name='x') + y = mdl.addVars(I, H, L, vtype=GRB.BINARY, name='y') + v = mdl.addVars(S, H, L, vtype=GRB.BINARY, name='v') + c = mdl.addVars(I, H, L, vtype=GRB.INTEGER, name='c') + + mdl.addConstrs(c[i, h, l] <= part_list[cpidx_2_part[i]] for i in range(I) for h in range(H) for l in range(L)) + + f = {} + for i in range(I): + if i not in part_feederbase.keys(): + for s in range(S): + f[s, i] = mdl.addVar(vtype=GRB.BINARY, name='f_' + str(s) + '_' + str(i)) + else: + for s in range(S): + f[s, i] = 1 if part_feederbase[i] == s + arg_slot_rng[0] else 0 + + p = mdl.addVars(S + (H - 1) * ratio, L, vtype=GRB.BINARY, name='p') + z = mdl.addVars(J, H, L, vtype=GRB.BINARY) + + d = mdl.addVars(L, H, vtype=GRB.INTEGER, name='d') + d_plus = mdl.addVars(J, H, L, vtype=GRB.INTEGER, name='d_plus') + d_minus = mdl.addVars(J, H, L, vtype=GRB.INTEGER, name='d_minus') + + max_cycle = math.ceil(len(self.step_data) / H) + PU = mdl.addVars(S + (H - 1) * ratio, L, vtype=GRB.INTEGER, name='PU') + WL = mdl.addVars(L, vtype=GRB.INTEGER, ub=max_cycle, name='WL') + NC = mdl.addVars(H, vtype=GRB.INTEGER, name='NC') + + part_2_cpidx = defaultdict(int) + for idx, part in cpidx_2_part.items(): + part_2_cpidx[part] = idx + + if self.initial: + # initial some variables to speed up the search process + # ensure the priority of the workload assignment + cycle_index = sorted(range(len(cycle_assignment)), key=lambda k: cycle_assignment[k], reverse=True) + part_list = [] + + for cycle in cycle_index: + cycle_part = part_assignment[cycle] + for part in cycle_part: + if part != -1 and part not in part_list: + part_list.append(part) + slot = 0 + for part in part_list: + if self.feeder_data is not None: + while slot in self.feeder_data.keys(): + slot += 1 # skip assigned feeder slot + + if part_2_cpidx[part] in part_feederbase.keys(): + continue + + part_feederbase[part_2_cpidx[part]] = slot + f[slot, part_2_cpidx[part]].Start = 1 + slot += 1 + + for idx, cycle in enumerate(cycle_index): + WL[idx].Start = cycle_assignment[cycle] + for h in range(H): + part = part_assignment[cycle][h] + if part == -1: + continue + i = part_2_cpidx[part] + y[i, h, idx].Start = 1 + v[part_feederbase[i], h, idx].Start = 1 + + # === Objective === + mdl.setObjective(self.cycle_weight * quicksum(WL[l] for l in range(L)) + + 2 * self.nozzle_change_weight * quicksum(NC[h] for h in range(H)) + + self.pickup_weight * quicksum(PU[s, l] for s in range(-(H - 1) * ratio, S) for l in range(L))) + + # === Constraint === + if not self.partition: + mdl.addConstrs(WL[l] <= 1 for l in range(L)) + + # work completion + mdl.addConstrs(c[i, h, l] == WL[l] * y[i, h, l] for i in range(I) for h in range(H) for l in range(L)) + mdl.addConstrs(c[i, h, l] <= max_cycle * y[i, h, l] for i in range(I) for h in range(H) for l in range(L)) + mdl.addConstrs(c[i, h, l] <= WL[l] for i in range(I) for h in range(H) for l in range(L)) + mdl.addConstrs(c[i, h, l] >= WL[l] - max_cycle * (1 - y[i, h, l]) for i in range(I) for h in range(H) + for l in range(L)) + + mdl.addConstrs(quicksum(c[i, h, l] for h in range(H) for l in range(L)) == part_list[cpidx_2_part[i]] + for i in range(I)) + + # variable constraint + mdl.addConstrs(quicksum(y[i, h, l] for i in range(I)) <= 1 for h in range(H) for l in range(L)) + + # simultaneous pick + for s in range(S + (H - 1) * ratio): + rng = list(range(max(0, -math.floor(s / ratio)), min(H, math.ceil((S - s) / ratio)))) + for l in range(L): + mdl.addConstr(quicksum(v[s + h * ratio, h, l] for h in rng) <= H * p[s, l]) + mdl.addConstr(quicksum(v[s + h * ratio, h, l] for h in rng) >= p[s, l]) + + mdl.addConstrs(PU[s, l] == p[s, l] * WL[l] for s in range(S + (H - 1) * ratio) for l in range(L)) + # mdl.addConstrs(PU[s, l] <= max_cycle * p[s, l] for s in range(S + (H - 1) * ratio) for l in range(L)) + # mdl.addConstrs(PU[s, l] <= WL[l] for s in range(-(H - 1) * ratio, S) for l in range(L)) + # mdl.addConstrs(PU[s, l] >= WL[l] - max_cycle * (1 - p[s, l]) for s in range(-(H - 1) * ratio, S) for l in + # range(L)) + + # nozzle change + mdl.addConstrs( + z[j, h, l] - z[j, h, l + 1] == d_plus[j, h, l] - d_minus[j, h, l] for l in range(L - 1) for j in range(J) + for h in range(H)) + + mdl.addConstrs(z[j, h, 0] - z[j, h, L - 1] == d_plus[j, h, L - 1] - d_minus[j, h, L - 1] for j in range(J) + for h in range(H)) + + mdl.addConstrs( + 2 * d[l, h] == quicksum(d_plus[j, h, l] for j in range(J)) + quicksum(d_minus[j, h, l] for j in range(J)) + for l in range(L) for h in range(H)) + + mdl.addConstrs(NC[h] == quicksum(d[l, h] for l in range(L)) for h in range(H)) + mdl.addConstrs(quicksum(y[i, h, l] for i in range(I) for h in range(H)) * M >= WL[l] for l in range(L)) + + # nozzle-component compatibility + mdl.addConstrs(y[i, h, l] <= quicksum(HC[i][j] * z[j, h, l] for j in range(J)) for i in range(I) for h in + range(H) for l in range(L)) + + # available number of feeder + mdl.addConstrs(quicksum(f[s, i] for s in range(S)) <= part_feeder[cpidx_2_part[i]] for i in range(I)) + + # available number of nozzle + mdl.addConstrs(quicksum(z[j, h, l] for h in range(H)) <= H for j in range(J) for l in range(L)) + + # upper limit for occupation for feeder slot + mdl.addConstrs(quicksum(f[s, i] for i in range(I)) <= 1 for s in range(S)) + mdl.addConstrs(quicksum(v[s, h, l] for s in range(S)) >= quicksum(y[i, h, l] for i in range(I)) for h in + range(H) for l in range(L)) + + # others + mdl.addConstrs(quicksum(z[j, h, l] for j in range(J)) <= 1 for h in range(H) for l in range(L)) + mdl.addConstrs(quicksum(x[i, s, h, l] for h in range(H) for l in range(L)) >= f[s, i] for i in range(I) + for s in range(S)) + mdl.addConstrs(quicksum(x[i, s, h, l] for h in range(H) for l in range(L)) <= M * f[s, i] for i in + range(I) for s in range(S)) + + # mdl.addConstrs(f[s, i] >= x[i, s, h, l] for s in range(S) for i in range(I) for h in range(H) for l in range(L)) + # + # mdl.addConstrs(quicksum(x[i, s, h, l] for h in range(H) for l in range(L)) >= f[s, i] for s in + # range(S) for i in range(I)) + + # the constraints to speed up the search process + mdl.addConstrs(quicksum(x[i, s, h, l] for i in range(I) for s in range(S)) <= 1 for h in range(H) for l + in range(L)) + + if self.reduction: + mdl.addConstrs(WL[l] >= WL[l + 1] for l in range(L - 1)) + mdl.addConstr(quicksum(WL[l] for l in range(L)) <= sum(cycle_assignment)) + mdl.addConstr(quicksum(WL[l] for l in range(L)) >= math.ceil(len(self.step_data) / H)) + mdl.addConstrs(quicksum(z[j, h, l] for j in range(J) for h in range(H)) >= quicksum( + z[j, h, l + 1] for j in range(J) for h in range(H)) for l in range(L - 1)) + + mdl.addConstrs(y[i, h, l] <= WL[l] for i in range(I) for h in range(H) for l in range(L)) + mdl.addConstrs(v[s, h, l] <= WL[l] for s in range(S) for h in range(H) for l in range(L)) + + mdl.addConstrs(x[i, s, h, l] >= y[i, h, l] + v[s, h, l] - 1 for i in range(I) for s in range(S) for h in range(H) + for l in range(L)) + mdl.addConstrs(x[i, s, h, l] <= y[i, h, l] for i in range(I) for s in range(S) for h in range(H) + for l in range(L)) + + mdl.addConstrs(x[i, s, h, l] <= v[s, h, l] for i in range(I) for s in range(S) for h in range(H) + for l in range(L)) + + # === search process === + # mdl.update() + # mdl.write('mdl.lp') + + mdl.optimize(terminate_condition) + # mdl.optimize() + + # === result generation === + opt_res_list = defaultdict(OptResult) + if mdl.Status == GRB.OPTIMAL or mdl.Status == GRB.INTERRUPTED or mdl.Status == GRB.TIME_LIMIT: + # === selection from solution pool === + component_pos = defaultdict(list[Point]) + for _, data in self.step_data.iterrows(): + component_index = self.part_data[self.part_data.part == data.part].index.tolist()[0] + component_pos[component_index].append(Point(data.x, data.y)) + + for part in component_pos.keys(): + component_pos[part] = sorted(component_pos[part], key=lambda pos: (pos.x, pos.y)) + + for sol_counter in range(mdl.SolCount): + mdl.Params.SolutionNumber = sol_counter + opt_res = OptResult() + # == 杞崲鏍囧噯鐨勮创瑁呭ご鍒嗛厤鐨勮В === + for l in range(L): + if abs(WL[l].Xn) <= 1e-4: + continue + opt_res.cycle.append(round(WL[l].Xn)) + opt_res.part.append([-1] * head_num) + opt_res.slot.append([-1] * head_num) + + for h in range(head_num): + for i in range(I): + if abs(y[i, h, l].Xn) <= 1e-4: + continue + opt_res.part[-1][h] = i + + for s in range(S): + if abs(v[s, h, l].Xn - 1) < 1e-4 and opt_res.part[-1][h] != -1: + opt_res.slot[-1][h] = s + + # 鏍规嵁璐磋澶翠綅缃紝杞崲渚涙枡鍣ㄦЫ浣 + cp_avg_head, cp_sum_cycle = defaultdict(float), defaultdict(int) + for cycle, component_assign in enumerate(opt_res.part): + for head, part in enumerate(component_assign): + if part == -1: + continue + cp_avg_head[part] += opt_res.cycle[cycle] * head + cp_sum_cycle[part] += opt_res.cycle[cycle] + + for part, head in cp_avg_head.items(): + cp_avg_head[part] = head / cp_sum_cycle[part] + + avg_position = sum([data.x - cp_avg_head[part_2_cpidx[data.part]] * self.config.head_intv for _, data in + self.step_data.iterrows()]) / len(self.step_data) + avg_slot = 0 + D_PU, D_PL, D_BW, D_FW = 0, 0, 0, 0 + for cycle, slots in enumerate(opt_res.slot): + min_slot, max_slot = self.config.slot_num, 0 + for head, slot in enumerate(slots): + if slot == -1: + continue + min_slot = min(min_slot, slot - head * ratio) + max_slot = max(max_slot, slot - head * ratio) + avg_slot += (max_slot - min_slot) * opt_res.cycle[cycle] + D_PU += (max_slot - min_slot) * self.config.slot_intv * opt_res.cycle[cycle] # 鎷惧彇璺緞 + + avg_slot /= sum(opt_res.cycle) + start_slot = round((avg_position + self.config.stopper_pos.x - self.config.slotf1_pos.x) + / self.config.slot_intv + avg_slot / 2) + 1 + + for cycle in range(len(opt_res.slot)): + for head in range(head_num): + if (slot := opt_res.slot[cycle][head]) == -1: + continue + opt_res.slot[cycle][head] = start_slot + slot * (2 if ratio == 1 else 1) + + component_pos_counter = defaultdict(int) + cycle_place_pos = defaultdict(list[Point]) + for head in range(head_num): + for cycle in range(len(opt_res.cycle)): + if (part := opt_res.part[cycle][head]) == -1: + continue + + avg_place_pos = Point(0, 0, _h=head) + for counter in range(round(opt_res.cycle[cycle])): + avg_place_pos.x = (1 - 1.0 / (counter + 1)) * avg_place_pos.x + ( + component_pos[part][component_pos_counter[part]].x - head * self.config.head_intv) / \ + (counter + 1) + avg_place_pos.y = (1 - 1.0 / (counter + 1)) * avg_place_pos.y + component_pos[part][ + component_pos_counter[part]].y / (counter + 1) + component_pos_counter[part] += 1 + avg_place_pos.x += self.config.stopper_pos.x + avg_place_pos.y += self.config.stopper_pos.y + cycle_place_pos[cycle].append(avg_place_pos) + + intv_ratio = self.config.head_intv // self.config.slot_intv + for cycle in range(len(opt_res.cycle)): + min_slot, max_slot = self.config.slot_num, 0 + for head in range(head_num): + if (slot := opt_res.slot[cycle][head]) == -1: + continue + min_slot = min(min_slot, slot - head * intv_ratio) + max_slot = max(max_slot, slot - head * intv_ratio) + # cycle_place_pos[cycle] = sorted(cycle_place_pos[cycle], key=lambda pt: pt.x) + + pick_pos = copy.deepcopy(self.config.slotf1_pos) + pick_pos.x += (min_slot + max_slot) / 2 * self.config.slot_intv + _, seq = self.path_planner.dynamic_programming_cycle_path(cycle_place_pos[cycle], pick_pos) + head_position = [Point(0, 0) for _ in range(head_num)] + for point in cycle_place_pos[cycle]: + head_position[point.h] = point + + for idx in range(len(seq) - 1): + h1, h2 = seq[idx], seq[idx + 1] + D_PL += max(abs(head_position[h1].x - head_position[h2].x), + abs(head_position[h1].y - head_position[h2].y)) * opt_res.cycle[cycle] + + opt_res_list[sol_counter] = opt_res + + solution_number = 0 + # mdl.Params.SolutionNumber = 0 + if hinter: + print('total cost = {}'.format(mdl.objval)) + print('cycle = {}, nozzle change = {}, pick up = {}'.format(quicksum(WL[l].Xn for l in range(L)), quicksum( + NC[h].Xn for h in range(head_num)), quicksum( + PU[s, l].Xn for s in range(-(head_num - 1) * ratio, S) for l in range(L)))) + + print('workload: ') + for l in range(L): + print(WL[l].Xn, end=', ') + + print('') + print('result') + print('component assignment: ', opt_res_list[solution_number].part) + print('feeder assignment: ', opt_res_list[solution_number].slot) + print('cycle assignment: ', opt_res_list[solution_number].cycle) + + self.result = opt_res_list[solution_number] + diff --git a/opt/utils.py b/opt/utils.py new file mode 100644 index 0000000..bd252f8 --- /dev/null +++ b/opt/utils.py @@ -0,0 +1,279 @@ +from core.common import * +from data.type import * + + +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 + + +def axis_moving_time(distance, axis=0): + assert 0 <= axis <= 2 + 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 + elif axis == 1: + return 2 * math.sqrt(distance / y_max_acceleration) if distance < Lamax else 2 * Tmax + ( + distance - Lamax) / y_max_velocity + elif axis == 2: + return abs((distance + 180) % 360 - 180) * head_rotary_velocity + return .0 + + +def optimizer_result_hinter(config, part_data, opt_res, nozzle_hinter=False, part_hinter=False, + slot_hinter=False, place_hinter=False): + if nozzle_hinter: + columns = ['H{}'.format(i + 1) for i in range(config.head_num)] + ['cycle'] + + nozzle_assign = pd.DataFrame(columns=columns) + for cycle, components in enumerate(opt_res.part): + nozzle_assign_row = len(nozzle_assign) + nozzle_assign.loc[nozzle_assign_row, 'cycle'] = opt_res.cycle[cycle] + + for head in range(config.head_num): + index = opt_res.part[cycle][head] + if index == -1: + nozzle_assign.loc[nozzle_assign_row, 'H{}'.format(head + 1)] = '' + else: + nozzle = part_data.loc[index]['nz'] + nozzle_assign.loc[nozzle_assign_row, 'H{}'.format(head + 1)] = nozzle + + for head in range(config.head_num): + if nozzle_assign_row == 0 or nozzle_assign.loc[nozzle_assign_row - 1, 'H{}'.format(head + 1)] != \ + nozzle_assign.loc[nozzle_assign_row, 'H{}'.format(head + 1)]: + break + else: + nozzle_assign.loc[nozzle_assign_row - 1, 'cycle'] += nozzle_assign.loc[nozzle_assign_row, 'cycle'] + nozzle_assign.drop([len(nozzle_assign) - 1], inplace=True) + + print(nozzle_assign) + print('') + + if part_hinter: + columns = ['H{}'.format(i + 1) for i in range(config.head_num)] + ['cycle'] + + part_assign = pd.DataFrame(columns=columns) + for cycle, components in enumerate(opt_res.part): + part_assign.loc[cycle, 'cycle'] = opt_res.cycle[cycle] + for head in range(config.head_num): + index = opt_res.part[cycle][head] + if index == -1: + part_assign.loc[cycle, 'H{}'.format(head + 1)] = '' + else: + # component_assign.loc[cycle, 'H{}'.format(head + 1)] = component_data.loc[index]['part'] + part_assign.loc[cycle, 'H{}'.format(head + 1)] = 'C' + str(index) + + print(part_assign) + print('') + + if slot_hinter: + columns = ['H{}'.format(i + 1) for i in range(config.head_num)] + ['cycle'] + + slot_assign = pd.DataFrame(columns=columns) + for cycle, components in enumerate(opt_res.slot): + slot_assign.loc[cycle, 'cycle'] = opt_res.cycle[cycle] + for head in range(config.head_num): + slot = opt_res.slot[cycle][head] + if slot == -1: + slot_assign.loc[cycle, 'H{}'.format(head + 1)] = 'A' + else: + try: + slot_assign.loc[cycle, 'H{}'.format(head + 1)] = 'F{}'.format( + slot) if slot <= config.slot_num // 2 else 'R{}'.format(slot - config.head_num) + except: + print('') + + print(slot_assign) + print('') + + if place_hinter: + columns = ['H{}'.format(i + 1) for i in range(config.head_num)] + ['cycle'] + + place_assign = pd.DataFrame(columns=columns) + for cycle, _ in enumerate(opt_res.point): + place_assign.loc[cycle, 'cycle'] = 1 + for head in range(config.head_num): + point = opt_res.point[cycle][head] + if point != -1: + place_assign.loc[cycle, 'H{}'.format(head + 1)] = 'P{}'.format(point) + else: + place_assign.loc[cycle, 'H{}'.format(head + 1)] = '' + + headseq_assign = pd.DataFrame(columns=columns) + for cycle, headseq in enumerate(opt_res.sequence): + headseq_assign.loc[cycle, 'cycle'] = 1 + for head in range(len(headseq)): + headseq_assign.loc[cycle, 'H{}'.format(head + 1)] = 'H{}'.format(headseq[head]) + + print(place_assign) + print(headseq_assign) + print('') + + +def evaluation(config: MachineConfig, part_data, step_data, opt_res: OptResult, hinter=False): + # === 优化结果参数 === + info = OptInfo() + # === 校验 === + info.total_points = 0 + for cycle, part in enumerate(opt_res.part): + for head, component in enumerate(part): + if component == -1: + continue + info.total_points += opt_res.cycle[cycle] + + if info.total_points != len(step_data): + warning_info = 'the number of ' + str(info.total_points) + ' placement point(s) is not match with the PCB data. ' + warnings.warn(warning_info, UserWarning) + + interval_ratio = config.head_intv / config.slot_intv + if opt_res.point: + total_points = info.total_points + for placements in opt_res.point: + for placement in placements: + if placement == -1: + continue + total_points -= 1 + + if total_points != 0: + warnings.warn( + 'the optimization result of component assignment result and placement result are not consistent. ', + UserWarning) + return OptInfo() + + feeder_arrangement = defaultdict(set) + for cycle, feeder_slots in enumerate(opt_res.slot): + for head, slot in enumerate(feeder_slots): + if slot == -1: + continue + feeder_arrangement[opt_res.part[cycle][head]].add(slot) + + info.total_components = len(feeder_arrangement.keys()) + for part, data in part_data.iterrows(): + if part in feeder_arrangement.keys() and data.fdn < len(feeder_arrangement[part]): + info = 'the number of arranged feeder of [' + data['part'] + '] exceeds the quantity limit' + warnings.warn(info, UserWarning) + + cur_pos, next_pos = config.anc_pos, Point(0, 0) # 贴装头当前位置 + + # 初始化首个周期的吸嘴装配信息 + nozzle_assigned = ['Empty' for _ in range(config.head_num)] + for head in range(config.head_num): + for cycle in range(len(opt_res.part)): + idx = opt_res.part[cycle][head] + if idx == -1: + continue + else: + nozzle_assigned[head] = part_data.loc[idx]['nz'] + + for cycle_set, _ in enumerate(opt_res.part): + floor_cycle, ceil_cycle = sum(opt_res.cycle[:cycle_set]), sum(opt_res.cycle[:(cycle_set + 1)]) + for cycle in range(floor_cycle, ceil_cycle): + if sum(opt_res.part[cycle_set]) == -config.head_num: + continue + pick_slot, mount_pos, mount_angle = [], [], [] + nozzle_pick_counter, nozzle_put_counter = 0, 0 # 吸嘴更换次数统计(拾取/放置分别算一次) + for head in range(config.head_num): + if opt_res.slot[cycle_set][head] != -1: + pick_slot.append(opt_res.slot[cycle_set][head] - interval_ratio * head) + if opt_res.part[cycle_set][head] == -1: + continue + nozzle = part_data.loc[opt_res.part[cycle_set][head]]['nz'] + if nozzle != nozzle_assigned[head]: + if nozzle_assigned[head] != 'Empty': + nozzle_put_counter += 1 + nozzle_pick_counter += 1 + nozzle_assigned[head] = nozzle + + # ANC处进行吸嘴更换 + if nozzle_pick_counter + nozzle_put_counter > 0: + next_pos = config.anc_pos + move_time = max(axis_moving_time(cur_pos.x - next_pos.x, 0), + axis_moving_time(cur_pos.y - next_pos.y, 1)) + info.round_time += move_time + info.anc_round_counter += 1 + info.total_distance += max(abs(cur_pos.x - next_pos.x), abs(cur_pos.y - next_pos.y)) + cur_pos = next_pos + + pick_slot = list(set(pick_slot)) + pick_slot = sorted(pick_slot, reverse=True) + + # 拾取路径(自右向左) + for idx, slot in enumerate(pick_slot): + if slot < config.slot_num // 2: + next_pos = Point(config.slotf1_pos.x + config.slot_intv * (slot - 1), config.slotf1_pos.y) + else: + next_pos = Point(config.slotr1_pos.x - config.slot_intv * (config.slot_num - slot - 1), config.slotr1_pos.y) + info.operation_time += config.pick_time + info.pickup_counter += 1 + + move_time = max(axis_moving_time(cur_pos.x - next_pos.x, 0), + axis_moving_time(cur_pos.y - next_pos.y, 1)) + if idx == 0: + info.round_time += move_time + else: + info.pickup_time += move_time + + info.total_distance += max(abs(cur_pos.x - next_pos.x), abs(cur_pos.y - next_pos.y)) + if slot != pick_slot[0]: + info.pickup_distance += max(abs(cur_pos.x - next_pos.x), abs(cur_pos.y - next_pos.y)) + cur_pos = next_pos + + # 贴装路径 + if opt_res.point and opt_res.sequence: + head_angle = [0 for _ in range(config.head_num)] + for head in opt_res.sequence[cycle]: + index = opt_res.point[cycle][head] + if index == -1: + continue + mount_pos.append(Point(step_data.loc[index].x - head * config.head_intv + config.stopper_pos.x, + step_data.loc[index].y + config.stopper_pos.y)) + head_angle[head] = step_data.loc[index]['r'] + + # 单独计算贴装路径 + for cntPoints in range(len(mount_pos) - 1): + info.place_distance += max(abs(mount_pos[cntPoints].x - mount_pos[cntPoints + 1].x), + abs(mount_pos[cntPoints].y - mount_pos[cntPoints + 1].y)) + + if mount_pos[0].x < mount_pos[-1].x: + mount_pos = reversed(mount_pos) + + # 考虑R轴预旋转,补偿同轴角度转动带来的额外贴装用时 + info.operation_time += config.nozzle_install_time * nozzle_put_counter + \ + config.nozzle_uninstall_time * nozzle_pick_counter + for idx, pos in enumerate(mount_pos): + info.operation_time += config.place_time + + if idx == 0: + move_time = max(axis_moving_time(cur_pos.x - pos.x, 0), + axis_moving_time(cur_pos.y - pos.y, 1)) + info.round_time += move_time + else: + + cur_head = opt_res.sequence[cycle][idx] + side_head = cur_head - 1 if cur_head % 2 else cur_head + 1 + if opt_res.sequence[cycle][idx - 1] != side_head: + move_time = max(axis_moving_time(cur_pos.x - pos.x, 0), + axis_moving_time(cur_pos.y - pos.y, 1)) + else: + move_time = max(axis_moving_time(cur_pos.x - pos.x, 0), + axis_moving_time(cur_pos.y - pos.y, 1), + axis_moving_time(head_angle[cur_head] - head_angle[side_head], 2)) + info.place_time += move_time + + info.total_distance += max(abs(cur_pos.x - pos.x), abs(cur_pos.y - pos.y)) + cur_pos = pos + + info.nozzle_change_counter += nozzle_put_counter + nozzle_pick_counter + + info.total_time = info.pickup_time + info.round_time + info.place_time + info.operation_time + info.cycle_counter = sum(opt_res.cycle) + if hinter: + optimizer_result_hinter(config, part_data, opt_res, part_hinter=True, nozzle_hinter=True, slot_hinter=True) + info.print() + + return info + +