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
This commit is contained in:
Billy Huddleston
2025-12-17 18:51:57 -05:00
parent 08cc569d6a
commit 79aa87c593
4 changed files with 104 additions and 29 deletions

View File

@@ -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

View File

@@ -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})
# -------------------------------------------

View File

@@ -179,22 +179,37 @@ std::vector<int> 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<int> 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<int> 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
}
}

View File

@@ -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")