diff --git a/README.md b/README.md index 9ac479f1..d1516139 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,8 @@ const auto print_vertex_callback{[](const auto vertex) { traverse(my_graph, start_vertex, print_vertex_callback); ``` -For more details, take a look at the [documentation](https://bobluppes.github.io/graaf/). +For more examples, take a look at our [example section](./examples/README.md). +More details can be found in our [documentation](https://bobluppes.github.io/graaf/). ## Requirements - C++ 20 diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..78c1709b --- /dev/null +++ b/examples/README.md @@ -0,0 +1,6 @@ +# Examples +This section contains example usages of the Graaf library. +If there is a usecase you would like to see an example of, please open an issue in our [issue tracker](https://github.com/bobluppes/graaf/issues). + +- [Shortest path](./shortest_path/README.md) +- [Dot serialization](./dot_serialization/README.md) \ No newline at end of file diff --git a/examples/dot_serialization/README.md b/examples/dot_serialization/README.md index 6e0f030e..62296de0 100644 --- a/examples/dot_serialization/README.md +++ b/examples/dot_serialization/README.md @@ -1,4 +1,4 @@ -# Dot Serialization +# Dot Serialization Example The `to_dot` function as defined under `graaf::io` can be used to searialize graphs to the [dot format](https://graphviz.org/doc/info/lang.html). This can be handy for debugging purposes, as well as for post-processing of your graphs in another tool which supports the format. @@ -38,18 +38,20 @@ We define two lambdas to serialize these vertices and edges. Here we can use any **Vertex writer** ```c++ -const auto vertex_writer{[](const my_vertex& vertex) -> std::string { - const auto color{vertex.number <= 25 ? "lightcyan" : "mediumspringgreen"}; - return fmt::format("label=\"{}\", fillcolor={}, style=filled", vertex.name, color); +const auto vertex_writer{[](graaf::vertex_id_t vertex_id, + const my_vertex& vertex) -> std::string { + const auto color{vertex.number <= 25 ? "lightcyan" : "mediumspringgreen"}; + return fmt::format("label=\"{}: {}\", fillcolor={}, style=filled", vertex_id, vertex.name, color); }}; ``` **Edge writer** ```c++ -const auto edge_writer{[](const my_edge& edge) -> std::string { - const auto style{edge.priority == edge_priority::HIGH ? "solid" : "dashed"}; - return fmt::format("label=\"{}\", style={}", edge.weight, style); +const auto edge_writer{[](const graaf::vertex_ids_t& /*edge_id*/, + const my_edge& edge) -> std::string { + const auto style{edge.priority == edge_priority::HIGH ? "solid" : "dashed"}; + return fmt::format("label=\"{}\", style={}, color=gray, fontcolor=gray", edge.weight, style); }}; ``` @@ -77,4 +79,6 @@ dot -Tpng ./my_graph.dot -o my_graph.png Alternatively, you can use [graphviz online](https://dreampuf.github.io/GraphvizOnline/#digraph%20G%20%7B%0A%0A%20%20subgraph%20cluster_0%20%7B%0A%20%20%20%20style%3Dfilled%3B%0A%20%20%20%20color%3Dlightgrey%3B%0A%20%20%20%20node%20%5Bstyle%3Dfilled%2Ccolor%3Dwhite%5D%3B%0A%20%20%20%20a0%20-%3E%20a1%20-%3E%20a2%20-%3E%20a3%3B%0A%20%20%20%20label%20%3D%20%22process%20%231%22%3B%0A%20%20%7D%0A%0A%20%20subgraph%20cluster_1%20%7B%0A%20%20%20%20node%20%5Bstyle%3Dfilled%5D%3B%0A%20%20%20%20b0%20-%3E%20b1%20-%3E%20b2%20-%3E%20b3%3B%0A%20%20%20%20label%20%3D%20%22process%20%232%22%3B%0A%20%20%20%20color%3Dblue%0A%20%20%7D%0A%20%20start%20-%3E%20a0%3B%0A%20%20start%20-%3E%20b0%3B%0A%20%20a1%20-%3E%20b3%3B%0A%20%20b2%20-%3E%20a3%3B%0A%20%20a3%20-%3E%20a0%3B%0A%20%20a3%20-%3E%20end%3B%0A%20%20b3%20-%3E%20end%3B%0A%0A%20%20start%20%5Bshape%3DMdiamond%5D%3B%0A%20%20end%20%5Bshape%3DMsquare%5D%3B%0A%7D) for easy visualization: -![my_graph](./graph.png) \ No newline at end of file +

+ +

\ No newline at end of file diff --git a/examples/dot_serialization/graph.png b/examples/dot_serialization/graph.png index a1377742..78c1761b 100644 Binary files a/examples/dot_serialization/graph.png and b/examples/dot_serialization/graph.png differ diff --git a/examples/dot_serialization/main.cpp b/examples/dot_serialization/main.cpp index 6b4f45ae..e5680971 100644 --- a/examples/dot_serialization/main.cpp +++ b/examples/dot_serialization/main.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include @@ -49,17 +50,20 @@ auto create_graph() { int main() { const auto my_graph{create_graph()}; - const auto vertex_writer{[](const my_vertex& vertex) -> std::string { + const auto vertex_writer{[](graaf::vertex_id_t vertex_id, + const my_vertex& vertex) -> std::string { const auto color{vertex.number <= 25 ? "lightcyan" : "mediumspringgreen"}; - return fmt::format("label=\"{}\", fillcolor={}, style=filled", vertex.name, - color); + return fmt::format("label=\"{}: {}\", fillcolor={}, style=filled", + vertex_id, vertex.name, color); }}; - const auto edge_writer{[](const my_edge& edge) -> std::string { + const auto edge_writer{[](const graaf::vertex_ids_t& /*edge_id*/, + const my_edge& edge) -> std::string { const auto style{edge.priority == edge_priority::HIGH ? "solid" : "dashed"}; - return fmt::format("label=\"{}\", style={}", edge.weight, style); + return fmt::format("label=\"{}\", style={}, color=gray, fontcolor=gray", + edge.weight, style); }}; - const std::filesystem::path dof_file_path{"./my_graph.dot"}; + const std::filesystem::path dof_file_path{"./dot_example.dot"}; graaf::io::to_dot(my_graph, dof_file_path, vertex_writer, edge_writer); } \ No newline at end of file diff --git a/examples/shortest_path/README.md b/examples/shortest_path/README.md new file mode 100644 index 00000000..0ed58cdb --- /dev/null +++ b/examples/shortest_path/README.md @@ -0,0 +1,63 @@ +# Shortest Path Example +The shortest path algorithm implemented in `graaf::algorithm::get_shortest_path` can be used to compute the shortest path between any two vertices in a graph. + +Consider the following graph: + +

+ +

+ +In order to compute the shortest path between *vertex 0* and *vertex 2*, we call: + +```c++ +const auto maybe_shortest_path{get_shortest_path(graph, start, target)}; + +// Assert that we found a path at all +assert(maybe_shortest_path.has_value()); +auto shortest_path{maybe_shortest_path.value()}; +``` + +## Visualizing the shortest path +If we want to visualize the shortest path on the graph, we can create our own vertex and edge writers. These writers then determine the vertex and edge attributes based on whether the vertex or edge is contained in the shortest path. + +First, we create a datastructure of all edges on the shortest path such that we can query it in the edge writer: + +```c++ +// We use a set here for O(1) time contains checks +std::unordered_set edges_on_shortest_path{}; + +// Convert the list of vertices on the shortest path to edges +graaf::vertex_id_t prev{shortest_path.vertices.front()}; +shortest_path.vertices.pop_front(); +for (const auto current : shortest_path.vertices) { + edges_on_shortest_path.insert(std::make_pair(prev, current)); + prev = current; +} +``` + +Now we can specify our custom writers: + +```c++ +const auto vertex_writer{ + [start, target](graaf::vertex_id_t vertex_id, int vertex) -> std::string { + if (vertex_id == start) { + return "label=start"; + } else if (vertex_id == target) { + return "label=target"; + } + return "label=\"\""; +}}; + +const auto edge_writer{ +[&edges_on_shortest_path](const graaf::vertex_ids_t& edge_id, int edge) -> std::string { + if (edges_on_shortest_path.contains(edge_id)) { + return "label=\"\", color=red"; + } + return "label=\"\", color=gray, style=dashed"; +}}; +``` +This yields us the following visualization: + +

+ +

\ No newline at end of file diff --git a/examples/shortest_path/graph.png b/examples/shortest_path/graph.png new file mode 100644 index 00000000..bb902b59 Binary files /dev/null and b/examples/shortest_path/graph.png differ diff --git a/examples/shortest_path/main.cpp b/examples/shortest_path/main.cpp new file mode 100644 index 00000000..7889ca58 --- /dev/null +++ b/examples/shortest_path/main.cpp @@ -0,0 +1,78 @@ +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +struct graph_with_start_and_target { + graaf::directed_graph graph{}; + graaf::vertex_id_t start{}; + graaf::vertex_id_t target{}; +}; + +graph_with_start_and_target create_graph_with_start_and_target() { + graaf::directed_graph graph{}; + + const auto vertex_1{graph.add_vertex(10)}; + const auto vertex_2{graph.add_vertex(20)}; + const auto vertex_3{graph.add_vertex(30)}; + const auto vertex_4{graph.add_vertex(40)}; + const auto vertex_5{graph.add_vertex(50)}; + const auto vertex_6{graph.add_vertex(60)}; + + graph.add_edge(vertex_1, vertex_2, 100); + graph.add_edge(vertex_3, vertex_2, 200); + graph.add_edge(vertex_3, vertex_5, 300); + graph.add_edge(vertex_2, vertex_4, 400); + graph.add_edge(vertex_4, vertex_3, 500); + graph.add_edge(vertex_4, vertex_6, 600); + graph.add_edge(vertex_6, vertex_3, 700); + + return {graph, vertex_1, vertex_3}; +} + +int main() { + const auto [graph, start, target]{create_graph_with_start_and_target()}; + + const auto maybe_shortest_path{graaf::algorithm::get_shortest_path< + graaf::algorithm::edge_strategy::UNWEIGHTED>(graph, start, target)}; + assert(maybe_shortest_path.has_value()); + auto shortest_path{maybe_shortest_path.value()}; + + std::unordered_set + edges_on_shortest_path{}; + + graaf::vertex_id_t prev{shortest_path.vertices.front()}; + shortest_path.vertices.pop_front(); + for (const auto current : shortest_path.vertices) { + edges_on_shortest_path.insert(std::make_pair(prev, current)); + prev = current; + } + + const auto vertex_writer{ + [start, target](graaf::vertex_id_t vertex_id, int vertex) -> std::string { + if (vertex_id == start) { + return "label=start, fillcolor=white, style=filled"; + } else if (vertex_id == target) { + return "label=target, fillcolor=white, style=filled"; + } + return "label=\"\", color=gray, fillcolor=white, style=filled"; + }}; + + const auto edge_writer{ + [&edges_on_shortest_path](const graaf::vertex_ids_t& edge_id, + int edge) -> std::string { + if (edges_on_shortest_path.contains(edge_id)) { + return "label=\"\", color=red"; + } + return "label=\"\", color=gray, style=dashed"; + }}; + + const std::filesystem::path output{"shortest_path.dot"}; + graaf::io::to_dot(graph, output, vertex_writer, edge_writer); +} \ No newline at end of file diff --git a/examples/shortest_path/shortest_path.png b/examples/shortest_path/shortest_path.png new file mode 100644 index 00000000..69436068 Binary files /dev/null and b/examples/shortest_path/shortest_path.png differ diff --git a/src/graaflib/io/dot.h b/src/graaflib/io/dot.h index b94fa522..23011848 100644 --- a/src/graaflib/io/dot.h +++ b/src/graaflib/io/dot.h @@ -16,15 +16,17 @@ concept string_convertible = requires(T element) { std::to_string(element); }; template requires string_convertible -const auto default_vertex_writer{[](const T& vertex) -> std::string { - return fmt::format("label=\"{}\"", std::to_string(vertex)); -}}; +const auto default_vertex_writer{ + [](vertex_id_t vertex_id, const T& vertex) -> std::string { + return fmt::format("label=\"{}: {}\"", vertex_id, std::to_string(vertex)); + }}; template requires string_convertible -const auto default_edge_writer{[](const T& edge) -> std::string { - return fmt::format("label=\"{}\"", std::to_string(edge)); -}}; +const auto default_edge_writer{ + [](const vertex_ids_t& /*edge_id*/, const T& edge) -> std::string { + return fmt::format("label=\"{}\"", std::to_string(edge)); + }}; } // namespace detail /** @@ -34,19 +36,20 @@ const auto default_edge_writer{[](const T& edge) -> std::string { * @tparam E The edge type of the graph. * @param graph The graph we want to serialize. * @param vertex_writer Function used for serializing the vertices. Should - * accept a type V and serialize it to a string. Default implementations are - * provided for primitive numeric types. - * @param edge_writer Function used for serializing the edges. Should accept a - * type E and serialize it to a string. Default implementations are provided - * for primitive numeric types. + * accept a vertex_id_t and a type V and serialize it to a string. Default + * implementations are provided for primitive numeric types. + * @param edge_writer Function used for serializing the edges. Should accept an + * edge_id_t and a type E and serialize it to a string. Default implementations + * are provided for primitive numeric types. * @param path Path to the output dot file. */ template ), typename EDGE_WRITER_T = decltype(detail::default_edge_writer)> requires std::is_invocable_r_v && - std::is_invocable_r_v + vertex_id_t, const V&> && + std::is_invocable_r_v void to_dot( const graph& graph, const std::filesystem::path& path, const VERTEX_WRITER_T& vertex_writer = detail::default_vertex_writer, diff --git a/src/graaflib/io/dot.tpp b/src/graaflib/io/dot.tpp index 718b535f..85bdf289 100644 --- a/src/graaflib/io/dot.tpp +++ b/src/graaflib/io/dot.tpp @@ -66,8 +66,9 @@ constexpr const char* spec_to_edge_specifier(const graph_spec& spec) { template requires std::is_invocable_r_v && - std::is_invocable_r_v + vertex_id_t, const V&> && + std::is_invocable_r_v void to_dot(const graph& graph, const std::filesystem::path& path, const VERTEX_WRITER_T& vertex_writer, const EDGE_WRITER_T& edge_writer) { @@ -79,14 +80,15 @@ void to_dot(const graph& graph, const std::filesystem::path& path, append_line(fmt::format("{} {{", detail::spec_to_string(S))); for (const auto& [vertex_id, vertex] : graph.get_vertices()) { - append_line(fmt::format("\t{} [{}];", vertex_id, vertex_writer(vertex))); + append_line( + fmt::format("\t{} [{}];", vertex_id, vertex_writer(vertex_id, vertex))); } const auto edge_specifier{detail::spec_to_edge_specifier(S)}; - for (const auto& [vertices, edge] : graph.get_edges()) { - const auto [source_id, target_id]{vertices}; + for (const auto& [edge_id, edge] : graph.get_edges()) { + const auto [source_id, target_id]{edge_id}; append_line(fmt::format("\t{} {} {} [{}];", source_id, edge_specifier, - target_id, edge_writer(edge))); + target_id, edge_writer(edge_id, edge))); } append_line("}"); diff --git a/test/graaflib/io/dot_test.cpp b/test/graaflib/io/dot_test.cpp index 7ff1d414..8d728008 100644 --- a/test/graaflib/io/dot_test.cpp +++ b/test/graaflib/io/dot_test.cpp @@ -14,13 +14,15 @@ namespace graaf::io { namespace { -const auto int_vertex_writer{[](int vertex) -> std::string { - return fmt::format("label=\"{}\"", std::to_string(vertex)); -}}; +const auto int_vertex_writer{ + [](vertex_id_t /*vertex_id*/, int vertex) -> std::string { + return fmt::format("label=\"{}\"", std::to_string(vertex)); + }}; -const auto int_edge_writer{[](int edge) -> std::string { - return fmt::format("label=\"{}\"", std::to_string(edge)); -}}; +const auto int_edge_writer{ + [](const vertex_ids_t& /*edge_id*/, int edge) -> std::string { + return fmt::format("label=\"{}\"", std::to_string(edge)); + }}; template const auto make_vertex_string{ @@ -204,12 +206,14 @@ TEST(DotTest, UserProvidedVertexAndEdgeClass) { const auto vertex_2{graph.add_vertex({20, "vertex 2"})}; graph.add_edge(vertex_1, vertex_2, {100, "edge 1"}); - const auto vertex_writer{[](const vertex_t& vertex) { - return fmt::format("{}, {}", vertex.numeric_data, vertex.string_data); - }}; - const auto edge_writer{[](const edge_t& edge) { - return fmt::format("{}, {}", edge.numeric_data, edge.string_data); - }}; + const auto vertex_writer{ + [](vertex_id_t /*vertex_id*/, const vertex_t& vertex) { + return fmt::format("{}, {}", vertex.numeric_data, vertex.string_data); + }}; + const auto edge_writer{ + [](const vertex_ids_t& /*edge_id*/, const edge_t& edge) { + return fmt::format("{}, {}", edge.numeric_data, edge.string_data); + }}; // WHEN to_dot(graph, path, vertex_writer, edge_writer); @@ -238,10 +242,10 @@ TEST(DotTest, DefaultWriters) { // THEN const auto dot_content{read_to_string(path)}; - ASSERT_TRUE(dot_content.find(fmt::format("{} [label=\"10\"];", vertex_1)) != - std::string::npos); - ASSERT_TRUE(dot_content.find(fmt::format("{} [label=\"20\"];", vertex_2)) != - std::string::npos); + ASSERT_TRUE(dot_content.find(fmt::format("{} [label=\"{}: 10\"];", vertex_1, + vertex_1)) != std::string::npos); + ASSERT_TRUE(dot_content.find(fmt::format("{} [label=\"{}: 20\"];", vertex_2, + vertex_2)) != std::string::npos); // For the float value we only check up until the first decimal place ASSERT_TRUE(dot_content.find(fmt::format("{} -> {} [label=\"3.3", vertex_1, vertex_2)) != std::string::npos);