"""
ssp_ts.py: Construction and tabu search for the stable set problem.

The Stable Set Problem (SSP) is a combinatorial optimization problem
where: given a graph, find the maximum subset of nodes S such that
there are no edges between nodes in S.  This problem is equivalent to
finding the maximum clique in the complementary graph.

This file contains a set of functions to illustrate construction and a
simple variant of tabu search.

Copyright (c) by Joao Pedro PEDROSO and Mikio KUBO, 2007
"""
LOG = True      # whether or not to print intermediate solutions
import random
Infinity = 1.e10000

from graphtools import *

def evaluate(nodes, adj, sol):
    """Evaluate a solution.

    Determines:
      - the cardinality of a solution, i.e., the number of nodes in the stable set;
      - the number of conflicts in the solution (pairs of nodes for which there is an edge);
      - b[i] - number of nodes adjacent to i in the stable set;
    """
    card = len(sol)
    b = [0 for i in nodes]
    infeas = 0
    for i in sol:
        for j in adj[i]:
            b[j] += 1
            if j in sol:
                infeas += 1
    return card, infeas/2, b


def construct(nodes, adj):
    """A simple construction method.

    The solution is represented by a set, which includes the vertices 
    in the stable set.

    This function constructs a maximal stable set.
    """
    sol = set([])
    b = [0 for i in nodes]
    indices = list(nodes)
    random.shuffle(indices)
    for ii in nodes:
        i = indices[ii]
        if b[i] == 0:
            sol.add(i)
            for j in adj[i]:
                b[j] += 1
    return sol


def find_add(nodes, adj, sol, b, tabu, tabulen, iteration):
    """Find the best non-tabu vertex for adding into 'sol' (the stable set) """
    xdelta = Infinity
    istar = []
    for i in set(nodes) - sol:
        # if tabu[i] <= iteration:
        if random.random() > float(tabu[i] - iteration)/tabulen:
            delta = b[i]
            if delta < xdelta:
                xdelta = delta
                istar = [i]
            elif delta == xdelta:
                istar.append(i)
            
    if istar != []:
        return random.choice(istar)
    
    print "blocked, no non-tabu move"
    for i in nodes:     # reset tabu information
        tabu[i] = min(tabu[i],iteration)
    return find_add(nodes, adj, sol, b, tabu, tabulen, iteration)


def find_drop(nodes, adj, sol, b, tabu, tabulen, iteration):
    """Find the best non-tabu vertex for removing from 'sol' (the stable set) """
    xdelta = -Infinity
    istar = []
    for i in sol:
        # if tabu[i] <= iteration:
        if random.random() > float(tabu[i] - iteration)/tabulen:
            delta = b[i]
            if delta > xdelta:
                xdelta = delta
                istar = [i]
            elif delta == xdelta:
                istar.append(i)
            
    if istar != []:
        return random.choice(istar)
    
    print "blocked, no non-tabu move"
    for i in nodes:     # reset tabu information
        tabu[i] = min(tabu[i],iteration)
    return find_drop(nodes, adj, sol, b, tabu, tabulen, iteration)


def move_in(nodes, adj, sol, b, tabu, tabuIN, tabuOUT, iteration):
    """Determine and execute the best non-tabu insertion into the solution."""

    # find the best move
    i = find_add(nodes, adj, sol, b, tabu, tabuOUT, iteration)
    # print "{} <- %d\t" % i,
    tabu[i] = iteration + tabuIN
    sol.add(i)

    # update cost structure for nodes connected to i
    deltainfeas = 0
    for j in adj[i]:
        b[j] += 1
        if j in sol:
            deltainfeas += 1
    return deltainfeas


def move_out(nodes, adj, sol, b, tabu, tabuIN, tabuOUT, iteration):
    """Determine and execute the best non-tabu removal from the solution."""

    # find the best move
    i = find_drop(nodes, adj, sol, b, tabu, tabuIN, iteration)
    # print "{} -> %d\t" % i,
    tabu[i] = iteration + tabuOUT
    sol.remove(i)

    # update cost structure for nodes connected to i
    deltainfeas = 0
    for j in adj[i]:
        b[j] -= 1
        if j in sol:
            deltainfeas -= 1
    return deltainfeas


def tabu_search(nodes, adj, sol, max_iter, tabulen, report=None):
    """Execute a tabu search run."""
    n = len(nodes)
    tabu = [0 for i in nodes]

    card, infeas, b = evaluate(nodes, adj, sol)
    assert infeas == 0
    bestsol, bestcard = set(sol), card
    if LOG:
        print "iter:", 0, "\tcard: %d (%d conflicts)" % (card,infeas),\
              "/ best:", bestcard # , "\t", sol
    for it in range(max_iter):
        tabuIN = 1+int(tabulen/100. * card)     # update tabu parameter for inserting vertices
        tabuOUT = 1+int(tabulen/100. * (n-card))# update tabu parameter for removing vertices
        if infeas == 0: # solution is feasible, add a new vertex
            infeas += move_in(nodes, adj, sol, b, tabu, tabuIN, tabuOUT, it)
            card += 1
        else:           # solution is infeasible, remove a vertex
            infeas += move_out(nodes, adj, sol, b, tabu, tabuIN, tabuOUT, it)
            card -= 1

        if infeas == 0 and card > bestcard:
            bestsol, bestcard = set(sol), card
            if report:
                report(card, "iter:%d" % it)

        if LOG:
            print "iter:", it+1, "\tcard: %d (%d conflicts)" % (card,infeas),\
                  "/ best:", bestcard # , "\t", sol
            
    # check solution correctness:
    # xcard, xinfeas, xb = evaluate(nodes, adj, bestsol)
    # assert bestcard == xcard and xinfeas == 0
    return bestsol, bestcard
        

if __name__ == "__main__":
    """Tabu search for the Stable Set Problem: sample usage"""
    import sys
    if len(sys.argv) == 1:
        # rndseed = 1	# uncomment for having always the same behavior
        # random.seed(rndseed)
        instance = "randomly created"
        nodes,edges = rnd_graph(100,.5)
        edges = complement(nodes,edges)
        adj = adjacent(nodes, edges)
        max_iterations = 1000
        tabulength = len(nodes)/10
    elif len(sys.argv) == 5:
        instance = sys.argv[1]
        rndseed = int(sys.argv[2])
        random.seed(rndseed)
        tabulength = float(sys.argv[3])
        max_iterations = int(sys.argv[4])
        print "reading file:", instance
        nodes,adj = read_compl_graph(instance)
    else:
        print "usage:", sys.argv[0], "instance seed tabulength iterations"
        exit(-1)

    # function for printing best found solution when it is found
    from time import clock
    init = clock()
    def report_sol(obj,s=""):
        print "cpu:%g\tobj:%g\t%s" % \
              (clock(), obj, s)

    print "*** stable set problem ***"
    print

    print "instance", instance
    sol = construct(nodes,adj)
    print "initial solution:", sol
    xcard, xinfeas, xb = evaluate(nodes, adj, sol)
    print "cardinality: %d (%d conflicts)" % (xcard, xinfeas)
    assert xinfeas == 0
    print

    print "starting tabu search"
    sol, card = tabu_search(nodes, adj, sol, max_iterations, tabulength, report_sol)
    print
    print "final solution: z =", card
    print sol
    print
