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');