diff --git a/Mid_point_240805/Mid_point_240805.zip b/Mid_point_240805/Mid_point_240805.zip new file mode 100644 index 0000000..953b237 Binary files /dev/null and b/Mid_point_240805/Mid_point_240805.zip differ diff --git a/Mid_point_240805/custom_util.py b/Mid_point_240805/custom_util.py new file mode 100644 index 0000000..288d723 --- /dev/null +++ b/Mid_point_240805/custom_util.py @@ -0,0 +1,145 @@ +from util import * + +from itertools import permutations + +import math + +def custom_try_merging_bundles(K, dist_mat, all_orders, bundle1, bundle2, all_riders): + merged_orders = bundle1.shop_seq + bundle2.shop_seq + total_volume = get_total_volume(all_orders, merged_orders) + best_bundle = None + min_total_cost = float('inf') + for rider in all_riders: + if rider.available_number > 0 : + if total_volume <= rider.capa and len(merged_orders) <= 5: + for shop_pem in permutations(merged_orders): + for dlv_pem in permutations(merged_orders): + feasibility_check = test_route_feasibility(all_orders, rider, shop_pem, dlv_pem) + if feasibility_check == 0: # feasible! + total_dist = get_total_distance(K, dist_mat, shop_pem, dlv_pem) + temp_bundle = Bundle(all_orders, rider, list(shop_pem), list(dlv_pem), bundle1.total_volume + bundle2.total_volume, total_dist) + temp_bundle.update_cost() + + if temp_bundle.cost < min_total_cost: + min_total_cost = temp_bundle.cost + best_bundle = temp_bundle + + return best_bundle + +def custom_try_bundle_rider_changing(all_orders, dist_mat, bundle, all_riders): + old_rider = bundle.rider + best_shop_seq = None + best_dlv_seq = None + best_rider = None + min_total_cost = float('inf') + + for rider in all_riders: + if bundle.total_volume <= rider.capa: + orders = bundle.shop_seq + + for shop_pem in permutations(orders): + for dlv_pem in permutations(orders): + feasibility_check = test_route_feasibility(all_orders, rider, shop_pem, dlv_pem) + if feasibility_check == 0: # feasible! + total_dist = get_total_distance(len(all_orders), dist_mat, shop_pem, dlv_pem) + bundle.shop_seq = list(shop_pem) + bundle.dlv_seq = list(dlv_pem) + bundle.rider = rider + bundle.total_dist = total_dist + bundle.update_cost() + if bundle.cost < min_total_cost: + min_total_cost = bundle.cost + best_shop_seq = list(shop_pem) + best_dlv_seq = list(dlv_pem) + best_rider = rider + + if best_shop_seq and best_dlv_seq and best_rider: + # Note: in-place replacing! + bundle.shop_seq = best_shop_seq + bundle.dlv_seq = best_dlv_seq + bundle.rider = best_rider + bundle.total_dist = get_total_distance(len(all_orders), dist_mat, best_shop_seq, best_dlv_seq) + bundle.update_cost() # update the cost with the best sequences and rider + if old_rider != best_rider : + old_rider.available_number += 1 + best_rider.available_number -= 1 + return True + + return False + +def count_bundles(all_bundles): + counts = { + 'WALK': {'total': 0, 'lengths': {}}, + 'BIKE': {'total': 0, 'lengths': {}}, + 'CAR': {'total': 0, 'lengths': {}} + } + + # 각 요소를 순회하며 카운팅 + for bundle in all_bundles: + transport_type = bundle.rider.type + counts[transport_type]['total'] += 1 + + length = len(bundle.shop_seq) # `shop_seq`의 길이를 기준으로 한다. + if length not in counts[transport_type]['lengths']: + counts[transport_type]['lengths'][length] = 0 + counts[transport_type]['lengths'][length] += 1 + + # 결과 출력 + for transport_type, data in counts.items(): + total_count = data['total'] + print(f"{transport_type}: 총 {total_count}개") + for length, count in sorted(data['lengths'].items()): + print(f" 길이 {length}: {count}개") + +def avg_loc(all_orders, all_bundles): + bundles_index = [bundle.shop_seq for bundle in all_bundles] + bundles_avg_loc = [] + for index_seq in bundles_index: + ords_loc = [((order.shop_lat, order.shop_lon), (order.dlv_lat, order.dlv_lon)) for order in all_orders if order.id in index_seq] + bundle_loc = np.zeros((2,2)) + for shop_loc, dlv_loc in ords_loc: + bundle_loc[0] += np.array(shop_loc) + bundle_loc[1] += np.array(dlv_loc) + bundle_loc /= len(ords_loc) + bundles_avg_loc.append(bundle_loc) + + return bundles_avg_loc + +def haversine_distance(coord1, coord2): + lat1, lon1 = coord1 + lat2, lon2 = coord2 + R = 6371.0 # 지구의 반지름 (킬로미터) + + phi1 = math.radians(lat1) + phi2 = math.radians(lat2) + delta_phi = math.radians(lat2 - lat1) + delta_lambda = math.radians(lon2 - lon1) + + a = math.sin(delta_phi / 2.0)**2 + \ + math.cos(phi1) * math.cos(phi2) * \ + math.sin(delta_lambda / 2.0)**2 + c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) + + distance = R * c + return distance + +def dist_mat_by_loc(all_bundles_avg_loc): + + N = len(all_bundles_avg_loc) + bundles_dist_mat = np.zeros((2 * N, 2 * N)) + + for i in range(N): + for j in range(N): + # 픽업 지점 간 거리 + bundles_dist_mat[i][j] = haversine_distance(all_bundles_avg_loc[i][0], all_bundles_avg_loc[j][0]) + + # 배송 지점과 픽업 지점 간 거리 + bundles_dist_mat[i + N][j] = haversine_distance(all_bundles_avg_loc[i][1], all_bundles_avg_loc[j][0]) + + # 픽업 지점과 배송 지점 간 거리 + bundles_dist_mat[i][j + N] = haversine_distance(all_bundles_avg_loc[i][0], all_bundles_avg_loc[j][1]) + + # 배송 지점 간 거리 + bundles_dist_mat[i + N][j + N] = haversine_distance(all_bundles_avg_loc[i][1], all_bundles_avg_loc[j][1]) + + return bundles_dist_mat \ No newline at end of file diff --git a/Mid_point_240805/myalgorithm.py b/Mid_point_240805/myalgorithm.py new file mode 100644 index 0000000..0ebedf2 --- /dev/null +++ b/Mid_point_240805/myalgorithm.py @@ -0,0 +1,233 @@ +from util import * +from custom_util import * +import heapq + +def algorithm(K, all_orders, all_riders, dist_mat, timelimit=60): + + start_time = time.time() + + print('Code Start') + print('---------------------------------------------------------------------------------------') + + for r in all_riders: + r.T = np.round(dist_mat/r.speed + r.service_time) + + # A solution is a list of bundles + solution = [] + + #------------- Custom algorithm code starts from here --------------# + + walk_rider = None + for r in all_riders: + if r.type == 'WALK': + walk_rider = r + + car_rider = None + for r in all_riders: + if r.type == 'CAR': + car_rider = r + + all_bundles = [] + all_orders_tmp = all_orders.copy() + + heap = [] + cant_walk_list = [] + filt_ord = [] + + for rider in all_riders: + if rider.type == "WALK": + walk_speed = rider.speed #도보 속도 + walk_time_mat = np.round(dist_mat/rider.speed + rider.service_time) #도보 이동시간 + break + + for order in all_orders_tmp: + ready_time = order.order_time + order.cook_time + time_diff = order.deadline - ready_time #해당 order의 준비~데드라인의 시간 차이 + walk_time = walk_time_mat[order.id][order.id+K] #해당 order를 배송하기 위해 도보로 이동할 떄 필요한 시간 + + if time_diff < walk_time: #만약 주어진 시간이 모자라면 + cant_walk_list.append(order.id) #배달 불가능한 배달 번호 추가 + + fcut_orders = [order for order in all_orders_tmp if order.id not in cant_walk_list] #불가능한 orders를 첫번째 잘라내고 남은 orders + + for f_order in fcut_orders: + cant_merge_list = [f_order.id] + for s_order in fcut_orders: + #첫번째 order의 데드라인보다 두번째 order의 레디가 더 늦은 경우 cut + if f_order.deadline < s_order.order_time + s_order.cook_time or f_order.order_time + f_order.cook_time > s_order.deadline: + cant_merge_list.append(s_order.id) + # 두번째 order의 레디에 2픽업->1도착의 이동 시간을 더해서 첫번째 order의 데드라인보다 늦으면 cut + elif s_order.order_time + s_order.cook_time + walk_time_mat[s_order.id][f_order.id+K] > f_order.deadline: + cant_merge_list.append(s_order.id) + #print(f"{f_order.id}번째 order는 {cant_merge_list}와 결합 불가능") + scut_orders = [order for order in fcut_orders if order.id not in cant_merge_list] #불가능한 orders를 두번째 잘라내고 남은 orders + + #Cut 이후에 merging 작업 진행 + ord = f_order + new_bundle = Bundle(all_orders, walk_rider, [ord.id], [ord.id], ord.volume, dist_mat[ord.id, ord.id + K]) + + for s_ord in scut_orders: + new_bundle.shop_seq.append(s_ord.id) + new_bundle.dlv_seq.append(s_ord.id) + + for dlv_pem in permutations(new_bundle.dlv_seq): + feasibility_check = test_route_feasibility(all_orders, walk_rider, new_bundle.shop_seq, dlv_pem) + if feasibility_check == 0: # feasible! + cost_1 = walk_rider.calculate_cost(dist_mat[ord.id, ord.id + K]) + cost_2 = walk_rider.calculate_cost(dist_mat[s_ord.id, s_ord.id + K]) + fea_bundle = Bundle(all_orders, walk_rider, new_bundle.shop_seq[:], list(dlv_pem), + new_bundle.total_volume + s_ord.volume, get_total_distance(K, dist_mat, new_bundle.shop_seq, dlv_pem)) + fea_bundle.update_cost() + cost_new = fea_bundle.cost + cost_diff = cost_1 + cost_2 - cost_new + heapq.heappush(heap, [-cost_diff, fea_bundle.shop_seq, fea_bundle.dlv_seq, fea_bundle.total_volume, fea_bundle.total_dist]) + + new_bundle.shop_seq.pop() + new_bundle.dlv_seq.pop() + + while heap: + smallest = heapq.heappop(heap) + if all(item not in filt_ord for item in smallest[1]): + filt_ord.extend(smallest[1]) + good_bundle = Bundle(all_orders, walk_rider, smallest[1], smallest[2], smallest[3], smallest[4]) + all_bundles.append(good_bundle) + walk_rider.available_number -= 1 + + print(f'time: {time.time()-start_time}') + count_bundles(all_bundles) + print('---------------------------------------------------------------------------------------') + + # Update all_orders_tmp + all_orders_tmp = [order for order in all_orders_tmp if order.id not in filt_ord] + + # Create initial bundles using a greedy approach based on distance + while all_orders_tmp: + ord = all_orders_tmp.pop(0) + #일단 car_rider를 넣어 feasible한 bundle을 찾음 + new_bundle = Bundle(all_orders, car_rider, [ord.id], [ord.id], ord.volume, dist_mat[ord.id, ord.id + K]) + # Try to add the nearest orders to the current bundle + while True: + nearest_order = None + min_dist = float('inf') + for other_ord in all_orders_tmp: + dist = dist_mat[ord.id, other_ord.id] + dist_mat[ord.id + K, other_ord.id + K] + (dist_mat[ord.id, ord.id + K] + dist_mat[ord.id, other_ord.id + K] + dist_mat[ord.id + K, other_ord.id] + dist_mat[other_ord.id, other_ord.id + K])*0.25 + if dist < min_dist and new_bundle.total_volume + other_ord.volume <= car_rider.capa: + min_dist = dist + nearest_order = other_ord + + if nearest_order: + new_bundle.shop_seq.append(nearest_order.id) + new_bundle.dlv_seq.append(nearest_order.id) + new_bundle.total_volume += nearest_order.volume + new_bundle.total_dist += min_dist + + feasibility_check = test_route_feasibility(all_orders, car_rider, new_bundle.shop_seq, new_bundle.dlv_seq) + if feasibility_check == 0: # Feasible + car_rider.available_number -= 1 + all_orders_tmp.remove(nearest_order) + new_bundle.update_cost() + custom_try_bundle_rider_changing(all_orders, dist_mat, new_bundle, all_riders) + else: + # Remove last added order if not feasible + new_bundle.shop_seq.pop() + new_bundle.dlv_seq.pop() + new_bundle.total_volume -= nearest_order.volume + new_bundle.total_dist -= min_dist + break + + else: + break + + all_bundles.append(new_bundle) + best_obj = sum((bundle.cost for bundle in all_bundles)) / K + print(f'time: {time.time()-start_time}') + print(f'Best obj = {best_obj}') + + count_bundles(all_bundles) + print('---------------------------------------------------------------------------------------') + + #print(all_bundles) + + while True: + iter = 0 + len_one = len([bundle for bundle in all_bundles if len(bundle.shop_seq) == 1]) + print(f'number of len one bundles: {len_one}') + + #각 번들들의 픽업과 배송 지점의 평균 위치 계산 + all_bundles_avg_loc = avg_loc(all_orders, all_bundles) + + #평균 위치를 토대로 각 번들들 간의 거리 행렬 생성 + all_bundles_dist_mat = dist_mat_by_loc(all_bundles_avg_loc) + + N = len(all_bundles) + + dist_heap = [] + + for i in range(N): + for j in range(i+1, N): + dist = all_bundles_dist_mat[i, j] + all_bundles_dist_mat[i+N, j+N] + (all_bundles_dist_mat[i, i+N] + all_bundles_dist_mat[i, j+N] + all_bundles_dist_mat[i+N, j] + all_bundles_dist_mat[j, j+N])*0.25 + + heapq.heappush(dist_heap, [dist, i, len(all_bundles[i].shop_seq), j, len(all_bundles[j].shop_seq)]) + + if len_one >= 3: + len_one_heap = [item for item in dist_heap if item[2] == 1 or item[4] == 1] + heapq.heapify(len_one_heap) + dist_heap = len_one_heap + + while dist_heap: + smallest = heapq.heappop(dist_heap) + + bundle1 = all_bundles[smallest[1]] + bundle2 = all_bundles[smallest[3]] + new_bundle = custom_try_merging_bundles(K, dist_mat, all_orders, bundle1, bundle2, all_riders) + + if new_bundle is not None: + all_bundles.remove(bundle1) + bundle1.rider.available_number += 1 + + all_bundles.remove(bundle2) + bundle2.rider.available_number += 1 + + all_bundles.append(new_bundle) + new_bundle.rider.available_number -= 1 + + cur_obj = sum((bundle.cost for bundle in all_bundles)) / K + if cur_obj < best_obj: + best_obj = cur_obj + print(f'time: {time.time()-start_time}') + print(f'Best obj = {best_obj}') + count_bundles(all_bundles) + print('---------------------------------------------------------------------------------------') + break + + else: + iter += 1 + + if time.time() - start_time > timelimit: + break + + if time.time() - start_time > timelimit: + break + + cur_obj = sum((bundle.cost for bundle in all_bundles)) / K + if cur_obj < best_obj: + best_obj = cur_obj + print(f'time: {time.time()-start_time}') + print(f'Best obj = {best_obj}') + count_bundles(all_bundles) + print('---------------------------------------------------------------------------------------') + print(iter) + + # Solution is a list of bundle information + solution = [ + # rider type, shop_seq, dlv_seq + [bundle.rider.type, bundle.shop_seq, bundle.dlv_seq] + for bundle in all_bundles + ] + + #------------- End of custom algorithm code--------------# + + + + return solution + \ No newline at end of file diff --git a/Mid_point_240805/util.py b/Mid_point_240805/util.py new file mode 100644 index 0000000..7707e48 --- /dev/null +++ b/Mid_point_240805/util.py @@ -0,0 +1,470 @@ + +import json +import numpy as np +from itertools import permutations +import random +import time +import pprint + +import matplotlib.pyplot as plt + + +# Change history +# 2024/7/20 - Fixed a bug in solution_check() to make sure pickups == deliveries +# 2024/7/1 - Update total_dist for a new bundle as well in try_bundle_rider_changing() +# 2024/6/21 - Fixed a comment in Order.__init__() +# 2024/6/16 - Fixed a bug that does not set the bundle routes in try_bundle_rider_changing() +# 2024/5/17 - Fixed a bug in get_pd_times() + + +# 주문 class +class Order: + def __init__(self, order_info): + # [ORD_ID, ORD_TIME, SHOP_LAT, SHOP_LON, DLV_LAT, DLV_LON, COOK_TIME, VOL, DLV_DEADLINE] + self.id = order_info[0] + self.order_time = order_info[1] + self.shop_lat = order_info[2] + self.shop_lon = order_info[3] + self.dlv_lat = order_info[4] + self.dlv_lon = order_info[5] + self.cook_time = order_info[6] + self.volume = order_info[7] + self.deadline = order_info[8] + + self.ready_time = self.order_time + self.cook_time + + def __repr__(self) -> str: + return f'Order([{self.id}, {self.order_time}, {self.shop_lat}, {self.shop_lon}, {self.dlv_lat}, {self.dlv_lon}, {self.volume}, {self.cook_time}, {self.deadline}])' + +# 배달원 class +class Rider: + def __init__(self, rider_info): + # [type, speed, capa, var_cost, fixed_cost, service_time, available number] + self.type = rider_info[0] + self.speed = rider_info[1] + self.capa = rider_info[2] + self.var_cost = rider_info[3] + self.fixed_cost = rider_info[4] + self.service_time = rider_info[5] + self.available_number = rider_info[6] + + def __repr__(self) -> str: + return f'Rider([{self.type}, {self.speed}, {self.capa}, {self.var_cost}, {self.fixed_cost}, {self.service_time}, {self.available_number}])' + + # 주어진 거리에 대한 배달원 비용 계산 + # = 배달원별 고정비 + 이동거리로 계산된 변동비 + def calculate_cost(self, dist): + return self.fixed_cost + dist / 100.0 * self.var_cost + +# 묶음 주문 정보 +class Bundle: + def __init__(self, all_orders, rider, shop_seq, dlv_seq, total_volume, total_dist, feasible=True): + self.rider = rider + self.all_orders = all_orders + self.feasible = feasible + self.shop_seq = shop_seq + self.dlv_seq = dlv_seq + self.total_volume = total_volume + self.total_dist = total_dist + self.update_cost() + + # 묶음 주문의 비용 update + def update_cost(self): + self.cost = self.rider.calculate_cost(self.total_dist) + self.cost_per_ord = self.cost / len(self.shop_seq) + + + def __repr__(self) -> str: + return f'Bundle(all_orders, {self.rider.type}, {self.shop_seq}, {self.dlv_seq}, {self.total_volume}, {self.feasible})' + + +# 주문들의 총 부피 계산 +# shop_seq는 주문들의 pickup list +# Note: shop_seq는 주문 id의 list와 동일 +def get_total_volume(all_orders, shop_seq): + return sum(all_orders[k].volume for k in shop_seq) + +# shop_seq의 순서로 pickup하고 dlv_seq 순서로 배달할 때 총 거리 계산 +# Note: shop_seq 와 dlv_seq는 같은 주문 id들을 가져야 함. 즉, set(shop_seq) == seq(dlv_seq). (주문 id들의 순서는 바뀔 수 있음) +def get_total_distance(K, dist_mat, shop_seq, dlv_seq): + return sum(dist_mat[i,j] for (i,j) in zip(shop_seq[:-1], shop_seq[1:])) + dist_mat[shop_seq[-1], dlv_seq[0]+K] + sum(dist_mat[i+K,j+K] for (i,j) in zip(dlv_seq[:-1], dlv_seq[1:])) + +# shop_seq의 순서로 pickup하고 dlv_seq 순서로 배달할 때 pickup과 delivery시간을 반환 +# Note: shop_seq 와 dlv_seq는 같은 주문 id들을 가져야 함. 즉, set(shop_seq) == seq(dlv_seq). (주문 id들의 순서는 바뀔 수 있음) +def get_pd_times(all_orders, rider, shop_seq, dlv_seq): + + K = len(all_orders) + + pickup_times = {} + + k = shop_seq[0] + t = all_orders[k].order_time + all_orders[k].cook_time # order time + order cook time + pickup_times[k] = t + for next_k in shop_seq[1:]: + t = max(t+rider.T[k, next_k], all_orders[next_k].ready_time) # max{travel time + service time, ready time} + pickup_times[next_k] = t + + k = next_k + + dlv_times = {} + + k = dlv_seq[0] + t += rider.T[shop_seq[-1], k + K] + + dlv_times[k] = t + + for next_k in dlv_seq[1:]: + t += rider.T[k + K, next_k + K] + + dlv_times[next_k] = t + + k = next_k + + return pickup_times, dlv_times + +# shop_seq의 순서로 pickup하고 dlv_seq 순서로 배달원 rider가 배달할 때 묶음주문 제약 만족 여부 테스트 +# 모든 제약을 만족하면 0 반환 +# 용량 제약을 만족하지 못하면 -1 반환 +# 시간 제약을 만족하지 못하면 -2 반환 +# Note: shop_seq 와 dlv_seq는 같은 주문 id들을 가져야 함. 즉, set(shop_seq) == seq(dlv_seq). (주문 id들의 순서는 바뀔 수 있음) +def test_route_feasibility(all_orders, rider, shop_seq, dlv_seq): + + total_vol = get_total_volume(all_orders, shop_seq) + if total_vol > rider.capa: + # Capacity overflow! + return -1 # Capacity infeasibility + + pickup_times, dlv_times = get_pd_times(all_orders, rider, shop_seq, dlv_seq) + + for k, dlv_time in dlv_times.items(): + if dlv_time > all_orders[k].deadline: + return -2 # Deadline infeasibility + + return 0 + +# 두 개의 bundle이 제약을 만족하면서 묶일 수 있는지 테스트 +# 합쳐진 붂음배송 경로는 가능한 모든 pickup/delivery 조합을 확인 +# 두 개의 bundle을 합치는게 가능하면 합쳐진 새로운 bundle을 반환 +# 합치는게 불가능하면 None을 반환 +# Note: 이 때 배달원은 두 개의 주어진 bundle을 배달하는 배달원들만 후보로 테스트(주어진 bundle에 사용되지 않는 배달원을 묶는게 가능할 수 있음!) +# Note: 여러개의 배달원으로 묶는게 가능할 때 가장 먼저 가능한 배달원 기준으로 반환(비용을 고려하지 않음) +def try_merging_bundles(K, dist_mat, all_orders, bundle1, bundle2): + merged_orders = bundle1.shop_seq + bundle2.shop_seq + total_volume = get_total_volume(all_orders, merged_orders) + if bundle1.rider.type == bundle2.rider.type: + riders = [bundle1.rider] + else: + riders = [bundle1.rider, bundle2.rider] + for rider in riders: + # We skip the test if there are too many orders + if total_volume <= rider.capa and len(merged_orders) <= 5: + for shop_pem in permutations(merged_orders): + for dlv_pem in permutations(merged_orders): + feasibility_check = test_route_feasibility(all_orders, rider, shop_pem, dlv_pem) + if feasibility_check == 0: # feasible! + total_dist = get_total_distance(K, dist_mat, shop_pem, dlv_pem) + return Bundle(all_orders, rider, list(shop_pem), list(dlv_pem), bundle1.total_volume+bundle2.total_volume, total_dist) + + return None + +# 주어진 bundle의 배달원을 변경하는것이 가능한지 테스트 +# Note: 원래 bindle의 방문 순서가 최적이 아닐수도 있기 때문에 방문 순서 조합을 다시 확인함 +def try_bundle_rider_changing(all_orders, dist_mat, bundle, rider): + if bundle.rider.type != rider.type and bundle.total_volume <= rider.capa: + orders = bundle.shop_seq + for shop_pem in permutations(orders): + for dlv_pem in permutations(orders): + feasibility_check = test_route_feasibility(all_orders, rider, shop_pem, dlv_pem) + if feasibility_check == 0: # feasible! + # Note: in-place replacing! + bundle.shop_seq = list(shop_pem) + bundle.dlv_seq = list(dlv_pem) + bundle.rider = rider + bundle.total_dist = get_total_distance(len(all_orders), dist_mat, bundle.shop_seq, bundle.dlv_seq) + bundle.update_cost() + return True + + return False + +# 남아 있는 배달원 중에 *변동비*가 더 싼 배달원을 반환 +# 더 싼 배달원이 없으면 None 반환 +def get_cheaper_available_riders(all_riders, rider): + for r in all_riders: + if r.available_number > 0 and r.var_cost < rider.var_cost: + return r + + return None + +# 주어진 bundle list에서 임의로 두 개를 반환(중복없이) +def select_two_bundles(all_bundles): + bundle1, bundle2 = random.sample(all_bundles, 2) + return bundle1, bundle2 + +# 평균 비용(목적함수) 계산 +# = 총 비용 / 주문 수 +def get_avg_cost(all_orders, all_bundles): + return sum([bundle.cost for bundle in all_bundles]) / len(all_orders) + +# 주어진 bundle list에서 제출용 solution 포멧으로 반환 +def create_solution(prob_name, bundles): + sol = { + 'bundles' : [ + # rider type, shop_seq, dlv_seq + [bundle.rider.type, bundle.shop_seq, bundle.dlv_seq] + for bundle in bundles + ] + } + return sol + + + +# 주어진 solution의 feasibility를 테스트 +# Note: solution은 [배달원유형, pickup 순서, 배달 순서]의 list +# 반환하는 dict에는 solution이 feasible일 경우에는 평균 비용등의 정보가 추가적으로 포함됨 +# solution이 infeasible일 경우에는 그 이유가 'infeasibility' 항목(key)으로 반환 +def solution_check(K, all_orders, all_riders, dist_mat, solution): + + + total_cost = 0 + total_dist = 0 + + infeasibility = None + + if isinstance(solution, list): + + used_riders = { + 'CAR': 0, + 'WALK': 0, + 'BIKE': 0 + } + + all_deliveies = [] + + for bundle_info in solution: + if not isinstance(bundle_info, list) or len(bundle_info) != 3: + infeasibility = f'A bundle information must be a list of rider type, shop_seq, and dlv_seq! ===> {bundle_info}' + break + + rider_type = bundle_info[0] + shop_seq = bundle_info[1] + dlv_seq = bundle_info[2] + + # rider type check + if not rider_type in ['BIKE', 'WALK', 'CAR']: + infeasibility = f'Rider type must be either of BIKE, WALK, or CAR! ===> {rider_type}' + break + + # Get rider object + rider = None + for r in all_riders: + if r.type == rider_type: + rider = r + + # Increase used rider by 1 + used_riders[rider_type] += 1 + + # Pickup sequence check + if not isinstance(shop_seq, list): + infeasibility = f'The second bundle infomation must be a list of pickups! ===> {shop_seq}' + break + + for k in shop_seq: + if not isinstance(k, int) or k<0 or k>=K: + infeasibility = f'Pickup sequence has invalid order number: {k}' + break + + # Delivery sequence check + if not isinstance(dlv_seq, list): + infeasibility = f'The third bundle infomation must be a list of deliveries! ===> {dlv_seq}' + break + + for k in dlv_seq: + if not isinstance(k, int) or k<0 or k>=K: + infeasibility = f'Delivery sequence has invalid order number: {k}' + break + + # Pickup == delivery check + if set(shop_seq) != set(dlv_seq): + infeasibility = f'Sets of pickups and deliveries must be identical: {set(shop_seq)} != {set(dlv_seq)}' + break + + # Volume check + total_volume = get_total_volume(all_orders, shop_seq) + if total_volume > rider.capa: + infeasibility = f"Bundle's total volume exceeds the rider's capacity!: {total_volume} > {rider.capa}" + break + + # Deadline chaeck + pickup_times, dlv_times = get_pd_times(all_orders, rider.T, shop_seq, dlv_seq) + for k in dlv_seq: + all_deliveies.append(k) + if dlv_times[k] > all_orders[k].deadline: + infeasibility = f'Order {k} deadline is violated!: {dlv_times[k]} > {all_orders[k].deadline}' + break + + dist = get_total_distance(K, dist_mat, shop_seq, dlv_seq) + cost = rider.calculate_cost(dist) + + total_dist += dist + total_cost += cost + + if infeasibility is not None: + break + + + if infeasibility is None: + # Check used number of riders + for r in all_riders: + if r.available_number < used_riders[r.type]: + infeasibility = f'The number of used riders of type {r.type} exceeds the given available limit!' + break + + # Check deliveries + for k in range(K): + count = 0 + for k_sol in all_deliveies: + if k == k_sol: + count += 1 + + if count > 1: + infeasibility = f'Order {k} is assigned more than once! ===> {count} > 1' + break + elif count == 0: + infeasibility = f'Order {k} is NOT assigned!' + break + + else: + infeasibility = 'Solution must be a list of bundle information!' + + + if infeasibility is None: # All checks are passed! + checked_solution = { + 'total_cost': float(total_cost), + 'avg_cost': float(total_cost / K), + 'num_drivers': len(solution), + 'total_dist': int(total_dist), + 'feasible': True, + 'infeasibility': None, + 'bundles': solution + } + else: + print(infeasibility) + checked_solution = { + 'feasible': False, + 'infeasibility': infeasibility, + 'bundles': solution + } + + + return checked_solution + +# 주어진 solution의 경로를 visualize +def draw_route_solution(all_orders, solution=None): + + plt.subplots(figsize=(8, 8)) + node_size = 5 + + shop_x = [order.shop_lon for order in all_orders] + shop_y = [order.shop_lat for order in all_orders] + plt.scatter(shop_x, shop_y, c='red', s=node_size, label='SHOPS') + + dlv_x = [order.dlv_lon for order in all_orders] + dlv_y = [order.dlv_lat for order in all_orders] + plt.scatter(dlv_x, dlv_y, c='blue', s=node_size, label='DLVS') + + + if solution is not None: + + rider_idx = { + 'BIKE': 0, + 'CAR': 0, + 'WALK': 0 + } + + for bundle_info in solution['bundles']: + rider_type = bundle_info[0] + shop_seq = bundle_info[1] + dlv_seq = bundle_info[2] + + rider_idx[rider_type] += 1 + + route_color = 'gray' + if rider_type == 'BIKE': + route_color = 'green' + elif rider_type == 'WALK': + route_color = 'orange' + + route_x = [] + route_y = [] + for i in shop_seq: + route_x.append(all_orders[i].shop_lon) + route_y.append(all_orders[i].shop_lat) + + for i in dlv_seq: + route_x.append(all_orders[i].dlv_lon) + route_y.append(all_orders[i].dlv_lat) + + plt.plot(route_x, route_y, c=route_color, linewidth=0.5) + + plt.legend() + +# 주어진 soliution의 묶음 배송 방문 시간대를 visualize +def draw_bundle_solution(all_orders, all_riders, dist_mat, solution): + + plt.subplots(figsize=(6, len(solution['bundles']))) + + x_max = max([ord.deadline for ord in all_orders]) + + bundle_gap = 0.3 + y = 0.2 + + plt.yticks([]) + + for idx, bundle_info in enumerate(solution['bundles']): + rider_type = bundle_info[0] + shop_seq = bundle_info[1] + dlv_seq = bundle_info[2] + + rider = None + for r in all_riders: + if r.type == rider_type: + rider = r + + y_delta = 0.2 + + pickup_times, dlv_times = get_pd_times(all_orders, rider.T, shop_seq, dlv_seq) + + total_volume = 0 + for k in shop_seq: + total_volume += all_orders[k].volume # order volume + plt.hlines(y+y_delta/2, all_orders[k].ready_time, all_orders[k].deadline, colors='gray') + plt.vlines(all_orders[k].ready_time, y, y+y_delta, colors='gray') + plt.vlines(all_orders[k].deadline, y, y+y_delta, colors='gray') + + if total_volume > rider.capa: + plt.scatter(pickup_times[k], y+y_delta/2, c='red', zorder=100, marker='^', edgecolors='red', linewidth=0.5) + else: + plt.scatter(pickup_times[k], y+y_delta/2, c='green', zorder=100) + + if dlv_times[k] > all_orders[k].deadline: + plt.scatter(dlv_times[k], y+y_delta/2, c='red', zorder=100, marker='*', edgecolors='red', linewidth=0.5) + else: + plt.scatter(dlv_times[k], y+y_delta/2, c='orange', zorder=100) + + plt.text(all_orders[k].ready_time, y+y_delta/2, f'{all_orders[k].ready_time} ', ha='right', va='center', c='white', fontsize=6) + plt.text(all_orders[k].deadline, y+y_delta/2, f' {all_orders[k].deadline}', ha='left', va='center', c='white', fontsize=6) + + y += y_delta + + dist = get_total_distance(len(all_orders), dist_mat, shop_seq, dlv_seq) + cost = rider.calculate_cost(dist) + + plt.text(0, y+y_delta, f'{rider_type}: {shop_seq}-{dlv_seq}, tot_cost={cost}, tot_dist={dist}', ha='left', va='top', c='gray', fontsize=8) + y += bundle_gap + plt.hlines(y, 0, x_max, colors='gray', linestyles='dotted') + y += y_delta/2 + + + plt.ylim(0, y) + + diff --git a/Mix_gw_hy_240806/Mix_gw_hy_240806.zip b/Mix_gw_hy_240806/Mix_gw_hy_240806.zip new file mode 100644 index 0000000..5a3436f Binary files /dev/null and b/Mix_gw_hy_240806/Mix_gw_hy_240806.zip differ diff --git a/Mix_gw_hy_240806/custom_util.py b/Mix_gw_hy_240806/custom_util.py new file mode 100644 index 0000000..fc759c1 --- /dev/null +++ b/Mix_gw_hy_240806/custom_util.py @@ -0,0 +1,432 @@ +from util import * +import math +import itertools +from itertools import permutations, combinations + +# 2024-08-03 : all_partitions, find_nearest_bundles, custom_try_merging_multiple_bundles_by_distance, simulated_annealing 함수 추가 +# 2024-08-06 : find_nearest_triples, get_infeasible_pairs 함수 추가 + +def get_infeasible_pairs(all_orders, dist_mat, all_riders): + infeasible_pairs = set() + num_orders = len(all_orders) + + for i in range(num_orders): + for j in range(i + 1, num_orders): + if i != j: + order_i = all_orders[i] + order_j = all_orders[j] + feasible = False # Assume infeasible until proven otherwise + + for rider in all_riders: + if get_total_volume(all_orders, [i, j]) <= rider.capa: + for shop_seq in itertools.permutations([i, j]): + for dlv_seq in itertools.permutations([i, j]): + feasibility = test_route_feasibility(all_orders, rider, shop_seq, dlv_seq) + if feasibility == 0: + feasible = True # Found a feasible combination + break + if feasible: + break + if feasible: + break + + if not feasible: + infeasible_pairs.add((i, j)) + infeasible_pairs.add((j, i)) + + return infeasible_pairs + +def find_nearest_triples(dist_mat, all_bundles): + bundle_triples = [] + for i in range(len(all_bundles)): + for j in range(i + 1, len(all_bundles)): + for k in range(j + 1, len(all_bundles)): + if i!=j and j!=k and i!= k : + bundle1 = all_bundles[i] + bundle2 = all_bundles[j] + bundle3 = all_bundles[k] + min_dist = min( + dist_mat[bundle1.shop_seq[-1]][bundle2.shop_seq[0]], + dist_mat[bundle2.shop_seq[-1]][bundle3.shop_seq[0]], + dist_mat[bundle3.shop_seq[-1]][bundle1.shop_seq[0]] + ) + bundle_triples.append((min_dist, bundle1, bundle2, bundle3)) + bundle_triples = sorted(bundle_triples, key=lambda x: x[0]) + return bundle_triples + +def find_nearest_triples_with_middle(all_orders, all_bundles): + bundle_triples = [] + + all_bundles_avg_loc = avg_loc(all_orders, all_bundles) + all_bundles_dist_mat = dist_mat_by_loc(all_bundles_avg_loc) + N = len(all_bundles) + + for i in range(len(all_bundles)): + for j in range(i + 1, len(all_bundles)): + for k in range(j + 1, len(all_bundles)): + if i!=j and j!=k and i!= k : + dist_ij = all_bundles_dist_mat[i, j] + all_bundles_dist_mat[i+N, j+N] + (all_bundles_dist_mat[i, i+N] + all_bundles_dist_mat[i, j+N] + all_bundles_dist_mat[i+N, j] + all_bundles_dist_mat[j, j+N])*0.25 + dist_jk = all_bundles_dist_mat[j, k] + all_bundles_dist_mat[j+N, k+N] + (all_bundles_dist_mat[j, j+N] + all_bundles_dist_mat[j, k+N] + all_bundles_dist_mat[j+N, k] + all_bundles_dist_mat[k, k+N])*0.25 + dist_ki = all_bundles_dist_mat[k, i] + all_bundles_dist_mat[k+N, i+N] + (all_bundles_dist_mat[k, k+N] + all_bundles_dist_mat[k, i+N] + all_bundles_dist_mat[k+N, i] + all_bundles_dist_mat[i, i+N])*0.25 + total_dist = dist_ij + dist_jk + dist_ki + + bundle1 = all_bundles[i] + bundle2 = all_bundles[j] + bundle3 = all_bundles[k] + + bundle_triples.append((total_dist, bundle1, bundle2, bundle3)) + bundle_triples = sorted(bundle_triples, key=lambda x: x[0]) + return bundle_triples + +def draw_route_bundles(all_orders, all_bundles): + plt.subplots(figsize=(12, 12)) + node_size = 5 + + shop_x = [order.shop_lon for order in all_orders] + shop_y = [order.shop_lat for order in all_orders] + plt.scatter(shop_x, shop_y, c='red', s=node_size, label='SHOPS') + + dlv_x = [order.dlv_lon for order in all_orders] + dlv_y = [order.dlv_lat for order in all_orders] + plt.scatter(dlv_x, dlv_y, c='blue', s=node_size, label='DLVS') + + rider_idx = { + 'BIKE': 0, + 'CAR': 0, + 'WALK': 0 + } + + for bundle in all_bundles: + rider_type = bundle.rider.type + shop_seq = bundle.shop_seq + dlv_seq = bundle.dlv_seq + + rider_idx[rider_type] += 1 + + route_color = 'gray' + if rider_type == 'BIKE': + route_color = 'green' + elif rider_type == 'WALK': + route_color = 'orange' + + route_x = [] + route_y = [] + for i in shop_seq: + route_x.append(all_orders[i].shop_lon) + route_y.append(all_orders[i].shop_lat) + + for i in dlv_seq: + route_x.append(all_orders[i].dlv_lon) + route_y.append(all_orders[i].dlv_lat) + + plt.plot(route_x, route_y, c=route_color, linewidth=0.5) + + plt.legend() + plt.show() + +def count_bundles(all_bundles): + counts = { + 'WALK': {'total': 0, 'lengths': {}}, + 'BIKE': {'total': 0, 'lengths': {}}, + 'CAR': {'total': 0, 'lengths': {}} + } + + # 각 요소를 순회하며 카운팅 + for bundle in all_bundles: + transport_type = bundle.rider.type + counts[transport_type]['total'] += 1 + + length = len(bundle.shop_seq) # `shop_seq`의 길이를 기준으로 한다. + if length not in counts[transport_type]['lengths']: + counts[transport_type]['lengths'][length] = 0 + counts[transport_type]['lengths'][length] += 1 + + # 결과 출력 + for transport_type, data in counts.items(): + total_count = data['total'] + print(f"{transport_type}: 총 {total_count}개") + for length, count in sorted(data['lengths'].items()): + print(f" 길이 {length}: {count}개") + +def find_nearest_bundles(dist_mat, all_bundles): + nearest_pairs = [] + for i, bundle1 in enumerate(all_bundles): + min_dist = float('inf') + nearest_bundle = None + for j, bundle2 in enumerate(all_bundles): + if i != j: + dist = dist_mat[bundle1.shop_seq[-1], bundle2.shop_seq[0]] #단순하게 bundle1의 pickup 지점의 끝과, bundle2의 pickup 지점의 시작 사이의 거리를 + if dist < min_dist: + min_dist = dist + nearest_bundle = bundle2 + if nearest_bundle: + nearest_pairs.append((min_dist, bundle1, nearest_bundle)) + nearest_pairs = sorted(nearest_pairs, key=lambda x: x[0]) + #print(f'Nearest pairs: {nearest_pairs}') # 디버깅 출력 + return nearest_pairs + +def all_partitions(orders): + if len(orders) == 1: + yield [orders] + return + + first = orders[0] + for smaller in all_partitions(orders[1:]): + # insert `first` in each of the subpartition's subsets + for n, subset in enumerate(smaller): + yield smaller[:n] + [[first] + subset] + smaller[n + 1:] + # put `first` in its own subset + yield [[first]] + smaller + +def filter_partitions(orders, num_parts): + partitions = list(all_partitions(orders)) + return [p for p in partitions if len(p) == num_parts] + +def evaluate_bundles(bundles): + total_cost = sum(bundle.cost for bundle in bundles) + return total_cost + +def custom_try_merging_multiple_bundles_by_distance(K, dist_mat, all_orders, bundles, all_riders, infeasible_pairs): + merged_orders = list(set([order for bundle in bundles for order in bundle.shop_seq])) # 중복 주문 제거 + total_volume = get_total_volume(all_orders, merged_orders) + best_bundles = [] + min_total_cost = float('inf') + + def evaluate_bundles(bundles): + total_cost = sum(bundle.cost for bundle in bundles) + return total_cost + + def heuristic_merge(orders, rider): + # Heuristic approach to find a feasible sequence + shop_seq = [orders[0]] + dlv_seq = [orders[0]] + remaining_orders = set(orders[1:]) + while remaining_orders: + last_shop = shop_seq[-1] + nearest_order = min(remaining_orders, key=lambda x: dist_mat[last_shop][x]) + shop_seq.append(nearest_order) + dlv_seq.append(nearest_order) + remaining_orders.remove(nearest_order) + return shop_seq, dlv_seq + + # Try to merge all orders into one bundle + if len(merged_orders) <= 5: + for rider in all_riders: + if rider.available_number > 0 and total_volume <= rider.capa: + for shop_pem in itertools.permutations(merged_orders): + if any((shop_pem[i], shop_pem[j]) in infeasible_pairs for i in range(len(shop_pem)) for j in range(i + 1, len(shop_pem))): + continue + for dlv_pem in itertools.permutations(merged_orders): + feasibility_check = test_route_feasibility(all_orders, rider, shop_pem, dlv_pem) + if feasibility_check == 0: + total_dist = get_total_distance(K, dist_mat, shop_pem, dlv_pem) + temp_bundle = Bundle(all_orders, rider, list(shop_pem), list(dlv_pem), total_volume, total_dist) + temp_bundle.update_cost() + current_cost = evaluate_bundles([temp_bundle]) + if current_cost < min_total_cost: + min_total_cost = current_cost + best_bundles = [temp_bundle] + #print(f'one bundle : {best_bundles}') + + # Try to split into two bundles + if len(merged_orders) > 1: + two_partitions = filter_partitions(merged_orders, 2) + for partition in two_partitions: + if any((i, j) in infeasible_pairs for part in partition for i in part for j in part if i != j): + continue + candidate_bundles = [] + valid_partition = True + for part in partition: + part_volume = get_total_volume(all_orders, part) + part_bundle = None + for rider in all_riders: + if rider.available_number > 1 and part_volume <= rider.capa: + for shop_pem in itertools.permutations(part): + if any((shop_pem[i], shop_pem[j]) in infeasible_pairs for i in range(len(shop_pem)) for j in range(i + 1, len(shop_pem))): + continue + for dlv_pem in itertools.permutations(part): + feasibility_check = test_route_feasibility(all_orders, rider, shop_pem, dlv_pem) + if feasibility_check == 0: + total_dist = get_total_distance(K, dist_mat, shop_pem, dlv_pem) + part_bundle = Bundle(all_orders, rider, list(shop_pem), list(dlv_pem), part_volume, total_dist) + part_bundle.update_cost() + candidate_bundles.append(part_bundle) + break + if part_bundle: + break + if part_bundle: + break + if not part_bundle: + valid_partition = False + break + + if valid_partition and len(candidate_bundles) == 2: + current_cost = evaluate_bundles(candidate_bundles) + if current_cost < min_total_cost: + min_total_cost = current_cost + best_bundles = candidate_bundles + #print(f'two bundles : {best_bundles}') + + # Try to split into three bundles + if len(merged_orders) > 2: + three_partitions = filter_partitions(merged_orders, 3) + for partition in three_partitions: + if any((i, j) in infeasible_pairs for part in partition for i in part for j in part if i != j): + continue + candidate_bundles = [] + valid_partition = True + for part in partition: + part_volume = get_total_volume(all_orders, part) + part_bundle = None + for rider in all_riders: + if rider.available_number > 2 and part_volume <= rider.capa: + for shop_pem in itertools.permutations(part): + if any((shop_pem[i], shop_pem[j]) in infeasible_pairs for i in range(len(shop_pem)) for j in range(i + 1, len(shop_pem))): + continue + for dlv_pem in itertools.permutations(part): + feasibility_check = test_route_feasibility(all_orders, rider, shop_pem, dlv_pem) + if feasibility_check == 0: + total_dist = get_total_distance(K, dist_mat, shop_pem, dlv_pem) + part_bundle = Bundle(all_orders, rider, list(shop_pem), list(dlv_pem), part_volume, total_dist) + part_bundle.update_cost() + candidate_bundles.append(part_bundle) + break + if part_bundle: + break + if part_bundle: + break + if not part_bundle: + valid_partition = False + break + + if valid_partition and len(candidate_bundles) == 3: + current_cost = evaluate_bundles(candidate_bundles) + if current_cost < min_total_cost: + min_total_cost = current_cost + best_bundles = candidate_bundles + #print(f'three bundles : {best_bundles}') + + return best_bundles + +# Simulated Annealing을 사용하여 번들을 최적화하는 함수 +def simulated_annealing(all_bundles, K, dist_mat, all_orders, all_riders, timelimit=60, initial_temp=100, cooling_rate=0.99): + current_solution = all_bundles # 현재 해를 초기 해로 설정 + current_cost = sum(bundle.cost for bundle in all_bundles) / K # 초기 해의 비용 계산 + best_solution = current_solution[:] # 초기 해를 최적 해로 설정 + best_cost = current_cost # 초기 해의 비용을 최적 비용으로 설정 + temperature = initial_temp # 초기 온도 설정 + + if len(current_solution) > 1: # 번들이 2개 이상일 때만 수행 + i, j = random.sample(range(len(current_solution)), 2) # 무작위로 두 번들을 선택 + selected_bundles = [current_solution[i], current_solution[j]] + new_bundles = custom_try_merging_multiple_bundles_by_distance(K, dist_mat, all_orders, selected_bundles, all_riders) + + if new_bundles: # 새로운 번들이 생성되었을 때 + new_cost = sum(bundle.cost for bundle in new_bundles) / K + + # 새로운 비용이 현재 비용보다 적거나, 확률적으로 수락할 때 + if new_cost < current_cost or random.uniform(0, 1) < math.exp((current_cost - new_cost) / temperature): + current_solution = [bundle for k, bundle in enumerate(current_solution) if k != i and k != j] + new_bundles + current_cost = new_cost + + if current_cost < best_cost: # 새로운 해가 최적 해보다 나을 때 + best_solution = current_solution[:] + best_cost = current_cost + + temperature *= cooling_rate # 온도 감소 + + return best_solution, best_cost # 최적 해와 최적 비용 반환 + +def custom_try_bundle_rider_changing(all_orders, dist_mat, bundle, all_riders): + old_rider = bundle.rider + best_shop_seq = None + best_dlv_seq = None + best_rider = None + min_total_cost = float('inf') + + for rider in all_riders: + if bundle.total_volume <= rider.capa: + orders = bundle.shop_seq + + for shop_pem in permutations(orders): + for dlv_pem in permutations(orders): + feasibility_check = test_route_feasibility(all_orders, rider, shop_pem, dlv_pem) + if feasibility_check == 0: # feasible! + total_dist = get_total_distance(len(all_orders), dist_mat, shop_pem, dlv_pem) + bundle.shop_seq = list(shop_pem) + bundle.dlv_seq = list(dlv_pem) + bundle.rider = rider + bundle.total_dist = total_dist + bundle.update_cost() + if bundle.cost < min_total_cost: + min_total_cost = bundle.cost + best_shop_seq = list(shop_pem) + best_dlv_seq = list(dlv_pem) + best_rider = rider + + if best_shop_seq and best_dlv_seq and best_rider: + # Note: in-place replacing! + bundle.shop_seq = best_shop_seq + bundle.dlv_seq = best_dlv_seq + bundle.rider = best_rider + bundle.total_dist = get_total_distance(len(all_orders), dist_mat, best_shop_seq, best_dlv_seq) + bundle.update_cost() # update the cost with the best sequences and rider + if old_rider != best_rider : + old_rider.available_number += 1 + best_rider.available_number -= 1 + return True + + return False + +def avg_loc(all_orders, all_bundles): + bundles_index = [bundle.shop_seq for bundle in all_bundles] + bundles_avg_loc = [] + for index_seq in bundles_index: + ords_loc = [((order.shop_lat, order.shop_lon), (order.dlv_lat, order.dlv_lon)) for order in all_orders if order.id in index_seq] + bundle_loc = np.zeros((2,2)) + for shop_loc, dlv_loc in ords_loc: + bundle_loc[0] += np.array(shop_loc) + bundle_loc[1] += np.array(dlv_loc) + bundle_loc /= len(ords_loc) + bundles_avg_loc.append(bundle_loc) + + return bundles_avg_loc + +def haversine_distance(coord1, coord2): + lat1, lon1 = coord1 + lat2, lon2 = coord2 + R = 6371.0 # 지구의 반지름 (킬로미터) + + phi1 = math.radians(lat1) + phi2 = math.radians(lat2) + delta_phi = math.radians(lat2 - lat1) + delta_lambda = math.radians(lon2 - lon1) + + a = math.sin(delta_phi / 2.0)**2 + \ + math.cos(phi1) * math.cos(phi2) * \ + math.sin(delta_lambda / 2.0)**2 + c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) + + distance = R * c + return distance + +def dist_mat_by_loc(all_bundles_avg_loc): + + N = len(all_bundles_avg_loc) + bundles_dist_mat = np.zeros((2 * N, 2 * N)) + + for i in range(N): + for j in range(N): + # 픽업 지점 간 거리 + bundles_dist_mat[i][j] = haversine_distance(all_bundles_avg_loc[i][0], all_bundles_avg_loc[j][0]) + + # 배송 지점과 픽업 지점 간 거리 + bundles_dist_mat[i + N][j] = haversine_distance(all_bundles_avg_loc[i][1], all_bundles_avg_loc[j][0]) + + # 픽업 지점과 배송 지점 간 거리 + bundles_dist_mat[i][j + N] = haversine_distance(all_bundles_avg_loc[i][0], all_bundles_avg_loc[j][1]) + + # 배송 지점 간 거리 + bundles_dist_mat[i + N][j + N] = haversine_distance(all_bundles_avg_loc[i][1], all_bundles_avg_loc[j][1]) + + return bundles_dist_mat \ No newline at end of file diff --git a/Mix_gw_hy_240806/myalgorithm.py b/Mix_gw_hy_240806/myalgorithm.py new file mode 100644 index 0000000..748a6d1 --- /dev/null +++ b/Mix_gw_hy_240806/myalgorithm.py @@ -0,0 +1,263 @@ +import heapq +from util import * +from custom_util import * + +def algorithm(K, all_orders, all_riders, dist_mat, timelimit=60): + + start_time = time.time() + + print('Code Start') + print('---------------------------------------------------------------------------------------') + + for r in all_riders: + r.T = np.round(dist_mat / r.speed + r.service_time) + + # A solution is a list of bundles + solution = [] + + #------------- Custom algorithm code starts from here --------------# + + walk_rider = None + for r in all_riders: + if r.type == 'WALK': + walk_rider = r + + car_rider = None + for r in all_riders: + if r.type == 'CAR': + car_rider = r + + all_bundles = [] + all_orders_tmp = all_orders.copy() + infeasible_pairs = get_infeasible_pairs(all_orders, dist_mat, all_riders) + print(f'num of infeasible pairs : {len(infeasible_pairs)}') + heap = [] + cant_walk_list = [] + filt_ord = [] + + for rider in all_riders: + if rider.type == "WALK": + walk_speed = rider.speed #도보 속도 + walk_time_mat = np.round(dist_mat/rider.speed + rider.service_time) #도보 이동시간 + break + + for order in all_orders_tmp: + ready_time = order.order_time + order.cook_time + time_diff = order.deadline - ready_time #해당 order의 준비~데드라인의 시간 차이 + walk_time = walk_time_mat[order.id][order.id+K] #해당 order를 배송하기 위해 도보로 이동할 떄 필요한 시간 + + if time_diff < walk_time: #만약 주어진 시간이 모자라면 + cant_walk_list.append(order.id) #배달 불가능한 배달 번호 추가 + + fcut_orders = [order for order in all_orders_tmp if order.id not in cant_walk_list] #불가능한 orders를 첫번째 잘라내고 남은 orders + + for f_order in fcut_orders: + cant_merge_list = [f_order.id] + for s_order in fcut_orders: + #첫번째 order의 데드라인보다 두번째 order의 레디가 더 늦은 경우 cut + if f_order.deadline < s_order.order_time + s_order.cook_time or f_order.order_time + f_order.cook_time > s_order.deadline: + cant_merge_list.append(s_order.id) + # 두번째 order의 레디에 2픽업->1도착의 이동 시간을 더해서 첫번째 order의 데드라인보다 늦으면 cut + elif s_order.order_time + s_order.cook_time + walk_time_mat[s_order.id][f_order.id+K] > f_order.deadline: + cant_merge_list.append(s_order.id) + #print(f"{f_order.id}번째 order는 {cant_merge_list}와 결합 불가능") + scut_orders = [order for order in fcut_orders if order.id not in cant_merge_list] #불가능한 orders를 두번째 잘라내고 남은 orders + + #Cut 이후에 merging 작업 진행 + ord = f_order + new_bundle = Bundle(all_orders, walk_rider, [ord.id], [ord.id], ord.volume, dist_mat[ord.id, ord.id + K]) + + for s_ord in scut_orders: + new_bundle.shop_seq.append(s_ord.id) + new_bundle.dlv_seq.append(s_ord.id) + + for dlv_pem in permutations(new_bundle.dlv_seq): + feasibility_check = test_route_feasibility(all_orders, walk_rider, new_bundle.shop_seq, dlv_pem) + if feasibility_check == 0: # feasible! + cost_1 = walk_rider.calculate_cost(dist_mat[ord.id, ord.id + K]) + cost_2 = walk_rider.calculate_cost(dist_mat[s_ord.id, s_ord.id + K]) + fea_bundle = Bundle(all_orders, walk_rider, new_bundle.shop_seq[:], list(dlv_pem), + new_bundle.total_volume + s_ord.volume, get_total_distance(K, dist_mat, new_bundle.shop_seq, dlv_pem)) + fea_bundle.update_cost() + cost_new = fea_bundle.cost + cost_diff = cost_1 + cost_2 - cost_new + heapq.heappush(heap, [-cost_diff, fea_bundle.shop_seq, fea_bundle.dlv_seq, fea_bundle.total_volume, fea_bundle.total_dist]) + + new_bundle.shop_seq.pop() + new_bundle.dlv_seq.pop() + + while heap: + smallest = heapq.heappop(heap) + if all(item not in filt_ord for item in smallest[1]): + filt_ord.extend(smallest[1]) + good_bundle = Bundle(all_orders, walk_rider, smallest[1], smallest[2], smallest[3], smallest[4]) + all_bundles.append(good_bundle) + walk_rider.available_number -= 1 + + # Update all_orders_tmp + all_orders_tmp = [order for order in all_orders_tmp if order.id not in filt_ord] + time_1 = time.time() + count_bundles(all_bundles) + print('---------------------------------------------------------------------------------------') + # draw_route_bundles(all_orders, all_bundles) + # Create initial bundles using a greedy approach based on distance + + while all_orders_tmp: + ord = all_orders_tmp.pop(0) + # 일단 car_rider를 넣어 feasible한 bundle을 찾음 + new_bundle = Bundle(all_orders, car_rider, [ord.id], [ord.id], ord.volume, dist_mat[ord.id, ord.id + K]) + # Try to add the nearest orders to the current bundle + while True: + nearest_order = None + min_dist = float('inf') + for other_ord in all_orders_tmp: + dist = dist_mat[ord.id, other_ord.id] + dist_mat[ord.id + K, other_ord.id + K] + (dist_mat[ord.id, ord.id + K] + dist_mat[ord.id, other_ord.id + K] + dist_mat[ord.id + K, other_ord.id] + dist_mat[other_ord.id, other_ord.id + K])*0.25 + if dist < min_dist and new_bundle.total_volume + other_ord.volume <= car_rider.capa: + min_dist = dist + nearest_order = other_ord + + if nearest_order: + new_bundle.shop_seq.append(nearest_order.id) + new_bundle.dlv_seq.append(nearest_order.id) + new_bundle.total_volume += nearest_order.volume + new_bundle.total_dist += min_dist + + feasibility_check = test_route_feasibility(all_orders, car_rider, new_bundle.shop_seq, new_bundle.dlv_seq) + if feasibility_check == 0: # Feasible + car_rider.available_number -= 1 + all_orders_tmp.remove(nearest_order) + new_bundle.update_cost() + custom_try_bundle_rider_changing(all_orders, dist_mat, new_bundle, all_riders) + else: + # Remove last added order if not feasible + new_bundle.shop_seq.pop() + new_bundle.dlv_seq.pop() + new_bundle.total_volume -= nearest_order.volume + new_bundle.total_dist -= min_dist + break + + else: + break + + all_bundles.append(new_bundle) + best_obj = sum((bundle.cost for bundle in all_bundles)) / K + print(f'Initial best obj = {best_obj}') + #print(all_bundles) + count_bundles(all_bundles) + #draw_route_bundles(all_orders, all_bundles) + time_2 = time.time() + print(f'Elapsed time for initializing: {time_2 - time_1}') + print('---------------------------------------------------------------------------------------') + + #1개짜리인 bundle을 모아서 따로 리스트에 저장 + #각 bundle 별로 merge + cant_merge_list = [] + + while True : + no_walk_bundles = [bundle for bundle in all_bundles if bundle.rider.type != 'WALK'] + single_order_bundles = [(-bundle.cost, bundle) for bundle in all_bundles if len(bundle.shop_seq) == 1 and bundle.shop_seq[0] not in cant_merge_list] + if len(single_order_bundles) <= 3 : + break + heapq.heapify(single_order_bundles) + best_improvement = 0 + best_candidate = None + best_new_bundles = None + _, single_bundle = heapq.heappop(single_order_bundles) + #print(f'single_bundle : {single_bundle}') + Flag = False + for i in range(len(no_walk_bundles)): + if no_walk_bundles[i] != single_bundle : + candidate_bundles = [single_bundle, no_walk_bundles[i]] + #print(f'candidate_bundles : {candidate_bundles}') + new_bundles = custom_try_merging_multiple_bundles_by_distance(K, dist_mat, all_orders, candidate_bundles, all_riders, infeasible_pairs) + #print(f'new bundles : {new_bundles}') + if new_bundles: + #print(new_bundles) + current_cost = sum(bundle.cost for bundle in candidate_bundles) + new_cost = sum(bundle.cost for bundle in new_bundles) + improvement = current_cost - new_cost + #print(f'improvement : {improvement}') + if improvement > best_improvement: + best_improvement = improvement + best_candidate = candidate_bundles + best_new_bundles = new_bundles + Flag = True + if not Flag : + cant_merge_list.append(single_bundle.shop_seq[0]) + + if best_new_bundles: + #print(f'best_new_bundles : {best_new_bundles}') + #print(f'improvement : {best_improvement}') + for tmp_bundle in best_candidate : + #print(tmp_bundle) + tmp_bundle.rider.available_number += 1 + all_bundles.remove(tmp_bundle) + for tmp_bundle in best_new_bundles : + tmp_bundle.rider.available_number -= 1 + all_bundles.extend(best_new_bundles) + #count_bundles(all_bundles) + + cur_obj = sum((bundle.cost for bundle in all_bundles)) / K + if cur_obj < best_obj: + best_obj = cur_obj + print(f'Current best obj = {best_obj}') + count_bundles(all_bundles) + time_3 = time.time() + print(f'Elapsed time for merging single bundles: {time_3 - time_2}') + #draw_route_bundles(all_orders, all_bundles) + #print(all_bundles) + print('---------------------------------------------------------------------------------------') + + #--------------- + iter = 0 + while time.time() - start_time < timelimit and len(all_bundles) > 1: + no_walk_bundles = [bundle for bundle in all_bundles if bundle.rider.type != 'WALK'] + iter += 1 + num_less_two = len([bundle for bundle in no_walk_bundles if len(bundle.shop_seq) <= 2]) + if num_less_two >= 20: + limited_bundles = [bundle for bundle in no_walk_bundles if len(bundle.shop_seq) <= 2] + else : + limited_bundles = [bundle for bundle in no_walk_bundles if len(bundle.shop_seq) <= 3] + nearest_triples = find_nearest_triples_with_middle(all_orders, limited_bundles) + improved = False + for min_dist, bundle1, bundle2, bundle3 in nearest_triples: + new_bundles = custom_try_merging_multiple_bundles_by_distance(K, dist_mat, all_orders, [bundle1, bundle2, bundle3], all_riders, infeasible_pairs) + if new_bundles: + improvement = sum(bundle.cost for bundle in [bundle1, bundle2, bundle3]) - sum(bundle.cost for bundle in new_bundles) + if improvement > 0: + bundle1.rider.available_number += 1 + bundle2.rider.available_number += 1 + bundle3.rider.available_number += 1 + all_bundles.remove(bundle1) + all_bundles.remove(bundle2) + all_bundles.remove(bundle3) + for new_bundle in new_bundles: + new_bundle.rider.available_number -= 1 + all_bundles.append(new_bundle) + cur_obj = sum((bundle.cost for bundle in all_bundles)) / K + if cur_obj < best_obj: + best_obj = cur_obj + print(f'Current best obj = {best_obj}') + count_bundles(all_bundles) + print(f'Current time: {time.time() - start_time}') + print('---------------------------------------------------------------------------------------') + #draw_route_bundles(all_orders, all_bundles) + improved = True + break + if time.time() - start_time >= timelimit: + break + if not improved: + break + + cur_obj = sum((bundle.cost for bundle in all_bundles)) / K + if cur_obj < best_obj: + best_obj = cur_obj + print(f'Final best obj = {best_obj}') + count_bundles(all_bundles) + solution = [ + [bundle.rider.type, bundle.shop_seq, bundle.dlv_seq] + for bundle in all_bundles + ] + print(solution) + + return solution diff --git a/Mix_gw_hy_240806/util.py b/Mix_gw_hy_240806/util.py new file mode 100644 index 0000000..7707e48 --- /dev/null +++ b/Mix_gw_hy_240806/util.py @@ -0,0 +1,470 @@ + +import json +import numpy as np +from itertools import permutations +import random +import time +import pprint + +import matplotlib.pyplot as plt + + +# Change history +# 2024/7/20 - Fixed a bug in solution_check() to make sure pickups == deliveries +# 2024/7/1 - Update total_dist for a new bundle as well in try_bundle_rider_changing() +# 2024/6/21 - Fixed a comment in Order.__init__() +# 2024/6/16 - Fixed a bug that does not set the bundle routes in try_bundle_rider_changing() +# 2024/5/17 - Fixed a bug in get_pd_times() + + +# 주문 class +class Order: + def __init__(self, order_info): + # [ORD_ID, ORD_TIME, SHOP_LAT, SHOP_LON, DLV_LAT, DLV_LON, COOK_TIME, VOL, DLV_DEADLINE] + self.id = order_info[0] + self.order_time = order_info[1] + self.shop_lat = order_info[2] + self.shop_lon = order_info[3] + self.dlv_lat = order_info[4] + self.dlv_lon = order_info[5] + self.cook_time = order_info[6] + self.volume = order_info[7] + self.deadline = order_info[8] + + self.ready_time = self.order_time + self.cook_time + + def __repr__(self) -> str: + return f'Order([{self.id}, {self.order_time}, {self.shop_lat}, {self.shop_lon}, {self.dlv_lat}, {self.dlv_lon}, {self.volume}, {self.cook_time}, {self.deadline}])' + +# 배달원 class +class Rider: + def __init__(self, rider_info): + # [type, speed, capa, var_cost, fixed_cost, service_time, available number] + self.type = rider_info[0] + self.speed = rider_info[1] + self.capa = rider_info[2] + self.var_cost = rider_info[3] + self.fixed_cost = rider_info[4] + self.service_time = rider_info[5] + self.available_number = rider_info[6] + + def __repr__(self) -> str: + return f'Rider([{self.type}, {self.speed}, {self.capa}, {self.var_cost}, {self.fixed_cost}, {self.service_time}, {self.available_number}])' + + # 주어진 거리에 대한 배달원 비용 계산 + # = 배달원별 고정비 + 이동거리로 계산된 변동비 + def calculate_cost(self, dist): + return self.fixed_cost + dist / 100.0 * self.var_cost + +# 묶음 주문 정보 +class Bundle: + def __init__(self, all_orders, rider, shop_seq, dlv_seq, total_volume, total_dist, feasible=True): + self.rider = rider + self.all_orders = all_orders + self.feasible = feasible + self.shop_seq = shop_seq + self.dlv_seq = dlv_seq + self.total_volume = total_volume + self.total_dist = total_dist + self.update_cost() + + # 묶음 주문의 비용 update + def update_cost(self): + self.cost = self.rider.calculate_cost(self.total_dist) + self.cost_per_ord = self.cost / len(self.shop_seq) + + + def __repr__(self) -> str: + return f'Bundle(all_orders, {self.rider.type}, {self.shop_seq}, {self.dlv_seq}, {self.total_volume}, {self.feasible})' + + +# 주문들의 총 부피 계산 +# shop_seq는 주문들의 pickup list +# Note: shop_seq는 주문 id의 list와 동일 +def get_total_volume(all_orders, shop_seq): + return sum(all_orders[k].volume for k in shop_seq) + +# shop_seq의 순서로 pickup하고 dlv_seq 순서로 배달할 때 총 거리 계산 +# Note: shop_seq 와 dlv_seq는 같은 주문 id들을 가져야 함. 즉, set(shop_seq) == seq(dlv_seq). (주문 id들의 순서는 바뀔 수 있음) +def get_total_distance(K, dist_mat, shop_seq, dlv_seq): + return sum(dist_mat[i,j] for (i,j) in zip(shop_seq[:-1], shop_seq[1:])) + dist_mat[shop_seq[-1], dlv_seq[0]+K] + sum(dist_mat[i+K,j+K] for (i,j) in zip(dlv_seq[:-1], dlv_seq[1:])) + +# shop_seq의 순서로 pickup하고 dlv_seq 순서로 배달할 때 pickup과 delivery시간을 반환 +# Note: shop_seq 와 dlv_seq는 같은 주문 id들을 가져야 함. 즉, set(shop_seq) == seq(dlv_seq). (주문 id들의 순서는 바뀔 수 있음) +def get_pd_times(all_orders, rider, shop_seq, dlv_seq): + + K = len(all_orders) + + pickup_times = {} + + k = shop_seq[0] + t = all_orders[k].order_time + all_orders[k].cook_time # order time + order cook time + pickup_times[k] = t + for next_k in shop_seq[1:]: + t = max(t+rider.T[k, next_k], all_orders[next_k].ready_time) # max{travel time + service time, ready time} + pickup_times[next_k] = t + + k = next_k + + dlv_times = {} + + k = dlv_seq[0] + t += rider.T[shop_seq[-1], k + K] + + dlv_times[k] = t + + for next_k in dlv_seq[1:]: + t += rider.T[k + K, next_k + K] + + dlv_times[next_k] = t + + k = next_k + + return pickup_times, dlv_times + +# shop_seq의 순서로 pickup하고 dlv_seq 순서로 배달원 rider가 배달할 때 묶음주문 제약 만족 여부 테스트 +# 모든 제약을 만족하면 0 반환 +# 용량 제약을 만족하지 못하면 -1 반환 +# 시간 제약을 만족하지 못하면 -2 반환 +# Note: shop_seq 와 dlv_seq는 같은 주문 id들을 가져야 함. 즉, set(shop_seq) == seq(dlv_seq). (주문 id들의 순서는 바뀔 수 있음) +def test_route_feasibility(all_orders, rider, shop_seq, dlv_seq): + + total_vol = get_total_volume(all_orders, shop_seq) + if total_vol > rider.capa: + # Capacity overflow! + return -1 # Capacity infeasibility + + pickup_times, dlv_times = get_pd_times(all_orders, rider, shop_seq, dlv_seq) + + for k, dlv_time in dlv_times.items(): + if dlv_time > all_orders[k].deadline: + return -2 # Deadline infeasibility + + return 0 + +# 두 개의 bundle이 제약을 만족하면서 묶일 수 있는지 테스트 +# 합쳐진 붂음배송 경로는 가능한 모든 pickup/delivery 조합을 확인 +# 두 개의 bundle을 합치는게 가능하면 합쳐진 새로운 bundle을 반환 +# 합치는게 불가능하면 None을 반환 +# Note: 이 때 배달원은 두 개의 주어진 bundle을 배달하는 배달원들만 후보로 테스트(주어진 bundle에 사용되지 않는 배달원을 묶는게 가능할 수 있음!) +# Note: 여러개의 배달원으로 묶는게 가능할 때 가장 먼저 가능한 배달원 기준으로 반환(비용을 고려하지 않음) +def try_merging_bundles(K, dist_mat, all_orders, bundle1, bundle2): + merged_orders = bundle1.shop_seq + bundle2.shop_seq + total_volume = get_total_volume(all_orders, merged_orders) + if bundle1.rider.type == bundle2.rider.type: + riders = [bundle1.rider] + else: + riders = [bundle1.rider, bundle2.rider] + for rider in riders: + # We skip the test if there are too many orders + if total_volume <= rider.capa and len(merged_orders) <= 5: + for shop_pem in permutations(merged_orders): + for dlv_pem in permutations(merged_orders): + feasibility_check = test_route_feasibility(all_orders, rider, shop_pem, dlv_pem) + if feasibility_check == 0: # feasible! + total_dist = get_total_distance(K, dist_mat, shop_pem, dlv_pem) + return Bundle(all_orders, rider, list(shop_pem), list(dlv_pem), bundle1.total_volume+bundle2.total_volume, total_dist) + + return None + +# 주어진 bundle의 배달원을 변경하는것이 가능한지 테스트 +# Note: 원래 bindle의 방문 순서가 최적이 아닐수도 있기 때문에 방문 순서 조합을 다시 확인함 +def try_bundle_rider_changing(all_orders, dist_mat, bundle, rider): + if bundle.rider.type != rider.type and bundle.total_volume <= rider.capa: + orders = bundle.shop_seq + for shop_pem in permutations(orders): + for dlv_pem in permutations(orders): + feasibility_check = test_route_feasibility(all_orders, rider, shop_pem, dlv_pem) + if feasibility_check == 0: # feasible! + # Note: in-place replacing! + bundle.shop_seq = list(shop_pem) + bundle.dlv_seq = list(dlv_pem) + bundle.rider = rider + bundle.total_dist = get_total_distance(len(all_orders), dist_mat, bundle.shop_seq, bundle.dlv_seq) + bundle.update_cost() + return True + + return False + +# 남아 있는 배달원 중에 *변동비*가 더 싼 배달원을 반환 +# 더 싼 배달원이 없으면 None 반환 +def get_cheaper_available_riders(all_riders, rider): + for r in all_riders: + if r.available_number > 0 and r.var_cost < rider.var_cost: + return r + + return None + +# 주어진 bundle list에서 임의로 두 개를 반환(중복없이) +def select_two_bundles(all_bundles): + bundle1, bundle2 = random.sample(all_bundles, 2) + return bundle1, bundle2 + +# 평균 비용(목적함수) 계산 +# = 총 비용 / 주문 수 +def get_avg_cost(all_orders, all_bundles): + return sum([bundle.cost for bundle in all_bundles]) / len(all_orders) + +# 주어진 bundle list에서 제출용 solution 포멧으로 반환 +def create_solution(prob_name, bundles): + sol = { + 'bundles' : [ + # rider type, shop_seq, dlv_seq + [bundle.rider.type, bundle.shop_seq, bundle.dlv_seq] + for bundle in bundles + ] + } + return sol + + + +# 주어진 solution의 feasibility를 테스트 +# Note: solution은 [배달원유형, pickup 순서, 배달 순서]의 list +# 반환하는 dict에는 solution이 feasible일 경우에는 평균 비용등의 정보가 추가적으로 포함됨 +# solution이 infeasible일 경우에는 그 이유가 'infeasibility' 항목(key)으로 반환 +def solution_check(K, all_orders, all_riders, dist_mat, solution): + + + total_cost = 0 + total_dist = 0 + + infeasibility = None + + if isinstance(solution, list): + + used_riders = { + 'CAR': 0, + 'WALK': 0, + 'BIKE': 0 + } + + all_deliveies = [] + + for bundle_info in solution: + if not isinstance(bundle_info, list) or len(bundle_info) != 3: + infeasibility = f'A bundle information must be a list of rider type, shop_seq, and dlv_seq! ===> {bundle_info}' + break + + rider_type = bundle_info[0] + shop_seq = bundle_info[1] + dlv_seq = bundle_info[2] + + # rider type check + if not rider_type in ['BIKE', 'WALK', 'CAR']: + infeasibility = f'Rider type must be either of BIKE, WALK, or CAR! ===> {rider_type}' + break + + # Get rider object + rider = None + for r in all_riders: + if r.type == rider_type: + rider = r + + # Increase used rider by 1 + used_riders[rider_type] += 1 + + # Pickup sequence check + if not isinstance(shop_seq, list): + infeasibility = f'The second bundle infomation must be a list of pickups! ===> {shop_seq}' + break + + for k in shop_seq: + if not isinstance(k, int) or k<0 or k>=K: + infeasibility = f'Pickup sequence has invalid order number: {k}' + break + + # Delivery sequence check + if not isinstance(dlv_seq, list): + infeasibility = f'The third bundle infomation must be a list of deliveries! ===> {dlv_seq}' + break + + for k in dlv_seq: + if not isinstance(k, int) or k<0 or k>=K: + infeasibility = f'Delivery sequence has invalid order number: {k}' + break + + # Pickup == delivery check + if set(shop_seq) != set(dlv_seq): + infeasibility = f'Sets of pickups and deliveries must be identical: {set(shop_seq)} != {set(dlv_seq)}' + break + + # Volume check + total_volume = get_total_volume(all_orders, shop_seq) + if total_volume > rider.capa: + infeasibility = f"Bundle's total volume exceeds the rider's capacity!: {total_volume} > {rider.capa}" + break + + # Deadline chaeck + pickup_times, dlv_times = get_pd_times(all_orders, rider.T, shop_seq, dlv_seq) + for k in dlv_seq: + all_deliveies.append(k) + if dlv_times[k] > all_orders[k].deadline: + infeasibility = f'Order {k} deadline is violated!: {dlv_times[k]} > {all_orders[k].deadline}' + break + + dist = get_total_distance(K, dist_mat, shop_seq, dlv_seq) + cost = rider.calculate_cost(dist) + + total_dist += dist + total_cost += cost + + if infeasibility is not None: + break + + + if infeasibility is None: + # Check used number of riders + for r in all_riders: + if r.available_number < used_riders[r.type]: + infeasibility = f'The number of used riders of type {r.type} exceeds the given available limit!' + break + + # Check deliveries + for k in range(K): + count = 0 + for k_sol in all_deliveies: + if k == k_sol: + count += 1 + + if count > 1: + infeasibility = f'Order {k} is assigned more than once! ===> {count} > 1' + break + elif count == 0: + infeasibility = f'Order {k} is NOT assigned!' + break + + else: + infeasibility = 'Solution must be a list of bundle information!' + + + if infeasibility is None: # All checks are passed! + checked_solution = { + 'total_cost': float(total_cost), + 'avg_cost': float(total_cost / K), + 'num_drivers': len(solution), + 'total_dist': int(total_dist), + 'feasible': True, + 'infeasibility': None, + 'bundles': solution + } + else: + print(infeasibility) + checked_solution = { + 'feasible': False, + 'infeasibility': infeasibility, + 'bundles': solution + } + + + return checked_solution + +# 주어진 solution의 경로를 visualize +def draw_route_solution(all_orders, solution=None): + + plt.subplots(figsize=(8, 8)) + node_size = 5 + + shop_x = [order.shop_lon for order in all_orders] + shop_y = [order.shop_lat for order in all_orders] + plt.scatter(shop_x, shop_y, c='red', s=node_size, label='SHOPS') + + dlv_x = [order.dlv_lon for order in all_orders] + dlv_y = [order.dlv_lat for order in all_orders] + plt.scatter(dlv_x, dlv_y, c='blue', s=node_size, label='DLVS') + + + if solution is not None: + + rider_idx = { + 'BIKE': 0, + 'CAR': 0, + 'WALK': 0 + } + + for bundle_info in solution['bundles']: + rider_type = bundle_info[0] + shop_seq = bundle_info[1] + dlv_seq = bundle_info[2] + + rider_idx[rider_type] += 1 + + route_color = 'gray' + if rider_type == 'BIKE': + route_color = 'green' + elif rider_type == 'WALK': + route_color = 'orange' + + route_x = [] + route_y = [] + for i in shop_seq: + route_x.append(all_orders[i].shop_lon) + route_y.append(all_orders[i].shop_lat) + + for i in dlv_seq: + route_x.append(all_orders[i].dlv_lon) + route_y.append(all_orders[i].dlv_lat) + + plt.plot(route_x, route_y, c=route_color, linewidth=0.5) + + plt.legend() + +# 주어진 soliution의 묶음 배송 방문 시간대를 visualize +def draw_bundle_solution(all_orders, all_riders, dist_mat, solution): + + plt.subplots(figsize=(6, len(solution['bundles']))) + + x_max = max([ord.deadline for ord in all_orders]) + + bundle_gap = 0.3 + y = 0.2 + + plt.yticks([]) + + for idx, bundle_info in enumerate(solution['bundles']): + rider_type = bundle_info[0] + shop_seq = bundle_info[1] + dlv_seq = bundle_info[2] + + rider = None + for r in all_riders: + if r.type == rider_type: + rider = r + + y_delta = 0.2 + + pickup_times, dlv_times = get_pd_times(all_orders, rider.T, shop_seq, dlv_seq) + + total_volume = 0 + for k in shop_seq: + total_volume += all_orders[k].volume # order volume + plt.hlines(y+y_delta/2, all_orders[k].ready_time, all_orders[k].deadline, colors='gray') + plt.vlines(all_orders[k].ready_time, y, y+y_delta, colors='gray') + plt.vlines(all_orders[k].deadline, y, y+y_delta, colors='gray') + + if total_volume > rider.capa: + plt.scatter(pickup_times[k], y+y_delta/2, c='red', zorder=100, marker='^', edgecolors='red', linewidth=0.5) + else: + plt.scatter(pickup_times[k], y+y_delta/2, c='green', zorder=100) + + if dlv_times[k] > all_orders[k].deadline: + plt.scatter(dlv_times[k], y+y_delta/2, c='red', zorder=100, marker='*', edgecolors='red', linewidth=0.5) + else: + plt.scatter(dlv_times[k], y+y_delta/2, c='orange', zorder=100) + + plt.text(all_orders[k].ready_time, y+y_delta/2, f'{all_orders[k].ready_time} ', ha='right', va='center', c='white', fontsize=6) + plt.text(all_orders[k].deadline, y+y_delta/2, f' {all_orders[k].deadline}', ha='left', va='center', c='white', fontsize=6) + + y += y_delta + + dist = get_total_distance(len(all_orders), dist_mat, shop_seq, dlv_seq) + cost = rider.calculate_cost(dist) + + plt.text(0, y+y_delta, f'{rider_type}: {shop_seq}-{dlv_seq}, tot_cost={cost}, tot_dist={dist}', ha='left', va='top', c='gray', fontsize=8) + y += bundle_gap + plt.hlines(y, 0, x_max, colors='gray', linestyles='dotted') + y += y_delta/2 + + + plt.ylim(0, y) + +