From 69a4e817611f33dcae3509bbbb6166f3ea446da2 Mon Sep 17 00:00:00 2001 From: E2 Date: Wed, 9 Dec 2020 04:19:53 -0500 Subject: [PATCH 1/8] Added documentation, support for custom distances, performance optimizations Fixes --- .gitignore | 2 + README.md | 16 +++--- salesman.js | 150 ++++++++++++++++++++++++++++++++++++++++------------ 3 files changed, 128 insertions(+), 40 deletions(-) diff --git a/.gitignore b/.gitignore index 5148e52..fac8ed9 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ jspm_packages # Optional REPL history .node_repl_history + +package-lock.json diff --git a/README.md b/README.md index adc19d4..cf0b8f0 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ ## salesman **See**: [demo](https://lovasoa.github.io/salesman.js/) **Author:** Ophir LOJKINE + salesman npm module Good heuristic for the traveling salesman problem using simulated annealing. @@ -10,7 +11,7 @@ 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> @@ -19,7 +20,7 @@ Good heuristic for the traveling salesman problem using simulated annealing. #### 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 | @@ -29,20 +30,21 @@ Represents a point in two dimensions. -### 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. +**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. | +| points | [ 'Array' ].<Point> | | The points that the path will have to visit. | +| [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/salesman.js b/salesman.js index 90f2967..f13a0ff 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. @@ -10,16 +11,52 @@ /** * @private + * + * 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. */ -function Path(points) { +function Path(points, distanceFunc) { 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; }; -// Random index between 1 and the last position in the array of points +/** + * Retrieve a random index between 1 and the last position in the array of points. + * @returns {Number} A random index. + */ Path.prototype.randomPos = function() { 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 + */ +function Point(x, y) { + this.x = x; + this.y = y; +}; + /** * Solves the following problem: * Given a list of points and the distances between each pair of points, @@ -78,8 +159,9 @@ 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. * @@ -92,34 +174,36 @@ Path.prototype.randomPos = function() { * var 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) { + var 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"; + var hasCallback = typeof(callback) === "function"; for (var 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) { +function euclidean(p, q) { var dx = p.x - q.x, dy = p.y - q.y; return Math.sqrt(dx*dx + dy*dy); } From 8d9d556897ac722ab56ecc6edc9bf21ffb356d13 Mon Sep 17 00:00:00 2001 From: E2 Date: Wed, 9 Dec 2020 18:26:43 -0500 Subject: [PATCH 2/8] Moved changes out of gh-pages, use number instead of Number --- salesman.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/salesman.js b/salesman.js index f13a0ff..4e0bc0e 100644 --- a/salesman.js +++ b/salesman.js @@ -120,9 +120,9 @@ Path.prototype.access = function(i) { }; /** * Access the cached distance between two points, by their indices. - * @param {Number} i The first index as an integer - * @param {Number} j The second index as an integer - * @returns {Number} The distance between point i and point j. + * @param {number} i The first index as an integer + * @param {number} j The second index as an integer + * @returns {number} The distance between point i and point j. */ Path.prototype.distance = function(i, j) { if (i === j) return 0; // Identity. @@ -135,7 +135,7 @@ Path.prototype.distance = function(i, j) { }; /** * Retrieve a random index between 1 and the last position in the array of points. - * @returns {Number} A random index. + * @returns {number} A random index. */ Path.prototype.randomPos = function() { return 1 + Math.floor(Math.random() * (this.points.length - 1)); @@ -144,8 +144,8 @@ Path.prototype.randomPos = function() { /** * Represents a point in two dimensions. Used as the input for `solve`. * @class - * @param {Number} x abscissa - * @param {Number} y ordinate + * @param {number} x abscissa + * @param {number} y ordinate */ function Point(x, y) { this.x = x; @@ -159,11 +159,11 @@ function Point(x, y) { * 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. Smaller values (0.9) work faster but give poorer solutions, whereas values closer to 1 (0.99999) work slower, but give better solutions. + * @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. + * @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 = [ @@ -201,7 +201,7 @@ function solve(points, temp_coeff = 0.999, callback, distance = euclidean) { * A simple distance function, to use as the default. * @param {Point} p * @param {Point} q - * @returns {Number} The Euclidean distance between p and q + * @returns {number} The Euclidean distance between p and q */ function euclidean(p, q) { var dx = p.x - q.x, dy = p.y - q.y; From e29a3dd200d0467ea01fc4fd6e616a09f3a25ca0 Mon Sep 17 00:00:00 2001 From: E2 Date: Wed, 9 Dec 2020 18:51:14 -0500 Subject: [PATCH 3/8] Created perf_test.js --- perf_test.js | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 perf_test.js diff --git a/perf_test.js b/perf_test.js new file mode 100644 index 0000000..7359a94 --- /dev/null +++ b/perf_test.js @@ -0,0 +1,42 @@ +const { + performance, + PerformanceObserver +} = require('perf_hooks'); +var salesman = require("./salesman.js"); + +var width = 100; +var height = 100; +var size = 5000; +var perfTestCount = 500; + +function createPoint(id) { + return {id, x: width * Math.random(), y: height * Math.random()}; +} + +var durations = []; + +function arraySum(arr) { + return arr.reduce((a,b) => a + b, 0); +} + +function arrayAvg(arr) { + return arraySum(arr) / arr.length; +} + +for (var i = 1; i <= perfTestCount; i++) { + console.log(`Running test ${i}`); + + var testPoints = [...Array(size).keys()].map((index) => (createPoint(index))); + + var startTime = performance.now(); + var result = salesman.solve(testPoints); + var 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 From 84cdccf7f47442bb93d667c222e864a4169a1575 Mon Sep 17 00:00:00 2001 From: Eric Myllyoja Date: Wed, 9 Dec 2020 22:48:19 -0500 Subject: [PATCH 4/8] Update .gitignore Co-authored-by: Ophir LOJKINE --- .gitignore | 76 ++++++++++++++++++++++++++---------------------------- 1 file changed, 37 insertions(+), 39 deletions(-) diff --git a/.gitignore b/.gitignore index fac8ed9..f7f9a88 100644 --- a/.gitignore +++ b/.gitignore @@ -1,39 +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 - -package-lock.json +# 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 From db78a97755d3f06df36cd304b43609fdef87ed57 Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Thu, 10 Dec 2020 10:46:33 +0100 Subject: [PATCH 5/8] Update auto-generated documentation --- README.md | 22 +++++++++++----------- package.json | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index cf0b8f0..730986e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## salesman **See**: [demo](https://lovasoa.github.io/salesman.js/) -**Author:** Ophir LOJKINE +**Author**: Ophir LOJKINE salesman npm module @@ -11,12 +11,12 @@ 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], [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) @@ -25,26 +25,26 @@ 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], [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. Smaller values (0.9) work faster but give poorer solutions, whereas values closer to 1 (0.99999) work slower, but give better solutions. | +| points | Array.<Point> | | The points that the path will have to visit. | +| [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. | +| [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" } } From b72f9782736ae5ab40854df3d5cc0a1374b7dd84 Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Thu, 10 Dec 2020 11:20:54 +0100 Subject: [PATCH 6/8] Fix type annotations --- salesman.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/salesman.js b/salesman.js index 4e0bc0e..90c9e28 100644 --- a/salesman.js +++ b/salesman.js @@ -10,13 +10,13 @@ /** - * @private * * 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, distanceFunc) { this.points = points; @@ -55,7 +55,7 @@ Path.prototype.initializeDistances = function() { * This random chance is based on how bad the move is, * as well as how early in the annealing process we are (the "temperature"). * - * @param {*} temp The current temperature of the algorithm. + * @param {number} temp The current temperature of the algorithm. */ Path.prototype.change = function(temp) { var i = this.randomPos(), j = this.randomPos(); @@ -66,8 +66,8 @@ Path.prototype.change = function(temp) { }; /** * Swap two points in the path order by their indices. - * @param {*} i The first index to swap. - * @param {*} j The second index to swap. + * @param {number} i The first index to swap. + * @param {number} j The second index to swap. */ Path.prototype.swap = function(i,j) { var tmp = this.order[i]; @@ -81,9 +81,9 @@ Path.prototype.swap = function(i,j) { * plus the distance between j and i's neighbors, minus the current distances. * * If the value is negative, it would make the path shorter to swap the values. - * @param {*} i The first index to compare. - * @param {*} j The second index to compare. - * @returns The change in path distance if i and j were swapped. + * @param {number} i The first index to compare. + * @param {number} j The second index to compare. + * @returns {number} The change in path distance if i and j were swapped. */ Path.prototype.delta_distance = function(i, j) { var jm1 = this.index(j-1), @@ -105,8 +105,10 @@ Path.prototype.delta_distance = function(i, j) { }; /** * Get the ith point in the point array. - * @param {*} i The index to retrieve. - * If i is greater than or less + * The path is cyclic, so i can be positive or negative, + * smaller or larger than the total number of points. + * @param {number} i The index to retrieve. + * @returns {number} */ Path.prototype.index = function(i) { return (i + this.points.length) % this.points.length; From a3291c650dec1cc2fda406ddfd699f8f51619aab Mon Sep 17 00:00:00 2001 From: kikawet Date: Thu, 8 Apr 2021 18:35:45 +0200 Subject: [PATCH 7/8] refactored to ES6 --- salesman.js | 273 +++++++++++++++++++++++++++------------------------- 1 file changed, 144 insertions(+), 129 deletions(-) diff --git a/salesman.js b/salesman.js index 90c9e28..1978fe2 100644 --- a/salesman.js +++ b/salesman.js @@ -18,130 +18,143 @@ * @param {Function} distanceFunc The function to use to calculate the distance between two points. * @private */ -function Path(points, distanceFunc) { - this.points = points; - this.distanceFunc = distanceFunc; - this.initializeOrder(); - this.initializeDistances(); -} -/** - * Creates the default order for the points. - */ -Path.prototype.initializeOrder = function() { - // A loop is about 3x faster than using a spread operator. - this.order = new Array(this.points.length); - for (var i = 0; i < this.order.length; i++) this.order[i] = i; -} -/** - * Calculates the distance for all the points. - */ -Path.prototype.initializeDistances = function() { - this.distances = new Array(this.points.length * this.points.length); - for(var i = 0; i < this.points.length; i++) { - // Optimization: Starting at i+1 avoids repeats and identity distances. - // We just need to make sure we don't access the empty cells later. - for(var j = i + 1; j < this.points.length; j++) { - this.distances[j + i * this.points.length] = this.distanceFunc(this.points[i], this.points[j]); +class Path { + constructor(points, distanceFunc){ + this.points = points; + this.distanceFunc = distanceFunc; + this.initializeOrder(); + this.initializeDistances(); + } + + /** + * Creates the default order for the points. + */ + initializeOrder() { + // A loop is about 3x faster than using a spread operator. + this.order = new Array(this.points.length); + for (let i = 0; i < this.order.length; i++) this.order[i] = i; + } + + /** + * Calculates the distance for all the points. + */ + initializeDistances() { + this.distances = new Array(this.points.length * this.points.length); + for(let i = 0; i < this.points.length; i++) { + // Optimization: Starting at i+1 avoids repeats and identity distances. + // We just need to make sure we don't access the empty cells later. + for(let j = i + 1; j < this.points.length; j++) { + this.distances[j + i * this.points.length] = this.distanceFunc(this.points[i], this.points[j]); + } } } -}; -/** - * Perform one iteration of the simulated annealing. - * - * Choose two random points in the path, and calculate how much the path distance would change - * if you swapped the two points. If it would make the path shorter, swap them. - * - * If not, have a random chance to swap them anyway. - * This random chance is based on how bad the move is, - * as well as how early in the annealing process we are (the "temperature"). - * - * @param {number} temp The current temperature of the algorithm. - */ -Path.prototype.change = function(temp) { - var i = this.randomPos(), j = this.randomPos(); - var delta = this.delta_distance(i, j); - if (delta < 0 || Math.random() < Math.exp(-delta / temp)) { - this.swap(i,j); + + /** + * Perform one iteration of the simulated annealing. + * + * Choose two random points in the path, and calculate how much the path distance would change + * if you swapped the two points. If it would make the path shorter, swap them. + * + * If not, have a random chance to swap them anyway. + * This random chance is based on how bad the move is, + * as well as how early in the annealing process we are (the "temperature"). + * + * @param {number} temp The current temperature of the algorithm. + */ + change(temp) { + const i = this.randomPos(), j = this.randomPos(); + const delta = this.delta_distance(i, j); + if (delta < 0 || Math.random() < Math.exp(-delta / temp)) { + this.swap(i,j); + } } -}; -/** - * Swap two points in the path order by their indices. - * @param {number} i The first index to swap. - * @param {number} j The second index to swap. - */ -Path.prototype.swap = function(i,j) { - var tmp = this.order[i]; - this.order[i] = this.order[j]; - this.order[j] = tmp; -}; -/** - * Calculate the change in path distance if i and j were swapped. - * - * Calculate the distance between i and j's neighbors, - * plus the distance between j and i's neighbors, minus the current distances. - * - * If the value is negative, it would make the path shorter to swap the values. - * @param {number} i The first index to compare. - * @param {number} j The second index to compare. - * @returns {number} The change in path distance if i and j were swapped. - */ -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; -}; -/** - * Get the ith point in the point array. - * The path is cyclic, so i can be positive or negative, - * smaller or larger than the total number of points. - * @param {number} i The index to retrieve. - * @returns {number} - */ -Path.prototype.index = function(i) { - return (i + this.points.length) % this.points.length; -}; -/** - * Get the ith point in the path order. - * @param {*} i The index to retrieve. - */ -Path.prototype.access = function(i) { - return this.points[this.order[this.index(i)]]; -}; -/** - * Access the cached distance between two points, by their indices. - * @param {number} i The first index as an integer - * @param {number} j The second index as an integer - * @returns {number} The distance between point i and point j. - */ -Path.prototype.distance = function(i, j) { - if (i === j) return 0; // Identity. - // Ensure low is actually lower. - var low = this.order[i], high = this.order[j]; - if (low > high) { low = this.order[j]; high = this.order[i]; } + /** + * Swap two points in the path order by their indices. + * @param {number} i The first index to swap. + * @param {number} j The second index to swap. + */ + swap(i,j) { + const tmp = this.order[i]; + this.order[i] = this.order[j]; + this.order[j] = tmp; + }; + + /** + * Calculate the change in path distance if i and j were swapped. + * + * Calculate the distance between i and j's neighbors, + * plus the distance between j and i's neighbors, minus the current distances. + * + * If the value is negative, it would make the path shorter to swap the values. + * @param {number} i The first index to compare. + * @param {number} j The second index to compare. + * @returns {number} The change in path distance if i and j were swapped. + */ + delta_distance(i, j) { + const jm1 = this.index(j-1), + jp1 = this.index(j+1), + im1 = this.index(i-1), + ip1 = this.index(i+1); + let 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; + } + + /** + * Get the ith point in the point array. + * The path is cyclic, so i can be positive or negative, + * smaller or larger than the total number of points. + * @param {number} i The index to retrieve. + * @returns {number} + */ + index(i) { + return (i + this.points.length) % this.points.length; + } + + /** + * Get the ith point in the path order. + * @param {*} i The index to retrieve. + */ + access(i) { + return this.points[this.order[this.index(i)]]; + } + + /** + * Access the cached distance between two points, by their indices. + * @param {number} i The first index as an integer + * @param {number} j The second index as an integer + * @returns {number} The distance between point i and point j. + */ + distance(i, j) { + if (i === j) return 0; // Identity. + + // Ensure low is actually lower. + let low = this.order[i], high = this.order[j]; + if (low > 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)); + }; +} + - 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. - */ -Path.prototype.randomPos = function() { - return 1 + Math.floor(Math.random() * (this.points.length - 1)); -}; /** * Represents a point in two dimensions. Used as the input for `solve`. @@ -149,9 +162,11 @@ Path.prototype.randomPos = function() { * @param {number} x abscissa * @param {number} y ordinate */ -function Point(x, y) { - this.x = x; - this.y = y; +class Point { + constructor(x, y) { + this.x = x; + this.y = y; + } }; /** @@ -168,16 +183,16 @@ function Point(x, y) { * @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 = 0.999, callback, distance = euclidean) { - var path = new Path(points, distance); + 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. @@ -186,9 +201,9 @@ function solve(points, temp_coeff = 0.999, callback, distance = euclidean) { // Create a temperature coefficient. if (!temp_coeff) temp_coeff = 1 - Math.exp(-10 - Math.min(points.length,1e6)/1e5); - var hasCallback = 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); @@ -206,7 +221,7 @@ function solve(points, temp_coeff = 0.999, callback, distance = euclidean) { * @returns {number} The Euclidean distance between p and q */ function euclidean(p, q) { - var dx = p.x - q.x, dy = p.y - q.y; + const dx = p.x - q.x, dy = p.y - q.y; return Math.sqrt(dx*dx + dy*dy); } From 4af497200119ced175b26e2d2cf017578c0a8d80 Mon Sep 17 00:00:00 2001 From: kikawet Date: Thu, 8 Apr 2021 18:53:58 +0200 Subject: [PATCH 8/8] updated tests to ES6 --- perf_test.js | 22 +++++++++++----------- test.js | 13 +++++++------ 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/perf_test.js b/perf_test.js index 7359a94..297863c 100644 --- a/perf_test.js +++ b/perf_test.js @@ -2,18 +2,18 @@ const { performance, PerformanceObserver } = require('perf_hooks'); -var salesman = require("./salesman.js"); +const salesman = require("./salesman.js"); -var width = 100; -var height = 100; -var size = 5000; -var perfTestCount = 500; +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()}; } -var durations = []; +const durations = []; function arraySum(arr) { return arr.reduce((a,b) => a + b, 0); @@ -23,14 +23,14 @@ function arrayAvg(arr) { return arraySum(arr) / arr.length; } -for (var i = 1; i <= perfTestCount; i++) { +for (let i = 1; i <= perfTestCount; i++) { console.log(`Running test ${i}`); - var testPoints = [...Array(size).keys()].map((index) => (createPoint(index))); + const testPoints = [...Array(size).keys()].map((index) => (createPoint(index))); - var startTime = performance.now(); - var result = salesman.solve(testPoints); - var duration = (performance.now() - startTime) / 1000; // Milliseconds + 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}`); } 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');