algorithm.NQueens.java Source code

Java tutorial

Introduction

Here is the source code for algorithm.NQueens.java

Source

/**
 * Genetic Algorithm Research -- N Queens Puzzle
 *
 * Copyright (C) 2013, Jonathan Gillett, Joseph Heron, and Daniel Smullen
 * All rights reserved.
 *
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package algorithm;

import gameboard.QueenBoard;
import gameboard.QueenGame;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Random;

import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics;

import static org.kohsuke.args4j.ExampleMode.ALL;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.Option;

import com.google.common.primitives.Doubles;
import com.google.common.primitives.Ints;

public class NQueens {
    /* Command line arguments */
    @Option(name = "-p", usage = "Population size, default is 64")
    private static Integer populationSize = 64;

    @Option(name = "-i", usage = "Inbreeding percentage threshold, default is 0.15 (15%)")
    private static Double inbreedingThreshold = 0.15;

    @Option(name = "-m", usage = "The fixed mutation rate, not used by default")
    private static Double mutation;

    @Option(name = "-q", usage = "The number of queens, the default is 8")
    private static Integer numQueens = 8;

    @Option(name = "-g", usage = "The maximum number of generations before terminating, the default is 10 Million")
    private static Integer maxGenerations = 10000000;

    @Option(name = "-o", required = true, metaVar = "OUTPUT", usage = "The output directory to store the results")
    private static String outputDir;

    @Option(name = "-r", required = true, usage = "The program run number, a unique identifier")
    private static Integer runNumber;

    @Option(name = "-n", usage = "Multi-run mode, the number of times to re-run the program, default is 1")
    private static Integer numRuns = 1;

    @Option(name = "-s", usage = "Number of solutions to display, default is 10")
    private static Integer numDisplay = 10;

    @Argument
    private static List<String> arguments = new ArrayList<String>();

    /* Mapping of the distinct solutions for the N Queens problem, sequence A000170 from OEIS */
    private static int[] distinctSolutions = { 1, 0, 0, 2, 10, 4, 40, 92, 352, 724, 2680, 14200, 73712, 365596,
            2279184, 14772512, 95815104, 666090624 };

    private static ArrayList<Chromosome> population;
    private static ArrayList<Chromosome> solutions = new ArrayList<Chromosome>();
    private static Chromosome rotation;
    private static Chromosome reflection;

    /* Buffers for storing the fitness, mutation, and similarity for multiple generations */
    private static ArrayList<Double> fitnessBuffer = new ArrayList<Double>();
    private static ArrayList<Double> similarityBuffer = new ArrayList<Double>();
    private static ArrayList<Double> mutationBuffer = new ArrayList<Double>();
    private static HashMap<Integer, Integer> duplicateBuffer = new HashMap<Integer, Integer>();

    /* Data for calculating descriptive statistics */
    private static LinkedHashMap<Integer, Integer> solutionGeneration = new LinkedHashMap<Integer, Integer>();
    private static LinkedHashMap<Integer, Integer> rotationMiss = new LinkedHashMap<Integer, Integer>();
    private static LinkedHashMap<Integer, Integer> reflectionMiss = new LinkedHashMap<Integer, Integer>();
    private static LinkedHashMap<Integer, ArrayList<Double>> duplicateStats = new LinkedHashMap<Integer, ArrayList<Double>>();
    private static LinkedHashMap<Integer, Double> fitnessStats = new LinkedHashMap<Integer, Double>();
    private static LinkedHashMap<Integer, Double> similarityStats = new LinkedHashMap<Integer, Double>();
    private static LinkedHashMap<Integer, Double> mutationStats = new LinkedHashMap<Integer, Double>();

    private static Random random = new Random();
    private static Integer numGenerations = 1;
    private static Double curSimilarity = 0.0;
    private static Integer curIndex = 0;

    /**
     * Calculates statistics for the current data in the fitness, similarity, and mutation rate
     * buffers. After the statistics have been calculated they are added to the appropriate
     * fitnessStats, similarityStats, and mutationStats class members.
     * 
     * @param units The units for generations such as thousands (1000) or millions.
     */
    @SuppressWarnings("serial")
    public static void calcStatistics(int units) {
        /* Calculate fitness stats */
        DescriptiveStatistics descStats = new DescriptiveStatistics(Doubles.toArray(fitnessBuffer));
        fitnessStats.put((int) Math.ceil(numGenerations / (double) units), descStats.getMean());

        /* Calculate chromosome similarity stats */
        descStats = new DescriptiveStatistics(Doubles.toArray(similarityBuffer));
        similarityStats.put((int) Math.ceil(numGenerations / (double) units), descStats.getMean());

        /* Calculate mutation rate stats for variable mutation */
        if (mutation == null) {
            descStats = new DescriptiveStatistics(Doubles.toArray(mutationBuffer));
            mutationStats.put((int) Math.ceil(numGenerations / (double) units), descStats.getMean());
        }

        /* Calculate the duplicate solutions statistics */
        if (duplicateBuffer.size() > 0) {
            final DescriptiveStatistics descDupStats = new DescriptiveStatistics(
                    Doubles.toArray(duplicateBuffer.values()));
            duplicateStats.put((int) Math.ceil(numGenerations / (double) units), new ArrayList<Double>() {
                {
                    add((double) duplicateBuffer.size());
                    add(descDupStats.getMean());
                }
            });
        } else {
            duplicateStats.put((int) Math.ceil(numGenerations / (double) units), new ArrayList<Double>() {
                {
                    add(0.0);
                    add(0.0);
                }
            });
        }

        /* Clear the buffers */
        mutationBuffer.clear();
        fitnessBuffer.clear();
        similarityBuffer.clear();
        duplicateBuffer.clear();
    }

    /**
     * Writes the current results for the number of solutions found, as well as the record of
     * population fitness, mutation rate, and chromosome similarity to disk. After the results
     * have been saved the fitness, mutation rate, and chromosome similarity arrays are reset.
     */
    @SuppressWarnings("serial")
    public static void writeResults() {
        OutputWriter ow = new OutputWriter(outputDir);

        try {
            /* Write the number of solutions found and the generation */
            ow.saveResults(solutionGeneration, new ArrayList<String>() {
                {
                    add("solution");
                    add("generation");
                }
            }, "solution_generation_" + runNumber + ".csv");

            /* Write the number of duplicate statistics aggregated for every 1000 generations */
            ow.saveResultsMul(duplicateStats, new ArrayList<String>() {
                {
                    add("generation");
                    add("n");
                    add("duplicates");
                }
            }, "duplicate_solutions_" + runNumber + ".csv");

            /* Write the number of rotation misses found for each generation */
            ow.saveResults(rotationMiss, new ArrayList<String>() {
                {
                    add("generation");
                    add("misses");
                }
            }, "rotation_misses_" + runNumber + ".csv");

            /* Write the number of reflection misses found for each generation */
            ow.saveResults(reflectionMiss, new ArrayList<String>() {
                {
                    add("generation");
                    add("misses");
                }
            }, "reflection_misses_" + runNumber + ".csv");

            /* Write the fitness statistics aggregated for every 1000 generations */
            ow.saveResults(fitnessStats, new ArrayList<String>() {
                {
                    add("generation");
                    add("fitness");
                }
            }, "fitness_stats_" + runNumber + ".csv");

            /* Write the chromosome similarity statistics aggregated for every 1000 generations */
            ow.saveResults(similarityStats, new ArrayList<String>() {
                {
                    add("generation");
                    add("similarity");
                }
            }, "chromosome_similarity_" + runNumber + ".csv");

            /* Write the mutation rate statistics aggregated for every 1000 generations */
            if (mutation == null) {
                ow.saveResults(mutationStats, new ArrayList<String>() {
                    {
                        add("generation");
                        add("mutation");
                    }
                }, "mutation_rate_" + runNumber + ".csv");
            }
        }
        /* Exit if a write error occurs, no point in continuing with missing data */
        catch (IOException e) {
            System.err.println("FATAL ERROR: Failed to write results to file!");
            e.printStackTrace();
            System.exit(1);
        }

        /* Clear all of the data */
        solutionGeneration.clear();
        duplicateStats.clear();
        rotationMiss.clear();
        reflectionMiss.clear();
        fitnessStats.clear();
        similarityStats.clear();
        mutationStats.clear();
    }

    /**
     * Determines if the solution to the eight queens puzzle is unique if
     * so returns true.
     * 
     * @param chromosome The potential unique solution chromosome
     * @return True if the chromosome is a unique solution
     */
    public static boolean uniqueSolution(Chromosome chromosome) {
        boolean unique = true;

        for (Chromosome uniqChromosome : solutions) {
            for (int i = 0; i < uniqChromosome.size(); ++i) {
                /* If any of the chromosome genes differ the genes are unique */
                if (chromosome.get(i).compareTo(uniqChromosome.get(i)) == 0) {
                    /* Current genes match */
                    unique = false;
                } else {
                    unique = true;
                    break;
                }
            }

            /* If the current chromosome is not unique don't continue checking */
            if (!unique) {
                break;
            }
        }

        return unique;
    }

    /**
     * Calculates the percentage of chromosomes that are similar, this is an indication
     * about how much in-breeding has occurred.
     * 
     * @return Percentage of the number of chromosomes that are similar
     */
    public static Double similarChromosomes(ArrayList<Chromosome> chromosomes) {
        int similar = 0;
        boolean matched = false;
        String value = "";

        /* Create an array with a single string representation for each chromosome */
        ArrayList<String> stringChromosomes = new ArrayList<String>(populationSize);

        for (Chromosome chromosome : chromosomes) {
            value = "";

            /* For each gene compute the value */
            for (int i = 0; i < chromosome.size(); ++i) {
                value += chromosome.get(i).toString();
            }
            stringChromosomes.add(value);
        }

        /* Count the number of similar chromosomes based on their value */
        Collections.sort(stringChromosomes);
        for (int i = 0; i < stringChromosomes.size() - 1; ++i) {
            if (stringChromosomes.get(i).compareTo(stringChromosomes.get(i + 1)) == 0) {
                ++similar;
                matched = true;
            }
            /* Add an additional match for the last match in a set */
            else if (matched) {
                ++similar;
                matched = false;
            }

            /* Add an additional match if there is a match with the last item in list */
            if (matched && (i + 1 == stringChromosomes.size() - 1)) {
                ++similar;
            }
        }

        /* Calculate the percentage of chromosomes that were similar */
        if (similar > 1) {
            return (double) similar / (double) chromosomes.size();
        }

        return 0.0;
    }

    /**
     * Creates an initial population of uniformly random chromosomes
     */
    public static void initPopulation() {
        /* Create an array of uniformly random chromosomes for initial population */
        population = new ArrayList<Chromosome>(populationSize);

        while (population.size() < populationSize) {
            population.add(new Chromosome(random, numQueens));
        }
    }

    /**
     * In the case of mult-run mode resets the current program state so that another
     * fresh execution can be done.
     */
    public static void reset() {
        /* Reset chromosome population and solutions */
        population.clear();
        solutions.clear();

        /* Reset buffers */
        fitnessBuffer.clear();
        similarityBuffer.clear();
        mutationBuffer.clear();
        duplicateBuffer.clear();

        /* Reset statistics */
        solutionGeneration.clear();
        rotationMiss.clear();
        reflectionMiss.clear();
        duplicateStats.clear();
        fitnessStats.clear();
        similarityStats.clear();
        mutationStats.clear();

        /* Rest counters, indexes */
        random = new Random();
        numGenerations = 1;
        curSimilarity = 0.0;
        curIndex = 0;
    }

    public static void main(String[] args) throws InterruptedException, IOException {
        new NQueens().doMain(args);
    }

    public void doMain(String[] args) throws InterruptedException, IOException {
        CmdLineParser parser = new CmdLineParser(this);

        try {
            /* Parse the arguments */
            parser.parseArgument(args);
        } catch (CmdLineException e) {
            System.err.println(e.getMessage());
            System.err.println("java NQueens [options...] arguments...");
            parser.printUsage(System.err);

            /* Print program sample showing all of the options */
            System.err.println("\n Example: java NQueens" + parser.printExample(ALL));
            System.exit(1);
        }

        try {
            String resultantPath = "/" + numQueens + "_q" + "/";
            if (mutation == null) {
                resultantPath += "variable/";
            } else {
                resultantPath += mutation.toString() + "/";
            }

            outputDir += resultantPath;

            File dir = new File(outputDir);
            File figureDir = new File(outputDir + "/figures/");
            /* 
             * Returns true if all the directories are created
             * Returns false and the directories may have been made
             * (so far returns false every other time and it works every time)
             */
            dir.mkdirs();
            figureDir.mkdirs();
        } catch (Exception e) {
            System.err.println(e.getMessage());
            System.exit(1);
        }

        for (int count = 0; count < numRuns; ++count) {
            /* Create an initial population of uniformly random chromosomes */
            initPopulation();

            /* Initialize the Breed operation */
            if (mutation != null) {
                Breed.init(new Random(), mutation);
            } else {
                Breed.init(new Random());
            }

            /* Iterate until all of the solutions for the N queens problem has been found */
            while (solutions.size() < distinctSolutions[numQueens - 1] && numGenerations <= maxGenerations) {
                /* If the percentage of similar chromosomes due to in-breeding exceeds
                 * the minimum threshold value, increase the amount of mutation
                 */
                curSimilarity = similarChromosomes(population);
                if (mutation == null) {
                    if (curSimilarity >= inbreedingThreshold) {
                        Breed.inBreeding(true);
                    } else {
                        Breed.inBreeding(false);
                    }
                }

                /* Calculate the fitness distribution of the current population */
                HashMap<Chromosome, Double> fitness = Fitness.calculate(population);

                /* Instantiate the selection iterator using the fitness distribution,
                 * the selection iterator uses roulette wheel selection to select
                 * each chromosome.
                 */
                Selection selection = new Selection(new Random());
                selection.init(fitness);

                /* Generate the next population by selecting chromosomes from the current
                 * population using selection iterator and applying the cloning, crossover,
                 * and mutation operations.
                 */
                ArrayList<Chromosome> nextPopulation = new ArrayList<Chromosome>(populationSize);
                ArrayList<Chromosome> chromosomes = new ArrayList<Chromosome>(2);

                while (nextPopulation.size() < populationSize) {
                    /* Select a random number and apply the breeding operation */
                    Integer randomNum = random.nextInt(100);

                    /* Pair of parent chromosomes continue on to the next generation.*/
                    if (Breed.CLONING.contains(randomNum)) {
                        chromosomes.addAll(Breed.cloning(selection));
                    }
                    /* Pair of parent chromosomes are cross-overed to create new pair */
                    else if (Breed.CROSSOVER.contains(randomNum)) {
                        chromosomes.addAll(Breed.crossover(selection));
                    }

                    /* Apply the background mutation operator to the chromosomes */
                    for (Chromosome chromosome : chromosomes) {
                        randomNum = random.nextInt(100);

                        if (Breed.MUTATION.contains(randomNum)) {
                            nextPopulation.add(Breed.mutation(chromosome));
                        } else {
                            nextPopulation.add(chromosome);
                        }
                    }
                    chromosomes.clear();
                }

                /* If there are any solutions (fitness of 1) that are unique save them */
                for (Chromosome chromosome : fitness.keySet()) {
                    if (fitness.get(chromosome) == 1.0) {
                        if (uniqueSolution(chromosome)) {
                            /* Save a copy of the chromosome */
                            Chromosome solution = new Chromosome(new ArrayList<Integer>(chromosome.get()),
                                    chromosome.size());
                            solutions.add(solution);
                            solutionGeneration.put(solutions.size(), numGenerations);

                            /* Perform three rotations then a reflection followed by three more rotations */
                            for (int i = 0; i < 6; ++i) {
                                rotation = Transformation.rotate(solutions.get(solutions.size() - 1));

                                if (uniqueSolution(rotation)) {
                                    solutions.add(rotation);
                                    solutionGeneration.put(solutions.size(), numGenerations);
                                } else {
                                    if (rotationMiss.containsKey(numGenerations)) {
                                        rotationMiss.put(numGenerations, rotationMiss.get(numGenerations) + 1);
                                    } else {
                                        rotationMiss.put(numGenerations, 1);
                                    }
                                }

                                if (i == 2) {
                                    reflection = Transformation.reflect(solution);

                                    if (uniqueSolution(reflection)) {
                                        solutions.add(reflection);
                                        solutionGeneration.put(solutions.size(), numGenerations);
                                    } else {
                                        if (reflectionMiss.containsKey(numGenerations)) {
                                            reflectionMiss.put(numGenerations,
                                                    reflectionMiss.get(numGenerations) + 1);
                                        } else {
                                            reflectionMiss.put(numGenerations, 1);
                                        }
                                    }
                                }
                            }
                        } else {
                            if (duplicateBuffer.containsKey(numGenerations)) {
                                duplicateBuffer.put(numGenerations, duplicateBuffer.get(numGenerations) + 1);
                            } else {
                                duplicateBuffer.put(numGenerations, 1);
                            }
                        }
                    }
                }

                /* Save average fitness for the current generation */
                DescriptiveStatistics descStats = new DescriptiveStatistics(Doubles.toArray(fitness.values()));
                fitnessBuffer.add(descStats.getMean());

                /* Save chromosome similarity and mutation rate for current generation */
                similarityBuffer.add(curSimilarity);

                /* Save the variable mutation rate */
                if (mutation == null) {
                    mutationBuffer.add((Breed.MUTATION.upperEndpoint() - Breed.MUTATION.lowerEndpoint()) / 100.0);
                }

                /* Calculate statistics for the fitness, similarity, and mutation buffer every 1000, generations */
                if ((numGenerations % 1000) == 0) {
                    calcStatistics(1000);
                }

                /* Write the current results to file every 10,000 generations */
                if ((numGenerations % 10000) == 0) {
                    writeResults();
                }

                /* Set the current population as the NEXT population */
                fitness.clear();
                population = nextPopulation;

                ++numGenerations;
            }

            /* Calculate statistics and write any remaining results */
            if (fitnessBuffer.size() > 0) {
                calcStatistics(1000);
            }
            writeResults();

            /* Display random solutions for the number of solutions specified */
            for (int j = 0; j < numDisplay; ++j) {
                /* Display a random solution */
                Chromosome solution = solutions.get(random.nextInt(solutions.size()));

                try {
                    QueenGame myGame = new QueenGame(new QueenBoard(Ints.toArray(solution.get()), numQueens));
                    myGame.playGame(
                            outputDir + "/figures/" + "figure_run_" + String.valueOf(runNumber) + "_" + j + ".png");
                } catch (Exception e) {
                    System.out.println("Bad set of Queens");
                }
            }

            /* Reset the current state for the next run and increment run number */
            reset();
            ++runNumber;
        }
    }
}