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
|
# Maintainers: keep this list of plugins up to date
|
||||||
# List plugins in %%{_libdir}/%%{name}/lib, less '.so' and 'Gui.so', here
|
# 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
|
%global exported_libs libOndselSolver
|
||||||
|
|
||||||
|
|||||||
@@ -139,14 +139,14 @@ SET_PYTHON_PREFIX_SUFFIX(Path)
|
|||||||
|
|
||||||
INSTALL(TARGETS Path DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
INSTALL(TARGETS Path DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||||
|
|
||||||
# --- TSP Solver Python module (pybind11) ---
|
add_library(tsp_solver SHARED tsp_solver_pybind.cpp tsp_solver.cpp)
|
||||||
find_package(pybind11 REQUIRED)
|
target_include_directories(tsp_solver PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${pybind11_INCLUDE_DIR})
|
||||||
|
target_link_libraries(tsp_solver PRIVATE pybind11::module Python3::Python)
|
||||||
pybind11_add_module(tsp_solver MODULE tsp_solver_pybind.cpp tsp_solver.cpp)
|
if (FREECAD_WARN_ERROR)
|
||||||
target_include_directories(tsp_solver PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
target_compile_warn_error(tsp_solver)
|
||||||
|
endif()
|
||||||
|
|
||||||
SET_BIN_DIR(tsp_solver tsp_solver /Mod/CAM)
|
SET_BIN_DIR(tsp_solver tsp_solver /Mod/CAM)
|
||||||
SET_PYTHON_PREFIX_SUFFIX(tsp_solver)
|
SET_PYTHON_PREFIX_SUFFIX(tsp_solver)
|
||||||
|
|
||||||
INSTALL(TARGETS tsp_solver DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
INSTALL(TARGETS tsp_solver DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||||
# -------------------------------------------
|
|
||||||
|
|||||||
@@ -179,22 +179,37 @@ std::vector<int> solve_impl(
|
|||||||
// Two optimization techniques:
|
// Two optimization techniques:
|
||||||
// 1. 2-Opt: Reverse segments of the route to eliminate crossing paths
|
// 1. 2-Opt: Reverse segments of the route to eliminate crossing paths
|
||||||
// 2. Relocation: Move individual points to better positions in the route
|
// 2. Relocation: Move individual points to better positions in the route
|
||||||
bool improvementFound = true;
|
//
|
||||||
while (improvementFound) {
|
// For open routes (no endPoint), additional optimizations are applied that
|
||||||
improvementFound = false;
|
// 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 ---
|
// --- 2-Opt Optimization ---
|
||||||
// Try reversing every possible segment of the route.
|
// Try reversing every possible segment of the route.
|
||||||
// If reversing segment [i+1...j-1] reduces total distance, keep it.
|
// 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
|
// 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;
|
bool reorderFound = true;
|
||||||
while (reorderFound) {
|
while (reorderFound) {
|
||||||
reorderFound = false;
|
reorderFound = false;
|
||||||
for (size_t i = 0; i + 3 < route.size(); ++i) {
|
for (size_t i = 0; i < limitReorderI; ++i) {
|
||||||
for (size_t j = i + 3; j < route.size(); ++j) {
|
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
|
// 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]]);
|
+ dist(pts[route[j - 1]], pts[route[j]]);
|
||||||
|
|
||||||
// New edges after reversal: (i+1)→j and i→(j-1)
|
// New edges after reversal: (i+1)→j and i→(j-1)
|
||||||
@@ -205,8 +220,23 @@ std::vector<int> solve_impl(
|
|||||||
if (newLen < curLen) {
|
if (newLen < curLen) {
|
||||||
// Reverse the segment between i+1 and j (exclusive)
|
// Reverse the segment between i+1 and j (exclusive)
|
||||||
std::reverse(route.begin() + i + 1, route.begin() + j);
|
std::reverse(route.begin() + i + 1, route.begin() + j);
|
||||||
|
subRouteLengthCurrentPart = dist(pts[route[i]], pts[route[i + 1]]);
|
||||||
reorderFound = true;
|
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 ---
|
// --- Relocation Optimization ---
|
||||||
// Try moving each point to a different position in the route.
|
// Try moving each point to a different position in the route.
|
||||||
// If moving point i to position j improves the route, do it.
|
// If moving point i to position j improves the route, do it.
|
||||||
|
if (lastImprovementAtStep == 2) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
bool relocateFound = true;
|
bool relocateFound = true;
|
||||||
while (relocateFound) {
|
while (relocateFound) {
|
||||||
relocateFound = false;
|
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)
|
// 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)
|
// Current cost: edges around point i and edge j→(j+1)
|
||||||
double curLen = dist(pts[route[i - 1]], pts[route[i]])
|
double curLen = subRouteLengthCurrentPart
|
||||||
+ dist(pts[route[i]], pts[route[i + 1]])
|
|
||||||
+ dist(pts[route[j]], pts[route[j + 1]]);
|
+ dist(pts[route[j]], pts[route[j + 1]]);
|
||||||
|
|
||||||
// New cost: bypass i, insert i after j
|
// New cost: bypass i, insert i after j
|
||||||
double newLen = dist(pts[route[i - 1]], pts[route[i + 1]])
|
double newLen = subRouteLengthNewPart + dist(pts[route[j]], pts[route[i]])
|
||||||
+ dist(pts[route[j]], pts[route[i]])
|
+ dist(pts[route[i]], pts[route[j + 1]]);
|
||||||
+ dist(pts[route[i]], pts[route[j + 1]]) + Base::Precision::Confusion();
|
|
||||||
|
|
||||||
if (newLen < curLen) {
|
if (newLen < curLen) {
|
||||||
// Move point i to position after j
|
// Move point i to position after j
|
||||||
int node = route[i];
|
int node = route[i];
|
||||||
route.erase(route.begin() + i);
|
route.erase(route.begin() + i);
|
||||||
route.insert(route.begin() + j + 1, node);
|
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;
|
relocateFound = true;
|
||||||
improvementFound = true;
|
lastImprovementAtStep = 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try moving point i forward (to positions after i)
|
// Try moving point i forward (to positions after i)
|
||||||
for (size_t j = i + 1; j + 1 < route.size(); ++j) {
|
for (size_t j = i + 1; j < limitRelocationJ; ++j) {
|
||||||
double curLen = dist(pts[route[i - 1]], pts[route[i]])
|
double curLen = subRouteLengthCurrentPart
|
||||||
+ dist(pts[route[i]], pts[route[i + 1]])
|
|
||||||
+ dist(pts[route[j]], pts[route[j + 1]]);
|
+ dist(pts[route[j]], pts[route[j + 1]]);
|
||||||
|
|
||||||
double newLen = dist(pts[route[i - 1]], pts[route[i + 1]])
|
double newLen = subRouteLengthNewPart + dist(pts[route[j]], pts[route[i]])
|
||||||
+ dist(pts[route[j]], pts[route[i]])
|
+ dist(pts[route[i]], pts[route[j + 1]]);
|
||||||
+ dist(pts[route[i]], pts[route[j + 1]]) + Base::Precision::Confusion();
|
|
||||||
|
|
||||||
if (newLen < curLen) {
|
if (newLen < curLen) {
|
||||||
int node = route[i];
|
int node = route[i];
|
||||||
route.erase(route.begin() + i);
|
route.erase(route.begin() + i);
|
||||||
route.insert(route.begin() + j, node);
|
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;
|
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):
|
class TestTSPSolver(PathTestBase):
|
||||||
"""Test class for the TSP (Traveling Salesman Problem) solver."""
|
"""Test class for the TSP (Traveling Salesman Problem) solver."""
|
||||||
|
|
||||||
|
DEBUG = False # Global debug flag for print_tunnels
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""Set up test environment."""
|
"""Set up test environment."""
|
||||||
# Create test points arranged in a simple pattern
|
# Create test points arranged in a simple pattern
|
||||||
@@ -54,6 +56,8 @@ class TestTSPSolver(PathTestBase):
|
|||||||
|
|
||||||
def print_tunnels(self, tunnels, title):
|
def print_tunnels(self, tunnels, title):
|
||||||
"""Helper function to print tunnel information."""
|
"""Helper function to print tunnel information."""
|
||||||
|
if not self.DEBUG:
|
||||||
|
return
|
||||||
print(f"\n{title}:")
|
print(f"\n{title}:")
|
||||||
for i, tunnel in enumerate(tunnels):
|
for i, tunnel in enumerate(tunnels):
|
||||||
orig_idx = tunnel.get("index", "N/A")
|
orig_idx = tunnel.get("index", "N/A")
|
||||||
|
|||||||
Reference in New Issue
Block a user