优化器类的定义和实现
This commit is contained in:
172
opt/smm/aggregation.py
Normal file
172
opt/smm/aggregation.py
Normal file
@@ -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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
363
opt/smm/basis.py
Normal file
363
opt/smm/basis.py
Normal file
@@ -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<75><70><EFBFBD><EFBFBD><EFBFBD><EFBFBD>feeder_group<75><70>ƫ<EFBFBD><C6AB><EFBFBD><EFBFBD>
|
||||
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:
|
||||
# <20>·<EFBFBD><C2B7>乩<EFBFBD><E4B9A9><EFBFBD><EFBFBD>
|
||||
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:
|
||||
# ʹ<>þɹ<C3BE><C9B9><EFBFBD><EFBFBD><EFBFBD>
|
||||
for feeder_index, feeder_part in enumerate(max_common_part):
|
||||
if feeder_part != -1:
|
||||
new_feeder_group[feeder_index] = -1
|
||||
|
||||
# ȥ<><C8A5><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ԫ<EFBFBD><D4AA>
|
||||
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)
|
||||
|
||||
# ȷ<><C8B7><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>İ<EFBFBD>װλ<D7B0><CEBB>
|
||||
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)
|
||||
|
||||
# Ԫ<><D4AA>ʹ<EFBFBD>õ<EFBFBD>ͷ
|
||||
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)
|
||||
|
||||
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>˳<EFBFBD><CBB3>
|
||||
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: <20><>δ<EFBFBD><CEB4><EFBFBD>ǻ<EFBFBD>е<EFBFBD><D0B5>λ
|
||||
feeder_group_slot = [-1] * len(feeder_group)
|
||||
feeder_lane_state = [0] * self.config.slot_num # 0<><30>ʾ<EFBFBD>գ<EFBFBD>1<EFBFBD><31>ʾ<EFBFBD><CABE>ռ<EFBFBD><D5BC>
|
||||
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-<2D><><EFBFBD><EFBFBD>, 0-<2D><><EFBFBD><EFBFBD>
|
||||
left_out_range, right_out_range = False, False
|
||||
while True:
|
||||
assign_slot = best_slot + step if search_dir else best_slot - step
|
||||
# <20><><EFBFBD><EFBFBD>Խ<EFBFBD>磬<EFBFBD><E7A3AC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
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 # <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
else:
|
||||
search_dir = 1 - search_dir # ˫<><CBAB><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
if search_dir == 0:
|
||||
step += 1
|
||||
|
||||
assign_available = True
|
||||
|
||||
# === <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ӧ<EFBFBD><D3A6>λ ===
|
||||
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!')
|
||||
|
||||
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ƥ<EFBFBD><C6A5>ԭ<EFBFBD><D4AD><EFBFBD><EFBFBD>ȷ<EFBFBD><C8B7><EFBFBD><EFBFBD>Ԫ<EFBFBD><D4AA><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʰȡ<CAB0><C8A1>λ
|
||||
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 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD><D2BB>Ԫ<EFBFBD><D4AA><EFBFBD><EFBFBD>ƫ<EFBFBD><C6AB><EFBFBD><EFBFBD>
|
||||
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: # <20><><EFBFBD><EFBFBD><EFBFBD>ظ<EFBFBD>ѡȡ
|
||||
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
|
||||
|
||||
# <20>Ƴ<EFBFBD><C6B3>ظ<EFBFBD>Ԫ<EFBFBD><D4AA>
|
||||
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
|
||||
|
||||
237
opt/smm/cell_division.py
Normal file
237
opt/smm/cell_division.py
Normal file
@@ -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)
|
||||
791
opt/smm/feeder_priority.py
Normal file
791
opt/smm/feeder_priority.py
Normal file
@@ -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
|
||||
|
||||
# 计算扫描后的代价函数,记录扫描后的最优解
|
||||
# 短期收益
|
||||
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
|
||||
535
opt/smm/hybrid_genetic.py
Normal file
535
opt/smm/hybrid_genetic.py
Normal file
@@ -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)
|
||||
|
||||
|
||||
|
||||
290
opt/smm/path_plan.py
Normal file
290
opt/smm/path_plan.py
Normal file
@@ -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):
|
||||
# ʰȡ<CAB0><C8A1>ʼ<EFBFBD><CABC><EFBFBD><EFBFBD>ֹλ<D6B9><CEBB> <20><> <20><>ͬ<EFBFBD><CDAC>
|
||||
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))
|
||||
|
||||
# <20><><EFBFBD>ڵ<EFBFBD>֮<EFBFBD><D6AE><EFBFBD>ľ<EFBFBD><C4BE><EFBFBD>
|
||||
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<64><70><EFBFBD><EFBFBD>
|
||||
for s in range(1, 1 << num_pos, 2):
|
||||
# <20><><EFBFBD>ǽڵ㼯<DAB5><E3BCAF>s<EFBFBD><73><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ڵ<EFBFBD>0
|
||||
if not (s & 1):
|
||||
continue
|
||||
for j in range(1, num_pos):
|
||||
# <20>յ<EFBFBD>j<EFBFBD><6A><EFBFBD>ڵ<EFBFBD>ǰ<EFBFBD><C7B0><EFBFBD>ǽڵ㼯<DAB5><E3BCAF>s<EFBFBD><73>
|
||||
if not (s & (1 << j)):
|
||||
continue
|
||||
if s == int((1 << j) | 1):
|
||||
# <20><><EFBFBD><EFBFBD><EFBFBD>ǽڵ㼯<DAB5><E3BCAF>s<EFBFBD><73><EFBFBD><EFBFBD><EFBFBD>ڵ<EFBFBD>0<EFBFBD>ͽڵ<CDBD>j<EFBFBD><6A>dp<64>߽磬<DFBD><E7A3AC><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ֵ
|
||||
# print('j:', j)
|
||||
min_path[s][j] = [j]
|
||||
min_dist[s][j] = dist[0][j]
|
||||
|
||||
# ö<><C3B6><EFBFBD><EFBFBD>һ<EFBFBD><D2BB><EFBFBD>ڵ<EFBFBD>i<EFBFBD><69><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
for i in range(1, num_pos):
|
||||
# <20><>һ<EFBFBD><D2BB><EFBFBD>ڵ<EFBFBD>i<EFBFBD><69><EFBFBD>ڿ<EFBFBD><DABF>ǽڵ㼯<DAB5><E3BCAF>s<EFBFBD><73>
|
||||
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 = []
|
||||
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>̹<EFBFBD><CCB9>ܶٻ<DCB6>·
|
||||
for i in range(1, num_pos):
|
||||
if min_dist[(1 << num_pos) - 1][i] + dist[i][0] < ans_dist:
|
||||
# <20><><EFBFBD>£<EFBFBD><C2A3><EFBFBD>·<EFBFBD><C2B7>
|
||||
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]
|
||||
|
||||
# <20><>¼<EFBFBD><C2BC>װ<EFBFBD><D7B0><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ͷ<EFBFBD>Ӧ<EFBFBD><D3A6>λ<EFBFBD><CEBB><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
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): # <20><>ͬ<EFBFBD><CDAC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>װͷ<D7B0><CDB7><EFBFBD><EFBFBD>ʼ<EFBFBD><CABC><EFBFBD><EFBFBD>ѡȡ<D1A1><C8A1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͬ
|
||||
if search_dir == 0:
|
||||
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
search_points = np.arange(left_boundary, (left_boundary + right_boundary) / 2, search_step)
|
||||
head_range = list(range(head_num))
|
||||
elif search_dir == 1:
|
||||
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
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:
|
||||
# <20><><EFBFBD>м<EFBFBD><D0BC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
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
|
||||
|
||||
484
opt/smm/two_phase.py
Normal file
484
opt/smm/two_phase.py
Normal file
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user