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:
@@ -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
|
||||
|
||||
|
||||
@@ -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})
|
||||
# -------------------------------------------
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user