"""
atsp.py: Construction and local optimization for the ATSP.

The Asymmetric Traveling Salesman Problem (ATSP) is a combinatorial
optimization problem, where given a distance matrix, one wants to find
an order for visiting all the cities in such a way that the travel
distance is minimal.

This file contains a set of functions to illustrate:
  - construction heuristics for the ATSP
  - improvement heuristics for a previously constructed solution
  - local search, and random-start local search.

Copyright (c) by Joao Pedro PEDROSO, 2010
"""
from __future__ import with_statement

import math
import random
import gzip

def read_atsplib(filename):
    "basic function for reading a ATSP problem on the TSPLIB format"
    "NOTE: only works for explicit matrices"

    if filename[-3:] == ".gz":
        f = gzip.open(filename, 'r')
        data = f.readlines()
    else:
        with open(filename, 'r') as f:
            data = f.readlines()

    for line in data:
        if line.find("DIMENSION") >= 0:
            n = int(line.split()[1])
            # print "dimension:", n
            break
    else:
        raise IOError("'DIMENSION' keyword not found in file '%s'" % filename)

    for line in data:
        if line.find("EDGE_WEIGHT_TYPE") >= 0:
            if line.split()[1] == "EXPLICIT":
                # print "explicit"
                break
    else:
        raise IOError("'EDGE_WEIGHT_TYPE' is not 'EXPLICIT' in file '%s'" % filename)
    
    for k,line in enumerate(data):
        if line.find("EDGE_WEIGHT_SECTION") >= 0:
            # print "found weights"
            break
    else:
        raise IOError("'EDGE_WEIGHT_SECTION' not found in file '%s'" % filename)

    D = {}      # dictionary to hold n times n matrix
    # flatten list of distances
    dist = []
    for line in data[k+1:]:
        if line.find("EOF") >= 0:
            break
        for val in line.split():
            dist.append(int(val))

    k = 0
    for i in range(n):
        for j in range(n):
            D[i,j] = dist[k]
            k += 1

    return n, D


def mk_closest(D, n, m):
    """Compute a sorted list of the 'm' closest distances for each of the nodes.

    For each node, the entry is in the form [(d1,i1), (d2,i2), ...]
    where each tuple is a pair (distance,node).

    returns (I,O)  where 'I' is for in nodes, 'O' for out nodes.
    """
    I = []	# lowest in-arcs
    for i in range(n):
        dlist = [(D[j,i], j) for j in range(n) if j != i]
        dlist.sort()
        I.append(dlist[:m])
    O = []	# lowest out-arcs
    for i in range(n):
        dlist = [(D[i,j], j) for j in range(n) if j != i]
        dlist.sort()
        O.append(dlist[:m])
    return I,O


def length(sol, D):
    """Calculate the length of a tour according to distance matrix 'D'."""
    z = D[sol[-1], sol[0]]    # edge from last to first city of the sol
    for i in range(1,len(sol)):
        z += D[sol[i-1], sol[i]]      # add length of edge from city i-1 to i
    return z


def randtour(unvisited):
    """Construct a random sol of size 'n'."""
    sol = list(unvisited)
    random.shuffle(sol) # place it in a random order
    return sol


def nearest(unvisited, last, D):
    """Return the index of the node which is closest to 'last'."""
    near = unvisited[0]
    min_dist = D[last, near]
    for i in unvisited[1:]:
        if D[last,i] < min_dist:
            near = i
            min_dist = D[last, near]
    return near


def nearest_neighbor(unvisited_, i, D):
    """Return sol starting from city 'i', going through all 'unvisited', using the Nearest Neighbor.

    Uses the Nearest Neighbor heuristic to construct a solution:
    - start visiting city i
    - while there are unvisited cities, follow to the closest one
    - return to city i
    """
    unvisited = list(unvisited_)
    unvisited.remove(i)
    last = i
    sol = [i]
    while unvisited != []:
        next = nearest(unvisited, last, D)
        sol.append(next)
        unvisited.remove(next)
        last = next
    return sol


def greedy(nodes, D):
    """Return sol constructed with the Greedy heuristics.

    Uses the greedy heuristics to construct a solution:
    - add allowed nodes until all nodes are connected
    """
    n = len(nodes)
    arcs = [(D[i,j],i,j) for i in nodes for j in nodes if j != i]
    arcs.sort()
    from_set = set([])
    to_set = set([])
    dist, narcs = 0,0
    succ = [None for i in nodes]
    while arcs != []:
        d,i,j = arcs.pop(0)
        if i in from_set or j in to_set:
            continue
        allowed = True
        if narcs < n-1:
            j_ = succ[j]
            while j_ != None:
                if i == j_:
                    allowed = False
                    break
                j_ = succ[j_]
        if not allowed:
            continue
        succ[i] = j
        from_set.add(i)
        to_set.add(j)
        dist += d
        narcs += 1
        print "\tadded", (i,j), "--->", dist
    i = 0
    sol = [i]
    j = succ[i]
    while j != 0:
        sol.append(j)
        j = succ[j]
    assert len(sol) == n
    return sol, dist


def improve_simplistic(sol, z, D):
    """Try to improve 'sol' by exchanging arcs; return improved sol length.
    
    If possible, make a series of local improvements on the solution 'sol',
    using a first-improve strategy, until reaching a local optimum.
    """
    n = len(sol)
    for i in range(n-2):
        a,b = sol[i],sol[i+1]
        for j in range(i+1,n-1):
            c,d = sol[j],sol[j+1]
            for k in range(j+1,n):
                e,f = sol[k],sol[(k+1)%n]
                delta = (D[a,d] + D[c,f] + D[e,b]) - (D[a,b] + D[c,d] + D[e,f])
                if delta < 0:
                    sol = sol[0:i+1] + sol[j+1:k+1] + sol[i+1:j+1] + sol[k+1:] 
                    a,b = sol[i],sol[i+1]   # must update after exchange
                    c,d = sol[j],sol[j+1]
                    z += delta
                    # print "\t * ", sol, z
    return sol, z


def improve(sol, tinv, z, D, I, O):
    """Try to improve 'sol' by exchanging arcs; return improved sol length.
    
    If possible, make a series of local improvements on the solution 'sol',
    using a best first strategy, until reaching a local optimum.

    Sol is represented as (->-ab-->--cd-->--ef-->--), and the only possible move
    is a 3-exchange into (->-ad-->--eb-->--cf-->--), for having to path inversions.    
    """
    n = len(sol)
    N = range(n); random.shuffle(N)
    for j in N:		# j is the index of the middle arc
        c, d = sol[j-1], sol[j]
        dist_cd = D[c,d]
        improved = False

        for dist_cf,f in O[c]:
            if improved or dist_cf >= dist_cd:
                break
            k = tinv[f]; assert sol[k] == f
            assert k != j-1
            e = sol[k-1]
            dist_ef = D[e,f]

            for dist_eb,b in O[e]:
                if improved or dist_eb + dist_cf >= dist_cd + dist_ef:
                    break
                i = tinv[b]; assert sol[i] == b
                if i <= k <= j or k <= j <= i or j <= i <= k:   # infeasible
                    continue
                a = sol[i-1]
                dist_ab = D[a,b]
                dist_ad = D[a,d]

                delta = (dist_ad + dist_eb + dist_cf) - (dist_ab + dist_cd + dist_ef)
                if delta < 0:       # exchange decreases length
                    idx = sorted([i,j,k])
                    ii,jj,kk = tuple(idx)
                    sol = sol[0:ii] + sol[jj:kk] + sol[ii:jj] + sol[kk:]
                    # print z, "--->",
                    z += delta
                    # print z
                    for r in range(ii,kk):
                        tinv[sol[r]] = r  # update position of each city in 'sol'
                    improved = True # values for a, b, ... are no long uptodate

        for dist_ad,a in I[d]:
            if improved or dist_ad >= dist_cd:
                break
            i = (tinv[a]+1)%n; assert sol[i-1] == a
            assert i != j
            b = sol[i]
            dist_ab = D[a,b]

            for dist_eb,e in I[b]:
                if improved or dist_eb + dist_ad >= dist_ab + dist_cd:
                    break
                k = (tinv[e]+1)%n; assert sol[k-1] == e
                if i <= k <= j or k <= j <= i or j <= i <= k:   # infeasible
                    continue
                f = sol[k]
                dist_ef = D[e,f]
                dist_cf = D[c,f]

                delta = (dist_ad + dist_eb + dist_cf) - (dist_ab + dist_cd + dist_ef)
                if delta < 0:       # exchange decreases length
                    idx = sorted([i,j,k])
                    ii,jj,kk = tuple(idx)
                    sol = sol[0:ii] + sol[jj:kk] + sol[ii:jj] + sol[kk:] 
                    z += delta
                    for r in range(ii,kk):
                        tinv[sol[r]] = r  # update position of each city in 'sol'
                    improved = True # values for a, b, ... are no long uptodate

    return sol, z


def localsearch(sol, z, D, I=None, O=None):
    """Obtain a local optimum starting from solution t; return solution length.

    Parameters:
      sol -- initial sol
      z -- length of the initial sol
      D -- distance matrix
    """
    n = len(sol)
    tinv = [0 for i in sol]
    for k in range(n):
        tinv[sol[k]] = k  # position of each city in 'sol'
    if O == None:
        I,O = mk_closest(D, n, n)     # create a sorted list of distances to each node
    while 1:
        sol, newz = improve(sol, tinv, z, D, I, O)
        if newz < z:
            z = newz
        else:
            break
    assert newz == length(sol,D)
    return sol, z


def multistart_localsearch(k, unvisited, D, report=None):
    """Do k iterations of random-start local search, starting from random solutions.

    Parameters:
    -k -- number of iterations
    -D -- distance matrix
    -report -- if not None, call it to print verbose output

    Returns best solution and its cost.
    """
    I,O = mk_closest(D, n, n) # create a sorted list of distances to each node
    bestt=None
    bestz=None
    for i in range(0,k):
        sol = randtour(unvisited)
        z = length(sol, D)
        sol, z = localsearch(sol, z, D, I, O)
        if z < bestz or bestz == None:
            bestz = z
            bestt = list(sol)
            if report:
                report(z, sol)

    return bestt, bestz


def iterated_localsearch(k, unvisited, D, report=None):
    """Do k iterations of iterated local search.

    Parameters:
    -k -- number of iterations
    -D -- distance matrix
    -report -- if not None, call it to print verbose output

    Returns best solution and its cost.
    """
    bestt=None
    bestz=None
    sol = randtour(unvisited)
    n = len(sol)
    z = length(sol, D)
    for it in range(0,k):
        sol, z = localsearch(sol, z, D)
        if z < bestz or bestz == None:
            bestz = z
            bestt = list(sol)
            if report:
                report(z, sol)
        print z, sol[:25]
        # 4-exchange
        R = range(1,n)
        idx = []
        for r in range(4):
            i = random.choice(R); R.remove(i); idx.append(i)
        idx.sort()
        i,j,k,l = tuple(idx)
        a,b,c,d,e,f,g,h = sol[i-1], sol[i], sol[j-1], sol[j], sol[k-1], sol[k], sol[l-1], sol[l]
        delta = D[a,f] + D[g,d] + D[e,b] + D[c,h] - (
            D[a,b] + D[c,d] + D[e,f] + D[g,h])
        z += delta
        sol = sol[:i] + sol[k:l] + sol[j:k] + sol[i:j] + sol[l:]
        print "***", z, "---->",
        # assert z == length(sol, D)
    return bestt, bestz



if __name__ == "__main__":
    """Local search for the Asymmetric Travelling Saleman Problem: sample usage."""
    
    #
    # test the functions:
    #

    import sys
    if len(sys.argv) == 1:
        random.seed(14)    # uncomment for having always the same behavior
        instance = "toy.atsp"
        instance = "INSTANCES/br17.atsp.gz"
        instance = "INSTANCES/p43.atsp.gz"
        # instance = "INSTANCES/kro124p.atsp.gz"
    elif len(sys.argv) == 3:
        seed = int(sys.argv[1])
        random.seed(seed)
        print "seed:", seed
        instance = sys.argv[2]
    else:
        print "usage:", sys.argv[0], "[seed instance maxcpu method]"
    n, D = read_atsplib(instance)
    
    # function for printing best found solution when it is found
    from time import clock
    init = clock()
    def report_sol(obj, s=""):
        print "\ncpu:%r\tobj:%r\tsol:%s" % \
              (clock(), obj, s)


    # print "*** asymmetric travelling salesman problem ***"
    # print
    # print "*** D ***"
    # for i in range(n):
    #     for j in range(n):
    #         print "%5d" % D[i,j],
    #     print
    # print


    # random construction
    print "random construction + local search:"
    unvisited = range(n)
    sol = randtour(unvisited)     # create a random sol
    z = length(sol, D)     # calculate its length
    print "random:", sol, z, '  -->  ',
    sol, z = localsearch(sol, z, D)      # local search starting from the random sol
    print sol, z
    print

    # nearest neighbor construction
    print "construction with nearest neighbor + local search:"
    for i in range(n):
        sol = nearest_neighbor(unvisited, i, D)     # create a NN sol, visiting city 'i' first
        z = length(sol, D)
        print "nneigh:", sol, z, '  -->  ',
        sol, z = localsearch(sol, z, D)
        print sol, z
    print

    # greedy construction
    print "greedy construction + local search:"
    sol, z = greedy(unvisited,D)
    print "greedy:", sol, z, '  -->  ',
    sol, z = localsearch(sol, z, D)      # local search starting from the random sol
    print sol, z

    # multi-start local search
    print "random start local search:"
    niter = 100
    sol,z = multistart_localsearch(niter, unvisited, D, report_sol)
    assert z == length(sol, D)
    print "best found solution (%d iterations): z = %g" % (niter, z)
    print sol
    print

    # iterated local search
    print "iterated local search:"
    niter = 1000
    sol, z = iterated_localsearch(niter, unvisited, D, report_sol)
    print sol, z
    assert z == length(sol, D)

    # check if the code works for a subset of cities to visit
    unvisited = range(n/2)
    z = length(sol, D)
    print "subset local search:"
    sol = randtour(unvisited)    # create a random sol starting from 0
    z = length(sol, D)     # calculate its length
    print "random:", sol, z, '  -->  ',   
    sol, z = localsearch(sol, z, D)      # local search starting from the random sol
    print sol, z
    print
    assert z == length(sol, D)

    print "subset search: greedy construction with nearest neighbor + local search:"
    sol = nearest_neighbor(unvisited, 0, D)     # create a greedy sol, visiting city '0' first
    z = length(sol, D)
    print "nneigh:", sol, z, '  -->  ',
    sol, z = localsearch(sol, z, D)
    print sol, z
    print
    assert z == length(sol, D)
