change location

This commit is contained in:
Narongpol Kijrangsan 2025-02-28 23:12:31 -05:00
parent ca7327d5af
commit 1ff9e819f3
25 changed files with 15 additions and 1706 deletions

View file

@ -60,6 +60,7 @@ export const MapPlot = React.forwardRef((props, ref) => {
};
const onDefinedPoint = ({ lngLat }) => {
console.log(plotPoints.map(x => x.position));
dispatch(actions.addDefinedPoint(lngLat));
};
const layers = useMemo(() => [

View file

@ -102,14 +102,6 @@ export const MenuSolverControls = ({
{alg.friendlyName}
</SelectItem>
))}
<ListSubheader>Heuristic Improvement</ListSubheader>
{algorithms
.filter(alg => alg.type === "heuristic-improvement")
.map(alg => (
<SelectItem value={alg.solverKey} key={alg.solverKey}>
{alg.friendlyName}
</SelectItem>
))}
<ListSubheader>Exhaustive</ListSubheader>
{algorithms
.filter(alg => alg.type === "exhaustive")

View file

@ -1,99 +0,0 @@
---
type: exhaustive
order: 3
solverKey: branchAndBoundOnCost
friendlyName: Branch and Bound (Cost)
defaults:
evaluatingDetailLevel: 2
maxEvaluatingDetailLevel: 2
---
# Branch and Bound on Cost
This is a recursive algorithm, similar to depth first search, that is guaranteed to find the optimal solution.
The candidate solution space is generated by systematically traversing possible paths, and discarding large subsets of fruitless candidates by comparing the current solution to an upper and lower bound. In this case, the upper bound is the best path found so far.
While evaluating paths, if at any point the current solution is already more expensive (longer) than the best complete path discovered, there is no point continuing.
For example, imagine:
1. A -> B -> C -> D -> E -> A was already found with a cost of 100.
2. We are evaluating A -> C -> E, which has a cost of 110. There is **no point** evaluating the remaining solutions.
3. Instead of continuing to evaluate all of the child solutions from here, we can go down a different path, eliminating candidates not worth evaluating:
- `A -> C -> E -> D -> B -> A`
- `A -> C -> E -> B -> D -> A`
Implementation is very similar to depth first search, with the exception that we cut paths that are already longer than the current best.
## Implementation
```javascript
const branchAndBoundOnCost = async (
points,
path = [],
visited = null,
overallBest = Infinity
) => {
if (visited === null) {
// initial call
path = [points.shift()];
points = new Set(points);
visited = new Set();
}
// figure out which points are left
const available = setDifference(points, visited);
// calculate the cost, from here, to go home
const backToStart = [...path, path[0]];
const cost = pathCost(backToStart);
if (cost > overallBest) {
// we may not be done, but have already traveled further than the best path
// no reason to continue
return [null, null];
}
// still cheaper than the best, keep going deeper, and deeper, and deeper...
if (available.size === 0) {
// at the end of the path, return where we're at
return [cost, backToStart];
}
let [bestCost, bestPath] = [null, null];
// for every point yet to be visited along this path
for (const p of available) {
// go to that point
visited.add(p);
path.push(p);
// RECURSE - go through all the possible points from that point
const [curCost, curPath] = await branchAndBoundOnCost(
points,
path,
visited,
overallBest
);
// if that path is better and complete, keep it
if (curCost && (!bestCost || curCost < bestCost)) {
[bestCost, bestPath] = [curCost, curPath];
if (!overallBest || bestCost < overallBest) {
// found a new best complete path
overallBest = bestCost;
self.setBestPath(bestPath, bestCost);
}
}
// go back up and make that point available again
visited.delete(p);
path.pop();
}
return [bestCost, bestPath];
};
```

View file

@ -1,56 +0,0 @@
---
type: exhaustive
order: 4
solverKey: branchAndBoundOnCostAndCross
friendlyName: Branch and Bound (Cost, Crossings)
defaults:
evaluatingDetailLevel: 2
maxEvaluatingDetailLevel: 2
---
# Branch and Bound (Cost, Intersections)
This is the same as branch and bound on cost, with an additional heuristic added to further minimize the search space.
While traversing paths, if at any point the path intersects (crosses over) itself, than backtrack and try the next way. It's been proven that an optimal path will never contain crossings.
Implementation is almost identical to branch and bound on cost only, with the added heuristic below:
## Implementation
```javascript
const counterClockWise = (p, q, r) => {
return (q[0] - p[0]) * (r[1] - q[1]) <
(q[1] - p[1]) * (r[0] - q[0])
}
const intersects = (a, b, c, d) => {
return counterClockWise(a, c, d) !== counterClockWise(b, c, d) &&
counterClockWise(a, b, c) !== counterClockWise(a, b, d)
}
const branchAndBoundOnCostAndCross = async (...) => {
//
// .....
//
if (path.length > 3) {
// if this newly added edge crosses over the existing path,
// don't continue. It's been proven that an optimal path will
// not cross itself.
const newSegment = [
path[path.length-2], path[path.length-1]
]
for (let i=1; i<path.length-2; i++) {
if (intersects(path[i], path[i-1], ...newSegment)) {
return [null, null]
}
}
}
//
// .....
//
}
```

View file

@ -1,53 +0,0 @@
---
type: exhaustive
order: 2
solverKey: random
friendlyName: Random
defaults:
evaluatingDetailLevel: 1
maxEvaluatingDetailLevel: 1
---
# Random
This is an impractical, albeit exhaustive algorithm. It is here only for demonstration purposes, but will not find a reasonable path for traveling salesman problems above 7 or 8 points.
I consider it exhaustive because if it runs for infinity, eventually it will encounter every possible path.
1. From the starting path
2. Randomly shuffle the path
3. If it's better, keep it
4. If not, ditch it and keep going
## Implementation
```javascript
const random = async points => {
let best = Infinity;
while (true) {
// save off the starting point
const start = points.shift();
// sort the remaining points
const path = points.sort(() => Math.random() - 0.5);
// put the starting point back
path.unshift(start);
// return to the starting point
path.push(start);
// calculate the new cost
const cost = pathCost(path);
if (cost < best) {
// we found a better path
best = cost;
}
// get rid of starting point at the end
path.pop();
}
};
```

View file

@ -1,61 +0,0 @@
---
type: heuristic-construction
order: 2
solverKey: arbitraryInsertion
friendlyName: Arbitrary Insertion
defaults:
evaluatingDetailLevel: 1
maxEvaluatingDetailLevel: 1
---
# Arbitrary Insertion
This is a heuristic construction algorithm. It select a random point, and then figures out where the best place to put it will be.
1. From the starting point
2. First, go to the closest point
3. Choose a random point to go to
4. Find the cheapest place to add it in the path
5. Chosen point is no longer an "available point"
6. Continue from #3 until there are no available points, and then return to the start.
## Implementation
```javascript
const arbitraryInsertion = async points => {
// from the starting point
const path = [points.shift()];
//
// INITIALIZATION - go to the nearest point
//
points.sort((a, b) => distance(path[0], b) - distance(path[0], a));
path.push(points.pop());
// randomly sort points - this is the order they will be added
// to the path
points.sort(() => Math.random() - 0.5);
while (points.length > 0) {
//
// SELECTION - choose a next point randomly
//
const nextPoint = points.pop();
//
// INSERTION -find the insertion spot that minimizes distance
//
let [bestCost, bestIdx] = [Infinity, null];
for (let i = 1; i < path.length; i++) {
const insertionCost = pathCost([path[i - 1], nextPoint, path[i]]);
if (insertionCost < bestCost) {
[bestCost, bestIdx] = [insertionCost, i];
}
}
path.splice(bestIdx, 0, nextPoint);
}
// return to start after visiting all other points
path.push(path[0]);
};
```

View file

@ -1,111 +0,0 @@
---
type: heuristic-construction
order: 5
solverKey: convexHull
friendlyName: Convex Hull
defaults:
evaluatingDetailLevel: 2
maxEvaluatingDetailLevel: 2
---
# Convex Hull
This is a heuristic construction algorithm. It starts by building the [convex hull](https://en.wikipedia.org/wiki/Convex_hull), and adding interior points from there. This implmentation uses another heuristic for insertion based on the ratio of the cost of adding the new point to the overall length of the segment, however any insertion algorithm could be applied after building the hull.
There are a number of algorithms to determine the convex hull. This implementation uses the [gift wrapping algorithm](https://en.wikipedia.org/wiki/Gift_wrapping_algorithm).
In essence, the steps are:
1. Determine the leftmost point
2. Continually add the most counterclockwise point until the convex hull is formed
3. For each remaining point p, find the segment i => j in the hull that minimizes cost(i -> p) + cost(p -> j) - cost(i -> j)
4. Of those, choose p that minimizes cost(i -> p -> j) / cost(i -> j)
5. Add p to the path between i and j
6. Repeat from #3 until there are no remaining points
## Implementation
```javascript
const convexHull = async points => {
const sp = points[0];
// Find the "left most point"
let leftmost = points[0];
for (const p of points) {
if (p[1] < leftmost[1]) {
leftmost = p;
}
}
const path = [leftmost];
while (true) {
const curPoint = path[path.length - 1];
let [selectedIdx, selectedPoint] = [0, null];
// find the "most counterclockwise" point
for (let [idx, p] of points.entries()) {
if (!selectedPoint || orientation(curPoint, p, selectedPoint) === 2) {
// this point is counterclockwise with respect to the current hull
// and selected point (e.g. more counterclockwise)
[selectedIdx, selectedPoint] = [idx, p];
}
}
// adding this to the hull so it's no longer available
points.splice(selectedIdx, 1);
// back to the furthest left point, formed a cycle, break
if (selectedPoint === leftmost) {
break;
}
// add to hull
path.push(selectedPoint);
}
while (points.length > 0) {
let [bestRatio, bestPointIdx, insertIdx] = [Infinity, null, 0];
for (let [freeIdx, freePoint] of points.entries()) {
// for every free point, find the point in the current path
// that minimizes the cost of adding the point minus the cost of
// the original segment
let [bestCost, bestIdx] = [Infinity, 0];
for (let [pathIdx, pathPoint] of path.entries()) {
const nextPathPoint = path[(pathIdx + 1) % path.length];
// the new cost minus the old cost
const evalCost =
pathCost([pathPoint, freePoint, nextPathPoint]) -
pathCost([pathPoint, nextPathPoint]);
if (evalCost < bestCost) {
[bestCost, bestIdx] = [evalCost, pathIdx];
}
}
// figure out how "much" more expensive this is with respect to the
// overall length of the segment
const nextPoint = path[(bestIdx + 1) % path.length];
const prevCost = pathCost([path[bestIdx], nextPoint]);
const newCost = pathCost([path[bestIdx], freePoint, nextPoint]);
const ratio = newCost / prevCost;
if (ratio < bestRatio) {
[bestRatio, bestPointIdx, insertIdx] = [ratio, freeIdx, bestIdx + 1];
}
}
const [nextPoint] = points.splice(bestPointIdx, 1);
path.splice(insertIdx, 0, nextPoint);
}
// rotate the array so that starting point is back first
const startIdx = path.findIndex(p => p === sp);
path.unshift(...path.splice(startIdx, path.length));
// go back home
path.push(sp);
};
```

View file

@ -1,73 +0,0 @@
---
type: heuristic-construction
order: 4
solverKey: furthestInsertion
friendlyName: Furthest Insertion
defaults:
evaluatingDetailLevel: 1
maxEvaluatingDetailLevel: 1
---
# Furthest Insertion
This is a heuristic construction algorithm. It selects the furthest point from the path, and then figures out where the best place to put it will be.
1. From the starting point
2. First, go to the closest point
3. Choose the point that is furthest from any of the points on the path
4. Find the cheapest place to add it in the path
5. Chosen point is no longer an "available point"
6. Continue from #3 until there are no available points, and then return to the start.
## Implementation
```javascript
const furthestInsertion = async points => {
// from the starting point
const path = [points.shift()];
//
// INITIALIZATION - go to the nearest point first
//
points.sort((a, b) => distance(path[0], b) - distance(path[0], a));
path.push(points.pop());
while (points.length > 0) {
//
// SELECTION - furthest point from the path
//
let [selectedDistance, selectedIdx] = [0, null];
for (const [freePointIdx, freePoint] of points.entries()) {
// find the minimum distance to the path for freePoint
let [bestCostToPath, costToPathIdx] = [Infinity, null];
for (const pathPoint of path) {
const dist = distance(freePoint, pathPoint);
if (dist < bestCostToPath) {
[bestCostToPath, costToPathIdx] = [dist, freePointIdx];
}
}
// if this point is further from the path than the currently selected
if (bestCostToPath > selectedDistance) {
[selectedDistance, selectedIdx] = [bestCostToPath, costToPathIdx];
}
}
const [nextPoint] = points.splice(selectedIdx, 1);
//
// INSERTION - find the insertion spot that minimizes distance
//
let [bestCost, bestIdx] = [Infinity, null];
for (let i = 1; i < path.length; i++) {
const insertionCost = pathCost([path[i - 1], nextPoint, path[i]]);
if (insertionCost < bestCost) {
[bestCost, bestIdx] = [insertionCost, i];
}
}
path.splice(bestIdx, 0, nextPoint);
}
// return to start after visiting all other points
path.push(path[0]);
};
```

View file

@ -1,68 +0,0 @@
---
type: heuristic-construction
order: 3
solverKey: nearestInsertion
friendlyName: Nearest Insertion
defaults:
evaluatingDetailLevel: 1
maxEvaluatingDetailLevel: 1
---
# Furthest Insertion
This is a heuristic construction algorithm. It selects the closest point to the path, and then figures out where the best place to put it will be.
1. From the starting point
2. First, go to the closest point
3. Choose the point that is **nearest** to the current path
4. Find the cheapest place to add it in the path
5. Chosen point is no longer an "available point"
6. Continue from #3 until there are no available points, and then return to the start.
## Implementation
```javascript
const nearestInsertion = async points => {
// from the starting point
const path = [points.shift()];
//
// INITIALIZATION - go to the nearest point first
//
points.sort((a, b) => distance(path[0], b) - distance(path[0], a));
path.push(points.pop());
while (points.length > 0) {
//
// SELECTION - nearest point to the path
//
let [selectedDistance, selectedIdx] = [Infinity, null];
for (const [freePointIdx, freePoint] of points.entries()) {
for (const pathPoint of path) {
const dist = distance(freePoint, pathPoint);
if (dist < selectedDistance) {
[selectedDistance, selectedIdx] = [dist, freePointIdx];
}
}
}
// get the next point to add
const [nextPoint] = points.splice(selectedIdx, 1);
//
// INSERTION - find the insertion spot that minimizes distance
//
let [bestCost, bestIdx] = [Infinity, null];
for (let i = 1; i < path.length; i++) {
const insertionCost = pathCost([path[i - 1], nextPoint, path[i]]);
if (insertionCost < bestCost) {
[bestCost, bestIdx] = [insertionCost, i];
}
}
path.splice(bestIdx, 0, nextPoint);
}
// return to start after visiting all other points
path.push(path[0]);
};
```

View file

@ -1,105 +0,0 @@
---
type: heuristic-construction
order: 6
solverKey: simulatedAnnealing
friendlyName: Simulated Annealing
defaults:
evaluatingDetailLevel: 1
maxEvaluatingDetailLevel: 1
---
# Simulated Annealing
Simulated annealing (SA) is a probabilistic technique for approximating the global optimum of a given function. Specifically, it is a metaheuristic to approximate global optimization in a large search space for an optimization problem.
For problems where finding an approximate global optimum is more important than finding a precise local optimum in a fixed amount of time, simulated annealing may be preferable to exact algorithms
## Implementation
```javascript
const simulatedAnnealing = async points => {
const sp = points[0];
const path = points;
const tempCoeff =
path.length < 10
? 1 - 1e-4
: path.length < 15
? 1 - 1e-5
: path.length < 25
? 1 - 1e-6
: 1 - 5e-7;
const deltaDistance = (aIdx, bIdx) => {
const aPrev = (aIdx - 1 + path.length) % path.length;
const aNext = (aIdx + 1 + path.length) % path.length;
const bPrev = (bIdx - 1 + path.length) % path.length;
const bNext = (bIdx + 1 + path.length) % path.length;
let diff =
distance(path[bPrev], path[aIdx]) +
distance(path[aIdx], path[bNext]) +
distance(path[aPrev], path[bIdx]) +
distance(path[bIdx], path[aNext]) -
distance(path[aPrev], path[aIdx]) -
distance(path[aIdx], path[aNext]) -
distance(path[bPrev], path[bIdx]) -
distance(path[bIdx], path[bNext]);
if (bPrev === aIdx || bNext === aIdx) {
diff += 2 * distance(path[aIdx], path[bIdx]);
}
return diff;
};
const changePath = temperature => {
// 2 random points
const a = 1 + Math.floor(Math.random() * (path.length - 1));
const b = 1 + Math.floor(Math.random() * (path.length - 1));
const delta = deltaDistance(a, b);
if (delta < 0 || Math.random() < Math.exp(-delta / temperature)) {
// swap points
[path[a], path[b]] = [path[b], path[a]];
}
};
const initialTemp = 100 * distance(path[0], path[1]);
let i = 0;
for (
let temperature = initialTemp;
temperature > 1e-6;
temperature *= tempCoeff
) {
changePath(temperature);
if (i % 10000 == 0) {
self.setEvaluatingPaths(() => ({
paths: [{ path, color: EVALUATING_PATH_COLOR }],
cost: pathCost(path)
}));
await self.sleep();
}
if (i % 100000 == 0) {
path.push(sp);
self.setBestPath(path, pathCost(path));
path.pop();
}
i++;
}
// rotate the array so that starting point is back first
rotateToStartingPoint(path, sp);
// go back home
path.push(sp);
const cost = pathCost(path);
self.setEvaluatingPaths(() => ({
paths: [{ path }],
cost
}));
self.setBestPath(path, cost);
};
makeSolver(simulatedAnnealing);
```

View file

@ -1,60 +0,0 @@
---
type: heuristic-improvement
order: 1
solverKey: twoOptInversion
friendlyName: Two Opt Inversion
defaults:
evaluatingDetailLevel: 1
maxEvaluatingDetailLevel: 1
---
# Two-Opt inversion
This algorithm is also known as 2-opt, 2-opt mutation, and cross-aversion. The general goal is to find places where the path crosses over itself, and then "undo" that crossing. It repeats until there are no crossings. A characteristic of this algorithm is that afterwards the path is guaranteed to have no crossings.
1. While a better path has not been found.
2. For each pair of points:
3. Reverse the path between the selected points.
4. If the new path is cheaper (shorter), keep it and continue searching. Remember that we found a better path.
5. If not, revert the path and continue searching.
## Implementation
```javascript
const twoOptInversion = async path => {
path.push(path[0]);
let best = pathCost(path);
let swapped = true;
while (swapped) {
swapped = false;
for (let pt1 = 1; pt1 < path.length - 1; pt1++) {
for (let pt2 = pt1 + 1; pt2 < path.length - 1; pt2++) {
// section of the path to reverse
const section = path.slice(pt1, pt2 + 1);
// reverse section in place
section.reverse();
// replace section of path with reversed section in place
path.splice(pt1, pt2 + 1 - pt1, ...section);
// calculate new cost
const newPath = path;
const cost = pathCost(newPath);
if (cost < best) {
// found a better path after the swap, keep it
swapped = true;
best = cost;
self.setBestPath(newPath, best);
} else {
// un-reverse the section
section.reverse();
path.splice(pt1, pt2 + 1 - pt1, ...section);
}
}
}
}
};
```

View file

@ -1,57 +0,0 @@
---
type: heuristic-improvement
order: 2
solverKey: twoOptReciprocalExchange
friendlyName: Two Opt Reciprocal Exchange
defaults:
evaluatingDetailLevel: 1
maxEvaluatingDetailLevel: 1
---
# Two-Opt Reciprocal Exchange
This algorithm is similar to the 2-opt mutation or inversion algorithm, although generally will find a less optimal path. However, the computational cost of calculating new solutions is less intensive.
The big difference with 2-opt mutation is not reversing the path between the 2 points. This algorithm is **not** always going to find a path that doesn't cross itself.
It could be worthwhile to try this algorithm prior to 2-opt inversion because of the cheaper cost of calculation, but probably not.
1. While a better path has not been found.
2. For each pair of points:
3. Swap the points in the path. That is, go to point B before point A, continue along the same path, and go to point A where point B was.
4. If the new path is cheaper (shorter), keep it and continue searching. Remember that we found a better path.
5. If not, revert the path and continue searching.
## Implementation
```javascript
const twoOptReciprocalExchange = async path => {
path.push(path[0]);
let best = pathCost(path);
let swapped = true;
self.setBestPath(path, best);
while (swapped) {
swapped = false;
for (let pt1 = 1; pt1 < path.length - 1; pt1++) {
for (let pt2 = pt1 + 1; pt2 < path.length - 1; pt2++) {
// swap current pair of points
[path[pt1], path[pt2]] = [path[pt2], path[pt1]];
// calculate new cost
const cost = pathCost(path);
if (cost < best) {
// found a better path after the swap, keep it
swapped = true;
best = cost;
} else {
// swap back - this one's worse
[path[pt1], path[pt2]] = [path[pt2], path[pt1]];
}
}
}
}
};
```

View file

@ -12,7 +12,6 @@ export const useAlgorithmInfo = () => {
in: [
"exhaustive"
"heuristic-construction"
"heuristic-improvement"
]
}
}

View file

@ -1,131 +0,0 @@
/* eslint-disable no-restricted-globals */
import makeSolver from "../makeSolver";
import { pathCost, setDifference } from "../cost";
import {
EVALUATING_PATH_COLOR,
EVALUATING_ERROR_COLOR,
EVALUATING_SEGMENT_COLOR
} from "../../constants";
const branchAndBoundOnCost = async (
points,
path = [],
visited = null,
overallBest = Infinity
) => {
if (visited === null) {
// initial call
path = [points.shift()];
points = new Set(points);
visited = new Set();
}
// figure out which points are left
const available = setDifference(points, visited);
// calculate the cost, from here, to go home
const backToStart = [...path, path[0]];
const cost = pathCost(backToStart);
if (cost > overallBest) {
// we may not be done, but have already traveled further than the best path
// no reason to continue
self.setEvaluatingPaths(
() => ({
paths: [
{
path: path.slice(0, path.length - 1),
color: EVALUATING_SEGMENT_COLOR
},
{
path: path.slice(path.length - 2, path.length + 1),
color: EVALUATING_ERROR_COLOR
}
],
cost
}),
2
);
await self.sleep();
return [null, null];
}
// still cheaper than the best, keep going deeper, and deeper, and deeper...
else {
self.setEvaluatingPaths(
() => ({
paths: [
{
path: path.slice(0, path.length - 1),
color: EVALUATING_SEGMENT_COLOR
},
{
path: path.slice(path.length - 2, path.length + 1),
color: EVALUATING_PATH_COLOR
}
],
cost
}),
2
);
}
await self.sleep();
if (available.size === 0) {
// at the end of the path, return where we're at
self.setEvaluatingPath(() => ({
path: { path: backToStart, color: EVALUATING_SEGMENT_COLOR },
cost
}));
return [cost, backToStart];
}
let [bestCost, bestPath] = [null, null];
// for every point yet to be visited along this path
for (const p of available) {
// go to that point
visited.add(p);
path.push(p);
// RECURSE - go through all the possible points from that point
const [curCost, curPath] = await branchAndBoundOnCost(
points,
path,
visited,
overallBest
);
// if that path is better and complete, keep it
if (curCost && (!bestCost || curCost < bestCost)) {
[bestCost, bestPath] = [curCost, curPath];
if (!overallBest || bestCost < overallBest) {
// found a new best complete path
overallBest = bestCost;
self.setBestPath(bestPath, bestCost);
}
}
// go back up and make that point available again
visited.delete(p);
path.pop();
self.setEvaluatingPath(
() => ({
path: { path, color: EVALUATING_SEGMENT_COLOR }
}),
2
);
await self.sleep();
}
await self.sleep();
return [bestCost, bestPath];
};
makeSolver(branchAndBoundOnCost);

View file

@ -1,159 +0,0 @@
/* eslint-disable no-restricted-globals */
import makeSolver from "../makeSolver";
import { pathCost, setDifference, intersects } from "../cost";
import {
EVALUATING_PATH_COLOR,
EVALUATING_ERROR_COLOR,
EVALUATING_SEGMENT_COLOR
} from "../../constants";
const branchAndBoundOnCostAndCross = async (
points,
path = [],
visited = null,
overallBest = Infinity
) => {
if (visited === null) {
// initial call
path = [points.shift()];
points = new Set(points);
visited = new Set();
}
// figure out which points are left
const available = setDifference(points, visited);
// calculate the cost, from here, to go home
const backToStart = [...path, path[0]];
const cost = pathCost(backToStart);
if (path.length > 3) {
// if this newly added edge crosses over the existing path,
// don't continue. It's been proven that an optimal path will
// not cross itself.
const newSegment = [path[path.length - 2], path[path.length - 1]];
for (let i = 1; i < path.length - 2; i++) {
if (intersects(path[i], path[i - 1], ...newSegment)) {
self.setEvaluatingPaths(
() => ({
paths: [
{
path: path.slice(0, path.length - 1),
color: EVALUATING_SEGMENT_COLOR
},
{
path: path.slice(path.length - 2, path.length + 1),
color: EVALUATING_ERROR_COLOR
}
],
cost
}),
2
);
await self.sleep();
return [null, null];
}
}
}
if (cost > overallBest) {
// we may not be done, but have already traveled further than the best path
// no reason to continue
self.setEvaluatingPaths(
() => ({
paths: [
{
path: path.slice(0, path.length - 1),
color: EVALUATING_SEGMENT_COLOR
},
{
path: path.slice(path.length - 2, path.length + 1),
color: EVALUATING_ERROR_COLOR
}
],
cost
}),
2
);
await self.sleep();
return [null, null];
}
// still cheaper than the best, keep going deeper, and deeper, and deeper...
self.setEvaluatingPaths(
() => ({
paths: [
{
path: path.slice(0, path.length - 1),
color: EVALUATING_SEGMENT_COLOR
},
{
path: path.slice(path.length - 2, path.length + 1),
color: EVALUATING_PATH_COLOR
}
],
cost
}),
2
);
await self.sleep();
if (available.size === 0) {
// at the end of the path, return where we're at
self.setEvaluatingPath(() => ({
path: { path: backToStart, color: EVALUATING_SEGMENT_COLOR },
cost
}));
await self.sleep();
return [cost, backToStart];
}
let [bestCost, bestPath] = [null, null];
// for every point yet to be visited along this path
for (const p of available) {
// go to that point
visited.add(p);
path.push(p);
// RECURSE - go through all the possible points from that point
const [curCost, curPath] = await branchAndBoundOnCostAndCross(
points,
path,
visited,
overallBest
);
// if that path is better and complete, keep it
if (curCost && (!bestCost || curCost < bestCost)) {
[bestCost, bestPath] = [curCost, curPath];
if (!overallBest || bestCost < overallBest) {
// found a new best complete path
overallBest = bestCost;
self.setBestPath(bestPath, bestCost);
}
}
// go back up and make that point available again
visited.delete(p);
path.pop();
self.setEvaluatingPath(
() => ({
path: { path, color: EVALUATING_SEGMENT_COLOR }
}),
2
);
await self.sleep();
}
await self.sleep();
return [bestCost, bestPath];
};
makeSolver(branchAndBoundOnCostAndCross);

View file

@ -1,41 +0,0 @@
/* eslint-disable no-restricted-globals */
import makeSolver from "../makeSolver";
import { pathCost } from "../cost";
const random = async points => {
let best = Infinity;
while (true) {
// save off the starting point
const start = points.shift();
// sort the remaining points
const path = points.sort(() => Math.random() - 0.5);
// put the starting point back
path.unshift(start);
// return to the starting point
path.push(start);
// calculate the new cost
const cost = pathCost(path);
if (cost < best) {
// we found a better path
best = cost;
self.setBestPath(path, cost);
}
self.setEvaluatingPaths(() => ({
paths: [{ path }],
cost
}));
// get rid of starting point at the end
path.pop();
await self.sleep();
}
};
makeSolver(random);

View file

@ -1,63 +0,0 @@
/* eslint-disable no-restricted-globals */
import makeSolver from "../makeSolver";
import { pathCost, distance } from "../cost";
const arbitraryInsertion = async points => {
// from the starting point
const path = [points.shift()];
//
// INITIALIZATION - go to the nearest point first
//
points.sort((a, b) => distance(path[0], b) - distance(path[0], a));
path.push(points.pop());
self.setEvaluatingPaths(() => ({
paths: [{ path }],
cost: pathCost(path)
}));
// randomly sort points - this is the order they will be added
// to the path
points.sort(() => Math.random() - 0.5);
while (points.length > 0) {
//
// SELECTION - choose a next point randomly
//
const nextPoint = points.pop();
//
// INSERTION - find the insertion spot that minimizes distance
//
let [bestCost, bestIdx] = [Infinity, null];
for (let i = 1; i < path.length; i++) {
const insertionCost = pathCost([path[i - 1], nextPoint, path[i]]);
if (insertionCost < bestCost) {
[bestCost, bestIdx] = [insertionCost, i];
}
}
path.splice(bestIdx, 0, nextPoint);
self.setEvaluatingPaths(() => ({
paths: [{ path }],
cost: pathCost(path)
}));
await self.sleep();
}
// return to start after visiting all other points
path.push(path[0]);
const cost = pathCost(path);
self.setEvaluatingPaths(() => ({
paths: [{ path }],
cost
}));
await self.sleep();
self.setBestPath(path, cost);
};
makeSolver(arbitraryInsertion);

View file

@ -1,125 +0,0 @@
/* eslint-disable no-restricted-globals */
import makeSolver from "../makeSolver";
import { pathCost, counterClockWise, rotateToStartingPoint } from "../cost";
import {
EVALUATING_PATH_COLOR,
EVALUATING_SEGMENT_COLOR
} from "../../constants";
const convexHull = async points => {
const sp = points[0];
// Find the "left most point"
let leftmost = points[0];
for (const p of points) {
if (p[1] < leftmost[1]) {
leftmost = p;
}
}
const path = [leftmost];
while (true) {
const curPoint = path[path.length - 1];
let [selectedIdx, selectedPoint] = [0, null];
// find the "most counterclockwise" point
for (let [idx, p] of points.entries()) {
// eslint-disable-next-line
self.setEvaluatingPaths(
() => ({
paths: [
{
path: [...path, selectedPoint || curPoint],
color: EVALUATING_SEGMENT_COLOR
},
{ path: [curPoint, p], color: EVALUATING_PATH_COLOR }
]
}),
2
);
await self.sleep();
if (!selectedPoint || counterClockWise(curPoint, p, selectedPoint)) {
// this point is counterclockwise with respect to the current hull
// and selected point (e.g. more counterclockwise)
[selectedIdx, selectedPoint] = [idx, p];
}
}
// adding this to the hull so it's no longer available
points.splice(selectedIdx, 1);
// back to the furthest left point, formed a cycle, break
if (selectedPoint === leftmost) {
break;
}
// add to hull
path.push(selectedPoint);
}
self.setEvaluatingPaths(() => ({
paths: [{ path, color: EVALUATING_PATH_COLOR }],
cost: pathCost(path)
}));
await self.sleep();
while (points.length > 0) {
let [bestRatio, bestPointIdx, insertIdx] = [Infinity, null, 0];
for (let [freeIdx, freePoint] of points.entries()) {
// for every free point, find the point in the current path
// that minimizes the cost of adding the path minus the cost of
// the original segment
let [bestCost, bestIdx] = [Infinity, 0];
for (let [pathIdx, pathPoint] of path.entries()) {
const nextPathPoint = path[(pathIdx + 1) % path.length];
// the new cost minus the old cost
const evalCost =
pathCost([pathPoint, freePoint, nextPathPoint]) -
pathCost([pathPoint, nextPathPoint]);
if (evalCost < bestCost) {
[bestCost, bestIdx] = [evalCost, pathIdx];
}
}
// figure out how "much" more expensive this is with respect to the
// overall length of the segment
const nextPoint = path[(bestIdx + 1) % path.length];
const prevCost = pathCost([path[bestIdx], nextPoint]);
const newCost = pathCost([path[bestIdx], freePoint, nextPoint]);
const ratio = newCost / prevCost;
if (ratio < bestRatio) {
[bestRatio, bestPointIdx, insertIdx] = [ratio, freeIdx, bestIdx + 1];
}
}
const [nextPoint] = points.splice(bestPointIdx, 1);
path.splice(insertIdx, 0, nextPoint);
self.setEvaluatingPaths(() => ({
paths: [{ path }],
cost: pathCost(path)
}));
await self.sleep();
}
// rotate the array so that starting point is back first
rotateToStartingPoint(path, sp);
// go back home
path.push(sp);
const cost = pathCost(path);
self.setEvaluatingPaths(() => ({
paths: [{ path }],
cost
}));
self.setBestPath(path, cost);
};
makeSolver(convexHull);

View file

@ -1,79 +0,0 @@
/* eslint-disable no-restricted-globals */
import makeSolver from "../makeSolver";
import { pathCost, distance } from "../cost";
const furthestInsertion = async points => {
// from the starting point
const path = [points.shift()];
//
// INITIALIZATION - go to the nearest point first
//
points.sort((a, b) => distance(path[0], b) - distance(path[0], a));
path.push(points.pop());
self.setEvaluatingPaths(() => ({
paths: [{ path }],
cost: pathCost(path)
}));
await self.sleep();
while (points.length > 0) {
//
// SELECTION - furthest point from the path
//
let [selectedDistance, selectedIdx] = [0, null];
for (const [freePointIdx, freePoint] of points.entries()) {
// find the minimum distance to the path for freePoint
let [bestCostToPath, costToPathIdx] = [Infinity, null];
for (const pathPoint of path) {
const dist = distance(freePoint, pathPoint);
if (dist < bestCostToPath) {
[bestCostToPath, costToPathIdx] = [dist, freePointIdx];
}
}
// if this point is further from the path than the currently selected
if (bestCostToPath > selectedDistance) {
[selectedDistance, selectedIdx] = [bestCostToPath, costToPathIdx];
}
}
// get the next point to add
const [nextPoint] = points.splice(selectedIdx, 1);
//
// INSERTION - find the insertion spot that minimizes distance
//
let [bestCost, bestIdx] = [Infinity, null];
for (let i = 1; i < path.length; i++) {
const insertionCost = pathCost([path[i - 1], nextPoint, path[i]]);
if (insertionCost < bestCost) {
[bestCost, bestIdx] = [insertionCost, i];
}
}
path.splice(bestIdx, 0, nextPoint);
self.setEvaluatingPaths(() => ({
paths: [{ path }],
cost: pathCost(path)
}));
await self.sleep();
}
// return to start after visiting all other points
path.push(path[0]);
const cost = pathCost(path);
self.setEvaluatingPaths(() => ({
paths: [{ path }],
cost
}));
await self.sleep();
self.setBestPath(path, cost);
};
makeSolver(furthestInsertion);

View file

@ -1,72 +0,0 @@
/* eslint-disable no-restricted-globals */
import makeSolver from "../makeSolver";
import { pathCost, distance } from "../cost";
const nearestInsertion = async points => {
// from the starting point
const path = [points.shift()];
//
// INITIALIZATION - go to the nearest point first
//
points.sort((a, b) => distance(path[0], b) - distance(path[0], a));
path.push(points.pop());
self.setEvaluatingPaths(() => ({
paths: [{ path }],
cost: pathCost(path)
}));
await self.sleep();
while (points.length > 0) {
//
// SELECTION - nearest point to the path
//
let [selectedDistance, selectedIdx] = [Infinity, null];
for (const [freePointIdx, freePoint] of points.entries()) {
for (const pathPoint of path) {
const dist = distance(freePoint, pathPoint);
if (dist < selectedDistance) {
[selectedDistance, selectedIdx] = [dist, freePointIdx];
}
}
}
// get the next point to add
const [nextPoint] = points.splice(selectedIdx, 1);
//
// INSERTION - find the insertion spot that minimizes distance
//
let [bestCost, bestIdx] = [Infinity, null];
for (let i = 1; i < path.length; i++) {
const insertionCost = pathCost([path[i - 1], nextPoint, path[i]]);
if (insertionCost < bestCost) {
[bestCost, bestIdx] = [insertionCost, i];
}
}
path.splice(bestIdx, 0, nextPoint);
self.setEvaluatingPaths(() => ({
paths: [{ path }],
cost: pathCost(path)
}));
await self.sleep();
}
// return to start after visiting all other points
path.push(path[0]);
const cost = pathCost(path);
self.setEvaluatingPaths(() => ({
paths: [{ path }],
cost
}));
await self.sleep();
self.setBestPath(path, cost);
};
makeSolver(nearestInsertion);

View file

@ -1,90 +0,0 @@
/* eslint-disable no-restricted-globals */
import makeSolver from "../makeSolver";
import { pathCost, distance, rotateToStartingPoint } from "../cost";
import { EVALUATING_PATH_COLOR } from "../../constants";
const simulatedAnnealing = async points => {
const sp = points[0];
const path = points;
const tempCoeff =
path.length < 10
? 1 - 1e-4
: path.length < 15
? 1 - 1e-5
: path.length < 30
? 1 - 1e-6
: 1 - 5e-7;
const deltaDistance = (aIdx, bIdx) => {
const aPrev = (aIdx - 1 + path.length) % path.length;
const aNext = (aIdx + 1 + path.length) % path.length;
const bPrev = (bIdx - 1 + path.length) % path.length;
const bNext = (bIdx + 1 + path.length) % path.length;
let diff =
distance(path[bPrev], path[aIdx]) +
distance(path[aIdx], path[bNext]) +
distance(path[aPrev], path[bIdx]) +
distance(path[bIdx], path[aNext]) -
distance(path[aPrev], path[aIdx]) -
distance(path[aIdx], path[aNext]) -
distance(path[bPrev], path[bIdx]) -
distance(path[bIdx], path[bNext]);
if (bPrev === aIdx || bNext === aIdx) {
diff += 2 * distance(path[aIdx], path[bIdx]);
}
return diff;
};
const changePath = temperature => {
// 2 random points
const a = 1 + Math.floor(Math.random() * (path.length - 1));
const b = 1 + Math.floor(Math.random() * (path.length - 1));
const delta = deltaDistance(a, b);
if (delta < 0 || Math.random() < Math.exp(-delta / temperature)) {
// swap points
[path[a], path[b]] = [path[b], path[a]];
}
};
const initialTemp = 100 * distance(path[0], path[1]);
let i = 0;
for (
let temperature = initialTemp;
temperature > 1e-6;
temperature *= tempCoeff
) {
changePath(temperature);
if (i % 10000 == 0) {
self.setEvaluatingPaths(() => ({
paths: [{ path, color: EVALUATING_PATH_COLOR }],
cost: pathCost(path)
}));
await self.sleep();
}
if (i % 100000 == 0) {
path.push(sp);
self.setBestPath(path, pathCost(path));
path.pop();
}
i++;
}
// rotate the array so that starting point is back first
rotateToStartingPoint(path, sp);
// go back home
path.push(sp);
const cost = pathCost(path);
self.setEvaluatingPaths(() => ({
paths: [{ path }],
cost
}));
self.setBestPath(path, cost);
};
makeSolver(simulatedAnnealing);

View file

@ -1,72 +0,0 @@
/* eslint-disable no-restricted-globals */
import makeSolver from "../makeSolver";
import { pathCost } from "../cost";
import {
EVALUATING_PATH_COLOR,
EVALUATING_SEGMENT_COLOR
} from "../../constants";
const twoOptInversion = async path => {
path.push(path[0]);
let best = pathCost(path);
let swapped = true;
self.setBestPath(path, best);
while (swapped) {
swapped = false;
for (let pt1 = 1; pt1 < path.length - 1; pt1++) {
for (let pt2 = pt1 + 1; pt2 < path.length - 1; pt2++) {
// section of the path to reverse
const section = path.slice(pt1, pt2 + 1);
// reverse section in place
section.reverse();
// replace section of path with reversed section in place
path.splice(pt1, pt2 + 1 - pt1, ...section);
// calculate new cost
const newPath = path;
const cost = pathCost(newPath);
self.setEvaluatingPaths(() => ({
paths: [
{ path: path.slice(0, pt1), color: EVALUATING_SEGMENT_COLOR },
{ path: path.slice(pt1 + 1, pt2), color: EVALUATING_SEGMENT_COLOR },
{ path: path.slice(pt2 + 1), color: EVALUATING_SEGMENT_COLOR },
{
path: [path[pt1 - 1], path[pt1], path[pt1 + 1]],
color: EVALUATING_PATH_COLOR
},
{
path: [path[pt2 - 1], path[pt2], path[pt2 + 1]],
color: EVALUATING_PATH_COLOR
}
],
cost
}));
await self.sleep();
if (cost < best) {
// found a better path after the swap, keep it
swapped = true;
best = cost;
self.setBestPath(newPath, best);
} else {
// un-reverse the section
section.reverse();
path.splice(pt1, pt2 + 1 - pt1, ...section);
}
self.setEvaluatingPaths(() => ({
paths: [{ path, color: EVALUATING_SEGMENT_COLOR }]
}));
await self.sleep();
}
}
}
};
makeSolver(twoOptInversion);

View file

@ -1,65 +0,0 @@
/* eslint-disable no-restricted-globals */
import makeSolver from "../makeSolver";
import { pathCost } from "../cost";
import {
EVALUATING_PATH_COLOR,
EVALUATING_SEGMENT_COLOR
} from "../../constants";
const twoOptReciprocalExchange = async path => {
path.push(path[0]);
let best = pathCost(path);
let swapped = true;
self.setBestPath(path, best);
while (swapped) {
swapped = false;
for (let pt1 = 1; pt1 < path.length - 1; pt1++) {
for (let pt2 = pt1 + 1; pt2 < path.length - 1; pt2++) {
// swap current pair of points
[path[pt1], path[pt2]] = [path[pt2], path[pt1]];
// calculate new cost
const cost = pathCost(path);
self.setEvaluatingPaths(() => ({
paths: [
{ path: path.slice(0, pt1), color: EVALUATING_SEGMENT_COLOR },
{ path: path.slice(pt1 + 1, pt2), color: EVALUATING_SEGMENT_COLOR },
{ path: path.slice(pt2 + 1), color: EVALUATING_SEGMENT_COLOR },
{
path: [path[pt1 - 1], path[pt1], path[pt1 + 1]],
color: EVALUATING_PATH_COLOR
},
{
path: [path[pt2 - 1], path[pt2], path[pt2 + 1]],
color: EVALUATING_PATH_COLOR
}
],
cost
}));
await self.sleep();
if (cost < best) {
// found a better path after the swap, keep it
swapped = true;
best = cost;
self.setBestPath(path, best);
} else {
// swap back - this one's worse
[path[pt1], path[pt2]] = [path[pt2], path[pt1]];
}
self.setEvaluatingPath(() => ({
path: { path, color: EVALUATING_SEGMENT_COLOR }
}));
await self.sleep();
}
}
}
};
makeSolver(twoOptReciprocalExchange);

View file

@ -1,31 +1,7 @@
import random from "./exhaustive/random.worker";
import depthFirstSearch from "./exhaustive/depthFirstSearch.worker";
import branchAndBoundOnCost from "./exhaustive/branchAndBoundOnCost.worker";
import branchAndBoundOnCostAndCross from "./exhaustive/branchAndBoundOnCostAndCross.worker";
import nearestNeighbor from "./heuristic-construction/nearestNeighbor.worker";
import arbitraryInsertion from "./heuristic-construction/arbitraryInsertion.worker";
import nearestInsertion from "./heuristic-construction/nearestInsertion.worker";
import furthestInsertion from "./heuristic-construction/furthestInsertion.worker";
import convexHull from "./heuristic-construction/convexHull.worker";
import simulatedAnnealing from "./heuristic-construction/simulatedAnnealing.worker";
import twoOptInversion from "./heuristic-improvement/twoOptInversion.worker";
import twoOptReciprocalExchange from "./heuristic-improvement/twoOptReciprocalExchange.worker";
export default {
random,
depthFirstSearch,
branchAndBoundOnCost,
branchAndBoundOnCostAndCross,
nearestNeighbor,
arbitraryInsertion,
furthestInsertion,
nearestInsertion,
convexHull,
simulatedAnnealing,
twoOptInversion,
twoOptReciprocalExchange
};

View file

@ -1,47 +1,28 @@
import * as actions from "./actions";
const usTop12 = [
[-73.85835427500902, 40.56507951957753],
[-77.54976052500858, 38.772432514145194],
[-78.91206521250587, 42.66742768420476],
[-70.95796365000933, 42.66742768420476],
[-80.27436990000314, 26.176558881220437],
[-84.4052292750001, 34.108547937473524],
[-82.55952615000031, 28.24770207922181],
[-84.66890115000008, 30.089457425014395],
[-89.89839333750201, 29.746655988569763],
[-96.62202615000125, 32.640688397241334],
[-95.3036667750014, 29.287759374472813],
[-97.76460427500368, 30.089457425014395],
[-101.89546365000065, 34.97727964358472],
[-112.22261208749687, 33.23080293029681],
[-111.38765114999953, 35.01327961148759],
[-115.56245583750162, 36.08588188690158],
[-118.63862771249869, 33.999320468363095],
[-117.2323777124963, 32.97311239658548],
[-123.12104958749816, 38.222145234071036],
[-124.26362771250061, 41.13019627380825],
[-120.13276833749595, 39.72528830651809],
[-111.82710427499693, 41.13019627380825],
[-105.2353073999977, 39.961475963760066],
[-87.43745583749975, 41.69048709677229],
[-93.1064011499991, 45.29144400095841],
[-90.20601052499944, 38.772432514145194],
[-117.27632302500142, 47.50341272285311],
[-122.72554177499823, 45.8757982618686],
[-122.81343240000076, 48.152468818056875]
[100.47966551545956, 13.763377348238809],
// [95.99777586767772, 20.659057411016644], //myanmar
// [102.61447417504604, 18.354304323036597], //laos
// [104.82860225771425, 12.260559642146927], //cambodia
[105.61972922924626, 21.337690594700124],
[101.61025278140067, 3.852198947284515],
[106.83275037018893, -6.209465092032497],
[103.81336272752452, 1.3468280345835395],
[114.92250057366596, 4.945297365972065],
[121.00856338015852, 14.602156304250775]
];
const initialViewport = {
latitude: 39.8097343,
longitude: -98.5556199,
latitude: 8.880258536666247,
longitude: 113.01211067669622,
zoom: 4
};
const initialState = {
points: usTop12.sort(() => Math.random() + 0.5),
points: usTop12,
viewport: initialViewport,
algorithm: "convexHull",
algorithm: "depthFirstSearch",
delay: 100,
evaluatingDetailLevel: 2,
maxEvaluatingDetailLevel: 2,