iHarder.net

OpenTS - Tutorial

OpenTS

Summary

Documentation

License

OpenTS - Java Tabu Search Framework

by Robert Harder | Summary | Documentation | License |


Tutorial: Creating a Simple Tabu Search

Please read this tutorial "with a grain of salt" until it is reviewed for its final form.

We will begin by building a simple tabu search for the traveling salesman (TSP). Of course there is no end to the variations on the TSP, but we will take one and go with it, making it more complex as we go. At the beginning the problem will be defined in part by the following characteristics:
  • One salesman with infinite endurance, starting and ending at the same x-y coordinate.
  • An unknown number of customers generated randomly with x-y coordinate pairs.
  • The goal is to minimize the time it takes the salesman to visit all the customers.
Let's take a look at the division of labor between your code and the Tabu Search Engine. The figure below shows which code is responsible for each step in an iteration.


Division of labor between your code and the T.S. Engine

Solution Definition

The first required object that we will build is the solution object. Our solution object will need to extend the net.iharder.tabusearch25.TSSolution abstract class. There is only one method that is required: clone. The tabu search engine requires that we be able to duplicate our solution objects. The abstract class also has some book-keeping methods that the tabu search engine uses.

Knowing that we may do something more complicated in the future, we'll create Salesman and Customer objects that we can store in the Solution in a clever fashion. For now our solution will contain a reference to our one salesman and an array that contains the customers in the order to be visited.

Let's quickly define our Salesman and Customer objects. Note throughout the tutorial our generous use of the final declaration. This is an attempt to help smart JVM's optimize the code. It is not necessary to use final.

Salesman

public class Salesman
{
    private double homeX;
    private double homeY;
    private int id;
    private static int nextID = 0;


    // Constructor
    public Salesman( double x, double y )
    {   this.id = nextID++;
        this.homeX = x;
        this.homeY = y;
    }   // end constructor


    public final double getHomeX()
    {	return homeX;
    }   // end getHomeX


    public final double getHomeY()
    {   return homeY;
    }   // end getHomeY


    public final int getID()
    {   return id;
    }   // end getID

}   // end class Salesman

Customer

public class Customer
{
    private double x;
    private double y;
    private int id;
    private static int nextID = 1;


    // Constructor
    public Customer( double x, double y )
    {   this.id = nextID++;
        this.x = x;
        this.y = y;
    }   // end constructor


    public final double getX()
    {	return x;
    }   // end getX


    public final double getY()
    {   return y;
    }   // end getY


    public final int getID()
    {   return id;
    }   // end getID

}   // end class Customer

Now we can design a TSPSolution class that stores the salesman and customers and permits simple access to the data including moving customers from one position to another. Note that when we clone the solution, as required by TSSolution, we do not need to clone the salesman and customer objects. We only need to copy the pointers to these objects.

TSPSolution

import net.iharder.tabusearch25.*;

public class TSPSolution
extends TSSolution
{
    private Salesman salesman;
    private Customer[] customers;


    // Normal constructor
    public TSPSolution( 
    Salesman salesman, Customer[] customers )
    {	this.salesman = salesman;
        this.customers = customers;
    }   // end constructor


    // This constructor aids in cloning
    public TSPSolution( TSPSolution copyThis )
    {
        // You should call super( copyThis ) so that the
        // abstract class can do its book-keeping.
        super( copyThis );
        
        // Copy our data
        this.salesman = copyThis.getSalesman();
        Customer[] otherCust = copyThis.getCustomers();
        this.customers = new Customer[ otherCust.length ];
        System.arraycopy( otherCust, 0, 
            this.customers, 0, otherCust.length );
    }   // end constructor


    public final Salesman getSalesman()
    {   return salesman;
    }   // end getSalesman


    public final Customer[] getCustomers()
    {   return customers;
    }   // end getCustomers


    // A quick and easy way to move customers around.
    public final void swapCustomers( 
    final int positionOne, final int positionTwo )
    {
        // Swap the customers
        Customer cust = customers[positionOne];
        customers[positionOne] = customers[positionTwo];
        customers[positionTwo] = cust;
        setObjectiveValid( false );
    }   // end swapCustomers

    
    // 'clone' is required by TSSolution
    public final Object clone()
    {   return new TSPSolution( this );
    }   // end clone


}   // end class TSPSolution

Since we are extending TSSolution we need to be sure that the tabusearch25.jar file is in the classpath. For example:

    javac -classpath .;tabusearch25.jar Solution.java
    
This assumes that tabusearch25.jar is in the current directory. You may need to "back up" several directories if you're using the tutorial source files where they lie in docs/tutorial/simple in which case you can use the following:
    javac -classpath .;../../../tabusearch25.jar Solution.java
    

Moves & the Move Manager

Next we must define how we intend to manipulate the solution space. A tabu search makes many small changes to a solution at each iteration and determines which of these new solutions, or neighbors, are the best. To accomplish that with this tabu search engine we will create a SwapMove class that performs some sort of swapping operation and a MoveManager class that generates these SwapMove objects at each iteration.

To create a SwapMove class we implement TSMove which requires that we have an operateOn method that will manipulate a Solution object. Note that since we actually included a swap method in the solution object itself, the move only has to call that method.

SwapMove

import net.iharder.tabusearch25.*;

public class SwapMove
implements TSMove
{
    private int positionOne;
    private int positionTwo;

    // Constructor
    public SwapMove( int positionOne, int positionTwo )
    {   this.positionOne = positionOne;
        this.positionTwo = positionTwo;
    }   // end constructor

    
    public final int getPositionOne()
    {   return positionOne;
    }   // end getPositionOne


    public final int getPositionTwo()
    {   return positionTwo;
    }   // getPositionTwo


    // This method is required by TSMove
    public final void operateOn( final TSSolution solution )
    {
        // First we must cast the TSSolution to our
        // solution type
        TSPSolution soln = (TSPSolution) solution;

        // Now swap
        soln.swapCustomers( positionOne, positionTwo );
    }   // end operateOn

}   // end class SwapMove

The tabu search engine will need a list of all the moves we're interested in at each iteration, so we need to create a MoveManager that will handle this job. The interface TSMoveManager will pass a current solution to our move mananger, and our move manager should return a list of moves to evaluate on that iteration. Note that we don't need a constructor in this class.

MoveManager

import net.iharder.tabusearch25.*;

public class MoveManager
implements TSMoveManager
{
    
    // This method is required by TSMoveManager
    public final TSMove[] getAllMoves( final TSSolution solution )
    {
        // First we must cast the TSSolution to our
        // solution type
        TSPSolution soln = (TSPSolution) solution;

        // Determine the number of customers
        int numCust = soln.getCustomers().length;

        // Make all possible swap moves.
        int count = 0;
        for( int i = 0; i < numCust; i++ )
            count += i;
        SwapMove[] moves = new SwapMove[ count ];
        int index = 0;
        for( int i = 0; i < numCust-1; i++ )
            for( int j = i+1; j < numCust; j++ )
                moves[index++] = new SwapMove( i, j );

        return moves;
    }   // end getAllMoves

}   // end class MoveManager

Tabu List

Next we need a tabu list to be the tabu search's memory. Two popular techniques for tabu lists are to save a hash value for the solution and to save attributes of the moves. In this tutorial we will base the tabu list TSPTabuList on attributes of moves that we've executed. We will prohibit any customer that has been moved from moving again for some period of time.

TSPTabuList

import net.iharder.tabusearch25.*;

public class TSPTabuList
implements TSTabuList
{
    private int[] disturbedMoves;
    private int nextFreshPosition = 0;


    // Constructor
    public TSPTabuList( int tenure )
    {   disturbedMoves = new int[ tenure ];
    }   // end constructor


    public final void registerMove( 
    final TSMove executedMove, final TSSolution fromSolution )
    {
        // Cast the move and solution to our types
        SwapMove move = (SwapMove) executedMove;
        TSPSolution soln = (TSPSolution) fromSolution;

        // Find which customers will be disturbed.
        Customer[] customers = soln.getCustomers();
        int disturbedOne = customers[ move.getPositionOne() ].getID();
        int disturbedTwo = customers[ move.getPositionTwo() ].getID();

        // Record the new moves in the tabu list
        disturbedMoves[ nextFreshPosition++ % 
            disturbedMoves.length ] = disturbedOne;
        disturbedMoves[ nextFreshPosition++ % 
            disturbedMoves.length ] = disturbedTwo;

    }   // end registerMove


    public final boolean allowMove( 
    final TSMove proposedMove, final TSSolution fromSolution )
    {
        // Cast the move and solution to our types
        SwapMove move = (SwapMove) proposedMove;
        TSPSolution soln = (TSPSolution) fromSolution;

        // Find which customers will be disturbed.
        Customer[] customers = soln.getCustomers();
        int disturbedOne = customers[ move.getPositionOne() ].getID();
        int disturbedTwo = customers[ move.getPositionTwo() ].getID();

        // Search the list for these values
        boolean allow = true;
        int i = 0;
        while( allow && ( i < disturbedMoves.length ) )
        {   int tabu = disturbedMoves[i];
            if( ( disturbedOne == tabu ) || (disturbedTwo == tabu ) )
                allow = false;
            i++;
        }   // end while: through list or until tabu found

        return allow;
    }   // end allowMove

}   // end class TSPTabuList

Objective Function

Now we need a way to evaluate the solution. We'll create an
ObjectiveFunction that will evaluate the time it takes the salesman to make the rounds. To avoid doing extra Euclidian geometry every time we evaluate the solution, we will pre-calculate a time matrix that includes the time it takes to get from every point to every other point. We can index the matrix with the customer id's, since we started those at one, and treat the zero-th elements as the salesman home.

ObjectiveFunction

import net.iharder.tabusearch25.*;

public class ObjectiveFunction
implements TSFunction
{
    private double[][] timeMatrix;

    // Constructor
    public ObjectiveFunction( TSPSolution initialSolution )
    {   initTimeMatrix( initialSolution );
    }   // end constructor


    // Initialize a time matrix so we don't have
    // to do lots of trig calculations at every iteration.
    private final void initTimeMatrix( final TSPSolution soln )
    {
        // Index a two-dimension matrix by the customer's id,
        // with zero being the home base for the salesman.
        // We have a symmetric matrix, but we'll go ahead
        // and fill in the whole thing.
        final Salesman salesman = soln.getSalesman();
        final Customer[] customers = soln.getCustomers();
        final int numCust = customers.length;
        timeMatrix = new double[ numCust + 1 ][ numCust + 1 ];

        // Fill in to/from home base numbers
        for( int i = 0; i < customers.length; i++ )
        {
            int custId = customers[i].getID();
            double x1 = salesman.getHomeX();
            double y1 = salesman.getHomeY();
            double x2 = customers[i].getX();
            double y2 = customers[i].getY();
            double distance = distanceBetween( x1, y1, x2, y2 );
            timeMatrix[ 0 ][ custId ] = distance;
            timeMatrix[ custId ][ 0 ] = distance;
        }   // end for: each customer

        // Use double loop to fill in inter-customer distances
        for( int i = 0; i < customers.length; i++ )
        {
            int custId1 = customers[i].getID();
            double x1 = customers[i].getX();
            double y1 = customers[i].getY();

            for( int j = i+1; j < customers.length; j++ )
            {
                int custId2 = customers[j].getID();
                double x2 = customers[j].getX();
                double y2 = customers[j].getY();
                double distance = distanceBetween( x1, y1, x2, y2 );
                timeMatrix[ custId1 ][ custId2 ] = distance;
                timeMatrix[ custId2 ][ custId1 ] = distance;
            }   // end for: inner loop through customers
        }   // end for: outer loop through customers

    }   // end initTimeMatrix


    // Calculate Euclidean distance between two points.
    private final double distanceBetween( 
    final double x1, final double y1,
    final double x2, final double y2 )
    {   final double deltaX = x2 - x1;
        final double deltaY = y2 - y1;
        return Math.sqrt( deltaX*deltaX + deltaY*deltaY );
    }   // end distanceBetween


    // Evaluate a solution based on a move.
    public final double[] evaluate( 
    final TSSolution solution, final TSMove theMove )
    {
        // First we must cast the TSSolution to our
        // solution type
        TSPSolution soln = (TSPSolution) solution;
        SwapMove move = (SwapMove) theMove;
        
        // Get current cost
        double[] costs = soln.getObjectiveValue();
        double cost = 0; // Work with a primitive
        if( costs != null ) 
            cost = costs[0];

        // Evaluate incremental change to solution.
        if( move != null )
            cost += calculateIncremental( soln, move );
        else cost = calculateBase( soln );
        
        // Be sure to return the value as an array, even
        // though we're not making use of that capability.
        return new double[]{ cost };
    }   // end evaluate

    
    // Find base value of solution without a move
    private final double calculateBase( final TSPSolution soln )
    {   
        // Setup
            double cost = 0;
        Customer[] customers = soln.getCustomers();

        // Add distances for start/finish
        cost += timeMatrix[ 0 ][ customers[0].getID() ];
        cost += timeMatrix[ customers[ 
            customers.length-1].getID() ][ 0 ];

        // Add distances for each leg
        for( int i = 1; i < customers.length; i++ )
            cost += timeMatrix[ customers[i-1].getID() ]
                [ customers[i].getID() ];

        return cost;
    }   // end calculateBase
    

    // Find incremental change to the solution.
    private final double calculateIncremental( 
    final TSPSolution soln, final SwapMove move )
    {
        // For a tour: A  -  B  -  C  -  D  -  E
        // swapping B and D would change the costs
        // like so: -AB -BC -CD -DE +AD +DC + CB + BE
        // which, in a symmetric matrix reduces to
        //         -AB +AD -DE + BE

        // Get customers
        Customer[] customers = soln.getCustomers();

        // Get positions B and D. Make sure the
        // order is right.
        int posOne = move.getPositionOne();
        int posTwo = move.getPositionTwo();
        int posB = Math.min( posOne, posTwo );
        int posD = Math.max( posOne, posTwo );

        // Get matrix positions for customers who move
        int matrPosB = customers[ posB ].getID();
        int matrPosD = customers[ posD ].getID();

        // Positions A and E could be the home base, so
        // their position in the time matrix could be 0.
        int matrPosA = ( posB == 0 ) 
            ? 0 : customers[ posB-1 ].getID();
        int matrPosE = ( posD == customers.length-1 ) 
            ? 0 : customers[ posD+1 ].getID();

        // AB, AD, DE, BE
        double AB = timeMatrix[ matrPosA ][ matrPosB ];
        double AD = timeMatrix[ matrPosA ][ matrPosD ];
        double DE = timeMatrix[ matrPosD ][ matrPosE ];
        double BE = timeMatrix[ matrPosB ][ matrPosE ];

        return ( -AB +AD -DE +BE );
    }   // end calculateIncremental


}   // end class ObjectiveFunction

Putting it all Together

Now that we've created the tabu search components, we can create a simple application that creates instance of this problem, solves it, and prints out results. Clearly you will want a better user interface than we present here in Main.

Main

import net.iharder.tabusearch25.*;

public class Main
{
    // Random number generator.
    private static java.util.Random R;

    public static void main( String[] args )
    {
        // Use a constant random number seed.
        long seed = 123456789;
        R = new java.util.Random( seed );

        // Define an operating region.
        double x1 = 0;
        double y1 = 0;
        double x2 = 400;
        double y2 = 400;

        // The first argument when calling the 
        // program can be the number of customers. 
        // Otherwise, it defaults to 100 customers.
        int custCount = 100;
        try{ custCount = Integer.parseInt( args[0] ); }
        catch( Exception e ){}
        System.out.println( 
          "Number of customers: " + custCount );

        // The second argument when calling the 
        // program can be the tabu list tenure. 
        // Otherwise, it defaults to 10.
        int tenure = 10;
        try{ tenure = Integer.parseInt( args[1] ); }
        catch( Exception e ){}
        System.out.println( 
          "Tabu list tenure: " + tenure );

        // The third argument when calling the 
        // program can be the number of iterations. 
        // Otherwise, it defaults to 500.
        int iterations = 500;
        try{ iterations = Integer.parseInt( args[2] ); }
        catch( Exception e ){}
        System.out.println( 
          "Number of iterations: " + iterations );

        // Create customers in an area. 
        Customer[] customers = createRandomCustomers( 
          x1, y1, x2, y2, custCount );

        // Create a salesman.
        Salesman salesman = createRandomSalesman(
          x1, y1, x2, y2 );

        // Instantiate all the tabu search objects
        TSPSolution initialSoln = new TSPSolution(
          salesman, customers );
        MoveManager moveMgr = new MoveManager();
        TSPTabuList tabuList = new TSPTabuList( tenure );
        ObjectiveFunction objFunc = 
          new ObjectiveFunction( initialSoln );

        // Create the Engine that will do the iterations.
        // The 'false' indicates minimization.
        TSEngine engine = new TSEngine( initialSoln,
          tabuList, objFunc, moveMgr, false );

        // Start the engine and immediately return 
        // control to this thread.
        engine.startSolving( iterations );

        // Just stall this thread until the engine finishes.
        engine.waitToFinish();

        // Get the best solution (after casting it back).
        TSPSolution bestSoln = 
          (TSPSolution) engine.getBestSolution();

        // Print the results
        printSolution( bestSoln );
        showSolutionFrame( bestSoln );
    }   // end main


    private static Customer[] createRandomCustomers( 
      double lowerBoundX, double lowerBoundY, 
      double upperBoundX, double upperBoundY, int count )
    {   
        // Some initializing
        Customer[] customers = new Customer[ count ];
        double spanX = upperBoundX - lowerBoundX;
        double spanY = upperBoundY - lowerBoundY;

        // Create customers within bounds
        for( int i = 0; i < count; i++ )
        {
            // 0.0 to 0.999...
            double rx = R.nextDouble(); 
            double ry = R.nextDouble();
            double x = ( rx * spanX ) + lowerBoundX;
            double y = ( ry * spanY ) + lowerBoundY;
            customers[ i ] = new Customer( x, y );
        }   // end for: through each customer

        return customers;
    }   // end createRandomCustomers



    private static Salesman createRandomSalesman( 
      double lowerBoundX, double lowerBoundY, 
      double upperBoundX, double upperBoundY )
    {   
        // Some initializing
        double spanX = upperBoundX - lowerBoundX;
        double spanY = upperBoundY - lowerBoundY;

        // Create salesman within bounds
        // 0.0 to 0.999...
        double rx = R.nextDouble(); 
        double ry = R.nextDouble();
        double x = ( rx * spanX ) + lowerBoundX;
        double y = ( ry * spanY ) + lowerBoundY;
        return new Salesman( x, y );

    }   // end createRandomSalesman


    // Print out a solution.
    private static void printSolution( TSPSolution soln )
    {
        Salesman salesman = soln.getSalesman();
        Customer[] customers = soln.getCustomers();
        double cost = soln.getObjectiveValue()[0];

        System.out.println( "Solution" );
        System.out.println( "  Cost: " + cost );
        System.out.print  ( "  Sequence: H" );
        for( int i = 0; i <= customers.length; i++ )
            System.out.print( 
              ( i < customers.length ) 
              ? " - " + customers[ i ].getID() 
              : " - H" );
    }   // end printSolution

    private static void showSolutionFrame( final TSPSolution soln )
    {   final Salesman salesman = soln.getSalesman();
        final Customer[] customers = soln.getCustomers();
        java.awt.Panel panel = new java.awt.Panel()
        {   public void paint( java.awt.Graphics g )
            {   // Paint trip.
                for( int i = 0; i < customers.length; i++ )
                {   // First?
                    if( i == 0 )
                    {   g.setColor( java.awt.Color.red );
                        g.drawLine( (int)salesman.getHomeX(), 
                            (int)salesman.getHomeY(),
                            (int)customers[0].getX(), 
                            (int)customers[0].getY() );
                        g.drawString( "HOME", 
                            (int)salesman.getHomeX(), 
                            (int)salesman.getHomeY() );
                        g.setColor( java.awt.Color.green.darker() );
                    }   // end if: first customer
                    // Last?
                    else if( i == (customers.length-1) )
                    {   g.setColor( java.awt.Color.blue );
                        g.drawLine( 
                            (int)customers[customers.length-1].getX(), 
                            (int)customers[customers.length-1].getY(),
                            (int)salesman.getHomeX(), 
                            (int)salesman.getHomeY() );
                    }   // end else if: last
                    // In between
                    else
                    {   g.drawLine(
                            (int)customers[i].getX(), 
                            (int)customers[i].getY(),
                            (int)customers[i+1].getX(), 
                            (int)customers[i+1].getY() );
                    }   // end else: in between
                }   // end for
            }   // end paint
        }; // end panel
        
        java.awt.Frame frame = new java.awt.Frame();
        frame.add( panel, java.awt.BorderLayout.CENTER );
        java.awt.Dimension dim = 
            java.awt.Toolkit.getDefaultToolkit().getScreenSize();
        int width, height;
        width = height = 420;
        frame.setBounds( 
            (dim.width-width)/2, 
            (dim.height-height)/2, 
            width, height );
        frame.addWindowListener( new java.awt.event.WindowAdapter()
        {   public void windowClosing( java.awt.event.WindowEvent evt )
            {   System.exit(0);
            }   // end windowClosing
        }); // end window adapter
        frame.show();
        
    }   // end showSolutionFrame
    
}   // end class Main

Running the Program

We're ready to compile and run the tutorial. Put your eight tutorial *.java files in one directory and copy the tabusearch25.jar file into that directory. Compile the program from a command prompt like this:


    javac -classpath .;tabusearch25.jar *.java

Now run the program with default settings like this:


    java -classpath .;tabusearch25.jar Main

You should see a window like this:

Not bad for such a simple tabu search.

 


by Robert Harder | Summary | Download now | Documentation | License | Top