Files
smt-optimizer/optimizer_heuristic.py

430 lines
20 KiB
Python

import copy
import math
import random
import numpy as np
from base_optimizer.optimizer_common import *
from base_optimizer.optimizer_feederpriority import *
from base_optimizer.result_analysis import *
# TODO: nozzle tool available restriction
# TODO: consider with the PCB placement topology
def assembly_time_estimator(assignment_points, arranged_feeders, component_data):
nozzle_heads, nozzle_points = defaultdict(int), defaultdict(int)
for idx, points in enumerate(assignment_points):
if points == 0:
continue
nozzle_points[component_data.iloc[idx]['nz']] += points
nozzle_heads[component_data.iloc[idx]['nz']] = 1
while sum(nozzle_heads.values()) != max_head_index:
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(max_head_index)]
for idx, nozzle_item in enumerate(nozzle_group):
if idx < max_head_index:
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 = t_cycle * max(head_total_points)
for head in range(max_head_index):
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 += t_nozzle_change
else:
if tmp_head_nozzle_assignment[head][cycle][0] != tmp_head_nozzle_assignment[head][cycle + 1][0]:
cost += t_nozzle_change
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(max_head_index):
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(max_head_index):
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 t_cycle * (max(head_total_points) - max_cycle) < t_nozzle_change * nozzle_change_counter:
break
cost -= t_cycle * (max(head_total_points) - max_cycle) - t_nozzle_change * nozzle_change_counter
required_points = 0 # 待均摊的贴装点数较多的吸嘴类型
for head in min_head_list:
points = average_points - head_total_points[head]
tmp_head_nozzle_assignment[head].append([max_head_nozzle, points])
head_total_points[head] = average_points
required_points += points
for head in max_head_list:
tmp_head_nozzle_assignment[head][-1][1] -= required_points // len(max_head_list)
head_total_points[head] -= required_points // len(max_head_list)
required_points -= (required_points // len(max_head_list)) * len(max_head_list)
for head in max_head_list:
if required_points <= 0:
break
tmp_head_nozzle_assignment[head][-1][1] -= 1
head_total_points[head] -= 1
required_points -= 1
if min_cost is None or cost < min_cost:
min_cost = cost
head_nozzle_assignment = copy.deepcopy(tmp_head_nozzle_assignment)
else:
break
# 在吸嘴组中增加一个吸嘴
idx, nozzle = 0, nozzle_group[0][0]
for idx, [nozzle_, _] in enumerate(nozzle_group):
if nozzle_ != nozzle:
break
average_points, remainder_points = nozzle_points[nozzle] // (idx + 1), nozzle_points[nozzle] % (idx + 1)
nozzle_group.append([nozzle, 0])
for idx, [nozzle_, _] in enumerate(nozzle_group):
if nozzle_ == nozzle:
nozzle_group[idx][1] = average_points + (1 if remainder_points > 0 else 0)
remainder_points -= 1
cycle_counter, nozzle_change_counter = 0, 0
for head in range(max_head_index):
head_cycle_counter = 0
for cycle in range(len(head_nozzle_assignment[head])):
if cycle + 1 == len(head_nozzle_assignment[head]):
if head_nozzle_assignment[head][0][0] != head_nozzle_assignment[head][-1][0]:
nozzle_change_counter += 1
else:
if head_nozzle_assignment[head][cycle][0] != head_nozzle_assignment[head][cycle + 1][0]:
nozzle_change_counter += 1
head_cycle_counter += head_nozzle_assignment[head][cycle][1]
cycle_counter = max(cycle_counter, head_cycle_counter)
# === 元件拾取次数预估 ===
cp_info = []
for idx, points in enumerate(assignment_points):
if points == 0:
continue
feeder_limit = int(component_data.iloc[idx]['feeder-limit'])
reminder_points = points % feeder_limit
for _ in range(feeder_limit):
cp_info.append(
[idx, points // feeder_limit + (1 if reminder_points > 0 else 0), component_data.iloc[idx]['nz']])
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())
placement_counter = sum(assignment_points)
pickup_movement = 0
for points in assignment_points:
if points:
pickup_movement += 1
# 返回加权预估时间
return t_cycle * cycle_counter + t_nozzle_change * nozzle_change_counter + t_pick * pickup_counter + \
t_place * placement_counter + 0.1 * pickup_movement
def line_optimizer_heuristic(component_data, machine_number):
# the number of placement points, the number of available feeders, and nozzle type of component respectively
component_number = len(component_data)
nozzle_points = defaultdict(int) # the number of placements of nozzle
total_points = 0
for _, data in component_data.iterrows():
nozzle = data['nz']
nozzle_points[nozzle] += data['points']
total_points += data['point']
# first step: generate the initial solution with equalized workload
assignment_result = [[0 for _ in range(len(component_data))] for _ in range(machine_number)]
assignment_points = [0 for _ in range(machine_number)]
average_points = total_points // machine_number
weighted_points = list(
map(lambda _, data: data['points'] + 1e-5 * nozzle_points[data['nz']], component_data.iterrows()))
# for part_index in np.argsort(weighted_points)[::-1]:
for part_index in np.argsort(weighted_points)[::-1]:
if (total_points := component_data.iloc[part_index]['points']) == 0: # total placements for each component type
continue
machine_set = []
# define the machine that assigning placement points (considering the feeder limitation)
for machine_index in np.argsort(assignment_points):
if len(machine_set) >= component_data.iloc[part_index]['points'] or len(machine_set) >= \
component_data.iloc[part_index]['feeder-limit']:
break
machine_set.append(machine_index)
if weighted_points[part_index] + assignment_points[machine_index] < average_points:
break
# Allocation of mounting points to available machines according to the principle of equality
while total_points:
assign_machine = list(filter(lambda x: assignment_points[x] == min(assignment_points), machine_set))
if len(assign_machine) == len(machine_set):
# averagely assign point to all available machines
points = total_points // len(assign_machine)
for machine_index in machine_set:
assignment_points[machine_index] += points
assignment_result[machine_index][part_index] += points
total_points -= points * len(assign_machine)
for machine_index in machine_set:
if total_points == 0:
break
assignment_points[machine_index] += 1
assignment_result[machine_index][part_index] += 1
total_points -= 1
else:
# assigning placements to make up for the gap between the least and the second least
second_least_machine, second_least_machine_points = -1, max(assignment_points) + 1
for idx in machine_set:
if assignment_points[idx] < second_least_machine_points and assignment_points[idx] != min(
assignment_points):
second_least_machine_points = assignment_points[idx]
second_least_machine = idx
assert second_least_machine != -1
if len(assign_machine) * (second_least_machine_points - min(assignment_points)) < total_points:
min_points = min(assignment_points)
total_points -= len(assign_machine) * (second_least_machine_points - min_points)
for machine_index in assign_machine:
assignment_points[machine_index] += (second_least_machine_points - min_points)
assignment_result[machine_index][part_index] += (
second_least_machine_points - min_points)
else:
points = total_points // len(assign_machine)
for machine_index in assign_machine:
assignment_points[machine_index] += points
assignment_result[machine_index][part_index] += points
total_points -= points * len(assign_machine)
for machine_index in assign_machine:
if total_points == 0:
break
assignment_points[machine_index] += 1
assignment_result[machine_index][part_index] += 1
total_points -= 1
prev_max_assembly_time, prev_assignment_result = None, None
while True:
# second step: estimate the assembly time for each machine
arranged_feeders = defaultdict(list)
for machine_index in range(machine_number):
arranged_feeders[machine_index] = [0 for _ in range(len(component_data))]
for part_index in range(len(component_data)):
feeder_limit = component_data.iloc[part_index]['feeder-limit'] # 总体可用数
for machine_index in range(machine_number):
if assignment_result[machine_index][part_index] == 0:
continue
feeder_limit -= 1
# 已分配元件的机器至少安装1把供料器
arranged_feeders[machine_index][part_index] = 1
assert feeder_limit >= 0
for part_index in range(len(component_data)):
total_feeder_limit = component_data.iloc[part_index]['feeder-limit'] - sum(
[arranged_feeders[machine_index][part_index] for machine_index in range(machine_number)])
while total_feeder_limit > 0:
max_ratio, max_ratio_machine = None, -1
for machine_index in range(machine_number):
if assignment_result[machine_index][part_index] == 0:
continue
ratio = assignment_result[machine_index][part_index] / arranged_feeders[machine_index][part_index]
if max_ratio is None or ratio > max_ratio:
max_ratio, max_ratio_machine = ratio, machine_index
assert max_ratio_machine is not None
arranged_feeders[max_ratio_machine][part_index] += 1
total_feeder_limit -= 1
assembly_time, chip_per_hour = [], []
for machine_index in range(machine_number):
assembly_time.append(
assembly_time_estimator(assignment_result[machine_index], arranged_feeders[machine_index],
component_data))
chip_per_hour.append(sum(assignment_result[machine_index]) / (assembly_time[-1] + 1e-10))
max_assembly_time = max(assembly_time)
if prev_max_assembly_time and (prev_max_assembly_time < max_assembly_time or abs(
max_assembly_time - prev_max_assembly_time) < 1e-10):
if prev_max_assembly_time < max_assembly_time:
assignment_result = copy.deepcopy(prev_assignment_result)
break
else:
prev_max_assembly_time = max_assembly_time
prev_assignment_result = copy.deepcopy(assignment_result)
# third step: adjust the assignment results to reduce maximal assembly time among all machines
# ideal averagely assigned points
average_assign_points = [round(total_points * chip_per_hour[mi] / sum(chip_per_hour)) for mi in
range(machine_number)]
machine_index = 0
while total_points != sum(average_assign_points):
if total_points > sum(average_assign_points):
average_assign_points[machine_index] += 1
else:
average_assign_points[machine_index] -= 1
machine_index += 1
if machine_index >= machine_number:
machine_index = 0
# the placement points that need to be re-allocated
machine_reallocate_points = [sum(assignment_result[mi]) - average_assign_points[mi] for mi in
range(machine_number)]
# workload balance
# 1. balance the number of placements of the same type between different machines.
for demand_mi in range(machine_number):
if machine_reallocate_points[demand_mi] >= 0:
continue
supply_machine_list = [mi for mi in range(machine_number) if machine_reallocate_points[mi] > 0]
supply_machine_list.sort(key=lambda mi: -machine_reallocate_points[mi])
for supply_mi in supply_machine_list:
for part_index in range(len(component_data)):
if assignment_result[supply_mi][part_index] <= 0:
continue
reallocate_points = min(assignment_result[supply_mi][part_index],
-machine_reallocate_points[demand_mi])
# upper available feeder restrictions
tmp_reallocate_result = [assignment_result[mi][part_index] for mi in range(machine_number)]
tmp_reallocate_result[supply_mi] -= reallocate_points
tmp_reallocate_result[demand_mi] += reallocate_points
if sum(1 for pt in tmp_reallocate_result if pt > 0) > component_data.iloc[part_index]['feeder-limit']:
continue
assignment_result[supply_mi][part_index] -= reallocate_points
machine_reallocate_points[supply_mi] -= reallocate_points
assignment_result[demand_mi][part_index] += reallocate_points
machine_reallocate_points[demand_mi] += reallocate_points
if machine_reallocate_points[demand_mi] <= 0:
break
# 2. balance the number of placements of the different type between different machines.
cp_info = []
for part_index in range(len(component_data)):
for machine_index in range(machine_number):
if assignment_result[machine_index][part_index] == 0:
continue
cp_info.append([machine_index, part_index, assignment_result[machine_index][part_index]])
for machine_index in range(machine_number):
if machine_reallocate_points[machine_index] >= 0:
continue
filter_cp_info = [info for info in cp_info if
info[0] != machine_index and machine_reallocate_points[info[0]] > 0]
while True:
if len(filter_cp_info) == 0 or machine_reallocate_points[machine_index] >= 0:
break
# todo: 对同时拾取数的影响
filter_cp_info.sort(key=lambda x: x[2] + machine_reallocate_points[machine_index])
info = filter_cp_info[0]
filter_cp_info.remove(info)
if abs(machine_reallocate_points[machine_index]) + abs(machine_reallocate_points[info[0]]) < abs(
machine_reallocate_points[machine_index] + info[2]) + abs(
machine_reallocate_points[info[0]] - info[2]):
continue
cp_info.remove(info)
assignment_result[info[0]][info[1]] = 0
assignment_result[machine_index][info[1]] += info[2]
machine_reallocate_points[info[0]] -= info[2]
machine_reallocate_points[machine_index] += info[2]
return assignment_result