221 lines
8.6 KiB
Python
221 lines
8.6 KiB
Python
from base_optimizer.optimizer_common import *
|
|
|
|
from ortools.sat.python import cp_model
|
|
from collections import defaultdict
|
|
|
|
|
|
@timer_wrapper
|
|
def optimizer_aggregation(component_data, pcb_data):
|
|
# === phase 0: data preparation ===
|
|
M = 1000 # a sufficient large number
|
|
a, b = 1, 6 # coefficient
|
|
K, I, J, L = max_head_index, 0, 0, 0 # the maximum number of heads, component types, nozzle types and batch level
|
|
|
|
component_list, nozzle_list = defaultdict(int), defaultdict(int)
|
|
cpidx_2_part, nzidx_2_nozzle = {}, {}
|
|
for _, data in pcb_data.iterrows():
|
|
part = data['part']
|
|
if part not in cpidx_2_part.values():
|
|
cpidx_2_part[len(cpidx_2_part)] = part
|
|
|
|
component_list[part] += 1
|
|
|
|
idx = component_data[component_data['part'] == part].index.tolist()[0]
|
|
nozzle = component_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(component_list.keys()), len(nozzle_list.keys())
|
|
L = I + 1
|
|
HC = [[M for _ in range(J)] for _ in range(I)] # the handing class when component i is handled by nozzle type j
|
|
# represent the nozzle-component compatibility
|
|
for i in range(I):
|
|
for _, item in enumerate(cpidx_2_part.items()):
|
|
index, part = item
|
|
cp_idx = component_data[component_data['part'] == part].index.tolist()[0]
|
|
nozzle = component_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 ===
|
|
model = cp_model.CpModel()
|
|
solver = cp_model.CpSolver()
|
|
|
|
# === Decision Variables ===
|
|
# the number of components of type i that are placed by nozzle type j on placement head k
|
|
X = {}
|
|
for i in range(I):
|
|
for j in range(J):
|
|
for k in range(K):
|
|
X[i, j, k] = model.NewIntVar(0, component_list[cpidx_2_part[i]], 'X_{}_{}_{}'.format(i, j, k))
|
|
|
|
# the total number of nozzle changes on placement head k
|
|
N = {}
|
|
for k in range(K):
|
|
N[k] = model.NewIntVar(0, J, 'N_{}'.format(k))
|
|
|
|
# the largest workload of all placement heads
|
|
WL = model.NewIntVar(0, len(pcb_data), 'WL')
|
|
|
|
# whether batch Xijk is placed on level l
|
|
Z = {}
|
|
for i in range(I):
|
|
for j in range(J):
|
|
for l in range(L):
|
|
for k in range(K):
|
|
Z[i, j, l, k] = model.NewBoolVar('Z_{}_{}_{}_{}'.format(i, j, l, k))
|
|
|
|
# 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
|
|
D = {}
|
|
for l in range(L):
|
|
for k in range(K):
|
|
D[l, k] = model.NewIntVar(0, 2, 'D_{}_{}'.format(l, k))
|
|
|
|
D_abs = {}
|
|
for l in range(L):
|
|
for j in range(J):
|
|
for k in range(K):
|
|
D_abs[l, j, k] = model.NewIntVar(0, M, 'D_abs_{}_{}_{}'.format(l, j, k))
|
|
|
|
# == Objective function ===
|
|
model.Minimize(a * WL + b * sum(N[k] for k in range(K)))
|
|
|
|
# === Constraint ===
|
|
for i in range(I):
|
|
model.Add(sum(X[i, j, k] for j in range(J) for k in range(K)) == component_list[cpidx_2_part[i]])
|
|
|
|
for k in range(K):
|
|
model.Add(sum(X[i, j, k] for i in range(I) for j in range(J)) <= WL)
|
|
|
|
for i in range(I):
|
|
for j in range(J):
|
|
for k in range(K):
|
|
model.Add(X[i, j, k] <= M * sum(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):
|
|
model.Add(sum(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):
|
|
model.Add(sum(Z[i, j, l, k] for l in range(L)) <= X[i, j, k])
|
|
|
|
for k in range(K):
|
|
for l in range(L - 1):
|
|
model.Add(sum(Z[i, j, l, k] for j in range(J) for i in range(I)) >= sum(
|
|
Z[i, j, l + 1, k] for j in range(J) for i in range(I)))
|
|
|
|
for l in range(I):
|
|
for k in range(K):
|
|
model.Add(sum(Z[i, j, l, k] for i in range(I) for j in range(J)) <= 1)
|
|
|
|
for l in range(L - 1):
|
|
for j in range(J):
|
|
for k in range(K):
|
|
model.AddAbsEquality(D_abs[l, j, k],
|
|
sum(Z[i, j, l, k] for i in range(I)) - sum(Z[i, j, l + 1, k] for i in range(I)))
|
|
|
|
for k in range(K):
|
|
for l in range(L):
|
|
model.Add(D[l, k] == sum(D_abs[l, j, k] for j in range(J)))
|
|
|
|
for k in range(K):
|
|
model.Add(N[k] == sum(D[l, k] for l in range(L)) - 1)
|
|
|
|
for l in range(L):
|
|
for k in range(K):
|
|
model.Add(0 >= sum(HC[i][j] * Z[i, j, l, k] for i in range(I) for j in range(J)))
|
|
|
|
# === Main Process ===
|
|
component_result, cycle_result = [], []
|
|
feeder_slot_result, placement_result, head_sequence = [], [], []
|
|
solver.parameters.max_time_in_seconds = 20.0
|
|
|
|
status = solver.Solve(model)
|
|
if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE:
|
|
print('total cost = {}'.format(solver.ObjectiveValue()))
|
|
|
|
# convert cp model solution to standard output
|
|
model_cycle_result, model_component_result = [], []
|
|
for l in range(L):
|
|
model_component_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 solver.BooleanValue(Z[i, j, l, k]) != 0:
|
|
model_component_result[-1][k] = cpidx_2_part[i]
|
|
model_cycle_result[-1][k] = solver.Value(X[i, j, k])
|
|
|
|
# remove redundant term
|
|
if sum(model_cycle_result[-1]) == 0:
|
|
model_component_result.pop()
|
|
model_cycle_result.pop()
|
|
|
|
head_component_index = [0 for _ in range(max_head_index)]
|
|
while True:
|
|
head_cycle = []
|
|
for head, index in enumerate(head_component_index):
|
|
head_cycle.append(model_cycle_result[index][head])
|
|
|
|
if len([cycle for cycle in head_cycle if cycle > 0]) == 0:
|
|
break
|
|
|
|
component_result.append([None for _ in range(max_head_index)])
|
|
min_cycle = min([cycle for cycle in head_cycle if cycle > 0])
|
|
for head, index in enumerate(head_component_index):
|
|
if model_cycle_result[index][head] != 0:
|
|
component_result[-1][head] = model_component_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_component_index[head] += 1
|
|
|
|
cycle_result.append(min_cycle)
|
|
|
|
part_2_index = {}
|
|
for index, data in component_data.iterrows():
|
|
part_2_index[data['part']] = index
|
|
|
|
for cycle in range(len(component_result)):
|
|
for head in range(max_head_index):
|
|
part = component_result[cycle][head]
|
|
component_result[cycle][head] = -1 if part is None else part_2_index[part]
|
|
|
|
feeder_slot_result = feeder_assignment(component_data, pcb_data, component_result, cycle_result)
|
|
|
|
# === phase 2: heuristic method ===
|
|
mount_point_pos = defaultdict(list)
|
|
for pcb_idx, data in pcb_data.iterrows():
|
|
part = data['part']
|
|
part_index = component_data[component_data['part'] == part].index.tolist()[0]
|
|
mount_point_pos[part_index].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]):
|
|
placement_result.append([-1 for _ in range(max_head_index)])
|
|
for head in range(max_head_index):
|
|
if component_result[cycle_idx][head] == -1:
|
|
continue
|
|
index_ = component_result[cycle_idx][head]
|
|
|
|
placement_result[-1][head] = mount_point_pos[index_][-1][2]
|
|
mount_point_pos[index_].pop()
|
|
head_sequence.append(dynamic_programming_cycle_path(pcb_data, placement_result[-1], feeder_slot_result[cycle_idx]))
|
|
|
|
else:
|
|
warnings.warn('No solution found!', UserWarning)
|
|
|
|
return component_result, cycle_result, feeder_slot_result, placement_result, head_sequence
|