Skynet
From Wiki of Jeroen De Dauw
Skynet is an implementation of GALib with WPF GUI that solves the Travelling Salesman Problem (TSP) using Genetic Algorithms (GA). It's completely open source and available under the GNU General Public License.
| Release status: beta | |
|---|---|
| Type | Application |
| | |
| Description | An implementation of GALib with WPF GUI that solves the Travelling Salesman Problem. |
| Author(s) | Jeroen De Dauw |
| Last Version | 0.1.7 (2010-02-07) |
| License | GPL |
| Download | Skynet latest.tar |
|
Requires .Net 4.0 beta 2 or above | |
Contents |
[edit] Download
Application
- Skynet0.1.7 setup.zip (includes installer and automatic updates)
- Download executables tarball (latest version of the application executables)
Source
- Download source GNU tarball (latest version of the source project)
[edit] Subversion downloads
You can also download the project code directly via SVN from the SourceForge source code repository, at https://csgalib-tsp.svn.sourceforge.net/svnroot/csgalib-tsp. From a command line, you can call the following:
svn checkout https://csgalib-tsp.svn.sourceforge.net/svnroot/csgalib-tsp
[edit] Background
The idea for creating this application came to me after reading the first part of Bio-Inspired Artificial Intelligence by Dario Floreano and Claudio Mattiussi. I figured I needed to do an implementation of what I've read to test myself. I split up the general GA code from the application itself and created GALib, a small C# Library that provides the scaffolding for Genetic Algorithm based functionality. All work was done in my free time.
See this page for more screenshots.
[edit] Using the application
The application consists of 3 big regions; a canvas on which the graphics are drawn, a datagrid giving you a a more clean view of the data, and an menu on the right containing controls and statistics.
[edit] Adding cities
You can add cities by clicking any place on the canvas, which is the open space in the top left of the application, above the datagrid. A city is represented by a small red circle, and will also be added to the datagrid, where you can see it's coordinates. By selecting the city in the datagrid, the circle on the canvas will get highlighted.
If you don't want to bother putting a bunch of cities on the canvas, you can just click 'Add cities circle' in the control panel on the right. Doing so will add 42 cities arranged in a circle to the canvas and datagrid. By default, the application will already do this at startup.
To remove the current cities, click 'Clear all cities' in the control panel.
[edit] Configuring the settings
All configuration is located in the 'Algorithm settings' box at the top right of the application. All are pretty self explanatory, except maybe generation delay, which allows you to delay every generation a certain amount of time. This is useful for debugging, and studying the first few steps in detail.
[edit] Running the algorithm
You can run the application by hitting 'Find the shortest route' in the control panel on the right. You can also do this via the menu, by clicking Algorithm -> Start. To pause or resume the evolution, click their corresponding menu items in the Algorithm menu. To cancel the evolution, either click 'Stop' in the control panel, or again the menu item in the Algorithm menu. The algorithm will stop on itself when either the maximum amount of generations or the stagnation limit is reached.
Once the evolution is complete, you'll be shown a brief report in the form of a report window. The datagrid will also show the travel number of each city. By sorting on this number, you can easily highlight the cities in the route one by one, in the correct order.
[edit] Statistics
You can view the current generation number and the length of the best route in it, as well as the length of the overall best route and last improvement in the 'Progress statistics' panel in the bottom right corner of the application. Two progressbars show the progress of the evolution (how much generations of the maximum amount have been evolved), and stagnation time-out (how many generations have gone without any improvement of the allowed amount).
You can disable the statistics by unchecking the check box next to the panels header. Unchecking it will re-enable the statistics, which will not be affected by any previous turning off.
[edit] How it works
This section explains how Skynet works as an implementation of GALib. If you are not familiar with how genetic algorithms work, you are advised to first have a good look at this Wikipedia article and related pages. This section will introduce you to how GA logic specific to the TSP works in a bottom-up fashion. For more information on the actual evolution, see GALib.
[edit] Dependency diagram
For a full dependency digram of both Skynet and GALib, see this image.
[edit] Location class
The Location class is a simple holder for locations, such as cities in the TSP. New instances are created by providing a Point. The class exposes the following properties: Latitude (Double), Longitude (Double), Id (Int32) and TravelNr (Int32). TravelNr is used to store a location's position in a Route.
[edit] Connection class
The Connection class consists out of 2 public fields of type Location, and is meant to represent the shortest connection between these two locations on a flat surface. You can get it's length via the read-only Length property.
[edit] Route class
The Route class, which contains the definition for the individuals that are getting evolved and is the core of the TSP implementation, inherits from Individual<Connection>. The genotype consists of a List<Connection>, which can be accessed via the public Connections property. The underneath sections describe how the main methods that define an IIndividual type are implemented. For all class members, or a continuous overview, see the source of Route.
[edit] Initialization functions
Route overrides 2 initialization functions defined in Individual.
protected override void doBaseInitialization() { this.genotype = new List<Connection>(); this.rand = new Random(); }
protected override void doRandomInitialization() { List<Location> locations = World.GetCitiesRandomized(); for (Int32 i = locations.Count - 1; i > 0; i--) { this.Genotype.Add(new Connection(locations[i], locations[i - 1])); } this.Genotype.Add(new Connection(locations[0], locations[locations.Count - 1])); }
[edit] Crossover
For the crossover definition, Route overrides the abstract protected void doCrossover from Individual.
/// <summary>Does a crossover operation between to parents to create a new child.</summary> protected override void doCrossover(IIndividual m, IIndividual f) { Route mother = (Route)m; Route father = (Route)f; // To keep track of all cities; they all need to be connected twice. Dictionary<Int32, Int32> cityUsage = Route.getUsageDictionary(); // Add the genes both parents agree on to the genotype. foreach(Connection connection in mother.Genotype) { if (father.Genotype.Contains(connection)) { this.Genotype.Add(connection); cityUsage[connection.Location1.Id]++; cityUsage[connection.Location2.Id]++; } } Route fittest = mother.Fitness > father.Fitness ? mother : father; Route nubbest = mother.Fitness <= father.Fitness ? mother : father; Int32 nrsToGo = cityUsage.Count / 2; // Connect up to half of the remaining locations with genes from the fittest parent. foreach (Connection connection in fittest.Genotype) { if (!this.Genotype.Contains(connection)) { nrsToGo--; this.Genotype.Add(connection); cityUsage[connection.Location1.Id]++; cityUsage[connection.Location2.Id]++; if (nrsToGo <= 0) break; } } // Connect up to half of the remaining locations with genes from the nubbest parent. foreach (Connection connection in nubbest.Genotype) { if (this.getConnectionValidity(connection, cityUsage)) { this.Genotype.Add(connection); cityUsage[connection.Location1.Id]++; cityUsage[connection.Location2.Id]++; } } // Connect the remaining locations with new random connections. Int32[] keys = cityUsage.Keys.ToArray(); foreach (Int32 cityId in keys) { while (cityUsage[cityId] < 2) { Connection connection; Int32 otherCityId; Boolean contains; do { otherCityId = this.rand.Next(cityUsage.Count); // Find a city that has not been connected to 2 times yet. while(cityUsage[otherCityId] >= 2 || otherCityId == cityId) { otherCityId++; if (otherCityId >= cityUsage.Count) otherCityId = 0; } connection = new Connection(World.Cities[cityId], World.Cities[otherCityId]); contains = this.Genotype.Contains(connection); } // Repeat finding a second city for the connection untill it's valid. while (!this.getConnectionValidity(connection, cityUsage)); this.Genotype.Add(connection); cityUsage[cityId]++; cityUsage[otherCityId]++; } } }
An important method used here is getConnectionValidity, which returns whether a connection is valid for the current city usage.
private Boolean getConnectionValidity(Connection connection, Dictionary<Int32, Int32> cityUsage) { if (connection.Location1 == connection.Location2 || cityUsage[connection.Location1.Id] > 1 || cityUsage[connection.Location2.Id] > 1 || this.Genotype.Contains(connection) ) { return false; } if (cityUsage[connection.Location1.Id] == 0 || cityUsage[connection.Location2.Id] == 0) { return true; } Int32 directionNr = 0; Int32 maxLength = cityUsage.Count; do { Int32 connectionAmount = 0; Location previouseLoc, currentLoc, endLocation; currentLoc = directionNr == 0 ? connection.Location1 : connection.Location2; endLocation = directionNr == 0 ? connection.Location2 : connection.Location1; previouseLoc = currentLoc; while (connectionAmount <= maxLength && currentLoc != endLocation) { Location prePreviouse = previouseLoc; previouseLoc = currentLoc; currentLoc = Route.GetNextLocation(currentLoc, prePreviouse, this.genotype); if (currentLoc == previouseLoc) break; // The same gets returned when there is no next connection. connectionAmount++; } // If the cities are connected by going through all cities, the connection is valid. if (connectionAmount + 1 >= maxLength) { return true; } // If the cities are connected without going through all cities, the connection is invalid. else if (currentLoc == endLocation) { return false; } directionNr++; } while (directionNr < 2); // No connection between the cities yet, so it's valid. return true; }
[edit] Fitness function
The fitness function is implemented by overriding the abstract protected Double determineFitness defined by Individual. It works pretty straight forward; a fitter Route has a shorter distance, therefore the reverse of the distance is returned.
protected override Double determineFitness() { Double distance = 0; foreach(Connection connection in this.genotype) { distance += connection.Length; } return -distance; }
[edit] Mutation
The mutation function, which overrides abstract protected void doMutation, defined by Individual, replaces the two connections to one random location with 2 randomly chosen other connections.
protected override void doMutation() { // Get a random connection, which will be the first connection to the location that gets excluded. Connection initialConn = this.Genotype[this.rand.Next(this.Genotype.Count)]; this.Genotype.Remove(initialConn); // This city will be taken out of the route, and then reintegrated via other connections. Location swapLocation = initialConn.Location1; Location newConn1, newConn2; newConn1 = initialConn.Location2; newConn2 = null; // Determine the second connection to the location that gets excluded. foreach(Connection conn in this.Genotype) { if ((conn.Location1 == swapLocation || conn.Location2 == swapLocation) && conn != initialConn) { newConn2 = conn.Location1 == swapLocation ? conn.Location2 : conn.Location1; this.Genotype.Remove(conn); break; } } // Add a new connection between the locations previously connected to the excluded location. Connection bridgeConnection = new Connection(newConn1, newConn2); this.Genotype.Add(bridgeConnection); // Find a connection to replace by 2 new ones that re-include the excluded location. Connection connToReplace; do { connToReplace = this.Genotype[this.rand.Next(this.Genotype.Count)]; } while(connToReplace == bridgeConnection); // Remove the connection and add 2 new ones to re-include the excluded location. this.Genotype.Remove(connToReplace); this.Genotype.Add(new Connection(connToReplace.Location1, swapLocation)); this.Genotype.Add(new Connection(connToReplace.Location2, swapLocation)); }
[edit] Starting the evolution
The evolution is initiated in the C# code for the MainWindow, by calling private void initiateAlgorithm. It creates a new instance of a Population of type Route, and sets several settings by getting values from the user interface. Depending on the selected selection algorithm, the corresponding evolutionary method will be called.
private void initiateAlgorithm() { Route.HasUsageDictionary = false; population = new Population<Route>( Math.Max((Int32)numPopulationSize.Value, 10), Math.Max(numMaxGenerations.Value, 1), Math.Max(numStagnationLimit.Value, 2) ); population.MutationRatio = (Int32)sliderMutationRatio.Value; population.ElitismPercentage = (Int32)sliderElitismPercantage.Value; population.GenerationDelay = (Int32)Utilities.getLogaritmicSliderValue(this.slideGenerationDelay); population.RemoveTwins = (Boolean)this.cbRemoveTwins.IsChecked; population.RemoveDuplicates = (Boolean)this.cbRemoveDuplicates.IsChecked; population.GenerationComplete += new Population<Route>.GenerationCompleteEventHandler(population_GenerationComplete); population.EvolutionComplete += new Population<Route>.EvolutionCompleteEventHandler(population_EvolutionComplete); population.NewFittest += new Population<Route>.NewFittestEventHandler(population_NewFittest); this.timer.Start(); String selectionAlgorithm = this.ddSelectionAlgorithm.Text; switch (selectionAlgorithm) { case "Rank based": population.DoRankBasedSelection(); break; case "Truncated rank": population.DoTruncatedRankSelection((Int32)numTruncationSize.Value); break; case "Wheel based": population.DoRouletteWheelSelection(); break; case "Tournament based": population.DoTournamentBasedSelection(numTournamentSize.Value); break; } this.setControlStates(); }
[edit] Points of Interest
Since my main motivation for creating this application was exercise, I learned a lot from building it. It's my first decent C# application, as well as the first time I've created one using WPF and the first time I've done any GA programming (or AI in general). It also gave me the chance to familiarize myself with some of the new things of .Net 4.0, some profiling tools, and have some fun with navigation based windows. The biggest challenge in the application itself (so not counting GALib), was definitely creating the crossover algorithm for the Route individual type. At first I simply took half of the connections of one parent, and then the other half from the other parent, but I rewrote this to take all common connections. Although the crossover algorithm works fine now, it's pretty heavy on the cpu, and limiting the maximum speed of the application severely. If anyone finds a way to speed it up, be sure to let me know :)
[edit] See also
- SourceForge project page
- SourceForge SVN repository
- GALib - The library used in this GA implementation GALib.




