diff --git a/CHANGELOG.md b/CHANGELOG.md index b6c06b9..da2c42a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,7 @@ - `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` 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. diff --git a/agents/bgl_migration_strategy.md b/agents/bgl_migration_strategy.md index ab8b50d..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. --- @@ -60,7 +60,6 @@ graph-v3 is a ground-up C++20 redesign targeting ISO standardization (P3126–P3 **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 `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) @@ -107,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. @@ -131,7 +132,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 | @@ -1201,13 +1202,12 @@ 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 | -> **Done since the previous revision of this plan:** `filtered_graph` adaptor, DOT/GraphML/JSON I/O plus full BGL I/O parity (DIMACS via `dimacs.hpp`, METIS via `metis.hpp`, adjacency-list text via `adjacency_list_text.hpp`), Erdős-Rényi G(n,p)/G(n,m) / Barabási-Albert / 2D grid / path / complete-graph / Watts-Strogatz / R-MAT / PLOD / SSCA#2 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 plus full BGL I/O parity (DIMACS via `dimacs.hpp`, METIS via `metis.hpp`, adjacency-list text via `adjacency_list_text.hpp`), Erdős-Rényi G(n,p)/G(n,m) / Barabási-Albert / 2D grid / path / complete-graph / Watts-Strogatz / R-MAT / PLOD / SSCA#2 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 @@ -1228,7 +1228,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 | @@ -1322,7 +1321,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..0b021e2 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,133 @@ 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 | +|-----------|------------| +| `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) | +| 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) +bool e2 = g.exists(0, 1); // O(1) + +// Weighted (double) — value recovered via edge_value CPO or const operator()(u, v) +adjacency_matrix w(3); +w.add_edge(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); +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.exists(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 +838,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 +906,7 @@ order and advanced customization patterns. --- -## 6. Container Selection Guide +## 7. Container Selection Guide ``` ┌─ Already have data in → range-of-ranges @@ -792,6 +919,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 +935,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 new file mode 100644 index 0000000..ea041b2 --- /dev/null +++ b/include/graph/container/adjacency_matrix.hpp @@ -0,0 +1,470 @@ +#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 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). + * - 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 +#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: 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, std::monostate, EV>; + + /** + * @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_; } + [[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_; + 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{}) {} + + /** + * @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}); } + [[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_; } + + /// 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}; } + + /// 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& operator()(VId u, VId v) const noexcept { + assert(exists(u, v)); + return cells_[index(u, v)]; + } + + // ---- 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); + cells_[index(u, v)] = value; + if constexpr (!Directed) { + set_cell(v, u); + cells_[index(v, u)] = 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: + [[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_; + } + } + + [[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}; + } + + /// 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}; + } +}; + +#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..3c7c85b --- /dev/null +++ b/tests/container/adjacency_matrix/test_adjacency_matrix.cpp @@ -0,0 +1,213 @@ +#include + +#include "graph/algorithm/dijkstra_shortest_paths.hpp" +#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; + +// ============================================================================= +// 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(g.exists(0, 1)); + REQUIRE_FALSE(g.exists(3, 0)); + 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.exists(0, 1)); + REQUIRE(g.exists(1, 0)); + REQUIRE_FALSE(g.exists(0, 2)); + 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(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. + std::vector> ee; + for (auto uv : out_edges(g, u0)) { + ee.emplace_back(static_cast(target_id(g, uv)), edge_value(g, uv)); + } + std::ranges::sort(ee); + + 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__)) +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 +// ============================================================================= + +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.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]") { + 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(0, 1) == 1.5); + REQUIRE(g(0, 2) == 2.5); + REQUIRE(g(1, 2) == 3.5); +} +