diff --git a/.gitignore b/.gitignore index 29075b6..ad6bc2c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ -# Build directories +# Example output files +examples/**/output/* +!examples/**/output/.gitkeep build/ build*/ cmake-build-*/ diff --git a/AdaptingThirdPartyGraph/CMakeLists.txt b/AdaptingThirdPartyGraph/CMakeLists.txt new file mode 100644 index 0000000..5425e38 --- /dev/null +++ b/AdaptingThirdPartyGraph/CMakeLists.txt @@ -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 $<$:-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) diff --git a/AdaptingThirdPartyGraph/adapting_a_third_party_graph.cpp b/AdaptingThirdPartyGraph/adapting_a_third_party_graph.cpp new file mode 100644 index 0000000..f397722 --- /dev/null +++ b/AdaptingThirdPartyGraph/adapting_a_third_party_graph.cpp @@ -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 +#include +#include +#include +#include + + +namespace MyLibrary { // custom graph container, conceptually an adjacency list + +struct MyEdge { + std::string content; + int indexOfTarget; +}; + +struct MyVertex { + std::string content; + std::vector outEdges; +}; + +class MyGraph { + std::vector _vertices; + +public: + MyVertex const* getVertexByIndex(int index) const { return &_vertices[static_cast(index)]; } + + std::vector const& getAllVertices() const // !! one of customization points + { + return _vertices; + } // forced me to add this fun + + void setTopology(std::vector 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::const_iterator it) { + return static_cast(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); + + const MyLibrary::MyGraph g = [] { // populate the graph + MyLibrary::MyGraph r; + std::vector 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; +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d743c1..a7f2438 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,33 @@ - `is_sparse_vertex_container_v` 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** (``) — 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** (``) — produce `edge_list` 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` / `edge_entry` 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` 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). diff --git a/PageRank/pagerank.hpp b/PageRank/pagerank.hpp new file mode 100644 index 0000000..83ff96a --- /dev/null +++ b/PageRank/pagerank.hpp @@ -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 +#include +#include + +namespace graph { + +/** + * @brief Requirements for an edge value function: evf(uv) -> value. + * + * @tparam G Graph type. + * @tparam F Function type. +*/ +template +concept edge_weight_function = // e.g. weight(uv) + copy_constructible && is_arithmetic_v>>; + + +/** + * @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::max().) + * @param weight_fn The edge value function (default returns 1 for each edge value.) + */ +template (edge_reference_t)>> +requires ranges::random_access_range> && integral> && + is_arithmetic_v> && edge_weight_function +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::max(), + EVF weight_fn = [](edge_reference_t uv) { return ranges::range_value_t(1); }) { + using id_type = vertex_id_t; + using weight_type = ranges::range_value_t; + + std::vector plast(size(vertices(g))); + std::vector degrees(size(vertices(g))); + + // alpha * 1 / (sum of outgoing weights) -- used to determine + // out of mass spread from src to dst + std::vector 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(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 diff --git a/PageRank/pagerank_tests.cpp b/PageRank/pagerank_tests.cpp new file mode 100644 index 0000000..20a7932 --- /dev/null +++ b/PageRank/pagerank_tests.cpp @@ -0,0 +1,56 @@ +#include +#include +#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 +#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; +using routes_volf_graph_type = graph::container::dynamic_adjacency_graph; + +TEST_CASE("PageRank", "[pagerank]") { + init_console(); + using G = routes_volf_graph_type; + auto&& g = load_ordered_graph(TEST_DATA_ROOT_DIR "germany_routes.csv", name_order_policy::source_order_found); + + std::vector page_rank(size(vertices(g))); + graph::pagerank(g, page_rank, 0.85, 1e-4, 10); + + std::vector 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)); + } +} \ No newline at end of file diff --git a/agents/bgl_graph_adapt_goal.md b/agents/archive/bgl_graph_adapt_goal.md similarity index 100% rename from agents/bgl_graph_adapt_goal.md rename to agents/archive/bgl_graph_adapt_goal.md diff --git a/agents/bgl_graph_adapt_plan.md b/agents/archive/bgl_graph_adapt_plan.md similarity index 100% rename from agents/bgl_graph_adapt_plan.md rename to agents/archive/bgl_graph_adapt_plan.md diff --git a/agents/bgl_graph_adapt_strategy.md b/agents/archive/bgl_graph_adapt_strategy.md similarity index 100% rename from agents/bgl_graph_adapt_strategy.md rename to agents/archive/bgl_graph_adapt_strategy.md diff --git a/agents/bgl_migration_strategy.md b/agents/bgl_migration_strategy.md index 3a335c1..d2af443 100644 --- a/agents/bgl_migration_strategy.md +++ b/agents/bgl_migration_strategy.md @@ -2,6 +2,8 @@ A comprehensive analysis of the Boost Graph Library (BGL) and graph-v3, identifying gaps, migration paths, and recommended extensions to enable a smooth upgrade transition. +> **Last reviewed:** 2026-05-03 against `include/graph/` source tree. + --- ## Table of Contents @@ -35,17 +37,21 @@ graph-v3 is a ground-up C++20 redesign targeting ISO standardization (P3126–P3 - `vertex_property_map` auto-selects `vector` vs `unordered_map` based on graph type - Lazy search views (`vertices_bfs`, `edges_dfs`) composable with `std::views` - `transpose_view` is a zero-cost adaptor (no wrapper descriptor types) +- `filtered_graph` adaptor (vertex/edge predicates) modelling `adjacency_list` +- Ready-to-use BGL adaptor (`graph::bgl::graph_adaptor`) for incremental migration +- Three I/O formats already implemented (DOT, GraphML, JSON) **Key gaps requiring attention for BGL migration:** - Dozens of missing algorithms across flow, matching, coloring, planarity, isomorphism, centrality, layout, and related areas - No `subgraph` hierarchy with descriptor mapping -- No graph I/O (DIMACS, METIS) -- Graph generators partially implemented (Erdős-Rényi, Barabási-Albert, grid, path available; Watts-Strogatz, R-MAT still missing) -- `dynamic_graph` lacks individual mutation (`add_vertex`, `add_edge`, `remove_vertex`, `remove_edge`) +- No DIMACS or METIS I/O +- Graph generators partially implemented (Erdős-Rényi G(n,p), Barabási-Albert, 2D grid, path available; Watts-Strogatz, R-MAT, complete graph still missing) +- `dynamic_graph` lacks individual mutation (`add_vertex`, `add_edge`, `remove_vertex`, `remove_edge`); `undirected_adjacency_list` provides `create_vertex` / `create_edge` / `erase_edge` but no `erase_vertex` - No `adjacency_matrix` container - No `copy_graph` utility with cross-type and property mapping support - No `labeled_graph` adaptor (string labels → vertex mapping) - No named parameter interface (BGL users must learn new positional API) +- No composable visitor adaptors (`make_visitor(...)` factory) --- @@ -233,9 +239,9 @@ dijkstra_shortest_paths(g, {s}, container_value_fn(dist), ...); | **Dijkstra** | `dijkstra_shortest_paths(g, s, weight_map(w).distance_map(d).predecessor_map(p))` | `dijkstra_shortest_paths(g, sources, dist_fn, pred_fn, weight_fn, visitor)` | graph-v3: function-object properties, multi-source, custom compare/combine | | **Bellman-Ford** | `bellman_ford_shortest_paths(g, N, weight_map(w).distance_map(d))` | `bellman_ford_shortest_paths(g, sources, dist_fn, pred_fn, weight_fn, visitor)` | Similar; graph-v3 detects negative cycles via visitor | | **Kruskal MST** | `kruskal_minimum_spanning_tree(g, back_inserter(mst))` | `kruskal(edges, mst_output)` | graph-v3 works on edge lists directly; also `inplace_kruskal` | -| **Prim MST** | `prim_minimum_spanning_tree(g, pmap)` | `prim(g, mst_output, weight_fn)` | Similar interface | +| **Prim MST** | `prim_minimum_spanning_tree(g, pmap)` | `prim(g, source, mst_output, weight_fn)` | graph-v3 takes a source; thin wrapper over `dijkstra_shortest_paths` | | **Connected Components** | `connected_components(g, comp_map)` | `connected_components(g, comp_fn)` | graph-v3 uses function objects | -| **Strong Components** | `strong_components(g, comp_map)` | `kosaraju(g, comp_fn)` + `tarjan_scc(g, comp_fn)` | graph-v3 has both Kosaraju and Tarjan | +| **Strong Components** | `strong_components(g, comp_map)` | `kosaraju(g, comp_fn)` + `tarjan_scc(g, comp_fn)` | graph-v3 has both Kosaraju and Tarjan; `tarjan_scc.hpp` is **not** in `algorithms.hpp` umbrella (include directly) | | **Biconnected Components** | `biconnected_components(g, comp_map)` | `biconnected_components(g, output)` | Similar | | **Topological Sort** | `topological_sort(g, back_inserter(order))` | `topological_sort(g, output_iter)` | Both use output iterators; graph-v3 returns bool (cycle detection) | | **Articulation Points** | `articulation_points(g, back_inserter(art))` | `articulation_points(g, output_iter)` | Similar | @@ -348,13 +354,13 @@ dijkstra_shortest_paths(g, {s}, container_value_fn(dist), ...); ### Algorithms in graph-v3 but NOT in BGL -| Algorithm | Notes | -|-----------|-------| -| **Jaccard Coefficient** | Similarity metric for adjacent vertex neighborhoods | -| **Label Propagation** | Community detection algorithm | -| **Maximal Independent Set** | Greedy MIS | -| **Triangle Count** | `triangle_count()` and `directed_triangle_count()` | -| **Afforest** | Parallel-friendly connected components (Sutton et al.) | +| Algorithm | Header | Notes | +|-----------|--------|-------| +| **Jaccard Coefficient** | `jaccard.hpp` | Similarity metric for adjacent vertex neighborhoods | +| **Label Propagation** | `label_propagation.hpp` | Community detection algorithm | +| **Maximal Independent Set** | `mis.hpp` | Greedy MIS | +| **Triangle Count** | `tc.hpp` | `triangle_count()` and `directed_triangle_count()` | +| **Afforest** | `connected_components.hpp` | Parallel-friendly connected components (Sutton et al.) | --- @@ -414,9 +420,9 @@ breadth_first_search(g, {s}, my_visitor{}); **Missing events:** `non_tree_edge`, `gray_target`, `black_target` are BFS-specific events that distinguish edge targets by color state. These could be added to graph-v3's BFS visitor concept if needed for migration. -### Composable Visitor Adaptors — Gap +### Composable Visitor Adaptors — Gap (still open) -BGL provides reusable event visitor adaptors (`predecessor_recorder`, `distance_recorder`, `time_stamper`, `property_writer`) that can be composed via `std::pair` chaining. graph-v3 has no equivalent — users write monolithic visitor structs. This is less composable but also less complex. +BGL provides reusable event visitor adaptors (`predecessor_recorder`, `distance_recorder`, `time_stamper`, `property_writer`) that can be composed via `std::pair` chaining. graph-v3 has no equivalent — users write monolithic visitor structs. As of 2026-05, no `make_visitor(...)` factory or pre-built event adaptors are available. **Recommendation:** Consider providing lambda-based visitor construction: ```cpp @@ -493,9 +499,11 @@ graph-v3's lazy view system is a significant advancement over BGL: | **Adjacency List Text** | `operator<<` / `operator>>` | ❌ None | 🟢 Low | | **JSON** | None | ✅ `write_json()`, `read_json()` | 🟡 Medium (modern format) | -**Recommendation:** Implement DOT and GraphML as the first I/O formats. These cover the vast majority of BGL user needs. Design the I/O layer as generic free functions taking any `adjacency_list`. +**Status:** DOT, GraphML, and JSON readers/writers are implemented and shipped. Headers live under `include/graph/io/` (`dot.hpp`, `graphml.hpp`, `json.hpp`). Reader functions return type-tagged graph objects (`dot_graph`, `graphml_graph`, `json_graph`) suitable for use with all graph-v3 algorithms. + +**Recommendation:** Implement DIMACS next — needed for the standard max-flow benchmark suite once flow algorithms land. METIS and adjacency-list text are low priority. -### Proposed DOT API — `std::format`-Based +### DOT API — `std::format`-Based (implemented) The DOT writer should leverage `std::format` (C++20) for type-safe value serialization. This avoids inventing a new extension point — users who specialize `std::formatter` get DOT output for free. @@ -578,7 +586,7 @@ write_dot(cout, g, }); ``` -### Proposed GraphML API +### GraphML API (implemented) ```cpp #include @@ -1040,13 +1048,14 @@ These items block migration for the largest number of BGL users: | Item | Type | Effort | Rationale | |------|------|--------|-----------| | **Individual mutation on directed graph** | Container | High | `add_vertex()`, `add_edge()`, `remove_vertex()`, `remove_edge()` on `dynamic_graph`. Most BGL code builds graphs incrementally. | -| **`filtered_graph_view`** | Adaptor | Medium | Preserves `adjacency_list` concept; enables algorithm composition on subsets | +| **`erase_vertex` on `undirected_adjacency_list`** | Container | Medium | Currently only `erase_edge` is supported on the mutation-friendly container | | **A\* Search** | Algorithm | Medium | Heavily used in pathfinding, robotics, game AI | -| **DOT format read/write** | I/O | Medium | Primary graph interchange format | -| **Erdos-Renyi generator** | Generator | Low | Essential for testing and benchmarking | | **`copy_graph` utility** | Utility | Low | Cross-type graph copy with property mapping | | **Betweenness Centrality** | Algorithm | Medium | Core network analysis metric | | **PageRank** | Algorithm | Low | Widely used iterative algorithm | +| **DIMACS read/write** | I/O | Low | Required for max-flow benchmark suites | + +> **Done since the previous revision of this plan:** `filtered_graph` adaptor, DOT/GraphML/JSON I/O, Erdős-Rényi / Barabási-Albert / 2D grid / path generators, `kosaraju` + `tarjan_scc`, `afforest`, library-shipped BGL adaptor (`include/graph/adaptors/bgl/`). ### Phase 2: Common Algorithm Coverage @@ -1061,7 +1070,7 @@ These items block migration for the largest number of BGL users: | **Transitive Closure/Reduction** | Algorithm | Medium | DAG analysis | | **Core Numbers (k-core)** | Algorithm | Medium | Network analysis | | **Cuthill-McKee Ordering** | Algorithm | Medium | Sparse matrix bandwidth reduction | -| **GraphML read/write** | I/O | Medium | XML-based interchange | +| **DIMACS I/O** | I/O | Low | Needed for flow algorithm benchmarks | ### Phase 3: Advanced Features @@ -1083,8 +1092,7 @@ These items block migration for the largest number of BGL users: | Item | Type | Effort | Rationale | |------|------|--------|-----------| -| **DIMACS I/O** | I/O | Low | Needed for flow algorithm benchmarks | -| **JSON I/O** | I/O | Medium | Modern interchange format | +| **METIS I/O** | I/O | Low | Legacy partitioning format | | **Parallel algorithms** | Algorithm | High | Parallel BFS, CC, PageRank | | **`grid_graph`** | Container | Medium | Implicit N-dimensional grid | | **Condensation graph** | Algorithm | Low | DAG from SCC | @@ -1149,7 +1157,7 @@ The scores below are directional editorial estimates, not audited counts. | **Isomorphism** | 3 algorithms | 0 | 0% | | **Ordering/bandwidth** | 8 algorithms | 0 | 0% | | **Layout** | 5 algorithms | 0 | 0% | -| **Graph adaptors** | 5 adaptors | 1 (transpose) | 20% | +| **Graph adaptors** | 5 adaptors | 3 (transpose, filtered, BGL adaptor) | 60% | | **Graph I/O** | 5 formats | 3 (DOT, GraphML, JSON) | 60% | | **Graph generators** | 6 generators | 4 (path, grid, Erdős–Rényi, Barabási–Albert) | 67% | | **Visitors** | 5 types + composable adaptors | Concept-checked visitors | 75% | diff --git a/agents/multi_type_analysis.md b/agents/multi_type_analysis.md new file mode 100644 index 0000000..be20cc0 --- /dev/null +++ b/agents/multi_type_analysis.md @@ -0,0 +1,877 @@ +# Multi-Partite Graph Design Analysis + +**Goal.** Today every graph in the library carries a *single* `vertex_value_type` (VV) +and *single* `edge_value_type` (EV). Multi-partite scenarios — bipartite movie/actor, +heterogeneous knowledge graphs, multi-layer networks — must shoehorn all vertex +kinds into one type, typically a `std::variant` (see [`bacon3()` in +examples/BGLWorkshop2026/bacon.cpp](../examples/BGLWorkshop2026/bacon.cpp)). +This document analyses the library to identify what would need to change to +support a graph whose **partition is the discriminator on a vertex's C++ type**, +i.e. each partition `p ∈ [0..P)` has its own `VV_p` and (independently) its own +edge-value types `EV_{p→q}`. + +--- + +## 1. Where the single-type assumption lives today + +### 1.1 Container template signature + +[`include/graph/container/dynamic_graph.hpp` line 893](../include/graph/container/dynamic_graph.hpp#L893): + +```cpp +template +class dynamic_graph_base { +public: + using partition_id_type = VId; + using partition_vector = std::vector; + using vertex_id_type = VId; + using vertex_type = dynamic_vertex; + using edge_type = dynamic_out_edge; + using vertices_type = typename Traits::vertices_type; // e.g. std::vector + using edges_type = typename Traits::edges_type; // e.g. std::list + ... +}; +``` + +`vertices_type` is one container of one `vertex_type`. The same holds for +`compressed_graph_base` (CSR row values are a single `vector`). + +### 1.2 Descriptor payload + +[`vertex_value` ADL hook in dynamic_graph.hpp lines 1700–1715](../include/graph/container/dynamic_graph.hpp#L1700-L1715): + +```cpp +template +requires std::derived_from, dynamic_graph_base> && vertex_descriptor_type && + (!std::is_void_v) +[[nodiscard]] friend constexpr decltype(auto) vertex_value(G&& g, U&& u) noexcept { + return std::forward(u).inner_value(std::forward(g).vertices_).value(); +} +``` + +`vertex_value(g,u)` always returns a reference to the *single* `VV`. Same story +for `edge_value(g,uv) → EV&` lines 1730–1755. + +### 1.3 Partitions exist but are vertex-id ranges, not types + +[compressed_graph.hpp lines 1370–1430](../include/graph/container/compressed_graph.hpp#L1370-L1430): + +```cpp +friend constexpr auto partition_id(G&& g, const VertexDesc& u) noexcept -> partition_id_type { + const auto vid = u.vertex_id(); + ... + auto it = std::upper_bound(g.partition_.begin(), g.partition_.end() - 1, vid); + --it; + return static_cast(std::distance(g.partition_.begin(), it)); +} +``` + +Today's "partition" is a contiguous **vertex-id range** stored in a +`std::vector partition_`, queried by binary search. There is no link +between partition id and C++ type — every vertex in every partition still has +the same `VV`. The CPOs `partition_id(g,u)`, `num_partitions(g)`, +`vertices(g, pid)`, `num_vertices(g, pid)` are all integer-driven. + +### 1.4 CPOs are uniform across all vertices/edges + +The CPO list (from +[adj_list/detail/graph_cpo.hpp](../include/graph/adj_list/detail/graph_cpo.hpp) +and the friend functions on `dynamic_graph_base`): + +| CPO | Returns | Currently single-type? | +|---|---|---| +| `vertices(g)` | range of `vertex_t` | yes | +| `vertices(g, pid)` | range of `vertex_t` | yes (subrange of same type) | +| `vertex_id(g, u)` | `vertex_id_t` | yes | +| `vertex_value(g, u)` | `vertex_value_t&` | **yes — single VV** | +| `find_vertex(g, uid)` | iterator to `vertex_t` | yes | +| `partition_id(g, u)` | `partition_id_type` (integer) | n/a | +| `num_partitions(g)` | integer | n/a | +| `edges(g, u)` / `edges(g, uid)` | range of `edge_t` | yes | +| `edge_value(g, uv)` | `edge_value_t&` | **yes — single EV** | +| `target_id`, `source_id`, `target`, `source` | `vertex_id_t` / iter | yes | + +Every signature is parameterised over a single `G` whose `vertex_t` and +`edge_t` are concrete types. There is no language for *"the value type of +the vertex `u` you happen to have"*. + +### 1.5 Views and algorithms — actually mostly type-agnostic + +This is the **good news** that shapes the design space. + +[`include/graph/views/bfs.hpp` lines 144–148](../include/graph/views/bfs.hpp#L144-L148): + +```cpp +template , ...> +class vertices_bfs_view; + +template , ...> +class edges_bfs_view; +``` + +Views never read `vertex_value_t` directly — they accept a **user value +function** `vvf : (G&, vertex_t) -> auto` and propagate +`std::invoke_result_t`. So `vertices_bfs(g, seed, vvf)` already returns +`[v, val]` tuples whose `val` type can vary with `v`. The same is true of +`edges_bfs`, `vertices_dfs`, `vertexlist`, `edgelist`, `neighbors`, etc. + +Algorithms are similar. +[dijkstra_shortest_paths.hpp lines 90–120](../include/graph/algorithm/dijkstra_shortest_paths.hpp#L90-L120): + +```cpp +* @param weight Edge weight function: (const G&, const edge_t&) -> Distance. +``` + +Dijkstra never calls `edge_value(g,uv)` itself — the weight comes from a +user-supplied `WF`. Same with `bellman_ford`, `mst`, `articulation_points`, +`tarjan_scc`. The library deliberately decoupled "value" from "weight/key" +years ago. + +**Implication.** Whatever we do at the *container* layer to support multiple +vertex/edge types, **most views and algorithms work unchanged** as long as the +graph still satisfies the basic adjacency-list concepts (you can iterate +`vertices(g)`, `edges(g,u)`, get `vertex_id`/`target_id`, etc.) and as long as +"the descriptor" remains a single concrete type. The pinch points are exactly: + +1. `vertex_value(g,u)` and `edge_value(g,uv)` — what type do they return when + the vertex/edge type is partition-dependent? +2. `vertex_t` and `edge_t` — does the library still have *one* + descriptor type per graph? +3. `find_vertex(g, uid)`, `vertices(g)` — does iteration cross partitions? + +The three design options below differ exactly on how they answer these +questions. + +--- + +## 2. Design Options + +| # | Name | Vertex C++ types | Single descriptor? | Library impact | +|---|---|---|---|---| +| **A** | **Variant value (status quo)** | one VV = `variant<...>` | yes | none — works today | +| **B** | **Type-erased payload** | one VV = `any` / poly handle | yes | small — value access stays `VV&` | +| **C** | **Partition-typed values, single graph** | `tuple` indexed by partition | yes | medium — `vertex_value` becomes a *visitor* CPO | +| **D** | **Multi-graph composite** | `tuple`; vertex desc = `(pid, sub_desc)` | **no** — descriptor carries pid | large — descriptors, CPOs, views, algorithms | +| **E** | **Heterogeneous descriptors via expression-template wrapper** | each partition keeps its real type; a wrapper graph projects them | yes per-partition, multiple per composite | large — dual-layer concept hierarchy | + +The three serious candidates are **B**, **C**, and **D**. Option A is what +`bacon3()` does already. The body of this document develops B, C, and D in full, +and §2.5 gives a complete treatment of E for comparison. + +--- + +### 2.1 Option B — Type-erased payload (`std::any` / polymorphic handle) + +**Idea.** Keep `dynamic_graph` exactly as it is, but +choose `VV = std::any` (or a pointer to a polymorphic base, or a small-buffer +handle). Each vertex stores any payload; the user retrieves it with +`std::any_cast(vertex_value(g,u))` after checking +`partition_id(g,u)`. + +**`bacon3` rewrite:** + +```cpp +using G = dynamic_adjacency_graph< + uol_graph_traits>; +G g; +g.load_vertices(movies, [](const auto& m) { return VD{m, std::any{movie{m}}}; }); +g.load_vertices(actors, [](const auto& a) { return VD{a, std::any{actor{a}}}; }); +// ... +const actor& a = std::any_cast(vertex_value(g, u)); +``` + +**CPOs.** No change. `vertex_value` still returns `std::any&`. + +**Views & algorithms.** No change. They never inspect the payload. + +**Pros.** +- Zero library changes. +- Cleaner than `variant` for *open* type sets (new vertex kinds at runtime). +- Default-constructibility issue from `bacon3()` disappears (`std::any{}` is fine). + +**Cons.** +- Every payload access is a runtime type check + heap allocation + (`std::any` for non-SBO types). For a `bipartite movie ↔ actor` graph this + is strictly worse than the variant. +- No compile-time type safety — `bad_any_cast` instead of a compile error. +- Doesn't address the *partition* dimension at all. The user has to invent a + `partition → type` map themselves. + +**Verdict.** A wash. It removes the variant boilerplate but loses static +type safety and pessimises performance. Useful only for plug-in +architectures where vertex kinds are not known at compile time. + +--- + +### 2.2 Option C — Partition-typed values in a single graph + +This is the design I think best fits the library's existing shape. + +**Idea.** Replace the single `VV` template parameter with a **`std::tuple` of +per-partition value types** `VVs = tuple`. The +graph stores **P parallel vertex containers** — one per partition — and +the descriptor encodes `(partition_id, local_index)`. + +```cpp +template +class multipart_graph_base; +// EVs = tuple — edge value per *target* partition (or per (src,tgt) pair, see §2.2.4) +// VVs = tuple — vertex value per partition +``` + +#### 2.2.1 New trait + +```cpp +template +struct multipart_uol_traits { + using edge_value_types = EVs; // tuple + using vertex_value_types = VVs; // tuple + using vertex_id_type = VId; + static constexpr std::size_t partition_count = std::tuple_size_v; + + // One vertex container per partition. + // For a 2-partition movie/actor graph this is + // tuple>, + // unordered_map>> + template + using vertex_container = std::unordered_map, std::tuple_element_t, ...>>; + + using vertices_type = std::tuple< /* expand partitions */ >; + using edges_type = /* either uniform std::list or one per (src,tgt) pair */; +}; +``` + +#### 2.2.2 New descriptor + +```cpp +namespace graph::adj_list { +template // one Iter type per partition +class multipart_vertex_descriptor { + std::size_t pid_; // which partition + std::variant stored_iter_; // exactly one alternative active +public: + constexpr std::size_t partition() const noexcept { return pid_; } + + template + constexpr auto& iter() const noexcept { return std::get

(stored_iter_); } + + // vertex_id(g,u) is uniform — VId is the same across partitions + constexpr decltype(auto) vertex_id() const noexcept; +}; +} +``` + +The descriptor is *one C++ type* for the whole graph (so views and the +adjacency_list concept still see "one vertex_t"), but it remembers which +partition it came from. + +#### 2.2.3 CPO impact + +| CPO | Change | Notes | +|---|---|---| +| `vertices(g)` | unchanged interface | returns a *flattened view* (`std::ranges::join` over the per-partition containers); element type is `multipart_vertex_descriptor` | +| `vertices(g, pid)` | now returns the **typed** subrange of partition `pid` | element type is still the multipart descriptor — only iteration scope differs | +| `vertex_id(g, u)` | unchanged | `VId` is uniform | +| **`vertex_value(g, u)`** | **now a visitor** — see below | | +| `partition_id(g, u)` | reads `u.partition()` directly | O(1) instead of O(log P) | +| `num_partitions(g)` | `std::tuple_size_v` | constexpr | +| `edges(g, u)` | unchanged | element type still `edge_t` | +| `target_id` / `source_id` | unchanged | | +| `edge_value(g, uv)` | visitor (same shape as vertex_value) | | +| `find_vertex(g, uid)` | **ambiguous** — see §2.2.5 | needs disambiguation | + +**`vertex_value` becomes a `visit` CPO.** Two flavours: + +```cpp +// 1. Compile-time dispatch — caller knows the partition +template +constexpr auto& vertex_value(G&& g, const U& u); // mandates u.partition() == P at runtime + +// 2. Visitor-style — caller provides an overload set +template +constexpr decltype(auto) visit_vertex_value(G&& g, const U& u, Visitor&& vis); +// calls vis(actor&), vis(movie&), etc., dispatched on u.partition() +``` + +Existing call sites that today write `vertex_value(g,u)` with a single VV +**continue to compile** when `partition_count == 1` via a partial +specialisation — the multi-partite path is opt-in. + +#### 2.2.4 Edge value typing + +Three sub-options for `EVs`: + +1. **Uniform across partitions** — `EVs = E` (a single type). Trivial. +2. **One EV per source partition** — `EVs = tuple`. + All edges leaving partition `p` carry `EV_p`. Fits "movie-edges have a year, + actor-edges have a credit-order". +3. **One EV per (src, tgt) pair** — `EVs = matrix`. Most expressive, + but `O(P²)` instantiations. Fits knowledge graphs where each relation type + is its own table. + +I recommend **(2) per source-partition** as the default — it lines up with +how out-edges are stored, keeps the type matrix linear, and matches the +common case (every relation is owned by one of its endpoints). + +#### 2.2.5 `find_vertex(g, uid)` — the hardest CPO + +If two partitions share the same `VId` type (say both keyed by `std::string`), +`find_vertex(g, "Top Gun")` is ambiguous: is it the movie or the actor? + +Three answers, in increasing strictness: + +a. **Search all partitions in declaration order, return the first match.** + Simple; matches the variant-key approach in `bacon3()` Option 3 (key IS + the discriminator). Cost: O(P) lookups per find. + +b. **Require the user to pass the partition id**: `find_vertex<1>(g, uid)` + or `find_vertex(g, pid, uid)`. New overload; existing single-partition + `find_vertex(g, uid)` still works. + +c. **Mandate distinct `VId` types per partition** at the trait level. + `find_vertex` then dispatches on the argument type. Most type-safe; rules + out string-keyed bipartite graphs where both sides are strings. + +I recommend (b) as the primary CPO, with (a) as a free-standing helper for +discovery use-cases. + +#### 2.2.6 Example — `bacon4()` under Option C + +```cpp +using VVs = std::tuple; // partition 0 = movie, 1 = actor +using EVs = void; // edges carry no value +using G = multipart_adjacency_graph< + multipart_uol_traits>; + +G g; +g.load_vertices<0>(movies, [](const auto& m) { return movie{m}; }); +g.load_vertices<1>(actors, [](const auto& a) { return actor{a}; }); +g.load_edges<0,1>(movies_actors, [](const auto& e) { + return std::pair{std::get<0>(e), std::get<1>(e)}; // movie → actor +}); +g.load_edges<1,0>(movies_actors, [](const auto& e) { + return std::pair{std::get<1>(e), std::get<0>(e)}; // actor → movie +}); + +auto seed = find_vertex<1>(g, "Kevin Bacon"s); // partition 1 = actor + +// Views work unchanged: +for (auto&& [uv] : graph::views::edges_bfs(g, *seed)) { /* ... */ } + +// Visitor for printing: +visit_vertex_value(g, u, overloaded{ + [](const movie& m) { cout << "[movie] " << m.title() << '\n'; }, + [](const actor& a) { cout << "[actor] " << a.name() << '\n'; }, +}); +``` + +#### 2.2.7 Pros / cons + +**Pros.** +- Static type safety — `vertex_value

(g,u)` returns the *real* type. +- Each partition can use a different storage strategy (one `vector`-backed + partition for dense ids, one `unordered_map`-backed for sparse) by + parametrising `Traits` per-partition. Powerful. +- BFS / DFS / dijkstra / etc. all work unchanged because they treat + `vertex_t` opaquely. +- `partition_id(g,u)` becomes O(1) (descriptor carries it directly). +- The descriptor is still *one* type → `adjacency_list` concept holds. + +**Cons.** +- The descriptor grows from `iterator` to `(size_t, variant)` + — typically 16→24 or 24→40 bytes. Affects BFS queue / DFS stack memory. +- `vertex_value` callers need the visitor pattern (or a compile-time pid) + to use the typed payload. More verbose at the value-access call site. +- `find_vertex` requires partition disambiguation in the general case. +- `vertices(g)` has to be a `join_view` over heterogeneous containers — the + iterator type is non-trivial; needs `std::common_iterator` machinery. +- New trait family `multipart_*_traits` doubles the trait surface. +- `compressed_graph` cannot be retrofitted easily — its CSR layout assumes + one row-values vector. Would be `multipart_compressed_graph` from scratch. + +--- + +### 2.3 Option D — Multi-graph composite + +**Idea.** Each partition is a *separate* `dynamic_graph` with its own VV and +EV. A *composite graph* holds `tuple` plus an inter-partition +edge map. Vertex descriptors carry `(pid, vertex_t)`. + +```cpp +template +class composite_graph { + std::tuple partitions_; + std::array cross_edges_; +}; +``` + +#### CPO impact + +Every CPO must be re-implemented at the composite level because +`vertex_t` is now a tagged variant, not a single iterator type. +`adjacency_list>` requires a fresh proof — most concept +checks fail until the composite forwards them. + +`edges(g, u)` becomes a *concatenation* of: + +- `edges(get(g.partitions_), u.local_desc())` — intra-partition +- `cross_edges_[(u.pid, *)].find(u.local_id)` — inter-partition + +Algorithm impact: + +- BFS/DFS view construction needs a way to *push* a heterogeneous descriptor + into the queue. Today the queue is `std::queue>`; in the + composite that becomes `std::queue` with a tagged + union. Doable but invasive — every visited tracker, every heap, every + predecessor map needs to key on the composite descriptor type. +- Dijkstra's `vector_position_map` and `assoc_position_map` (see + [`heap_position_map.hpp`](../include/graph/detail/heap_position_map.hpp)) + rely on `vertex_id_t` being hashable / index-able. With composite + descriptors the user must provide a hash for `(pid, sub_id)`. + +#### Pros / cons + +**Pros.** +- Each partition keeps its existing graph type unchanged — *zero* changes to + per-partition CPOs, views, algorithms. +- Maximum heterogeneity — partitions can be `dynamic_graph`, `compressed_graph`, + even user-supplied adapters, all in the same composite. +- Conceptually clean: you compose what you have rather than parameterise a + new container. + +**Cons.** +- The composite layer reimplements *every* CPO. The amount of glue rivals the + size of `dynamic_graph_base` itself. +- `vertex_t` is necessarily heavier than today's vertex descriptor + (must encode pid). More expensive in queues / maps. +- Iteration order across partitions is not contiguous → poor cache behaviour + for BFS-heavy workloads. +- Existing single-partition algorithms must learn to "see" the composite + shape. They do this via concepts today, but the concept proofs become + conditional. + +**Verdict.** Theoretically the cleanest separation of concerns, but the +composite layer is a second framework on top of the first. Best reserved for +truly heterogeneous storage (e.g., one partition is a remote graph, another +is in-memory). + +--- + +### 2.5 Option E — Heterogeneous descriptors via a concept-projection wrapper + +**Idea.** Every partition is a normal, self-contained, single-type graph. +A thin *wrapper* graph owns no vertex or edge storage itself; it holds +references to the underlying partition graphs and **re-exposes a projected +view** of them through the standard `adjacency_list` concept. Within each +partition the user works with the partition's own types; across partition +boundaries the wrapper provides a *minimal common projection* for algorithms +that need to walk the whole graph (e.g., BFS over all partitions). + +This is the "expression-template" parallel: just as `std::views::join` wraps +a range-of-ranges without owning storage, the wrapper graph wraps a +graph-of-graphs without owning vertices or edges. + +#### 2.5.1 Layered concept hierarchy + +Option E is only coherent if the library supports **two levels** of graph +concept: + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ partitioned_graph (multi-partition; heterogeneous) │ +│ requires: │ +│ – partition_count (constexpr integral) │ +│ – partition_graph_t (the graph type for partition P)│ +│ – the partition graph satisfies adjacency_list │ +└──────────────┬──────────────────────────────────────────────────┘ + │ projected/erased cross-partition descriptor + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ adjacency_list (today's concept; single-type) │ +│ requires: │ +│ – vertices(g), edges(g,u), vertex_id(g,u), ... │ +└─────────────────────────────────────────────────────────────────┘ +``` + +The wrapper satisfies `adjacency_list` via a projected view, allowing all +existing single-partition algorithms to consume it unchanged. The richer +`partitioned_graph` concept lets multi-partition-aware code access typed +partition graphs directly. + +#### 2.5.2 Wrapper structure + +```cpp +template // one graph type per partition +class partitioned_graph_wrapper { + + std::tuple partitions_; // non-owning; each Gs is stored elsewhere + +public: + // ── Cross-partition (projected) interface ────────────────────────────── + // Satisfies adjacency_list by erasing the per-partition type into a + // common cross_vertex_descriptor. + + struct cross_vertex_descriptor { + std::size_t pid; // which partition + std::size_t lid; // local vertex index within that partition + }; + + // vertices(wrapper) = join over all partition vertices, tagged with pid. + // Returns a range whose value_type is cross_vertex_descriptor. + friend auto vertices(const partitioned_graph_wrapper& g) { + return cross_vertex_range{g.partitions_}; // see §2.5.3 + } + + // vertex_id(wrapper, cvd) = some globally unique id, e.g. (pid, lid) pair + friend cross_vertex_descriptor vertex_id(const partitioned_graph_wrapper& g, + const cross_vertex_descriptor& cvd) { + return cvd; // the descriptor IS the id in this design + } + + // edges(wrapper, cvd) = partition-local out-edges, tagged with pid + friend auto edges(const partitioned_graph_wrapper& g, + const cross_vertex_descriptor& cvd) { + return std::visit([&](auto* part) { + return cross_edge_range{cvd.pid, graph::edges(*part, cvd.lid)}; + }, partition_ptr_variant(g.partitions_, cvd.pid)); + } + + // ── Per-partition typed interface ────────────────────────────────────── + // Satisfies partitioned_graph; gives back the real graph. + + template + auto& partition() noexcept { return *std::get

(partitions_); } + + template + const auto& partition() const noexcept { return *std::get

(partitions_); } + + static constexpr std::size_t partition_count = sizeof...(Gs); +}; +``` + +#### 2.5.3 The cross-vertex range and descriptor + +`vertices(wrapper)` must expose a lazy, joined range whose iterator yields +`cross_vertex_descriptor`. The simplest strategy: + +```cpp +class cross_vertex_range { + // Iterates partition 0 first (pid=0, lid=0..N0-1), then partition 1, etc. + // The iterator wraps a (pid, local_iterator) pair, advances through + // partitions when the local iterator hits end(). +}; +``` + +This range satisfies `std::ranges::forward_range`. `vertex_t` = +`cross_vertex_descriptor` — a plain struct with `pid` and `lid`, no +iterator storage. + +The cross-edge descriptor mirrors it: + +```cpp +struct cross_edge_descriptor { + cross_vertex_descriptor source; + cross_vertex_descriptor target; + // No EV stored — for typed edge access, go through partition

(). +}; +``` + +#### 2.5.4 Value access — the fundamental split + +This is where Option E differs most sharply from the others. + +**Within a partition** (typed, static, direct): + +```cpp +// User holds a compile-time partition index P and a local descriptor. +auto& m = graph::vertex_value(wrapper.partition<0>(), local_u); // → movie& +auto& a = graph::vertex_value(wrapper.partition<1>(), local_v); // → actor& +``` + +Nothing new here — these are calls on the ordinary single-type sub-graphs. + +**Across partitions** (via the projected wrapper): + +```cpp +// visitor dispatch on pid at runtime: +auto val = visit_vertex(wrapper, cvd, overloaded{ + [](const movie& m, std::integral_constant) { ... }, + [](const actor& a, std::integral_constant) { ... }, +}); +``` + +`visit_vertex` is a new free function, not a CPO: + +```cpp +template +decltype(auto) visit_vertex(PG&& pg, const CVD& cvd, Visitor&& vis) { + // runtime switch on cvd.pid; calls vis(vertex_value(partition

(), local), ic) + return [&](std::index_sequence) { + using result_t = std::common_type_t< + std::invoke_result_t>, + std::integral_constant + >...>; + result_t ret; + ([&]{ if (cvd.pid == Ps) { + ret = vis(vertex_value(pg.partition(), to_local(cvd)), ic); + } }(), ...); + return ret; + }(std::make_index_sequence{}); +} +``` + +`vertex_value(wrapper, cvd)` (the CPO) is intentionally **not defined** for +the wrapper — it would have to return a `std::variant` anyway, collapsing +back to Option A. Instead, value access always goes either: +- directly through `partition

()` for typed single-partition work, or +- through `visit_vertex` for cross-partition visitor dispatch. + +#### 2.5.5 Views and algorithms over the wrapper + +Because the wrapper satisfies `adjacency_list`, existing views work: + +```cpp +// BFS over all partitions, no changes to bfs_view: +for (auto&& [uv] : graph::views::edges_bfs(wrapper, seed_cvd)) { + // uv is cross_edge_descriptor; no value access here + auto uid = source_id(wrapper, uv); // → cross_vertex_descriptor + auto vid = target_id(wrapper, uv); // → cross_vertex_descriptor +} +``` + +As long as the algorithm doesn't call `vertex_value` or `edge_value` on the +wrapper itself — and today's algorithms don't — this works without a single +line of algorithm code changing. + +Algorithms that *do* need values (e.g., user code inside a BFS visitor +callback) retrieve them via `visit_vertex`: + +```cpp +for (auto&& [uv] : graph::views::edges_bfs(wrapper, seed)) { + auto v = target_id(wrapper, uv); + visit_vertex(wrapper, v, overloaded{ + [](const movie& m, auto) { cout << "[movie] " << m.title() << '\n'; }, + [](const actor& a, auto) { cout << "[actor] " << a.name() << '\n'; }, + }); +} +``` + +#### 2.5.6 `find_vertex` in the wrapper + +`find_vertex(wrapper, uid)` where `uid` is a `cross_vertex_descriptor` +is trivial — the descriptor *is* the id. The harder case is +"find by application key": + +```cpp +// "Find the movie vertex called Top Gun" +auto local_it = graph::find_vertex(wrapper.partition<0>(), "Top Gun"s); +cross_vertex_descriptor cvd{0, local_it->vertex_id()}; +``` + +There is no single `find_vertex(wrapper, "Top Gun"s)` because the +partition is not deducible from the key type alone (same ambiguity as +Option C §2.2.5). The same three solutions apply; the recommended +answer is the same: require an explicit partition index. + +#### 2.5.7 `bacon4()` under Option E + +```cpp +void bacon4() { + using namespace graph::container; + + // Each partition is an ordinary single-type dynamic graph. + using MovieGraph = dynamic_adjacency_graph>; + using ActorGraph = dynamic_adjacency_graph>; + + MovieGraph mg; + ActorGraph ag; + + mg.load_vertices(movies, [](const auto& m) { return VD{m, movie{m}}; }); + ag.load_vertices(actors, [](const auto& a) { return VD{a, actor{a}}; }); + + // The wrapper holds cross-partition edges; it does NOT store intra-partition edges. + // Cross edges are stored in a separate bipartite edge table. + partitioned_graph_wrapper wrapper{mg, ag}; + wrapper.load_cross_edges<0,1>(movies_actors, [](const auto& e) { + return cross_edge_t{"Top Gun", "Tom Cruise"}; // movie key → actor key + }); + wrapper.load_cross_edges<1,0>(movies_actors, [](const auto& e) { + return cross_edge_t{"Tom Cruise", "Top Gun"}; + }); + + // seed is a cross_vertex_descriptor encoding (partition=1, local="Kevin Bacon") + auto seed_local = graph::find_vertex(wrapper.partition<1>(), "Kevin Bacon"s); + cross_vertex_descriptor seed{1, seed_local->vertex_id()}; + + std::unordered_map depth{{seed, 0}}; + for (auto&& [uv] : graph::views::edges_bfs(wrapper, seed)) { + depth[target_id(wrapper, uv)] = depth[source_id(wrapper, uv)] + 1; + } + + for (const auto& a : actors) { + auto local = graph::find_vertex(wrapper.partition<1>(), a); + cross_vertex_descriptor cvd{1, local->vertex_id()}; + auto it = depth.find(cvd); + cout << a << " has Bacon number " + << (it == depth.end() ? "infinity" : std::to_string(it->second / 2)) + << '\n'; + } +} +``` + +#### 2.5.8 Pros / cons + +**Pros.** +- **Zero changes to existing partition graph code.** `MovieGraph` and + `ActorGraph` are completely ordinary graphs. `vertex_value` on each works + exactly as today. +- **No new descriptor variant overhead for single-partition work.** When + code only touches partition 0, it works with `vertex_t` + directly — a plain iterator, not a fat tagged struct. +- **Cleanest separation of concerns.** The wrapper enforces: "I can + traverse all vertices/edges for algorithmic purposes; for value access go + to the typed partition." +- **Open to non-`dynamic_graph` backends.** `partition<2>()` can be a + `compressed_graph`, an external database adapter, or any type that + satisfies `adjacency_list`. +- **Incremental adoption.** Existing code continues to use `MovieGraph` + directly; only code that needs cross-partition traversal touches the wrapper. + +**Cons.** +- **Two-tier concept hierarchy is new library infrastructure.** The + `partitioned_graph` concept, `visit_vertex`, `cross_vertex_descriptor`, + and the `cross_vertex_range` iterator are all genuinely new. +- **Cross-partition edges need a separate store.** In a bipartite graph + *all* edges are cross-partition, so the "wrapper holds no edges" stance + forces a new `cross_edge_table` container. That container needs its own + `load_edges` interface, iterator, CPO hooks. +- **`find_vertex` across the whole wrapper is ambiguous** (same issue as + C and D) and there is no single-natural solution. +- **BFS visited tracker keys on `cross_vertex_descriptor`**, which is a + struct — needs a `std::hash` specialisation (or uses the ordered-map + path). Straightforward but non-zero friction. +- **Depth map / predecessor map complexity doubles.** Today + `std::unordered_map, size_t>` where `vertex_t` is a plain + iterator. Under E it becomes `unordered_map`, + requiring a hash. Under C the descriptor is the same shape (pid, lid) so + the cost is identical — but C hides it inside a single graph type. +- **`vertex_value(wrapper, cvd)` is intentionally absent.** This is + philosophically correct, but means any generic algorithm that expects + `vertex_value` to be callable on *any* `adjacency_list` will silently + fail (compile error or `static_assert`) when given the wrapper. The + library must document this clearly. + +#### 2.5.9 Relationship to the other options + +| Dimension | C | D | E | +|---|---|---|---| +| Storage ownership | single composite container | tuple of full graphs | tuple of external graphs (non-owning) | +| Descriptor type | one fat multipart descriptor | tagged sub-descriptor | `cross_vertex_descriptor` | +| Typed value access | `vertex_value

(g,u)` CPO | `vertex_value(get

(g),u)` | `vertex_value(wrapper.partition

(),u)` | +| Cross-partition value | `visit_vertex_value` CPO | `visit_vertex_value` CPO | `visit_vertex` free function | +| `vertex_value` on composite | returns `variant` (optional) | returns `variant` (optional) | **intentionally absent** | +| Algorithms on composite | `adjacency_list` holds automatically | `adjacency_list` re-proved | `adjacency_list` holds via projection | +| Single-partition existing code | must use `partition

()` API | must use `get

(g)` API | **unchanged — uses original graph** | + +The key philosophical difference: C and D bring all vertex types *into* a +single graph object (they own the storage). E keeps each partition *outside* +the wrapper (the wrapper borrows them). This makes E the most composition- +friendly option for codebases where partition graphs already exist +independently, and the most invasive option for new graph construction +(cross edges need a separate home). + +--- + +## 3. Comparison Summary + +| Concern | A — variant VV | B — `std::any` | C — typed-tuple | D — composite | E — wrapper | +|---|---|---|---|---|---| +| Static type safety on `vertex_value` | weak (`std::get`) | none (`any_cast`) | **strong** (per-partition CPO) | **strong** (per sub-graph) | **strong** (per partition) | +| `vertex_value` on composite | returns `variant` | returns `any` | `variant` or absent | `variant` or absent | **absent by design** | +| Performance vs. status quo | baseline | worse (heap+RTTI) | ≈baseline | worse (het. queues) | ≈baseline | +| Library code changes | none | none | **medium** | **large** | **large** | +| Concept hierarchy changes | none | none | none | `adjacency_list` re-proved | new `partitioned_graph` concept | +| `find_vertex` story | trivial | trivial | `

` overload | composite-defined | `partition

().find_vertex` | +| Storage choice per partition | no | no | **yes** | **yes** | **yes** | +| Existing single-partition code | unchanged | unchanged | use `partition

()` | use `get

(g)` | **unchanged** | +| Cross-edge storage | in-graph | in sub-graphs | in composite | in composite | **separate store needed** | +| Best for | small ad-hoc bipartite | open/plug-in vertex kinds | static finite partitions | mixed storage backends | *composing pre-existing graphs* | + +--- + +## 4. Recommendation + +**Option C** is the right next step. + +1. It preserves the library's invariants: one `vertex_t`, one + `vertex_id_t`, the same `adjacency_list` concept, the same view + pipeline. +2. It makes `partition_id` *meaningful* — today it's an integer tied to a + contiguous id range with no semantic content; in Option C it indexes into + `tuple`. +3. The work is bounded and incremental: + - Add `multipart_*_traits` (sibling of `vol_graph_traits` etc.). + - Add `multipart_vertex_descriptor` (sibling of `vertex_descriptor`). + - Implement `multipart_dynamic_graph` re-using `dynamic_vertex` / + `dynamic_out_edge` per partition. + - Add `vertex_value

(g,u)` and `visit_vertex_value(g,u,vis)` CPOs. + - Add `find_vertex

(g,uid)` overload. + - Provide the partial specialisation that collapses to today's behaviour + when `partition_count == 1`. +4. Views and algorithms need *no changes* — they already accept user value + functions (`VVF`, `EVF`, `WF`) and never call `vertex_value` / + `edge_value` themselves. + +Option B is a useful escape hatch for the "open type set" case and could be +shipped as a documented convention (`VV = std::any`) without library +changes. + +Option D is interesting future work for federated graphs but pays for +flexibility with a doubled CPO surface — premature for now. + +--- + +## 5. Migration story for `bacon3()` + +Under Option C the example collapses to (no `std::variant`, no `std::monostate`, +no `std::hash` specialisations): + +```cpp +void bacon4() { + using namespace graph::container; + + using VVs = std::tuple; + using G = multipart_adjacency_graph< + multipart_uol_traits>; + + G g; + g.load_vertices<0>(movies, [](const auto& m) { return movie{m}; }); + g.load_vertices<1>(actors, [](const auto& a) { return actor{a}; }); + g.load_edges<0,1>(movies_actors, [](const auto& e) { + return copyable_edge_t{std::get<0>(e), std::get<1>(e)}; + }); + g.load_edges<1,0>(movies_actors, [](const auto& e) { + return copyable_edge_t{std::get<1>(e), std::get<0>(e)}; + }); + + auto seed = *find_vertex<1>(g, "Kevin Bacon"s); + + std::unordered_map, std::size_t> depth{{seed, 0}}; + for (auto&& [uv] : graph::views::edges_bfs(g, seed)) { + auto u = *find_vertex(g, source_id(g, uv)); // partition disambiguated by descriptor + auto v = *find_vertex(g, target_id(g, uv)); + depth[v] = depth[u] + 1; + } + + for (const auto& a : actors) { + auto it = depth.find(*find_vertex<1>(g, a)); + cout << a << " has Bacon number " + << (it == depth.end() ? std::string{"infinity"} : std::to_string(it->second / 2)) + << '\n'; + } +} +``` + +The domain classes `movie` and `actor` need **no** comparison/hash/default +constructors — exactly the constraint that drove the `bacon3()` complexity. diff --git a/docs/assets/adjacency_list_concepts.dot b/docs/assets/adjacency_list_concepts.dot new file mode 100644 index 0000000..d5e974c --- /dev/null +++ b/docs/assets/adjacency_list_concepts.dot @@ -0,0 +1,93 @@ +// Concept hierarchy for graph::adj_list +// Generate image: dot -Tsvg adjacency_list_concepts.dot -o adjacency_list_concepts.svg + +digraph ConceptHierarchy { + rankdir = TB + node [shape=box, style="filled,rounded", fontname="Helvetica", fontsize=11] + edge [fontname="Helvetica", fontsize=9] + + // ── Primitive concepts ──────────────────────────────────────────────────── + // Foundational — no prerequisites + + node [fillcolor="#cce5ff"] // light blue + + edge_c [label="edge\nsource_id · target_id"] + vertex_c [label="vertex\nvertex_descriptor\nvertex_id · find_vertex"] + hashable_vertex_id [label="hashable_vertex_id\nhash(vertex_id_t)"] + + // ── Range concepts ──────────────────────────────────────────────────────── + // Built from primitives; parameterised on a range type R + + node [fillcolor="#d4edda"] // light green + + out_edge_range [label="out_edge_range\nforward_range of edge"] + in_edge_range [label="in_edge_range\nforward_range of edge"] + vertex_range [label="vertex_range\nforward_range + sized_range\nof vertex"] + + // ── Vertex range refinements ────────────────────────────────────────────── + + node [fillcolor="#fff3cd"] // light yellow + + index_vertex_range [label="index_vertex_range\nintegral vid\nintegral storage_type"] + mapped_vertex_range [label="mapped_vertex_range\n!index_vertex_range\nhashable_vertex_id\nfind_vertex"] + + // ── Core graph concepts ─────────────────────────────────────────────────── + + node [fillcolor="#f8d7da"] // light red/salmon + + adjacency_list [label="adjacency_list\nvertex_range + out_edge_range"] + ordered_vertex_edges [label="ordered_vertex_edges\nadjacency_list\n+ edges sorted by target_id"] + bidirectional_adjacency_list [label="bidirectional_adjacency_list\nadjacency_list\n+ in_edge_range"] + + // ── Compound / specialised concepts ────────────────────────────────────── + + node [fillcolor="#e2d9f3"] // light purple + + index_adjacency_list [label="index_adjacency_list\nadjacency_list\n+ index_vertex_range"] + mapped_adjacency_list [label="mapped_adjacency_list\nadjacency_list\n+ mapped_vertex_range"] + index_bidirectional_adjacency_list [label="index_bidirectional\n_adjacency_list\nbidirectional\n+ index_vertex_range"] + mapped_bidirectional_adjacency_list [label="mapped_bidirectional\n_adjacency_list\nbidirectional\n+ mapped_vertex_range"] + + // ── Edges (arrows = "is required by" / "refines") ───────────────────────── + + // Primitives → ranges + edge_c -> out_edge_range + edge_c -> in_edge_range + vertex_c -> vertex_range + + // vertex_range → vertex range refinements + vertex_range -> index_vertex_range + vertex_range -> mapped_vertex_range [style=dashed, label=" !index_vertex_range"] + hashable_vertex_id -> mapped_vertex_range + + // Ranges → adjacency_list + out_edge_range -> adjacency_list + vertex_range -> adjacency_list + + // adjacency_list → refinements + adjacency_list -> ordered_vertex_edges + adjacency_list -> bidirectional_adjacency_list + in_edge_range -> bidirectional_adjacency_list + + // adjacency_list + vertex range kind → compound concepts + adjacency_list -> index_adjacency_list + index_vertex_range -> index_adjacency_list + + adjacency_list -> mapped_adjacency_list + mapped_vertex_range -> mapped_adjacency_list + + bidirectional_adjacency_list -> index_bidirectional_adjacency_list + index_vertex_range -> index_bidirectional_adjacency_list + + bidirectional_adjacency_list -> mapped_bidirectional_adjacency_list + mapped_vertex_range -> mapped_bidirectional_adjacency_list + + // ── Rank hints for cleaner layout ───────────────────────────────────────── + { rank=same; edge_c; vertex_c; hashable_vertex_id } + { rank=same; out_edge_range; in_edge_range; vertex_range } + { rank=same; index_vertex_range; mapped_vertex_range } + { rank=same; adjacency_list } + { rank=same; ordered_vertex_edges; bidirectional_adjacency_list } + { rank=same; index_adjacency_list; mapped_adjacency_list; + index_bidirectional_adjacency_list; mapped_bidirectional_adjacency_list } +} diff --git a/docs/assets/adjacency_list_concepts.svg b/docs/assets/adjacency_list_concepts.svg new file mode 100644 index 0000000..f6e715a --- /dev/null +++ b/docs/assets/adjacency_list_concepts.svg @@ -0,0 +1,246 @@ + + + + + + +ConceptHierarchy + + + +edge_c + +edge<G,E> +source_id · target_id + + + +out_edge_range + +out_edge_range<R,G> +forward_range of edge<G,·> + + + +edge_c->out_edge_range + + + + + +in_edge_range + +in_edge_range<R,G> +forward_range of edge<G,·> + + + +edge_c->in_edge_range + + + + + +vertex_c + +vertex<G,V> +vertex_descriptor +vertex_id · find_vertex + + + +vertex_range + +vertex_range<R,G> +forward_range + sized_range +of vertex<G,·> + + + +vertex_c->vertex_range + + + + + +hashable_vertex_id + +hashable_vertex_id<G> +hash(vertex_id_t<G>) + + + +mapped_vertex_range + +mapped_vertex_range<G> +!index_vertex_range +hashable_vertex_id +find_vertex + + + +hashable_vertex_id->mapped_vertex_range + + + + + +adjacency_list + +adjacency_list<G> +vertex_range + out_edge_range + + + +out_edge_range->adjacency_list + + + + + +bidirectional_adjacency_list + +bidirectional_adjacency_list<G> +adjacency_list ++ in_edge_range + + + +in_edge_range->bidirectional_adjacency_list + + + + + +index_vertex_range + +index_vertex_range<G> +integral vid +integral storage_type + + + +vertex_range->index_vertex_range + + + + + +vertex_range->mapped_vertex_range + + +  !index_vertex_range + + + +vertex_range->adjacency_list + + + + + +index_adjacency_list + +index_adjacency_list<G> +adjacency_list ++ index_vertex_range + + + +index_vertex_range->index_adjacency_list + + + + + +index_bidirectional_adjacency_list + +index_bidirectional +_adjacency_list<G> +bidirectional ++ index_vertex_range + + + +index_vertex_range->index_bidirectional_adjacency_list + + + + + +mapped_adjacency_list + +mapped_adjacency_list<G> +adjacency_list ++ mapped_vertex_range + + + +mapped_vertex_range->mapped_adjacency_list + + + + + +mapped_bidirectional_adjacency_list + +mapped_bidirectional +_adjacency_list<G> +bidirectional ++ mapped_vertex_range + + + +mapped_vertex_range->mapped_bidirectional_adjacency_list + + + + + +ordered_vertex_edges + +ordered_vertex_edges<G> +adjacency_list ++ edges sorted by target_id + + + +adjacency_list->ordered_vertex_edges + + + + + +adjacency_list->bidirectional_adjacency_list + + + + + +adjacency_list->index_adjacency_list + + + + + +adjacency_list->mapped_adjacency_list + + + + + +bidirectional_adjacency_list->index_bidirectional_adjacency_list + + + + + +bidirectional_adjacency_list->mapped_bidirectional_adjacency_list + + + + + diff --git a/docs/assets/edge_list_concepts.dot b/docs/assets/edge_list_concepts.dot new file mode 100644 index 0000000..6372c3f --- /dev/null +++ b/docs/assets/edge_list_concepts.dot @@ -0,0 +1,30 @@ +// Concept hierarchy for graph::edge_list +// Generate image: dot -Tsvg edge_list_concepts.dot -o edge_list_concepts.svg + +digraph EdgeListConceptHierarchy { + rankdir = TB + node [shape=box, style="filled,rounded", fontname="Helvetica", fontsize=11] + edge [fontname="Helvetica", fontsize=9] + + // ── Primitive concepts ──────────────────────────────────────────────────── + + node [fillcolor="#cce5ff"] // light blue + + basic_sourced_edgelist [label="basic_sourced_edgelist\ninput_range\n!range-of-ranges\nsource_id · target_id\n(compatible return types)"] + + // ── Refinements ─────────────────────────────────────────────────────────── + + node [fillcolor="#d4edda"] // light green + + basic_sourced_index_edgelist [label="basic_sourced_index_edgelist\nbasic_sourced_edgelist\n+ integral source_id\n+ integral target_id"] + + has_edge_value [label="has_edge_value\nbasic_sourced_edgelist\n+ edge_value(el, uv)"] + + // ── Edges ───────────────────────────────────────────────────────────────── + + basic_sourced_edgelist -> basic_sourced_index_edgelist + basic_sourced_edgelist -> has_edge_value + + // ── Rank hints ──────────────────────────────────────────────────────────── + { rank=same; basic_sourced_index_edgelist; has_edge_value } +} diff --git a/docs/assets/edge_list_concepts.svg b/docs/assets/edge_list_concepts.svg new file mode 100644 index 0000000..8c62f42 --- /dev/null +++ b/docs/assets/edge_list_concepts.svg @@ -0,0 +1,52 @@ + + + + + + +EdgeListConceptHierarchy + + + +basic_sourced_edgelist + +basic_sourced_edgelist<EL> +input_range +!range-of-ranges +source_id · target_id +(compatible return types) + + + +basic_sourced_index_edgelist + +basic_sourced_index_edgelist<EL> +basic_sourced_edgelist ++ integral source_id ++ integral target_id + + + +basic_sourced_edgelist->basic_sourced_index_edgelist + + + + + +has_edge_value + +has_edge_value<EL> +basic_sourced_edgelist ++ edge_value(el, uv) + + + +basic_sourced_edgelist->has_edge_value + + + + + diff --git a/docs/contributing/cpo-implementation.md b/docs/contributing/cpo-implementation.md index 9e65ad7..2981278 100644 --- a/docs/contributing/cpo-implementation.md +++ b/docs/contributing/cpo-implementation.md @@ -568,7 +568,7 @@ The following CPOs are implemented (or planned) for the Graph Container Interfac | `target_id(g, uv)` | `(G&, edge_t)` | `vertex_id_t` | **yes** | pattern extraction (integral/pair/tuple) | | `source_id(g, uv)` | `(G&, edge_t)` | `vertex_id_t` | **yes** | descriptor source | | `find_vertex(g, uid)` | `(G&, vertex_id_t)` | `vertex_iterator_t` | no | `begin(vertices(g)) + uid` | -| `num_vertices(g)` | `(const G&)` | `integral` | no | `ranges::size(vertices(g))` | +| `num_vertices(g)` | `(const G&)` | `integral` | no | `ranges::size(vertices(g))` (4-tier: member → ADL → `size(vertices(g))` → `size(g)`) | | `num_edges(g)` | `(const G&)` | `integral` | no | sum of `distance(edges(g,u))` | | `degree(g, u)` | `(const G&, vertex_t)` | `integral` | no | `ranges::size(edges(g,u))` | | `target(g, uv)` | `(G&, edge_t)` | `vertex_t` | no | `*find_vertex(g, target_id(g,uv))` | diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 0000000..7da234a --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,212 @@ + + + +
graph-v3 logo + +# Examples + +> Complete working programs that demonstrate graph-v3 features — from simple traversals to full algorithm usage and third-party graph adaptation. + +
+ +> [← Back to Documentation Index](index.md) + +--- + +## Quick Reference + +| Example | What it demonstrates | +|---------|---------------------| +| [Adapting a Third-Party Graph](#adapting-a-third-party-graph) | Wrapping an existing type with CPO friend functions | +| [CppCon 2021](#cppcon-2021) | BFS, Dijkstra, path reconstruction on real-world graph data | +| [CppCon 2022](#cppcon-2022) | Range-of-ranges adaptor, Dijkstra, Graphviz output | +| [PageRank](#pagerank) | PageRank algorithm (placeholder) | +| [Basic Usage](#basic-usage) | Minimal vertex/edge traversal | +| [Dijkstra CLRS](#dijkstra-clrs) | Dijkstra example from CLRS textbook | +| [MST Usage](#mst-usage) | Minimum spanning tree | +| [BGL Adaptor](#bgl-adaptor) | Using graph-v3 on Boost.Graph data structures | + +--- + +## Adapting a Third-Party Graph + +**Directory:** `examples/AdaptingThirdPartyGraph/` + +Demonstrates how to adapt an existing third-party graph container for use with +graph-v3 without modifying the original type. You provide a small set of +ADL-findable free functions in the type's namespace: + +- `vertices(g)` — vertex range (wrapped into `vertex_descriptor_view`) +- `edges(g, u)` — out-edge range for a vertex descriptor +- `target_id(g, uv)` — target vertex ID from an edge descriptor +- `find_vertex(g, uid)` — vertex descriptor from a vertex ID + +Once these are defined, the type satisfies `graph::adjacency_list` and works +with all views and algorithms. + +**Key concepts demonstrated:** +- CPO friend-function pattern with SFINAE trailing return types +- Vertex descriptor vs. vertex reference distinction +- Optional `vertex_value` / `edge_value` overrides + +**Build target:** `adapting_third_party_graph` + +--- + +## CppCon 2021 + +**Directory:** `examples/CppCon2021/` + +Four standalone programs from the CppCon 2021 presentation, refactored from +graph-v2 to graph-v3. Each program is self-contained with its own graph data. + +### graphs.cpp + +Basic graph construction and traversal using `vertexlist` and `incidence` views. +Shows structured bindings `[uid, u]` and `[vid, uv]`. + +**Build target:** `cppcon21_graphs` + +### bacon.cpp + +The Kevin Bacon "six degrees of separation" problem. Uses BFS +(`vertices_breadth_first_search`) to find shortest paths in a social graph. + +**Build target:** `cppcon21_bacon` + +### ospf.cpp + +OSPF-style shortest-path routing. Runs `dijkstra_shortest_paths` on a +weighted network graph and reconstructs the shortest-path tree. + +**Build target:** `cppcon21_ospf` + +### imdb.cpp + +Builds an actor/movie bipartite graph from the IMDB dataset, then uses BFS to +find paths between actors. + +**Build target:** `cppcon21_imdb` + +**Supporting files:** +- `graphs/` — graph data headers (karate, ospf, spice, imdb) +- `include/utilities.hpp` — shared output helpers + +--- + +## CppCon 2022 + +**Directory:** `examples/CppCon2022/` + +Germany routes graph demonstration from the CppCon 2022 presentation, refactored +from graph-v2 to graph-v3. + +### germany_routes_example.cpp + +Builds a Germany inter-city routes graph using a generic `rr_adaptor` (range-of-ranges +adaptor), traverses vertices and edges, then runs Dijkstra shortest paths twice: +1. **Segment count** — unweighted (each edge costs 1) +2. **Kilometers** — weighted by route distance + +Prints the shortest-path tree and reconstructs the path to the farthest city. + +**Build target:** `cppcon22_germany_routes` + +### rr_adaptor.hpp + +A reusable generic adaptor that wraps any random-access range-of-ranges (e.g. +`vector>`) plus an optional parallel vertex-value vector and exposes +the full graph-v3 CPO interface. + +**Key implementation details:** +- Data members declared *before* friend function trailing-return-type declarations + (C++ class scope rule: trailing return types only see members declared above) +- `vertex_value` uses `decltype(auto)` without trailing return type +- Template friend functions with SFINAE guards for `edges`, `target_id`, `edge_value` +- `find_vertex` constructs a `vertex_descriptor_view::iterator` directly from a `size_t` index + +### graphviz_output.hpp + +Three function templates that write Graphviz `.gv` files: +- `output_routes_graphviz` — full graph via `vertexlist` + `incidence` +- `output_routes_graphviz_dfs` — DFS tree via `edges_dfs` + +Uses `vertex_value(g, u)` for city names and `edge_value(g, uv)` for distances. + +--- + +## PageRank + +**Directory:** `examples/PageRank/` + +Placeholder for a PageRank implementation. PageRank was removed from the +standard algorithm list because there is no single universally-agreed +implementation — convergence criteria, damping factor, and normalization vary +across textbooks and production systems. + +--- + +## Basic Usage + +**File:** `examples/basic_usage.cpp` + +Minimal example: constructs a `vector>` graph, iterates vertices and +edges using CPOs. Good starting point for understanding the descriptor model. + +**Build target:** `basic_usage` + +--- + +## Dijkstra CLRS + +**File:** `examples/dijkstra_clrs_example.cpp` + +Dijkstra's algorithm on the textbook graph from *Introduction to Algorithms* +(Cormen, Leiserson, Rivest, Stein). Demonstrates `dijkstra_shortest_paths` with +weight function, distance, and predecessor containers. + +**Build target:** `dijkstra_example` + +--- + +## MST Usage + +**File:** `examples/mst_usage_example.cpp` + +Minimum spanning tree (Kruskal's algorithm) on a weighted graph. Shows +`kruskal_minimum_spanning_tree` with edge-value weight extraction. + +**Build target:** `mst_usage_example` + +--- + +## BGL Adaptor + +**File:** `examples/bgl_adaptor_example.cpp` + +Demonstrates using graph-v3 views and algorithms on Boost.Graph `adjacency_list` +via the BGL adaptor (`graph::bgl::graph_adaptor`). Shows property bridge +functions for mapping BGL property maps to graph-v3 value functions. + +**Build target:** `bgl_adaptor_example` + +--- + +## Building the Examples + +All examples are built as part of the default CMake build: + +```bash +cmake --preset linux-gcc-debug +cmake --build build/linux-gcc-debug +``` + +To build a specific example: + +```bash +cmake --build build/linux-gcc-debug --target cppcon22_germany_routes +``` + +Examples link against the `graph3` library target and require no external +dependencies beyond the library itself (Boost is needed only for +`bgl_adaptor_example`). diff --git a/docs/getting-started.md b/docs/getting-started.md index 25d33fc..ef9ef3b 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -13,6 +13,8 @@ running an algorithm, and working with edge lists. > [← Back to Documentation Index](index.md) +> **See also:** [Examples](examples.md) — complete programs demonstrating adaptation, traversal, shortest paths, and more. + --- ## 1. Requirements diff --git a/docs/index.md b/docs/index.md index 7d0d2f2..7fdfa2a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -23,6 +23,7 @@ - [Algorithms](user-guide/algorithms.md) — Dijkstra and Bellman-Ford shortest-paths, minimal spanning tree, connected components, and more - [Graph I/O](user-guide/io.md) — read and write DOT (GraphViz), GraphML (XML), and JSON formats - [Generators](user-guide/generators.md) — synthetic graph generators (path, grid, Erdős–Rényi, Barabási–Albert) +- [Examples](examples.md) — complete working programs (third-party adaptation, CppCon 2021/2022 demos, Dijkstra, MST, BGL adaptor) ## Reference diff --git a/docs/reference/concepts.md b/docs/reference/concepts.md index 74cfff9..b8ccf3d 100644 --- a/docs/reference/concepts.md +++ b/docs/reference/concepts.md @@ -23,21 +23,32 @@ Header: `` ### Concept Hierarchy ``` -edge -└── out_edge_range - └── vertex - └── vertex_range - ├── index_vertex_range - └── mapped_vertex_range - └── adjacency_list ← required by all algorithms - ├── index_adjacency_list - │ ├── ordered_vertex_edges - │ └── index_bidirectional_adjacency_list - ├── mapped_adjacency_list - │ └── mapped_bidirectional_adjacency_list - └── bidirectional_adjacency_list +Primitives + edge · vertex · hashable_vertex_id + +Ranges (parameterised on a range type R) + out_edge_range requires edge + in_edge_range requires edge + vertex_range requires vertex + +Vertex range specialisations + index_vertex_range vertex_range with integral IDs + mapped_vertex_range vertex_range + hashable_vertex_id + +Core graph concepts + adjacency_list vertex_range + out_edge_range ← required by all algorithms + ordered_vertex_edges adjacency_list with edges sorted by target_id + bidirectional_adjacency_list adjacency_list + in_edge_range + +Compound concepts + index_adjacency_list adjacency_list + index_vertex_range + mapped_adjacency_list adjacency_list + mapped_vertex_range + index_bidirectional_adjacency_list bidirectional_adjacency_list + index_vertex_range + mapped_bidirectional_adjacency_list bidirectional_adjacency_list + mapped_vertex_range ``` +![Adjacency List Concept Hierarchy](../assets/adjacency_list_concepts.svg) + ### `edge` An edge exposes at least a target vertex ID. @@ -200,6 +211,8 @@ reverse BFS/DFS/topological-sort views. Header: `` +![Edge List Concept Hierarchy](../assets/edge_list_concepts.svg) + ### `basic_sourced_edgelist` An edge list — a flat `forward_range` where each element has both source and diff --git a/docs/reference/cpo-reference.md b/docs/reference/cpo-reference.md index 5964e09..d5dc768 100644 --- a/docs/reference/cpo-reference.md +++ b/docs/reference/cpo-reference.md @@ -142,6 +142,15 @@ Returns the number of vertices. | **Default** | `ranges::size(vertices(g))` | | **Complexity** | O(1) | +**Resolution order for `num_vertices(g)`:** + +1. `g.num_vertices()` — member function +2. ADL `num_vertices(g)` — free function in graph's namespace +3. `std::ranges::size(vertices(g))` — size of the vertex range (works for any graph that provides `vertices(g)` returning a `sized_range`) +4. `std::ranges::size(g)` — graph is itself a `std::ranges::sized_range` + +Most graphs need no override: tier 3 fires automatically whenever `vertices(g)` is defined and returns a sized range (e.g. `vertex_descriptor_view` over a `std::vector`). + --- ## Edge CPOs @@ -439,15 +448,18 @@ Returns the number of partitions. ## Resolution Order -Every CPO follows the same three-step customization point protocol: +Most CPOs follow a three-step customization point protocol: 1. **Member function**: `g.cpo_name(args...)` or `uv.cpo_name(args...)` 2. **ADL function**: unqualified `cpo_name(args...)` found via argument-dependent lookup 3. **Default**: library-provided fallback (if available) -The `edge_value` and `target_id` CPOs have an extended resolution chain that -also checks for data members (`.value`, `.target_id`) and tuple-like access -(`get`). +Some CPOs have extended resolution chains: + +- **`num_vertices(g)`** adds a tier between ADL and the graph-as-range fallback: + 3. `std::ranges::size(vertices(g))` — vertex range size (fires for any graph that provides `vertices(g)`) + 4. `std::ranges::size(g)` — graph is itself a `sized_range` +- **`edge_value`** and **`target_id`** also check for data members (`.value`, `.target_id`) and tuple-like access (`get`). --- diff --git a/docs/status/coverage.md b/docs/status/coverage.md index 21a927b..8dad165 100644 --- a/docs/status/coverage.md +++ b/docs/status/coverage.md @@ -7,24 +7,27 @@ -> **Generated:** 2026-02-23 | **Compiler:** GCC 15.1.0 | **Preset:** `linux-gcc-coverage` +> **Generated:** 2026-05-03 | **Compiler:** GCC 15.1.0 | **Preset:** `linux-gcc-coverage` -> **Tests:** 4343 passed, 0 failed (100% pass rate) -> **Overall line coverage:** 96.3% (3624 / 3764 lines) -> **Overall function coverage:** 91.8% (29030 / 31639 functions) +> **Tests:** 4873 passed, 0 failed (100% pass rate) +> **Overall line coverage:** 95.6% (4456 / 4663 lines) +> **Overall function coverage:** 92.4% (27107 / 29333 functions) --- ## Summary by Category -| Category | Lines Hit | Lines Total | Line Coverage | -|----------|-----------|-------------|---------------| -| Adjacency list infrastructure | 375 | 375 | **100.0%** | -| Algorithms | 795 | 833 | **95.4%** | -| Containers | 938 | 1000 | **93.8%** | -| Detail / CPOs | 24 | 24 | **100.0%** | -| Edge list | 15 | 16 | **93.8%** | -| Views | 1477 | 1514 | **97.6%** | +| Category | Lines Hit | Lines Total | Line % | Funcs Hit | Funcs Total | Func % | +|----------|-----------|-------------|--------|-----------|-------------|--------| +| Adjacency list infrastructure (`adj_list/`) | 360 | 362 | **99.4%** | 10255 | 10331 | **99.3%** | +| Algorithms (`algorithm/`) | 922 | 943 | **97.8%** | 955 | 992 | **96.3%** | +| Containers (`container/`) | 931 | 994 | **93.7%** | 11002 | 12967 | **84.8%** | +| Detail / internal helpers (`detail/`) | 114 | 114 | **100.0%** | 820 | 837 | **97.9%** | +| Edge list (`edge_list/`) | 15 | 16 | **93.8%** | 21 | 21 | **100.0%** | +| Generators (`generators/`) | 83 | 86 | **96.5%** | 13 | 13 | **100.0%** | +| I/O (`io/`) | 484 | 560 | **86.4%** | 35 | 36 | **97.2%** | +| Views (`views/`) | 1501 | 1538 | **97.6%** | 3951 | 4073 | **97.0%** | +| Adaptors (`adaptors/`) | 46 | 48 | **95.8%** | 55 | 61 | **90.2%** | --- @@ -32,80 +35,126 @@ ### Adjacency List Infrastructure (`adj_list/`) -All adjacency list descriptor and CPO support headers reach **100% line coverage**. - | File | Lines Hit / Total | Line % | Funcs Hit / Total | Func % | |------|-------------------|--------|-------------------|--------| | `descriptor_traits.hpp` | 10 / 10 | 100.0% | 8 / 8 | 100.0% | -| `detail/graph_cpo.hpp` | 179 / 179 | 100.0% | 4500 / 4503 | 99.9% | -| `edge_descriptor.hpp` | 84 / 84 | 100.0% | 1293 / 1293 | 100.0% | -| `edge_descriptor_view.hpp` | 37 / 37 | 100.0% | 2453 / 2454 | 100.0% | -| `vertex_descriptor.hpp` | 35 / 35 | 100.0% | 1379 / 1380 | 99.9% | -| `vertex_descriptor_view.hpp` | 30 / 30 | 100.0% | 2410 / 2481 | 97.1% | +| `detail/graph_cpo.hpp` | 180 / 180 | 100.0% | 3993 / 3996 | 99.9% | +| `edge_descriptor.hpp` | 51 / 51 | 100.0% | 996 / 996 | 100.0% | +| `edge_descriptor_view.hpp` | 33 / 33 | 100.0% | 1886 / 1887 | 99.9% | +| `vertex_descriptor.hpp` | 37 / 37 | 100.0% | 1155 / 1156 | 99.9% | +| `vertex_descriptor_view.hpp` | 30 / 30 | 100.0% | 2082 / 2153 | 96.7% | +| `vertex_property_map.hpp` | 19 / 21 | 90.5% | 135 / 135 | 100.0% | + +> **Not measured** (compile-time only, no executable lines): `adjacency_list_concepts.hpp`, +> `adjacency_list_traits.hpp`, `descriptor.hpp`, `graph_utility.hpp`. ### Algorithms (`algorithm/`) | File | Lines Hit / Total | Line % | Funcs Hit / Total | Func % | |------|-------------------|--------|-------------------|--------| -| `articulation_points.hpp` | 53 / 53 | 100.0% | 2 / 2 | 100.0% | -| `bellman_ford_shortest_paths.hpp` | 54 / 63 | 85.7% | 31 / 31 | 100.0% | -| `biconnected_components.hpp` | 68 / 68 | 100.0% | 4 / 4 | 100.0% | -| `breadth_first_search.hpp` | 29 / 29 | 100.0% | 12 / 12 | 100.0% | -| `connected_components.hpp` | 168 / 182 | 92.3% | 18 / 18 | 100.0% | -| `depth_first_search.hpp` | 38 / 38 | 100.0% | 5 / 5 | 100.0% | -| `dijkstra_shortest_paths.hpp` | 65 / 65 | 100.0% | 36 / 36 | 100.0% | -| `jaccard.hpp` | 24 / 24 | 100.0% | 5 / 5 | 100.0% | -| `label_propagation.hpp` | 66 / 69 | 95.7% | 3 / 3 | 100.0% | -| `mis.hpp` | 25 / 28 | 89.3% | 1 / 1 | 100.0% | -| `mst.hpp` | 117 / 126 | 92.9% | 24 / 24 | 100.0% | -| `tc.hpp` | 27 / 27 | 100.0% | 2 / 2 | 100.0% | -| `topological_sort.hpp` | 52 / 52 | 100.0% | 7 / 7 | 100.0% | -| `traversal_common.hpp` | 9 / 9 | 100.0% | 3 / 3 | 100.0% | +| `articulation_points.hpp` | 52 / 52 | 100.0% | 8 / 8 | 100.0% | +| `bellman_ford_shortest_paths.hpp` | 58 / 59 | 98.3% | 151 / 157 | 96.2% | +| `biconnected_components.hpp` | 65 / 65 | 100.0% | 16 / 16 | 100.0% | +| `breadth_first_search.hpp` | 29 / 29 | 100.0% | 54 / 54 | 100.0% | +| `connected_components.hpp` | 182 / 195 | 93.3% | 50 / 50 | 100.0% | +| `depth_first_search.hpp` | 41 / 41 | 100.0% | 46 / 46 | 100.0% | +| `dijkstra_shortest_paths.hpp` | 106 / 106 | 100.0% | 430 / 461 | 93.3% | +| `jaccard.hpp` | 22 / 22 | 100.0% | 11 / 11 | 100.0% | +| `label_propagation.hpp` | 43 / 43 | 100.0% | 23 / 23 | 100.0% | +| `mis.hpp` | 26 / 29 | 89.7% | 7 / 7 | 100.0% | +| `mst.hpp` | 121 / 125 | 96.8% | 66 / 66 | 100.0% | +| `tarjan_scc.hpp` | 52 / 52 | 100.0% | 3 / 3 | 100.0% | +| `tc.hpp` | 54 / 54 | 100.0% | 5 / 5 | 100.0% | +| `topological_sort.hpp` | 53 / 53 | 100.0% | 35 / 35 | 100.0% | +| `traversal_common.hpp` | 18 / 18 | 100.0% | 50 / 50 | 100.0% | ### Containers (`container/`) | File | Lines Hit / Total | Line % | Funcs Hit / Total | Func % | |------|-------------------|--------|-------------------|--------| -| `compressed_graph.hpp` | 198 / 223 | 88.8% | 580 / 606 | 95.7% | -| `container_utility.hpp` | 6 / 6 | 100.0% | 393 / 393 | 100.0% | +| `compressed_graph.hpp` | 194 / 220 | 88.2% | 578 / 604 | 95.7% | +| `container_utility.hpp` | 6 / 6 | 100.0% | 301 / 301 | 100.0% | | `detail/undirected_adjacency_list_impl.hpp` | 312 / 321 | 97.2% | 158 / 163 | 96.9% | -| `dynamic_graph.hpp` | 316 / 343 | 92.1% | 11986 / 14415 | 83.1% | +| `dynamic_graph.hpp` | 313 / 340 | 92.1% | 9833 / 11765 | 83.6% | | `undirected_adjacency_list.hpp` | 106 / 107 | 99.1% | 132 / 134 | 98.5% | -### Views (`views/`) +> **Not measured** (compile-time type aliases): `container/traits/*.hpp` (18 graph-traits headers), +> `detail/undirected_adjacency_list_api.hpp`. + +### Detail / Internal Helpers (`detail/`) | File | Lines Hit / Total | Line % | Funcs Hit / Total | Func % | |------|-------------------|--------|-------------------|--------| -| `adaptors.hpp` | 146 / 146 | 100.0% | 113 / 113 | 100.0% | -| `bfs.hpp` | 218 / 234 | 93.2% | 193 / 204 | 94.6% | -| `dfs.hpp` | 232 / 246 | 94.3% | 316 / 327 | 96.6% | -| `edge_accessor.hpp` | 12 / 12 | 100.0% | 55 / 55 | 100.0% | -| `edgelist.hpp` | 219 / 221 | 99.1% | 556 / 578 | 96.2% | -| `incidence.hpp` | 124 / 124 | 100.0% | 613 / 619 | 99.0% | -| `neighbors.hpp` | 134 / 134 | 100.0% | 386 / 390 | 99.0% | -| `search_base.hpp` | 5 / 5 | 100.0% | 11 / 11 | 100.0% | -| `topological_sort.hpp` | 252 / 257 | 98.1% | 296 / 305 | 97.0% | -| `transpose.hpp` | 23 / 23 | 100.0% | 22 / 22 | 100.0% | -| `vertexlist.hpp` | 112 / 112 | 100.0% | 507 / 509 | 99.6% | +| `detail/edge_cpo.hpp` | 23 / 23 | 100.0% | 515 / 515 | 100.0% | +| `detail/heap_position_map.hpp` | 16 / 16 | 100.0% | 11 / 11 | 100.0% | +| `detail/indexed_dary_heap.hpp` | 75 / 75 | 100.0% | 294 / 311 | 94.5% | -### Other +> **Not measured** (compile-time only): `detail/cpo_common.hpp`, `detail/graph_using.hpp`. + +### Edge List (`edge_list/`) | File | Lines Hit / Total | Line % | Funcs Hit / Total | Func % | |------|-------------------|--------|-------------------|--------| -| `detail/edge_cpo.hpp` | 24 / 24 | 100.0% | 500 / 500 | 100.0% | | `edge_list/edge_list_descriptor.hpp` | 15 / 16 | 93.8% | 21 / 21 | 100.0% | -| `graph_data.hpp` | 0 / 2 | 0.0% | 0 / 2 | 0.0% | + +> **Not measured** (type aliases / umbrella includes): `edge_list/edge_list.hpp`, `edge_list/edge_list_traits.hpp`. + +### Generators (`generators/`) + +| File | Lines Hit / Total | Line % | Funcs Hit / Total | Func % | +|------|-------------------|--------|-------------------|--------| +| `generators/barabasi_albert.hpp` | 32 / 32 | 100.0% | 4 / 4 | 100.0% | +| `generators/common.hpp` | 10 / 10 | 100.0% | 1 / 1 | 100.0% | +| `generators/erdos_renyi.hpp` | 15 / 16 | 93.8% | 2 / 2 | 100.0% | +| `generators/grid.hpp` | 19 / 20 | 95.0% | 4 / 4 | 100.0% | +| `generators/path.hpp` | 7 / 8 | 87.5% | 2 / 2 | 100.0% | + +### I/O (`io/`) + +| File | Lines Hit / Total | Line % | Funcs Hit / Total | Func % | +|------|-------------------|--------|-------------------|--------| +| `io/dot.hpp` | 145 / 158 | 91.8% | 11 / 11 | 100.0% | +| `io/graphml.hpp` | 189 / 210 | 90.0% | 9 / 9 | 100.0% | +| `io/json.hpp` | 150 / 192 | 78.1% | 15 / 16 | 93.8% | + +> **Not measured**: `io/detail/common.hpp` (internal helper included transitively). + +### Views (`views/`) + +| File | Lines Hit / Total | Line % | Funcs Hit / Total | Func % | +|------|-------------------|--------|-------------------|--------| +| `views/adaptors.hpp` | 146 / 146 | 100.0% | 113 / 113 | 100.0% | +| `views/bfs.hpp` | 222 / 238 | 93.3% | 242 / 253 | 95.7% | +| `views/dfs.hpp` | 232 / 246 | 94.3% | 340 / 351 | 96.9% | +| `views/edge_accessor.hpp` | 12 / 12 | 100.0% | 89 / 89 | 100.0% | +| `views/edgelist.hpp` | 219 / 221 | 99.1% | 898 / 974 | 92.2% | +| `views/incidence.hpp` | 125 / 125 | 100.0% | 715 / 721 | 99.2% | +| `views/neighbors.hpp` | 134 / 134 | 100.0% | 386 / 390 | 99.0% | +| `views/search_base.hpp` | 24 / 24 | 100.0% | 54 / 56 | 96.4% | +| `views/topological_sort.hpp` | 251 / 256 | 98.0% | 330 / 340 | 97.1% | +| `views/transpose.hpp` | 23 / 23 | 100.0% | 21 / 21 | 100.0% | +| `views/vertexlist.hpp` | 113 / 113 | 100.0% | 763 / 765 | 99.7% | + +> **Not measured** (compile-time only): `views/view_concepts.hpp`, `views/basic_views.hpp`. ### Adaptors (`adaptors/`) | File | Lines Hit / Total | Line % | Funcs Hit / Total | Func % | |------|-------------------|--------|-------------------|--------| -| `filtered_graph.hpp` | — | — | — | — | -| `bgl/graph_adaptor.hpp` | — | — | — | — | -| `bgl/bgl_edge_iterator.hpp` | — | — | — | — | -| `bgl/property_bridge.hpp` | — | — | — | — | +| `adaptors/filtered_graph.hpp` | 46 / 48 | 95.8% | 55 / 61 | 90.2% | +| `adaptors/bgl/graph_adaptor.hpp` | — | — | — | — | +| `adaptors/bgl/bgl_edge_iterator.hpp` | — | — | — | — | +| `adaptors/bgl/property_bridge.hpp` | — | — | — | — | + +> BGL adaptor headers require an external Boost installation and are excluded from automated coverage collection. + +### Other + +| File | Lines Hit / Total | Line % | Funcs Hit / Total | Func % | +|------|-------------------|--------|-------------------|--------| +| `graph_data.hpp` | 0 / 2 | 0.0% | 0 / 2 | 0.0% | -> Coverage data for adaptors not yet collected (BGL adaptor requires external Boost headers). +> `graph_data.hpp` contains only metadata type aliases with no executable paths; the 0% figure is expected. --- @@ -115,10 +164,26 @@ Files below 90% line coverage, ranked by gap size: | File | Line % | Lines Missed | Notes | |------|--------|--------------|-------| -| `graph_data.hpp` | 0.0% | 2 | Metadata-only header; no executable paths exercised | -| `bellman_ford_shortest_paths.hpp` | 85.7% | 9 | Negative-cycle detection path not fully exercised | -| `compressed_graph.hpp` | 88.8% | 25 | Some construction / accessor overloads untested | -| `mis.hpp` | 89.3% | 3 | Some branches not fully exercised | +| `io/json.hpp` | 78.1% | 42 | Error-handling and edge-case serialisation paths not fully exercised | +| `graph_data.hpp` | 0.0% | 2 | Metadata-only header; no executable paths (expected) | +| `generators/path.hpp` | 87.5% | 1 | One edge-case branch not exercised | +| `compressed_graph.hpp` | 88.2% | 26 | Some construction / accessor overloads untested | +| `mis.hpp` | 89.7% | 3 | Some branches in the randomised MIS variant not exercised | +| `io/graphml.hpp` | 90.0% | 21 | Attribute parsing edge cases not fully covered | + +--- + +## Changes Since Last Report (2026-02-23) + +| Area | Change | +|------|--------| +| Tests | +530 tests (4343 → 4873), 100% pass rate maintained | +| New files measured | `vertex_property_map.hpp`, `tarjan_scc.hpp`, `heap_position_map.hpp`, `indexed_dary_heap.hpp` | +| New sections | Generators (`generators/`), I/O (`io/`), Adaptors now has real data | +| `bellman_ford_shortest_paths.hpp` | Improved 85.7% → 98.3% (+12.6 pp) | +| `label_propagation.hpp` | Improved 95.7% → 100.0% | +| `mst.hpp` | Improved 92.9% → 96.8% | +| Total instrumented lines | +899 lines (3764 → 4663) from new files entering coverage | --- @@ -129,17 +194,17 @@ Files below 90% line coverage, ranked by gap size: cmake --preset linux-gcc-coverage # Build -cmake --build --preset linux-gcc-coverage +cmake --build build/linux-gcc-coverage -j $(nproc) # Run tests -ctest --preset linux-gcc-coverage +ctest --test-dir build/linux-gcc-coverage --output-on-failure -j $(nproc) # Generate HTML report (output in build/linux-gcc-coverage/coverage_html/) cd build/linux-gcc-coverage -lcov --directory . --capture --output-file coverage.info --ignore-errors mismatch +lcov --directory . --capture --output-file coverage.info --ignore-errors mismatch,source lcov --remove coverage.info '/usr/*' '/opt/*' '*/tests/*' '*/build/*' '*/_deps/*' \ - --output-file coverage.info --ignore-errors mismatch -genhtml coverage.info --output-directory coverage_html --ignore-errors mismatch + --output-file coverage_filtered.info --ignore-errors mismatch,source +genhtml coverage_filtered.info --output-directory coverage_html --ignore-errors mismatch,source ``` Open `build/linux-gcc-coverage/coverage_html/index.html` in a browser for the full interactive report. diff --git a/docs/status/implementation_matrix.md b/docs/status/implementation_matrix.md index 41f70d7..da1182a 100644 --- a/docs/status/implementation_matrix.md +++ b/docs/status/implementation_matrix.md @@ -13,9 +13,29 @@ --- +## Table of Contents + +- [Algorithms](#algorithms) +- [Views](#views) +- [Adaptors](#adaptors) +- [Containers](#containers) +- [dynamic\_graph Trait Combinations](#dynamic_graph-trait-combinations) +- [Generators](#generators) +- [Graph I/O](#graph-io) +- [Edge List](#edge-list) +- [Adjacency List Infrastructure](#adjacency-list-infrastructure) +- [Top-Level Headers](#top-level-headers) +- [Umbrella Headers](#umbrella-headers) +- [Namespace Organization](#namespace-organization) +- [CMake Consumer Instructions](#cmake-consumer-instructions) + +--- + ## Algorithms -14 implemented algorithms in `include/graph/algorithm/` (excluding `traversal_common.hpp`): +15 algorithm headers in `include/graph/algorithm/` (14 user-facing + 1 shared infrastructure): + +> **Note:** `tarjan_scc.hpp` is not included by the `algorithms.hpp` umbrella header — include it directly. | Algorithm | Header | Test File | Status | |-----------|--------|-----------|--------| @@ -25,7 +45,7 @@ | Depth-first search | `depth_first_search.hpp` | `test_depth_first_search.cpp` | Implemented | | Topological sort | `topological_sort.hpp` | `test_topological_sort.cpp` | Implemented | | Connected components (Kosaraju SCC) | `connected_components.hpp` | `test_connected_components.cpp`, `test_scc_bidirectional.cpp` | Implemented | -| Tarjan SCC | `tarjan_scc.hpp` | `test_tarjan_scc.cpp` | Implemented | +| Tarjan SCC | `tarjan_scc.hpp` | `test_tarjan_scc.cpp` | Implemented ⚠️ not in umbrella | | Articulation points | `articulation_points.hpp` | `test_articulation_points.cpp` | Implemented | | Biconnected components | `biconnected_components.hpp` | `test_biconnected_components.cpp` | Implemented | | MST (Prim / Kruskal) | `mst.hpp` | `test_mst.cpp` | Implemented | @@ -38,7 +58,7 @@ ## Views -10 user-facing views in `include/graph/views/` (excluding infrastructure headers): +8 user-facing view headers in `include/graph/views/`: | View | Header | Category | |------|--------|----------| @@ -51,6 +71,8 @@ | Topological sort (vertices + edges) | `topological_sort.hpp` | Search | | Transpose adaptor | `transpose.hpp` | Adaptor | +Infrastructure headers (not user-facing views): `view_concepts.hpp`, `basic_views.hpp`, `adaptors.hpp`, `search_base.hpp`, `edge_accessor.hpp`. + --- ## Adaptors @@ -66,7 +88,7 @@ Supporting headers: - `adaptors/bgl/bgl_edge_iterator.hpp` — C++20 iterator wrapper for BGL iterators - `adaptors/bgl/property_bridge.hpp` — Bridge BGL property maps to graph-v3 value functions -Infrastructure headers (not user views): `view_concepts.hpp`, `adaptors.hpp`, `basic_views.hpp`, `search_base.hpp`, `edge_accessor.hpp`. + --- @@ -165,11 +187,14 @@ Test file: `tests/generators/test_generators.cpp` 3 I/O formats in `include/graph/io/`: -| Format | Header | Writer | Reader | Description | -|--------|--------|--------|--------|-------------| -| DOT (GraphViz) | `io/dot.hpp` | `write_dot()` | `read_dot()` | Most common graph visualization format | -| GraphML (XML) | `io/graphml.hpp` | `write_graphml()` | `read_graphml()` | XML-based graph interchange | -| JSON | `io/json.hpp` | `write_json()` | `read_json()` | JGF-inspired JSON format | +| Format | Header | Writer | Reader | Reader return type | Description | +|--------|--------|--------|--------|-------------------|-------------| +| DOT (GraphViz) | `io/dot.hpp` | `write_dot()` | `read_dot()` | `dot_graph` | Most common graph visualization format | +| GraphML (XML) | `io/graphml.hpp` | `write_graphml()` | `read_graphml()` | `graphml_graph` | XML-based graph interchange | +| JSON | `io/json.hpp` | `write_json()` | `read_json()` | `json_graph` | JGF-inspired JSON format | + +> Readers return a format-specific intermediate structure (`dot_graph`, `graphml_graph`, `json_graph`) +> rather than populating a graph container directly. Callers convert the structure into a container of their choice. Shared utilities: `io/detail/common.hpp` (concepts: `formattable`, `has_vertex_value`, `has_edge_value`) @@ -177,15 +202,63 @@ Test file: `tests/io/test_io.cpp` --- +## Edge List + +3 headers in `include/graph/edge_list/`: + +| Header | Description | +|--------|-------------| +| `edge_list/edge_list_descriptor.hpp` | `edge_descriptor` — descriptor for a single (source, target, value) edge | +| `edge_list/edge_list_traits.hpp` | `edge_list_traits` — type aliases (`vertex_id_t`, `edge_value_t`, etc.) for edge-list containers | +| `edge_list/edge_list.hpp` | Umbrella: includes both of the above | + +Test files: `tests/edge_list/test_edge_list_concepts.cpp`, `test_edge_list_cpo.cpp`, `test_edge_list_descriptor.cpp`, `test_edge_list_integration.cpp` + +--- + +## Adjacency List Infrastructure + +11 headers in `include/graph/adj_list/` providing the descriptor-based CPO machinery: + +| Header | Description | +|--------|-------------| +| `adj_list/descriptor.hpp` | Core descriptor concepts, `out_edge_tag`, `in_edge_tag`, base descriptor traits | +| `adj_list/descriptor_traits.hpp` | `is_vertex_descriptor_v`, `is_edge_descriptor_v` type traits | +| `adj_list/vertex_descriptor.hpp` | `vertex_descriptor` — wraps an iterator with a `vertex_id()` method | +| `adj_list/vertex_descriptor_view.hpp` | `vertex_descriptor_view` — range adaptor that synthesizes vertex descriptors over a container | +| `adj_list/vertex_property_map.hpp` | `vertex_property_map` — O(1) per-vertex property storage (index-based graphs) | +| `adj_list/edge_descriptor.hpp` | `edge_descriptor` — wraps edge and vertex iterators with source/target access | +| `adj_list/edge_descriptor_view.hpp` | `edge_descriptor_view` — range adaptor that synthesizes edge descriptors | +| `adj_list/adjacency_list_concepts.hpp` | Concepts: `adjacency_list`, `index_adjacency_list`, `bidirectional_adjacency_list`, `mapped_adjacency_list`, etc. | +| `adj_list/adjacency_list_traits.hpp` | Type aliases: `vertex_t`, `edge_t`, `vertex_id_t`, `vertex_range_t`, etc. | +| `adj_list/graph_utility.hpp` | Utility: `num_vertices`, `num_edges`, and related convenience wrappers | +| `adj_list/detail/graph_cpo.hpp` | All CPO definitions: `vertices`, `out_edges`, `in_edges`, `find_vertex`, `source_id`, `target_id`, `vertex_id`, `edge_value`, `vertex_value`, `graph_value`, `degree`, `find_vertex_edge`, `contains_edge`, and partition CPOs | + +--- + +## Top-Level Headers + +| Header | Description | +|--------|-------------| +| `graph/graph.hpp` | Core umbrella: adj_list infrastructure, edge_list types, views, `graph_data.hpp`, `graph_concepts.hpp` | +| `graph/graph_concepts.hpp` | `vertex_value_function` and `edge_value_function` concepts used by views and algorithms | +| `graph/graph_data.hpp` | `graph_error`, `vertex_data`, `edge_data` — value aggregates returned by views | +| `graph/views.hpp` | All 8 view headers + infrastructure | +| `graph/algorithms.hpp` | 13 of 14 algorithm headers (excludes `tarjan_scc.hpp`) | +| `graph/generators.hpp` | All 4 generator headers | +| `graph/io.hpp` | All 3 I/O format headers | + +--- + ## Umbrella Headers -| Header | Includes | Status | -|--------|----------|--------| -| `graph/graph.hpp` | Core types, concepts, traits, views, containers | Verified | -| `graph/views.hpp` | All views + CPOs | Verified | -| `graph/algorithms.hpp` | All 13 algorithm headers | Verified (fixed Phase 0) | -| `graph/generators.hpp` | All 4 generator headers | Verified | -| `graph/io.hpp` | All 3 I/O format headers | Verified | +| Header | Includes | Notes | +|--------|----------|-------| +| `graph/graph.hpp` | `adj_list/` infrastructure, `edge_list/`, views, `graph_data.hpp`, `graph_concepts.hpp` | Does **not** include containers, algorithms, generators, or I/O | +| `graph/views.hpp` | All 8 view headers + infrastructure | — | +| `graph/algorithms.hpp` | 13 of 14 algorithm headers | `tarjan_scc.hpp` must be included separately | +| `graph/generators.hpp` | All 4 generator headers | — | +| `graph/io.hpp` | All 3 I/O format headers | — | --- diff --git a/docs/user-guide/adjacency-lists.md b/docs/user-guide/adjacency-lists.md index d986bc7..42e7a3b 100644 --- a/docs/user-guide/adjacency-lists.md +++ b/docs/user-guide/adjacency-lists.md @@ -74,12 +74,16 @@ All library algorithms require `adjacency_list`. Index-based containers automatically; map-based containers (`map`, `unordered_map`) satisfy `mapped_adjacency_list`. Both are accepted by every algorithm. +![Adjacency List Concept Hierarchy](../assets/adjacency_list_concepts.svg) + --- ## 3. Accessing Graph Structure — CPOs All graph operations are **Customization Point Objects (CPOs)** that resolve in three tiers: custom member → ADL free function → built-in default. +Some CPOs extend this chain; `num_vertices(g)` for example checks +`size(vertices(g))` before falling back to `size(g)`. ### Structure CPOs diff --git a/docs/user-guide/algorithms.md b/docs/user-guide/algorithms.md index 8048852..39d7dec 100644 --- a/docs/user-guide/algorithms.md +++ b/docs/user-guide/algorithms.md @@ -23,7 +23,7 @@ - [Roadmap](#roadmap) ## Overview - + All graph-v3 algorithms require a graph satisfying the `adjacency_list` concept. This supports two families of vertex storage: diff --git a/docs/user-guide/edge-lists.md b/docs/user-guide/edge-lists.md index 1b78d88..ea6f530 100644 --- a/docs/user-guide/edge-lists.md +++ b/docs/user-guide/edge-lists.md @@ -61,6 +61,8 @@ The distinction between `basic_sourced_edgelist` and ID type (including `std::string`), while the index variant requires integral IDs. Most graph algorithms require integral IDs for fast vertex lookup. +![Edge List Concept Hierarchy](../assets/edge_list_concepts.svg) + --- ## 3. Edge Patterns diff --git a/docs/user-guide/views.md b/docs/user-guide/views.md index a239ff2..fee6df5 100644 --- a/docs/user-guide/views.md +++ b/docs/user-guide/views.md @@ -16,6 +16,7 @@ - [Incoming Edge Views](#incoming-edge-views) — in_incidence, in_neighbors - [Simplified Views (basic\_)](#simplified-views-basic_) — id-only variants - [Search Views](#search-views) — DFS, BFS, topological sort +- [Non-Integral Vertex IDs](#non-integral-vertex-ids) — string-keyed and map-backed graphs - [Reverse Search (Accessor Parameter)](#reverse-search-accessor-parameter) — BFS/DFS/topo over incoming edges - [Range Adaptor Syntax](#range-adaptor-syntax) - [Value Functions](#value-functions) @@ -341,9 +342,13 @@ auto edges_dfs(G&& g, Seed seed, EVF&& evf, Alloc alloc); **Iterator category**: input (single-pass). +**Graph requirements**: satisfies the `adjacency_list` concept. Works with +any vertex descriptor type — including non-integral ids such as +`std::string` (see [Non-Integral Vertex IDs](#non-integral-vertex-ids)). + **Example**: ```cpp -// DFS from vertex 0 +// DFS from vertex 0 (index-based graph) for (auto [v] : views::vertices_dfs(g, 0)) { std::cout << "DFS visited: " << vertex_id(g, v) << "\n"; } @@ -390,9 +395,13 @@ auto edges_bfs(G&& g, Seed seed, EVF&& evf, Alloc alloc); **Iterator category**: input (single-pass). +**Graph requirements**: satisfies the `adjacency_list` concept. Works with +any vertex descriptor type — including non-integral ids such as +`std::string` (see [Non-Integral Vertex IDs](#non-integral-vertex-ids)). + **Example**: ```cpp -// BFS from vertex 0 +// BFS from vertex 0 (index-based graph) for (auto [v] : views::vertices_bfs(g, 0)) { std::cout << "BFS level order: " << vertex_id(g, v) << "\n"; } @@ -441,6 +450,11 @@ auto edges_topological_sort_safe(G&& g); // returns tl::expected` bitset | O(V) bits, O(1) ops | +| Descriptor with `.value()` returning integral (e.g. vector-backed iterator) | `std::vector` bitset | O(V) bits, O(1) ops | +| Anything else (e.g. map/unordered_map iterator, `std::string` key) | `std::unordered_set` | O(visited) space, O(1) amortised ops | + +For topological sort, the recursion stack used by cycle detection (`_safe` +variants) uses the same adaptive strategy. + +**Example — BFS on a string-keyed graph**: +```cpp +#include +#include +#include + +using namespace graph; +using namespace graph::views; + +// unordered_map vertices, list<…> edges +using StrGraph = dynamic_adjacency_graph< + uol_graph_traits>; + +StrGraph g; +auto a = find_vertex(g, "a"); +auto b = find_vertex(g, "b"); +auto c = find_vertex(g, "c"); +// ... add edges ... + +// Seed is a vertex descriptor — no integer id required +for (auto [v] : vertices_bfs(g, a)) { + std::cout << vertex_id(g, v) << "\n"; // prints string keys } ``` +**Example — cycle detection on a string-keyed DAG**: +```cpp +auto result = vertices_topological_sort_safe(g); +if (!result) { + // result.error() is a vertex descriptor; use vertex_id() to get the key + std::cerr << "Cycle at: " << vertex_id(g, result.error()) << "\n"; +} +``` + +> **Note:** The seed for BFS and DFS can be either a vertex descriptor *or* +> a vertex id. For string-keyed graphs, pass a descriptor obtained from +> `find_vertex()` or directly from a prior iteration. + +--- + ## Reverse Search (Accessor Parameter) All search views (BFS, DFS, topological sort) accept an **Accessor** template @@ -678,12 +753,12 @@ auto dfs = g | vertices_dfs(0); // Allocates visited tracker | incidence | O(1) | References graph | | neighbors | O(1) | References graph | | edgelist | O(1) | References graph | -| vertices_dfs | O(V) | Visited tracker + stack | -| edges_dfs | O(V) | Visited tracker + stack | -| vertices_bfs | O(V) | Visited tracker + queue | -| edges_bfs | O(V) | Visited tracker + queue | -| vertices_topological_sort | O(V) | Visited tracker + result | -| edges_topological_sort | O(V) | Visited tracker + result | +| vertices_dfs | O(V) | Visited tracker (bitset or hash set) + stack | +| edges_dfs | O(V) | Visited tracker (bitset or hash set) + stack | +| vertices_bfs | O(V) | Visited tracker (bitset or hash set) + queue | +| edges_bfs | O(V) | Visited tracker (bitset or hash set) + queue | +| vertices_topological_sort | O(V) | Visited tracker + post-order result vector | +| edges_topological_sort | O(V) | Visited tracker + post-order result vector | | in_incidence | O(1) | References graph | | in_neighbors | O(1) | References graph | diff --git a/examples/AdaptingThirdPartyGraph/adapting_a_third_party_graph.cpp b/examples/AdaptingThirdPartyGraph/adapting_a_third_party_graph.cpp new file mode 100644 index 0000000..24bdfa6 --- /dev/null +++ b/examples/AdaptingThirdPartyGraph/adapting_a_third_party_graph.cpp @@ -0,0 +1,154 @@ +// 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 example demonstrates how to adapt an existing third-party graph container +// for use with the graph-v3 library using Customization Point Objects (CPOs). +// +// graph-v3 dispatches all graph operations (vertex iteration, edge access, ID +// extraction, …) through CPOs rather than through a traits specialisation or +// virtual functions. A type satisfies graph::adjacency_list once a small +// set of ADL-findable free functions has been provided in the type's own +// namespace. No changes to the third-party type itself are required. +// +// The four functions below are all that is needed: +// +// vertices(g) — range of vertices, wrapped into a vertex_descriptor_view +// edges(g, u) — out-edge range for a vertex_descriptor u +// target_id(g, uv) — target vertex ID from an edge_descriptor uv +// vertex_value(g, u) — user-visible vertex data (MyVertex) for a vertex_descriptor u +// +// The trailing-return-type SFINAE pattern keeps the templates narrow: they are +// excluded from overload resolution automatically when the argument does not +// have the expected interface (e.g. when u is a plain integer rather than a +// vertex_descriptor, or uv is not an edge_descriptor). + +#include // vertices, out_edges, vertex_value, vertexlist, + // vertices_dfs, index_adjacency_list, … +#include +#include +#include +#include + + +namespace MyLibrary { // ── Third-party graph container (left unchanged) ──────── + +struct MyEdge { + std::string content; + int indexOfTarget; +}; + +struct MyVertex { + std::string content; + std::vector outEdges; +}; + +class MyGraph { + std::vector _vertices; + +public: + MyVertex const* getVertexByIndex(int index) const { return &_vertices[static_cast(index)]; } + + std::vector const& getAllVertices() const { return _vertices; } + + void setTopology(std::vector t) { _vertices = std::move(t); } +}; + +} // namespace MyLibrary + + +namespace MyLibrary { // ── graph-v3 CPO customisation (unintrusive) ──────────── + +// --------------------------------------------------------------------------- +// vertices(g) +// Returns a range over all vertices. Because std::vector::const_iterator +// is a random-access iterator, the CPO wraps the result in a +// vertex_descriptor_view whose storage_type is size_t. Vertex IDs are +// therefore contiguous integral indices, and find_vertex / num_vertices +// work via their built-in random-access defaults — no extra code needed. +// --------------------------------------------------------------------------- +auto vertices(const MyGraph& g) { + return std::views::all(g.getAllVertices()); +} + +// --------------------------------------------------------------------------- +// edges(g, u) — u is a graph-v3 vertex_descriptor (stores a size_t index) +// The trailing return type makes the function SFINAE-friendly: it is +// excluded from overload resolution for argument types that lack vertex_id() +// (e.g. a plain size_t), so the CPO's built-in default path (which calls +// find_vertex then edges(g, descriptor)) remains available. +// The CPO wraps the returned range in an edge_descriptor_view automatically. +// --------------------------------------------------------------------------- +template +auto edges(const MyGraph& g, const VDesc& u) + -> decltype(std::views::all(g.getAllVertices()[u.vertex_id()].outEdges)) { + return std::views::all(g.getAllVertices()[u.vertex_id()].outEdges); +} + +// --------------------------------------------------------------------------- +// target_id(g, uv) — uv is a graph-v3 edge_descriptor +// uv.value() returns an iterator to the underlying MyEdge. Dereferencing +// it gives the MyEdge from which we read indexOfTarget. +// The trailing return type restricts the function to edge descriptors that +// expose a value() iterator (SFINAE). +// --------------------------------------------------------------------------- +template +auto target_id(const MyGraph&, const EdgeDesc& uv) + -> decltype((*uv.value()).indexOfTarget) { + return (*uv.value()).indexOfTarget; +} + +// --------------------------------------------------------------------------- +// vertex_value(g, u) — u is a graph-v3 vertex_descriptor +// Returns the MyVertex data for use in display or further inspection. +// The default CPO path (u.inner_value(g)) would call MyGraph::operator[], +// which MyGraph does not provide, so we supply this ADL override instead. +// --------------------------------------------------------------------------- +template +auto vertex_value(const MyGraph& g, const VDesc& u) + -> decltype(g.getAllVertices()[u.vertex_id()]) { + return g.getAllVertices()[u.vertex_id()]; +} + +} // namespace MyLibrary + + +int main() { + // Verify concept satisfaction at compile time. + // index_adjacency_list is required by vertices_dfs (integral vertex IDs, + // random-access vertex storage). + static_assert(graph::adjacency_list); + static_assert(graph::index_adjacency_list); + + const MyLibrary::MyGraph g = []{ // ── build the graph ─────────────────── + MyLibrary::MyGraph r; + std::vector topo{ + // A | + /*0*/ {"A", {{"", 1}, {"", 2}}}, // / \ | + /*1*/ {"B", {{"", 3}}}, // B C | + /*2*/ {"C", {{"", 3}}}, // \ / | + /*3*/ {"D", {}} // D | + }; + r.setTopology(std::move(topo)); + return r; + }(); + + // DFS traversal starting from vertex 0. + // + // vertices_dfs yields one vertex_descriptor per visited vertex via a + // structured binding [v]. graph::vertex_value(g, v) fetches the + // associated MyVertex so we can access its 'content' field. + // + // graph-v3 v2 used [vid, v] where v was a raw vertex reference. + // In v3, v is an opaque vertex_descriptor; content is accessed via the + // vertex_value CPO. + for (auto [v] : graph::views::vertices_dfs(g, std::size_t{0})) + std::cout << graph::vertex_value(g, v).content << " "; + + std::cout << "\n"; + + // Expected output: A B D C (or A C D B, depending on DFS tie-breaking) + return 0; +} diff --git a/examples/BGLWorkshop2026/CMakeLists.txt b/examples/BGLWorkshop2026/CMakeLists.txt new file mode 100644 index 0000000..6c18243 --- /dev/null +++ b/examples/BGLWorkshop2026/CMakeLists.txt @@ -0,0 +1,53 @@ +# BGL Workshop 2026 examples + +set(BGLWS_INCLUDE_DIRS + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/graphs) + +add_executable(bglws_bacon bacon_gv3.cpp) +target_link_libraries(bglws_bacon PRIVATE graph3) +target_include_directories(bglws_bacon PRIVATE ${BGLWS_INCLUDE_DIRS}) +target_compile_features(bglws_bacon PRIVATE cxx_std_20) +target_compile_definitions(bglws_bacon PRIVATE + BGLWS_OUTPUT_DIR="${CMAKE_CURRENT_SOURCE_DIR}/output") + +add_executable(bglws_france_routes france_routes_gv3.cpp) +target_link_libraries(bglws_france_routes PRIVATE graph3) +target_include_directories(bglws_france_routes PRIVATE ${BGLWS_INCLUDE_DIRS}) +target_compile_features(bglws_france_routes PRIVATE cxx_std_20) +target_compile_definitions(bglws_france_routes PRIVATE + BGLWS_DATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/graphs" + BGLWS_OUTPUT_DIR="${CMAKE_CURRENT_SOURCE_DIR}/output") + +# BGL Bacon example — requires Boost headers (header-only, no linking needed). +# Resolution order: BGL_INCLUDE_DIR cache var → $BGL_INCLUDE_DIR env → $BOOST_ROOT env → ~/dev_graph/boost. +if(NOT DEFINED _bglws_bgl_inc) + set(_bglws_bgl_inc "") + foreach(_c "${BGL_INCLUDE_DIR}" "$ENV{BGL_INCLUDE_DIR}" "$ENV{BOOST_ROOT}" + "$ENV{HOME}/dev_graph/boost" "/usr/local/include" "/usr/include") + if(_c AND EXISTS "${_c}/boost/graph/adjacency_list.hpp") + set(_bglws_bgl_inc "${_c}") + break() + endif() + endforeach() +endif() + +if(_bglws_bgl_inc) + message(STATUS "BGLWorkshop2026: BGL headers at ${_bglws_bgl_inc}") + add_executable(bglws_bacon_bgl bacon_bgl.cpp) + target_link_libraries(bglws_bacon_bgl PRIVATE graph3) + target_include_directories(bglws_bacon_bgl PRIVATE ${BGLWS_INCLUDE_DIRS} ${_bglws_bgl_inc}) + target_compile_features(bglws_bacon_bgl PRIVATE cxx_std_20) + target_compile_definitions(bglws_bacon_bgl PRIVATE + BGLWS_OUTPUT_DIR="${CMAKE_CURRENT_SOURCE_DIR}/output") + + add_executable(bglws_france_routes_bgl france_routes_bgl.cpp) + target_link_libraries(bglws_france_routes_bgl PRIVATE graph3) + target_include_directories(bglws_france_routes_bgl PRIVATE ${BGLWS_INCLUDE_DIRS} ${_bglws_bgl_inc}) + target_compile_features(bglws_france_routes_bgl PRIVATE cxx_std_20) + target_compile_definitions(bglws_france_routes_bgl PRIVATE + BGLWS_DATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/graphs" + BGLWS_OUTPUT_DIR="${CMAKE_CURRENT_SOURCE_DIR}/output") +else() + message(STATUS "BGLWorkshop2026: Boost not found — skipping bglws_bacon_bgl (set BGL_INCLUDE_DIR)") +endif() diff --git a/examples/BGLWorkshop2026/bacon_bgl.cpp b/examples/BGLWorkshop2026/bacon_bgl.cpp new file mode 100644 index 0000000..4f486b5 --- /dev/null +++ b/examples/BGLWorkshop2026/bacon_bgl.cpp @@ -0,0 +1,252 @@ +// bacon_bgl.cpp — Bacon number examples using Boost Graph Library (BGL). +// +// bacon1_bgl: mirrors bacon1 from bacon_gv3.cpp. +// - boost::adjacency_list — actors only, integral vertex ids +// - boost::breadth_first_search with DepthRecorder visitor +// - Bacon number = BFS depth (direct actor↔actor graph, no bipartite step) +// +// bacon2_bgl: mirrors bacon2 from bacon_gv3.cpp. +// - boost::adjacency_list (undirectedS + vertex bundle) — bipartite movie↔actor graph +// - boost::breadth_first_search with a custom visitor for BFS depths +// - manual DOT output (same visual style as bacon_gv3) +// - Bacon number = depth / 2 (bipartite path: actor → movie → actor) +// +// Example: dot -Tjpeg output/bacon2_bgl.dot -o output/bacon2_bgl.jpg + +#include "graphs/imdb-graph.hpp" + +#include +#include + +#include "graph/io/dot.hpp" // for graph::io::detail::dot_escape + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using std::cout; +using std::string; +using std::vector; +using namespace std::string_literals; + +// ── bacon1_bgl ─────────────────────────────────────────────────────────────── +// Actors-only graph with integral vertex ids; direct BFS gives Bacon numbers. +// Uses the same hardcoded costars adjacency list as bacon1 in bacon_gv3.cpp. +namespace bacon1_bgl { + +using vid_t = std::size_t; +// Undirected actor-actor graph; each co-star pair stored once. +using graph_t = boost::adjacency_list; +using edge_desc_t = boost::graph_traits::edge_descriptor; + +// BGL visitor: depth[v] = depth[u] + 1 on each tree edge. +class DepthRecorder : public boost::default_bfs_visitor { +public: + explicit DepthRecorder(vector& depth) : depth_(depth) {} + void tree_edge(edge_desc_t e, const graph_t& g) const { + depth_[boost::target(e, g)] = depth_[boost::source(e, g)] + 1; + } +private: + vector& depth_; +}; + +void run() { + constexpr vid_t infinity = std::numeric_limits::max(); + + // Same costars adjacency list as bacon1 in bacon_gv3.cpp. + // costars[actor_id] = list of co-star actor ids (actors only, no movies). + const vector> costars{ + {1, 5, 6}, {7, 10, 0, 5, 12}, {4, 3, 11}, {2, 11}, {8, 9, 2, 12}, {0, 1}, + {7, 0}, {6, 1, 10}, {4, 9}, {4, 8}, {7, 1}, {2, 3}, + {1, 4}}; + + // Build undirected BGL graph from costars. + const vid_t n = static_cast(actors.size()); + graph_t g(n); + for (vid_t u = 0; u < n; ++u) + for (vid_t v : costars[u]) + if (u < v) // add each undirected edge once + boost::add_edge(u, v, g); + + // BFS from Kevin Bacon. + const vid_t seed = static_cast(std::ranges::find(actors, "Kevin Bacon") - actors.begin()); + vector bacon_number(n, infinity); + bacon_number[seed] = 0; + boost::breadth_first_search(g, seed, boost::visitor(DepthRecorder{bacon_number})); + + for (vid_t uid = 0; uid < n; ++uid) { + if (bacon_number[uid] == infinity) + cout << actors[uid] << " has Bacon number infinity\n"; + else + cout << actors[uid] << " has Bacon number " << bacon_number[uid] << '\n'; + } +} + +} // namespace bacon1_bgl + +namespace bacon2_bgl { + +// ── Graph types ─────────────────────────────────────────────────────────────── + +enum class vertex_kind { movie, actor }; + +struct VertexProps { + string name; + vertex_kind kind; +}; + +// undirectedS: each edge stored once, traversable in both directions. +// vecS for vertex/edge containers: vertex_descriptor is a plain size_t. +using graph_t = boost::adjacency_list; +using vertex_desc_t = boost::graph_traits::vertex_descriptor; +using edge_desc_t = boost::graph_traits::edge_descriptor; + +// ── load() ──────────────────────────────────────────────────────────────────── + +struct load_result { + graph_t g; + std::unordered_map id_map; // name → vertex descriptor +}; + +// Build the bipartite movie↔actor graph from the global imdb-graph.hpp data. +load_result load() { + graph_t g; + std::unordered_map id_map; + + for (const auto& title : movies) { + auto vd = boost::add_vertex(VertexProps{title, vertex_kind::movie}, g); + id_map[title] = vd; + } + for (const auto& name : actors) { + auto vd = boost::add_vertex(VertexProps{name, vertex_kind::actor}, g); + id_map[name] = vd; + } + // Add one undirected edge per movies_actors tuple (no reverse needed). + for (const auto& [movie, actor] : movies_actors) + boost::add_edge(id_map.at(movie), id_map.at(actor), g); + + return {std::move(g), std::move(id_map)}; +} + +// ── eval() — BFS depth ──────────────────────────────────────────────────────── + +struct eval_result { + graph_t g; + std::unordered_map id_map; + vector depth; // indexed by vertex_descriptor (vecS → size_t) +}; + +// BGL visitor: records BFS tree depth by propagating parent depth + 1. +// tree_edge() is called exactly once per discovered vertex (when the edge +// that first reaches it is relaxed). +class DepthRecorder : public boost::default_bfs_visitor { +public: + explicit DepthRecorder(vector& depth) : depth_(depth) {} + + void tree_edge(edge_desc_t e, const graph_t& g) const { + const vertex_desc_t u = boost::source(e, g); + const vertex_desc_t v = boost::target(e, g); + depth_[v] = depth_[u] + 1; + } + +private: + vector& depth_; +}; + +// Build the graph and run BFS from "Kevin Bacon". +// Depths are bipartite distances: actor Bacon number = depth / 2. +eval_result eval() { + auto [g, id_map] = load(); + + constexpr std::size_t inf = std::numeric_limits::max(); + vector depth(boost::num_vertices(g), inf); + + vertex_desc_t source = id_map.at("Kevin Bacon"); + depth[source] = 0; + + boost::breadth_first_search(g, source, boost::visitor(DepthRecorder{depth})); + + return {std::move(g), std::move(id_map), std::move(depth)}; +} + +// ── run() — print Bacon numbers ─────────────────────────────────────────────── + +void run() { + constexpr std::size_t inf = std::numeric_limits::max(); + auto [g, id_map, depth] = eval(); + + for (const string& actor : actors) { + std::size_t d = depth[id_map.at(actor)]; + if (d == inf) + cout << actor << " has Bacon number infinity\n"; + else + cout << actor << " has Bacon number " << (d / 2) << '\n'; + } +} + +// ── dot() — write Graphviz DOT ──────────────────────────────────────────────── +// Same visual style as bacon_gv3: actors = blue ellipses with Bacon number, +// movies = yellow boxes. Because the BGL graph is undirected, boost::edges() +// returns each edge exactly once — no deduplication set needed. + +void dot(const string& filename = BGLWS_OUTPUT_DIR "/bacon2_bgl.dot") { + constexpr std::size_t inf = std::numeric_limits::max(); + auto [g, id_map, depth] = eval(); + + std::ofstream out(filename); + if (!out) + throw std::runtime_error("Cannot open: " + filename); + + out << "graph BaconNumbers_BGL {\n"; + + // Vertices + for (auto [vit, vend] = boost::vertices(g); vit != vend; ++vit) { + const vertex_desc_t vd = *vit; + const VertexProps& props = g[vd]; + const string esc = graph::io::detail::dot_escape(props.name); + + if (props.kind == vertex_kind::actor) { + const string bn = (depth[vd] == inf) ? "inf"s : std::to_string(depth[vd] / 2); + out << " " << std::quoted(props.name) + << std::format(" [shape=ellipse, style=filled, fillcolor=lightblue, label=\"{}\\n({})\"]", + esc, bn) + << ";\n"; + } else { + out << " " << std::quoted(props.name) + << std::format(" [shape=box, style=filled, fillcolor=lightyellow, label=\"{}\"]", esc) + << ";\n"; + } + } + + // Edges — undirectedS guarantees each edge appears once in boost::edges(). + for (auto [eit, eend] = boost::edges(g); eit != eend; ++eit) { + const string& sn = g[boost::source(*eit, g)].name; + const string& tn = g[boost::target(*eit, g)].name; + out << " " << std::quoted(sn) << " -- " << std::quoted(tn) << ";\n"; + } + + out << "}\n"; + cout << "Wrote " << filename << '\n'; +} + +} // namespace bacon2_bgl + +// ── main ────────────────────────────────────────────────────────────────────── + +int main() { + cout << "\n--- bacon1_bgl ---\n"; + bacon1_bgl::run(); + + cout << "\n--- bacon2_bgl ---\n"; + bacon2_bgl::run(); + + cout << "\n--- bacon2_bgl dot ---\n"; + bacon2_bgl::dot(); +} diff --git a/examples/BGLWorkshop2026/bacon_gv3.cpp b/examples/BGLWorkshop2026/bacon_gv3.cpp new file mode 100644 index 0000000..c0da611 --- /dev/null +++ b/examples/BGLWorkshop2026/bacon_gv3.cpp @@ -0,0 +1,435 @@ +#include "graph/graph.hpp" +#include "graph/algorithm/breadth_first_search.hpp" +#include "graph/views/bfs.hpp" +#include "graph/container/dynamic_graph.hpp" +#include "graph/container/traits/uol_graph_traits.hpp" +#include "graph/container/traits/mol_graph_traits.hpp" +#include "imdb-graph.hpp" + +#include "graph/io/dot.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using std::cout; +using std::vector; +using std::string; +using namespace std::ranges; +using namespace std::string_literals; +using namespace std::string_view_literals; +using namespace graph; + +// Example dot conversion to jpeg: dot -Tjpeg output/bacon2.dot -o output/bacon2.jpg + +// ── bacon1 ────────────────────────────────────────────────────────────────── +// Simple BFS on an unweighted graph with integral vertex ids. +// costars[actor_id] = vector of pre-computed co-star actor_ids. +namespace bacon1 { + +void run() { + // actors only + using vid_t = size_t; // vertex id type + constexpr vid_t infinity = std::numeric_limits::max(); + + using G = vector>; + G costars{{1, 5, 6}, {7, 10, 0, 5, 12}, {4, 3, 11}, {2, 11}, {8, 9, 2, 12}, {0, 1}, + {7, 0}, {6, 1, 10}, {4, 9}, {4, 8}, {7, 1}, {2, 3}, + {1, 4}}; + + const vid_t seed = static_cast(std::ranges::find(actors, "Kevin Bacon") - begin(actors)); + vector bacon_number(size(actors), infinity); + bacon_number[seed] = 0; + + // Evaluate Bacon numbers via BFS from source. The graph is unweighted, so the shortest + for (auto&& [uv] : graph::views::edges_bfs(costars, seed)) { + auto uid = source_id(costars, uv); + auto vid = target_id(costars, uv); + bacon_number[vid] = bacon_number[uid] + 1; + } + + // Print results. Bacon number is the length of the shortest path from the seed actor to each actor. + for (vid_t uid = 0; uid < size(actors); ++uid) { + if (bacon_number[uid] == infinity) + cout << actors[uid] << " has Bacon number infinity\n"; + else + cout << actors[uid] << " has Bacon number " << bacon_number[uid] << '\n'; + } +} + +// Same as run() but uses graph::breadth_first_search with a visitor instead of the +// edges_bfs range view. The visitor's on_examine_edge callback fires for every edge +// examined; guard on infinity ensures only the first (tree) edge sets the depth. + +// Visitor for run2(): on_examine_edge fires for every edge (u → v) before the +// visited check, so guarding on infinity replicates BGL's on_tree_edge behaviour. +// Declared at namespace scope so the template member is legal. +using costars_t = vector>; +struct BaconDepthVisitor { + vector& depth; + template + void on_examine_edge(const G& g, const E& uv) { + auto uid = source_id(g, uv); + auto vid = target_id(g, uv); + if (depth[vid] == std::numeric_limits::max()) + depth[vid] = depth[uid] + 1; + } +}; + +void run1b() { + using vid_t = size_t; + constexpr vid_t infinity = std::numeric_limits::max(); + const vid_t seed = static_cast(std::ranges::find(actors, "Kevin Bacon") - begin(actors)); + + using G = vector>; + G costars{{1, 5, 6}, {7, 10, 0, 5, 12}, {4, 3, 11}, {2, 11}, {8, 9, 2, 12}, {0, 1}, + {7, 0}, {6, 1, 10}, {4, 9}, {4, 8}, {7, 1}, {2, 3}, + {1, 4}}; + + vector bacon_number(size(actors), infinity); + bacon_number[seed] = 0; + + graph::breadth_first_search(costars, seed, BaconDepthVisitor{bacon_number}); + + for (vid_t uid = 0; uid < size(actors); ++uid) { + if (bacon_number[uid] == infinity) + cout << actors[uid] << " has Bacon number infinity\n"; + else + cout << actors[uid] << " has Bacon number " << bacon_number[uid] << '\n'; + } +} + +} // namespace bacon1 + +// ── bacon2 ────────────────────────────────────────────────────────────────── +// Bipartite movie↔actor graph with string vertex ids and unordered_map-based storage. +namespace bacon2 { + +// This outputs the bacon2 graph contents for debugging +#if 0 + // Show the bipartite structure: each vertex with its kind and neighbors. + for (auto&& u : vertices(g)) { + const auto& uid = vertex_id(g, u); + const auto& uval = vertex_value(g, u); + const char* kind = (uval == vertex_kind::movie) ? "movie" : "actor"; + cout << '[' << kind << "] " << uid << '\n'; + for (auto&& uv : edges(g, u)) { + cout << " -> " << target_id(g, uv) << '\n'; + } + } + cout << '\n'; +#endif + +// Shared types: defined at namespace scope so both run() and dot() share the +// same graph type without repeating the template arguments. + +enum class vertex_kind { movie, actor }; +using traits_t = graph::container::uol_graph_traits; +using graph_t = graph::container::dynamic_adjacency_graph; + +// Builds the bipartite movie↔actor graph used by bacon2() and bacon2_dot(). +// Both directions of each movies_actors edge are loaded so a BFS starting +// from an actor can reach co-stars via the movies they share. +graph_t load() { + using namespace graph::container; + using vertex_data_t = copyable_vertex_t; + using edge_data_t = copyable_edge_t; + + graph_t g; + + // Load all movie and actor vertices. + g.load_vertices(movies, [](const string& title) -> vertex_data_t { return {title, vertex_kind::movie}; }); + g.load_vertices(actors, [](const string& name) -> vertex_data_t { return {name, vertex_kind::actor}; }); + + // Edges from movies_actors. + // Add both directions so a BFS from an actor can reach co-stars via the movies they share. + g.load_edges(movies_actors, [](const auto& e) -> edge_data_t { auto& [title, name] = e; return {title, name}; }); + g.load_edges(movies_actors, [](const auto& e) -> edge_data_t { auto& [title, name] = e; return {name, title}; }); + return g; +} + +// Holds the loaded graph and the BFS depth map returned by eval(). +struct eval_result { + graph_t g; + std::unordered_map depth; // key = vertex id (string); value = BFS depth +}; + +// Builds the graph, runs BFS from "Kevin Bacon", and returns both. +// Depth is keyed by vertex id (string) so it works from const graph references. +// Because the graph is bipartite (actor <-> movie), each co-star step traverses +// two edges (actor -> movie -> actor); an actor's Bacon number is depth / 2. +eval_result eval() { + using G = graph_t; + G g = load(); + + // Compute Bacon numbers via the edges_bfs view from "Kevin Bacon". + // edges_bfs dispatches its visited tracker on the id type — + // std::unordered_set for non-integral ids, std::vector for integral ids. + vertex_t seed = *find_vertex(g, "Kevin Bacon"s); + std::unordered_map depth; + depth[vertex_id(g, seed)] = 0; + for (auto&& [uv] : graph::views::edges_bfs(g, seed)) { + const vertex_id_t& u_id = source_id(g, uv); // const string& — stable ref to unordered_map key + depth[target_id(g, uv)] = depth[u_id] + 1; + } + return {std::move(g), std::move(depth)}; +} + +// sparse & non-integral vertex id +// bipartite graph: movies <-> actors; edges link each actor to every film they appeared in (and back) +void run() { + auto [g, depth] = eval(); + + // Print results. Bacon number is the length of the shortest path from the seed actor + // to each actor, divided by 2 for the bipartite graph. + for (const string& a : actors) { + auto it = depth.find(a); + if (it == depth.end()) + cout << a << " has Bacon number infinity\n"; + else + cout << a << " has Bacon number " << (it->second / 2) << '\n'; + } +} + +// bipartite graph with string vertex ids — loads the same graph as run() and +// writes it to a Graphviz DOT file. Vertices are coloured by kind (movie/actor) +// and actors are labelled with their Bacon number. +// Edges are deduplicated so the bidirectional pairs collapse to single undirected lines. +void dot(const string& filename = BGLWS_OUTPUT_DIR "/bacon2.dot") { + auto [g, depth] = eval(); + + std::ofstream out(filename); + if (!out) + throw std::runtime_error("Cannot open: " + filename); + + out << "graph BaconNumbers {\n"; + + // Vertices: actors as blue ellipses with Bacon number; movies as yellow boxes. + for (auto u : vertices(g)) { + const string& uid = vertex_id(g, u); + bool is_actor = (vertex_value(g, u) == vertex_kind::actor); + const string esc = graph::io::detail::dot_escape(uid); + if (is_actor) { + auto it_d = depth.find(uid); + string bn = (it_d != depth.end()) ? std::to_string(it_d->second / 2) : "inf"; + out << " " << std::quoted(uid) + << std::format(" [shape=ellipse, style=filled, fillcolor=lightblue, label=\"{}\\n({})\"", + esc, bn) + << "];\n"; + } else { + out << " " << std::quoted(uid) + << std::format(" [shape=box, style=filled, fillcolor=lightyellow, label=\"{}\"]", esc) + << ";\n"; + } + } + + // Edges: emit each undirected pair once using min/max on string keys. + std::set> emitted; + for (auto [sid, tid, uv] : graph::views::edgelist(g)) { + auto key = (sid < tid) ? std::make_pair(sid, tid) : std::make_pair(tid, sid); + if (emitted.insert(key).second) + out << " " << std::quoted(sid) << " -- " << std::quoted(tid) << ";\n"; + } + + out << "}\n"; + cout << "Wrote " << filename << '\n'; +} + +} // namespace bacon2 + +// ── Domain types for namespace bacon3 ──────────────────────────────────────── +// Defined at file scope (not inside namespace bacon3) so that std::hash can be +// specialised — std can only be extended at namespace scope. + +class movie { +public: + movie() = default; // required by graph vertex storage (value_ = value_type()) + movie(const string& title) : title_(title) {} + const string& title() const { return title_; } + // Enables std::variant::operator< and std::hash + auto operator<=>(const movie& o) const { return title_ <=> o.title_; } + bool operator==(const movie& o) const { return title_ == o.title_; } +protected: + string title_; +}; + +class actor { +public: + actor() = default; + actor(const string& name) : name_(name) {} + const string& name() const { return name_; } + auto operator<=>(const actor& o) const { return name_ <=> o.name_; } + bool operator==(const actor& o) const { return name_ == o.name_; } +protected: + string name_; +}; + +namespace std { +template <> +struct hash { + size_t operator()(const movie& m) const noexcept { + return hash{}(m.title()); + } +}; +template <> +struct hash { + size_t operator()(const actor& a) const noexcept { + return hash{}(a.name()); + } +}; +} // namespace std + +// ── bacon3 ────────────────────────────────────────────────────────────────── +// Bipartite movie↔actor graph using a std::variant as the vertex id. +namespace bacon3 { + +void run() { + using namespace graph::container; + + // movie and actor are defined at file scope (not inside this namespace) so that std::hash + // can be specialised — a requirement of the BFS visited tracker when + // variant_t is used as the vertex id with mol_graph_traits. + + // ── Design Options ────────────────────────────────────────────────────────── + // + // movies_actors encodes a bipartite relationship between movies and actors. + // Two independent choices produce four concrete designs: + // + // Topology: Bipartite — movies AND actors are vertices; edges link each + // actor to every film they appeared in (and back). + // bacon_number = BFS depth / 2. + // Co-star — actors only; an edge links two actors who share a + // film and carries the movie object as its value. + // bacon_number = BFS depth directly. + // + // Vertex id: string — human-readable name; uol_graph_traits + // (unordered_map); visited tracker → unordered_set. + // size_t — dense integer; vector bitset tracker; + // fastest BFS; bipartite needs offset encoding. + // variant_t — domain object IS the key; mol_graph_traits + // (std::map); requires operator<=> on movie/actor. + // Movies sort before actors (by variant index). + // + // ┌──────┬──────────────────────────┬──────────────┬──────────────────────────────────────┐ + // │ │ Topology │ Vertex id │ Graph trait / notes │ + // ├──────┼──────────────────────────┼──────────────┼──────────────────────────────────────┤ + // │ 1 │ Bipartite (movie↔actor) │ string │ uol_graph_traits (unordered_map+list)│ + // │ 2 │ Bipartite (movie↔actor) │ size_t │ vol_graph_traits (vector+list) │ + // │ 3 │ Bipartite (movie↔actor) │ variant_t │ mol_graph_traits (map+list) ◄──here │ + // │ 4 │ Co-star (actor↔actor) │ string │ uol_graph_traits │ + // │ 5 │ Co-star (actor↔actor) │ size_t │ vol_graph_traits │ + // └──────┴──────────────────────────┴──────────────┴──────────────────────────────────────┘ + + // ── variant_t as vertex id ────────────────────────────────────────────────── + // + // std::variant::operator< is defined by the standard (C++17): + // 1. A valueless variant is less than any non-valueless variant. + // 2. If indices differ, the variant with the lower index is less. + // 3. If indices match, the contained values are compared with <. + // + // So monostate < movie < actor (by index order), and within each alternative + // objects are compared by title/name respectively via the operator<=> above. + // + // mol_graph_traits uses std::map which requires only + // operator< on VId — no hash needed. Because variant_t is now the key, + // vertex_value_type can be void: the domain object is already the key. + // + // Trade-off vs. string key: + // + find_vertex(g, movie{"Top Gun"}) is a single O(log V) map lookup. + // + No string↔object mapping needed; the variant directly identifies + // whether a vertex is a movie or an actor. + // − Requires operator<=> on the domain classes (non-intrusive alternatives + // are also possible — see the custom comparator note below). + // − std::map lookup (O(log V)) is slower than unordered_map (O(1) avg). + // + // Custom comparator alternative (no changes to movie/actor): + // If modifying the domain classes is not allowed, mol_graph_traits cannot + // be used as-is (it passes VId directly to std::map with default less<>). + // Instead, a specialisation of std::less or a custom Traits + // struct that parameterises the map comparator would be required. + // That is straightforward but out-of-scope here since the member variables + // (and by extension the class names) are retained and operator<=> only + // adds a public interface without changing the member layout. + + // std::monostate is still listed first for default-constructibility in + // internal graph storage, but in practice every vertex holds movie or actor. + using variant_t = std::variant; + + using traits3 = mol_graph_traits; // not bidirectional + using G3 = dynamic_adjacency_graph; + + // Type aliases for load_vertices / load_edges + using VD3 = copyable_vertex_t; + using ED3 = copyable_edge_t; + + G3 g; + // Load movie vertices — each variant_t{movie{title}} is the map key. + g.load_vertices(movies, [](const string& m) -> VD3 { + return {variant_t{movie{m}}}; + }); + // Load actor vertices. + g.load_vertices(actors, [](const string& a) -> VD3 { + return {variant_t{actor{a}}}; + }); + // Bipartite edges (both directions) from movies_actors. + g.load_edges(movies_actors, [](const auto& e) -> ED3 { + return {variant_t{movie{std::get<0>(e)}}, variant_t{actor{std::get<1>(e)}}}; // movie → actor + }); + g.load_edges(movies_actors, [](const auto& e) -> ED3 { + return {variant_t{actor{std::get<1>(e)}}, variant_t{movie{std::get<0>(e)}}}; // actor → movie + }); + + // BFS from Kevin Bacon — seed is a vertex descriptor found by variant_t key. + using VDesc = vertex_t; + std::map depth; // map because VDesc needs operator< + + VDesc seed = *find_vertex(g, variant_t{actor{"Kevin Bacon"}}); + depth[seed] = 0; + + for (auto&& [uv] : graph::views::edges_bfs(g, seed)) { + auto u = *find_vertex(g, source_id(g, uv)); + auto v = *find_vertex(g, target_id(g, uv)); + depth[v] = depth[u] + 1; + } + + // Print results — extract the actor name from the variant key. + for (const auto& a : actors) { + auto it = depth.find(*find_vertex(g, variant_t{actor{a}})); + if (it == depth.end()) + cout << a << " has Bacon number infinity\n"; + else + cout << a << " has Bacon number " << (it->second / 2) << '\n'; + } +} + +} // namespace bacon3 + + +int main() { + cout << "\n--- bacon1 ---\n"; + bacon1::run(); + cout << "\n--- bacon1b (visitor) ---\n"; + bacon1::run1b(); + cout << "\n--- bacon2 ---\n"; + bacon2::run(); + cout << "\n--- bacon2_dot ---\n"; + bacon2::dot(); + cout << "\n--- bacon3 ---\n"; + bacon3::run(); + return 0; +} diff --git a/examples/BGLWorkshop2026/bglws_outline.md b/examples/BGLWorkshop2026/bglws_outline.md new file mode 100755 index 0000000..87f2f0b --- /dev/null +++ b/examples/BGLWorkshop2026/bglws_outline.md @@ -0,0 +1,116 @@ +# BGL Workshop Outline + +I (Phil) am scheduled to do a presentation to BGL users with Andrew on May 6, 2026. +We have 90 minutes with the following agenda: +- Retrospective on BGL and lessons learned over 26 years (Andrew) +- The nwgraph library using standard C++ containers (Andrew) +- graph-v3 and standardization (Phil) +- Graphs and agentic coding (Phil) + - Creating graph-v3 using vibe coding + - Observations on using an AI agent to convert BGL to use C++20 +- Q&A (15 minutes) + +Definitions and People +- BGL: Boost Graph Library written circa 2000 by Andrew Lumsdaine, et al +- graph-v3: A modern graph library using C++20 by Phil Ratzloff with contributors Andrew Lumsdaine + Kevin Deweese, and Scott McMillan. It was started in 2021 with the goal of adding it to the + C++ standard library. + +The following sections describe the organization of the presentation, but it may change as +it is refined. + +## Introduction to graph-v3 +This section compares graph-v3 with BGL + +### Kevin Bacon + +General Syntax differences +- Graph data: actors, directors, and movies + - Show graph image +- Problem(s): six degrees of Kevin Bacon + - Bacon number +- bacon1 + - graph construction + - edge iteration, views + - Bacon number +- bacon2 + - non-integral & sparse vertex ids + - building & constructing the bipartite graph + +### French Roads + - Dijkstra shortest_paths & shortest_distances + - properties + - internal: city_name, route_distance + - container_value_fn(distances) + +### Performance comparison +- P3337 + +### Agentic Coding + - graph-v3 + - bgl modernization + - algorithm demo (optional): Adamic-Adar Index, Transitive Closure + +### Parallel Narrative + +- goals: + - easy to use, natural C++20 style when used + - expressive + - performant +- SG19 + - getting started + - learn, grow (be open to critique, set aside ego) + - play with the big boys +- Discoveries + - ranges + - CPOs + - Descriptors +- Current State + +## Design +- Guiding Principles: simplicity, expressiveness, flexibility, performance + - use features & idioms in the C++20 std: ranges, concepts, r-value references, free functions (e.g. begin(r), end(r)), structured bindings, fnc objs +- Architecture + - Layers: algorithms, views, GCI, graph containers + - Data model: range-of-ranges as key abstraction supported & enforced by concepts + - algorithms writes to + - graphs are adapted to + - primitive functions (CPOs) and default impl + - values (std) vs. properties + - descriptors +- Views +- Algorithms + - visitors +- Graph Containers + - std-based container patterns + - library graphs: compressed_graph, dynamic_graph, undirected_graph + - BYOG; patterns supported + - typical integration - 4-7 fnc overloads + +## Feature Comparison BGL graph-v3 +- Create & init graph +- vertex & edge iteration +- concepts, traits, types + - graph_traits required only on dynamic_graph +- properties + +- Other BGL + - BGL wrapper + + +## Miscellaneous Notes (agent must ignore) + + BGL graph-v3 +Graph Defintion graph_traits + +properties: property maps vs. fnc objs, internal vs. external properties +concepts +parallel algorithms +distributed algorithms +features not supported (yet) +function creation +Examples: Kevin Bacon, social network, road network +iteration +key C++ features: ranges, concepts, structured bindings, constexpr +BYOG; patterns supported + diff --git a/examples/BGLWorkshop2026/france_routes_bgl.cpp b/examples/BGLWorkshop2026/france_routes_bgl.cpp new file mode 100644 index 0000000..d8bdc22 --- /dev/null +++ b/examples/BGLWorkshop2026/france_routes_bgl.cpp @@ -0,0 +1,248 @@ +// france_routes_bgl.cpp — France Routes example using Boost Graph Library (BGL). +// +// Mirrors france_routes_gv3.cpp but uses BGL instead of graph-v3. +// Demonstrates: +// - Loading a directed weighted road graph from JSON into a BGL adjacency_list +// - Running Dijkstra's shortest paths from Paris via boost::dijkstra_shortest_paths +// - Reconstructing and printing the shortest path to each city +// - Writing a Graphviz DOT file + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using std::cout; +using std::string; +using std::vector; +using namespace std::string_literals; + +// ── France Routes (BGL) ──────────────────────────────────────────────────────── +// Directed weighted graph of French city road connections. +// Vertex descriptors are plain size_t (vecS container); vertex bundles store the +// city name for reverse lookup. Edge bundles hold distance (km) and road name. + +namespace france_bgl { + +using vid_t = std::size_t; // vertex descriptor == index (vecS) +using dist_t = int; // distance in km + +// ── Bundled properties ───────────────────────────────────────────────────────── + +struct VertexProps { + string name; +}; + +struct EdgeProps { + dist_t distance_km = 0; + string road; // e.g. "A1", "A11 -> N157" +}; + +// Directed weighted graph; vecS vertex container keeps vertex_descriptor == vid_t. +using graph_t = boost::adjacency_list; +using vertex_desc_t = boost::graph_traits::vertex_descriptor; // = size_t +using edge_desc_t = boost::graph_traits::edge_descriptor; + +// ── load_result ──────────────────────────────────────────────────────────────── + +struct load_result { + graph_t g; + std::unordered_map city_id; // name -> vertex index +}; + +// Forward declarations +load_result load(const string& filename = BGLWS_DATA_DIR "/france_routes.json"); +void run (const string& filename = BGLWS_DATA_DIR "/france_routes.json"); +void dot (const string& dot_filename = BGLWS_OUTPUT_DIR "/france_routes_bgl.dot", + const string& json_filename = BGLWS_DATA_DIR "/france_routes.json"); + +// ── load() ───────────────────────────────────────────────────────────────────── +// Parses the JSON file with the same regex approach as france_routes_gv3.cpp and +// builds the BGL graph. + +load_result load(const string& filename) { + std::ifstream f(filename); + if (!f) + throw std::runtime_error("Cannot open file: "s + filename); + + string content((std::istreambuf_iterator(f)), std::istreambuf_iterator()); + + // ── Parse city names ────────────────────────────────────────────────────── + static const std::regex cities_re(R"re("cities"\s*:\s*\[([^\]]*)\])re"); + static const std::regex name_re(R"re("([^"]+)")re"); + + std::unordered_map city_id; + vector city_names; + + std::smatch cm; + if (std::regex_search(content, cm, cities_re)) { + string cities_block = cm[1].str(); + for (auto it = std::sregex_iterator(cities_block.begin(), cities_block.end(), name_re), + end = std::sregex_iterator{}; + it != end; ++it) { + string name = (*it)[1].str(); + city_id[name] = city_names.size(); + city_names.push_back(name); + } + } + + // ── Build BGL graph — one vertex per city ───────────────────────────────── + graph_t g(city_names.size()); + for (vid_t i = 0; i < city_names.size(); ++i) + g[i].name = city_names[i]; + + // ── Parse and add directed edges ────────────────────────────────────────── + static const std::regex route_re( + R"re(\{\s*"from"\s*:\s*"([^"]+)"\s*,\s*"to"\s*:\s*"([^"]+)"\s*,\s*"distance_km"\s*:\s*(\d+)\s*,\s*"route"\s*:\s*"([^"]+)"\s*\})re"); + + for (auto it = std::sregex_iterator(content.begin(), content.end(), route_re), + end = std::sregex_iterator{}; + it != end; ++it) { + auto& m = *it; + vid_t src = city_id.at(m[1].str()); + vid_t tgt = city_id.at(m[2].str()); + dist_t dist = std::stoi(m[3].str()); + boost::add_edge(src, tgt, EdgeProps{dist, m[4].str()}, g); + } + + return {std::move(g), std::move(city_id)}; +} + +// ── run() ────────────────────────────────────────────────────────────────────── + +void run(const string& filename) { + auto [g, city_id] = load(filename); + + const vid_t n = static_cast(boost::num_vertices(g)); + const vid_t source_id = city_id.at("Paris"); + constexpr dist_t inf = std::numeric_limits::max(); + + // ── Dijkstra ────────────────────────────────────────────────────────────── + // boost::dijkstra_shortest_paths initialises distances (inf) and source (0) + // internally; predecessor_map is initialised to identity. + vector distances(n, inf); + vector predecessors(n); + std::iota(predecessors.begin(), predecessors.end(), 0); // each vertex → itself + + auto idx = boost::get(boost::vertex_index, g); + auto dist_map = boost::make_iterator_property_map(distances.begin(), idx); + auto pred_map = boost::make_iterator_property_map(predecessors.begin(), idx); + auto wt_map = boost::get(&EdgeProps::distance_km, g); + + boost::dijkstra_shortest_paths(g, source_id, + boost::predecessor_map(pred_map) + .distance_map(dist_map) + .weight_map(wt_map)); + + // ── Print all distances, sorted nearest-first ───────────────────────────── + { + vector> by_dist; + by_dist.reserve(n); + for (vid_t uid = 0; uid < n; ++uid) + if (uid != source_id) + by_dist.emplace_back(distances[uid], uid); + std::ranges::sort(by_dist); // sort by (distance, uid) + + cout << "Shortest road distances from " << g[source_id].name << ":\n"; + for (auto& [d, uid] : by_dist) { + if (d == inf) + cout << " " << g[uid].name << ": unreachable\n"; + else + cout << " " << g[uid].name << ": " << d << " km\n"; + } + } + + // ── Reconstruct shortest path from Paris to Nice ────────────────────────── + { + const vid_t dest_id = city_id.at("Nice"); + cout << "\nShortest path: " << g[source_id].name << " -> " << g[dest_id].name << "\n"; + + vector path; + for (vid_t cur = dest_id; cur != source_id; cur = predecessors[cur]) { + if (path.size() >= n) { + cout << " (no path found)\n"; + return; + } + path.push_back(cur); + } + path.push_back(source_id); + std::ranges::reverse(path); + + for (size_t i = 0; i + 1 < path.size(); ++i) { + vid_t from = path[i]; + vid_t to = path[i + 1]; + + // Find the directed edge from → to by scanning out-edges. + edge_desc_t ed{}; + bool found = false; + for (auto [eit, eend] = boost::out_edges(from, g); eit != eend && !found; ++eit) { + if (boost::target(*eit, g) == to) { + ed = *eit; + found = true; + } + } + if (!found) { cout << " (edge not found)\n"; return; } + + const EdgeProps& ep = g[ed]; + cout << " " << g[from].name << " -> " << g[to].name + << " (" << ep.distance_km << " km, " << ep.road << ")\n"; + } + cout << " Total: " << distances[dest_id] << " km\n"; + } +} + +// ── dot() ────────────────────────────────────────────────────────────────────── +// Writes an undirected DOT file. The graph is stored as directed (one entry +// per direction in the JSON) so directed pairs are deduplicated with min/max. + +void dot(const string& dot_filename, const string& json_filename) { + auto [g, city_id] = load(json_filename); + + std::ofstream out(dot_filename); + if (!out) + throw std::runtime_error("Cannot open output file: "s + dot_filename); + + out << "graph FranceRoutes_BGL {\n"; + + // Vertices + for (auto [vit, vend] = boost::vertices(g); vit != vend; ++vit) { + vid_t vd = *vit; + out << " " << vd << " [label=\"" << g[vd].name << "\"];\n"; + } + + // Edges — emit each undirected pair once using min/max on vertex ids. + std::set> emitted; + for (auto [eit, eend] = boost::edges(g); eit != eend; ++eit) { + vid_t s = boost::source(*eit, g); + vid_t t = boost::target(*eit, g); + auto key = std::make_pair(std::min(s, t), std::max(s, t)); + if (emitted.insert(key).second) { + const EdgeProps& ep = g[*eit]; + out << " " << s << " -- " << t + << " [label=\"" << ep.distance_km << " km\\n(" << ep.road << ")\"" + << ", weight=" << ep.distance_km << "];\n"; + } + } + + out << "}\n"; + cout << "Wrote " << dot_filename << '\n'; +} + +} // namespace france_bgl + +int main() { + france_bgl::run(); + france_bgl::dot(); + return 0; +} diff --git a/examples/BGLWorkshop2026/france_routes_gv3.cpp b/examples/BGLWorkshop2026/france_routes_gv3.cpp new file mode 100644 index 0000000..83c2b02 --- /dev/null +++ b/examples/BGLWorkshop2026/france_routes_gv3.cpp @@ -0,0 +1,232 @@ +// france_routes.cpp +// BGL Workshop 2026 — France Routes Example +// +// Demonstrates: +// - Loading a directed weighted road graph from JSON +// - Running Dijkstra's shortest paths from Paris +// - Reconstructing and printing shortest paths to each city + +#include "graph/algorithm/dijkstra_shortest_paths.hpp" +#include "graph/container/dynamic_graph.hpp" +#include "graph/container/traits/vom_graph_traits.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using std::cout; +using std::string; +using std::vector; +using namespace graph; +using namespace graph::views; +using namespace std::string_literals; + +// ── France Routes ────────────────────────────────────────────────────────────── +// Directed weighted graph of French city road connections. +// Vertex IDs are contiguous uint32_t indices; vertex values store the city name +// for reverse lookup. Edge values hold distance (km) and road name. + +namespace france { + +using vid_t = size_t; // changing this may require the use of static_cast to avoid warnings +using dist_t = int; // distance in km + +struct route_t { + dist_t distance_km = 0; + string road; // e.g. "A1", "A11 -> N157" +}; + +// Vertex value = city name string; edge value = route_t (distance + road label). +using traits_t = container::vom_graph_traits; +using graph_t = container::dynamic_adjacency_graph; + +struct load_result { + graph_t g; + std::unordered_map city_id; // name -> vertex id +}; + +// Forward declarations +load_result load(const string& filename = BGLWS_DATA_DIR "/france_routes.json"); +void run (const string& filename = BGLWS_DATA_DIR "/france_routes.json"); +void dot (const string& dot_filename = BGLWS_OUTPUT_DIR "/france_routes.dot", + const string& json_filename = BGLWS_DATA_DIR "/france_routes.json"); + +load_result load(const string& filename) { + std::ifstream f(filename); + if (!f) + throw std::runtime_error("Cannot open file: "s + filename); + + string content((std::istreambuf_iterator(f)), std::istreambuf_iterator()); + + // ── Parse city names ────────────────────────────────────────────────────── + // Capture the "cities" array: [ "City1", "City2", ... ] + static const std::regex cities_re(R"re("cities"\s*:\s*\[([^\]]*)\])re"); + static const std::regex name_re(R"re("([^"]+)")re"); + + std::unordered_map city_id; + vector city_names; + + std::smatch cm; + if (std::regex_search(content, cm, cities_re)) { + string cities_block = cm[1].str(); + for (auto it = std::sregex_iterator(cities_block.begin(), cities_block.end(), name_re), + end = std::sregex_iterator{}; + it != end; ++it) { + string name = (*it)[1].str(); + city_id[name] = city_names.size(); + city_names.push_back(name); + } + } + + // ── Load vertices (city name as vertex value) ───────────────────────────── + using vertex_data = copyable_vertex_t; + vector vv; + vv.reserve(city_names.size()); + for (vid_t i = 0; i < city_names.size(); ++i) + vv.push_back({i, city_names[i]}); + + graph_t g; + g.load_vertices(vv); + + // ── Parse and load edges ────────────────────────────────────────────────── + // Note: named raw-string delimiter (re) avoids collision with ")" in the pattern. + static const std::regex route_re( + R"re(\{\s*"from"\s*:\s*"([^"]+)"\s*,\s*"to"\s*:\s*"([^"]+)"\s*,\s*"distance_km"\s*:\s*(\d+)\s*,\s*"route"\s*:\s*"([^"]+)"\s*\})re"); + + using edge_data = copyable_edge_t; // target vertex id + route info + vector edges; + + for (auto it = std::sregex_iterator(content.begin(), content.end(), route_re), + end = std::sregex_iterator{}; + it != end; ++it) { + auto& m = *it; + vid_t src = city_id.at(m[1].str()); + vid_t tgt = city_id.at(m[2].str()); + dist_t dist = std::stoi(m[3].str()); + edges.push_back({src, tgt, route_t{dist, m[4].str()}}); + } + + g.load_edges(edges); + return {std::move(g), std::move(city_id)}; +} + +void run(const string& filename) { + auto [g, city_id] = load(filename); + + // Vertex & edge properties + auto city_name = [&](const auto& g2, vid_t u) -> string { + return vertex_value(g2, *find_vertex(g2, u)); + }; + auto route_distance = [](const auto& g2, const auto& uv) -> dist_t { + return edge_value(g2, uv).distance_km; + }; + auto route_road = [](const auto& g2, const auto& uv) -> string { + return edge_value(g2, uv).road; + }; + + // ── Evaluate shortest paths from source ──────────────────────────────────── + const vid_t source_id = city_id.at("Paris"); + + // Distance and predecessor vectors indexed by vertex id. + vector distances(num_vertices(g)); + vector predecessors(num_vertices(g)); + init_shortest_paths(g, distances, predecessors); + + dijkstra_shortest_paths(g, source_id, + container_value_fn(distances), + container_value_fn(predecessors), + route_distance); + + // ── Print all distances, sorted nearest-first ────────────────────────────── + { + vector> by_dist; + by_dist.reserve(num_vertices(g)); + for (vid_t uid = 0; uid < num_vertices(g); ++uid) { + if (uid != source_id) + by_dist.emplace_back(distances[uid], uid); + } + std::ranges::sort(by_dist); // sort by (distance, uid) — default pair comparison + + cout << "Shortest road distances from " << city_name(g, source_id) << ":\n"; + for (auto& [d, uid] : by_dist) { + if (d == infinite_distance()) + cout << " " << city_name(g, uid) << ": unreachable\n"; + else + cout << " " << city_name(g, uid) << ": " << d << " km\n"; + } + } + + // ── Reconstruct shortest path from Paris to Nice ──────────────────────────── + { + const vid_t dest_id = city_id.at("Nice"); + cout << "\nShortest path: " << city_name(g, source_id) << " -> " << city_name(g, dest_id) << "\n"; + + vector path; + for (vid_t cur = dest_id; cur != source_id; cur = predecessors[cur]) { + if (path.size() >= num_vertices(g)) { + cout << " (no path found)\n"; + return; + } + path.push_back(cur); + } + path.push_back(source_id); + std::ranges::reverse(path); + + for (size_t i = 0; i + 1 < path.size(); ++i) { + vid_t from_id = path[i]; + vid_t to_id = path[i + 1]; + auto uv = find_vertex_edge(g, from_id, to_id); + cout << " " << city_name(g, from_id) << " -> " << city_name(g, to_id) + << " (" << route_distance(g, uv) << " km, " << route_road(g, uv) << ")\n"; + } + cout << " Total: " << distances[dest_id] << " km\n"; + } +} + +void dot(const string& dot_filename, const string& json_filename) { + auto [g, city_id] = load(json_filename); + + auto city_name = [&](vertex_t u) -> string { + return vertex_value(g, u); + }; + + std::ofstream out(dot_filename); + if (!out) + throw std::runtime_error("Cannot open output file: "s + dot_filename); + + out << "graph FranceRoutes {\n"; + + for (auto u : vertices(g)) { + out << " " << vertex_id(g, u) << " [label=\"" << city_name(u) << "\"];\n"; + } + + // Emit each undirected edge once: use the copy where sid < tid so that + // both Paris->Dijon and Dijon->Paris collapse to a single line. + std::set> emitted; + for (auto [sid, tid, uv] : views::edgelist(g)) { + auto key = std::make_pair(std::min(sid, tid), std::max(sid, tid)); + if (emitted.insert(key).second) { + const route_t& r = edge_value(g, uv); + out << " " << sid << " -- " << tid + << " [label=\"" << r.distance_km << " km\\n(" << r.road << ")\"" + << ", weight=" << r.distance_km << "];\n"; + } + } + + out << "}\n"; + cout << "Wrote " << dot_filename << '\n'; +} + +} // namespace france + +int main() { + france::run(); + france::dot(); + return 0; +} diff --git a/examples/BGLWorkshop2026/graphs/france_routes.json b/examples/BGLWorkshop2026/graphs/france_routes.json new file mode 100644 index 0000000..0c820dd --- /dev/null +++ b/examples/BGLWorkshop2026/graphs/france_routes.json @@ -0,0 +1,53 @@ +{ + "cities": [ + "Paris", "Lille", "Strasbourg", "Lyon", "Marseille", "Nice", + "Toulouse", "Bordeaux", "Nantes", "Rennes", "Montpellier", "Dijon" + ], + "routes": [ + { "from": "Paris", "to": "Lille", "distance_km": 225, "route": "A1" }, + { "from": "Paris", "to": "Rennes", "distance_km": 350, "route": "A11 → N157" }, + { "from": "Paris", "to": "Nantes", "distance_km": 385, "route": "A11" }, + { "from": "Paris", "to": "Dijon", "distance_km": 315, "route": "A5 → A31" }, + { "from": "Paris", "to": "Strasbourg", "distance_km": 490, "route": "A4" }, + + { "from": "Lille", "to": "Paris", "distance_km": 225, "route": "A1" }, + + { "from": "Rennes", "to": "Paris", "distance_km": 350, "route": "A11 → N157" }, + { "from": "Rennes", "to": "Nantes", "distance_km": 110, "route": "N137" }, + + { "from": "Nantes", "to": "Paris", "distance_km": 385, "route": "A11" }, + { "from": "Nantes", "to": "Rennes", "distance_km": 110, "route": "N137" }, + { "from": "Nantes", "to": "Bordeaux", "distance_km": 345, "route": "A83 → A10" }, + + { "from": "Bordeaux", "to": "Nantes", "distance_km": 345, "route": "A83 → A10" }, + { "from": "Bordeaux", "to": "Toulouse", "distance_km": 245, "route": "A62" }, + + { "from": "Toulouse", "to": "Bordeaux", "distance_km": 245, "route": "A62" }, + { "from": "Toulouse", "to": "Montpellier", "distance_km": 240, "route": "A61 → A9" }, + { "from": "Toulouse", "to": "Lyon", "distance_km": 540, "route": "A61 → A7" }, + + { "from": "Montpellier", "to": "Toulouse", "distance_km": 240, "route": "A61 → A9" }, + { "from": "Montpellier", "to": "Marseille", "distance_km": 170, "route": "A9 → A7" }, + + { "from": "Marseille", "to": "Montpellier", "distance_km": 170, "route": "A9 → A7" }, + { "from": "Marseille", "to": "Lyon", "distance_km": 315, "route": "A7" }, + { "from": "Marseille", "to": "Nice", "distance_km": 200, "route": "A8" }, + + { "from": "Nice", "to": "Marseille", "distance_km": 200, "route": "A8" }, + { "from": "Nice", "to": "Lyon", "distance_km": 470, "route": "A8 → A7" }, + + { "from": "Lyon", "to": "Dijon", "distance_km": 195, "route": "A6" }, + { "from": "Lyon", "to": "Marseille", "distance_km": 315, "route": "A7" }, + { "from": "Lyon", "to": "Nice", "distance_km": 470, "route": "A7 → A8" }, + { "from": "Lyon", "to": "Strasbourg", "distance_km": 490, "route": "A36" }, + { "from": "Lyon", "to": "Toulouse", "distance_km": 540, "route": "A7 → A61" }, + + { "from": "Dijon", "to": "Paris", "distance_km": 315, "route": "A31 → A5" }, + { "from": "Dijon", "to": "Lyon", "distance_km": 195, "route": "A6" }, + { "from": "Dijon", "to": "Strasbourg", "distance_km": 335, "route": "A36" }, + + { "from": "Strasbourg", "to": "Paris", "distance_km": 490, "route": "A4" }, + { "from": "Strasbourg", "to": "Dijon", "distance_km": 335, "route": "A36" }, + { "from": "Strasbourg", "to": "Lyon", "distance_km": 490, "route": "A36" } + ] +} diff --git a/examples/BGLWorkshop2026/graphs/imdb-graph.hpp b/examples/BGLWorkshop2026/graphs/imdb-graph.hpp new file mode 100644 index 0000000..b4b6755 --- /dev/null +++ b/examples/BGLWorkshop2026/graphs/imdb-graph.hpp @@ -0,0 +1,262 @@ +// +// Authors +// Copyright +// License +// No Warranty Disclaimer +// + +// IMDB Example + + +#include +#include +#include + + +// clang-format off + +std::vector actors{ + "Tom Cruise", + "Kevin Bacon", + "Hugo Weaving", + "Carrie-Anne Moss", + "Natalie Portman", + "Jack Nicholson", + "Kelly McGillis", + "Harrison Ford", + "Sebastian Stan", + "Mila Kunis", + "Michelle Pfeiffer", + "Keanu Reeves", + "Julia Roberts", +}; + + +std::vector movies{ + "A Few Good Men", + "Top Gun", + "Black Swan", + "V for Vendetta", + "The Matrix", + "Witness", + "What Lies Beneath", + "Closer", + "Flatliners", +}; + + +std::vector> movies_actors{ + {"A Few Good Men", "Tom Cruise"}, + {"A Few Good Men", "Kevin Bacon"}, + {"A Few Good Men", "Jack Nicholson"}, + {"What Lies Beneath", "Harrison Ford"}, + {"What Lies Beneath", "Kevin Bacon"}, + {"Top Gun", "Tom Cruise"}, + {"Top Gun", "Kelly McGillis"}, + {"Witness", "Harrison Ford"}, + {"Witness", "Kelly McGillis"}, + {"Black Swan", "Sebastian Stan"}, + {"Black Swan", "Natalie Portman"}, + {"Black Swan", "Mila Kunis"}, + {"V for Vendetta", "Hugo Weaving"}, + {"V for Vendetta", "Natalie Portman"}, + {"The Matrix", "Carrie-Anne Moss"}, + {"The Matrix", "Keanu Reeves"}, + {"The Matrix", "Hugo Weaving"}, + {"What Lies Beneath", "Michelle Pfeiffer"}, + {"Closer", "Natalie Portman"}, + {"Closer", "Julia Roberts"}, + {"Flatliners", "Kevin Bacon"}, + {"Flatliners", "Julia Roberts"}, +}; + + +std::vector> movie_actor_edge_list { +{ 0, 0}, +{ 0, 1}, +{ 0, 5}, +{ 1, 0}, +{ 1, 6}, +{ 2, 8}, +{ 2, 4}, +{ 2, 9}, +{ 3, 2}, +{ 3, 4}, +{ 4, 3}, +{ 4, 11}, +{ 4, 2}, +{ 5, 7}, +{ 5, 6}, +{ 6, 7}, +{ 6, 1}, +{ 6, 10}, +{ 7, 4}, +{ 7, 12}, +{ 8, 1}, +{ 8, 12}, +}; + + + std::vector> actor_movie_edge_list { +{ 0, 0}, +{ 0, 1}, +{ 1, 0}, +{ 1, 6}, +{ 1, 8}, +{ 2, 3}, +{ 2, 4}, +{ 3, 4}, +{ 4, 2}, +{ 4, 3}, +{ 4, 7}, +{ 5, 0}, +{ 6, 1}, +{ 6, 5}, +{ 7, 6}, +{ 7, 5}, +{ 8, 2}, +{ 9, 2}, +{ 10, 6}, +{ 11, 4}, +{ 12, 7}, +{ 12, 8}, + }; + + +std::vector> movie_actor_index_adjacency_list { + /* 0*/ { 0, 1, 5, }, + /* 1*/ { 0, 6, }, + /* 2*/ { 8, 4, 9, }, + /* 3*/ { 2, 4, }, + /* 4*/ { 3, 11, 2, }, + /* 5*/ { 7, 6, }, + /* 6*/ { 7, 1, 10, }, + /* 7*/ { 4, 12, }, + /* 8*/ { 1, 12, }, +}; + + +std::vector> actor_movie_index_adjacency_list { + /* 0*/ { 0, 1, }, + /* 1*/ { 0, 6, 8,}, + /* 2*/ { 3, 4, }, + /* 3*/ { 4, }, + /* 4*/ { 2, 3, 7,}, + /* 5*/ { 0, }, + /* 6*/ { 1, 5, }, + /* 7*/ { 6, 5, }, + /* 8*/ { 2, }, + /* 9*/ { 2, }, + /* 10*/ { 6, }, + /* 11*/ { 4, }, + /* 12*/ { 7, 8, }, +}; + + +std::vector> actor_actor_edge_list { +{ 0, 1, 0}, +{ 0, 5, 0}, +{ 0, 6, 1}, +{ 1, 0, 0}, +{ 1, 5, 0}, +{ 1, 7, 6}, +{ 1, 10, 6}, +{ 1, 12, 8}, +{ 2, 4, 3}, +{ 2, 3, 4}, +{ 2, 11, 4}, +{ 3, 11, 4}, +{ 3, 2, 4}, +{ 4, 8, 2}, +{ 4, 9, 2}, +{ 4, 2, 3}, +{ 4, 12, 7}, +{ 5, 0, 0}, +{ 5, 1, 0}, +{ 6, 0, 1}, +{ 6, 7, 5}, +{ 7, 1, 6}, +{ 7, 10, 6}, +{ 7, 6, 5}, +{ 8, 4, 2}, +{ 8, 9, 2}, +{ 9, 8, 2}, +{ 9, 4, 2}, +{ 10, 7, 6}, +{ 10, 1, 6}, +{ 11, 3, 4}, +{ 11, 2, 4}, +{ 12, 4, 7}, +{ 12, 1, 8}, +}; + +std::vector>> actor_actor_adjacency_list { + /* 0*/ { { 1, 0 }, { 5, 0 }, { 6, 1 }, }, + /* 1*/ { { 0, 0 }, { 5, 0 }, { 7, 6 }, { 10, 6 }, { 12, 8 },}, + /* 2*/ { { 4, 3 }, { 3, 4 }, { 11, 4 }, }, + /* 3*/ { { 11, 4 }, { 2, 4 }, }, + /* 4*/ { { 8, 2 }, { 9, 2 }, { 2, 3 }, { 12, 7 }, }, + /* 5*/ { { 0, 0 }, { 1, 0 }, }, + /* 6*/ { { 0, 1 }, { 7, 5 }, }, + /* 7*/ { { 1, 6 }, { 10, 6 }, { 6, 5 }, }, + /* 8*/ { { 4, 2 }, { 9, 2 }, }, + /* 9*/ { { 8, 2 }, { 4, 2 }, }, + /* 10*/ { { 7, 6 }, { 1, 6 }, }, + /* 11*/ { { 3, 4 }, { 2, 4 }, }, + /* 12*/ { { 4, 7 }, { 1, 8 }, }, +}; + +std::vector> movie_movie_edge_list { +{ 0, 1, 0}, +{ 0, 5, 0}, +{ 0, 6, 1}, +{ 1, 0, 0}, +{ 1, 5, 0}, +{ 1, 7, 6}, +{ 1, 10, 6}, +{ 1, 12, 8}, +{ 2, 4, 3}, +{ 2, 3, 4}, +{ 2, 11, 4}, +{ 3, 11, 4}, +{ 3, 2, 4}, +{ 4, 8, 2}, +{ 4, 9, 2}, +{ 4, 2, 3}, +{ 4, 12, 7}, +{ 5, 0, 0}, +{ 5, 1, 0}, +{ 6, 0, 1}, +{ 6, 7, 5}, +{ 7, 1, 6}, +{ 7, 10, 6}, +{ 7, 6, 5}, +{ 8, 4, 2}, +{ 8, 9, 2}, +{ 9, 8, 2}, +{ 9, 4, 2}, +{ 10, 7, 6}, +{ 10, 1, 6}, +{ 11, 3, 4}, +{ 11, 2, 4}, +{ 12, 4, 7}, +{ 12, 1, 8}, +}; + +std::vector>> movie_movie_adjacency_list { + /* 0*/ { { 1, 0 }, { 5, 0 }, { 6, 1 }, }, + /* 1*/ { { 0, 0 }, { 5, 0 }, { 7, 6 }, { 10, 6 }, { 12, 8 },}, + /* 2*/ { { 4, 3 }, { 3, 4 }, { 11, 4 }, }, + /* 3*/ { { 11, 4 }, { 2, 4 }, }, + /* 4*/ { { 8, 2 }, { 9, 2 }, { 2, 3 }, { 12, 7 }, }, + /* 5*/ { { 0, 0 }, { 1, 0 }, }, + /* 6*/ { { 0, 1 }, { 7, 5 }, }, + /* 7*/ { { 1, 6 }, { 10, 6 }, { 6, 5 }, }, + /* 8*/ { { 4, 2 }, { 9, 2 }, }, + /* 9*/ { { 8, 2 }, { 4, 2 }, }, + /* 10*/ { { 7, 6 }, { 1, 6 }, }, + /* 11*/ { { 3, 4 }, { 2, 4 }, }, + /* 12*/ { { 4, 7 }, { 1, 8 }, }, +}; + +// clang-format on diff --git a/examples/BGLWorkshop2026/output/.gitkeep b/examples/BGLWorkshop2026/output/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 6b12faa..18f9034 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -2,10 +2,18 @@ add_executable(basic_usage basic_usage.cpp) target_link_libraries(basic_usage PRIVATE graph3) +add_executable(adapting_third_party_graph + AdaptingThirdPartyGraph/adapting_a_third_party_graph.cpp) +target_link_libraries(adapting_third_party_graph PRIVATE graph3) + add_executable(dijkstra_example dijkstra_clrs_example.cpp) target_link_libraries(dijkstra_example PRIVATE graph3) target_include_directories(dijkstra_example PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) +add_subdirectory(CppCon2021) +add_subdirectory(CppCon2022) +add_subdirectory(BGLWorkshop2026) + # BGL adaptor example (requires Boost headers) option(BUILD_BGL_EXAMPLES "Build BGL adaptor examples (requires Boost headers)" OFF) diff --git a/examples/CppCon2021/CMakeLists.txt b/examples/CppCon2021/CMakeLists.txt new file mode 100644 index 0000000..85de257 --- /dev/null +++ b/examples/CppCon2021/CMakeLists.txt @@ -0,0 +1,21 @@ +# CppCon 2021 examples — refactored from graph-v2 to graph-v3 + +set(CPPCON21_INCLUDE_DIRS + ${CMAKE_CURRENT_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR}/graphs) + +add_executable(cppcon21_graphs examples/graphs.cpp) +target_link_libraries(cppcon21_graphs PRIVATE graph3) +target_include_directories(cppcon21_graphs PRIVATE ${CPPCON21_INCLUDE_DIRS}) + +add_executable(cppcon21_bacon examples/bacon.cpp) +target_link_libraries(cppcon21_bacon PRIVATE graph3) +target_include_directories(cppcon21_bacon PRIVATE ${CPPCON21_INCLUDE_DIRS}) + +add_executable(cppcon21_ospf examples/ospf.cpp) +target_link_libraries(cppcon21_ospf PRIVATE graph3) +target_include_directories(cppcon21_ospf PRIVATE ${CPPCON21_INCLUDE_DIRS}) + +add_executable(cppcon21_imdb examples/imdb.cpp) +target_link_libraries(cppcon21_imdb PRIVATE graph3) +target_include_directories(cppcon21_imdb PRIVATE ${CPPCON21_INCLUDE_DIRS}) diff --git a/examples/CppCon2021/examples/bacon.cpp b/examples/CppCon2021/examples/bacon.cpp new file mode 100644 index 0000000..40bffd8 --- /dev/null +++ b/examples/CppCon2021/examples/bacon.cpp @@ -0,0 +1,30 @@ +#include "graph/graph.hpp" +#include "graph/views/bfs.hpp" +#include "imdb-graph.hpp" + +#include +#include +#include + +std::vector> costars{{1, 5, 6}, {7, 10, 0, 5, 12}, {4, 3, 11}, {2, 11}, {8, 9, 2, 12}, {0, 1}, + {7, 0}, {6, 1, 10}, {4, 9}, {4, 8}, {7, 1}, {2, 3}, + {1, 4}}; + +int main() { + + std::vector bacon_number(size(actors)); + + // In v3, edges_bfs yields [uv] (edge descriptor only). + // Extract source and target IDs from the edge descriptor via CPOs. + for (auto&& [uv] : graph::views::edges_bfs(costars, std::size_t{1})) { + auto u = graph::source_id(costars, uv); + auto v = graph::target_id(costars, uv); + bacon_number[v] = bacon_number[u] + 1; + } + + for (size_t i = 0; i < size(actors); ++i) { + std::cout << actors[i] << " has Bacon number " << bacon_number[i] << std::endl; + } + + return 0; +} diff --git a/examples/CppCon2021/examples/graphs.cpp b/examples/CppCon2021/examples/graphs.cpp new file mode 100644 index 0000000..c06bcd8 --- /dev/null +++ b/examples/CppCon2021/examples/graphs.cpp @@ -0,0 +1,219 @@ +// +// Authors +// Copyright +// License +// No Warranty Disclaimer +// +// Make sure various combinations of graphs can compile + +#if defined(__GNUC__) || defined(__clang__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wsign-conversion" +# pragma GCC diagnostic ignored "-Wsign-compare" +# pragma GCC diagnostic ignored "-Wunused-result" +#elif defined(_MSC_VER) +# pragma warning(push) +# pragma warning(disable : 4834) // warning C4834: discarding return value of function with 'nodiscard' attribute +# pragma warning(disable : 4267) // warning C4267: 'argument': conversion from 'size_t' to 'int', possible loss of data +#endif + +#include +#include +#include +#include + +#include "graph/graph.hpp" +#include "graph/views/bfs.hpp" + +#include "imdb-graph.hpp" +#include "karate-graph.hpp" +#include "ospf-graph.hpp" +#include "spice-graph.hpp" +#include "utilities.hpp" + +int main() { + + /** + * Karate is only represented as index edge list and index adjacency list + */ + std::vector> G(34); + push_back_plain_fill(karate_index_edge_list, G, false, 0); + static_assert(graph::adjacency_list); + std::cout << "Karate adjacency list:\n"; + std::cout << "size = " << G.size() << std::endl; + for (size_t uid = 0; uid < G.size(); ++uid) { + std::cout << std::setw(3) << uid << ":"; + for (auto&& vid : G[uid]) { + std::cout << " " << vid; + } + std::cout << std::endl; + } + + std::vector>> H(34); + push_back_plain_fill(karate_index_edge_list, H, false, 0); + std::cout << "\nKarate (edge_list plain fill):\n"; + std::cout << "size = " << H.size() << std::endl; + for (size_t uid = 0; uid < H.size(); ++uid) { + std::cout << std::setw(3) << uid << ":"; + for (auto&& [vid] : H[uid]) { + std::cout << " " << vid; + } + std::cout << std::endl; + } + + + push_back_fill(karate_index_edge_list, H, false, 0); + std::cout << "\nKarate (edge_list fill...adding more):\n"; + std::cout << "size = " << H.size() << std::endl; + for (size_t uid = 0; uid < H.size(); ++uid) { + std::cout << std::setw(3) << uid << ":"; + for (auto&& [vid] : H[uid]) { + std::cout << " " << vid; + } + std::cout << std::endl; + } + + //---------------------------------------------------------------------------- + + /** + * Other graphs have vertices and edges tables + */ + auto a = make_plain_graph(ospf_vertices, ospf_edges); + std::cout << "\nOSPF plain graph:\n"; + std::cout << "size = " << a.size() << std::endl; + for (size_t uid = 0; uid < a.size(); ++uid) { + std::cout << std::setw(3) << ospf_vertices[uid] << ":"; + for (auto&& vid : a[uid]) { + std::cout << " " << ospf_vertices[vid]; + } + std::cout << std::endl; + } + + auto b = make_property_graph(ospf_vertices, ospf_edges); + std::cout << "\nOSPF property graph:\n"; + std::cout << "size = " << b.size() << std::endl; + for (size_t uid = 0; uid < b.size(); ++uid) { + std::cout << std::setw(3) << ospf_vertices[uid] << ":"; + for (auto&& [vid, val] : b[uid]) { + std::cout << " " << ospf_vertices[vid] << ":" << val; + } + std::cout << std::endl; + } + + auto c = make_index_graph(ospf_vertices, ospf_edges); + std::cout << "\nOSPF index graph:\n"; + std::cout << "size = " << c.size() << std::endl; + for (size_t uid = 0; uid < c.size(); ++uid) { + std::cout << std::setw(3) << ospf_vertices[uid] << ":"; + for (auto&& [vid, val] : c[uid]) { + std::cout << " " << ospf_vertices[vid] << ":" << std::get<2>(ospf_edges[val]); + } + std::cout << std::endl; + } + + auto d = make_plain_graph>>( + ospf_vertices, ospf_edges, true); + std::cout << "\nOSPF plain graph (vector of lists):\n"; + std::cout << "size = " << d.size() << std::endl; + for (size_t uid = 0; uid < d.size(); ++uid) { + std::cout << std::setw(3) << ospf_vertices[uid] << ":"; + for (auto&& vid : d[uid]) { + std::cout << " " << ospf_vertices[vid]; + } + std::cout << std::endl; + } + + auto e = make_index_graph>>>(ospf_vertices, ospf_edges, true); + std::cout << "\nOSPF index graph (vector of vector of tuples):\n"; + std::cout << "size = " << e.size() << std::endl; + for (size_t uid = 0; uid < e.size(); ++uid) { + std::cout << std::setw(3) << ospf_vertices[uid] << ":"; + for (auto&& [vid, val] : e[uid]) { + std::cout << " " << ospf_vertices[vid] << ":" << std::get<2>(ospf_edges[val]); + } + std::cout << std::endl; + } + + //---------------------------------------------------------------------------- + + auto [f, g] = make_plain_bipartite_graphs<>(movies, actors, movies_actors); + auto h = make_plain_bipartite_graph(movies, actors, movies_actors, 0); + auto i = make_plain_bipartite_graph(movies, actors, movies_actors, 1); + std::cout << "\nMovies-actors plain bipartite graphs\n"; + std::cout << "index 0: " << f.size() << "==" << h.size() << std::endl; + for (size_t uid = 0; uid < f.size(); ++uid) { + std::cout << std::setw(20) << movies[uid] << ": |"; + for (auto&& vid : f[uid]) { + std::cout << actors[vid] << "|"; + } + std::cout << std::endl; + } + std::cout << "index 1: " << g.size() << "==" << i.size() << std::endl; + for (size_t uid = 0; uid < g.size(); ++uid) { + std::cout << std::setw(20) << actors[uid] << ": |"; + for (auto&& vid : g[uid]) { + std::cout << movies[vid] << "|"; + } + std::cout << std::endl; + } + + auto [j, k] = make_plain_bipartite_graphs>>(movies, actors, movies_actors); + auto l = make_plain_bipartite_graph>>(movies, actors, movies_actors, 0); + auto m = make_plain_bipartite_graph>>(movies, actors, movies_actors, 1); + std::cout << "\nMovies-actors plain bipartite graphs (vector of lists)\n"; + std::cout << "index 0: " << j.size() << "==" << l.size() << std::endl; + for (size_t uid = 0; uid < j.size(); ++uid) { + std::cout << std::setw(20) << movies[uid] << ": |"; + for (auto&& vid : j[uid]) { + std::cout << actors[vid] << "|"; + } + std::cout << std::endl; + } + std::cout << "index 1: " << k.size() << "==" << m.size() << std::endl; + for (size_t uid = 0; uid < k.size(); ++uid) { + std::cout << std::setw(20) << actors[uid] << ": |"; + for (auto&& vid : k[uid]) { + std::cout << movies[vid] << "|"; + } + std::cout << std::endl; + } + + //---------------------------------------------------------------------------- + + auto n = make_plain_graph>>( + spice_vertices, spice_edges); + auto o = make_plain_graph>>( + spice_vertices, spice_edges_values); + auto p = make_index_graph(spice_vertices, spice_edges); + auto q = make_index_graph(spice_vertices, spice_edges_values); + auto r = make_property_graph(spice_vertices, spice_edges); + auto s = make_property_graph(spice_vertices, spice_edges_values); + + std::cout << "\nSpice property graph (using edges+values)\n"; + std::cout << "Size: " << s.size() << std::endl; + for (size_t uid = 0; uid < s.size(); ++uid) { + std::cout << std::setw(4) << spice_vertices[uid] << ": |"; + for (auto&& [vid, comp, val] : s[uid]) { + std::cout << std::setw(3) << spice_vertices[vid] << ":" << comp << "/" << val << "|"; + } + std::cout << std::endl; + } + + // Compile-check: create BFS edge views (results discarded) + graph::views::edges_bfs(n, std::size_t{1}); + graph::views::edges_bfs(o, std::size_t{1}); + graph::views::edges_bfs(p, std::size_t{1}); + graph::views::edges_bfs(q, std::size_t{0}); + + return 0; +} + +#if defined(__clang__) || defined(__GNUC__) +# pragma GCC diagnostic pop +#elif defined(_MSC_VER) +# pragma warning(pop) +#endif diff --git a/examples/CppCon2021/examples/imdb.cpp b/examples/CppCon2021/examples/imdb.cpp new file mode 100644 index 0000000..7729c13 --- /dev/null +++ b/examples/CppCon2021/examples/imdb.cpp @@ -0,0 +1,86 @@ +// +// Authors +// Copyright +// License +// No Warranty Disclaimer +// + +// IMDB Example + +#if defined(__GNUC__) || defined(__clang__) +#elif defined(_MSC_VER) +# pragma warning(push) +# pragma warning(disable : 4267) // warning C4267: 'argument': conversion from 'size_t' to 'int', possible loss of data +#endif + +#include +#include +#include + +#include "graph/graph.hpp" +#include "graph/views/bfs.hpp" +#include "imdb-graph.hpp" +#include "utilities.hpp" + +int main() { + + // Get actor-movie and movie-actor graphs (plain: vector>) + auto&& [G, H] = make_plain_bipartite_graphs<>(movies, actors, movies_actors); + + // Will also work with this type of graph (vector>>) + auto&& [J, K] = make_bipartite_graphs<>(movies, actors, movies_actors); + + // Create actor-actor graph: L[actor] -> list of {co-star, movie} + auto L = join(G, H); + auto M = join(H, G); + + // Can also join with explicit IndexGraph types + auto N = join>>>(G, H); + auto O = join>>>(H, G); + + size_t kevin_bacon = 1; + std::vector distance(L.size()); + std::vector parents(L.size()); + std::vector together_in(L.size()); + + // In v3, edges_bfs yields [uv] without source. + // Pass a value function to also get the movie index per edge. + // The edge tuple in L is {target_actor_id, movie_id}; *uv.value() gives + // the raw tuple, so std::get<1>(*uv.value()) is the movie index. + auto kprop = [](const auto& /*g*/, const auto& uv) { + return std::get<1>(*uv.value()); // movie index stored as second element + }; + + for (auto&& [uv, k] : graph::views::edges_bfs(L, kevin_bacon, kprop)) { + auto u = graph::source_id(L, uv); + auto v = graph::target_id(L, uv); + distance[v] = distance[u] + 1; + parents[v] = u; + together_in[v] = k; + } + + std::cout << actors[kevin_bacon] << " has a bacon number of " << distance[kevin_bacon] << std::endl; + std::cout << std::endl; + + // Iterate through all actors (other than Kevin Bacon) + for (size_t i = 0; i < actors.size(); ++i) { + if (i != kevin_bacon) { + auto bacon_number = distance[i]; + std::cout << actors[i] << " has a bacon number of " << distance[i] << std::endl; + + auto k = i; + size_t d = distance[k]; + while (k != kevin_bacon) { + std::cout << " " << actors[k] << " starred with " << actors[parents[k]] << " in " << movies[together_in[k]] + << std::endl; + k = parents[k]; + if (d-- == 0) { + break; + } + } + std::cout << std::endl; + } + } + + return 0; +} diff --git a/examples/CppCon2021/examples/ospf.cpp b/examples/CppCon2021/examples/ospf.cpp new file mode 100644 index 0000000..ee3e619 --- /dev/null +++ b/examples/CppCon2021/examples/ospf.cpp @@ -0,0 +1,109 @@ +// +// Authors +// Copyright +// License +// No Warranty Disclaimer +// + +#include +#include +#include + +#include "graph/algorithm/dijkstra_shortest_paths.hpp" +#include "ospf-graph.hpp" +#include "utilities.hpp" + +int main() { + + static_assert(graph::adjacency_list); + + // ── ospf_index_adjacency_list: vector>> ────── + // Each edge tuple is {target_id, weight}. + // In v3: init_shortest_paths takes the graph first; dijkstra uses + // container_value_fn to adapt vectors into (g, uid) -> value& functions, + // and the weight function signature is (const G&, const edge_t&). + + std::vector d(size(ospf_index_adjacency_list)); + std::vector p(size(ospf_index_adjacency_list)); + graph::init_shortest_paths(ospf_index_adjacency_list, d, p); + graph::dijkstra_shortest_paths( + ospf_index_adjacency_list, + std::size_t{5}, + graph::container_value_fn(d), + graph::container_value_fn(p), + [](const auto& /*g*/, const auto& uv) { return std::get<1>(*uv.value()); }); + + std::cout << "----------------" << std::endl; + std::cout << "Contents of ospf_index_adjacency_list (the correct answer)" << std::endl; + + for (size_t i = 0; i < size(ospf_vertices); ++i) { + std::cout << std::setw(6) << ospf_vertices[i] << std::setw(6) << d[i] << std::endl; + } + + std::cout << "----------------" << std::endl; + std::cout << "Results from make_property_graph(osp_vertices)" << std::endl; + + // ── make_property_graph: vector>> ──────────── + // Each edge tuple is {target_id, weight} (property = weight). + + auto G = make_property_graph(ospf_vertices, ospf_edges, true); + + // Alternatively + auto H = make_property_graph>>>(ospf_vertices, ospf_edges, true); + auto I = make_property_graph>>>(ospf_vertices, ospf_edges, true); + + static_assert(graph::adjacency_list); + + p.resize(graph::num_vertices(G)); + std::vector e(graph::num_vertices(G)); + graph::init_shortest_paths(G, e, p); + graph::dijkstra_shortest_paths( + G, + std::size_t{5}, + graph::container_value_fn(e), + graph::container_value_fn(p), + [](const auto& /*g*/, const auto& uv) { return std::get<1>(*uv.value()); }); + + bool pass = true; + for (size_t i = 0; i < size(ospf_vertices); ++i) { + std::cout << std::setw(6) << ospf_vertices[i] << std::setw(6) << e[i] << std::endl; + if (e[i] != d[i]) + pass = false; + } + std::cout << (pass ? "***PASS***" : "***FAIL***") << std::endl; + + std::cout << "----------------" << std::endl; + std::cout << "Results from make_index_graph(osp_vertices)" << std::endl; + + // ── make_index_graph: vector>> ─────────────── + // Each edge tuple is {target_id, edge_index_into_ospf_edges}. + // Access the original weight via ospf_edges[edge_index]. + + auto J = make_index_graph(ospf_vertices, ospf_edges, true); + + p.resize(graph::num_vertices(J)); + std::vector f(graph::num_vertices(J)); + graph::init_shortest_paths(J, f, p); + graph::dijkstra_shortest_paths( + J, + std::size_t{5}, + graph::container_value_fn(f), + graph::container_value_fn(p), + [](const auto& /*g*/, const auto& uv) { + // *uv.value() = tuple + auto edge_index = std::get<1>(*uv.value()); + return std::get<2>(ospf_edges[edge_index]); + }); + + bool pass2 = true; + for (size_t i = 0; i < size(ospf_vertices); ++i) { + std::cout << std::setw(6) << ospf_vertices[i] << std::setw(6) << e[i] << std::endl; + if (e[i] != d[i]) + pass2 = false; + } + std::cout << (pass2 ? "***PASS***" : "***FAIL***") << std::endl; + + return 0; +} diff --git a/examples/CppCon2021/graphs/imdb-graph.hpp b/examples/CppCon2021/graphs/imdb-graph.hpp new file mode 100644 index 0000000..89dbf34 --- /dev/null +++ b/examples/CppCon2021/graphs/imdb-graph.hpp @@ -0,0 +1,262 @@ +// +// Authors +// Copyright +// License +// No Warranty Disclaimer +// + +// IMDB Example + + +#include +#include +#include + + +// clang-format off + +std::vector actors{ + "Tom Cruise", + "Kevin Bacon", + "Hugo Weaving", + "Carrie-Anne Moss", + "Natalie Portman", + "Jack Nicholson", + "Kelly McGillis", + "Harrison Ford", + "Sebastian Stan", + "Mila Kunis", + "Michelle Pfeiffer", + "Keanu Reeves", + "Julia Roberts", +}; + + +std::vector movies{ + "A Few Good Men", + "Top Gun", + "Black Swan", + "V for Vendetta", + "The Matrix", + "Witness", + "What Lies Beneath", + "Closer", + "Flatliners", +}; + + +std::vector> movies_actors{ + {"A Few Good Men", "Tom Cruise"}, + {"A Few Good Men", "Kevin Bacon"}, + {"A Few Good Men", "Jack Nicholson"}, + {"What Lies Beneath", "Harrison Ford"}, + {"What Lies Beneath", "Kevin Bacon"}, + {"Top Gun", "Tom Cruise"}, + {"Top Gun", "Kelly McGillis"}, + {"Witness", "Harrison Ford"}, + {"Witness", "Kelly McGillis"}, + {"Black Swan", "Sebastian Stan"}, + {"Black Swan", "Natalie Portman"}, + {"Black Swan", "Mila Kunis"}, + {"V for Vendetta", "Hugo Weaving"}, + {"V for Vendetta", "Natalie Portman"}, + {"The Matrix", "Carrie-Anne Moss"}, + {"The Matrix", "Keanu Reeves"}, + {"The Matrix", "Hugo Weaving"}, + {"What Lies Beneath", "Michelle Pfeiffer"}, + {"Closer", "Natalie Portman"}, + {"Closer", "Julia Roberts"}, + {"Flatliners", "Kevin Bacon"}, + {"Flatliners", "Julia Roberts"}, +}; + + +std::vector> movie_actor_edge_list { +{ 0, 0}, +{ 0, 1}, +{ 0, 5}, +{ 1, 0}, +{ 1, 6}, +{ 2, 8}, +{ 2, 4}, +{ 2, 9}, +{ 3, 2}, +{ 3, 4}, +{ 4, 3}, +{ 4, 11}, +{ 4, 2}, +{ 5, 7}, +{ 5, 6}, +{ 6, 7}, +{ 6, 1}, +{ 6, 10}, +{ 7, 4}, +{ 7, 12}, +{ 8, 1}, +{ 8, 12}, +}; + + + std::vector> actor_movie_edge_list { +{ 0, 0}, +{ 0, 1}, +{ 1, 0}, +{ 1, 6}, +{ 1, 8}, +{ 2, 3}, +{ 2, 4}, +{ 3, 4}, +{ 4, 2}, +{ 4, 3}, +{ 4, 7}, +{ 5, 0}, +{ 6, 1}, +{ 6, 5}, +{ 7, 6}, +{ 7, 5}, +{ 8, 2}, +{ 9, 2}, +{ 10, 6}, +{ 11, 4}, +{ 12, 7}, +{ 12, 8}, + }; + + +std::vector> movie_actor_index_adjacency_list { + /* 0*/ { 0, 1, 5, }, + /* 1*/ { 0, 6, }, + /* 2*/ { 8, 4, 9, }, + /* 3*/ { 2, 4, }, + /* 4*/ { 3, 11, 2, }, + /* 5*/ { 7, 6, }, + /* 6*/ { 7, 1, 10, }, + /* 7*/ { 4, 12, }, + /* 8*/ { 1, 12, }, +}; + + +std::vector> actor_movie_index_adjacency_list { + /* 0*/ { 0, 1, }, + /* 1*/ { 0, 6, 8,}, + /* 2*/ { 3, 4, }, + /* 3*/ { 4, }, + /* 4*/ { 2, 3, 7,}, + /* 5*/ { 0, }, + /* 6*/ { 1, 5, }, + /* 7*/ { 6, 5, }, + /* 8*/ { 2, }, + /* 9*/ { 2, }, + /* 10*/ { 6, }, + /* 11*/ { 4, }, + /* 12*/ { 7, 8, }, +}; + + +std::vector> actor_actor_edge_list { +{ 0, 1, 0}, +{ 0, 5, 0}, +{ 0, 6, 1}, +{ 1, 0, 0}, +{ 1, 5, 0}, +{ 1, 7, 6}, +{ 1, 10, 6}, +{ 1, 12, 8}, +{ 2, 4, 3}, +{ 2, 3, 4}, +{ 2, 11, 4}, +{ 3, 11, 4}, +{ 3, 2, 4}, +{ 4, 8, 2}, +{ 4, 9, 2}, +{ 4, 2, 3}, +{ 4, 12, 7}, +{ 5, 0, 0}, +{ 5, 1, 0}, +{ 6, 0, 1}, +{ 6, 7, 5}, +{ 7, 1, 6}, +{ 7, 10, 6}, +{ 7, 6, 5}, +{ 8, 4, 2}, +{ 8, 9, 2}, +{ 9, 8, 2}, +{ 9, 4, 2}, +{ 10, 7, 6}, +{ 10, 1, 6}, +{ 11, 3, 4}, +{ 11, 2, 4}, +{ 12, 4, 7}, +{ 12, 1, 8}, +}; + +std::vector>> actor_actor_adjacency_list { + /* 0*/ { { 1, 0 }, { 5, 0 }, { 6, 1 }, }, + /* 1*/ { { 0, 0 }, { 5, 0 }, { 7, 6 }, { 10, 6 }, { 12, 8 },}, + /* 2*/ { { 4, 3 }, { 3, 4 }, { 11, 4 }, }, + /* 3*/ { { 11, 4 }, { 2, 4 }, }, + /* 4*/ { { 8, 2 }, { 9, 2 }, { 2, 3 }, { 12, 7 }, }, + /* 5*/ { { 0, 0 }, { 1, 0 }, }, + /* 6*/ { { 0, 1 }, { 7, 5 }, }, + /* 7*/ { { 1, 6 }, { 10, 6 }, { 6, 5 }, }, + /* 8*/ { { 4, 2 }, { 9, 2 }, }, + /* 9*/ { { 8, 2 }, { 4, 2 }, }, + /* 10*/ { { 7, 6 }, { 1, 6 }, }, + /* 11*/ { { 3, 4 }, { 2, 4 }, }, + /* 12*/ { { 4, 7 }, { 1, 8 }, }, +}; + +std::vector> movie_movie_edge_list { +{ 0, 1, 0}, +{ 0, 5, 0}, +{ 0, 6, 1}, +{ 1, 0, 0}, +{ 1, 5, 0}, +{ 1, 7, 6}, +{ 1, 10, 6}, +{ 1, 12, 8}, +{ 2, 4, 3}, +{ 2, 3, 4}, +{ 2, 11, 4}, +{ 3, 11, 4}, +{ 3, 2, 4}, +{ 4, 8, 2}, +{ 4, 9, 2}, +{ 4, 2, 3}, +{ 4, 12, 7}, +{ 5, 0, 0}, +{ 5, 1, 0}, +{ 6, 0, 1}, +{ 6, 7, 5}, +{ 7, 1, 6}, +{ 7, 10, 6}, +{ 7, 6, 5}, +{ 8, 4, 2}, +{ 8, 9, 2}, +{ 9, 8, 2}, +{ 9, 4, 2}, +{ 10, 7, 6}, +{ 10, 1, 6}, +{ 11, 3, 4}, +{ 11, 2, 4}, +{ 12, 4, 7}, +{ 12, 1, 8}, +}; + +std::vector>> movie_movie_adjacency_list { + /* 0*/ { { 1, 0 }, { 5, 0 }, { 6, 1 }, }, + /* 1*/ { { 0, 0 }, { 5, 0 }, { 7, 6 }, { 10, 6 }, { 12, 8 },}, + /* 2*/ { { 4, 3 }, { 3, 4 }, { 11, 4 }, }, + /* 3*/ { { 11, 4 }, { 2, 4 }, }, + /* 4*/ { { 8, 2 }, { 9, 2 }, { 2, 3 }, { 12, 7 }, }, + /* 5*/ { { 0, 0 }, { 1, 0 }, }, + /* 6*/ { { 0, 1 }, { 7, 5 }, }, + /* 7*/ { { 1, 6 }, { 10, 6 }, { 6, 5 }, }, + /* 8*/ { { 4, 2 }, { 9, 2 }, }, + /* 9*/ { { 8, 2 }, { 4, 2 }, }, + /* 10*/ { { 7, 6 }, { 1, 6 }, }, + /* 11*/ { { 3, 4 }, { 2, 4 }, }, + /* 12*/ { { 4, 7 }, { 1, 8 }, }, +}; + +// clang-format on diff --git a/examples/CppCon2021/graphs/karate-graph.hpp b/examples/CppCon2021/graphs/karate-graph.hpp new file mode 100644 index 0000000..1396305 --- /dev/null +++ b/examples/CppCon2021/graphs/karate-graph.hpp @@ -0,0 +1,163 @@ +#pragma once + +#include +#include + +// clang-format off + +std::vector> karate_index_edge_list { +{ 1, 0 }, +{ 2, 0 }, +{ 3, 0 }, +{ 4, 0 }, +{ 5, 0 }, +{ 6, 0 }, +{ 7, 0 }, +{ 8, 0 }, +{ 10, 0 }, +{ 11, 0 }, +{ 12, 0 }, +{ 13, 0 }, +{ 17, 0 }, +{ 19, 0 }, +{ 21, 0 }, +{ 31, 0 }, +{ 2, 1 }, +{ 3, 1 }, +{ 7, 1 }, +{ 13, 1 }, +{ 17, 1 }, +{ 19, 1 }, +{ 21, 1 }, +{ 30, 1 }, +{ 3, 2 }, +{ 7, 2 }, +{ 8, 2 }, +{ 9, 2 }, +{ 13, 2 }, +{ 27, 2 }, +{ 28, 2 }, +{ 32, 2 }, +{ 7, 3 }, +{ 12, 3 }, +{ 13, 3 }, +{ 6, 4 }, +{ 10, 4 }, +{ 6, 5 }, +{ 10, 5 }, +{ 16, 5 }, +{ 16, 6 }, +{ 30, 8 }, +{ 32, 8 }, +{ 33, 8 }, +{ 33, 9 }, +{ 33, 13 }, +{ 32, 14 }, +{ 33, 14 }, +{ 32, 15 }, +{ 33, 15 }, +{ 32, 18 }, +{ 33, 18 }, +{ 33, 19 }, +{ 32, 20 }, +{ 33, 20 }, +{ 32, 22 }, +{ 33, 22 }, +{ 25, 23 }, +{ 27, 23 }, +{ 29, 23 }, +{ 32, 23 }, +{ 33, 23 }, +{ 25, 24 }, +{ 27, 24 }, +{ 31, 24 }, +{ 31, 25 }, +{ 29, 26 }, +{ 33, 26 }, +{ 33, 27 }, +{ 31, 28 }, +{ 33, 28 }, +{ 32, 29 }, +{ 33, 29 }, +{ 32, 30 }, +{ 33, 30 }, +{ 32, 31 }, +{ 33, 31 }, +{ 33, 32 }, +}; + +std::vector> karate_directed_adjacency_list { + /* 0*/ { }, + /* 1*/ { 0,}, + /* 2*/ { 0, 1,}, + /* 3*/ { 0, 1, 2,}, + /* 4*/ { 0,}, + /* 5*/ { 0,}, + /* 6*/ { 0, 4, 5,}, + /* 7*/ { 0, 1, 2, 3,}, + /* 8*/ { 0, 2,}, + /* 9*/ { 2,}, + /* 10*/ { 0, 4, 5,}, + /* 11*/ { 0,}, + /* 12*/ { 0, 3,}, + /* 13*/ { 0, 1, 2, 3,}, + /* 14*/ { }, + /* 15*/ { }, + /* 16*/ { 5, 6,}, + /* 17*/ { 0, 1,}, + /* 18*/ { }, + /* 19*/ { 0, 1,}, + /* 20*/ { }, + /* 21*/ { 0, 1,}, + /* 22*/ { }, + /* 23*/ { }, + /* 24*/ { }, + /* 25*/ { 23, 24,}, + /* 26*/ { }, + /* 27*/ { 2, 23, 24,}, + /* 28*/ { 2,}, + /* 29*/ { 23, 26,}, + /* 30*/ { 1, 8,}, + /* 31*/ { 0, 24, 25, 28,}, + /* 32*/ { 2, 8, 14, 15, 18, 20, 22, 23, 29, 30, 31,}, + /* 33*/ { 8, 9, 13, 14, 15, 18, 19, 20, 22, 23, 26, 27, 28, 29, 30, 31, 32,}, +}; + +std::vector> karate_undirected_adjacency_list { + /* 0*/ { 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 17, 19, 21, 31,}, + /* 1*/ { 0, 2, 3, 7, 13, 17, 19, 21, 30,}, + /* 2*/ { 0, 1, 3, 7, 8, 9, 13, 27, 28, 32,}, + /* 3*/ { 0, 1, 2, 7, 12, 13,}, + /* 4*/ { 0, 6, 10,}, + /* 5*/ { 0, 6, 10, 16,}, + /* 6*/ { 0, 4, 5, 16,}, + /* 7*/ { 0, 1, 2, 3,}, + /* 8*/ { 0, 2, 30, 32, 33,}, + /* 9*/ { 2, 33,}, + /* 10*/ { 0, 4, 5,}, + /* 11*/ { 0,}, + /* 12*/ { 0, 3,}, + /* 13*/ { 0, 1, 2, 3, 33,}, + /* 14*/ { 32, 33,}, + /* 15*/ { 32, 33,}, + /* 16*/ { 5, 6,}, + /* 17*/ { 0, 1,}, + /* 18*/ { 32, 33,}, + /* 19*/ { 0, 1, 33,}, + /* 20*/ { 32, 33,}, + /* 21*/ { 0, 1,}, + /* 22*/ { 32, 33,}, + /* 23*/ { 25, 27, 29, 32, 33,}, + /* 24*/ { 25, 27, 31,}, + /* 25*/ { 23, 24, 31,}, + /* 26*/ { 29, 33,}, + /* 27*/ { 2, 23, 24, 33,}, + /* 28*/ { 2, 31, 33,}, + /* 29*/ { 23, 26, 32, 33,}, + /* 30*/ { 1, 8, 32, 33,}, + /* 31*/ { 0, 24, 25, 28, 32, 33,}, + /* 32*/ { 2, 8, 14, 15, 18, 20, 22, 23, 29, 30, 31, 33,}, + /* 33*/ { 8, 9, 13, 14, 15, 18, 19, 20, 22, 23, 26, 27, 28, 29, 30, 31, 32,}, + }; + +// clang-format on diff --git a/examples/CppCon2021/graphs/ospf-graph.hpp b/examples/CppCon2021/graphs/ospf-graph.hpp new file mode 100644 index 0000000..f0cbcb6 --- /dev/null +++ b/examples/CppCon2021/graphs/ospf-graph.hpp @@ -0,0 +1,205 @@ +// +// Authors +// Copyright +// License +// No Warranty Disclaimer +// +#pragma once + +// Data and graph from OSPF example in Boost Graph Library book + +#include +#include +#include + +// clang-format off + +std::vector ospf_vertices { + "RT1", + "RT2", + "RT3", + "RT4", + "RT5", + "RT6", + "RT7", + "RT8", + "RT9", + "RT10", + "RT11", + "RT12", + "N1", + "N2", + "N3", + "N4", + "N6", + "N7", + "N8", + "N9", + "N10", + "N11", + "N12", + "N13", + "N14", + "N15", + "H1", +}; + +std::vector> ospf_edges { + {"RT1", "N1", 3}, + {"RT1", "N3", 1}, + {"RT2", "N2", 3}, + {"RT2", "N3", 1}, + {"RT3", "RT6", 8}, + {"RT3", "N3", 1}, + {"RT3", "N4", 2}, + {"RT4", "N3", 1}, + {"RT4", "RT5", 8}, + {"RT5", "RT4", 8}, + {"RT5", "RT6", 7}, + {"RT5", "RT7", 6}, + {"RT5", "N12", 8}, + {"RT5", "N13", 8}, + {"RT5", "N14", 8}, + {"RT6", "RT3", 6}, + {"RT6", "RT5", 6}, + {"RT6", "RT10", 7}, + {"RT7", "RT5", 6}, + {"RT7", "N6", 1}, + {"RT7", "N12", 2}, + {"RT7", "N15", 9}, + {"RT8", "N6", 1}, + {"RT8", "N7", 4}, + {"RT9", "N9", 1}, + {"RT9", "N11", 3}, + {"RT10", "RT6", 5}, + {"RT10", "N6", 1}, + {"RT10", "N8", 3}, + {"RT11", "N8", 2}, + {"RT11", "N9", 1}, + {"RT12", "N9", 1}, + {"RT12", "N10", 2}, + {"RT12", "H1", 10}, + {"N3", "RT1", 0}, + {"N3", "RT2", 0}, + {"N3", "RT3", 0}, + {"N3", "RT4", 0}, + {"N6", "RT7", 0}, + {"N6", "RT8", 0}, + {"N6", "RT10", 0}, + {"N8", "RT10", 0}, + {"N8", "RT11", 0}, + {"N9", "RT9", 0}, + {"N9", "RT11", 0}, + {"N9", "RT12", 0}, + }; + +std::vector> ospf_index_edge_list { +{ 0, 12, 3}, +{ 0, 14, 1}, +{ 1, 13, 3}, +{ 1, 14, 1}, +{ 2, 5, 8}, +{ 2, 14, 1}, +{ 2, 15, 2}, +{ 3, 14, 1}, +{ 3, 4, 8}, +{ 4, 3, 8}, +{ 4, 5, 7}, +{ 4, 6, 6}, +{ 4, 22, 8}, +{ 4, 23, 8}, +{ 4, 24, 8}, +{ 5, 2, 6}, +{ 5, 4, 6}, +{ 5, 9, 7}, +{ 6, 4, 6}, +{ 6, 16, 1}, +{ 6, 22, 2}, +{ 6, 25, 9}, +{ 7, 16, 1}, +{ 7, 17, 4}, +{ 8, 19, 1}, +{ 8, 21, 3}, +{ 9, 5, 5}, +{ 9, 16, 1}, +{ 9, 18, 3}, +{ 10, 18, 2}, +{ 10, 19, 1}, +{ 11, 19, 1}, +{ 11, 20, 2}, +{ 11, 26, 10}, +{ 14, 0, 0}, +{ 14, 1, 0}, +{ 14, 2, 0}, +{ 14, 3, 0}, +{ 16, 6, 0}, +{ 16, 7, 0}, +{ 16, 9, 0}, +{ 18, 9, 0}, +{ 18, 10, 0}, +{ 19, 8, 0}, +{ 19, 10, 0}, +{ 19, 11, 0}, +}; + +std::vector>> ospf_index_adjacency_list { + /* 0 */ { { 12, 3 },{ 14, 1 },}, + /* 1 */ { { 13, 3 },{ 14, 1 },}, + /* 2 */ { { 5, 8 },{ 14, 1 },{ 15, 2 },}, + /* 3 */ { { 14, 1 },{ 4, 8 },}, + /* 4 */ { { 3, 8 },{ 5, 7 },{ 6, 6 },{ 22, 8 },{ 23, 8 },{ 24, 8 },}, + /* 5 */ { { 2, 6 },{ 4, 6 },{ 9, 7 },}, + /* 6 */ { { 4, 6 },{ 16, 1 },{ 22, 2 },{ 25, 9 },}, + /* 7 */ { { 16, 1 },{ 17, 4 },}, + /* 8 */ { { 19, 1 },{ 21, 3 },}, + /* 9 */ { { 5, 5 },{ 16, 1 },{ 18, 3 },}, + /* 10 */ { { 18, 2 },{ 19, 1 },}, + /* 11 */ { { 19, 1 },{ 20, 2 },{ 26, 10 },}, + /* 12 */ { }, + /* 13 */ { }, + /* 14 */ { { 0, 0 },{ 1, 0 },{ 2, 0 },{ 3, 0 },}, + /* 15 */ { }, + /* 16 */ { { 6, 0 },{ 7, 0 },{ 9, 0 },}, + /* 17 */ { }, + /* 18 */ { { 9, 0 },{ 10, 0 },}, + /* 19 */ { { 8, 0 },{ 10, 0 },{ 11, 0 },}, + /* 20 */ { }, + /* 21 */ { }, + /* 22 */ { }, + /* 23 */ { }, + /* 24 */ { }, + /* 25 */ { }, + /* 26 */ { }, +}; + +std::vector> ospf_shortest_path_distances { + { "RT1", 7 }, + { "RT2", 7 }, + { "RT3", 6 }, + { "RT4", 7 }, + { "RT5", 6 }, + { "RT6", 0 }, + { "RT7", 8 }, + { "RT8", 8 }, + { "RT9", 11 }, + { "RT10", 7 }, + { "RT11", 10 }, + { "RT12", 11 }, + { "N1", 10 }, + { "N2", 10 }, + { "N3", 7 }, + { "N4", 8 }, + { "N6", 8 }, + { "N7", 12 }, + { "N8", 10 }, + { "N9", 11 }, + { "N10", 13 }, + { "N11", 14 }, + { "N12", 10 }, + { "N13", 14 }, + { "N14", 14 }, + { "N15", 17 }, + { "H1", 21 }, +}; + +// clang-format on diff --git a/examples/CppCon2021/graphs/spice-graph.hpp b/examples/CppCon2021/graphs/spice-graph.hpp new file mode 100644 index 0000000..157fb0f --- /dev/null +++ b/examples/CppCon2021/graphs/spice-graph.hpp @@ -0,0 +1,129 @@ +// +// Authors +// Copyright +// License +// No Warranty Disclaimer +// + +// Data and graph from Spice example in Cppcon slides + +#include +#include +#include + +// clang-format off + +std::vector spice_vertices { + "GND", + "n0", + "Vdd", + "n1", + "out", + "n2", +}; + +std::vector spice_elements { + "C1", + "AC", + "R2", + "R0", + "R1", + "L1", + "C0", + "R3", +}; + +std::vector> spice_elements_values { + { "C1", 1.e-6 }, + { "AC", 3 }, + { "R2", 1.e4 }, + { "R0", 2.7e3 }, + { "R1", 3.3e4 }, + { "L1", 1.e-4 }, + { "C0", 10.e-9}, + { "R3", 1.e3 }, +}; + +std::vector> spice_edges { + { "n0", "n1", "C1" }, + { "Vdd", "GND", "AC" }, + { "n0", "Vdd", "R2" }, + { "n2", "Vdd", "R0" }, + { "GND", "n2", "R1" }, + { "n2", "Vout", "L1" }, + { "GND", "n0", "C0"}, + { "n2", "n1", "R3"}, +}; + +std::vector> spice_edges_values { + { "n0", "n1", "C1", 1.e-6 }, + { "Vdd", "GND", "AC", 3 }, + { "n0", "Vdd", "R2", 1.e4 }, + { "n2", "Vdd", "R0", 2.7e3 }, + { "GND", "n2", "R1", 3.3e4 }, + { "n2", "Vout", "L1", 1.e-4 }, + { "GND", "n0", "C0", 10.e-9}, + { "n2", "n1", "R3", 1.e3 }, +}; + +std::vector> spice_index_edge_list { + { 1, 3, 0 }, + { 2, 0, 1 }, + { 1, 2, 2 }, + { 5, 2, 3 }, + { 0, 5, 4 }, + { 5, 4, 5 }, + { 0, 1, 6 }, + { 5, 3, 7 }, +}; + +std::vector> spice_property_edge_list { + { 1, 3, "C1" }, + { 2, 0, "AC" }, + { 1, 2, "R2" }, + { 5, 2, "R0" }, + { 0, 5, "R1" }, + { 5, 4, "L1" }, + { 0, 1, "C0" }, + { 5, 3, "R3" }, +}; + +std::vector> spice_property_edge_list_values { + { 1, 3, "C1", 1.e-6 }, + { 1, 0, "AC", 3 }, + { 1, 2, "R2", 1.e4 }, + { 5, 2, "R0", 2.7e3 }, + { 0, 5, "R1", 3.3e4 }, + { 5, 4, "L1", 1.e-4 }, + { 0, 1, "C0", 10.e-9}, + { 5, 3, "R3", 1.e3 }, +}; + +std::vector>> spice_index_adjacency_list { + /* 0 */ { { 1, 6 }, { 5, 4 } }, + /* 1 */ { { 2, 2 }, { 3, 0 } }, + /* 2 */ { { 0, 1 } }, + /* 3 */ { }, + /* 4 */ { }, + /* 5 */ { { 2, 3 }, { 4, 5 }, { 3, 7 } }, +}; + +std::vector>> spice_property_adjacency_list { + /* 0 */ { { 1, "C0" }, { 5, "R1" }, }, + /* 1 */ { { 2, "R2" }, { 3, "C1" } }, + /* 2 */ { { 0, "AC" } }, + /* 3 */ { }, + /* 4 */ { }, + /* 5 */ { { 2, "R0" }, { 4, "L1" }, { 3, "R3" } }, +}; + +std::vector>> spice_property_adjacency_list_values { + /* 0 */ { { 1, "C0", 10.e-9 }, { 5, "R1", 3.3e4 } }, + /* 1 */ { { 2, "R2", 1.e-4 }, { 3, "C1", 1.e-6 } }, + /* 2 */ { { 0, "AC", 3 } }, + /* 3 */ { }, + /* 4 */ { }, + /* 5 */ { { 2, "R0", 2.7e3 }, { 4, "L1", 1.e-4 }, { 3, "R3", 1.e3 } }, +}; + +// clang-format on diff --git a/examples/CppCon2021/include/utilities.hpp b/examples/CppCon2021/include/utilities.hpp new file mode 100644 index 0000000..65ee0cd --- /dev/null +++ b/examples/CppCon2021/include/utilities.hpp @@ -0,0 +1,322 @@ +// +// Authors +// Copyright +// License +// No Warranty Disclaimer +// + +#include +#include +#include +#include + +/** + * Tuple utilities to get the "cdr" of a tuple (for our purposes) + */ + +template +auto nth_cdr(std::tuple t) { + return [&](std::index_sequence) { + return std::tuple{std::get(t)...}; + }(std::make_index_sequence()); +} + +template +auto props(std::tuple t) { + return nth_cdr<2>(t); +} + +template +auto graph_edge(std::tuple t) { + return nth_cdr<1>(t); +} + +/** + * Fill a plain graph from edge list + */ +template +void push_back_plain_fill(const EdgeList& edge_list, Adjacency& adj, bool directed, size_t idx) { + const size_t jdx = (idx + 1) % 2; + + for (auto&& e : edge_list) { + + if (idx == 0) { + std::apply([&](size_t u, size_t v) { adj[u].emplace_back(v); }, e); + if (!directed) { + std::apply([&](size_t u, size_t v) { adj[v].emplace_back(u); }, e); + } + } else { + std::apply([&](size_t u, size_t v) { adj[v].emplace_back(u); }, e); + if (!directed) { + std::apply([&](size_t u, size_t v) { adj[u].emplace_back(v); }, e); + } + } + } +} + +/** + * Fill a non-plain graph from edge list + */ +template +void push_back_fill(const EdgeList& edge_list, Adjacency& adj, bool directed, size_t idx) { + const size_t jdx = (idx + 1) % 2; + + for (auto&& e : edge_list) { + + if (idx == 0) { + std::apply([&](size_t u, size_t v, auto... props) { adj[u].emplace_back(v, props...); }, e); + if (!directed) { + std::apply([&](size_t u, size_t v, auto... props) { adj[v].emplace_back(u, props...); }, e); + } + } else { + std::apply([&](size_t u, size_t v, auto... props) { adj[v].emplace_back(u, props...); }, e); + if (!directed) { + std::apply([&](size_t u, size_t v, auto... props) { adj[u].emplace_back(v, props...); }, e); + } + } + } +} + +/** + * Make a map from data to the index value of each element in its container + */ +template +auto make_index_map(const R& range) { + using value_type = std::ranges::range_value_t; + + std::map the_map; + for (size_t i = 0; i < size(range); ++i) { + the_map[range[i]] = i; + } + return the_map; +} + +/** + * Make an edge list with properties copied from original data, e.g., vector> + */ +//template