From 79aa87c593e0b26327fa9409cc4f103c0725324a Mon Sep 17 00:00:00 2001 From: Billy Huddleston Date: Wed, 17 Dec 2025 18:51:57 -0500 Subject: [PATCH] CAM: Add open-route optimizations to point-based TSP solver Improves the point-based TSP solver to match tunnel solver behavior for open routes (no endpoint constraint). Now applies 2-opt and relocation optimizations that allow reversing or relocating segments to the end of the route, resulting in better path optimization when the ending point is flexible. Now links tsp_solver with Python3::Python and uses add_library for compatibility with FreeCAD and Fedora packaging. src/Mod/CAM/App/tsp_solver.cpp: - Add optimization limit variables for controlled iteration - Add 2-opt and relocation optimizations for open routes - Use Base::Precision::Confusion() for epsilon values - Track last improvement step for efficient loop control src/Mod/CAM/App/CMakeLists.txt: - Switch tsp_solver from pybind11_add_module to add_library - Link tsp_solver with pybind11::module and Python3::Python - Update include directories for consistency --- package/fedora/freecad.spec | 2 +- src/Mod/CAM/App/CMakeLists.txt | 12 +-- src/Mod/CAM/App/tsp_solver.cpp | 115 +++++++++++++++++++++----- src/Mod/CAM/CAMTests/TestTSPSolver.py | 4 + 4 files changed, 104 insertions(+), 29 deletions(-) diff --git a/package/fedora/freecad.spec b/package/fedora/freecad.spec index 604dd17179..fab5fb8dbc 100644 --- a/package/fedora/freecad.spec +++ b/package/fedora/freecad.spec @@ -29,7 +29,7 @@ Source0: freecad-sources.tar.gz # Maintainers: keep this list of plugins up to date # List plugins in %%{_libdir}/%%{name}/lib, less '.so' and 'Gui.so', here -%global plugins AssemblyApp AssemblyGui CAMSimulator DraftUtils Fem FreeCAD Import Inspection MatGui Materials Measure Mesh MeshPart Part PartDesignGui Path PathApp PathSimulator Points QtUnitGui ReverseEngineering Robot Sketcher Spreadsheet Start Surface TechDraw Web _PartDesign area flatmesh libDriver libDriverDAT libDriverSTL libDriverUNV libE57Format libMEFISTO2 libSMDS libSMESH libSMESHDS libStdMeshers libarea-native +%global plugins AssemblyApp AssemblyGui CAMSimulator DraftUtils Fem FreeCAD Import Inspection MatGui Materials Measure Mesh MeshPart Part PartDesignGui Path PathApp PathSimulator Points QtUnitGui ReverseEngineering Robot Sketcher Spreadsheet Start Surface TechDraw Web _PartDesign area flatmesh libDriver libDriverDAT libDriverSTL libDriverUNV libE57Format libMEFISTO2 libSMDS libSMESH libSMESHDS libStdMeshers libarea-native tsp_solver %global exported_libs libOndselSolver diff --git a/src/Mod/CAM/App/CMakeLists.txt b/src/Mod/CAM/App/CMakeLists.txt index 4ec9936d3c..9a6cdbaa50 100644 --- a/src/Mod/CAM/App/CMakeLists.txt +++ b/src/Mod/CAM/App/CMakeLists.txt @@ -139,14 +139,14 @@ 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}) +add_library(tsp_solver SHARED tsp_solver_pybind.cpp tsp_solver.cpp) +target_include_directories(tsp_solver PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${pybind11_INCLUDE_DIR}) +target_link_libraries(tsp_solver PRIVATE pybind11::module Python3::Python) +if (FREECAD_WARN_ERROR) + target_compile_warn_error(tsp_solver) +endif() SET_BIN_DIR(tsp_solver tsp_solver /Mod/CAM) SET_PYTHON_PREFIX_SUFFIX(tsp_solver) INSTALL(TARGETS tsp_solver DESTINATION ${CMAKE_INSTALL_LIBDIR}) -# ------------------------------------------- diff --git a/src/Mod/CAM/App/tsp_solver.cpp b/src/Mod/CAM/App/tsp_solver.cpp index 9fb7961096..64c071630d 100644 --- a/src/Mod/CAM/App/tsp_solver.cpp +++ b/src/Mod/CAM/App/tsp_solver.cpp @@ -179,22 +179,37 @@ std::vector solve_impl( // Two optimization techniques: // 1. 2-Opt: Reverse segments of the route to eliminate crossing paths // 2. Relocation: Move individual points to better positions in the route - bool improvementFound = true; - while (improvementFound) { - improvementFound = false; + // + // For open routes (no endPoint), additional optimizations are applied that + // allow reversing/relocating segments to the end of the route. + size_t limitReorderI = route.size() - 2; + if (tempEndIdx != -1) { + limitReorderI -= 1; + } + size_t limitReorderJ = route.size(); + size_t limitRelocationI = route.size() - 1; + size_t limitRelocationJ = route.size() - 1; + int lastImprovementAtStep = 0; + + while (true) { // --- 2-Opt Optimization --- // Try reversing every possible segment of the route. // If reversing segment [i+1...j-1] reduces total distance, keep it. // // Example: Route A-B-C-D-E becomes A-D-C-B-E if reversing B-C-D is better + if (lastImprovementAtStep == 1) { + break; + } bool reorderFound = true; while (reorderFound) { reorderFound = false; - for (size_t i = 0; i + 3 < route.size(); ++i) { - for (size_t j = i + 3; j < route.size(); ++j) { + for (size_t i = 0; i < limitReorderI; ++i) { + double subRouteLengthCurrentPart = dist(pts[route[i]], pts[route[i + 1]]); + + for (size_t j = i + 3; j < limitReorderJ; ++j) { // Current edges: i→(i+1) and (j-1)→j - double curLen = dist(pts[route[i]], pts[route[i + 1]]) + double curLen = subRouteLengthCurrentPart + dist(pts[route[j - 1]], pts[route[j]]); // New edges after reversal: (i+1)→j and i→(j-1) @@ -205,8 +220,23 @@ std::vector solve_impl( if (newLen < curLen) { // Reverse the segment between i+1 and j (exclusive) std::reverse(route.begin() + i + 1, route.begin() + j); + subRouteLengthCurrentPart = dist(pts[route[i]], pts[route[i + 1]]); reorderFound = true; - improvementFound = true; + lastImprovementAtStep = 1; + } + } + + // Open route optimization: can reverse from i to end if no endpoint constraint + if (tempEndIdx == -1) { + double curLen = dist(pts[route[i]], pts[route[i + 1]]); + double newLen = dist(pts[route[i]], pts[route[limitReorderJ - 1]]) + + Base::Precision::Confusion(); + + if (newLen < curLen) { + // Reverse the order of points after i-th to the last point + std::reverse(route.begin() + i + 1, route.begin() + limitReorderJ); + reorderFound = true; + lastImprovementAtStep = 1; } } } @@ -215,52 +245,93 @@ std::vector solve_impl( // --- Relocation Optimization --- // Try moving each point to a different position in the route. // If moving point i to position j improves the route, do it. + if (lastImprovementAtStep == 2) { + break; + } bool relocateFound = true; while (relocateFound) { relocateFound = false; - for (size_t i = 1; i + 1 < route.size(); ++i) { + for (size_t i = 1; i < limitRelocationI; ++i) { + double subRouteLengthCurrentPart = dist(pts[route[i - 1]], pts[route[i]]) + + dist(pts[route[i]], pts[route[i + 1]]); + double subRouteLengthNewPart = dist(pts[route[i - 1]], pts[route[i + 1]]) + + Base::Precision::Confusion(); // Try moving point i backward (to positions before i) - for (size_t j = 1; j + 2 < i; ++j) { + for (size_t j = 0; j + 2 < i; ++j) { // Current cost: edges around point i and edge j→(j+1) - double curLen = dist(pts[route[i - 1]], pts[route[i]]) - + dist(pts[route[i]], pts[route[i + 1]]) + double curLen = subRouteLengthCurrentPart + dist(pts[route[j]], pts[route[j + 1]]); // New cost: bypass i, insert i after j - double newLen = dist(pts[route[i - 1]], pts[route[i + 1]]) - + dist(pts[route[j]], pts[route[i]]) - + dist(pts[route[i]], pts[route[j + 1]]) + Base::Precision::Confusion(); + double newLen = subRouteLengthNewPart + dist(pts[route[j]], pts[route[i]]) + + dist(pts[route[i]], pts[route[j + 1]]); if (newLen < curLen) { // Move point i to position after j int node = route[i]; route.erase(route.begin() + i); route.insert(route.begin() + j + 1, node); + subRouteLengthCurrentPart = dist(pts[route[i - 1]], pts[route[i]]) + + dist(pts[route[i]], pts[route[i + 1]]); + subRouteLengthNewPart = dist(pts[route[i - 1]], pts[route[i + 1]]) + + Base::Precision::Confusion(); relocateFound = true; - improvementFound = true; + lastImprovementAtStep = 2; } } // Try moving point i forward (to positions after i) - for (size_t j = i + 1; j + 1 < route.size(); ++j) { - double curLen = dist(pts[route[i - 1]], pts[route[i]]) - + dist(pts[route[i]], pts[route[i + 1]]) + for (size_t j = i + 1; j < limitRelocationJ; ++j) { + double curLen = subRouteLengthCurrentPart + dist(pts[route[j]], pts[route[j + 1]]); - double newLen = dist(pts[route[i - 1]], pts[route[i + 1]]) - + dist(pts[route[j]], pts[route[i]]) - + dist(pts[route[i]], pts[route[j + 1]]) + Base::Precision::Confusion(); + double newLen = subRouteLengthNewPart + dist(pts[route[j]], pts[route[i]]) + + dist(pts[route[i]], pts[route[j + 1]]); if (newLen < curLen) { int node = route[i]; route.erase(route.begin() + i); route.insert(route.begin() + j, node); + subRouteLengthCurrentPart = dist(pts[route[i - 1]], pts[route[i]]) + + dist(pts[route[i]], pts[route[i + 1]]); + subRouteLengthNewPart = dist(pts[route[i - 1]], pts[route[i + 1]]) + + Base::Precision::Confusion(); relocateFound = true; - improvementFound = true; + lastImprovementAtStep = 2; } } } + + // Open route optimization: can relocate the last point anywhere + if (tempEndIdx == -1) { + double subRouteLengthCurrentPart + = dist(pts[route[route.size() - 2]], pts[route[route.size() - 1]]); + + for (size_t j = 0; j + 2 < route.size(); ++j) { + double curLen = subRouteLengthCurrentPart + + dist(pts[route[j]], pts[route[j + 1]]); + + double newLen = dist(pts[route[j]], pts[route[route.size() - 1]]) + + dist(pts[route[route.size() - 1]], pts[route[j + 1]]) + + Base::Precision::Confusion(); + + if (newLen < curLen) { + // Relocate the last point after j-th point + int node = route[route.size() - 1]; + route.erase(route.begin() + route.size() - 1); + route.insert(route.begin() + j + 1, node); + subRouteLengthCurrentPart + = dist(pts[route[route.size() - 2]], pts[route[route.size() - 1]]); + relocateFound = true; + lastImprovementAtStep = 2; + } + } + } + } + + if (lastImprovementAtStep == 0) { + break; // No additional improvements could be made } } diff --git a/src/Mod/CAM/CAMTests/TestTSPSolver.py b/src/Mod/CAM/CAMTests/TestTSPSolver.py index ca908fd363..69d4e3a193 100644 --- a/src/Mod/CAM/CAMTests/TestTSPSolver.py +++ b/src/Mod/CAM/CAMTests/TestTSPSolver.py @@ -31,6 +31,8 @@ from CAMTests.PathTestUtils import PathTestBase class TestTSPSolver(PathTestBase): """Test class for the TSP (Traveling Salesman Problem) solver.""" + DEBUG = False # Global debug flag for print_tunnels + def setUp(self): """Set up test environment.""" # Create test points arranged in a simple pattern @@ -54,6 +56,8 @@ class TestTSPSolver(PathTestBase): def print_tunnels(self, tunnels, title): """Helper function to print tunnel information.""" + if not self.DEBUG: + return print(f"\n{title}:") for i, tunnel in enumerate(tunnels): orig_idx = tunnel.get("index", "N/A")