diff --git a/.gitignore b/.gitignore index 5148e52..f7f9a88 100644 --- a/.gitignore +++ b/.gitignore @@ -1,37 +1,37 @@ -# Logs -logs -*.log -npm-debug.log* - -# Runtime data -pids -*.pid -*.seed - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules -jspm_packages - -# Optional npm cache directory -.npm - -# Optional REPL history -.node_repl_history +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules +jspm_packages + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history diff --git a/README.md b/README.md index adc19d4..730986e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,8 @@ ## salesman **See**: [demo](https://lovasoa.github.io/salesman.js/) -**Author:** Ophir LOJKINE +**Author**: Ophir LOJKINE + salesman npm module Good heuristic for the traveling salesman problem using simulated annealing. @@ -10,39 +11,40 @@ Good heuristic for the traveling salesman problem using simulated annealing. * [salesman](#module_salesman) * [~Point](#module_salesman..Point) * [new Point(x, y)](#new_module_salesman..Point_new) - * [~solve(points, [temp_coeff], [callback=])](#module_salesman..solve) ⇒ Array.<Number> + * [~solve(points, [temp_coeff], [callback], [callback])](#module_salesman..solve) ⇒ Array.<number> ### salesman~Point -**Kind**: inner class of [salesman](#module_salesman) +**Kind**: inner class of [salesman](#module_salesman) #### new Point(x, y) -Represents a point in two dimensions. +Represents a point in two dimensions. Used as the input for `solve`. | Param | Type | Description | | --- | --- | --- | -| x | Number | abscissa | -| y | Number | ordinate | +| x | number | abscissa | +| y | number | ordinate | -### salesman~solve(points, [temp_coeff], [callback=]) ⇒ Array.<Number> +### salesman~solve(points, [temp_coeff], [callback], [callback]) ⇒ Array.<number> Solves the following problem: Given a list of points and the distances between each pair of points, what is the shortest possible route that visits each point exactly once and returns to the origin point? -**Kind**: inner method of [salesman](#module_salesman) -**Returns**: Array.<Number> - An array of indexes in the original array. Indicates in which order the different points are visited. +**Kind**: inner method of [salesman](#module_salesman) +**Returns**: Array.<number> - An array of indexes in the original array. Indicates in which order the different points are visited. | Param | Type | Default | Description | | --- | --- | --- | --- | | points | Array.<Point> | | The points that the path will have to visit. | -| [temp_coeff] | Number | 0.999 | changes the convergence speed of the algorithm: the closer to 1, the slower the algorithm and the better the solutions. | -| [callback=] | function | | An optional callback to be called after each iteration. | +| [temp_coeff] | number | 0.999 | changes the convergence speed of the algorithm. Smaller values (0.9) work faster but give poorer solutions, whereas values closer to 1 (0.99999) work slower, but give better solutions. | +| [callback] | function | | An optional callback to be called after each iteration. | +| [callback] | function | euclidean | An optional argument to specify how distances are calculated. The function takes two Point objects as arguments and returns a number for distance. Defaults to simple Euclidean distance calculation. | **Example** ```js diff --git a/package.json b/package.json index 8e50ee2..4a9ce3e 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "salesman.js", "scripts": { "test": "node test.js", - "prepare": "node salesman.js && node node_modules/jsdoc-to-markdown/bin/cli.js --src salesman.js > README.md" + "prepare": "node salesman.js && node node_modules/jsdoc-to-markdown/bin/cli.js --files salesman.js > README.md" }, "repository": { "type": "git", @@ -27,6 +27,6 @@ }, "homepage": "https://github.com/lovasoa/salesman.js", "devDependencies": { - "jsdoc-to-markdown": "1.3" + "jsdoc-to-markdown": "^6.0.1" } } diff --git a/perf_test.js b/perf_test.js new file mode 100644 index 0000000..297863c --- /dev/null +++ b/perf_test.js @@ -0,0 +1,42 @@ +const { + performance, + PerformanceObserver +} = require('perf_hooks'); +const salesman = require("./salesman.js"); + +const width = 100; +const height = 100; +const size = 5000; +const perfTestCount = 500; + +function createPoint(id) { + return {id, x: width * Math.random(), y: height * Math.random()}; +} + +const durations = []; + +function arraySum(arr) { + return arr.reduce((a,b) => a + b, 0); +} + +function arrayAvg(arr) { + return arraySum(arr) / arr.length; +} + +for (let i = 1; i <= perfTestCount; i++) { + console.log(`Running test ${i}`); + + const testPoints = [...Array(size).keys()].map((index) => (createPoint(index))); + + const startTime = performance.now(); + const result = salesman.solve(testPoints); + const duration = (performance.now() - startTime) / 1000; // Milliseconds + durations.push(duration); + console.log(`Test ${i} done, took ${duration}`); +} + +console.log('RESULTS'); +console.log('-------'); +console.log(`* Average Time: ${arrayAvg(durations)}`); +console.log(`* Max Time: ${Math.max(...durations)}`); +console.log(`* Min Time: ${Math.min(...durations)}`); \ No newline at end of file diff --git a/salesman.js b/salesman.js index 90f2967..1978fe2 100644 --- a/salesman.js +++ b/salesman.js @@ -1,6 +1,7 @@ /** * @module * @author Ophir LOJKINE + * * salesman npm module * * Good heuristic for the traveling salesman problem using simulated annealing. @@ -9,66 +10,163 @@ /** + * + * Represents a path between points. + * Includes an internal order for those points, + * along with an array which maintains a record of distances between points. + * @param {Points[]} points The points in the path. + * @param {Function} distanceFunc The function to use to calculate the distance between two points. * @private */ -function Path(points) { - this.points = points; - this.order = new Array(points.length); - for(var i=0; i high) { low = this.order[j]; high = this.order[i]; } + + return this.distances[low * this.points.length + high] || 0; + } + + /** + * Retrieve a random index between 1 and the last position in the array of points. + * @returns {number} A random index. + */ + randomPos() { + return 1 + Math.floor(Math.random() * (this.points.length - 1)); + }; +} + + + +/** + * Represents a point in two dimensions. Used as the input for `solve`. + * @class + * @param {number} x abscissa + * @param {number} y ordinate + */ +class Point { + constructor(x, y) { + this.x = x; + this.y = y; } - return s; -}; -Path.prototype.swap = function(i,j) { - var tmp = this.order[i]; - this.order[i] = this.order[j]; - this.order[j] = tmp; -}; -Path.prototype.delta_distance = function(i, j) { - var jm1 = this.index(j-1), - jp1 = this.index(j+1), - im1 = this.index(i-1), - ip1 = this.index(i+1); - var s = - this.distance(jm1, i ) - + this.distance(i , jp1) - + this.distance(im1, j ) - + this.distance(j , ip1) - - this.distance(im1, i ) - - this.distance(i , ip1) - - this.distance(jm1, j ) - - this.distance(j , jp1); - if (jm1 === i || jp1 === i) - s += 2*this.distance(i,j); - return s; -}; -Path.prototype.index = function(i) { - return (i + this.points.length) % this.points.length; -}; -Path.prototype.access = function(i) { - return this.points[this.order[this.index(i)]]; -}; -Path.prototype.distance = function(i, j) { - return this.distances[this.order[i] * this.points.length + this.order[j]]; -}; -// Random index between 1 and the last position in the array of points -Path.prototype.randomPos = function() { - return 1 + Math.floor(Math.random() * (this.points.length - 1)); }; /** @@ -78,49 +176,52 @@ Path.prototype.randomPos = function() { * once and returns to the origin point? * * @param {Point[]} points The points that the path will have to visit. - * @param {Number} [temp_coeff=0.999] changes the convergence speed of the algorithm: the closer to 1, the slower the algorithm and the better the solutions. - * @param {Function} [callback=] An optional callback to be called after each iteration. + * @param {number} [temp_coeff=0.999] changes the convergence speed of the algorithm. Smaller values (0.9) work faster but give poorer solutions, whereas values closer to 1 (0.99999) work slower, but give better solutions. + * @param {Function} [callback=undefined] An optional callback to be called after each iteration. + * @param {Function} [callback=euclidean] An optional argument to specify how distances are calculated. The function takes two Point objects as arguments and returns a number for distance. Defaults to simple Euclidean distance calculation. * - * @returns {Number[]} An array of indexes in the original array. Indicates in which order the different points are visited. + * @returns {number[]} An array of indexes in the original array. Indicates in which order the different points are visited. * * @example - * var points = [ + * const points = [ * new salesman.Point(2,3) * //other points * ]; - * var solution = salesman.solve(points); - * var ordered_points = solution.map(i => points[i]); + * const solution = salesman.solve(points); + * const ordered_points = solution.map(i => points[i]); * // ordered_points now contains the points, in the order they ought to be visited. **/ -function solve(points, temp_coeff, callback) { - var path = new Path(points); - if (points.length < 2) return path.order; // There is nothing to optimize +function solve(points, temp_coeff = 0.999, callback, distance = euclidean) { + const path = new Path(points, distance); + // Optimization: If there is only one point in the list, there is no path. + if (points.length < 2) return path.order; + // Optimization: If the user would provide a bad input, end immediately. + if (temp_coeff >= 1 || temp_coeff <= 0) return path.order; + + // Create a temperature coefficient. if (!temp_coeff) temp_coeff = 1 - Math.exp(-10 - Math.min(points.length,1e6)/1e5); - var has_callback = typeof(callback) === "function"; + const hasCallback = typeof(callback) === "function"; - for (var temperature = 100 * distance(path.access(0), path.access(1)); + for (let temperature = 100 * distance(path.access(0), path.access(1)); temperature > 1e-6; temperature *= temp_coeff) { path.change(temperature); - if (has_callback) callback(path.order); + if (hasCallback) callback(path.order); } return path.order; }; /** - * Represents a point in two dimensions. - * @class - * @param {Number} x abscissa - * @param {Number} y ordinate + * @private + * + * A simple distance function, to use as the default. + * @param {Point} p + * @param {Point} q + * @returns {number} The Euclidean distance between p and q */ -function Point(x, y) { - this.x = x; - this.y = y; -}; - -function distance(p, q) { - var dx = p.x - q.x, dy = p.y - q.y; +function euclidean(p, q) { + const dx = p.x - q.x, dy = p.y - q.y; return Math.sqrt(dx*dx + dy*dy); } diff --git a/test.js b/test.js index 6bad092..ac52351 100644 --- a/test.js +++ b/test.js @@ -1,14 +1,15 @@ -var assert = require("assert"); -var salesman = require("./salesman.js"); +const assert = require("assert"); +const salesman = require("./salesman.js"); -var tests = [ +const tests = [ {q:[[0,0]], r:[0]}, {q:[[0,0],[1,1]], r:[0,1]}, ]; for(let test of tests) { - var points = test.q.map(([x,y])=>new salesman.Point(x,y)); - var res = salesman.solve(points); - assert.deepEqual(test.r, res); + const points = test.q.map(([x,y])=>new salesman.Point(x,y)); + const res = salesman.solve(points); + assert.deepStrictEqual(test.r, res); } +console.log('Test finalized successfully');