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)