Add 2-Opt TSP solver

This update introduces a new C++ 2-Opt TSP solver with Python bindings.

src/Mod/CAM/App/CMakeLists.txt:
- Add build and install rules for the new pybind11-based tsp_solver Python module

src/Mod/CAM/App/tsp_solver.cpp:
- Add new C++ implementation of a 2-Opt TSP solver with nearest-neighbor initialization

src/Mod/CAM/App/tsp_solver.h:
- Add TSPPoint struct and TSPSolver class with 2-Opt solve method

src/Mod/CAM/App/tsp_solver_pybind.cpp:
- Add pybind11 wrapper exposing the TSP solver to Python as tsp_solver.solve

src/Mod/CAM/PathScripts/PathUtils.py:
- Add sort_locations_tsp Python wrapper for the C++ TSP solver
- Use tsp_solver.solve for TSP-based
This commit is contained in:
Billy Huddleston
2025-10-17 14:08:31 -04:00
parent 442a36bea6
commit f9347c781b
5 changed files with 216 additions and 0 deletions

View File

@@ -138,3 +138,15 @@ SET_BIN_DIR(Path PathApp /Mod/CAM)
SET_PYTHON_PREFIX_SUFFIX(Path)
INSTALL(TARGETS Path DESTINATION ${CMAKE_INSTALL_LIBDIR})
# --- TSP Solver Python module (pybind11) ---
find_package(pybind11 REQUIRED)
pybind11_add_module(tsp_solver MODULE tsp_solver_pybind.cpp tsp_solver.cpp)
target_include_directories(tsp_solver PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
SET_BIN_DIR(tsp_solver tsp_solver /Mod/CAM)
SET_PYTHON_PREFIX_SUFFIX(tsp_solver)
INSTALL(TARGETS tsp_solver DESTINATION ${CMAKE_INSTALL_LIBDIR})
# -------------------------------------------

View File

@@ -0,0 +1,105 @@
/***************************************************************************
* Copyright (c) 2025 Billy Huddleston <billy@ivdc.com> *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License (LGPL) *
* as published by the Free Software Foundation; either version 2 of *
* the License, or (at your option) any later version. *
* for detail see the LICENCE text file. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU Library General Public License for more details. *
* *
* You should have received a copy of the GNU Library General Public *
* License along with this program; if not, write to the Free Software *
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
* USA *
* *
***************************************************************************/
#include "tsp_solver.h"
#include <vector>
#include <limits>
#include <cmath>
#include <algorithm>
#include <Base/Precision.h>
namespace
{
// Euclidean distance between two points
double dist(const TSPPoint& a, const TSPPoint& b)
{
double dx = a.x - b.x;
double dy = a.y - b.y;
return std::sqrt(dx * dx + dy * dy);
}
// Calculate total path length
double pathLength(const std::vector<TSPPoint>& points, const std::vector<int>& path)
{
double total = 0.0;
size_t n = path.size();
for (size_t i = 0; i < n - 1; ++i) {
total += dist(points[path[i]], points[path[i + 1]]);
}
// Optionally, close the loop: total += dist(points[path[n-1]], points[path[0]]);
return total;
}
// 2-Opt swap
void twoOptSwap(std::vector<int>& path, size_t i, size_t k)
{
std::reverse(path.begin() + static_cast<long>(i), path.begin() + static_cast<long>(k) + 1);
}
} // namespace
std::vector<int> TSPSolver::solve(const std::vector<TSPPoint>& points)
{
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;
size_t current = 0;
path.push_back(static_cast<int>(current));
visited[current] = true;
for (size_t step = 1; step < n; ++step) {
double min_dist = std::numeric_limits<double>::max();
size_t next = n; // Use n as an invalid index
for (size_t i = 0; i < n; ++i) {
if (!visited[i]) {
double d = dist(points[current], points[i]);
if (d < min_dist) {
min_dist = d;
next = i;
}
}
}
current = next;
path.push_back(static_cast<int>(current));
visited[current] = true;
}
// 2-Opt optimization
bool improved = true;
while (improved) {
improved = false;
for (size_t i = 1; i < n - 1; ++i) {
for (size_t k = i + 1; k < n; ++k) {
double delta = dist(points[path[i - 1]], points[path[k]])
+ dist(points[path[i]], points[path[(k + 1) % n]])
- dist(points[path[i - 1]], points[path[i]])
- dist(points[path[k]], points[path[(k + 1) % n]]);
if (delta < -Base::Precision::Confusion()) {
twoOptSwap(path, i, k);
improved = true;
}
}
}
}
return path;
}

View File

@@ -0,0 +1,42 @@
/***************************************************************************
* Copyright (c) 2025 Billy Huddleston <billy@ivdc.com> *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License (LGPL) *
* as published by the Free Software Foundation; either version 2 of *
* the License, or (at your option) any later version. *
* for detail see the LICENCE text file. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU Library General Public License for more details. *
* *
* You should have received a copy of the GNU Library General Public *
* License along with this program; if not, write to the Free Software *
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
* USA *
* *
***************************************************************************/
#pragma once
#include <vector>
#include <utility>
#include <limits>
#include <cmath>
struct TSPPoint
{
double x, y;
TSPPoint(double x_, double y_)
: x(x_)
, y(y_)
{}
};
class TSPSolver
{
public:
// Returns a vector of indices representing the visit order using 2-Opt
static std::vector<int> solve(const std::vector<TSPPoint>& points);
};

View File

@@ -0,0 +1,44 @@
/***************************************************************************
* Copyright (c) 2025 Billy Huddleston <billy@ivdc.com> *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License (LGPL) *
* as published by the Free Software Foundation; either version 2 of *
* the License, or (at your option) any later version. *
* for detail see the LICENCE text file. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU Library General Public License for more details. *
* *
* You should have received a copy of the GNU Library General Public *
* License along with this program; if not, write to the Free Software *
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
* USA *
* *
***************************************************************************/
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include "tsp_solver.h"
namespace py = pybind11;
std::vector<int> tspSolvePy(const std::vector<std::pair<double, double>>& points)
{
std::vector<TSPPoint> pts;
for (const auto& p : points) {
pts.emplace_back(p.first, p.second);
}
return TSPSolver::solve(pts);
}
PYBIND11_MODULE(tsp_solver, m)
{
m.doc() = "Simple TSP solver (2-Opt) for FreeCAD";
m.def("solve",
&tspSolvePy,
py::arg("points"),
"Solve TSP for a list of (x, y) points using 2-Opt, returns visit order");
}

View File

@@ -2,6 +2,7 @@
# ***************************************************************************
# * Copyright (c) 2014 Dan Falck <ddfalck@gmail.com> *
# * Copyright (c) 2025 Billy Huddleston <billy@ivdc.com> *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
@@ -29,6 +30,7 @@ import Path
import Path.Main.Job as PathJob
import math
from numpy import linspace
import tsp_solver
# lazily loaded modules
from lazy_loader.lazy_loader import LazyLoader
@@ -611,6 +613,17 @@ def sort_locations(locations, keys, attractors=None):
return out
def sort_locations_tsp(locations, keys, attractors=None):
"""
Python wrapper for the C++ TSP solver. Takes a list of dicts (locations),
a list of keys (e.g. ['x', 'y']), and optional attractors.
Returns the sorted list of locations in TSP order, starting at (0,0).
"""
points = [(loc[keys[0]], loc[keys[1]]) for loc in locations]
order = tsp_solver.solve(points)
return [locations[i] for i in order]
def guessDepths(objshape, subs=None):
"""
takes an object shape and optional list of subobjects and returns a depth_params