优化器类的定义和实现

This commit is contained in:
2025-11-14 11:34:48 +08:00
parent a37ee38369
commit 79b09b2578
11 changed files with 4004 additions and 0 deletions

539
opt/hyper_heuristic.py Normal file
View File

@@ -0,0 +1,539 @@
from opt.predictor import NeuralPredictor
from opt.utils import *
from core.interface import *
from core.common import *
os.environ['KMP_DUPLICATE_LIB_OK'] = 'True'
class Heuristic:
@staticmethod
def apply(cp_points, cp_nozzle, cp_assign, config: defaultdict[int]):
return -1
class LeastPoints(Heuristic):
@staticmethod
def apply(cp_points, cp_nozzle, cp_assign, config: defaultdict[int]):
machine_index, machine_points = [], []
for index in config.keys():
if len(cp_assign[index]) == 0:
return index
machine_index.append(index)
machine_points.append(sum([cp_points[cp_idx] for cp_idx in cp_assign[index]]))
return machine_index[np.argmin(machine_points)]
class LeastNzTypes(Heuristic):
@staticmethod
def apply(cp_points, cp_nozzle, cp_assign, config: defaultdict[int]):
machine_index, machine_nozzle = [], []
for index in config.keys():
if len(cp_assign[index]) == 0:
return index
machine_index.append(index)
machine_nozzle.append([cp_nozzle[cp_idx] for cp_idx in cp_assign[index]])
index = np.argmin(
[len(set(nozzle)) + 1e-5 * sum(cp_points[c] for c in cp_assign[machine_idx]) for machine_idx, nozzle in
enumerate(machine_nozzle)])
return machine_index[index]
class LeastCpTypes(Heuristic):
@staticmethod
def apply(cp_points, cp_nozzle, cp_assign, config: defaultdict[int]):
machine_index, machine_types = [], []
for index in config.keys():
machine_index.append(index)
machine_types.append(len(cp_assign[index]) + 1e-5 * sum(cp_points[cp] for cp in cp_assign[index]))
return machine_index[np.argmin(machine_types)]
class LeastCpNzRatio(Heuristic):
@staticmethod
def apply(cp_points, cp_nozzle, cp_assign, config: defaultdict[int]):
machine_index, machine_nz_type, machine_cp_type = [], [], []
for index in config.keys():
if len(cp_assign[index]) == 0:
return index
machine_index.append(index)
machine_nz_type.append(set(cp_nozzle[cp_idx] for cp_idx in cp_assign[index]))
machine_cp_type.append(len(cp_assign[index]))
min_idx = np.argmin([(machine_cp_type[idx] + 1e-5 * sum(
cp_points[c] for c in cp_assign[machine_index[idx]])) / (len(machine_nz_type[idx]) + 1e-5) for idx in
range(len(machine_index))])
return machine_index[min_idx]
def nozzle_assignment(cp_points, cp_nozzle, cp_assign, head_num):
nozzle_points = defaultdict(int)
for cp_idx in cp_assign:
nozzle_points[cp_nozzle[cp_idx]] += cp_points[cp_idx]
while len(nozzle_points.keys()) > head_num:
del nozzle_points[min(nozzle_points.items(), key=lambda x: x[1])[0]]
sum_points = sum(nozzle_points.values())
nozzle_points = defaultdict(int, {k: v for k, v in nozzle_points.items() if v / sum_points >= 0.8 / head_num})
nozzle_heads = defaultdict(int, {k: 1 for k in nozzle_points.keys()})
while sum(nozzle_heads.values()) != head_num:
max_cycle_nozzle = None
for nozzle, head_cnt in nozzle_heads.items():
if max_cycle_nozzle is None or nozzle_points[nozzle] / head_cnt > nozzle_points[max_cycle_nozzle] / \
nozzle_heads[max_cycle_nozzle]:
max_cycle_nozzle = nozzle
assert max_cycle_nozzle is not None
nozzle_heads[max_cycle_nozzle] += 1
return nozzle_heads, nozzle_points
class LeastCycle(Heuristic):
@staticmethod
def apply(cp_points, cp_nozzle, cp_assign, config: defaultdict[int]):
machine_index, machine_cycle = [], []
for index, head_num in config.items():
assign_component = cp_assign[index]
if len(assign_component) == 0:
return index
nozzle_heads, nozzle_points = nozzle_assignment(cp_points, cp_nozzle, assign_component, head_num)
machine_index.append(index)
machine_cycle.append(
max(nozzle_points[nozzle] / head for nozzle, head in nozzle_heads.items()) + 1e-5 * sum(
cp_points[c] for c in cp_assign[index]))
return machine_index[np.argmin(machine_cycle)]
class LeastNzChange(Heuristic):
@staticmethod
def apply(cp_points, cp_nozzle, cp_assign, config: defaultdict[int]):
machine_index, machine_nozzle_change = [], []
for index, head_num in config.items():
assign_component = cp_assign[index]
if len(assign_component) == 0:
return index
heads_points = []
nozzle_heads, nozzle_points = nozzle_assignment(cp_points, cp_nozzle, assign_component, head_num)
for nozzle, head in nozzle_heads.items():
for _ in range(head):
heads_points.append(nozzle_points[nozzle] / nozzle_heads[nozzle])
machine_index.append(index)
machine_nozzle_change.append(np.std(heads_points) + 1e-5 * sum(cp_points[c] for c in cp_assign[index]))
return machine_index[np.argmin(machine_nozzle_change)]
class LeastPickup(Heuristic):
@staticmethod
def apply(cp_points, cp_nozzle, cp_assign, config: defaultdict[int]):
machine_index, machine_pick_up = [], []
for index, head_num in config.items():
assign_component = cp_assign[index]
if len(assign_component) == 0:
return index
nozzle_heads, nozzle_points = nozzle_assignment(cp_points, cp_nozzle, assign_component, head_num)
nozzle_level, nozzle_counter = defaultdict(int), defaultdict(int)
level_points = defaultdict(int)
for cp_idx in sorted(assign_component, key=lambda x: cp_points[x], reverse=True):
nozzle, points = cp_nozzle[cp_idx], cp_points[cp_idx]
if nozzle not in nozzle_heads.keys():
continue
if nozzle_counter[nozzle] and nozzle_counter[nozzle] % nozzle_heads[nozzle] == 0:
nozzle_level[nozzle] += 1
level = nozzle_level[nozzle]
level_points[level] = max(level_points[level], points)
nozzle_counter[nozzle] += 1
machine_index.append(index)
machine_pick_up.append(sum(points for points in level_points.values()) + 1e-5 * sum(
cp_points[idx] for idx in cp_assign[index]))
return machine_index[np.argmin(machine_pick_up)]
class HyperHeuristicOpt(BaseOpt):
def __init__(self, machine_num, part_data, step_data, feeder_data=None):
super().__init__(None, part_data, step_data, feeder_data)
self.line_config = [MachineConfig() for _ in range(machine_num)]
# self.base_opt = FeederPriorityOpt
self.base_opt = CellDivisionOpt
self.heuristic_map = {
'p': LeastPoints,
'n': LeastNzTypes,
'c': LeastCpTypes,
'r': LeastCpNzRatio,
'k': LeastCycle,
'g': LeastNzChange,
'u': LeastPickup,
}
self.machine_num = machine_num
self.predictor = NeuralPredictor()
self.cp_feeders = defaultdict(int)
self.cp_nozzle = defaultdict(str)
self.cp_points = defaultdict(int)
self.cp_index = defaultdict(int)
part_points = defaultdict(int)
for _, data in self.step_data.iterrows():
part_points[data.part] += 1
division_part = []
for _, data in self.part_data.iterrows():
division_part.extend([part_points[data.part] / data.fdn for _ in range(data.fdn)])
division_points = sum(division_part) / len(division_part)
idx = 0
for cp_idx, data in self.part_data.iterrows():
self.cp_feeders[cp_idx] = 1
division_data = copy.deepcopy(data)
division_data['points'] = part_points[data.part]
feeder_limit, total_points = division_data.fdn, division_data.points
if feeder_limit != 1:
feeder_limit = round(min(max(total_points // division_points * 1.5, feeder_limit), total_points))
# feeder_limit = total_points # С<><D0A1>ģ<EFBFBD><C4A3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
surplus_points = total_points % feeder_limit
for _ in range(feeder_limit):
division_data.fdn, division_data.points = 1, math.floor(total_points / feeder_limit)
if surplus_points:
division_data.points += 1
surplus_points -= 1
self.cp_points[idx], self.cp_nozzle[idx] = division_data.points, division_data.nz
self.cp_index[idx] = cp_idx
idx += 1
self.board_width = self.step_data['x'].max() - self.step_data['x'].min()
self.board_height = self.step_data['y'].max() - self.step_data['y'].min()
def generate_pattern(self):
"""
Generates a random pattern.
:return: The generated pattern string.
"""
return "".join([random.choice(list(self.heuristic_map.keys()))
for _ in range(random.randrange(1, len(self.cp_points)))])
def convertor(self, component_list, individual):
component_num = len(self.cp_feeders.keys())
cp_assign = [[] for _ in range(self.machine_num)]
component_machine_assign = [[0 for _ in range(self.machine_num)] for _ in range(component_num)]
machine_assign_counter = [0 for _ in range(self.machine_num)]
for idx, div_cp_idx in enumerate(component_list):
h = individual[idx % len(individual)]
cp_idx = self.cp_index[div_cp_idx]
if self.cp_points[cp_idx] == 0:
continue
machine_config = defaultdict(int) # <20>ɱ<EFBFBD><C9B1><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ļ<EFBFBD><C4BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>-<2D><>Ƭͷ<C6AC><CDB7>
if sum(component_machine_assign[cp_idx][:]) < self.cp_feeders[cp_idx]:
for machine_index in range(self.machine_num):
if component_machine_assign[cp_idx][machine_index] or machine_assign_counter[machine_index] < \
self.predictor.max_placement_points:
machine_config[machine_index] = self.line_config[machine_index].head_num
machine_index = self.heuristic_map[h].apply(self.cp_points, self.cp_nozzle, cp_assign, machine_config)
else:
for machine_index in range(self.machine_num):
if component_machine_assign[cp_idx][machine_index]:
machine_config[machine_index] = self.line_config[machine_index].head_num
machine_index = self.heuristic_map[h].apply(self.cp_points, self.cp_nozzle, cp_assign, machine_config)
cp_assign[machine_index].append(div_cp_idx)
if component_machine_assign[cp_idx][machine_index] == 0:
machine_assign_counter[machine_index] += 1
component_machine_assign[cp_idx][machine_index] = 1
return cp_assign
def crossover(self, parent1, parent2):
"""
Attempt to perform crossover between two chromosomes.
:param parent1: The first parent.
:param parent2: The second parent.
:return: The two individuals after crossover has been performed.
"""
point1, point2 = random.randrange(len(parent1)), random.randrange(len(parent2))
substr1, substr2 = parent1[point1:], parent2[point2:]
offspring1, offspring2 = "".join((parent1[:point1], substr2)), "".join((parent2[:point2], substr1))
return offspring1[:len(self.cp_points)], offspring2[:len(self.cp_points)]
def mutation(self, individual):
"""
Attempts to mutate the individual by replacing a random heuristic in the chromosome by a generated pattern.
:param individual: The individual to mutate.
:return: The mutated individual.
"""
pattern = list(individual)
mutation_point = random.randrange(len(pattern))
pattern[mutation_point] = self.generate_pattern()
return ''.join(pattern)[:len(self.cp_points)]
def initialize(self, population_size):
return [self.generate_pattern() for _ in range(population_size)]
def cal_ind_val(self, component_list, individual):
machine_cp_assign = self.convertor(component_list, individual)
component_number = len(self.cp_feeders)
machine_cp_points = [[0 for _ in range(component_number)] for _ in range(self.machine_num)]
for machine_idx in range(self.machine_num):
for idx in machine_cp_assign[machine_idx]:
machine_cp_points[machine_idx][self.cp_index[idx]] += self.cp_points[idx]
machine_cp_feeders = [[0 for _ in range(component_number)] for _ in range(self.machine_num)]
for cp_idx in range(component_number):
if self.cp_points[cp_idx] == 0:
continue
feeder_nums = self.cp_feeders[cp_idx]
for machine_idx in range(self.machine_num):
if machine_cp_points[machine_idx][cp_idx]:
machine_cp_feeders[machine_idx][cp_idx] = 1
feeder_nums -= 1
while feeder_nums > 0:
assign_machine = None
for machine_idx in range(self.machine_num):
if machine_cp_points[machine_idx][cp_idx] == 0:
continue
if assign_machine is None:
assign_machine = machine_idx
continue
if machine_cp_points[assign_machine][cp_idx] / machine_cp_feeders[assign_machine][cp_idx] \
< machine_cp_points[machine_idx][cp_idx] / machine_cp_feeders[machine_idx][cp_idx]:
assign_machine = machine_idx
machine_cp_feeders[assign_machine][cp_idx] += 1
feeder_nums -= 1
nozzle_type = defaultdict(str)
for idx, cp_idx in self.cp_index.items():
nozzle_type[cp_idx] = self.cp_nozzle[idx]
obj = []
for machine_idx in range(self.machine_num):
div_cp_points, div_cp_nozzle = defaultdict(int), defaultdict(str)
idx = 0
for cp_idx in range(component_number):
total_points = machine_cp_points[machine_idx][cp_idx]
if total_points == 0:
continue
div_index = 0
div_points = [total_points // machine_cp_feeders[machine_idx][cp_idx] for _ in
range(machine_cp_feeders[machine_idx][cp_idx])]
while sum(div_points) < total_points:
div_points[div_index] += 1
div_index += 1
for points in div_points:
div_cp_points[idx] = points
div_cp_nozzle[idx] = nozzle_type[cp_idx]
idx += 1
obj.append(self.predictor.eval(div_cp_points, div_cp_nozzle,
self.board_width, self.board_height, self.line_config[machine_idx]))
return obj
def evaluate(self, assignment):
partial_step_data, partial_part_data = defaultdict(pd.DataFrame), defaultdict(pd.DataFrame)
for machine_index in range(self.machine_num):
partial_step_data[machine_index] = pd.DataFrame(columns=self.step_data.columns)
partial_part_data[machine_index] = self.part_data.copy(deep=True)
partial_part_data[machine_index]['points'] = 0
# averagely assign available feeder
for part_index, data in self.part_data.iterrows():
feeder_limit = data.fdn
feeder_points = [assignment[machine_index][part_index] for machine_index in range(self.machine_num)]
if sum(feeder_points) == 0:
continue
for machine_index in range(self.machine_num):
partial_part_data[machine_index].loc[part_index, 'points'] = 0
for machine_index in range(self.machine_num):
if feeder_points[machine_index] == 0:
continue
partial_part_data[machine_index].loc[part_index, 'fdn'] = 1
feeder_limit -= 1
while feeder_limit:
assign_machine = None
for machine_index in range(self.machine_num):
if feeder_limit <= 0:
break
if feeder_points[machine_index] == 0:
continue
if assign_machine is None or feeder_points[machine_index] / \
partial_part_data[machine_index].loc[part_index].fdn > feeder_points[
assign_machine] / partial_part_data[assign_machine].loc[part_index].fdn:
assign_machine = machine_index
assert assign_machine is not None
partial_part_data[assign_machine].loc[part_index, 'fdn'] += 1
feeder_limit -= 1
for machine_index in range(self.machine_num):
if feeder_points[machine_index] > 0:
assert partial_part_data[machine_index].loc[part_index].fdn > 0 # assignment[machine_index][part_index]
# === assign placements ===
part2idx = defaultdict(int)
for idx, data in self.part_data.iterrows():
part2idx[data.part] = idx
machine_average_pos = [[0, 0] for _ in range(self.machine_num)]
machine_step_counter = [0 for _ in range(self.machine_num)]
part_step_data = defaultdict(list)
for _, data in self.step_data.iterrows():
part_step_data[part2idx[data.part]].append(data)
multiple_component_index = []
for part_index in range(len(self.part_data)):
machine_assign_set = []
for machine_index in range(self.machine_num):
if assignment[machine_index][part_index]:
machine_assign_set.append(machine_index)
if len(machine_assign_set) == 1:
for data in part_step_data[part_index]:
machine_index = machine_assign_set[0]
machine_average_pos[machine_index][0] += data.x
machine_average_pos[machine_index][1] += data.y
machine_step_counter[machine_index] += 1
partial_part_data[machine_index].loc[part_index, 'points'] += 1
partial_step_data[machine_index] = pd.concat(
[partial_step_data[machine_index], pd.DataFrame(data).T])
elif len(machine_assign_set) > 1:
multiple_component_index.append(part_index)
for machine_index in range(self.machine_num):
if machine_step_counter[machine_index] == 0:
continue
machine_average_pos[machine_index][0] /= machine_step_counter[machine_index]
machine_average_pos[machine_index][1] /= machine_step_counter[machine_index]
for part_index in multiple_component_index:
for data in part_step_data[part_index]:
idx = -1
min_dist = None
for machine_index in range(self.machine_num):
if partial_part_data[machine_index].loc[part_index, 'points'] >= assignment[machine_index][part_index]:
continue
dist = (data.x - machine_average_pos[machine_index][0]) ** 2 + (
data.y - machine_average_pos[machine_index][1]) ** 2
if min_dist is None or dist < min_dist:
min_dist, idx = dist, machine_index
assert idx >= 0
machine_step_counter[idx] += 1
machine_average_pos[idx][0] += (1 - 1 / machine_step_counter[idx]) * machine_average_pos[idx][0] \
+ data.x / machine_step_counter[idx]
machine_average_pos[idx][1] += (1 - 1 / machine_step_counter[idx]) * machine_average_pos[idx][1] \
+ data.y / machine_step_counter[idx]
partial_part_data[idx].loc[part_index, 'points'] += 1
partial_step_data[idx] = pd.concat([partial_step_data[idx], pd.DataFrame(data).T])
obj, result = [], []
for machine_index in range(self.machine_num):
rows = partial_part_data[machine_index]['points'] != 0
partial_part_data[machine_index] = partial_part_data[machine_index][rows]
opt = self.base_opt(self.line_config[machine_index], partial_part_data[machine_index],
partial_step_data[machine_index])
opt.optimize(hinter=False)
info = evaluation(self.line_config[machine_index], partial_part_data[machine_index],
partial_step_data[machine_index], opt.result)
obj.append(info.total_time)
result.append(opt.result)
return max(obj), result
def optimize(self):
# genetic-based hyper-heuristic
crossover_rate, mutation_rate = 0.6, 0.1
population_size, total_generation = 20, 50
group_size = 10
best_val = np.inf
component_list = list(range(len(self.cp_points)))
with tqdm(total=total_generation * group_size) as pbar:
pbar.set_description('hyper-heuristic algorithm process for PCB assembly line balance')
for _ in range(group_size):
random.shuffle(component_list)
new_population = []
population = self.initialize(population_size)
# calculate fitness value
pop_val = [max(self.cal_ind_val(component_list, individual)) for individual in population]
for _ in range(total_generation):
population += new_population
for individual in new_population:
pop_val.append(max(self.cal_ind_val(component_list, individual)))
select_index = GenOpe.get_top_kth(pop_val, population_size, reverse=False)
population = [population[idx] for idx in select_index]
pop_val = [pop_val[idx] for idx in select_index]
# min-max convert
max_val = max(pop_val)
sel_pop_val = list(map(lambda v: max_val - v, pop_val))
sum_pop_val = sum(sel_pop_val) + 1e-10
sel_pop_val = [v / sum_pop_val + 1e-3 for v in sel_pop_val]
# crossover and mutation
new_population = []
for pop in range(population_size):
if pop % 2 == 0 and np.random.random() < crossover_rate:
index1 = GenOpe.roulette_wheel_selection(sel_pop_val)
while True:
index2 = GenOpe.roulette_wheel_selection(sel_pop_val)
if index1 != index2:
break
offspring1, offspring2 = self.crossover(population[index1], population[index2])
if np.random.random() < mutation_rate:
offspring1 = self.mutation(offspring1)
if np.random.random() < mutation_rate:
offspring2 = self.mutation(offspring2)
new_population.append(offspring1)
new_population.append(offspring2)
pbar.update(1)
machine_assign = self.convertor(component_list, population[0])
assignment_result = [[0 for _ in range(len(self.part_data))] for _ in range(self.machine_num)]
for machine_idx in range(self.machine_num):
for idx in machine_assign[machine_idx]:
assignment_result[machine_idx][self.cp_index[idx]] += self.cp_points[idx]
val, res = self.evaluate(assignment_result)
if best_val is None or val < best_val:
best_val = val
self.result = res

BIN
opt/param.pth Normal file

Binary file not shown.

314
opt/predictor.py Normal file
View File

@@ -0,0 +1,314 @@
from core.common import *
from opt.smm.basis import BaseOpt
import torch
class Predictor:
def __init__(self):
pass
@staticmethod
def training(self, params):
pass
@staticmethod
def testing(self, params):
pass
@staticmethod
def predict(self, cp_points, cp_nozzle, board_width=None, board_height=None):
pass
class Net(torch.nn.Module):
def __init__(self, input_size, hidden_size=1000, output_size=1):
super(Net, self).__init__()
self.fc1 = torch.nn.Linear(input_size, hidden_size)
self.relu = torch.nn.ReLU() # <20><><EFBFBD><EFBFBD><EEBAAF>
self.fc2 = torch.nn.Linear(hidden_size, hidden_size)
# self.relu1 = torch.nn.ReLU() # <20><><EFBFBD><EFBFBD><EEBAAF>
self.fc3 = torch.nn.Linear(hidden_size, output_size)
def forward(self, x):
x = self.fc1(x)
# x = self.relu(x)
x = self.fc2(x)
x = self.relu(x)
x = self.fc3(x)
return x
class NeuralPredictor(Predictor, BaseOpt):
def __init__(self):
super().__init__()
self.min_placement_points = 10
self.max_placement_points = 1000
self.max_component_types = 30
self.default_feeder_limit = 1
self.max_nozzle_types = 4
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
self.net = Net(input_size=self.get_feature(), output_size=1).to(self.device)
self.net_file = 'opt/param.pth'
try:
self.net.load_state_dict(torch.load(self.net_file, map_location=self.device))
except:
warnings.warn('the parameters of neural net model load failed', UserWarning)
def init_weights(self):
for m in self.net.modules():
if isinstance(m, torch.nn.Linear):
torch.nn.init.xavier_uniform_(m.weight)
torch.nn.init.zeros_(m.bias)
def subobjective(self, cp_points, cp_nozzle, config):
if len(cp_points.keys()) or sum(cp_points.values()) == 0:
return 0, 0, 0, 0
nozzle_heads, nozzle_points = defaultdict(int), defaultdict(int)
for idx, points in cp_points.items():
if points == 0:
continue
nozzle = cp_nozzle[idx]
nozzle_points[nozzle] += points
nozzle_heads[nozzle] = 1
anc_round_counter = 0
while sum(nozzle_heads.values()) != config.head_num:
max_cycle_nozzle = None
for nozzle, head_num in nozzle_heads.items():
if max_cycle_nozzle is None or nozzle_points[nozzle] / head_num > nozzle_points[max_cycle_nozzle] / \
nozzle_heads[max_cycle_nozzle]:
max_cycle_nozzle = nozzle
assert max_cycle_nozzle is not None
nozzle_heads[max_cycle_nozzle] += 1
head_nozzle_assignment, min_cost = None, None
# generate initial nozzle group
nozzle_group = []
# averagely assign for the same type of nozzles, and generate nozzle group
nozzle_points_cpy = copy.deepcopy(nozzle_points)
for nozzle, heads in nozzle_heads.items():
points = nozzle_points_cpy[nozzle] // heads
for _ in range(heads):
nozzle_group.append([nozzle, points])
nozzle_points_cpy[nozzle] -= heads * points
for idx, [nozzle, _] in enumerate(nozzle_group):
if nozzle_points_cpy[nozzle]:
nozzle_group[idx][1] += 1
nozzle_points_cpy[nozzle] -= 1
while True:
# assign nozzle group to each head
nozzle_group.sort(key=lambda x: -x[1])
tmp_head_nozzle_assignment = []
head_total_points = [0 for _ in range(config.head_num)]
for idx, nozzle_item in enumerate(nozzle_group):
if idx < config.head_num:
tmp_head_nozzle_assignment.append([nozzle_item.copy()])
head_total_points[idx] += nozzle_item[1]
else:
min_head = np.argmin(head_total_points)
tmp_head_nozzle_assignment[min_head].append(nozzle_item.copy())
head_total_points[min_head] += nozzle_item[1]
cost = config.cycle_time * max(head_total_points)
for head in range(config.head_num):
for cycle in range(len(tmp_head_nozzle_assignment[head])):
if cycle + 1 == len(tmp_head_nozzle_assignment[head]):
if tmp_head_nozzle_assignment[head][cycle][0] != tmp_head_nozzle_assignment[head][-1][0]:
cost += self.nozzle_change_weight
else:
if tmp_head_nozzle_assignment[head][cycle][0] != tmp_head_nozzle_assignment[head][cycle + 1][0]:
cost += self.nozzle_change_weight
while True:
min_head, max_head = np.argmin(head_total_points), np.argmax(head_total_points)
min_head_nozzle, max_head_nozzle = tmp_head_nozzle_assignment[min_head][-1][0], \
tmp_head_nozzle_assignment[max_head][-1][0]
if min_head_nozzle == max_head_nozzle:
break
min_head_list, max_head_list = [min_head], [max_head]
minmax_head_points = 0
for head in range(config.head_num):
if head in min_head_list or head in max_head_list:
minmax_head_points += head_total_points[head]
continue
# the max/min heads with the sum nozzle type
if tmp_head_nozzle_assignment[head][-1][0] == tmp_head_nozzle_assignment[min_head][-1][0]:
min_head_list.append(head)
minmax_head_points += head_total_points[head]
if tmp_head_nozzle_assignment[head][-1][0] == tmp_head_nozzle_assignment[max_head][-1][0]:
max_head_list.append(head)
minmax_head_points += head_total_points[head]
# todo: restriction of available nozzle
# the reduction of cycles is not offset the cost of nozzle change
average_points = minmax_head_points // (len(min_head_list) + len(max_head_list))
reminder_points = minmax_head_points % (len(min_head_list) + len(max_head_list))
max_cycle = average_points + (1 if reminder_points > 0 else 0)
for head in range(config.head_num):
if head in min_head_list or head in max_head_list:
continue
max_cycle = max(max_cycle, head_total_points[head])
nozzle_change_counter = 0
for head in min_head_list:
if tmp_head_nozzle_assignment[head][0] == tmp_head_nozzle_assignment[head][-1]:
nozzle_change_counter += 2
else:
nozzle_change_counter += 1
if self.cycle_weight * (max(head_total_points) - max_cycle) < self.nozzle_change_weight * nozzle_change_counter:
break
cost -= self.cycle_weight * (max(head_total_points) - max_cycle) - self.nozzle_change_weight * nozzle_change_counter
required_points = 0 # <20><><EFBFBD><EFBFBD>̯<EFBFBD><CCAF><EFBFBD><EFBFBD>װ<EFBFBD><D7B0><EFBFBD><EFBFBD><EFBFBD>϶<EFBFBD><CFB6><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
for head in min_head_list:
points = average_points - head_total_points[head]
tmp_head_nozzle_assignment[head].append([max_head_nozzle, points])
head_total_points[head] = average_points
required_points += points
for head in max_head_list:
tmp_head_nozzle_assignment[head][-1][1] -= required_points // len(max_head_list)
head_total_points[head] -= required_points // len(max_head_list)
required_points -= (required_points // len(max_head_list)) * len(max_head_list)
for head in max_head_list:
if required_points <= 0:
break
tmp_head_nozzle_assignment[head][-1][1] -= 1
head_total_points[head] -= 1
required_points -= 1
if min_cost is None or cost < min_cost:
min_cost = cost
head_nozzle_assignment = copy.deepcopy(tmp_head_nozzle_assignment)
else:
break
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD><D2BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
idx, nozzle = 0, nozzle_group[0][0]
for idx, [nozzle_, _] in enumerate(nozzle_group):
if nozzle_ != nozzle:
break
average_points, remainder_points = nozzle_points[nozzle] // (idx + 1), nozzle_points[nozzle] % (idx + 1)
nozzle_group.append([nozzle, 0])
for idx, [nozzle_, _] in enumerate(nozzle_group):
if nozzle_ == nozzle:
nozzle_group[idx][1] = average_points + (1 if remainder_points > 0 else 0)
remainder_points -= 1
cycle_counter, nozzle_change_counter = 0, 0
for head in range(config.head_num):
head_cycle_counter = 0
for cycle in range(len(head_nozzle_assignment[head])):
if cycle + 1 == len(head_nozzle_assignment[head]):
if head_nozzle_assignment[head][0][0] != head_nozzle_assignment[head][-1][0]:
nozzle_change_counter += 1
else:
if head_nozzle_assignment[head][cycle][0] != head_nozzle_assignment[head][cycle + 1][0]:
nozzle_change_counter += 1
head_cycle_counter += head_nozzle_assignment[head][cycle][1]
cycle_counter = max(cycle_counter, head_cycle_counter)
# === Ԫ<><D4AA>ʰȡ<CAB0><C8A1><EFBFBD><EFBFBD>Ԥ<EFBFBD><D4A4> ===
cp_info = []
for idx, points in cp_points.items():
if points == 0:
continue
feeder_limit = 1 # todo: <20><>ʱ<EFBFBD><CAB1><EFBFBD><EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD><D2BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
reminder_points = points % feeder_limit
for _ in range(feeder_limit):
cp_info.append([idx, points // feeder_limit + (1 if reminder_points > 0 else 0), cp_nozzle[idx]])
reminder_points -= 1
cp_info.sort(key=lambda x: -x[1])
nozzle_level, nozzle_counter = defaultdict(int), defaultdict(int)
level_points = defaultdict(int)
for info in cp_info:
nozzle = info[2]
if nozzle_counter[nozzle] and nozzle_counter[nozzle] % nozzle_heads[nozzle] == 0:
nozzle_level[nozzle] += 1
level = nozzle_level[nozzle]
level_points[level] = max(level_points[level], info[1])
nozzle_counter[nozzle] += 1
pickup_counter = sum(points for points in level_points.values())
return cycle_counter, nozzle_change_counter, anc_round_counter, pickup_counter
def encode(self, cp_points: defaultdict[str], cp_nozzle: defaultdict[str], board_width, board_height, config):
assert len(cp_points.keys()) == len(cp_nozzle.keys())
# === general info ===
total_points = sum(points for points in cp_points.values())
total_component_types, total_nozzle_types = len(cp_points.keys()), len(set(cp_nozzle.values()))
data = [total_points, total_component_types, total_nozzle_types]
data.extend([board_width, board_height])
# === heuristic info ===
cycle, nozzle_change, anc_move, pickup = self.subobjective(cp_points, cp_nozzle, config)
data.extend([cycle, nozzle_change, anc_move, pickup])
# === nozzle info ===
nozzle_points = defaultdict(int)
for cp_idx, nozzle in cp_nozzle.items():
nozzle_points[cp_nozzle[cp_idx]] += cp_points[cp_idx] # points for different nozzle type
nozzle_items = [[nozzle, points] for nozzle, points in nozzle_points.items()]
nozzle_items = sorted(nozzle_items, key=lambda x: x[1], reverse=True)
nz2idx = defaultdict(int)
nozzle_slice = [0 for _ in range(self.max_nozzle_types)]
for idx, [nozzle, points] in enumerate(nozzle_items):
nz2idx[nozzle] = idx if idx < self.max_nozzle_types else self.max_nozzle_types - 1
nozzle_slice[idx if idx < self.max_nozzle_types else -1] += points
data.extend(nozzle_slice)
# === part info ===
part_data_slice = defaultdict(list)
for idx in range(self.max_nozzle_types):
part_data_slice[idx] = []
cp_items = [[component, points] for component, points in cp_points.items()]
cp_items = sorted(cp_items, key=lambda x: (x[1], nz2idx[cp_nozzle[x[0]]] * 0.1 + x[1]), reverse=True)
for component, points in cp_items:
nozzle = cp_nozzle[component]
part_data_slice[nz2idx[nozzle]].append(points)
data_slice = [0 for _ in range(self.max_nozzle_types)]
for idx, part_list in part_data_slice.items():
data_slice[idx] = len(part_list)
data.extend(data_slice)
for idx in range(self.max_nozzle_types):
if len(part_data_slice[idx]) <= self.max_component_types:
part_data_slice[idx].extend([0 for _ in range(self.max_component_types - len(part_data_slice[idx]))])
else:
part_data_slice[idx] = part_data_slice[idx][:self.max_component_types]
data.extend(part_data_slice[idx])
return data
def get_feature(self):
return (self.max_component_types + 2) * self.max_nozzle_types + 5 + 4
def eval(self, cp_points, cp_nozzle, board_width, board_height, config):
encoding = np.array(self.encode(cp_points, cp_nozzle, board_width, board_height, config))
encoding = torch.from_numpy(encoding.reshape((-1, np.shape(encoding)[0]))).float().to(self.device)
return self.net(encoding)[0, 0].item()

172
opt/smm/aggregation.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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]

279
opt/utils.py Normal file
View File

@@ -0,0 +1,279 @@
from core.common import *
from data.type import *
head_rotary_velocity = 8e-5 # <20><>װͷR<CDB7><52><EFBFBD><EFBFBD>תʱ<D7AA><CAB1>
x_max_velocity, y_max_velocity = 1.4, 1.2
x_max_acceleration, y_max_acceleration = x_max_velocity / 0.079, y_max_velocity / 0.079
def axis_moving_time(distance, axis=0):
assert 0 <= axis <= 2
distance = abs(distance) * 1e-3
Lamax = x_max_velocity ** 2 / x_max_acceleration if axis == 0 else y_max_velocity ** 2 / y_max_acceleration
Tmax = x_max_velocity / x_max_acceleration if axis == 0 else y_max_velocity / y_max_acceleration
if axis == 0:
return 2 * math.sqrt(distance / x_max_acceleration) if distance < Lamax else 2 * Tmax + (
distance - Lamax) / x_max_velocity
elif axis == 1:
return 2 * math.sqrt(distance / y_max_acceleration) if distance < Lamax else 2 * Tmax + (
distance - Lamax) / y_max_velocity
elif axis == 2:
return abs((distance + 180) % 360 - 180) * head_rotary_velocity
return .0
def optimizer_result_hinter(config, part_data, opt_res, nozzle_hinter=False, part_hinter=False,
slot_hinter=False, place_hinter=False):
if nozzle_hinter:
columns = ['H{}'.format(i + 1) for i in range(config.head_num)] + ['cycle']
nozzle_assign = pd.DataFrame(columns=columns)
for cycle, components in enumerate(opt_res.part):
nozzle_assign_row = len(nozzle_assign)
nozzle_assign.loc[nozzle_assign_row, 'cycle'] = opt_res.cycle[cycle]
for head in range(config.head_num):
index = opt_res.part[cycle][head]
if index == -1:
nozzle_assign.loc[nozzle_assign_row, 'H{}'.format(head + 1)] = ''
else:
nozzle = part_data.loc[index]['nz']
nozzle_assign.loc[nozzle_assign_row, 'H{}'.format(head + 1)] = nozzle
for head in range(config.head_num):
if nozzle_assign_row == 0 or nozzle_assign.loc[nozzle_assign_row - 1, 'H{}'.format(head + 1)] != \
nozzle_assign.loc[nozzle_assign_row, 'H{}'.format(head + 1)]:
break
else:
nozzle_assign.loc[nozzle_assign_row - 1, 'cycle'] += nozzle_assign.loc[nozzle_assign_row, 'cycle']
nozzle_assign.drop([len(nozzle_assign) - 1], inplace=True)
print(nozzle_assign)
print('')
if part_hinter:
columns = ['H{}'.format(i + 1) for i in range(config.head_num)] + ['cycle']
part_assign = pd.DataFrame(columns=columns)
for cycle, components in enumerate(opt_res.part):
part_assign.loc[cycle, 'cycle'] = opt_res.cycle[cycle]
for head in range(config.head_num):
index = opt_res.part[cycle][head]
if index == -1:
part_assign.loc[cycle, 'H{}'.format(head + 1)] = ''
else:
# component_assign.loc[cycle, 'H{}'.format(head + 1)] = component_data.loc[index]['part']
part_assign.loc[cycle, 'H{}'.format(head + 1)] = 'C' + str(index)
print(part_assign)
print('')
if slot_hinter:
columns = ['H{}'.format(i + 1) for i in range(config.head_num)] + ['cycle']
slot_assign = pd.DataFrame(columns=columns)
for cycle, components in enumerate(opt_res.slot):
slot_assign.loc[cycle, 'cycle'] = opt_res.cycle[cycle]
for head in range(config.head_num):
slot = opt_res.slot[cycle][head]
if slot == -1:
slot_assign.loc[cycle, 'H{}'.format(head + 1)] = 'A'
else:
try:
slot_assign.loc[cycle, 'H{}'.format(head + 1)] = 'F{}'.format(
slot) if slot <= config.slot_num // 2 else 'R{}'.format(slot - config.head_num)
except:
print('')
print(slot_assign)
print('')
if place_hinter:
columns = ['H{}'.format(i + 1) for i in range(config.head_num)] + ['cycle']
place_assign = pd.DataFrame(columns=columns)
for cycle, _ in enumerate(opt_res.point):
place_assign.loc[cycle, 'cycle'] = 1
for head in range(config.head_num):
point = opt_res.point[cycle][head]
if point != -1:
place_assign.loc[cycle, 'H{}'.format(head + 1)] = 'P{}'.format(point)
else:
place_assign.loc[cycle, 'H{}'.format(head + 1)] = ''
headseq_assign = pd.DataFrame(columns=columns)
for cycle, headseq in enumerate(opt_res.sequence):
headseq_assign.loc[cycle, 'cycle'] = 1
for head in range(len(headseq)):
headseq_assign.loc[cycle, 'H{}'.format(head + 1)] = 'H{}'.format(headseq[head])
print(place_assign)
print(headseq_assign)
print('')
def evaluation(config: MachineConfig, part_data, step_data, opt_res: OptResult, hinter=False):
# === <20>Ż<EFBFBD><C5BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> ===
info = OptInfo()
# === У<><D0A3> ===
info.total_points = 0
for cycle, part in enumerate(opt_res.part):
for head, component in enumerate(part):
if component == -1:
continue
info.total_points += opt_res.cycle[cycle]
if info.total_points != len(step_data):
warning_info = 'the number of ' + str(info.total_points) + ' placement point(s) is not match with the PCB data. '
warnings.warn(warning_info, UserWarning)
interval_ratio = config.head_intv / config.slot_intv
if opt_res.point:
total_points = info.total_points
for placements in opt_res.point:
for placement in placements:
if placement == -1:
continue
total_points -= 1
if total_points != 0:
warnings.warn(
'the optimization result of component assignment result and placement result are not consistent. ',
UserWarning)
return OptInfo()
feeder_arrangement = defaultdict(set)
for cycle, feeder_slots in enumerate(opt_res.slot):
for head, slot in enumerate(feeder_slots):
if slot == -1:
continue
feeder_arrangement[opt_res.part[cycle][head]].add(slot)
info.total_components = len(feeder_arrangement.keys())
for part, data in part_data.iterrows():
if part in feeder_arrangement.keys() and data.fdn < len(feeder_arrangement[part]):
info = 'the number of arranged feeder of [' + data['part'] + '] exceeds the quantity limit'
warnings.warn(info, UserWarning)
cur_pos, next_pos = config.anc_pos, Point(0, 0) # <20><>װͷ<D7B0><CDB7>ǰλ<C7B0><CEBB>
# <20><>ʼ<EFBFBD><CABC><EFBFBD>׸<EFBFBD><D7B8><EFBFBD><EFBFBD>ڵ<EFBFBD><DAB5><EFBFBD><EFBFBD><EFBFBD>װ<EFBFBD><D7B0><EFBFBD><EFBFBD>Ϣ
nozzle_assigned = ['Empty' for _ in range(config.head_num)]
for head in range(config.head_num):
for cycle in range(len(opt_res.part)):
idx = opt_res.part[cycle][head]
if idx == -1:
continue
else:
nozzle_assigned[head] = part_data.loc[idx]['nz']
for cycle_set, _ in enumerate(opt_res.part):
floor_cycle, ceil_cycle = sum(opt_res.cycle[:cycle_set]), sum(opt_res.cycle[:(cycle_set + 1)])
for cycle in range(floor_cycle, ceil_cycle):
if sum(opt_res.part[cycle_set]) == -config.head_num:
continue
pick_slot, mount_pos, mount_angle = [], [], []
nozzle_pick_counter, nozzle_put_counter = 0, 0 # <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͳ<EFBFBD>ƣ<EFBFBD>ʰȡ/<2F><><EFBFBD>÷ֱ<C3B7><D6B1><EFBFBD>һ<EFBFBD>Σ<EFBFBD>
for head in range(config.head_num):
if opt_res.slot[cycle_set][head] != -1:
pick_slot.append(opt_res.slot[cycle_set][head] - interval_ratio * head)
if opt_res.part[cycle_set][head] == -1:
continue
nozzle = part_data.loc[opt_res.part[cycle_set][head]]['nz']
if nozzle != nozzle_assigned[head]:
if nozzle_assigned[head] != 'Empty':
nozzle_put_counter += 1
nozzle_pick_counter += 1
nozzle_assigned[head] = nozzle
# ANC<4E><43><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
if nozzle_pick_counter + nozzle_put_counter > 0:
next_pos = config.anc_pos
move_time = max(axis_moving_time(cur_pos.x - next_pos.x, 0),
axis_moving_time(cur_pos.y - next_pos.y, 1))
info.round_time += move_time
info.anc_round_counter += 1
info.total_distance += max(abs(cur_pos.x - next_pos.x), abs(cur_pos.y - next_pos.y))
cur_pos = next_pos
pick_slot = list(set(pick_slot))
pick_slot = sorted(pick_slot, reverse=True)
# ʰȡ·<C8A1><C2B7>(<28><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>)
for idx, slot in enumerate(pick_slot):
if slot < config.slot_num // 2:
next_pos = Point(config.slotf1_pos.x + config.slot_intv * (slot - 1), config.slotf1_pos.y)
else:
next_pos = Point(config.slotr1_pos.x - config.slot_intv * (config.slot_num - slot - 1), config.slotr1_pos.y)
info.operation_time += config.pick_time
info.pickup_counter += 1
move_time = max(axis_moving_time(cur_pos.x - next_pos.x, 0),
axis_moving_time(cur_pos.y - next_pos.y, 1))
if idx == 0:
info.round_time += move_time
else:
info.pickup_time += move_time
info.total_distance += max(abs(cur_pos.x - next_pos.x), abs(cur_pos.y - next_pos.y))
if slot != pick_slot[0]:
info.pickup_distance += max(abs(cur_pos.x - next_pos.x), abs(cur_pos.y - next_pos.y))
cur_pos = next_pos
# <20><>װ·<D7B0><C2B7>
if opt_res.point and opt_res.sequence:
head_angle = [0 for _ in range(config.head_num)]
for head in opt_res.sequence[cycle]:
index = opt_res.point[cycle][head]
if index == -1:
continue
mount_pos.append(Point(step_data.loc[index].x - head * config.head_intv + config.stopper_pos.x,
step_data.loc[index].y + config.stopper_pos.y))
head_angle[head] = step_data.loc[index]['r']
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>װ·<D7B0><C2B7>
for cntPoints in range(len(mount_pos) - 1):
info.place_distance += max(abs(mount_pos[cntPoints].x - mount_pos[cntPoints + 1].x),
abs(mount_pos[cntPoints].y - mount_pos[cntPoints + 1].y))
if mount_pos[0].x < mount_pos[-1].x:
mount_pos = reversed(mount_pos)
# <20><><EFBFBD><EFBFBD>R<EFBFBD><52>Ԥ<EFBFBD><D4A4>ת<EFBFBD><D7AA><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͬ<EFBFBD><CDAC><EFBFBD>Ƕ<EFBFBD>ת<EFBFBD><D7AA><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ķ<EFBFBD><C4B6><EFBFBD><EFBFBD><EFBFBD>װ<EFBFBD><D7B0>ʱ
info.operation_time += config.nozzle_install_time * nozzle_put_counter + \
config.nozzle_uninstall_time * nozzle_pick_counter
for idx, pos in enumerate(mount_pos):
info.operation_time += config.place_time
if idx == 0:
move_time = max(axis_moving_time(cur_pos.x - pos.x, 0),
axis_moving_time(cur_pos.y - pos.y, 1))
info.round_time += move_time
else:
cur_head = opt_res.sequence[cycle][idx]
side_head = cur_head - 1 if cur_head % 2 else cur_head + 1
if opt_res.sequence[cycle][idx - 1] != side_head:
move_time = max(axis_moving_time(cur_pos.x - pos.x, 0),
axis_moving_time(cur_pos.y - pos.y, 1))
else:
move_time = max(axis_moving_time(cur_pos.x - pos.x, 0),
axis_moving_time(cur_pos.y - pos.y, 1),
axis_moving_time(head_angle[cur_head] - head_angle[side_head], 2))
info.place_time += move_time
info.total_distance += max(abs(cur_pos.x - pos.x), abs(cur_pos.y - pos.y))
cur_pos = pos
info.nozzle_change_counter += nozzle_put_counter + nozzle_pick_counter
info.total_time = info.pickup_time + info.round_time + info.place_time + info.operation_time
info.cycle_counter = sum(opt_res.cycle)
if hinter:
optimizer_result_hinter(config, part_data, opt_res, part_hinter=True, nozzle_hinter=True, slot_hinter=True)
info.print()
return info