From 0a18b371aa04d026e25a4c7e71f028151242b7f5 Mon Sep 17 00:00:00 2001 From: Phil Ratzloff Date: Sun, 31 May 2026 17:40:32 -0400 Subject: [PATCH 1/6] feat(container): add dense adjacency_matrix container (C++20 + C++23 mdspan) include/graph/container/adjacency_matrix.hpp: - adjacency_matrix: portable C++20 dense matrix that models index_adjacency_list via the inner-value pattern (matrix is a random-access range of lazily-filtered row views; edge elements live in an owned dense plane so target_id/edge_value resolve with stable references) - md_adjacency_matrix: C++23 variant (guarded by __cpp_lib_mdspan) exposing the presence plane as a 2-D std::mdspan with natural m(u,v) access - O(1) has_edge/weight, O(n) per-vertex out-edge iteration, O(n^2) space tests/container/adjacency_matrix/test_adjacency_matrix.cpp: - static_asserts for adjacency_list / index_adjacency_list (weighted+unweighted) - directed/undirected/weighted behaviour, vertices(), out_edges(), target_id, edge_value - registered in tests/container/CMakeLists.txt --- include/graph/container/adjacency_matrix.hpp | 408 ++++++++++++++++++ tests/container/CMakeLists.txt | 3 + .../test_adjacency_matrix.cpp | 112 +++++ 3 files changed, 523 insertions(+) create mode 100644 include/graph/container/adjacency_matrix.hpp create mode 100644 tests/container/adjacency_matrix/test_adjacency_matrix.cpp diff --git a/include/graph/container/adjacency_matrix.hpp b/include/graph/container/adjacency_matrix.hpp new file mode 100644 index 0000000..2db636d --- /dev/null +++ b/include/graph/container/adjacency_matrix.hpp @@ -0,0 +1,408 @@ +#pragma once + +/** + * @file adjacency_matrix.hpp + * @brief Dense adjacency-matrix container that models the graph-v3 adjacency_list CPO concepts. + * + * This header provides two presentations of the same idea: + * + * 1. graph::container::adjacency_matrix + * A portable C++20 implementation. It owns two dense `n*n` planes: + * - a presence plane (`flags_`) giving O(1) `has_edge(u, v)`, and + * - an element plane (`cells_`) holding the edge element for each cell. + * The matrix models `index_adjacency_list` through the graph-v3 + * "inner value pattern": the container itself is a random-access range + * whose i-th element is a lightweight, lazily-filtered view over row @p i + * that yields only the edges that are present. Because edge elements live + * in the owned element plane, the per-vertex edge iterators yield stable + * references, which is what `target_id` / `edge_value` require. + * + * 2. graph::container::md_adjacency_matrix + * A C++23 variant (guarded by `__cpp_lib_mdspan`) that reuses the same + * owning storage but additionally exposes the dense presence plane through + * `std::mdspan`, giving natural `m(u, v)` element access and the option to + * select a memory layout (e.g. `layout_right` for contiguous out-edge + * rows). `std::mdspan` is used purely as a *view* over the owned + * `std::vector`; it is not the owning store and does not, by itself, + * model the graph CPO concepts. + * + * Concept wiring (how the CPOs resolve, no member `vertices()`/`edges()` needed): + * - `graph::vertices(g)` -> g is a random-access range -> inner-value pattern, + * wrapped automatically in a `vertex_descriptor_view`. + * `vertex_id` is the row index (integral) -> index list. + * - `graph::out_edges(g, u)` -> `u.inner_value(g)` is the row view, a forward range + * of edge-value elements -> wrapped in an + * `edge_descriptor_view`. + * - `graph::target_id(g, uv)`-> extracted from the stored row element: the column + * index (unweighted) or `pair::first` (weighted). + * - `graph::edge_value(g, uv)`-> the stored element's `pair::second` (weighted). + * + * Cost model (inherent to dense matrices): + * - Edge existence / weight lookup: O(1). + * - Iterating the out-edges of a vertex: O(n) (the whole row is scanned, absent + * cells are skipped), regardless of the vertex degree. + * - Space: O(n^2). + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "graph/graph.hpp" +#include "graph/adj_list/vertex_descriptor_view.hpp" +#include "graph/adj_list/edge_descriptor_view.hpp" + +#if defined(__has_include) +# if __has_include() +# include +# endif +#endif + +namespace graph::container { + +namespace _amx_detail { + + /// Presence flag storage type (1 byte; 0 = absent, 1 = present). + using flag_t = std::uint8_t; + + /// Edge element stored per cell: the target id (unweighted) or {target, weight} (weighted). + template + using edge_element_t = std::conditional_t, VId, std::pair>; + + /** + * @brief Forward iterator over the present edges of a single matrix row. + * + * The iterator stores raw pointers into the matrix's owning planes plus the + * current column. Dereferencing returns a *reference* into the owned element + * plane (`const edge_element_t&`), so the value stays alive independently of + * the (lightweight, non-owning) row view that produced the iterator. That is + * exactly what `edge_descriptor_view` and the `target_id` / `edge_value` CPOs + * require. + */ + template + class row_edge_iterator { + public: + using value_type = edge_element_t; + using reference = const value_type&; + using pointer = const value_type*; + using difference_type = std::ptrdiff_t; + using iterator_concept = std::forward_iterator_tag; + using iterator_category = std::forward_iterator_tag; + + constexpr row_edge_iterator() noexcept = default; + + constexpr row_edge_iterator(const flag_t* flags, const value_type* cells, VId n, VId col) noexcept + : flags_(flags), cells_(cells), n_(n), col_(col) { + skip_absent(); + } + + [[nodiscard]] constexpr reference operator*() const noexcept { return cells_[col_]; } + [[nodiscard]] constexpr pointer operator->() const noexcept { return cells_ + col_; } + + constexpr row_edge_iterator& operator++() noexcept { + ++col_; + skip_absent(); + return *this; + } + + constexpr row_edge_iterator operator++(int) noexcept { + row_edge_iterator tmp = *this; + ++(*this); + return tmp; + } + + [[nodiscard]] friend constexpr bool operator==(const row_edge_iterator& lhs, + const row_edge_iterator& rhs) noexcept { + return lhs.col_ == rhs.col_; + } + + private: + constexpr void skip_absent() noexcept { + while (col_ < n_ && flags_[col_] == flag_t{0}) { + ++col_; + } + } + + const flag_t* flags_ = nullptr; + const value_type* cells_ = nullptr; + VId n_ = 0; + VId col_ = 0; + }; + + /** + * @brief Lightweight, non-owning forward range over the present edges of one row. + * + * This is the element type produced by the matrix's vertex iterator (the + * "inner value" in graph-v3 terms). It owns nothing; it just points into the + * matrix's dense planes. Returning it by value is safe because the edge + * iterators it hands out are self-contained and reference the owned planes. + */ + template + class row_view : public std::ranges::view_interface> { + public: + using element_type = edge_element_t; + using iterator = row_edge_iterator; + using const_iterator = iterator; + + constexpr row_view() noexcept = default; + + constexpr row_view(const flag_t* flags, const element_type* cells, VId n) noexcept + : flags_(flags), cells_(cells), n_(n) {} + + [[nodiscard]] constexpr iterator begin() const noexcept { return iterator{flags_, cells_, n_, VId{0}}; } + [[nodiscard]] constexpr iterator end() const noexcept { return iterator{flags_, cells_, n_, n_}; } + + private: + const flag_t* flags_ = nullptr; + const element_type* cells_ = nullptr; + VId n_ = 0; + }; + + /** + * @brief Random-access iterator over the rows of an adjacency matrix. + * + * Each dereference produces a `row_view` by value (a proxy reference, like + * `std::ranges::iota_view`). This makes the matrix itself a random-access + * range, which is exactly what the graph-v3 inner-value pattern keys on to + * synthesise `vertices(g)` and integral, index-based vertex ids. + */ + template + class vertex_iterator { + using cell_type = edge_element_t; + + public: + using value_type = row_view; + using reference = row_view; // proxy prvalue + using difference_type = std::ptrdiff_t; + using iterator_concept = std::random_access_iterator_tag; + using iterator_category = std::random_access_iterator_tag; + + constexpr vertex_iterator() noexcept = default; + + constexpr vertex_iterator(const flag_t* flags, const cell_type* cells, VId n, VId row) noexcept + : flags_(flags), cells_(cells), n_(n), row_(row) {} + + [[nodiscard]] constexpr reference operator*() const noexcept { + const std::size_t offset = static_cast(row_) * static_cast(n_); + return row_view{flags_ + offset, cells_ + offset, n_}; + } + + [[nodiscard]] constexpr reference operator[](difference_type d) const noexcept { return *(*this + d); } + + constexpr vertex_iterator& operator++() noexcept { + ++row_; + return *this; + } + constexpr vertex_iterator operator++(int) noexcept { + vertex_iterator tmp = *this; + ++row_; + return tmp; + } + constexpr vertex_iterator& operator--() noexcept { + --row_; + return *this; + } + constexpr vertex_iterator operator--(int) noexcept { + vertex_iterator tmp = *this; + --row_; + return tmp; + } + + constexpr vertex_iterator& operator+=(difference_type d) noexcept { + row_ = static_cast(static_cast(row_) + d); + return *this; + } + constexpr vertex_iterator& operator-=(difference_type d) noexcept { return *this += -d; } + + [[nodiscard]] friend constexpr vertex_iterator operator+(vertex_iterator it, difference_type d) noexcept { + return it += d; + } + [[nodiscard]] friend constexpr vertex_iterator operator+(difference_type d, vertex_iterator it) noexcept { + return it += d; + } + [[nodiscard]] friend constexpr vertex_iterator operator-(vertex_iterator it, difference_type d) noexcept { + return it -= d; + } + [[nodiscard]] friend constexpr difference_type operator-(const vertex_iterator& lhs, + const vertex_iterator& rhs) noexcept { + return static_cast(lhs.row_) - static_cast(rhs.row_); + } + + [[nodiscard]] friend constexpr bool operator==(const vertex_iterator& lhs, const vertex_iterator& rhs) noexcept { + return lhs.row_ == rhs.row_; + } + [[nodiscard]] friend constexpr std::strong_ordering operator<=>(const vertex_iterator& lhs, + const vertex_iterator& rhs) noexcept { + return lhs.row_ <=> rhs.row_; + } + + private: + const flag_t* flags_ = nullptr; + const cell_type* cells_ = nullptr; + VId n_ = 0; + VId row_ = 0; + }; + +} // namespace _amx_detail + +/** + * @ingroup graph_containers + * @brief Dense adjacency-matrix graph container (portable C++20). + * + * @tparam EV Edge value (weight) type, or `void` for an unweighted graph. + * @tparam VId Vertex id / index type (must be integral). Defaults to `uint32_t`. + * @tparam Directed When true (default) `add_edge(u, v)` adds only `u -> v`; when + * false the reciprocal `v -> u` is added as well (symmetric matrix). + * + * The number of vertices is fixed at construction (a matrix is `order x order`). + * Edges are added/removed by setting cells; storage never reallocates afterwards, + * so the row views and their iterators remain valid for the matrix's lifetime. + */ +template +requires std::integral +class adjacency_matrix { + static constexpr bool weighted = !std::is_void_v; + using flag_t = _amx_detail::flag_t; + using element_type = _amx_detail::edge_element_t; + +public: + using vertex_id_type = VId; + using size_type = std::size_t; + using iterator = _amx_detail::vertex_iterator; + using const_iterator = _amx_detail::vertex_iterator; + + /// Construct an @p order x @p order matrix with no edges. + constexpr explicit adjacency_matrix(VId order = VId{0}) + : n_(order) + , flags_(static_cast(order) * static_cast(order), flag_t{0}) + , cells_(static_cast(order) * static_cast(order), element_type{}) {} + + // ---- range interface (drives the inner-value pattern) ------------------- + + [[nodiscard]] constexpr iterator begin() noexcept { return make_iter(VId{0}); } + [[nodiscard]] constexpr iterator end() noexcept { return make_iter(n_); } + [[nodiscard]] constexpr const_iterator begin() const noexcept { return make_iter(VId{0}); } + [[nodiscard]] constexpr const_iterator end() const noexcept { return make_iter(n_); } + [[nodiscard]] constexpr std::size_t size() const noexcept { return static_cast(n_); } + + /// Row access, required by the graph-v3 `underlying_value` machinery. + [[nodiscard]] constexpr _amx_detail::row_view operator[](std::size_t row) const noexcept { + const std::size_t offset = row * static_cast(n_); + return _amx_detail::row_view{flags_.data() + offset, cells_.data() + offset, n_}; + } + + // ---- queries ------------------------------------------------------------ + + [[nodiscard]] constexpr VId num_vertices() const noexcept { return n_; } + [[nodiscard]] constexpr VId order() const noexcept { return n_; } + [[nodiscard]] constexpr std::size_t num_edges() const noexcept { return edge_count_; } + + [[nodiscard]] constexpr bool has_edge(VId u, VId v) const noexcept { return flags_[index(u, v)] != flag_t{0}; } + + /// Edge weight of (u, v); only available for weighted matrices. + template + requires weighted + [[nodiscard]] constexpr const E& weight(VId u, VId v) const noexcept { + return cells_[index(u, v)].second; + } + + // ---- mutation ----------------------------------------------------------- + + /// Add an unweighted edge u -> v (and v -> u when undirected). + template + requires(!W) + constexpr void add_edge(VId u, VId v) { + set_cell(u, v); + if constexpr (!Directed) { + set_cell(v, u); + } + } + + /// Add a weighted edge u -> v (and v -> u when undirected). + template + requires weighted + constexpr void add_edge(VId u, VId v, E value) { + set_cell(u, v); + if constexpr (!Directed) { + set_cell(v, u); + cells_[index(v, u)].second = value; + } + cells_[index(u, v)].second = std::move(value); + } + +protected: + [[nodiscard]] constexpr const flag_t* flags_data() const noexcept { return flags_.data(); } + +private: + [[nodiscard]] constexpr std::size_t index(VId u, VId v) const noexcept { + return static_cast(u) * static_cast(n_) + static_cast(v); + } + + constexpr void set_cell(VId u, VId v) { + const std::size_t idx = index(u, v); + flag_t& cell = flags_[idx]; + if (cell == flag_t{0}) { + cell = flag_t{1}; + ++edge_count_; + } + if constexpr (weighted) { + cells_[idx].first = v; + } else { + cells_[idx] = v; + } + } + + [[nodiscard]] constexpr iterator make_iter(VId row) const noexcept { + return iterator{flags_.data(), cells_.data(), n_, row}; + } + + VId n_ = 0; + std::size_t edge_count_ = 0; + std::vector flags_; + std::vector cells_; +}; + +#if defined(__cpp_lib_mdspan) + +/** + * @ingroup graph_containers + * @brief C++23 adjacency matrix that adds a `std::mdspan` access layer. + * + * Identical concept wiring and owning storage as @ref adjacency_matrix, but the + * dense presence plane is additionally exposed as a 2-D `std::mdspan` view. This + * gives ergonomic, zero-overhead `m(u, v)` element access and lets callers + * reason about layout (`layout_right` keeps each out-edge row contiguous). The + * `mdspan` is a *view* over the owned `std::vector`; it never owns the data and + * is re-formed on demand so it cannot dangle across mutation. + */ +template +requires std::integral +class md_adjacency_matrix : public adjacency_matrix { + using base = adjacency_matrix; + using flag_t = _amx_detail::flag_t; + +public: + using extents_type = std::dextents; + using flag_mdspan = std::mdspan; + + using base::base; + + /// 2-D view of the presence plane: `presence()[u, v] != 0` iff edge u->v exists. + [[nodiscard]] flag_mdspan presence() const noexcept { + const std::size_t n = static_cast(this->order()); + return flag_mdspan{this->flags_data(), n, n}; + } + + /// Natural element access: true iff edge (u, v) is present. + [[nodiscard]] bool operator()(VId u, VId v) const noexcept { + return presence()[static_cast(u), static_cast(v)] != flag_t{0}; + } +}; + +#endif // __cpp_lib_mdspan + +} // namespace graph::container diff --git a/tests/container/CMakeLists.txt b/tests/container/CMakeLists.txt index fced52c..d9ed1ab 100644 --- a/tests/container/CMakeLists.txt +++ b/tests/container/CMakeLists.txt @@ -61,6 +61,9 @@ add_executable(graph3_container_tests # dynamic_graph - CPO tests (edge map containers - vom, mom) dynamic_graph/test_dynamic_graph_cpo_edge_map.cpp + # adjacency_matrix + adjacency_matrix/test_adjacency_matrix.cpp + # undirected_adjacency_list undirected_adjacency_list/test_undirected_adjacency_list.cpp undirected_adjacency_list/test_undirected_adjacency_list_cpo.cpp diff --git a/tests/container/adjacency_matrix/test_adjacency_matrix.cpp b/tests/container/adjacency_matrix/test_adjacency_matrix.cpp new file mode 100644 index 0000000..0330d43 --- /dev/null +++ b/tests/container/adjacency_matrix/test_adjacency_matrix.cpp @@ -0,0 +1,112 @@ +#include + +#include "graph/container/adjacency_matrix.hpp" + +#include +#include + +using namespace graph; +using namespace graph::adj_list; +using namespace graph::container; + +// ============================================================================= +// Concept conformance +// ============================================================================= + +static_assert(std::ranges::random_access_range>, + "matrix must be a random-access range to drive the inner-value pattern"); +static_assert(adjacency_list>, "unweighted matrix must model adjacency_list"); +static_assert(index_adjacency_list>, "unweighted matrix must model index_adjacency_list"); +static_assert(adjacency_list>, "weighted matrix must model adjacency_list"); +static_assert(index_adjacency_list>, "weighted matrix must model index_adjacency_list"); + +// ============================================================================= +// Unweighted, directed +// ============================================================================= + +TEST_CASE("adjacency_matrix: unweighted directed basics", "[container][adjacency_matrix]") { + adjacency_matrix<> g(4); + g.add_edge(0, 1); + g.add_edge(0, 2); + g.add_edge(1, 2); + g.add_edge(2, 3); + + REQUIRE(g.num_vertices() == 4); + REQUIRE(g.num_edges() == 4); + REQUIRE(g.has_edge(0, 1)); + REQUIRE_FALSE(g.has_edge(1, 0)); + + SECTION("vertices() yields sequential integral ids") { + std::vector ids; + for (auto u : vertices(g)) { + ids.push_back(u.vertex_id()); + } + REQUIRE(ids == std::vector{0, 1, 2, 3}); + } + + SECTION("out_edges yields only present targets") { + auto collect = [&](auto u) { + std::vector targets; + for (auto uv : out_edges(g, u)) { + targets.push_back(static_cast(target_id(g, uv))); + } + std::ranges::sort(targets); + return targets; + }; + + auto vs = vertices(g); + auto it = vs.begin(); + REQUIRE(collect(*it) == std::vector{1, 2}); // vertex 0 + ++it; + REQUIRE(collect(*it) == std::vector{2}); // vertex 1 + ++it; + REQUIRE(collect(*it) == std::vector{3}); // vertex 2 + ++it; + REQUIRE(collect(*it).empty()); // vertex 3 has no out-edges + } +} + +// ============================================================================= +// Unweighted, undirected (symmetric) +// ============================================================================= + +TEST_CASE("adjacency_matrix: undirected adds reciprocal edge", "[container][adjacency_matrix]") { + adjacency_matrix g(3); + g.add_edge(0, 1); + g.add_edge(1, 2); + + REQUIRE(g.has_edge(0, 1)); + REQUIRE(g.has_edge(1, 0)); + REQUIRE(g.has_edge(1, 2)); + REQUIRE(g.has_edge(2, 1)); + REQUIRE(g.num_edges() == 4); +} + +// ============================================================================= +// Weighted +// ============================================================================= + +TEST_CASE("adjacency_matrix: weighted edges expose target + value", "[container][adjacency_matrix]") { + adjacency_matrix g(3); + g.add_edge(0, 1, 1.5); + g.add_edge(0, 2, 2.5); + g.add_edge(1, 2, 3.5); + + REQUIRE(g.weight(0, 1) == 1.5); + REQUIRE(g.weight(1, 2) == 3.5); + + auto vs = vertices(g); + auto u0 = *vs.begin(); + + // out_edges yields the present targets; the weight is recovered via the + // edge_value CPO (the stored edge element is a {target, weight} pair). + std::vector> edges; + for (auto uv : out_edges(g, u0)) { + edges.emplace_back(static_cast(target_id(g, uv)), edge_value(g, uv)); + } + std::ranges::sort(edges); + + REQUIRE(edges.size() == 2); + REQUIRE(edges[0] == std::pair{1, 1.5}); + REQUIRE(edges[1] == std::pair{2, 2.5}); +} From adb151f99cdec20524376c4533051270b84d1344 Mon Sep 17 00:00:00 2001 From: Phil Ratzloff Date: Sun, 31 May 2026 17:44:07 -0400 Subject: [PATCH 2/6] feat(container): adjacency_matrix edge-range constructor + docs Add a dynamic_graph-style constructor taking the matrix order plus a range of edges and a projection to copyable_edge_t, used to initialize the edges. Add user-guide documentation for adjacency_matrix and md_adjacency_matrix, and mark the adjacency_matrix gap addressed in the BGL migration scorecard. --- agents/bgl_migration_strategy.md | 9 +- docs/user-guide/containers.md | 167 +++++++++++++++--- include/graph/container/adjacency_matrix.hpp | 33 ++++ .../test_adjacency_matrix.cpp | 38 ++++ 4 files changed, 221 insertions(+), 26 deletions(-) diff --git a/agents/bgl_migration_strategy.md b/agents/bgl_migration_strategy.md index 678800b..2a76cde 100644 --- a/agents/bgl_migration_strategy.md +++ b/agents/bgl_migration_strategy.md @@ -61,7 +61,6 @@ graph-v3 is a ground-up C++20 redesign targeting ISO standardization (P3126–P3 - No `subgraph` hierarchy with descriptor mapping - 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) -- 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) @@ -132,7 +131,7 @@ These are behavioral analogues, not strict one-to-one translations. In particula | BGL Container | Status in graph-v3 | Migration Path | |---------------|--------------------|--------------------| -| **`adjacency_matrix`** | ❌ Not available | Use `dynamic_graph` with `os`/`ous` edge container for O(log n)/O(1) edge lookup; or implement `adjacency_matrix` | +| **`adjacency_matrix`** | ✅ `adjacency_matrix` | Direct replacement — dense `n x n` container (C++20; C++23 `md_adjacency_matrix` adds an `mdspan` view). Build from an order or an edge range + projection | | **`directed_graph`** | ❌ Not available | Use `dynamic_graph` for a directed graph, or `true` when you also need in-edge traversal | | **`undirected_graph`** | ✅ `undirected_adjacency_list` | Direct replacement | | **`labeled_graph`** | ❌ Not available | Use map-based `dynamic_graph` with `mo*` traits and string keys; or implement as an adaptor | @@ -1189,14 +1188,13 @@ These items block migration for the largest number of BGL users: | Item | Type | Effort | Rationale | |------|------|--------|-----------| -| **`adjacency_matrix` container** | Container | High | BGL users relying on matrix storage have no direct equivalent | | **A\* Search** | Algorithm | Medium | Heavily used in pathfinding, robotics, game AI | | **`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/`), composable visitor toolkit (`visitor_factory.hpp`: `make_visitor`, single-event adaptors, `predecessor_recorder`, `distance_recorder`, `time_stamper`), `valid_visitor` strict concept with `static_assert` diagnostics in BFS/DFS/Dijkstra/Bellman-Ford. +> **Done since the previous revision of this plan:** `adjacency_matrix` dense container (C++20 + C++23 `mdspan` variant), `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/`), composable visitor toolkit (`visitor_factory.hpp`: `make_visitor`, single-event adaptors, `predecessor_recorder`, `distance_recorder`, `time_stamper`), `valid_visitor` strict concept with `static_assert` diagnostics in BFS/DFS/Dijkstra/Bellman-Ford. ### Phase 2: Common Algorithm Coverage @@ -1217,7 +1215,6 @@ These items block migration for the largest number of BGL users: | Item | Type | Effort | Rationale | |------|------|--------|-----------| -| **`adjacency_matrix`** | Container | Medium | Dense graph storage | | **`subgraph_view`** | Adaptor | High | Hierarchical subgraph with descriptor mapping | | **`labeled_graph` adaptor** | Adaptor | Medium | String label → vertex mapping | | **Boyer-Myrvold Planarity** | Algorithm | High | Planarity testing | @@ -1311,7 +1308,7 @@ The scores below are directional editorial estimates, not audited counts. | Category | BGL Features | graph-v3 Coverage | Score | |----------|-------------|-------------------|-------| -| **Core graph types** | 8 containers | 3 containers + zero-config | 60% | +| **Core graph types** | 8 containers | 4 containers + zero-config | 70% | | **Concepts & traits** | 18+ concepts | 9 concepts (broader) | 80% (by design) | | **Property system** | Interior + exterior + bundled (tag-dispatched) | Single `VV`/`EV` value (struct for multiple fields) + `container_value_fn` / `vertex_property_map` for external maps; tag dispatch eliminated by design | 100% (by design) | | **Traversal algorithms** | BFS, DFS, undirected DFS, topological sort | BFS, DFS, topological sort + lazy views | 100% | diff --git a/docs/user-guide/containers.md b/docs/user-guide/containers.md index 4e30db8..371e703 100644 --- a/docs/user-guide/containers.md +++ b/docs/user-guide/containers.md @@ -18,14 +18,15 @@ adjacency list concepts so all CPOs, views, and algorithms work interchangeably. | [`dynamic_graph`](#1-dynamic_graph) | Traits-configured vertex + edge containers | Mutable | General purpose, flexible container choice | | [`compressed_graph`](#2-compressed_graph) | CSR (Compressed Sparse Row) | Immutable after construction | Read-only, high performance, memory-compact | | [`undirected_adjacency_list`](#3-undirected_adjacency_list) | Dual doubly-linked lists per edge | Mutable, O(1) edge removal | Undirected graphs, frequent edge insertion/removal | +| [`adjacency_matrix`](#4-adjacency_matrix) | Dense `n x n` cell array | Fixed order, mutable cells | Small/dense graphs, O(1) edge existence queries | -All three live in `graph::container`. +All four live in `graph::container`. You can also use graphs without any library container: -- **[Range-of-Ranges Graphs](#4-range-of-ranges-graphs-no-library-graph-container-required)** — use standard containers (e.g. `vector>`) directly as graphs, zero-copy -- **[Custom Graphs](#5-custom-graphs)** — adapt your own graph data structure by overriding graph CPOs via ADL -- **[Container Selection Guide](#6-container-selection-guide)** — decision tree and comparison matrix +- **[Range-of-Ranges Graphs](#5-range-of-ranges-graphs-no-library-graph-container-required)** — use standard containers (e.g. `vector>`) directly as graphs, zero-copy +- **[Custom Graphs](#6-custom-graphs)** — adapt your own graph data structure by overriding graph CPOs via ADL +- **[Container Selection Guide](#7-container-selection-guide)** — decision tree and comparison matrix --- @@ -529,7 +530,130 @@ g.remove_vertex(2u); // O(V+E): renumbers higher vertex ids --- -## 4. Range-of-Ranges Graphs (No Library Graph Container Required) +## 4. `adjacency_matrix` + +```cpp +#include + +namespace graph::container { +template // false adds the reciprocal edge too +class adjacency_matrix; +} +``` + +`adjacency_matrix` is a **dense** container: it owns an `n x n` block of cells, +one per ordered vertex pair. This makes edge-existence and weight lookups O(1) +at the cost of O(n²) space, so it suits **small or dense** graphs and algorithms +that probe arbitrary `(u, v)` pairs (e.g. Floyd–Warshall, transitive closure). + +The number of vertices (the matrix **order**) is fixed at construction. Edges +are added by setting cells; storage never reallocates afterwards, so iterators +and views stay valid for the matrix's lifetime. + +Like the other containers, it satisfies `index_adjacency_list`, so all CPOs, +views, and algorithms work. `vertices(g)` yields the integral row indices, and +`out_edges(g, u)` lazily scans row `u`, skipping absent cells. + +### Properties + +| Property | Value | +|----------|-------| +| Vertex ID assignment | Contiguous (0 .. order-1) | +| Vertex range | Random access | +| Edge range per vertex | Forward (filtered row view) | +| Order | Fixed at construction | +| Append vertices | No | +| Append edges | Yes (set cells) | + +### Complexity guarantees + +| Operation | Complexity | +|-----------|------------| +| `has_edge(u, v)` / `weight(u, v)` | O(1) | +| `add_edge(u, v[, val])` | O(1) | +| Iterate `out_edges(g, u)` | O(order) (whole row scanned) | +| `num_edges()` / `num_vertices()` | O(1) | +| Space | O(order²) | + +### Quick usage + +```cpp +#include +using namespace graph::container; + +// Unweighted, directed, 4 vertices +adjacency_matrix<> g(4); +g.add_edge(0, 1); +g.add_edge(0, 2); +g.add_edge(2, 3); + +bool e = g.has_edge(0, 1); // O(1) + +// Weighted (double) — weight recovered via the edge_value CPO or weight(u, v) +adjacency_matrix w(3); +w.add_edge(0, 1, 1.5); +double wt = w.weight(0, 1); // 1.5 + +// Undirected — adds the reciprocal edge automatically +adjacency_matrix u(3); +u.add_edge(0, 1); // both (0,1) and (1,0) now present +``` + +### Construction from an edge range + +As with `dynamic_graph` and `compressed_graph`, an `adjacency_matrix` can be +built directly from a range of edges plus a projection to +`copyable_edge_t` (`{source_id, target_id [, value]}`). The matrix is +sized to the supplied order; endpoints are **not** scanned to grow it, so every +projected id must be `< order`. + +```cpp +#include +using namespace graph::container; + +// Already copyable_edge_t — use std::identity (the default) +std::vector> ee = {{0,1},{0,2},{1,2},{2,3}}; +adjacency_matrix<> g(4, ee); + +// Project from your own edge type +struct raw_edge { int from, to; double w; }; +std::vector raw = {{0,1,1.5},{0,2,2.5},{1,2,3.5}}; +adjacency_matrix wg(3, raw, [](const raw_edge& e) { + return graph::copyable_edge_t{ + static_cast(e.from), static_cast(e.to), e.w}; +}); +``` + +### Template parameters + +| Parameter | Default | Meaning | +|-----------|---------|---------| +| `EV` | `void` | Edge value (weight) type; `void` for an unweighted graph | +| `VId` | `uint32_t` | Vertex id / index type (must be integral) | +| `Directed` | `true` | When `false`, `add_edge(u, v)` also adds `v -> u` (symmetric matrix) | + +### C++23 `mdspan` variant + +When compiled as C++23 with `` available, the header also defines +`md_adjacency_matrix`. It has identical concept wiring and +owning storage, but additionally exposes the dense presence plane as a 2-D +`std::mdspan` (`presence()`) and offers natural `m(u, v)` element access. The +`mdspan` is a *view* over the owned storage — it never owns the data. + +```cpp +#if defined(__cpp_lib_mdspan) +md_adjacency_matrix g(3); +g.add_edge(0, 1, 2.5); +bool present = g(0, 1); // true +auto plane = g.presence(); // std::mdspan> +#endif +``` + +--- + +## 5. Range-of-Ranges Graphs (No Library Graph Container Required) You do not need `dynamic_graph`, `compressed_graph`, or any library container to use graph-v3. Any **range-of-ranges** whose elements follow a recognised @@ -711,7 +835,7 @@ for all range-of-ranges graphs without any extra annotation. --- -## 5. Custom Graphs +## 6. Custom Graphs If you have an existing graph data structure that does not model a range-of-ranges, you can still use it with all library views and algorithms by overriding the graph CPOs for your type. @@ -779,7 +903,7 @@ order and advanced customization patterns. --- -## 6. Container Selection Guide +## 7. Container Selection Guide ``` ┌─ Already have data in → range-of-ranges @@ -792,6 +916,9 @@ order and advanced customization patterns. │ with O(1) removal or │ mutable edge properties? Start ────────┤ + │ + ├─ Small or dense graph with → adjacency_matrix + │ O(1) (u,v) existence queries? (O(n²) space, fixed order) │ ├─ Graph is read-only → compressed_graph │ after construction? (smallest memory, best cache) @@ -805,19 +932,19 @@ edges must be duplicated (e.g. edges A/B and B/A for vertices A and B). Properti duplicated, or handled in a way that avoids duplication (e.g. a `std::shared_ptr` to a property struct), if they are mutable. -| Criterion | `dynamic_graph` | `compressed_graph` | `undirected_adjacency_list` | range-of-ranges | -|-----------|-----------------|--------------------|-----------------------------|------------------| -| Add/remove vertices | Yes | No | Yes | Depends on outer container | -| Add/remove edges | Yes | No | Yes (O(1) remove) | Depends on inner container | -| Directed | Yes | Yes | No (undirected) | Yes | -| Undirected | Yes (duplicated edges) | Yes (duplicated edges) | Yes | Yes (duplicated edges) | -| Mutable Properties | Directed (Yes), Undirected (No) | Directed (Yes), Undirected (No) | Yes | Directed (Yes), Undirected (No); your data | -| Memory efficiency | Medium | Best (CSR) | Highest overhead | Zero overhead (existing data) | -| Cache locality | Depends on trait | Excellent | Poor (linked-list) | Depends on containers used | -| Multi-partite | No | Yes | No | No | -| Container flexibility | 27 trait combos | Fixed (CSR) | Configurable random access vertex container | Any forward_range of forward_ranges | - -**Custom graphs.** See [Section 5 (Custom Graphs)](#5-custom-graphs) for how to use your own +| Criterion | `dynamic_graph` | `compressed_graph` | `undirected_adjacency_list` | `adjacency_matrix` | range-of-ranges | +|-----------|-----------------|--------------------|-----------------------------|--------------------|------------------| +| Add/remove vertices | Yes | No | Yes | No (fixed order) | Depends on outer container | +| Add/remove edges | Yes | No | Yes (O(1) remove) | Add only (set cells) | Depends on inner container | +| Directed | Yes | Yes | No (undirected) | Yes | Yes | +| Undirected | Yes (duplicated edges) | Yes (duplicated edges) | Yes | Yes (`Directed=false`) | Yes (duplicated edges) | +| Mutable Properties | Directed (Yes), Undirected (No) | Directed (Yes), Undirected (No) | Yes | Yes (cells) | Directed (Yes), Undirected (No); your data | +| Memory efficiency | Medium | Best (CSR) | Highest overhead | O(n²) (dense) | Zero overhead (existing data) | +| Cache locality | Depends on trait | Excellent | Poor (linked-list) | Excellent (contiguous) | Depends on containers used | +| Multi-partite | No | Yes | No | No | No | +| Container flexibility | 27 trait combos | Fixed (CSR) | Configurable random access vertex container | Fixed (dense) | Any forward_range of forward_ranges | + +**Custom graphs.** See [Section 6 (Custom Graphs)](#6-custom-graphs) for how to use your own graph data structure with all library views and algorithms by overriding graph CPOs. --- diff --git a/include/graph/container/adjacency_matrix.hpp b/include/graph/container/adjacency_matrix.hpp index 2db636d..a6db7f4 100644 --- a/include/graph/container/adjacency_matrix.hpp +++ b/include/graph/container/adjacency_matrix.hpp @@ -46,6 +46,7 @@ #include #include +#include #include #include #include @@ -281,6 +282,38 @@ class adjacency_matrix { , flags_(static_cast(order) * static_cast(order), flag_t{0}) , cells_(static_cast(order) * static_cast(order), element_type{}) {} + /** + * @brief Construct an @p order x @p order matrix and load it from a range of edges. + * + * Each element of @p erng is projected to a `copyable_edge_t` (i.e. + * `{source_id, target_id [, value]}`) by @p eproj and inserted via `add_edge`. + * When the projected value type already is `copyable_edge_t`, + * `std::identity` (the default) can be used. This mirrors the edge-range + + * projection constructors of `dynamic_graph` and `compressed_graph`. + * + * The matrix is sized to @p order; edge endpoints are NOT scanned to grow it, + * so every projected `source_id` / `target_id` must be `< order`. + * + * @tparam ERng Range type of the source edge data. + * @tparam EProj Projection from an @p erng element to `copyable_edge_t`. + * + * @param order The number of vertices (matrix is `order x order`). + * @param erng The source range of edge data. + * @param eproj The projection function (or `std::identity` when already projected). + */ + template + requires copyable_edge>, VId, EV> + constexpr adjacency_matrix(VId order, ERng&& erng, EProj eproj = EProj{}) : adjacency_matrix(order) { + for (auto&& elem : erng) { + const copyable_edge_t& uv = eproj(elem); + if constexpr (weighted) { + add_edge(static_cast(uv.source_id), static_cast(uv.target_id), uv.value); + } else { + add_edge(static_cast(uv.source_id), static_cast(uv.target_id)); + } + } + } + // ---- range interface (drives the inner-value pattern) ------------------- [[nodiscard]] constexpr iterator begin() noexcept { return make_iter(VId{0}); } diff --git a/tests/container/adjacency_matrix/test_adjacency_matrix.cpp b/tests/container/adjacency_matrix/test_adjacency_matrix.cpp index 0330d43..3d06aa4 100644 --- a/tests/container/adjacency_matrix/test_adjacency_matrix.cpp +++ b/tests/container/adjacency_matrix/test_adjacency_matrix.cpp @@ -110,3 +110,41 @@ TEST_CASE("adjacency_matrix: weighted edges expose target + value", "[container] REQUIRE(edges[0] == std::pair{1, 1.5}); REQUIRE(edges[1] == std::pair{2, 2.5}); } + +// ============================================================================= +// Edge-range + projection constructor +// ============================================================================= + +TEST_CASE("adjacency_matrix: construct from copyable_edge range (identity)", "[container][adjacency_matrix]") { + std::vector> ee = {{0, 1}, {0, 2}, {1, 2}, {2, 3}}; + + adjacency_matrix<> g(4, ee); + + REQUIRE(g.num_vertices() == 4); + REQUIRE(g.num_edges() == 4); + REQUIRE(g.has_edge(0, 1)); + REQUIRE(g.has_edge(2, 3)); + REQUIRE_FALSE(g.has_edge(3, 0)); +} + +TEST_CASE("adjacency_matrix: construct from edge range with projection", "[container][adjacency_matrix]") { + struct raw_edge { + int from; + int to; + double w; + }; + std::vector ee = {{0, 1, 1.5}, {0, 2, 2.5}, {1, 2, 3.5}}; + + auto proj = [](const raw_edge& e) { + return graph::copyable_edge_t{static_cast(e.from), + static_cast(e.to), e.w}; + }; + + adjacency_matrix g(3, ee, proj); + + REQUIRE(g.num_edges() == 3); + REQUIRE(g.weight(0, 1) == 1.5); + REQUIRE(g.weight(0, 2) == 2.5); + REQUIRE(g.weight(1, 2) == 3.5); +} + From d275d630a0c3f36f6e6b7b81aa1a3404155e96cf Mon Sep 17 00:00:00 2001 From: Phil Ratzloff Date: Mon, 1 Jun 2026 22:33:03 -0400 Subject: [PATCH 3/6] Refine adjacency_matrix call semantics and tests --- include/graph/container/adjacency_matrix.hpp | 65 ++++++++++++++----- .../test_adjacency_matrix.cpp | 51 +++++++++++++-- 2 files changed, 92 insertions(+), 24 deletions(-) diff --git a/include/graph/container/adjacency_matrix.hpp b/include/graph/container/adjacency_matrix.hpp index a6db7f4..ea041b2 100644 --- a/include/graph/container/adjacency_matrix.hpp +++ b/include/graph/container/adjacency_matrix.hpp @@ -33,9 +33,8 @@ * - `graph::out_edges(g, u)` -> `u.inner_value(g)` is the row view, a forward range * of edge-value elements -> wrapped in an * `edge_descriptor_view`. - * - `graph::target_id(g, uv)`-> extracted from the stored row element: the column - * index (unweighted) or `pair::first` (weighted). - * - `graph::edge_value(g, uv)`-> the stored element's `pair::second` (weighted). + * - `graph::target_id(g, uv)`-> extracted from the edge iterator's current column. + * - `graph::edge_value(g, uv)`-> extracted from the referenced matrix cell (weighted). * * Cost model (inherent to dense matrices): * - Edge existence / weight lookup: O(1). @@ -46,11 +45,14 @@ #include #include +#include +#include #include #include #include #include #include +#include #include #include "graph/graph.hpp" @@ -70,9 +72,13 @@ namespace _amx_detail { /// Presence flag storage type (1 byte; 0 = absent, 1 = present). using flag_t = std::uint8_t; - /// Edge element stored per cell: the target id (unweighted) or {target, weight} (weighted). + /// Edge element stored per cell: empty marker (unweighted) or edge value (weighted). + /// + /// Design note: target-id is not stored in each cell. The edge iterator keeps + /// the current column, allowing `target_id` to be answered directly from + /// descriptor position while keeping a single lookup into the row storage. template - using edge_element_t = std::conditional_t, VId, std::pair>; + using edge_element_t = std::conditional_t, std::monostate, EV>; /** * @brief Forward iterator over the present edges of a single matrix row. @@ -103,6 +109,11 @@ namespace _amx_detail { [[nodiscard]] constexpr reference operator*() const noexcept { return cells_[col_]; } [[nodiscard]] constexpr pointer operator->() const noexcept { return cells_ + col_; } + [[nodiscard]] constexpr VId target_id() const noexcept { return col_; } + + [[nodiscard]] constexpr reference edge_value_ref() const noexcept { + return cells_[col_]; + } constexpr row_edge_iterator& operator++() noexcept { ++col_; @@ -334,13 +345,18 @@ class adjacency_matrix { [[nodiscard]] constexpr VId order() const noexcept { return n_; } [[nodiscard]] constexpr std::size_t num_edges() const noexcept { return edge_count_; } - [[nodiscard]] constexpr bool has_edge(VId u, VId v) const noexcept { return flags_[index(u, v)] != flag_t{0}; } + /// True iff edge (u, v) is present. + [[nodiscard]] constexpr bool exists(VId u, VId v) const noexcept { return flags_[index(u, v)] != flag_t{0}; } - /// Edge weight of (u, v); only available for weighted matrices. + /// Back-compat alias for existence checks. + [[nodiscard]] constexpr bool has_edge(VId u, VId v) const noexcept { return exists(u, v); } + + /// Natural read-only value access for edge (u, v); requires the edge to exist. template requires weighted - [[nodiscard]] constexpr const E& weight(VId u, VId v) const noexcept { - return cells_[index(u, v)].second; + [[nodiscard]] constexpr const E& operator()(VId u, VId v) const noexcept { + assert(exists(u, v)); + return cells_[index(u, v)]; } // ---- mutation ----------------------------------------------------------- @@ -360,11 +376,29 @@ class adjacency_matrix { requires weighted constexpr void add_edge(VId u, VId v, E value) { set_cell(u, v); + cells_[index(u, v)] = value; if constexpr (!Directed) { set_cell(v, u); - cells_[index(v, u)].second = value; + cells_[index(v, u)] = value; } - cells_[index(u, v)].second = std::move(value); + } + + template + requires std::same_as, adjacency_matrix> && + requires(const EdgeDesc& uv) { + uv.value().target_id(); + } + [[nodiscard]] friend constexpr auto target_id(G&& /*g*/, const EdgeDesc& uv) noexcept { + return uv.value().target_id(); + } + + template + requires weighted && std::same_as, adjacency_matrix> && + requires(const EdgeDesc& uv) { + uv.value().edge_value_ref(); + } + [[nodiscard]] friend constexpr decltype(auto) edge_value(G&& /*g*/, const EdgeDesc& uv) noexcept { + return uv.value().edge_value_ref(); } protected: @@ -382,11 +416,6 @@ class adjacency_matrix { cell = flag_t{1}; ++edge_count_; } - if constexpr (weighted) { - cells_[idx].first = v; - } else { - cells_[idx] = v; - } } [[nodiscard]] constexpr iterator make_iter(VId row) const noexcept { @@ -430,8 +459,8 @@ class md_adjacency_matrix : public adjacency_matrix { return flag_mdspan{this->flags_data(), n, n}; } - /// Natural element access: true iff edge (u, v) is present. - [[nodiscard]] bool operator()(VId u, VId v) const noexcept { + /// True iff edge (u, v) is present. + [[nodiscard]] bool exists(VId u, VId v) const noexcept { return presence()[static_cast(u), static_cast(v)] != flag_t{0}; } }; diff --git a/tests/container/adjacency_matrix/test_adjacency_matrix.cpp b/tests/container/adjacency_matrix/test_adjacency_matrix.cpp index 3d06aa4..24a3d44 100644 --- a/tests/container/adjacency_matrix/test_adjacency_matrix.cpp +++ b/tests/container/adjacency_matrix/test_adjacency_matrix.cpp @@ -3,8 +3,15 @@ #include "graph/container/adjacency_matrix.hpp" #include +#include #include +#if !defined(NDEBUG) && (defined(__unix__) || defined(__APPLE__)) +#include +#include +#include +#endif + using namespace graph; using namespace graph::adj_list; using namespace graph::container; @@ -34,6 +41,8 @@ TEST_CASE("adjacency_matrix: unweighted directed basics", "[container][adjacency REQUIRE(g.num_vertices() == 4); REQUIRE(g.num_edges() == 4); REQUIRE(g.has_edge(0, 1)); + REQUIRE(g.exists(0, 1)); + REQUIRE_FALSE(g.exists(3, 0)); REQUIRE_FALSE(g.has_edge(1, 0)); SECTION("vertices() yields sequential integral ids") { @@ -79,6 +88,9 @@ TEST_CASE("adjacency_matrix: undirected adds reciprocal edge", "[container][adja REQUIRE(g.has_edge(1, 0)); REQUIRE(g.has_edge(1, 2)); REQUIRE(g.has_edge(2, 1)); + REQUIRE(g.exists(0, 1)); + REQUIRE(g.exists(1, 0)); + REQUIRE_FALSE(g.exists(0, 2)); REQUIRE(g.num_edges() == 4); } @@ -92,14 +104,19 @@ TEST_CASE("adjacency_matrix: weighted edges expose target + value", "[container] g.add_edge(0, 2, 2.5); g.add_edge(1, 2, 3.5); - REQUIRE(g.weight(0, 1) == 1.5); - REQUIRE(g.weight(1, 2) == 3.5); + REQUIRE(g(0, 1) == 1.5); + REQUIRE(g(1, 2) == 3.5); + REQUIRE(g.exists(0, 1)); + REQUIRE_FALSE(g.exists(2, 0)); + + const adjacency_matrix& cg2 = g; + REQUIRE(cg2(0, 1) == 1.5); auto vs = vertices(g); auto u0 = *vs.begin(); // out_edges yields the present targets; the weight is recovered via the - // edge_value CPO (the stored edge element is a {target, weight} pair). + // edge_value CPO. std::vector> edges; for (auto uv : out_edges(g, u0)) { edges.emplace_back(static_cast(target_id(g, uv)), edge_value(g, uv)); @@ -111,6 +128,26 @@ TEST_CASE("adjacency_matrix: weighted edges expose target + value", "[container] REQUIRE(edges[1] == std::pair{2, 2.5}); } +#if !defined(NDEBUG) && (defined(__unix__) || defined(__APPLE__)) +TEST_CASE("adjacency_matrix: operator()(u,v) asserts on missing edge in debug", "[container][adjacency_matrix]") { + adjacency_matrix g(3); + g.add_edge(0, 1, 1.5); + + const pid_t pid = fork(); + REQUIRE(pid >= 0); + + if (pid == 0) { + (void)g(2, 0); // Missing edge -> debug assert should fire. + _exit(0); + } + + int status = 0; + REQUIRE(waitpid(pid, &status, 0) == pid); + REQUIRE(WIFSIGNALED(status)); + REQUIRE(WTERMSIG(status) == SIGABRT); +} +#endif + // ============================================================================= // Edge-range + projection constructor // ============================================================================= @@ -123,8 +160,10 @@ TEST_CASE("adjacency_matrix: construct from copyable_edge range (identity)", "[c REQUIRE(g.num_vertices() == 4); REQUIRE(g.num_edges() == 4); REQUIRE(g.has_edge(0, 1)); + REQUIRE(g.exists(0, 1)); REQUIRE(g.has_edge(2, 3)); REQUIRE_FALSE(g.has_edge(3, 0)); + REQUIRE_FALSE(g.exists(3, 0)); } TEST_CASE("adjacency_matrix: construct from edge range with projection", "[container][adjacency_matrix]") { @@ -143,8 +182,8 @@ TEST_CASE("adjacency_matrix: construct from edge range with projection", "[conta adjacency_matrix g(3, ee, proj); REQUIRE(g.num_edges() == 3); - REQUIRE(g.weight(0, 1) == 1.5); - REQUIRE(g.weight(0, 2) == 2.5); - REQUIRE(g.weight(1, 2) == 3.5); + REQUIRE(g(0, 1) == 1.5); + REQUIRE(g(0, 2) == 2.5); + REQUIRE(g(1, 2) == 3.5); } From 9431c32cfa37da9a79c9e4f5f6c381b4d39d2b1b Mon Sep 17 00:00:00 2001 From: Phil Ratzloff Date: Mon, 1 Jun 2026 22:46:53 -0400 Subject: [PATCH 4/6] Add Dijkstra coverage for adjacency_matrix --- .../test_adjacency_matrix.cpp | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/tests/container/adjacency_matrix/test_adjacency_matrix.cpp b/tests/container/adjacency_matrix/test_adjacency_matrix.cpp index 24a3d44..3c7c85b 100644 --- a/tests/container/adjacency_matrix/test_adjacency_matrix.cpp +++ b/tests/container/adjacency_matrix/test_adjacency_matrix.cpp @@ -1,5 +1,6 @@ #include +#include "graph/algorithm/dijkstra_shortest_paths.hpp" #include "graph/container/adjacency_matrix.hpp" #include @@ -117,15 +118,38 @@ TEST_CASE("adjacency_matrix: weighted edges expose target + value", "[container] // out_edges yields the present targets; the weight is recovered via the // edge_value CPO. - std::vector> edges; + std::vector> ee; for (auto uv : out_edges(g, u0)) { - edges.emplace_back(static_cast(target_id(g, uv)), edge_value(g, uv)); + ee.emplace_back(static_cast(target_id(g, uv)), edge_value(g, uv)); } - std::ranges::sort(edges); + std::ranges::sort(ee); - REQUIRE(edges.size() == 2); - REQUIRE(edges[0] == std::pair{1, 1.5}); - REQUIRE(edges[1] == std::pair{2, 2.5}); + REQUIRE(ee.size() == 2); + REQUIRE(ee[0] == std::pair{1, 1.5}); + REQUIRE(ee[1] == std::pair{2, 2.5}); +} + +TEST_CASE("adjacency_matrix: dijkstra shortest distances", "[container][adjacency_matrix][dijkstra]") { + adjacency_matrix g(4); + g.add_edge(0, 1, 1.0); + g.add_edge(0, 2, 4.0); + g.add_edge(1, 2, 2.0); + g.add_edge(1, 3, 6.0); + g.add_edge(2, 3, 3.0); + + std::vector distance(num_vertices(g)); + init_shortest_paths(g, distance); + + dijkstra_shortest_distances(g, vertex_id_t>(0), + container_value_fn(distance), + [](const auto& graph_ref, const auto& uv) { + return edge_value(graph_ref, uv); + }); + + REQUIRE(distance[0] == 0.0); + REQUIRE(distance[1] == 1.0); + REQUIRE(distance[2] == 3.0); + REQUIRE(distance[3] == 6.0); } #if !defined(NDEBUG) && (defined(__unix__) || defined(__APPLE__)) From 7b1139559559c56900c6848174cabae8c341c87f Mon Sep 17 00:00:00 2001 From: Phil Ratzloff Date: Mon, 1 Jun 2026 22:50:14 -0400 Subject: [PATCH 5/6] Update docs for adjacency_matrix API changes --- CHANGELOG.md | 2 ++ agents/bgl_migration_strategy.md | 4 +++- docs/user-guide/containers.md | 11 +++++++---- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6c06b9..774f861 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,9 +60,11 @@ - `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) +- **`adjacency_matrix` Dijkstra coverage test** — added container-level shortest-path validation for weighted `adjacency_matrix` using `dijkstra_shortest_distances` and `edge_value` CPO weight access (`tests/container/adjacency_matrix/test_adjacency_matrix.cpp`). ### Changed - **`undirected_adjacency_list` mutation API renamed** to match `dynamic_graph` and BGL conventions: `create_vertex` → `add_vertex`, `create_edge` → `add_edge`, `erase_edge` → `remove_edge`. The old member names were removed (no backward-compatible aliases); update call sites accordingly. +- **`adjacency_matrix` API cleanup** — edge-existence query is now `exists(u, v)` (with `has_edge(u, v)` retained as an alias); weighted direct access uses const `operator()(u, v)`; removed `weight(u, v)` and removed non-const `operator()(u, v)`. - **`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). - **`compressed_graph::vertices(g)` returns `iota_view`** — simplified to `std::ranges::iota_view(0, num_vertices())`, which the `vertices` CPO wraps automatically via `_wrap_if_needed`. - **`vertex_descriptor_view` CTAD deduction guides** — updated from `Container::iterator`/`const_iterator` to `std::ranges::iterator_t<>` for compatibility with views like `iota_view`. diff --git a/agents/bgl_migration_strategy.md b/agents/bgl_migration_strategy.md index 111c60e..326b881 100644 --- a/agents/bgl_migration_strategy.md +++ b/agents/bgl_migration_strategy.md @@ -2,7 +2,7 @@ A comprehensive analysis of the Boost Graph Library (BGL) and graph-v3, identifying migration paths, gaps, and recommended extensions to enable a smooth upgrade transition. -> **Last reviewed:** 2026-05-30 against `include/graph/` source tree. +> **Last reviewed:** 2026-06-01 against `include/graph/` source tree. --- @@ -106,6 +106,8 @@ graph-v3 is a ground-up C++20 redesign targeting ISO standardization (P3126–P3 | `compressed_sparse_row_graph` | `compressed_graph` | Both CSR; graph-v3 uses projection-based loading | | `edge_list` | Any `input_range` satisfying `basic_sourced_edgelist` | graph-v3 is more flexible — any range of edge-like tuples works | +> **`adjacency_matrix` API note (2026-06):** use `exists(u, v)` (or `has_edge(u, v)` alias) for edge presence checks. For weighted matrices, direct value read access is `const operator()(u, v)`. The older `weight(u, v)` helper and mutable `operator()(u, v)` are removed. + ### Closest Selector Analogues These are behavioral analogues, not strict one-to-one translations. In particular, classic BGL `mapS` / `hash_mapS` selectors map to set-like containers, whereas graph-v3 `om` / `oum` traits use true map / unordered_map edge containers keyed by target vertex ID. diff --git a/docs/user-guide/containers.md b/docs/user-guide/containers.md index 371e703..0b021e2 100644 --- a/docs/user-guide/containers.md +++ b/docs/user-guide/containers.md @@ -571,7 +571,8 @@ views, and algorithms work. `vertices(g)` yields the integral row indices, and | Operation | Complexity | |-----------|------------| -| `has_edge(u, v)` / `weight(u, v)` | O(1) | +| `exists(u, v)` / `has_edge(u, v)` | O(1) | +| `operator()(u, v)` (weighted, const) | O(1) | | `add_edge(u, v[, val])` | O(1) | | Iterate `out_edges(g, u)` | O(order) (whole row scanned) | | `num_edges()` / `num_vertices()` | O(1) | @@ -590,11 +591,13 @@ g.add_edge(0, 2); g.add_edge(2, 3); bool e = g.has_edge(0, 1); // O(1) +bool e2 = g.exists(0, 1); // O(1) -// Weighted (double) — weight recovered via the edge_value CPO or weight(u, v) +// Weighted (double) — value recovered via edge_value CPO or const operator()(u, v) adjacency_matrix w(3); w.add_edge(0, 1, 1.5); -double wt = w.weight(0, 1); // 1.5 +const auto& cw = w; +double wt = cw(0, 1); // 1.5 // Undirected — adds the reciprocal edge automatically adjacency_matrix u(3); @@ -646,7 +649,7 @@ owning storage, but additionally exposes the dense presence plane as a 2-D #if defined(__cpp_lib_mdspan) md_adjacency_matrix g(3); g.add_edge(0, 1, 2.5); -bool present = g(0, 1); // true +bool present = g.exists(0, 1); // true auto plane = g.presence(); // std::mdspan> #endif ``` From 2645e0e2d6e777d7b4ea93e2724025df44ebbc5d Mon Sep 17 00:00:00 2001 From: Phil Ratzloff Date: Mon, 1 Jun 2026 22:53:17 -0400 Subject: [PATCH 6/6] Clarify adjacency_matrix addition in changelog --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 774f861..da2c42a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,11 +60,10 @@ - `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) -- **`adjacency_matrix` Dijkstra coverage test** — added container-level shortest-path validation for weighted `adjacency_matrix` using `dijkstra_shortest_distances` and `edge_value` CPO weight access (`tests/container/adjacency_matrix/test_adjacency_matrix.cpp`). +- **`adjacency_matrix` container** (`container/adjacency_matrix.hpp`) — added dense `n x n` graph container (C++20) with optional C++23 `md_adjacency_matrix` `mdspan` view variant; supports weighted/unweighted graphs and models graph-v3 adjacency CPO concepts. ### Changed - **`undirected_adjacency_list` mutation API renamed** to match `dynamic_graph` and BGL conventions: `create_vertex` → `add_vertex`, `create_edge` → `add_edge`, `erase_edge` → `remove_edge`. The old member names were removed (no backward-compatible aliases); update call sites accordingly. -- **`adjacency_matrix` API cleanup** — edge-existence query is now `exists(u, v)` (with `has_edge(u, v)` retained as an alias); weighted direct access uses const `operator()(u, v)`; removed `weight(u, v)` and removed non-const `operator()(u, v)`. - **`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). - **`compressed_graph::vertices(g)` returns `iota_view`** — simplified to `std::ranges::iota_view(0, num_vertices())`, which the `vertices` CPO wraps automatically via `_wrap_if_needed`. - **`vertex_descriptor_view` CTAD deduction guides** — updated from `Container::iterator`/`const_iterator` to `std::ranges::iterator_t<>` for compatibility with views like `iota_view`.