diff --git a/src/main/java/io/onclave/nsga/ii/Interface/IObjectiveFunction.java b/src/main/java/io/onclave/nsga/ii/Interface/IObjectiveFunction.java index fbd92f0..11d3454 100644 --- a/src/main/java/io/onclave/nsga/ii/Interface/IObjectiveFunction.java +++ b/src/main/java/io/onclave/nsga/ii/Interface/IObjectiveFunction.java @@ -7,8 +7,14 @@ import io.onclave.nsga.ii.datastructure.ParetoObject; /** - * - * @author sajib + * this is an interface that each objective function object that is created must implement. + * all the methods must be overridden. + * without implementing this interface, no objective function object can be plugged into the + * algorithm as the algorithm will not understand the objective function from a generic level. + * + * @author Debabrata Acharya + * @version 1.0 + * @since 0.1 */ public interface IObjectiveFunction { public double objectiveFunction(double geneVaue); diff --git a/src/main/java/io/onclave/nsga/ii/algorithm/Algorithm.java b/src/main/java/io/onclave/nsga/ii/algorithm/Algorithm.java index 03b0dc7..b5f8e74 100644 --- a/src/main/java/io/onclave/nsga/ii/algorithm/Algorithm.java +++ b/src/main/java/io/onclave/nsga/ii/algorithm/Algorithm.java @@ -3,6 +3,7 @@ */ package io.onclave.nsga.ii.algorithm; +import io.onclave.nsga.ii.api.GraphPlot; import io.onclave.nsga.ii.api.Reporter; import io.onclave.nsga.ii.api.Service; import io.onclave.nsga.ii.api.Synthesis; @@ -13,38 +14,127 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import org.jfree.ui.RefineryUtilities; /** - * - * @author sajib + * This is the starting point of the main NSGA-II algorithm. + * Run this class to get the desired output. + * + * @author Debabrata Acharya + * @version 1.1 + * @since 0.1 */ public class Algorithm { + /** + * This method first prepares the multi-objectives that it is going to work with. In this case, + * it works with 2 objectives. At generation 0, a random initial population is generated and then + * sorted using non-dominated population sorting. Using this initial parent population, a child + * population is generated. As the initial parent and child population are created, new generations + * are simulated and at each generation, the following actions are carried out: + * 1: the present parent and child are combined to create a new population containing all + * chromosomes from both parent and child. This ensures elitism. + * 2: this new combined population is then sorted using the fast non-dominated sorting algorithm + * to get a list of chromosomes that are grouped according to their rank. The higher the rank, + * more desirable they are to be carried forward into the next generation. + * 3: an iteration is carried over all the ranks as follows: + * i: the list of chromosomes from the current iterated rank is taken into account. + * ii: the amount of free remaining space in the new child population is calculated. + * iii: a crowd comparison sort is done after assigning crowding distance to the chromosomes. + * iv: if the number of chromosomes in this rank is less than or equal to the amount of free + * space available in the new child population, then the whole chromosome cluster is added + * to the new population, else only the available number of chromosomes is added to the + * child population according to their crowding distance. This is done for diversity + * preservation. + * v: end. + * 4: the new synthesized populace is added to the new child population. + * 5: if this is the last generation, then the present child is shown as the Pareto Front, otherwise, + * the present child is labeled as the new parent for the next generation, and a new child + * population is generated from this newly labeled parent population. This combination now becomes + * the new parent/child for the next generation. + * 6: all the child from all the generations are added to the Graph Rendering engine to show all the + * child data as fronts for that generation. + * 7: end. + * the plotted graphs are viewed. + * + * @param args pass command line arguments. Not required to run this code. + * @see Plotted graphs of all fronts as well as the Pareto Front as output. + */ + public static void main(String[] args) { + /* prepares the objectives [See Configuration.java file for more information.] */ Configuration.buildObjectives(); + GraphPlot multiPlotGraph = new GraphPlot(); - Population parent = Service.nonDominatedPopulationSort(Synthesis.syntesizePopulation()); + /** + * a new random population is synthesized and sorted using non-dominated population sort to get + * a sorted list of parent chromosomes at generation 0. + * child population generated from parent population. + */ + Population parent = Service.nonDominatedPopulationSort(Synthesis.syntesizePopulation()); Population child = Synthesis.synthesizeChild(parent); + /** + * a loop is run that iterates as new generations are created (new child population is created from previous parent + * population. + * the number of generations to be simulated are defined in the Configuration.java file. + */ for(int i = 2; i <= Configuration.getGENERATIONS(); i++) { - Population combinedPopulation = Service.createCombinedPopulation(parent, child); - HashMap> paretoFront = Service.fastNonDominatedSort(combinedPopulation); + System.out.println("GENERATION : " + i); + + /** + * a combined population of both latest parent and child is created to ensure elitism. + * the combined population created is then sorted using fast non-dominated sorting algorithm, + * to create rank wise divisions [chromosomes with rank 1 (non-dominated), + * chromosomes with rank 2 (dominated by 1 chromosome), etc.] + * this information is stored in a HashMap data-structure that maps one integer value + * to one list of chromosomes. The integer refers to the rank number while the list refers + * to the chromosomes that belong to that rank. + */ + HashMap> rankedFronts = Service.fastNonDominatedSort(Service.createCombinedPopulation(parent, child)); Population nextChildPopulation = new Population(); List childPopulace = new ArrayList<>(); - for(int j = 1; j <= paretoFront.size(); j++) { + /** + * an iteration is carried over the HashMap to go through each rank of chromosomes, and the + * most desired chromosomes (higher ranks) are included into the child population of the + * next generation. + */ + for(int j = 1; j <= rankedFronts.size(); j++) { - List singularFront = paretoFront.get(j); + /** + * during iteration, the current ranked list of chromosomes is chosen and the amount of + * free space (to accommodate chromosomes) of the current child population is calculated + * to check whether chromosomes from this rank can be fit into the new child population. + */ + List singularFront = rankedFronts.get(j); int usableSpace = Configuration.getPOPULATION_SIZE() - childPopulace.size(); + /** + * if the new list of chromosomes is not null and if the child population has free usable space, + * then an attempt to include some or all of the chromosomes is made otherwise, there is no more + * space in the child population and hence no more rank/chromosome checks are done and the program + * breaks out from the inner for-loop. + */ if(singularFront != null && !singularFront.isEmpty() && usableSpace > 0) { + /** + * if the amount of usable space is more than or equal to the number of chromosomes in the clot, + * the whole clot of chromosomes is added to the child population/populace, otherwise, only the + * number of chromosomes that can be fit within the usable space is chosen according to the + * crowding distance of the chromosomes. + */ if(usableSpace >= singularFront.size()) childPopulace.addAll(singularFront); else { + /** + * a crowd comparison sort is carried over the present clot of chromosomes after assigning them a + * crowding distance (to preserve diversity) and hence a list of ParetoObjects are prepared. + * [refer ParetoObject.java for more information] + */ List latestFront = Service.crowdComparisonSort(Service.crowdingDistanceAssignment(singularFront)); for(int k = 0; k < usableSpace; k++) childPopulace.add(latestFront.get(k).getChromosome()); @@ -52,12 +142,37 @@ public static void main(String[] args) { } else break; } + /** + * the new populace is added to the new child population + */ nextChildPopulation.setPopulace(childPopulace); + /** + * if this iteration is not the last generation, the new child created is made the parent for the next + * generation, and a new child is synthesized from this new parent for the next generation. + * this is the new parent and child for the next generation. + * if this is the last generation, no new parent/child combination is created, instead the Pareto Front + * is plotted and rendered as the latest created child is the actual Pareto Front. + */ if(i < Configuration.getGENERATIONS()) { parent = child; child = Synthesis.synthesizeChild(nextChildPopulation); } else Reporter.render2DGraph(child); + + /** + * this adds the child of each generation to the plotting to render the front of all the generations. + */ + multiPlotGraph.prepareMultipleDataset(child, i, "generation " + i); } + + System.out.println("\n\n----CHECK PARETO FRONT OUTPUT----\n\n"); + + /** + * the plotted and rendered chart/graph is viewed to the user. + */ + multiPlotGraph.configureMultiplePlotter(Configuration.getXaxisTitle(), Configuration.getYaxisTitle(), "All Pareto"); + multiPlotGraph.pack(); + RefineryUtilities.centerFrameOnScreen(multiPlotGraph); + multiPlotGraph.setVisible(true); } } diff --git a/src/main/java/io/onclave/nsga/ii/api/GraphPlot.java b/src/main/java/io/onclave/nsga/ii/api/GraphPlot.java index 812ae1b..c920d74 100644 --- a/src/main/java/io/onclave/nsga/ii/api/GraphPlot.java +++ b/src/main/java/io/onclave/nsga/ii/api/GraphPlot.java @@ -8,6 +8,7 @@ import java.awt.BasicStroke; import java.awt.Color; import java.awt.Paint; +import java.util.Random; import org.jfree.chart.ChartFactory; import org.jfree.chart.ChartPanel; import org.jfree.chart.JFreeChart; @@ -19,12 +20,17 @@ import org.jfree.ui.ApplicationFrame; /** - * - * @author sajib + * this class is the under-the-hood service layer for generating the graphs using jFreeCharts library. + * + * @author Debabrata Acharya + * @version 1.1 + * @since 1.0 */ public class GraphPlot extends ApplicationFrame { private final static XYSeriesCollection DATASET = new XYSeriesCollection(); + private final static XYSeriesCollection MULTIPLE_DATASET = new XYSeriesCollection(); + private final static XYLineAndShapeRenderer MULTIPLE_RENDERER = new XYLineAndShapeRenderer(); private final static String APPLICATION_TITLE = "NSGA-II"; private static String GRAPH_TITLE = "PARETO FRONT"; private static String KEY = "Pareto Front"; @@ -32,6 +38,11 @@ public class GraphPlot extends ApplicationFrame { private static int DIMENSION_Y = 600; private static Paint COLOR = Color.RED; private static float STROKE_THICKNESS = 2.0f; + private static final Random RANDOM = new Random(); + + public GraphPlot() { + super(APPLICATION_TITLE); + } public GraphPlot(Population population) { @@ -39,13 +50,42 @@ public GraphPlot(Population population) { createDataset(population); } + public void prepareMultipleDataset(final Population population, final int datasetIndex, final String key) { + + createDataset(population, key, MULTIPLE_DATASET); + + MULTIPLE_RENDERER.setSeriesPaint(datasetIndex, new Color(RANDOM.nextFloat(), RANDOM.nextFloat(), RANDOM.nextFloat())); + MULTIPLE_RENDERER.setSeriesStroke(datasetIndex, new BasicStroke(STROKE_THICKNESS)); + } + + public void configureMultiplePlotter(final String x_axis, final String y_axis, final String graphTitle) { + + JFreeChart xyLineChart = ChartFactory.createXYLineChart(graphTitle, x_axis, y_axis, MULTIPLE_DATASET, PlotOrientation.VERTICAL, true, true, false); + ChartPanel chartPanel = new ChartPanel(xyLineChart); + + chartPanel.setPreferredSize(new java.awt.Dimension(DIMENSION_X, DIMENSION_Y)); + + final XYPlot plot = xyLineChart.getXYPlot(); + + plot.setRenderer(MULTIPLE_RENDERER); + setContentPane(chartPanel); + } + private void createDataset(final Population population) { + createDataset(population, KEY); + } + + private void createDataset(final Population population, String key) { + createDataset(population, key, DATASET); + } + + private void createDataset(final Population population, String key, XYSeriesCollection dataset) { - final XYSeries paretoFront = new XYSeries(KEY); + final XYSeries paretoFront = new XYSeries(key); population.getPopulace().stream().forEach((c) -> { paretoFront.add(Configuration.getObjectives().get(0).objectiveFunction(c), Configuration.getObjectives().get(1).objectiveFunction(c)); }); - DATASET.addSeries(paretoFront); + dataset.addSeries(paretoFront); } public void configurePlotter(final String x_axis, final String y_axis) { diff --git a/src/main/java/io/onclave/nsga/ii/api/Reporter.java b/src/main/java/io/onclave/nsga/ii/api/Reporter.java index ee8600a..57bd958 100644 --- a/src/main/java/io/onclave/nsga/ii/api/Reporter.java +++ b/src/main/java/io/onclave/nsga/ii/api/Reporter.java @@ -12,8 +12,12 @@ import org.jfree.ui.RefineryUtilities; /** - * - * @author sajib + * this is the add-on class that communicates with the console and prints appropriate object + * details as necessary. + * + * @author Debabrata Acharya + * @version 1.1 + * @since 1.0 */ public class Reporter { @@ -37,7 +41,8 @@ public static void reportPopulation(Population population) { int i = 1; - for(Chromosome chromosome : population.getPopulace()) p("Chromosome " + i++ + " : " + chromosome.getUniqueID() + " | " + chromosome.getFitness()); + for(Chromosome chromosome : population.getPopulace()) + p("Chromosome " + i++ + " : " + chromosome.getUniqueID() + " | " + chromosome.getFitness()); } public static void reportGeneticCode(Allele[] geneticCode) { diff --git a/src/main/java/io/onclave/nsga/ii/api/Service.java b/src/main/java/io/onclave/nsga/ii/api/Service.java index 492b302..22a7ea2 100644 --- a/src/main/java/io/onclave/nsga/ii/api/Service.java +++ b/src/main/java/io/onclave/nsga/ii/api/Service.java @@ -16,64 +16,156 @@ import java.util.concurrent.ThreadLocalRandom; /** - * - * @author sajib + * This is the service class that does most of the under-the-hood work that is abstracted/encapsulated + * by other classes at the business/controller layer. + * + * @author Debabrata Acharya + * @version 1.1 + * @since 0.1 */ public class Service { + /** + * this is an implementation of the fast non-dominated sorting algorithm as defined in the + * NSGA-II paper [DOI: 10.1109/4235.996017] Section III Part A. + * + * @param population the population object that needs to undergo fast non-dominated sorting algorithm + * @return a HashMap with an integer key that labels the ranks and a list of chromosomes as values that clot chromosomes of same rank + */ public static HashMap> fastNonDominatedSort(Population population) { HashMap> paretoFront = new HashMap<>(); List singularFront = new ArrayList<>(); List populace = population.getPopulace(); + /** + * iterating over each chromosome of the population + */ for(Chromosome chromosome : populace) { + /** + * an initial domination rank of 0 is set for each chromosome and a blank list is set for the number of + * chromosomes that the present chromosome dominates. + */ chromosome.setDominationRank(0); chromosome.setDominatedChromosomes(new ArrayList<>()); + /** + * for each chromosome, the program iterates over all the other remaining chromosomes to find which other + * chromosomes are dominated by this chromosome and vice versa. + */ for (Chromosome competitor : populace) if(!competitor.equals(chromosome)) { - if(dominates(chromosome, competitor)) { if(!chromosome.getDominatedChromosomes().contains(competitor)) chromosome.getDominatedChromosomes().add(competitor); } - else if(dominates(competitor, chromosome)) chromosome.setDominationRank(chromosome.getDominationRank() + 1); + + /** + * if the present chromosome dominates the competitor, then: + * i: check if the competitor already exists in the list of dominated chromosomes of the present chromosome. + * ii: if the competitor does not exist within the list, then add it to the list of dominated chromosomes + * of the present chromosome. + * else, if the competitor dominates the present chromosome, then increment the domination rank of the present + * chromosome by one. + */ + if(dominates(chromosome, competitor)) { + if(!chromosome.getDominatedChromosomes().contains(competitor)) chromosome.getDominatedChromosomes().add(competitor); + } else if(dominates(competitor, chromosome)) chromosome.setDominationRank(chromosome.getDominationRank() + 1); } + /** + * if the domination rank of the present chromosome is 0, it means that this chromosome is a non-dominated chromosome + * and hence it is added to the clot of chromosomes that are also non-dominated. + */ if(chromosome.getDominationRank() == 0) singularFront.add(chromosome); } + /** + * the first clot of non-dominated chromosomes is added to the HashMap with rank label 1. + */ paretoFront.put(1, singularFront); int i = 1; List previousFront = paretoFront.get(i); List nextFront = new ArrayList<>(); + /** + * the current/previous ranked clot of chromosomes with rank i is iterated over to find the next clot of chromosomes + * with rank (i+1) + */ while(previousFront != null && !previousFront.isEmpty()) { - Reporter.reportSingularFront(previousFront, i); - + /** + * iterating over each chromosome from the previous clot of chromosomes ranked i. + */ for(Chromosome chromosome : previousFront) { + /** + * iterating over each of the dominated chromosomes from the present chromosome of rank i. + */ for(Chromosome recessive : chromosome.getDominatedChromosomes()) { + /** + * if the domination rank of the current recessive chromosome in consideration is not 0, then + * decrement it's rank by 1. + * if the domination rank of the current recessive chromosome in consideration is 0, then add + * it to the next front [clot of chromosomes that belong to rank (i+1)]. + */ if(recessive.getDominationRank() != 0) recessive.setDominationRank(recessive.getDominationRank() - 1); if(recessive.getDominationRank() == 0) if(!nextFront.contains(recessive)) nextFront.add(recessive); } } + /** + * this code snippet ensures "rank jumps" to create all the possible rank lists from the parent + * population. + * new ranks are created only when there are recessive chromosomes with domination rank = 1 which are + * decremented to domination rank 0 and then added to the next front. + * but, due to the randomness of the algorithm, situation may occur such that even after decrementing all recessive + * chromosome domination ranks by 1, none have domination rank 0 and hence the next front remains empty. + * to ensure that all recessive chromosomes are added to some rank list, the program jumps domination ranks + * of each recessive chromosome by decrementing domination rank by 1 until at least one of them reaches a + * domination rank count of 0 and then that recessive chromosome is added to the next front. + * + * if the next front is empty and the previous front has at least one dominated chromosome: + * i: find the minimum rank among all the recessive chromosomes available: + * 1: iterate over all the chromosomes of the previous front + * 2: while the chromosomes have no dominated chromosomes with rank 0: + * a: iterate over all the recessive chromosomes of the current chromosome + * b: if the minimum rank is greater than the dominated rank of the present recessive, + * mark this as the minimum rank recorded among all recessive chromosomes available. + * 3: end while + * ii: iterate over all the chromosomes of the previous front + * 1: while the chromosomes have no dominated chromosomes with rank 0: + * a: iterate over all the dominated chromosomes of the current chromosome + * b: if the domination rank of the recessive chromosome is not 0, then decrement the + * domination count by value of minimum rank. + * c: if the domination rank is 0, then add it to the next front. + * 2: end while + */ if(nextFront.isEmpty() && !isDominatedChromosomesEmpty(previousFront)) { - Chromosome chromosome = previousFront.get(0); + int minimumRank = -1; - while(hasRecessiveRankGreaterThanZero(chromosome)) { - - for(Chromosome recessive : chromosome.getDominatedChromosomes()) { - if(recessive.getDominationRank() != 0) recessive.setDominationRank(recessive.getDominationRank() - 1); - if(recessive.getDominationRank() == 0) if(!nextFront.contains(recessive)) nextFront.add(recessive); + for(Chromosome chromosome : previousFront) + while(hasRecessiveRankGreaterThanZero(chromosome)) + for(Chromosome recessive : chromosome.getDominatedChromosomes()) + if((minimumRank == -1) || minimumRank > recessive.getDominationRank()) minimumRank = recessive.getDominationRank(); + + if(minimumRank != -1) for(Chromosome chromosome : previousFront) + while(hasRecessiveRankGreaterThanZero(chromosome)) for(Chromosome recessive : chromosome.getDominatedChromosomes()) { + if(recessive.getDominationRank() != 0) recessive.setDominationRank(recessive.getDominationRank() - minimumRank); + if(recessive.getDominationRank() == 0) if(!nextFront.contains(recessive)) nextFront.add(recessive); } - } } + /** + * if the next front calculated is not empty, then it is added to the ranked HashMap data-structure + * with the rank (i+1), else all chromosomes are sorted into some rank or the other and the program + * breaks out of the loop. + */ if(!nextFront.isEmpty()) paretoFront.put(++i, nextFront); else break; + /** + * the next front (i) calculated is marked as the previous front for the next iteration (i+1) and + * an empty next front is created. + */ previousFront = nextFront; nextFront = new ArrayList<>(); } @@ -81,26 +173,56 @@ public static HashMap> fastNonDominatedSort(Population return paretoFront; } + /** + * this is the implementation of the crowding distance assignment algorithm as defined in the + * NSGA-II paper [DOI: 10.1109/4235.996017] Section III Part B. + * this ensures diversity preservation. + * + * @param singularFront a list of chromosomes whose crowding distances are to be calculated + * @return a list of ParetoObjects with assigned crowding distances. [Refer ParetoObject.java for more information] + */ public static List crowdingDistanceAssignment(List singularFront) { int i = 0; int end = singularFront.size() - 1; + Double maxObjectiveValue; + Double minObjectiveValue; List objectives = Configuration.getObjectives(); - final float INFINITE_CROWDING_DISTANCE = 9999f; List singlePareto = new ArrayList<>(); + /** + * for each chromosome in the input list, a new ParetoObject with an initial crowding distance of 0 + * is created and added to the list of ParetoObjects that are to be returned. + */ for(Chromosome chromosome : singularFront) singlePareto.add(i++, new ParetoObject(chromosome, 0f)); + /** + * iterating over each of the objective functions set [refer Configuration.java for more information], + * the ParetoObject list is sorted according to the objective functions and the first and last ParetoObjects + * are set a crowding distance of infinity. + */ for(IObjectiveFunction objective : objectives) { + maxObjectiveValue = null; + minObjectiveValue = null; singlePareto = sort(singlePareto, objective); - singlePareto.get(0).setCrowdingDistance(INFINITE_CROWDING_DISTANCE); - singlePareto.get(end).setCrowdingDistance(INFINITE_CROWDING_DISTANCE); + singlePareto.get(0).setCrowdingDistance(Double.MAX_VALUE); + singlePareto.get(end).setCrowdingDistance(Double.MAX_VALUE); - double maxObjectiveValue = objective.objectiveFunction(singlePareto.get(0)); - double minObjectiveValue = objective.objectiveFunction(singlePareto.get(end)); + /** + * the max and min objective values are calculated according to the present objective function + */ + for(ParetoObject paretoObject : singlePareto) { + + if((maxObjectiveValue == null) || (maxObjectiveValue < objective.objectiveFunction(paretoObject))) maxObjectiveValue = objective.objectiveFunction(paretoObject); + if((minObjectiveValue == null) || (minObjectiveValue > objective.objectiveFunction(paretoObject))) minObjectiveValue = objective.objectiveFunction(paretoObject); + } + /** + * the crowding distance of all ParetoObjects are calculated and assigned except the first and last ParetoObjects + * that have infinite crowding distance + */ for(i = 2; i < end; i++) singlePareto.get(i).setCrowdingDistance(calculateCrowdingDistance(singlePareto, i, objective, @@ -111,33 +233,76 @@ public static List crowdingDistanceAssignment(List sin return singlePareto; } + /** + * this method sorts a list of ParetoObjects based on the Crowd-Comparison Operator using the domination + * rank and crowding distance as discussed in the NSGA-II paper [DOI: 10.1109/4235.996017] Section III Part B. + * + * @param singleFront a list of ParetoObjects that are to be sorted according to their crowding distance + * @return a list of sorted ParetoObjects + */ public static List crowdComparisonSort(List singleFront) { - int i = 0; + int index = -1; List sortedFront = new ArrayList<>(); - - for(ParetoObject paretoObject : singleFront) { + ParetoObject presentParetoObject; + ParetoObject competitor; + + /** + * all the ParetoObjects are, at first, marked as false for crowding distance sorted. + */ + singleFront.stream().forEach((paretoObject) -> { paretoObject.setCrowdingDistanceSorted(false); }); + + /** + * iterating over each ParetoObject in the singular front input: + * i: the i-th ParetoObject is marked as presentParetoObject + * ii: if the presentParetoObject is not already sorted by crowding distance: + * 1: iterate over the rest of the ParetoObjects in the input list as competitors that are + * not already sorted using crowding distance + * 2: compare the i-th and the j-th chromosome using the crowd comparison operator: + * a: for different ranks, choose the one with the lower (better) rank. + * b: for same rank, choose the one which has lower crowding distance. + * 3: if competitor dominates the i-th chromosome, then mark competitor as presentParetoObject + * 4: continue until i-th chromosome is compared to all competitors. + * 5: mark the presentParetoObject as already sorted by crowding distance + * 6: add presentParetoObject into list of sorted front with an incremented index + */ + for(int i = 0; i < singleFront.size(); i++) { - ParetoObject presentParetoObject = paretoObject; - int index = singleFront.indexOf(paretoObject); + presentParetoObject = singleFront.get(i); - for(ParetoObject competitor : singleFront) { + if(!presentParetoObject.isCrowdingDistanceSorted()) { - if(!((presentParetoObject.getChromosome().getDominationRank() < competitor.getChromosome().getDominationRank()) - || ((presentParetoObject.getChromosome().getDominationRank() == competitor.getChromosome().getDominationRank()) - && (presentParetoObject.getCrowdingDistance() > competitor.getCrowdingDistance())))) { + for(int j = 0; j < singleFront.size(); j++) { + + competitor = singleFront.get(j); - presentParetoObject = competitor; - index = singleFront.indexOf(competitor); + if(!competitor.isCrowdingDistanceSorted()) { + + double dominationRank = presentParetoObject.getChromosome().getDominationRank(); + double competingDominationRank = competitor.getChromosome().getDominationRank(); + double crowdingDistance = presentParetoObject.getCrowdingDistance(); + double competingCrowdingDistance = competitor.getCrowdingDistance(); + + if(i != j) if((dominationRank > competingDominationRank) || ((dominationRank == competingDominationRank) && (crowdingDistance < competingCrowdingDistance))) presentParetoObject = competitor; + } } + + presentParetoObject.setCrowdingDistanceSorted(true); + sortedFront.add(++index, presentParetoObject); } - - sortedFront.add(i++, singleFront.get(index)); } return sortedFront; } + /** + * this method is not implemented, as it is not absolutely necessary for this algorithm to work. + * is kept if implementation is needed in future. + * returns the same unsorted parent population as of now. + * + * @param population the population that is to be sorted + * @return a sorted population + */ public static Population nonDominatedPopulationSort(Population population) { //--TO-DO-- @@ -145,15 +310,48 @@ public static Population nonDominatedPopulationSort(Population population) { return population; } + /** + * this method checks whether competitor1 dominates competitor2. + * requires that none of the values of the objective functions using competitor1 is smaller + * than the values of the objective functions using competitor2. + * at least one of the values of the objective functions using competitor1 is greater than + * the corresponding value of the objective functions using competitor2. + * + * @param competitor1 the chromosome that may dominate + * @param competitor2 the chromosome that may be dominated + * @return boolean logic whether competitor1 dominates competitor2. + */ public static boolean dominates(final Chromosome competitor1, final Chromosome competitor2) { + /** + * getting the list of configured objectives from Configuration.java + */ List objectives = Configuration.getObjectives(); + /** + * checks the negation of the predicate [none of the values of objective functions using competitor1 + * is less than values of objective functions using competitor2] meaning that at least one of the values + * of the objective functions using competitor1 is less than the values of the objective functions using + * competitor2, hence returning false as competitor1 does not dominate competitor2 + */ if (!objectives.stream().noneMatch((objective) -> (objective.objectiveFunction(competitor1) < objective.objectiveFunction(competitor2)))) return false; + /** + * returns the value of the predicate [at least one of the values of the objective functions using + * competitor1 is greater than the corresponding value of the objective function using competitor2] + */ return objectives.stream().anyMatch((objective) -> (objective.objectiveFunction(competitor1) > objective.objectiveFunction(competitor2))); } + /** + * the list is first converted to an array data-structure and then a randomized quick sort + * algorithm is followed. + * the resulting sorted array is again converted to a List data-structure before returning. + * + * @param singlePareto the list of ParetoObjects that are to be sorted. + * @param objective the objective function using which the ParetoObjects are sorted. + * @return sorted list of ParetoObjects. + */ private static List sort(List singlePareto, IObjectiveFunction objective) { ParetoObject[] paretoArray = new ParetoObject[singlePareto.size()]; @@ -164,6 +362,16 @@ private static List sort(List singlePareto, IObjecti return (new ArrayList<>(Arrays.asList(paretoArray))); } + /** + * refer [https://jordanspencerwu.github.io/randomized-quick-sort/] for more details on randomized + * quick sort algorithm. + * + * @param paretoArray the array to be sorted + * @param head the pointer/position of the head element + * @param tail the pointer/position of the tail element + * @param objective the objective function depending on which the sort is to take place + * @return the pivot index. + */ private static int partition(ParetoObject[] paretoArray, int head, int tail, IObjectiveFunction objective) { ParetoObject pivot = paretoArray[tail]; @@ -187,6 +395,16 @@ private static int partition(ParetoObject[] paretoArray, int head, int tail, IOb return (i + 1); } + /** + * refer [https://jordanspencerwu.github.io/randomized-quick-sort/] for more details on randomized + * quick sort algorithm. + * + * @param paretoArray the array to be sorted + * @param head the pointer/position of the head element + * @param tail the pointer/position of the tail element + * @param objective the objective function depending on which the sort is to take place + * @return the random partition position index. + */ private static int randomizedPartition(ParetoObject[] paretoArray, int head, int tail, IObjectiveFunction objective) { int random = ThreadLocalRandom.current().nextInt(head, tail + 1); @@ -198,6 +416,15 @@ private static int randomizedPartition(ParetoObject[] paretoArray, int head, int return partition(paretoArray, head, tail, objective); } + /** + * refer [https://jordanspencerwu.github.io/randomized-quick-sort/] for more details on randomized + * quick sort algorithm. + * + * @param paretoArray the array to be sorted + * @param head the pointer/position of the head element + * @param tail the pointer/position of the tail element + * @param objective the objective function depending on which the sort is to take place + */ private static void randomizedQuickSort(ParetoObject[] paretoArray, int head, int tail, IObjectiveFunction objective) { if(tail < head) { @@ -209,6 +436,24 @@ private static void randomizedQuickSort(ParetoObject[] paretoArray, int head, in } } + /** + * implementation of crowding distance calculation as defined in NSGA-II paper + * [DOI: 10.1109/4235.996017] Section III Part B. + * + * I[i]distance = I[i]distance + (I[i+1].m - I[i-1].m)/(f-max - f-min) + * + * I[i]distance = crowding distance of the i-th individual + * I[i+1].m = m-th objective function value of the (i+1)-th individual + * I[i-1].m = m-th objective function value of the (i-1)-th individual + * f-max, f-min = maximum and minimum values of the m-th objective function + * + * @param singlePareto the list of ParetoObjects + * @param presentIndex the present index of ParetoObject whose crowding distance is to be calculated + * @param objective the objective function over which the value of i-th individual is to be calculated + * @param maxObjectiveValue the maximum value for this objective function + * @param minObjectiveValue the minimum value for this objective function + * @return the crowding distance + */ private static double calculateCrowdingDistance(List singlePareto, final int presentIndex, final IObjectiveFunction objective, @@ -218,28 +463,42 @@ private static double calculateCrowdingDistance(List singlePareto, return ( singlePareto.get(presentIndex).getCrowdingDistance() + ((objective.objectiveFunction(singlePareto.get(presentIndex + 1)) - + objective.objectiveFunction(singlePareto.get(presentIndex - 1))) / (maxObjectiveValue - minObjectiveValue)) + - objective.objectiveFunction(singlePareto.get(presentIndex - 1))) / (maxObjectiveValue - minObjectiveValue)) ); } + /** + * checks whether any of the dominated chromosome list of the given front is empty, + * returns true if at least one set of dominated chromosomes is not non-empty. + * + * @param front list of chromosomes whose dominated chromosomes are to be checked + * @return boolean logic whether the dominated chromosomes are empty + */ private static boolean isDominatedChromosomesEmpty(List front) { - - if(front.isEmpty()) return true; - else if (!front.stream().noneMatch((chromosome) -> (!chromosome.getDominatedChromosomes().isEmpty()))) return false; - - return true; + return front.stream().anyMatch((chromosome) -> (!chromosome.getDominatedChromosomes().isEmpty())); } + /** + * checks if any of the dominated chromosomes of the input chromosome has a domination rank of 0, + * returns true if at least one dominated chromosome contains domination rank 0. + * + * @param chromosome chromosome to check whether it contains any dominated chromosome with rank 0 + * @return boolean logic whether dominated chromosomes contain rank 0. + */ private static boolean hasRecessiveRankGreaterThanZero(Chromosome chromosome) { - List recessives = chromosome.getDominatedChromosomes(); - - if(recessives.isEmpty()) return false; - else if (!recessives.stream().noneMatch((recessive) -> (recessive.getDominationRank() == 0))) return false; + if(chromosome.getDominatedChromosomes().isEmpty()) return false; - return true; + return chromosome.getDominatedChromosomes().stream().noneMatch((recessive) -> (recessive.getDominationRank() == 0)); } + /** + * the child and parent population is combined to create a larger population pool + * + * @param parent parent population + * @param child child population + * @return combined parent + child population + */ public static Population createCombinedPopulation(Population parent, Population child) { List combinedPopulace = new ArrayList<>(); @@ -252,6 +511,13 @@ public static Population createCombinedPopulation(Population parent, Population return combinedPopulation; } + /** + * this method decodes the genetic code that is represented as a string of binary values, converted into + * decimal value. + * + * @param geneticCode the genetic code as an array of Allele. Refer Allele.java for more information + * @return the decimal value of the corresponding binary string. + */ public static double decodeGeneticCode(final Allele[] geneticCode) { double value = 0; @@ -263,18 +529,40 @@ public static double decodeGeneticCode(final Allele[] geneticCode) { return value; } + /** + * fitness is calculated using min-max normalization + * + * @param geneticCode the genetic code whose fitness is to be calculated + * @return the corresponding calculated fitness + */ public static double calculateFitness(Allele[] geneticCode) { return minMaxNormalization(decodeGeneticCode(geneticCode)); } + /** + * an implementation of min-max normalization + * + * @param value the value that is to be normalized + * @return the normalized value + */ private static double minMaxNormalization(final double value) { return (((value - Configuration.ACTUAL_MIN) / (Configuration.ACTUAL_MAX - Configuration.ACTUAL_MIN)) * (Configuration.NORMALIZED_MAX - Configuration.NORMALIZED_MIN)) + Configuration.NORMALIZED_MIN; } + /** + * used to generate a random integer value + * + * @return a random integer value + */ public static int generateRandomInt() { return ThreadLocalRandom.current().nextInt(); } + /** + * a short hand for System.out.println(). + * + * @param string the string to print to console. + */ public static void p(String string) { System.out.println(string); } diff --git a/src/main/java/io/onclave/nsga/ii/api/Synthesis.java b/src/main/java/io/onclave/nsga/ii/api/Synthesis.java index daff242..d676dbe 100644 --- a/src/main/java/io/onclave/nsga/ii/api/Synthesis.java +++ b/src/main/java/io/onclave/nsga/ii/api/Synthesis.java @@ -12,17 +12,30 @@ import java.util.Random; /** - * - * @author sajib + * This is the synthesis class that does many of the under-the-hood work (biological simulation) that is abstracted/encapsulated + * by other classes at the business/controller layer. + * + * @author Debabrata Acharya + * @version 1.1 + * @since 0.2 */ public class Synthesis { private static final Random LOCAL_RANDOM = new Random(); + /** + * depending on the settings available in the Configuration.java file, this method synthesizes a + * random population of chromosomes with pseudo-randomly generated genetic code for each chromosome. + * + * @return a randomly generated population + */ public static Population syntesizePopulation() { List populace = new ArrayList<>(); + /** + * the number of chromosomes in the population is received from the Configuration.java file + */ for(int i = 0; i < Configuration.getPOPULATION_SIZE(); i++) { Chromosome chromosome = new Chromosome(); @@ -33,18 +46,43 @@ public static Population syntesizePopulation() { return new Population(populace); } + /** + * a child population of the same size as the parent is synthesized from the parent population + * + * @param parent the parent population object + * @return a child population synthesized from the parent population + */ public static Population synthesizeChild(Population parent) { Population child = new Population(); List populace = new ArrayList<>(); - while(populace.size() < Configuration.getPOPULATION_SIZE()) for(Chromosome chromosome : crossover(binaryTournamentSelection(parent), binaryTournamentSelection(parent))) populace.add(mutation(chromosome)); + /** + * child chromosomes undergo crossover and mutation. + * the child chromosomes are selected using binary tournament selection. + * crossover returns an array of exactly two child chromosomes synthesized from two parent + * chromosomes. + */ + while(populace.size() < Configuration.getPOPULATION_SIZE()) + for(Chromosome chromosome : crossover(binaryTournamentSelection(parent), binaryTournamentSelection(parent))) + populace.add(mutation(chromosome)); child.setPopulace(populace); return child; } + /** + * this is an implementation of basic binary tournament selection. + * for a tournament of size t, select t individuals (randomly) from population and determine winner of + * tournament with the highest fitness value. + * in case of binary tournament selection, t = 2. + * + * refer [https://stackoverflow.com/questions/36989783/binary-tournament-selection] for more information. + * + * @param population the population from which a child chromosome is to be selected + * @return the selected child chromosome + */ private static Chromosome binaryTournamentSelection(Population population) { Chromosome individual1 = population.getPopulace().get(LOCAL_RANDOM.nextInt(population.getPopulace().size())); @@ -53,6 +91,16 @@ private static Chromosome binaryTournamentSelection(Population population) { if(individual1.getFitness() > individual2.getFitness()) return individual1; else return individual2; } + /** + * this is a basic implementation of uniform crossover where the crossover/break point is the middle + * of the chromosomes. The genetic code of both the parent chromosomes are broken from the middle + * and crossover is done to create two child chromosomes. + * crossover probability is considered. + * + * @param chromosome1 the first parent chromosome taking part in crossover + * @param chromosome2 the second parent chromosome taking part in crossover + * @return an array of exactly two child chromosomes synthesized from two parent chromosomes. + */ public static Chromosome[] crossover(Chromosome chromosome1, Chromosome chromosome2) { Allele[] geneticCode1 = new Allele[Configuration.getCHROMOSOME_LENGTH()]; @@ -62,6 +110,11 @@ public static Chromosome[] crossover(Chromosome chromosome1, Chromosome chromoso Chromosome[] childChromosomes = {new Chromosome(), new Chromosome()}; int breakPoint = Configuration.getCHROMOSOME_LENGTH() / 2; + /** + * generating a new random float value and if this value is less than equal to the + * crossover probability mentioned in the Configuration file, then crossover occurs, + * otherwise the parents themselves are copied as child chromosomes. + */ if(LOCAL_RANDOM.nextFloat() <= Configuration.getCROSSOVER_PROBABILITY()) { for(int i = 0; i < Configuration.getCHROMOSOME_LENGTH(); i++) { @@ -85,6 +138,15 @@ public static Chromosome[] crossover(Chromosome chromosome1, Chromosome chromoso return childChromosomes; } + /** + * in this mutation operation implementation, a random bit-flip takes place. + * a random float value is generated and if this value is less than equal to the mutation + * probability defined in Configuration, then mutation takes place, otherwise the original + * chromosome is returned. + * + * @param chromosome the chromosome over which the mutation takes place + * @return the mutated chromosome + */ private static Chromosome mutation(Chromosome chromosome) { if(LOCAL_RANDOM.nextFloat() <= Configuration.getMUTATION_PROBABILITY()) { @@ -97,6 +159,13 @@ private static Chromosome mutation(Chromosome chromosome) { return chromosome; } + /** + * a genetic code as an array of Alleles is synthesized. + * refer Allele.java for more information. + * + * @param length the required length of the genetic code. + * @return the synthesized genetic code. + */ public static Allele[] synthesizeGeneticCode(final int length) { Allele[] geneticCode = new Allele[length]; @@ -106,6 +175,11 @@ public static Allele[] synthesizeGeneticCode(final int length) { return geneticCode; } + /** + * an allele object with a randomly selected boolean gene value is synthesized. + * + * @return a randomly generated Allele object + */ public static Allele synthesizeAllele() { return new Allele(LOCAL_RANDOM.nextBoolean()); } diff --git a/src/main/java/io/onclave/nsga/ii/configuration/Configuration.java b/src/main/java/io/onclave/nsga/ii/configuration/Configuration.java index b8c797b..d4ef5b8 100644 --- a/src/main/java/io/onclave/nsga/ii/configuration/Configuration.java +++ b/src/main/java/io/onclave/nsga/ii/configuration/Configuration.java @@ -10,22 +10,27 @@ import java.util.List; /** - * - * @author sajib + * this is the Configuration file for the algorithm, where all the values are set and the initial + * configurations are set and run. + * to change any aspect of the algorithm, this file may be tweaked. + * + * @author Debabrata Acharya + * @version 1.0 + * @since 0.1 */ public class Configuration { private static final int POPULATION_SIZE = 1000; - private static final int GENERATIONS = 10; - private static final int CHROMOSOME_LENGTH = 8; + private static final int GENERATIONS = 50; + private static final int CHROMOSOME_LENGTH = 30; private static final float CROSSOVER_PROBABILITY = 0.7f; private static final float MUTATION_PROBABILITY = 0.03f; private static List objectives = null; - public static final int ACTUAL_MIN = 0; - public static final int ACTUAL_MAX = 255; - public static final int NORMALIZED_MIN = 0; - public static final int NORMALIZED_MAX = 2; + public static final double ACTUAL_MIN = 0; + public static final double ACTUAL_MAX = Math.pow(2, CHROMOSOME_LENGTH) - 1; + public static final double NORMALIZED_MIN = 0; + public static final double NORMALIZED_MAX = 2; public static final String DEFAULT_X_AXIS_TITLE = "x-axis"; public static final String DEFAULT_Y_AXIS_TITLE = "y-axis"; @@ -41,6 +46,10 @@ public static int getCHROMOSOME_LENGTH() { return CHROMOSOME_LENGTH; } + /** + * this method sets the objective functions over which the algorithm is to operate. + * it is a list of IObjectionFunction objects. + */ public static void buildObjectives() { List newObjectives = new ArrayList<>(); diff --git a/src/main/java/io/onclave/nsga/ii/datastructure/Allele.java b/src/main/java/io/onclave/nsga/ii/datastructure/Allele.java index f52345b..abc04ff 100644 --- a/src/main/java/io/onclave/nsga/ii/datastructure/Allele.java +++ b/src/main/java/io/onclave/nsga/ii/datastructure/Allele.java @@ -4,8 +4,12 @@ package io.onclave.nsga.ii.datastructure; /** - * - * @author sajib + * this is a simulation of an allele in a biological chromosome that contains a gene value. + * an array of alleles create the genetic code for the chromosome. + * + * @author Debabrata Acharya + * @version 1.0 + * @since 0.1 */ public class Allele { diff --git a/src/main/java/io/onclave/nsga/ii/datastructure/Chromosome.java b/src/main/java/io/onclave/nsga/ii/datastructure/Chromosome.java index b791a80..ddc8dad 100644 --- a/src/main/java/io/onclave/nsga/ii/datastructure/Chromosome.java +++ b/src/main/java/io/onclave/nsga/ii/datastructure/Chromosome.java @@ -8,13 +8,17 @@ import java.util.List; /** - * - * @author sajib + * this is a simulation of a biological chromosome that contains a genetic code, a fitness value, + * a domination rank, an unique ID, and a list of dominated chromosomes. + * + * @author Debabrata Acharya + * @version 1.1 + * @since 0.1 */ public class Chromosome { public Chromosome() { - this(-9999d); + this(-Double.MIN_VALUE); } public Chromosome(final double fitness) { @@ -79,6 +83,10 @@ public Allele[] getGeneticCode() { return geneticCode; } + /** + * the new fitness value is set as soon as a new genetic code is set for a chromosome. + * @param geneticCode the genetic code that the chromosome carries. + */ public void setGeneticCode(Allele[] geneticCode) { this.geneticCode = geneticCode; this.setFitness(Service.calculateFitness(geneticCode)); diff --git a/src/main/java/io/onclave/nsga/ii/datastructure/ParetoObject.java b/src/main/java/io/onclave/nsga/ii/datastructure/ParetoObject.java index 05188c8..127a1b0 100644 --- a/src/main/java/io/onclave/nsga/ii/datastructure/ParetoObject.java +++ b/src/main/java/io/onclave/nsga/ii/datastructure/ParetoObject.java @@ -4,13 +4,23 @@ package io.onclave.nsga.ii.datastructure; /** - * - * @author sajib + * The ParetoObject class is used to define a single pareto object where each object has a chromosome, + * along with the corresponding assigned crowding distance. + * this is an IoC extension of the Chromosome class where each chromosomes are stored along with its + * corresponding crowding distance assigned to it. + * + * property crowdingDistanceSorted is a marker variable that keeps record whether the ParetoObject has + * already been considered during crowd comparison sorting. + * + * @author Debabrata Acharya + * @version 1.0 + * @since 0.1 */ public class ParetoObject { private Chromosome chromosome = null; private double crowdingDistance = -1f; + private boolean crowdingDistanceSorted = false; public ParetoObject(Chromosome chromosome) { this(chromosome, -1f); @@ -36,4 +46,12 @@ public double getCrowdingDistance() { public void setCrowdingDistance(double crowdingDistance) { this.crowdingDistance = crowdingDistance; } + + public boolean isCrowdingDistanceSorted() { + return crowdingDistanceSorted; + } + + public void setCrowdingDistanceSorted(boolean crowdingDistanceSorted) { + this.crowdingDistanceSorted = crowdingDistanceSorted; + } } diff --git a/src/main/java/io/onclave/nsga/ii/datastructure/Population.java b/src/main/java/io/onclave/nsga/ii/datastructure/Population.java index 5e2fb99..503cf24 100644 --- a/src/main/java/io/onclave/nsga/ii/datastructure/Population.java +++ b/src/main/java/io/onclave/nsga/ii/datastructure/Population.java @@ -6,8 +6,11 @@ import java.util.List; /** - * - * @author sajib + * this is a simulation of a population of chromosome known as a populace. + * + * @author Debabrata Acharya + * @version 1.0 + * @since 0.1 */ public class Population { diff --git a/src/main/java/io/onclave/nsga/ii/objectivefunction/SCH_1.java b/src/main/java/io/onclave/nsga/ii/objectivefunction/SCH_1.java index 6bdc93f..f2967b5 100644 --- a/src/main/java/io/onclave/nsga/ii/objectivefunction/SCH_1.java +++ b/src/main/java/io/onclave/nsga/ii/objectivefunction/SCH_1.java @@ -8,8 +8,11 @@ import io.onclave.nsga.ii.datastructure.ParetoObject; /** - * - * @author sajib + * the SCH objective function [f(x) = x^2] + * + * @author Debabrata Acharya + * @version 1.0 + * @since 0.1 */ public class SCH_1 implements IObjectiveFunction { diff --git a/src/main/java/io/onclave/nsga/ii/objectivefunction/SCH_2.java b/src/main/java/io/onclave/nsga/ii/objectivefunction/SCH_2.java index b51c54a..536f94c 100644 --- a/src/main/java/io/onclave/nsga/ii/objectivefunction/SCH_2.java +++ b/src/main/java/io/onclave/nsga/ii/objectivefunction/SCH_2.java @@ -8,8 +8,11 @@ import io.onclave.nsga.ii.datastructure.ParetoObject; /** - * - * @author sajib + * the SCH objective function [f(x) = (x - 2)^2] + * + * @author Debabrata Acharya + * @version 1.0 + * @since 0.1 */ public class SCH_2 implements IObjectiveFunction {