def test_redistribute_to_one_route(self): r1 = [0, 1, 4, 6, 0] rd1_redistribute = RouteData(r1, route_l(r1, self.D), route_d(r1, self.d), None) r2 = [0, 2, 3, 5, 7, 0] rd2_recieving = RouteData(r2, route_l(r2, self.D), route_d(r2, self.d), None) result = do_redistribute_move(rd1_redistribute, rd2_recieving, self.D, strategy=LSOPT.FIRST_ACCEPT) self.assertEqual( len(result), 3, "The redistribute operator should return the redistributed and the new combined routes and the delta" ) self.assertEqual( result[1].route, [0, 2, 3, 5, 7, 1, 4, 6, 0], "It should be possible to insert all and they sould be appended to the route" ) result = do_redistribute_move(rd1_redistribute, rd2_recieving, self.D, strategy=LSOPT.BEST_ACCEPT) self.assertEqual( len(result), 3, "The redistribute operator should return the redistributed and the new combined routes and the delta" ) self.assertEqual(result[1].route, [0, 1, 2, 3, 4, 5, 6, 7, 0], "It should be possible to insert all")
def delete_heuristic(routes, D, C, d, L): """ If several routes can be deleted, apply the operation that improves the solution most (or worsens it it least). """ improvement_found = False best_insertion_delta = float("inf") best_insertion_routes = None for rd1_idx, rd1 in enumerate(routes): if rd1.is_empty(): continue # do not allow empty routes other_routes = [r for r in routes if (not r.is_empty() and r != rd1)] result = do_redistribute_move(rd1, other_routes, D, d, C, L, best_delta=best_insertion_delta, recombination_level=0) delta = result[-1] if (delta is not None) and (delta < best_insertion_delta): improvement_found = True best_insertion_delta = result[-1] best_insertion_routes = result[:-1] if improvement_found: routes[:] = best_insertion_routes return improvement_found
def _refine_solution(sol, D, d, C, L, minimize_K): # refine until stuck at a local optima local_optima_reached = False while not local_optima_reached: sol = without_empty_routes(sol) if not minimize_K: sol.append(0) #make sure there is an empty route to move the pt to # improve with relocation and keep 2-optimal sol = do_local_search([do_1point_move, do_2opt_move], sol, D, d, C, L, LSOPT.BEST_ACCEPT) # try to redistribute the route with smallest demand sol = without_empty_routes(sol) routes = RouteData.from_solution(sol, D, d) min_rd = min(routes, key=lambda rd: rd.demand) routes.remove(min_rd) if not minimize_K: routes.append(RouteData()) if __debug__: log( DEBUG, "Applying do_redistribute_move on %s (%.2f)" % (str(sol), objf(sol, D))) redisribute_result = do_redistribute_move( min_rd, routes, D, d, C, L, strategy=LSOPT.FIRST_ACCEPT, #Note: Mole and Jameson do not specify exactly # how the redistribution is done (how many # different combinations are tried). # Increase the recombination_level if the # for more agressive and time consuming search # for redistributing the customers on other # routes. recombination_level=0) redisribute_delta = redisribute_result[-1] if (redisribute_delta is not None) and\ (minimize_K or redisribute_delta<0.0): updated_sol = RouteData.to_solution(redisribute_result[:-1]) if __debug__: log(DEBUG - 1, ("Improved from %s (%.2f) to %s (%.2f)" % (sol, objf(sol, D), updated_sol, objf(updated_sol, D))) + "using inter route heuristic do_redistribute_move\n") sol = updated_sol else: local_optima_reached = True if __debug__: log(DEBUG - 1, "No move with do_redistribute_move\n") return sol
def test_redistribute_to_two_routes(self): r1 = [0, 1, 4, 6, 0] rd1_redistribute = RouteData(r1, route_l(r1, self.D), route_d(r1, self.d), None) r2 = [0, 2, 3, 0] rd2_recieving = RouteData(r2, route_l(r2, self.D), route_d(r2, self.d), None) r3 = [0, 7, 5, 0] rd3_recieving = RouteData(r3, route_l(r3, self.D), route_d(r3, self.d), None) # depending on how the recombinations are made, the results differ FI = LSOPT.FIRST_ACCEPT BE = LSOPT.BEST_ACCEPT tests = [(0, "FIRST", FI, ([0, 2, 3, 1, 4, 0], [0, 7, 5, 6, 0])), (0, "BEST", BE, ([0, 1, 2, 3, 4, 0], [0, 7, 6, 5, 0])), (1, "FIRST", FI, ([0, 2, 3, 4, 1, 0], [0, 7, 5, 6, 0])), (1, "BEST", BE, ([0, 1, 2, 3, 4, 0], [0, 7, 6, 5, 0])), (2, "FIRST", FI, ([0, 2, 3, 1, 4, 0], [0, 7, 5, 6, 0])), (2, "BEST", BE, ([0, 1, 2, 3, 4, 0], [0, 7, 6, 5, 0])), (3, "FIRST", FI, ([0, 2, 3, 4, 1, 0], [0, 7, 5, 6, 0])), (3, "BEST", BE, ([0, 1, 2, 3, 4, 0], [0, 7, 6, 5, 0]))] for recombination_level, strategy_name, strategy, target_result in tests: result = do_redistribute_move( rd1_redistribute, [rd2_recieving, rd3_recieving], self.D, C=4.0, d=self.d, strategy=strategy, recombination_level=recombination_level) #print("QUALITY", strategy_name, "/", recombination_level, "delta", result[-1]) self.assertEqual( len(result), 4, "The redistribute operator should return the redistributed and the new combined routes and the delta" ) self.assertEqual( result[1].route, target_result[0], "It should be possible to insert all and they sould be appended to the route on recombination level %d with strategy %s" % (recombination_level, strategy_name)) self.assertEqual( result[2].route, target_result[1], "It should be possible to insert all and they sould be appended to the route on recombination level %d with strategy %s" % (recombination_level, strategy_name))
def test_redistribute_does_not_fit(self): demands = [0.0, 1, 1, 1, 1, 1, 1, 1] r1 = [0, 1, 2, 0] rd1_redistribute = RouteData(r1, route_l(r1, self.D), route_d(r1, demands), None) r2 = [0, 3, 4, 5, 6, 7, 0] rd2_recieving = RouteData(r2, route_l(r2, self.D), route_d(r2, demands), None) result = do_redistribute_move(rd1_redistribute, [rd2_recieving], self.D, C=6.0, d=self.d, strategy=LSOPT.BEST_ACCEPT, recombination_level=1) # Fails -> None,None...None is returned (no partial fits) self.assertTrue( all(rp == None for rp in result), "The redistribute operator should return the redistributed and the new combined routes and the delta" )
def test_redistribute_find_best_fit(self): demands = [0.0, 1, 1, 2, 3, 1, 3, 1] r1 = [0, 1, 4, 6, 0] rd1_redistribute = RouteData(r1, route_l(r1, self.D), route_d(r1, demands), None) r2 = [0, 2, 3, 0] rd2_recieving = RouteData(r2, route_l(r2, self.D), route_d(r2, demands), None) r3 = [0, 5, 0] rd3_recieving = RouteData(r3, route_l(r3, self.D), route_d(r3, demands), None) r4 = [0, 7, 0] rd4_recieving = RouteData(r4, route_l(r4, self.D), route_d(r4, demands), None) result = do_redistribute_move( rd1_redistribute, [rd2_recieving, rd3_recieving, rd4_recieving], self.D, C=4.0, d=demands, strategy=LSOPT.BEST_ACCEPT, recombination_level=1) self.assertEqual( len(result), 5, "The redistribute operator should return the redistributed and the new combined routes and the delta" ) self.assertEqual(result[0].route, [0, 0], "It should be possible to insert all customers") self.assertEqual(_normalise_route_order(result[1].route), [0, 1, 2, 3, 0], "n1 should be redistributed to first route") self.assertEqual(_normalise_route_order(result[2].route), [0, 4, 5, 0], "n4 should be redistributed to first route") self.assertEqual(_normalise_route_order(result[3].route), [0, 6, 7, 0], "n6 should be redistributed to first route")
def _regain_feasibility(to_move, r1_delta, r2_delta, rd1_aftermove, ri1, ri2, discard_at_most, d_excess, l_excess, route_datas, D, d, C, L): """ From Foster & Ryan (1976) "The result of such a (1 point) move may be to cause the receiving route to exceed the mileage or capacity restriction and so it must be permitted to discard deliveries to regain feasibility. ... these ... are in turn relocated in the current solution schedule without causing further infeasibility and that the total effect of all the relocations must be a reduction in the mileage." Thus, after the first of the stored 1 point moves causes the solution to become infeasible, this function tries to regain the feasibility by redistributing some of the customers from the recieving route to other routes so that the moves still produce an improved solution. However, to keep the entire operation improving, the redistribution should not increase the cost more than delta_slack.""" ## REPLACE MOVE # there is no room, have to replace one *OR MORE* nodes # We need to check removing all combinations of customers to make room and # store those that still would remove the problem. route2, r2_l, r2_d, _ = route_datas[ri2] assert route2[0] == 0 and route2[ -1] == 0, "All routes must start and end to the depot" r2_min_d = min(d[n] for n in route2[1:-1]) if d else 0 # First route is changed and reserve an empty route to receive the nodes. new_empty_rd = RouteData() routes_with_slack = [rd for ri, rd in enumerate(route_datas) if (ri!=ri1 and ri!=ri2 and (not C or C-rd.demand+C_EPS>r2_min_d))]\ +[rd1_aftermove,new_empty_rd] # A depth first search (no recursion) for removing JUST enough customers if d: stack = [(i + 1, [n], d[n]) for i, n in enumerate(route2[1:-1])] else: stack = [(i + 1, [n], None) for i, n in enumerate(route2[1:-1])] improving_rds = [] while stack: last_n_i, to_remove_ns, to_remove_d = stack.pop() # We have to recalculate the delta_r2 as removing nodes almost surely # will change the best position to insert to. new_route2 = [n for n in route2 if n not in to_remove_ns] new_r2_delta = float('inf') new_r2_l = 0.0 best_j = None for j in xrange(1, len(new_route2)): insert_after = new_route2[j - 1] insert_before = new_route2[j] new_r2_l += D[insert_after, insert_before] delta = +D[insert_after,to_move]\ +D[to_move,insert_before]\ -D[insert_after,insert_before] if delta < new_r2_delta: new_r2_delta = delta best_j = j to_remove_l = (r2_l + r2_delta) - (new_r2_l + new_r2_delta) new_route2 = new_route2[:best_j] + [to_move] + new_route2[best_j:] if (not d_excess or to_remove_d+C_EPS >= d_excess) and\ (not l_excess or to_remove_l+S_EPS >= l_excess): # Redistributing ALWAYS increases the cost at least a little, # it makes no sense to even try sometimes. # If all of the improvements goes to inserting to_move customer, # there is none left to try to redistribute. delta_slack = -(r1_delta + new_r2_delta) if delta_slack + S_EPS < 0: continue # After removing customers in to_remove_ns route2 becomes feasible, # now we have to check if redistributing the removed back to the # solution is possible. to_redistribute_r = [0] + to_remove_ns + [0] to_redisribute_rd = RouteData(to_redistribute_r, objf(to_redistribute_r, D), totald(to_redistribute_r, d)) result = do_redistribute_move(to_redisribute_rd, routes_with_slack, D, d, C, L, strategy=LSOPT.BEST_ACCEPT, recombination_level=0, best_delta=delta_slack) redistribute_delta = result[-1] if redistribute_delta is not None: new_r2_d = r2_d - to_remove_d + d[to_move] if d else 0 new_rd2 = RouteData(new_route2, new_r2_l + new_r2_delta, new_r2_d) improving_rds.append(new_rd2) improving_rds += result[1:-1] # includes modified rd1 #TODO: if one would like to explore all discard combinations, # one would do branching also here or at least if the redisribute # fails. elif not discard_at_most or len(to_remove_ns) < discard_at_most: # branch out for candidate_j, candidate_n in enumerate(route2[last_n_i + 1:-1]): stack.append( (last_n_i + 1 + candidate_j, to_remove_ns + [candidate_n], to_remove_d + d[candidate_n] if d else None)) return improving_rds