Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a29542f
docs: add Graph I/O and generators entries to CHANGELOG
Apr 30, 2026
dd3b329
feat: add AdaptingThirdPartyGraph example and extend num_vertices CPO
Apr 30, 2026
5ae6911
examples: add CppCon2021 examples refactored from graph-v2
Apr 30, 2026
b39506d
examples: add CppCon2022 Germany routes example refactored from graph-v2
Apr 30, 2026
b9b6a7e
docs: add examples page and CHANGELOG entries for new examples
Apr 30, 2026
5f43258
agents: move bgl_graph_adapt docs to archive
Apr 30, 2026
a9fa876
Add BGLWorkshop2026 outline draft
Apr 30, 2026
93c1f23
Add BGLWorkshop2026 bacon example with bipartite movie-actor graph
pratzl May 1, 2026
c933048
Generalise search views to support non-integral vertex ids
pratzl May 2, 2026
80a4c02
Add multi-type analysis doc; implement bacon3() with variant as VId, …
pratzl May 2, 2026
dd0534a
Make source_id/target_id return const ref for non-integral VId types
pratzl May 2, 2026
e4c6a68
bacon examples: namespaces, eval(), write_dot improvements, output dir
pratzl May 2, 2026
b523b8c
Add france_routes example and find_vertex_edge ADL friend
pratzl May 2, 2026
b8ae93c
examples: convert bacon2::dot() to undirected DOT output
pratzl May 2, 2026
db3e46e
Fix warnings; refactor france_routes; housekeeping
pratzl May 3, 2026
00f6250
Add BGL comparison examples and concept diagrams for BGLWorkshop2026
pratzl May 3, 2026
f3f5764
docs: add concept hierarchy diagrams and fix adjacency list concept h…
pratzl May 3, 2026
2f31119
docs: update status docs and fix rr_adaptor GCC 15 build
pratzl May 3, 2026
8cc767f
Fix PR errors in clang build
pratzl May 3, 2026
e0827dd
Add missing <array> in breadth_first_search.hpp
pratzl May 3, 2026
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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Build directories
# Example output files
examples/**/output/*
!examples/**/output/.gitkeep
build/
build*/
cmake-build-*/
Expand Down
12 changes: 12 additions & 0 deletions AdaptingThirdPartyGraph/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# example/AdaptingThirdPartyGraph/CMakeLists.txt

set(EXAMPLE_OUTPUT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/output/")
add_compile_definitions(EXAMPLE_OUTPUT_DIR="${EXAMPLE_OUTPUT_DIR}")

#add_library(catch_main STATIC catch_main.cpp)
#target_link_libraries(catch_main PUBLIC Catch2::Catch2)
#target_link_libraries(catch_main PRIVATE project_options)
#target_link_options(catch_main INTERFACE $<$<CXX_COMPILER_ID:GNU>:-pthread -fconcepts-diagnostics-depth=1>)

add_executable(AdaptingThirdPartyGraph "adapting_a_third_party_graph.cpp")
target_link_libraries(AdaptingThirdPartyGraph PRIVATE project_warnings project_options catch_main Catch2::Catch2WithMain graph)
83 changes: 83 additions & 0 deletions AdaptingThirdPartyGraph/adapting_a_third_party_graph.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright (C) 2025 Andrzej Krzemienski.
//
// Use, modification, and distribution is subject to the Boost Software
// License, Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at
// http://www.boost.org/LICENSE_1_0.txt)
//
// This test file demonstrates how one can adapt one's own graph container for use with
// this Graph Library

#include <graph/views/depth_first_search.hpp>
#include <graph/graph.hpp>
#include <string>
#include <vector>
#include <iostream>


namespace MyLibrary { // custom graph container, conceptually an adjacency list

struct MyEdge {
std::string content;
int indexOfTarget;
};

struct MyVertex {
std::string content;
std::vector<MyEdge> outEdges;
};

class MyGraph {
std::vector<MyVertex> _vertices;

public:
MyVertex const* getVertexByIndex(int index) const { return &_vertices[static_cast<unsigned>(index)]; }

std::vector<MyVertex> const& getAllVertices() const // !! one of customization points
{
return _vertices;
} // forced me to add this fun

void setTopology(std::vector<MyVertex> t) { _vertices = std::move(t); }
};

} // namespace MyLibrary

namespace MyLibrary { // customization for graph, unintrusive
// although forcing me to provide `vertices()` is superfluous

auto vertices(MyGraph const& g) { return std::views::all(g.getAllVertices()); } // for vertex_range_t

auto edges(MyGraph const&, const MyLibrary::MyVertex& v) { return std::views::all(v.outEdges); }

auto edges(MyGraph const& g, int i) { return edges(g, *g.getVertexByIndex(i)); }

int vertex_id(MyGraph const& g, std::vector<MyVertex>::const_iterator it) {
return static_cast<int>(std::distance(g.getAllVertices().begin(), it));
}

int target_id(MyGraph const&, MyEdge const& uv) { return uv.indexOfTarget; }

} // namespace MyLibrary


int main() {
static_assert(graph::adjacency_list<MyLibrary::MyGraph>);

const MyLibrary::MyGraph g = [] { // populate the graph
MyLibrary::MyGraph r;
std::vector<MyLibrary::MyVertex> topo{
// A |
/*0*/ {"A", {{"", 1}, {"", 2}}}, // / \ |
/*1*/ {"B", {{"", 3}}}, // B C |
/*2*/ {"C", {{"", 3}}}, // \ / |
/*3*/ {"D", {}} // D |
};
r.setTopology(std::move(topo));
return r;
}();

for (auto const& [vid, v] : graph::views::vertices_depth_first_search(g, 0))
std::cout << v.content << " ";

std::cout << std::endl;
}
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,33 @@
- `is_sparse_vertex_container_v<G>` trait for compile-time graph type dispatch
- Map-based graph test fixtures (`map_graph_fixtures.hpp`) with sparse vertex IDs
- 456 new algorithm tests for sparse graph types (4343 → 4799)
- **Graph I/O** (`<graph/io/>`) — read/write support for three formats:
- `write_dot(os, g)` / `write_dot(os, g, vertex_attr_fn, edge_attr_fn)` — DOT/GraphViz writer; auto-formats vertex/edge values when `std::formattable`, emits user-supplied attribute strings otherwise (`io/dot.hpp`)
- `read_dot(is) -> dot_graph` — DOT parser returning a lightweight `dot_graph` structure with string-keyed vertex/edge attributes (`io/dot.hpp`)
- `write_graphml(os, g)` / `write_graphml(os, g, vertex_properties_fn, edge_properties_fn)` — GraphML (XML) writer with optional property key declarations (`io/graphml.hpp`)
- `read_graphml(is) -> graphml_graph` — GraphML parser returning a `graphml_graph` structure with per-vertex/per-edge attribute maps (`io/graphml.hpp`)
- `write_json(os, g, indent = 2)` / `write_json(os, g, vertex_attr_fn, edge_attr_fn, indent)` — JSON writer; `indent=0` for compact single-line output (`io/json.hpp`)
- `read_json(is) -> json_graph` — JSON parser returning a `json_graph` structure with string-valued attribute maps (`io/json.hpp`)
- 19 I/O tests covering write, read, and roundtrip for all three formats
- **Graph generators** (`<graph/generators/>`) — produce `edge_list<VId, EV>` sorted by source, loadable into any container via `load_edges()`:
- `erdos_renyi(n, p, seed, dist)` — G(n,p) random directed graph using the Batagelj–Brandes geometric-skip algorithm; O(E) time (`generators/erdos_renyi.hpp`)
- `grid_2d(rows, cols, seed, dist)` — bidirectional 4-connected grid graph; E/V ≈ 4 (`generators/grid.hpp`)
- `barabasi_albert(n, m, seed, dist)` — preferential-attachment scale-free graph; E/V ≈ 2m (`generators/barabasi_albert.hpp`)
- `path_graph(n, seed, dist)` — directed path 0 → 1 → … → n−1; E = n−1 (`generators/path.hpp`)
- `weight_dist` enum (`uniform` U[1,100], `exponential` Exp(0.1)+1, `constant_one`) — passed to all generators (`generators/common.hpp`)
- `edge_list<VId, EV>` / `edge_entry<VId, EV>` type aliases; all generators accept a `VId` template parameter (default `uint32_t`) for large graphs (`generators/common.hpp`)
- 6 generator tests covering basic properties, weight distributions, and `uint64_t` vertex IDs
- **AdaptingThirdPartyGraph example** (`examples/AdaptingThirdPartyGraph/adapting_a_third_party_graph.cpp`) — demonstrates how to wrap an existing third-party graph type with graph-v3 CPO friend functions (vertices, edges, target_id, vertex_value, edge_value, find_vertex) so all views and algorithms work without modifying the original type
- **PageRank example** (`examples/PageRank/`) — placeholder for a PageRank implementation; PageRank was removed from the standard algorithm list because there is no single common implementation
- **CppCon 2021 examples** (`examples/CppCon2021/`) — four standalone programs refactored from graph-v2 to graph-v3:
- `graphs.cpp` — basic vertex/edge traversal and graph construction
- `bacon.cpp` — Kevin Bacon six-degrees problem using BFS
- `ospf.cpp` — OSPF-style shortest-path routing with Dijkstra
- `imdb.cpp` — IMDB actor/movie graph with BFS and path reconstruction
- **CppCon 2022 examples** (`examples/CppCon2022/`) — Germany routes graph demo refactored from graph-v2 to graph-v3:
- `rr_adaptor.hpp` — generic range-of-ranges graph adaptor; exposes graph-v3 CPO interface (vertices, edges, target_id, vertex_value, edge_value, find_vertex) using descriptor-based friend functions; data members declared before friend trailing-return-type declarations to satisfy C++ class scope rules
- `graphviz_output.hpp` — Graphviz `.gv` file writers using vertexlist, incidence, and edges_dfs views
- `germany_routes_example.cpp` — builds a Germany routes graph, traverses it, and runs Dijkstra twice (segment count and km distance)

### Changed
- **`edge_descriptor` simplified to iterator-only storage** — removed the `conditional_t<random_access_iterator, size_t, EdgeIter>` dual-storage path; edges always store the iterator directly since edges always have physical containers. Eliminates 38 `if constexpr` branches across 6 files (~500 lines removed).
Expand Down
122 changes: 122 additions & 0 deletions PageRank/pagerank.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/**
* @file pagerank.hpp
*
* @brief PageRank (PR) ranking algorithm.
*
* @copyright Copyright (c) 2022
*
* SPDX-License-Identifier: BSL-1.0
*
* @authors
* Muhammad Osama
*/

#ifndef GRAPH_PAGERANK_HPP
#define GRAPH_PAGERANK_HPP

#include "graph/graph.hpp"
#include "graph/views/incidence.hpp"
#include "graph/views/edgelist.hpp"

#include <execution>
#include <algorithm>
#include <vector>

namespace graph {

/**
* @brief Requirements for an edge value function: evf(uv) -> value.
*
* @tparam G Graph type.
* @tparam F Function type.
*/
template <class G, class F>
concept edge_weight_function = // e.g. weight(uv)
copy_constructible<F> && is_arithmetic_v<invoke_result_t<F, edge_reference_t<G>>>;


/**
* @brief PageRank (PR) algorithm.
*
* @tparam G The graph type.
* @tparam PageRank The ranks range type.
* @tparam EVF The edge value function that returns the weight of an edge.
*
* @param g The adjacency graph.
* @param scores [out] The page rank of each vertex in the graph, accessible through scores[uid], where uid is the vertex_id. The caller must assure size(scores) >= size(vertices(g)).
* @param damping_factor The alpha/damping factor (default = 0.85.)
* @param threshold The error threshold for convergence (default = 1e-4.)
* @param max_iterations Maximum number of iterations for convergence (default = std::numeric_limits<unsigned int>::max().)
* @param weight_fn The edge value function (default returns 1 for each edge value.)
*/
template <adjacency_list G,
ranges::random_access_range PageRank,
class EVF = std::function<ranges::range_value_t<PageRank>(edge_reference_t<G>)>>
requires ranges::random_access_range<vertex_range_t<G>> && integral<vertex_id_t<G>> &&
is_arithmetic_v<ranges::range_value_t<PageRank>> && edge_weight_function<G, EVF>
void pagerank(
G&& g, // graph
PageRank& scores, // out: page rank scores
const double damping_factor = 0.85,
const double threshold = 1e-4,
const std::size_t max_iterations = std::numeric_limits<unsigned int>::max(),
EVF weight_fn = [](edge_reference_t<G> uv) { return ranges::range_value_t<PageRank>(1); }) {
using id_type = vertex_id_t<G>;
using weight_type = ranges::range_value_t<PageRank>;

std::vector<weight_type> plast(size(vertices(g)));
std::vector<id_type> degrees(size(vertices(g)));

// alpha * 1 / (sum of outgoing weights) -- used to determine
// out of mass spread from src to dst
std::vector<weight_type> iweights(size(vertices(g)));

// Initialize the data, pagerank as 1/n_vertices.
std::ranges::fill(scores, 1.0 / double(size(vertices(g))));

for (auto&& [uid, u] : views::vertexlist(g)) {
// Calculate the degree of each vertex.
size_t edge_cnt = 0;
for (auto&& uv : edges(g, u)) {
++edge_cnt;
}
degrees[uid] = static_cast<id_type>(edge_cnt);

// Find the sum of outgoing weights.
weight_type val = 0;
for (auto&& [vid, uv, w] : views::incidence(g, uid, weight_fn)) {
val += w;
}
iweights[uid] = (val != 0) ? damping_factor / val : 0;
}

size_t iter;
for (iter = 0; iter < max_iterations; ++iter) {
// Make a copy of pagerank from previous iteration.
std::ranges::copy(scores.begin(), scores.end(), plast.begin());

// Handle "dangling nodes" (nodes w/ zero outdegree)
// could skip this if no nodes have sero outdegree
weight_type dsum = 0.0f;
for (auto&& [uid, u] : views::vertexlist(g)) {
dsum += (iweights[uid] == 0) ? damping_factor * scores[uid] : 0;
}

std::ranges::fill(scores, (1 - damping_factor + dsum) / double(size(vertices(g))));

double error = 0;
for (auto&& [uid, vid, uv, val] : views::edgelist(g, weight_fn)) {
weight_type update = plast[uid] * iweights[uid] * val;
scores[vid] += update;
error += fabs(scores[uid] - plast[uid]);
}

// Check for convergence
if (error < threshold)
break;
}
}

} // namespace graph

#endif // GRAPH_PAGERANK_HPP
56 changes: 56 additions & 0 deletions PageRank/pagerank_tests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/catch_template_test_macros.hpp>
#include "csv_routes.hpp"
#include "graph/graph.hpp"
#include "graph/container/dynamic_graph.hpp"
#include "graph/algorithm/pagerank.hpp"
#include "graph/views/incidence.hpp"
#include "graph/views/edgelist.hpp"
#include <cassert>
#ifdef _MSC_VER
# include "Windows.h"
#endif

#define TEST_OPTION_OUTPUT (1) // output tests for visual inspection
#define TEST_OPTION_GEN (2) // generate unit test code to be pasted into this file
#define TEST_OPTION_TEST (3) // run unit tests
#define TEST_OPTION TEST_OPTION_OUTPUT

using std::cout;
using std::endl;

using graph::vertex_t;
using graph::vertex_id_t;
using graph::vertex_reference_t;
using graph::vertex_iterator_t;
using graph::vertex_edge_range_t;
using graph::edge_t;

using graph::vertices;
using graph::edges;
using graph::vertex_value;
using graph::target_id;
using graph::target;
using graph::edge_value;
using graph::find_vertex;
using graph::vertex_id;

using routes_volf_graph_traits = graph::container::vofl_graph_traits<double, std::string, std::string>;
using routes_volf_graph_type = graph::container::dynamic_adjacency_graph<routes_volf_graph_traits>;

TEST_CASE("PageRank", "[pagerank]") {
init_console();
using G = routes_volf_graph_type;
auto&& g = load_ordered_graph<G>(TEST_DATA_ROOT_DIR "germany_routes.csv", name_order_policy::source_order_found);

std::vector<double> page_rank(size(vertices(g)));
graph::pagerank(g, page_rank, 0.85, 1e-4, 10);

std::vector<double> answer = {0.051086017487729, 0.065561667371485, 0.106818581147795, 0.141889899564636,
0.065561667371485, 0.078952299317762, 0.065561667371485, 0.078952299317762,
0.260972178563747, 0.084643725419772};

for (auto&& [uid, u] : graph::views::vertexlist(g)) {
REQUIRE(page_rank[uid] == Approx(answer[uid]).epsilon(1e-4));
}
}
File renamed without changes.
File renamed without changes.
Loading
Loading