Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed doc/Identifiers.docx
Binary file not shown.
88 changes: 88 additions & 0 deletions doc/algorithms.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Algorithms

## Definitions

A graph path _p_ is a possibly empty sequence of graph edges (_e_<sub>0</sub>, _e_<sub>1</sub>, ..., _e_<sub>_N_</sub>) where:
* _e_<sub>_i_</sub> ≠ _e_<sub>_j_</sub> for _i_ ≠ _j_,
* target(_e_<sub>_i_</sub>) = source(_e_<sub>_i_+1</sub>),
* source(_e_<sub>_i_</sub>) != source(_e_<sub>_j_</sub>) for _i_ ≠ _j_.

<code><i>path-source</i>(<i>p</i>)</code> = source(_e_<sub>0</sub>). <code><i>path-target</i>(<i>p</i>)</code> = target(_e_<sub>_N_</sub>).

<code><i>distance(p)</i></code> is a sum over _i_ of <code>weight</code>(_e_<sub>_i_</sub>).

<code><i>shortest-path</i>(g, u, v)</code> is a path in the set of all paths `p` in graph `g` with <code><i>path-source</i>(<i>p</i>)</code> = `u`
and <code><i>path-target</i>(<i>p</i>)</code> = v that has the smallest value of <code><i>distance(p)</i></code>.

<code><i>shortest-path-distance</i>(g, u, v)</code> is <code><i>distance</i>(<i>shortest-path</i>(g, u, v))</code> if it exists and _infinite-distance_ otherwise.

<code><i>shortest-path-predecessor</i>(g, u, v)</code>, in the set of all shortest paths <code><i>shortest-path</i>(g, u, v)</code> for any `v`:
* if there exists an edge _e_ with target(_e_) = v, then it is source(_e_),
* otherwise it is `v`.

## Visitors

TODO: explain the _GraphVisitor_ requirements

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some points that might useful to people.

  1. No runtime overhead occurs if the visitor function isn't defined on the visitor class.
  2. The visitor functions are the same used by boost::graph.
  3. Each algorithm defines the visitor functions they support. Additional functions included in the visitor that aren't supported are ignored.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied #1 and #3. Not sure about #2. First, strictly speaking this cannot be true. The Boost.Graph names are without the on_ prefix. Second, I wouldn't like to give an impression that one needs to know Boost.Graph to understand this library.

TODO: list and describe all possible visitation events


## `dijkstra_shortest_paths` (single source)

Header `<graph/algorithm/dijkstra_shortest_paths.hpp>`

```c++
template <class G,
class Distances,
class Predecessors,
class WF = function<range_value_t<Distances>(edge_reference_t<G>)>,
class Visitor = empty_visitor,
class Compare = less<range_value_t<Distances>>,
class Combine = plus<range_value_t<Distances>>>
constexpr void dijkstra_shortest_distances(
G&& g,
vertex_id_t<G> source,
Distances& distances,
Predecessors& predecessor,
WF&& weight = [](edge_reference_t<G> uv) { return range_value_t<Distances>(1); }, // default weight(uv) -> 1
Visitor&& visitor = empty_visitor(),
Compare&& compare = less<range_value_t<Distances>>(),
Combine&& combine = plus<range_value_t<Distances>>());
```
*Constraints:*
* `index_adjacency_list<G>` is `true`,
* `std::ranges::random_access_range<Distances>` is `true`,
* `std::ranges::sized_range<Distances>` is `true`,
* `std::ranges::random_access_range<Predecessors>` is `true`,
* `std::ranges::sized_range<Predecessors>` is `true`,
* `std::is_arithmetic_v<std::ranges::range_value_t<Distances>>` is `true`,
* `std::convertible_to<vertex_id_t<G>, std::ranges::range_value_t<Predecessors>>` is `true`,
* `basic_edge_weight_function<G, WF, std::ranges::range_value_t<Distances>, Compare, Combine>` is `true`.

*Preconditions:*
* <code>distances[<i>i</i>] == shortest_path_infinite_distance&lt;range_value_t&lt;Distances&gt;&gt;()</code> for each <code><i>i</i></code> in range [`0`; `num_vertices(g)`),
* <code>predecessor[<i>i</i>] == <i>i</i></code> for each <code><i>i</i></code> in range [`0`; `num_vertices(g)`),
* `weight` returns non-negative values.
* `visitor` adheres to the _GraphVisitor_ requirements.

*Hardened preconditions:*
* `0 <= source && source < num_vertices(g)` is `true`,
* `std::size(distances) >= num_vertices(g)` is `true`,
* `std::size(predecessor) >= num_vertices(g)` is `true`.

*Effects:* Supports the following visitation events: `on_initialize_vertex`, `on_discover_vertex`,
`on_examine_vertex`, `on_finish_vertex`, `on_examine_edge`, `on_edge_relaxed`, and `on_edge_not_relaxed`.

*Postconditions:* For each <code><i>i</i></code> in range [`0`; `num_vertices(g)`):
* <code>distances[<i>i</i>]</code> is <code><i>shortest-path-distance</i>(g, source, <i>i</i>)</code>,
* <code>predecessor[<i>i</i>]</code> is <code><i>shortest-path-predecessor</i>(g, source, <i>i</i>)</code>.

*Throws:* `std::bad_alloc` if memory for the internal data structures cannot be allocated.

*Complexity:* Either 𝒪((|_E_| + |_V_|)⋅log |_V_|) or 𝒪(|_E_| + |_V_|⋅log |_V_|), depending on the implementation.

*Remarks:* Duplicate sources do not affect the algorithm’s complexity or correctness.

## TODO

Document all other algorithms...
111 changes: 111 additions & 0 deletions doc/customization_points.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Customization Points

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find it interesting that you label this as Customization Points.

In contrast, I've labeled it as the Graph Container Interface (GCI) because I want the user to focus on the fact that all the free functions are used to describe the interface for a Graph Container. I use "Container" to associate & distinguish it from the standard STL Containers. It is a range-of-ranges container with unique properties.

To me, Customization Points are the means to achieve the functionality I want to be able to support, namely the ability to define and override the functions for a specific graph data structure.

Toward that end, I will describe the functions in the GCI and state that they are Customization Points to help those in the know what's going on, but after that I can just tell people to define the function for their graph data structure and it all just works. If I try to go beyond that then I risk confusing the uninitiated to CPOs.

Beyond that, I also say there are reasonable defaults for all the GCI functions and give examples like vector<vector> and vector<vector<pair<int, double>>> that works by default. There are likely gaps in what I've done. It hasn't been scrutinized very much.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. I will try to separate the docs clearly into the narrative part and the reference section, and use the term Graph Container Interface in the former.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I expanded the docs structure. This should address most of your remarks.

The algorithms and views in this library operate on graph representations via _Customization Point Objects_ (CPO).
A user-defined graph representation `G` is adapted for use with this library by making sure that the necessary CPOs are _valid_ for `G`.

Each customization point specifies individually what it takes to make it valid.
A customization point can be made valid in a number of ways.
For each customization point we provide an ordered list of ways in which it can be made valid. The order in this list matters: the match for validity is performed in order and if a given customization is determined to be valid, the subsequent ways, even if they would be valid, are ignored.

Often, the last item from the list serves the purpose of a "fallback" or "default" customization.

If none of the customization ways is valid for a given type, or set of types, the customization point is considered _invalid_ for this set of types.
The property or being valid or invalid can be statically tested in the program via SFINAE (like `enable_if`) tricks or `requires`-expressions.

All the customization points in this library are defined in namespace `::graph` and brought into the program code via including header `<graph/graph.hpp>`.

## The list of customization points

We use the following notation to represent the customization points:


| Symbol | Type | Meaning |
|--------|--------------------------------|------------------------------------------|
| `G` | | the type of the graph representation |
| `g` | `G` | the graph representation |
| `u` | `graph::vertex_reference_t<G>` | vertex in `g` |
| `ui` | `graph::vertex_iterator_t<G>` | iterator to a vertex in `g` |
| `uid` | `graph::vertex_id_t<G>` | _id_ of a vertex in `g` (often an index) |
| `uv` | `graph::edge_reference_t<G>` | an edge in `g` |


### `vertices`

The CPO `vertices(g)` is used to obtain the list of all vertices, in form of a `std::ranges::random_access_range`, from the graph-representing object `g`.
We also use its return type to determine the type of the vertex: `vertex_t<G>`.

#### Customization

1. Returns `g.vertices()`, if such member function exists and returns a `std::move_constructible` type.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move_constructible implies that ownership of the internal vertices would be moved to the caller, which is not what we want. It should be returning a random_access_range (or future, bidirectional_range) either as a reference or to something like a subrange that is returned as a value (not a reference).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

struct X { X(X&&) = delete;};
static_assert(std::move_constructible<X&>);

Returning a reference satisfies the std::move_constructible requirements.

2. Returns `vertices(g)`, if such function is ADL-discoverable and returns a `std::move_constructible` type.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as previous.

3. Returns `g`, if it is a `std::ranges::random_access_range`.


### `vertex_id`

The CPO `vertex_id(g, ui)` is used obtain the _id_ of the vertex, given the iterator.
We also use its return type to determine the type of the vertex id: `vertex_id_t<G>`.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine for now. With the addition of descriptors, this is the only function signature that changes. Once that's available, ui becomes u (descriptor).

#### Coustomization

1. Returns `ui->vertex_id(g)`, if this expression is valid and its type is `std::move_constructible`.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this will remain valid when descriptors are used. It means that vertex_id(g) would need to be defined on the descriptor, which may be possible.

2. Returns `vertex_id(g, ui)`, if this expression is valid and its type is `std::move_constructible`.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the return type is simple like an integral id then this is fine. If we want to extend the id type to be more than integral, like a user-defined type or a string, then we won't want to require move_constructible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that my goal is to document what is in the current library (modulo bugs), rather than a vision for the future.

After a bit of investigation, I conclude that the current Graph Container Interface requires the IDs to be copy_constructible!

Suppose that I have my own graph representation that needs to use non-copyable, non-movable IDs. How am I supposed to customize vertex_id?

ID const& vertex_id(Graph const& g, Graph::const_iterator it) { 
  return it->first;
}

Shall I return by value or by reference to const? If by value, then I need to copy. If by reference, then the CPO vertex_id will do the copying, because its return type is non-reference, and I am returning a reference to const, so even move will not work.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filed #174.

3. Returns <code>static_cast&lt;<em>vertex-id-t</em>&lt;G&gt;&gt;(ui - begin(vertices(g)))</code>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vertex_id_(g,u) is used to define vertex_id_t, so there may be a circular definition here (I hope that's not because of the current definition in graph-v2).

I think this needs to be flushed out a little more to define the vertex type based on vertex_t and whether it is a range or not (for #1 & #2 below).

if `std::ranges::random_access_range<vertex_range_t<G>>` is `true`, where <code><em>vertex-id-t</em></code> is defined as:

* `I`, when the type of `G` matches pattern `ranges::forward_list<ranges::forward_list<I>>` and `I` is `std::integral`,
* `I0`, when the type of `G` matches pattern <code>ranges::forward_list&lt;ranges::forward_list&lt;<em>tuple-like</em>&lt;I0, ...&gt;&gt;&gt;</code> and `I0` is `std::integral`,
* `std::size_t` otherwise.

### `find_vertex`

TODO `find_vertex(g, uid)`

### `edges(g, u)`

### `edges(g, uid)`

### `num_edges(g)`

### `target_id(g, uv)`

### `target_id(e)`

### `source_id(g, uv)`

### `source_id(e)`

### `target(g, uv)`

### `source(g, uv)`

### `find_vertex_edge(g, u, vid)`

### `find_vertex_edge(g, uid, vid)`

### `contains_edge(g, uid, vid)`

### `partition_id(g, u)`

### `partition_id(g, uid)`

### `num_vertices(g, pid)`

### `num_vertices(g)`

### `degree(g, u)`

### `degree(g, uid)`

### `vertex_value(g, u)`

### `edge_value(g, uv)`

### `edge_value(e)`

### `graph_value(g)`

### `num_partitions(g)`

### `has_edge(g)`

192 changes: 192 additions & 0 deletions doc/overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
# Overview

What this library has to offer:

1. A number of ready-to-use algoritms, like Dijkstra's shortest paths computation.
2. Make it easier for you to write your own graph algorithms: we provide a _view_ for traversing your graph in the preferred order (depth-first, breadth-first, topological), and you specify what is processed in each traversal step.
3. Customization of your graph representation, so that it can be used with our algorithms and views.
4. Our own container for representing a graph.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's my take on the intro statement. Take it for what it's worth.
This is a high-performance, general-purpose and extensible graph library that is designed to provide useful functionality to get started, with the flexibility to create your own algorithms and adapt to non-standard graph data structures, such as those found in embedded implementations in solutions.

It offers the following features:

  1. A set of commonly used algorithms to get you started.
  2. Views to traverse a graph in common ways, making it easier to write your own algorithms. The views include incidence, neighbor, edgelist, depth-first search, breadth-first search and topological sort.
  3. A couple of useful graph data structures to get you started, one a high-performance, read-only data structure and another for general-purpose use that allow you to define the containers used for vertices and edges.
  4. Extensibility:
  5. a. You can easily write your own algorithms using the provided views and the core Graph Container Interface.
  6. b. You can use your own graph data structure by defining 7 or less functions to adapt it to the existing infrastructure. This adds little, or no, overhead when calling your functions.

## Concepts

This is a _generic_ library. Graph algorithms operate on various graph representations through a well defined interface: a concept. The primary concept in this library is `index_adjacency_list`.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

index_adjacency_list just specifies that there's an integral vertex id and vertices in a random-access container. adjacency_list is also important long-term, supporting the use of map to store vertices. I don't know if that's important to state right now or now.

```c++
#include <graph/graph.hpp> // (1)

static_assert(graph::index_adjacency_list<MyType>); // (2) (3)
```
Notes:
1. Graph concepts are defined in header `<graph/graph.hpp>`.
2. All declarations reside in namespace `::graph`.
3. Use concept `graph::index_adjacency_list` to test if your type satisfies the syntactic requirements of an index adjacency list.

The graph representation that is most commonly used in this library is [_adjacency list_](https://en.wikipedia.org/wiki/Adjacency_list).
Very conceptually, we call it a random access range (corresponding to vertices) of forward ranges (corresponding to outgoing edges of a vertex).

This representation allows the algorithms to:

1. Perform an iteration over vertices, and to count them.
2. For each vertex, perform an iteration over its outgoing edges.
3. For each edge to to look up its target vertex in constant time.

Algorithms in this library express the requirements on the adjacency list representations via concept `graph::index_adjacency_list`.
This concept expresses its syntactic requirments mostly via [_customization points_](./customization_points.md).
We use the following notation to represent the constraints:

| Symbol | Meaning |
|--------|---------------------------------------------------------|
| `G` | the type of the graph representation |
| `g` | lvalue or lvalue reference of type `G` |
| `u` | lvalue reference of type `graph::vertex_reference_t<G>` |
| `ui` | value of type `graph::vertex_iterator_t<G>` |
| `uid` | value of type `graph::vertex_id_t<G>` |
| `uv` | lvalue reference of type `graph::edge_reference_t<G>` |

### Random access to vertices

Customization point `graph::vertices(g)` must be valid and its return type must satisfy type-requirements `std::ranges::sized_range` and `std::ranges::random_access_range`.

The algorithms will use it to access the vertices of graph represented by `g` in form of a random-access range.

Customization point `graph::vertex_id(g, ui)` must be valid and its return type must satisfy type-requirements `std::integral`.
The algorithms will use this function to convert the iterator pointing to a vertex to the _id_ of the vertex.


### Forward access to target edges

The following customization points must be valid and their return type shall satisfy type requirements `std::ranges::forward_range`:

* `graph::edges(g, uid)`,
* `graph::edges(g, u)`.

The algorithms will use this function to iterate over out edges of the vertex represended by either `uid` or `u`.


### Linking from target edges back to vertices

Customization point `graph::target_id(g, uv)` must be valid and its return type must satisfy type-requirements `std::integral`.

The algorithms will use this value to access a vertex in `graph::vertices(g)`.
Therefore we have a _semantic_ constraint: that the look up of the value returned from `graph::target_id(g, uv)` returns value `uid` that satisfies the condition
`0 <= uid && uid < graph::num_vertices(g)`.


### Associated types

Based on the customization points the library provides a number of associated types in namespace `graph`:

| Associated type | Definition |
|-----------------------------|----------------------------------------------------------|
| `vertex_range_t<G>` | `decltype(graph::vertices(g))` |
| `vertex_iterator_t<G>` | `std::ranges::iterator_t<vertex_range_t<G>>` |
| `vertex_t<G>` | `std::ranges::range_value_t<vertex_range_t<G>>` |
| `vertex_reference_t<G>` | `std::ranges::range_reference_t<vertex_range_t<G>>` |
| `vertex_id_t<G>` | `decltype(graph::vertex_id(g, ui))` |
| `vertex_edge_range_t<G>` | `decltype(graph::edges(g, u))` |
| `vertex_edge_iterator_t<G>` | `std::ranges::iterator_t<vertex_edge_range_t<G>>` |
| `edge_t<G>` | `std::ranges::range_value_t<vertex_edge_range_t<G>>` |
| `edge_reference_t<G>` | `std::ranges::range_reference_t<vertex_edge_range_t<G>>` |

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a number of missing types here. This document is a mix of P3126 Overview, P3130 Graph Container Interface and P3131 Graph Containers. It's not clear what your goal is here. I think you either need to include all the types and definitions, or consider breaking things out into separate documents.


## Views

This library can help you write your own algorithms via _views_ that represent graph traversal patterns as C++ ranges.

Suppose, your task is to compute the distance, in edges, from a given vertex _u_ in your graph _g_, to all vertices in _g_.
We will represent the adjacency list as a vector of vectors:


```c++
std::vector<std::vector<int>> g { //
/*0*/ {1, 3}, //
/*1*/ {0, 2, 4}, // (0) ----- (1) ----- (2)
/*2*/ {1, 5}, // | | |
/*3*/ {0, 4}, // | | |
/*4*/ {1, 3}, // | | |
/*5*/ {2, 6}, // (3) ----- (4) (5) ----- (6)
/*6*/ {5} //
}; //
```

The algorithm is simple: you start by assigning value zero to the start vertex,
then go through all the graph edges in the breadth-first order and for each edge (_u_, _v_)
you will assign the value for _v_ as the value for _u_ plus 1.

In order to do this, you can employ a depth-first search view from this library:

```c++
#include <graph/views/breadth_first_search.hpp>

int main()
{
std::vector<int> distances(g.size(), 0); // fill with zeros

for (auto const& [uid, vid, _] : graph::views::sourced_edges_breadth_first_search(g, 0))
distances[vid] = distances[uid] + 1;

assert((distances == std::vector{0, 1, 2, 1, 2, 3, 4}));
}
```

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should you be introducing views with BFS? graph-savvy people will understand it, but it might be a bit of a stretch for newbies. Would incidence and neighbors be a better choice?

## Algorithms

This library offers also full fledged algorithms.
However, due to the nature of graph algorithms, the way they communicate the results requires some additional code.
Let's, reuse the same graph topology, but use a different adjacency-list representation that is also able to store a weight for each edge:

```c++
std::vector<std::vector<std::tuple<int, double>>> g {
/*0*/ {{(1), 9.1}, {(3), 1.1} }, // 9.1 2.2
/*1*/ {{(0), 9.1}, {(2), 2.2}, {(4), 3.5}}, // (0)-------(1)-------(2)
/*2*/ {{(1), 2.2}, {(5), 1.0} }, // | | |
/*3*/ {{(0), 1.1}, {(4), 2.0} }, // |1.1 |3.5 |1.0
/*4*/ {{(1), 3.5}, {(3), 2.0} }, // | 2.0 | | 0.5
/*5*/ {{(2), 1.0}, {(6), 0.5} }, // (3)-------(4) (5)-------(6)
/*6*/ {{(5), 0.5}} //
};
```

Now, let's use Dijkstra's Shortest Paths algorithm to determine the distance from vertex `0` to each vertex in `g`, and also to determine these paths. The pathst are not obtained directly, but instead the list of predecessors is returned for each vertex:

```c++
auto weight = [](std::tuple<int, double> const& uv) {
return std::get<1>(uv);
};

std::vector<int> predecessors(g.size()); // we will store the predecessor of each vertex here

std::vector<double> distances(g.size()); // we will store the distance to each vertex here
graph::init_shortest_paths(distances); // fill with `infinity`

graph::dijkstra_shortest_paths(g, 0, distances, predecessors, weight); // from vertex 0

assert((distances == std::vector{0.0, 6.6, 8.8, 1.1, 3.1, 9.8, 10.3}));
assert((predecessors == std::vector{0, 4, 1, 0, 3, 2, 5}));
```

If you need to know the sequence of vertices in the path from, say, `0` to `5`, you have to compute it yourself:

```c++
auto path = [&predecessors](int from, int to)
{
std::vector<int> result;
for (; to != from; to = predecessors[to]) {
assert(to < predecessors.size());
result.push_back(to);
}
std::reverse(result.begin(), result.end());
return result;
};

assert((path(0, 5) == std::vector{3, 4, 1, 2, 5}));
```
The algorithms in this library are described in section [Algorithms](./algorithms.md).


------

TODO:

- Adapting
- use our container
Loading