Add startPoint and endPoint support to TSP solver and Python wrapper; add tests

- Enhanced the C++ TSP solver to accept optional start and end points, so the route can begin and/or end at the closest point to specified coordinates.
- Updated the Python pybind11 wrapper and PathUtils.sort_locations_tsp to support startPoint and endPoint as named parameters.
- Added a new Python test suite (TestTSPSolver.py) to verify correct handling of start/end points and integration with PathUtils.
- Registered the new test in TestCAMApp.py and CMakeLists.txt for automatic test discovery.
This commit is contained in:
Billy Huddleston
2025-10-17 15:03:10 -04:00
parent 75f1f184ac
commit 945389c761
7 changed files with 373 additions and 9 deletions

View File

@@ -55,16 +55,41 @@ void twoOptSwap(std::vector<int>& path, size_t i, size_t k)
}
} // namespace
std::vector<int> TSPSolver::solve(const std::vector<TSPPoint>& points)
/**
* @brief Solve the Traveling Salesperson Problem using 2-opt algorithm
*
* This implementation handles optional start and end point constraints:
* - If startPoint is provided, the path will begin at the point closest to startPoint
* - If endPoint is provided, the path will end at the point closest to endPoint
* - If both are provided, the path will respect both constraints while optimizing the middle path
* - The algorithm ensures all points are visited exactly once
*/
std::vector<int> TSPSolver::solve(const std::vector<TSPPoint>& points,
const TSPPoint* startPoint,
const TSPPoint* endPoint)
{
size_t n = points.size();
if (n == 0) {
return {};
}
// Start with a simple nearest neighbor path
std::vector<bool> visited(n, false);
std::vector<int> path;
// If startPoint provided, find the closest point to it
size_t current = 0;
if (startPoint) {
double minDist = std::numeric_limits<double>::max();
for (size_t i = 0; i < n; ++i) {
double d = dist(points[i], *startPoint);
if (d < minDist) {
minDist = d;
current = i;
}
}
}
path.push_back(static_cast<int>(current));
visited[current] = true;
for (size_t step = 1; step < n; ++step) {
@@ -101,5 +126,132 @@ std::vector<int> TSPSolver::solve(const std::vector<TSPPoint>& points)
}
}
}
// Handle end point constraint if specified
if (endPoint) {
// If both start and end points are specified, we need to handle them differently
if (startPoint) {
// Find the closest points to start and end
size_t startIdx = 0;
size_t endIdx = 0;
double minStartDist = std::numeric_limits<double>::max();
double minEndDist = std::numeric_limits<double>::max();
// Find the indices of the closest points to both start and end points
for (size_t i = 0; i < n; ++i) {
// Find closest to start
double dStart = dist(points[i], *startPoint);
if (dStart < minStartDist) {
minStartDist = dStart;
startIdx = i;
}
// Find closest to end
double dEnd = dist(points[i], *endPoint);
if (dEnd < minEndDist) {
minEndDist = dEnd;
endIdx = i;
}
}
// If start and end are different points, create a new path
if (startIdx != endIdx) {
// Create a new path starting with the start point and ending with the end point
// This ensures both constraints are met
std::vector<bool> visited(n, false);
std::vector<int> newPath;
// Add start point
newPath.push_back(static_cast<int>(startIdx));
visited[startIdx] = true;
// Add all other points except end point using nearest neighbor algorithm
// This builds a path that starts at startIdx and visits all intermediate points
size_t current = startIdx;
for (size_t step = 1; step < n - 1; ++step) {
double minDist = std::numeric_limits<double>::max();
size_t next = n; // Invalid index (n is out of bounds)
for (size_t i = 0; i < n; ++i) {
if (!visited[i] && i != endIdx) {
double d = dist(points[current], points[i]);
if (d < minDist) {
minDist = d;
next = i;
}
}
}
if (next == n) {
break; // No more points to add
}
current = next;
newPath.push_back(static_cast<int>(current));
visited[current] = true;
}
// Add end point as the final stop in the path
newPath.push_back(static_cast<int>(endIdx));
// Apply 2-opt optimization while preserving the start and end points
// The algorithm only swaps edges between interior points
bool improved = true;
while (improved) {
improved = false;
// Start from 1 and end before the last point to preserve start/end constraints
for (size_t i = 1; i < newPath.size() - 1; ++i) {
for (size_t k = i + 1; k < newPath.size() - 1; ++k) {
// Calculate improvement in distance if we swap these edges
double delta = dist(points[newPath[i - 1]], points[newPath[k]])
+ dist(points[newPath[i]], points[newPath[k + 1]])
- dist(points[newPath[i - 1]], points[newPath[i]])
- dist(points[newPath[k]], points[newPath[k + 1]]);
// If the swap reduces the total distance, make the swap
if (delta < -Base::Precision::Confusion()) {
std::reverse(newPath.begin() + static_cast<long>(i),
newPath.begin() + static_cast<long>(k) + 1);
improved = true;
}
}
}
}
path = newPath;
}
// If start and end are the same point, keep path as is
}
else {
// Only end point specified (no start point constraint)
// Find the point in the current path that's closest to the desired end point
double minDist = std::numeric_limits<double>::max();
size_t endIdx = 0;
for (size_t i = 0; i < n; ++i) {
double d = dist(points[path[i]], *endPoint);
if (d < minDist) {
minDist = d;
endIdx = i;
}
}
// Rotate the path so that endIdx is at the end
// This preserves the relative order of points while ensuring the path ends
// at the point closest to the specified end coordinates
if (endIdx != n - 1) {
std::vector<int> newPath;
// Start with points after endIdx
for (size_t i = endIdx + 1; i < n; ++i) {
newPath.push_back(path[i]);
}
// Then add points from beginning up to and including endIdx
for (size_t i = 0; i <= endIdx; ++i) {
newPath.push_back(path[i]);
}
path = newPath;
}
}
}
return path;
}