Skip to content

Commit

Permalink
Eulerian paths (#83)
Browse files Browse the repository at this point in the history
Eulerian paths added, true by default
  • Loading branch information
eyal0 committed Jan 25, 2018
1 parent eb531c5 commit 538707c
Show file tree
Hide file tree
Showing 15 changed files with 600 additions and 70 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
*.exe
*.out
*.app
*_test
*_tests

#built or generated project files
.deps/*
Expand Down
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ matrix:

before_install:
- if [ "$TRAVIS_OS_NAME" == "osx" ]; then brew update && brew reinstall -s libtool && brew outdated boost || brew upgrade boost && brew install gtkmm gerbv; fi
- if [ "$TRAVIS_OS_NAME" == "linux" ]; then wget https://sourceforge.net/projects/pcb2gcode/files/pcb2gcode/boost_1_56_0_amd64.tar.gz && tar -xf boost_1_56_0_amd64.tar.gz && export BOOST_DIR=$PWD/boost_1_56_0_amd64; fi
- if [ "$TRAVIS_OS_NAME" == "linux" ]; then wget https://github.com/eyal0/pcb2gcode/releases/download/1.3.3/boost_1_65_1_amd64.tar.gz && tar -xf boost_1_65_1_amd64.tar.gz && export BOOST_DIR=$PWD/boost_1_65_1_amd64; fi

install:
- export CXX=$COMPILER
Expand Down
8 changes: 6 additions & 2 deletions Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pcb2gcode_SOURCES = \
core.cpp \
drill.hpp \
drill.cpp \
eulerian_paths.hpp \
exporter.hpp \
Fixed.hpp \
geometry.hpp \
Expand All @@ -24,6 +25,7 @@ pcb2gcode_SOURCES = \
mill.hpp \
ngc_exporter.hpp \
ngc_exporter.cpp \
segmentize.hpp \
surface.hpp \
surface.cpp \
surface_vectorial.hpp \
Expand All @@ -50,7 +52,9 @@ LIBS = $(glibmm_LIBS) $(gdkmm_LIBS) $(gerbv_LIBS) $(BOOST_PROGRAM_OPTIONS_LIBS)

EXTRA_DIST = millproject

check_PROGRAMS = voronoi_tests
check_PROGRAMS = voronoi_tests eulerian_paths_tests segmentize_tests
voronoi_tests_SOURCES = voronoi.hpp voronoi.cpp voronoi_tests.cpp
eulerian_paths_tests_SOURCES = eulerian_paths_tests.cpp
segmentize_tests_SOURCES = segmentize_tests.cpp

TESTS = voronoi_tests
TESTS = $(check_PROGRAMS)
167 changes: 167 additions & 0 deletions eulerian_paths.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
#ifndef EULERIAN_PATHS_H
#define EULERIAN_PATHS_H

#include <vector>
#include <map>

#include "geometry.hpp"

/* This finds a minimal number of eulerian paths that cover the input.
* The number of paths returned is equal to the number of vertices
* with odd edge count divided by 2.
*
* To use, first get paths. Each path is a vector of n points that
* represents n-1 line segments. Each path is considerd
* bidirectional.
*
* After adding paths, build the Eulerian paths. The resulting paths
* cover all segments in the input paths with the minimum number of
* paths as described above.
*/

namespace eulerian_paths {

bool operator !=(const point_type& x, const point_type& y) {
return std::tie(x.x(), x.y()) != std::tie(y.x(), y.y());
}

bool operator ==(const point_type& x, const point_type& y) {
return std::tie(x.x(), x.y()) == std::tie(y.x(), y.y());
}

bool operator !=(const point_type_fp& x, const point_type_fp& y) {
return std::tie(x.x(), x.y()) != std::tie(y.x(), y.y());
}

bool operator ==(const point_type_fp& x, const point_type_fp& y) {
return std::tie(x.x(), x.y()) == std::tie(y.x(), y.y());
}

template <typename point_t, typename linestring_t, typename multi_linestring_t, typename point_less_than_p = std::less<point_t>>
multi_linestring_t get_eulerian_paths(const multi_linestring_t& paths) {
// Create a map from vertex to each path that starts or ends (or
// both) at that vertex. It's a map to an input into the input
// paths.
std::multimap<point_t, size_t, point_less_than_p> vertex_to_unvisited_path_index;
for (size_t i = 0; i < paths.size(); i++) {
auto& path = paths[i];
if (path.size() < 2) {
// Valid path must have a start and end.
continue;
}
point_t start = path.front();
point_t end = path.back();
vertex_to_unvisited_path_index.emplace(start, i);
vertex_to_unvisited_path_index.emplace(end, i);
}

// Given a point, make a path from that point as long as possible
// until a dead end. Assume that point itself is already in the
// list.
std::function<void(const point_t&, linestring_t*)> make_path = [&] (const point_t& point, linestring_t* new_path) -> void {
// Find an unvisited path that leads from point, any will do.
auto vertex_and_path_index = vertex_to_unvisited_path_index.find(point);
if (vertex_and_path_index == vertex_to_unvisited_path_index.end()) {
// No more paths to follow.
return;
}
size_t path_index = vertex_and_path_index->second;
auto& path = paths[path_index];
if (point == path.front()) {
// Append this path in the forward direction.
new_path->insert(new_path->end(), path.cbegin()+1, path.cend());
} else {
// Append this path in the reverse direction.
new_path->insert(new_path->end(), path.crbegin()+1, path.crend());
}
vertex_to_unvisited_path_index.erase(vertex_and_path_index); // Remove from the first vertex.
point_t& new_point = new_path->back();
// We're bound to find one, otherwise there is a serious error in
// the algorithm.
auto range = vertex_to_unvisited_path_index.equal_range(new_point);
for (auto iter = range.first; iter != range.second; iter++) {
if (iter->second == path_index) {
// Remove the path from the last vertex
vertex_to_unvisited_path_index.erase(iter);
break;
}
}
// Continue making the path from here.
make_path(new_point, new_path);
};

// Only call this when there are no vertices with odd edge count.
// This will traverse a path and, if it finds an unvisited edge,
// will make a Euler circuit there and stitch it into the current
// path. This continues until the end of the path.
auto stitch_loops = [&] (linestring_t *euler_path) -> void {
for (size_t i = 0; i < euler_path->size(); i++) {
// Does this vertex have any unvisited edges?
if (vertex_to_unvisited_path_index.count((*euler_path)[i]) > 0) {
// Make a path from here. We don't need the first element, it's already in our path.
linestring_t new_loop{};
make_path((*euler_path)[i], &new_loop);
// Now we stitch it in.
euler_path->insert(euler_path->begin()+i+1, new_loop.begin(), new_loop.end());
}
}
};

// We use Hierholzer's algorithm to find the minimum cycles. First,
// make a path from each vertex with odd path count until the path
// ends.
std::vector<point_t> odd_vertices;
// First find all vertices with odd count. We do it separate from
// the loop below because the below loop will modify
// vertex_to_unvisited_path_index and invalidate the iterator.
for (auto iter = vertex_to_unvisited_path_index.cbegin();
iter != vertex_to_unvisited_path_index.cend();) {
auto& vertex = iter->first;
if (vertex_to_unvisited_path_index.count(vertex) % 2 == 1) {
odd_vertices.push_back(vertex);
}
// Advance to a different vertex.
iter = vertex_to_unvisited_path_index.upper_bound(vertex);
}

multi_linestring_t euler_paths{};
for (auto iter = odd_vertices.cbegin(); iter != odd_vertices.cend(); iter++) {
auto& vertex = *iter;
// We check again because it might have become even after being
// connected to a different path.
if (vertex_to_unvisited_path_index.count(vertex) % 2 == 1) {
// Make a path starting from vertex with odd count.
linestring_t new_path;
new_path.push_back(vertex);
make_path(vertex, &new_path);
euler_paths.push_back(new_path);
}
}

// At this point, all remaining vertices have an even number of
// edges. So if we make a path from one, it is sure to end back
// where it started. We'll go over all our current Euler paths and
// stitch in loops anywhere that there is an unvisited edge.
for (auto& euler_path : euler_paths) {
stitch_loops(&euler_path);
}

// Anything remaining is on islands. Make all those paths, too. No
// stitching needed because all vertices have an even number of
// edges.
while(vertex_to_unvisited_path_index.size() > 0) {
const auto vertex = vertex_to_unvisited_path_index.cbegin()->first;
linestring_t new_path;
new_path.push_back(vertex);
make_path(vertex, &new_path);
// We can stitch right now because all vertices already have
// even number of edges.
stitch_loops(&new_path);
euler_paths.push_back(new_path);
}

return euler_paths;
}

} //namespace eulerian_paths
#endif //EULERIAN_PATHS_H
144 changes: 144 additions & 0 deletions eulerian_paths_tests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
#define BOOST_TEST_MODULE eulerian paths tests
#include <boost/test/included/unit_test.hpp>

#include "eulerian_paths.hpp"

using std::vector;
using namespace eulerian_paths;

BOOST_AUTO_TEST_SUITE(eulerian_paths_tests);

struct PointLessThan {
bool operator()(const point_type& a, const point_type& b) const {
return std::tie(a.x(), a.y()) < std::tie(b.x(), b.y());
}
};

BOOST_AUTO_TEST_CASE(do_nothing_points) {
BOOST_CHECK(0==0);
get_eulerian_paths<point_type, linestring_type, multi_linestring_type, PointLessThan>({{{1,1},{2,2},{3,3}}});
}

// 3x3 grid connected like a window pane:
// 1---2---3
// | | |
// 4---5---6
// | | |
// 7---8---9
BOOST_AUTO_TEST_CASE(window_pane) {
BOOST_CHECK(0==0);
vector<vector<int>> euler_paths = get_eulerian_paths<int, vector<int>, vector<vector<int>>>({
{1,2},
{2,3},
{4,5},
{5,6},
{7,8},
{8,9},
{1,4},
{4,7},
{2,5},
{5,8},
{3,6},
{6,9},
});
int edges_visited = 0;
for (size_t i = 0; i < euler_paths.size(); i++) {
edges_visited += euler_paths[i].size()-1;
for (size_t j = 0; j < euler_paths[i].size(); j++) {
printf("%d ", euler_paths[i][j]);
}
printf("\n");
}
BOOST_CHECK(edges_visited == 12);
BOOST_CHECK(euler_paths.size() == 2);
}

// 3x3 grid connected like a window pane, but corners are longer paths:
// 1---2---3
// | | |
// 4---5---6
// | | |
// 7---8---9
BOOST_AUTO_TEST_CASE(window_pane_with_longer_corners) {
BOOST_CHECK(0==0);
vector<vector<int>> euler_paths = get_eulerian_paths<int, vector<int>, vector<vector<int>>>({
{4,5},
{5,6},
{4,7,8},
{2,5},
{5,8},
{6,9,8},
{4,1,2},
{2,3,6},
});
int edges_visited = 0;
for (size_t i = 0; i < euler_paths.size(); i++) {
edges_visited += euler_paths[i].size()-1;
for (size_t j = 0; j < euler_paths[i].size(); j++) {
printf("%d ", euler_paths[i][j]);
}
printf("\n");
}
BOOST_CHECK(edges_visited == 12);
BOOST_CHECK(euler_paths.size() == 2);
}

// Bridge
// 5---2---1---6
// | | | |
// 3---4 7---8
BOOST_AUTO_TEST_CASE(bridge) {
BOOST_CHECK(0==0);
vector<vector<int>> euler_paths = get_eulerian_paths<int, vector<int>, vector<vector<int>>>({
{5,2},
{2,1},
{1,6},
{3,4},
{7,8},
{5,3},
{2,4},
{1,7},
{6,8},
});
int edges_visited = 0;
for (size_t i = 0; i < euler_paths.size(); i++) {
edges_visited += euler_paths[i].size()-1;
for (size_t j = 0; j < euler_paths[i].size(); j++) {
printf("%d ", euler_paths[i][j]);
}
printf("\n");
}
BOOST_CHECK(edges_visited == 9);
BOOST_CHECK(euler_paths.size() == 1);
}

// Disjoint Loops
// 5---2 1---6 0---9
// | | | |
// 3---4 7---8
BOOST_AUTO_TEST_CASE(disjoint_loops) {
BOOST_CHECK(0==0);
vector<vector<int>> euler_paths = get_eulerian_paths<int, vector<int>, vector<vector<int>>>({
{5,2},
{1,6},
{3,4},
{7,8},
{5,3},
{2,4},
{1,7},
{6,8},
{0,9},
});
int edges_visited = 0;
for (size_t i = 0; i < euler_paths.size(); i++) {
edges_visited += euler_paths[i].size()-1;
for (size_t j = 0; j < euler_paths[i].size(); j++) {
printf("%d ", euler_paths[i][j]);
}
printf("\n");
}
BOOST_CHECK(edges_visited == 9);
BOOST_CHECK(euler_paths.size() == 3);
}

BOOST_AUTO_TEST_SUITE_END()
2 changes: 2 additions & 0 deletions main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ int main(int argc, char* argv[])
isolator->zchange = vm["zchange"].as<double>() * unit;
isolator->extra_passes = vm["extra-passes"].as<int>();
isolator->optimise = vm["optimise"].as<bool>();
isolator->eulerian_paths = vm["eulerian-paths"].as<bool>();
isolator->tolerance = tolerance;
isolator->explicit_tolerance = explicit_tolerance;
isolator->mirror_absolute = vm["mirror-absolute"].as<bool>();
Expand All @@ -134,6 +135,7 @@ int main(int argc, char* argv[])
cutter->do_steps = true;
cutter->stepsize = vm["cut-infeed"].as<double>() * unit;
cutter->optimise = vm["optimise"].as<bool>();
isolator->eulerian_paths = vm["eulerian-paths"].as<bool>();
cutter->tolerance = tolerance;
cutter->explicit_tolerance = explicit_tolerance;
cutter->mirror_absolute = vm["mirror-absolute"].as<bool>();
Expand Down
1 change: 1 addition & 0 deletions mill.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class RoutingMill: public Mill
public:
double tool_diameter;
bool optimise;
bool eulerian_paths;
};

/******************************************************************************/
Expand Down
Loading

0 comments on commit 538707c

Please sign in to comment.