Skip to content

Commit

Permalink
docs: Shortest path example (#25)
Browse files Browse the repository at this point in the history
* Pass vertex and edge id to writers
* Added shortest path example
* Link to example section in readme
  • Loading branch information
bobluppes committed May 29, 2023
1 parent 3f8d83d commit 9317eaa
Show file tree
Hide file tree
Showing 12 changed files with 215 additions and 50 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ const auto print_vertex_callback{[](const auto vertex) {
traverse<search_strategy::BFS>(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
Expand Down
6 changes: 6 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -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)
20 changes: 12 additions & 8 deletions examples/dot_serialization/README.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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);
}};
```

Expand Down Expand Up @@ -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)
<p align="center">
<img src="./graph.png">
</p>
Binary file modified examples/dot_serialization/graph.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 10 additions & 6 deletions examples/dot_serialization/main.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include <fmt/core.h>
#include <graaflib/directed_graph.h>
#include <graaflib/io/dot.h>
#include <graaflib/types.h>

#include <filesystem>
#include <string>
Expand Down Expand Up @@ -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);
}
63 changes: 63 additions & 0 deletions examples/shortest_path/README.md
Original file line number Diff line number Diff line change
@@ -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:

<p align="center">
<img src="./graph.png">
</p>

In order to compute the shortest path between *vertex 0* and *vertex 2*, we call:

```c++
const auto maybe_shortest_path{get_shortest_path<edge_strategy::UNWEIGHTED>(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<graaf::vertex_ids_t, graaf::vertex_ids_hash> 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:

<p align="center">
<img src="./shortest_path.png">
</p>
Binary file added examples/shortest_path/graph.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
78 changes: 78 additions & 0 deletions examples/shortest_path/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#include <fmt/core.h>
#include <graaflib/algorithm/shortest_path.h>
#include <graaflib/directed_graph.h>
#include <graaflib/io/dot.h>
#include <graaflib/types.h>

#include <cassert>
#include <filesystem>
#include <string>
#include <unordered_set>

struct graph_with_start_and_target {
graaf::directed_graph<int, int> 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<int, int> 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<graaf::vertex_ids_t, graaf::vertex_ids_hash>
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);
}
Binary file added examples/shortest_path/shortest_path.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
29 changes: 16 additions & 13 deletions src/graaflib/io/dot.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,17 @@ concept string_convertible = requires(T element) { std::to_string(element); };

template <typename T>
requires string_convertible<T>
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 <typename T>
requires string_convertible<T>
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

/**
Expand All @@ -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 V, typename E, graph_spec S,
typename VERTEX_WRITER_T = decltype(detail::default_vertex_writer<V>),
typename EDGE_WRITER_T = decltype(detail::default_edge_writer<E>)>
requires std::is_invocable_r_v<std::string, const VERTEX_WRITER_T&,
const V&> &&
std::is_invocable_r_v<std::string, const EDGE_WRITER_T&, const E&>
vertex_id_t, const V&> &&
std::is_invocable_r_v<std::string, const EDGE_WRITER_T&,
const graaf::vertex_ids_t&, const E&>
void to_dot(
const graph<V, E, S>& graph, const std::filesystem::path& path,
const VERTEX_WRITER_T& vertex_writer = detail::default_vertex_writer<V>,
Expand Down
14 changes: 8 additions & 6 deletions src/graaflib/io/dot.tpp
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,9 @@ constexpr const char* spec_to_edge_specifier(const graph_spec& spec) {
template <typename V, typename E, graph_spec S, typename VERTEX_WRITER_T,
typename EDGE_WRITER_T>
requires std::is_invocable_r_v<std::string, const VERTEX_WRITER_T&,
const V&> &&
std::is_invocable_r_v<std::string, const EDGE_WRITER_T&, const E&>
vertex_id_t, const V&> &&
std::is_invocable_r_v<std::string, const EDGE_WRITER_T&,
const graaf::vertex_ids_t&, const E&>
void to_dot(const graph<V, E, S>& graph, const std::filesystem::path& path,
const VERTEX_WRITER_T& vertex_writer,
const EDGE_WRITER_T& edge_writer) {
Expand All @@ -79,14 +80,15 @@ void to_dot(const graph<V, E, S>& 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("}");
Expand Down
36 changes: 20 additions & 16 deletions test/graaflib/io/dot_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <typename T>
const auto make_vertex_string{
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 9317eaa

Please sign in to comment.