diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f27ddf..be9a8e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ ## [Unreleased] +### Added +- **Bidirectional edge access** — `in_edges`, `in_degree`, `find_in_edge`, `contains_in_edge` CPOs +- `bidirectional_adjacency_list` concept +- `in_edge_range_t`, `in_edge_iterator_t`, `in_edge_t` type aliases +- `out_edge_accessor` / `in_edge_accessor` stateless edge-access policies (`edge_accessor.hpp`) +- `in_incidence` and `in_neighbors` views with pipe-syntax adaptors +- Accessor template parameter on BFS, DFS, and topological sort views for reverse traversal +- `dynamic_graph` `Bidirectional` template parameter for incoming-edge lists +- `undirected_adjacency_list` bidirectional support (incoming = outgoing) +- Kosaraju SCC algorithm and `transpose_graph` view +- 144 new tests (4261 → 4405) +- New documentation: bidirectional access tutorial, updated views/concepts/CPO/container docs + ### Documentation - Complete documentation reorganization: user guide, reference, contributor docs - New README with badges, compiler table, feature highlights diff --git a/README.md b/README.md index 1763a67..63fda1b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # graph-v3 -**A modern C++20 graph library — header-only, descriptor-based, works with your containers.** +**A modern C++20 graph library — header-only, descriptor-based, works with your graphs.** @@ -13,7 +13,7 @@ [![C++ Standard](https://img.shields.io/badge/C%2B%2B-20-blue.svg)](https://en.cppreference.com/w/cpp/20) [![License](https://img.shields.io/badge/license-BSL--1.0-blue.svg)](LICENSE) -[![Tests](https://img.shields.io/badge/tests-4261%20passing-brightgreen.svg)](docs/status/metrics.md) +[![Tests](https://img.shields.io/badge/tests-4405%20passing-brightgreen.svg)](docs/status/metrics.md) --- @@ -23,16 +23,16 @@ - **Works with your containers** — `std::vector>` or `std::map>` are valid graphs out of the box - **13 algorithms** — Dijkstra, Bellman-Ford, BFS, DFS, topological sort, connected components, articulation points, biconnected components, MST, triangle counting, MIS, label propagation, Jaccard coefficient - **7 lazy views** — vertexlist, edgelist, incidence, neighbors, BFS, DFS, topological sort — all composable with range adaptors +- **Bidirectional edge access** — `in_edges`, `in_degree`, reverse BFS/DFS/topological sort via `in_edge_accessor` - **Customization Point Objects (CPOs)** — adapt existing data structures without modifying them - **3 containers, 27 trait combinations** — `dynamic_graph`, `compressed_graph`, `undirected_adjacency_list` with mix-and-match vertex/edge storage -- **4261 tests passing** — comprehensive Catch2 test suite +- **4405 tests passing** — comprehensive Catch2 test suite --- ## Quick Example — Dijkstra Shortest Paths ```cpp -// (simplified — see examples/dijkstra_clrs_example.cpp for the full version) #include "graph/graph.hpp" #include "graph/algorithm/dijkstra_shortest_paths.hpp" #include @@ -50,13 +50,25 @@ int main() { std::vector distance(graph::num_vertices(g)); std::vector predecessor(graph::num_vertices(g)); - graph::dijkstra_shortest_paths(g, uint32_t(0), distance, predecessor); + graph::init_shortest_paths(distance, predecessor); + graph::dijkstra_shortest_paths(g, uint32_t(0), distance, predecessor, + [](const auto& g, const auto& uv) { return graph::edge_value(g, uv); }); for (size_t v = 0; v < distance.size(); ++v) std::cout << "0 -> " << v << " : " << distance[v] << "\n"; } ``` +Output: + +``` +0 -> 0 : 0 +0 -> 1 : 8 +0 -> 2 : 5 +0 -> 3 : 9 +0 -> 4 : 16 +``` + -**Status:** 4261 / 4261 tests passing · 13 algorithms · 7 views · 3 containers · 27 trait combinations · C++20 · BSL-1.0 +**Status:** 4405 / 4405 tests passing · 13 algorithms · 7 views · 3 containers · 27 trait combinations · C++20 · BSL-1.0 diff --git a/agents/archive/dynamic_edge_refactor_strategy.md b/agents/archive/dynamic_edge_refactor_strategy.md new file mode 100644 index 0000000..c6bae04 --- /dev/null +++ b/agents/archive/dynamic_edge_refactor_strategy.md @@ -0,0 +1,518 @@ +# Strategy: Rename dynamic_edge → dynamic_out_edge, Create dynamic_in_edge, Remove Sourced + +## Motivation + +1. **Symmetric edge types for distinct roles.** Outgoing and incoming edges are symmetric: an out-edge stores a `target_id` (where it goes); an in-edge stores a `source_id` (where it comes from). Each edge object stores exactly one vertex id — the "other" id is always available from the edge descriptor (the owning vertex). Today, a single `dynamic_edge` class conflates both roles using the `Sourced` flag. Introducing `dynamic_out_edge` (stores `target_id`) and `dynamic_in_edge` (stores `source_id`) makes the design explicit, keeps both types lightweight, and allows the library to support both **uniform** and **non-uniform** bidirectional edge configurations. + +2. **Uniform and non-uniform bidirectional modes.** For bidirectional graphs, the library must support two edge configurations: + - **Uniform (default for bidirectional):** The same edge type (`dynamic_out_edge`) is used for both outgoing and incoming edge containers. Out-edges are always available; in-edges are optional. Using `dynamic_out_edge` as the common type keeps out-edges as the primary concept. In the in-edge container, the `target_id_` member stores the source vertex; the edge descriptor with `in_edge_tag` handles the semantic reinterpretation. This enables moving/copying edges between lists and simplifies generic code. + - **Non-uniform:** Outgoing edges use `dynamic_out_edge` (stores `target_id`) and incoming edges use `dynamic_in_edge` (stores `source_id`). Each type names its stored id for its role, making code more self-documenting. The CPO dispatch uses `_native_edge_member` (Tier 1) for `dynamic_in_edge::source_id()` and the descriptor (Tier 4) for out-edge `source_id`. Both modes produce identical CPO results. + The traits layer controls which mode is active. For non-bidirectional or uniform bidirectional graphs, traits only need to define `edge_type` — the `dynamic_graph` implementation derives `out_edge_type = edge_type` and `in_edge_type = edge_type`. Defining both `out_edge_type` and `in_edge_type` is only required for non-uniform bidirectional graphs. + +3. **The `Sourced` parameter is unnecessary.** Whether an edge stores source_id is now determined by the *type* chosen (`dynamic_out_edge` vs `dynamic_in_edge`), not by a `bool Sourced` template parameter. The edge descriptor for out-edges already provides `source_id()` from context (Tier 4 of the CPO dispatch), so storing it on out-edges is optional. For in-edges, source_id is always required. The `Sourced` parameter can be removed entirely. + +4. **Reduced specializations.** Removing `Sourced` eliminates the 2×2 specialization matrix (`EV × Sourced`) from the old `dynamic_edge`. The new design has `dynamic_out_edge` (2 specializations: `EV!=void`, `EV==void`) and `dynamic_in_edge` (2 specializations: `EV!=void`, `EV==void`). Total: 4 specializations, down from 4, but with clearer semantics and no conditional emptiness. + +## Current State + +### Classes affected in `dynamic_graph.hpp` + +| Class | Role | Specializations | +|-------|------|-----------------| +| `dynamic_edge_target<..., Sourced, ...>` | Stores `target_id` | 1 (always present) | +| `dynamic_edge_source<..., Sourced, ...>` | Stores `source_id` when `Sourced=true`; empty when `false` | 2 (primary + `Sourced=false` empty) | +| `dynamic_edge_value<..., Sourced, ...>` | Stores edge value when `EV!=void`; empty when `EV==void` | 2 (primary + `EV=void` empty) | +| `dynamic_edge` | Composes target + source + value | 4 (`EV×Sourced` = {EV,true}, {EV,false}, {void,true}, {void,false}) | +| `dynamic_vertex_bidir_base<..., Sourced, Bidir, ...>` | Bidirectional: stores `edges_type` for in-edges | 2 (`Bidir=true`, `Bidir=false` empty) | +| `dynamic_vertex_base<..., Sourced, ...>` | Stores outgoing edges, ADL friends for `edges()`, `in_edges()` | 1 | +| `dynamic_vertex<..., Sourced, ...>` | Vertex with optional value | 2 (`VV!=void`, `VV==void`) | +| `dynamic_graph_base<..., Sourced, ...>` | Core graph: `load_edges`, `vertices_`, CPO friends | 1 | +| `dynamic_graph<..., Sourced, ...>` | Graph with/without graph value | 2 (`GV!=void`, `GV==void`) | + +### Template parameter lists (current) + +Every class above carries `bool Sourced` as a template parameter pierced through the entire hierarchy. + +### Traits (27 files in `include/graph/container/traits/`) + +Each traits struct has the signature: +```cpp +template +struct xxx_graph_traits { + static constexpr bool sourced = Sourced; + using edge_type = dynamic_edge; + using vertex_type = dynamic_vertex; + using graph_type = dynamic_graph; + ... +}; +``` + +### Tests (48 files in `tests/container/dynamic_graph/`) + +- `graph_test_types.hpp` defines tag types where each tag has `template<..., bool Sourced> using traits = ...;` and `graph_test_types` generates `sourced_void`, `sourced_int`, `sourced_all` type aliases with `Sourced=true`. +- Individual test files (e.g., `test_dynamic_graph_uol.cpp`, `test_dynamic_graph_dous.cpp`) define `xxx_sourced` type aliases and test sourced-specific behavior. +- Bidirectional tests (`test_dynamic_graph_bidirectional.cpp`) use `Sourced=true, Bidirectional=true`. + +### `load_edges` in `dynamic_graph_base` + +The `load_edges` method has `if constexpr (Sourced)` branches: +- **Sourced=true:** Constructs `edge_type(source_id, target_id [, value])` (2 or 3 args) +- **Sourced=false:** Constructs `edge_type(target_id [, value])` (1 or 2 args) +- **Bidirectional (only when Sourced=true):** Also emplaces a reverse copy into `in_edges()` + +### `std::hash` specializations (bottom of `dynamic_graph.hpp`) + +Two specializations keyed on `Sourced`: +- `Sourced=true`: hashes `source_id() ^ target_id()` +- `Sourced=false`: hashes `target_id()` only + +### `dynamic_adjacency_graph` alias + +```cpp +template +using dynamic_adjacency_graph = dynamic_graph<..., Traits::sourced, ...>; +``` + +### CPO dispatch for `source_id` + +The `source_id(g, uv)` CPO in `edge_cpo.hpp` has 7 tiers. The `_native_edge_member` tier (priority 1) checks if the underlying edge has a `.source_id()` method. The `_adj_list_descriptor` tier (priority 4) uses the descriptor's `source_id()` which comes from the stored source vertex — no edge member needed. After removing `Sourced`, out-edges won't have `.source_id()`, so the CPO will naturally fall through to Tier 4 (descriptor-based), which is the correct behavior. + +For in-edges, the `dynamic_in_edge` will have `.source_id()`, so the `_native_edge_member` tier will fire and return the stored source id from the edge object. + +## Target State + +### New edge class hierarchy + +``` +// Base classes (composable property mixins) +dynamic_edge_target ← stores target_id_ (VId) +dynamic_edge_source ← stores source_id_ (VId) [repurposed; Sourced removed] +dynamic_edge_value ← stores value when EV!=void +dynamic_edge_value ← empty + +// Out-edge: stores target_id + optional value +dynamic_out_edge + inherits: dynamic_edge_target + dynamic_edge_value + specializations: EV!=void, EV==void + constructors: (target_id), (target_id, value), (target_id, value&&) + Members: target_id() — the vertex this edge points to + +// In-edge: stores source_id + optional value (symmetric with out-edge) +dynamic_in_edge + inherits: dynamic_edge_source + dynamic_edge_value + specializations: EV!=void, EV==void + constructors: (source_id), (source_id, value), (source_id, value&&) + Members: source_id() — the vertex this edge comes from +``` + +**Bidirectional mode selection** is expressed through the traits' type aliases, not through a template parameter. Traits only need to define `edge_type`; the `dynamic_graph` implementation derives `out_edge_type` and `in_edge_type` from it (defaulting both to `edge_type`). Traits that define `in_edge_type` explicitly opt into non-uniform mode: + +| Mode | Traits defines | `out_edge_type` (derived) | `in_edge_type` (derived) | Notes | +|------|---------------|--------------------------|--------------------------|-------| +| Non-bidirectional | `edge_type` only | `= edge_type` (`dynamic_out_edge`) | `= edge_type` (unused) | Lightweight; source_id from descriptor | +| Uniform bidirectional | `edge_type` only | `= edge_type` (`dynamic_out_edge`) | `= edge_type` (`dynamic_out_edge`) | Same type in both containers; in-edge `target_id_` stores source vertex; descriptor flips semantics | +| Non-uniform bidirectional | `edge_type` + `in_edge_type` | `= edge_type` (`dynamic_out_edge`) | `= Traits::in_edge_type` (`dynamic_in_edge`) | Each type names its stored VId for its role; most self-documenting | + +Both `dynamic_out_edge` and `dynamic_in_edge` store exactly **one VId** plus an optional value. They are structurally identical but name their stored id according to their role. The "other" id (source for out-edges, target for in-edges) is always provided by the edge descriptor from the owning vertex. + +In **uniform** mode, the in-edge container stores `dynamic_out_edge` objects where `target_id_` holds the source vertex. The `in_edge_tag` descriptor reinterprets: `source_id(g, uv)` extracts `target_id()` from the raw edge (the actual source), and `target_id(g, uv)` returns the owning vertex (the actual target). + +### Removed/repurposed classes + +- `dynamic_edge_source`: **Repurposed** — the `Sourced=false` empty specialization is removed; the primary class is kept (with `Sourced` removed from template params) as the base for `dynamic_in_edge`. It stores `source_id_` unconditionally. + +- All 4 specializations of the old `dynamic_edge` — replaced by `dynamic_out_edge` (2 specializations) and `dynamic_in_edge` (2 specializations). + +### Template parameter removal: `Sourced` + +`Sourced` is removed from **every** template parameter list: +- `dynamic_out_edge` — 6 params (was 7) +- `dynamic_in_edge` — 6 params (was N/A, new class) +- `dynamic_vertex_bidir_base`, `dynamic_vertex_base`, `dynamic_vertex` — 6 params +- `dynamic_graph_base`, `dynamic_graph` — 6 params (default: `>`) +- All traits structs — 5 params: `` + +### Traits changes + +The key usability principle: **traits only need to define `edge_type`** for non-bidirectional or uniform bidirectional graphs. The `dynamic_graph` implementation derives `out_edge_type` and `in_edge_type` from it. Explicitly defining `out_edge_type` and `in_edge_type` is only required for non-uniform bidirectional graphs. + +**How `dynamic_graph` derives the edge types from traits:** + +The graph implementation uses detection idioms to determine what the traits provide: + +```cpp +// In dynamic_graph_base (or a traits-helper): +using edge_type = typename Traits::edge_type; // required by all traits +using out_edge_type = edge_type; // alias: edge_type IS the out-edge type +using in_edge_type = detected_or_t; // Traits::in_edge_type if present, else edge_type +using edges_type = typename Traits::edges_type; // required by all traits +using in_edges_type = detected_or_t; // Traits::in_edges_type if present, else edges_type +``` + +When `Traits::in_edge_type` exists and differs from `edge_type`, the graph is in non-uniform mode. Otherwise it is uniform (both containers use `edge_type`). +Similarly, when `Traits::in_edges_type` is not provided, the graph defaults `in_edges_type = edges_type`. + +Note: `detected_or_t`/`detect_*` names above are illustrative pseudocode for a detection-idiom helper. + +**Standard traits (non-bidirectional or uniform bidirectional — the common case):** +```cpp +template +struct xxx_graph_traits { + static constexpr bool bidirectional = Bidirectional; + + using edge_type = dynamic_out_edge; + // No out_edge_type or in_edge_type needed — dynamic_graph derives them from edge_type. + // out_edge_type = edge_type, in_edge_type = edge_type (uniform). + + using vertex_type = dynamic_vertex; + using graph_type = dynamic_graph; + + using edges_type = Container; // e.g., vector, list, forward_list, set, ... + // Optional in_edges_type omitted in standard traits; dynamic_graph derives in_edges_type = edges_type. + using vertices_type = Container; +}; +``` + +This is the only form needed for the existing 27 standard traits files. The `edge_type` alias doubles as `out_edge_type`. For bidirectional graphs, both containers use the same `edge_type` (uniform mode), matching the current behavior where both containers store `dynamic_edge`. + +**Non-uniform bidirectional traits (user-defined or advanced):** +```cpp +template +struct xxx_nonuniform_graph_traits { + static constexpr bool bidirectional = true; + + using edge_type = dynamic_out_edge; + using in_edge_type = dynamic_in_edge; + // Defining in_edge_type signals non-uniform mode to dynamic_graph. + // out_edge_type is still derived as edge_type by dynamic_graph. + + using vertex_type = dynamic_vertex; + using graph_type = dynamic_graph; + + using edges_type = Container; // Container> + using in_edges_type = Container; // Required for non-uniform: container value type differs + using vertices_type = Container; +}; +``` + +**No detection needed in `load_edges`**: Because `edge_type` (= `out_edge_type`) is always `dynamic_out_edge`, out-edge construction is always `edge_type(target_id [, value])`. No `if constexpr` to detect `source_id()` on `edge_type` is needed. + +### `load_edges` changes + +The `if constexpr (Sourced)` branches are removed entirely. Construction is now straightforward: + +```cpp +// Out-edge construction (always) — edge_type = dynamic_out_edge in all modes +emplace_edge(vertex_edges, e.target_id, edge_type(e.target_id [, value])); + +// In-edge construction (bidirectional only) +if constexpr (Bidirectional) { + auto& rev = vertices_[e.target_id].in_edges(); + // in_edge_type is either dynamic_out_edge (uniform) or dynamic_in_edge (non-uniform) + // Both take a single VId: source_id for the edge being reversed + if constexpr (std::is_same_v) { + // Uniform: in_edge_type = dynamic_out_edge, construct with source_id + // The target_id_ member stores what is semantically the source vertex + emplace_edge(rev, e.source_id, in_edge_type(e.source_id [, value])); + } else { + // Non-uniform: in_edge_type = dynamic_in_edge, construct with source_id + emplace_edge(rev, e.source_id, in_edge_type(e.source_id [, value])); + } +} +``` + +Note: Both branches of the `if constexpr` produce the same call (`in_edge_type(source_id [, value])`), so the detection can be omitted entirely — it is shown above only for documentation clarity. The simplified form is: + +```cpp +if constexpr (Bidirectional) { + auto& rev = vertices_[e.target_id].in_edges(); + emplace_edge(rev, e.source_id, in_edge_type(e.source_id [, value])); +} +``` + +Key points: +- The `Sourced` guard is gone; `edge_type` is always `dynamic_out_edge`, so no constructor-arity detection is needed. +- Out-edge construction is always `edge_type(target_id [, value])`. +- In-edge construction is always `in_edge_type(source_id [, value])` — this works for both uniform and non-uniform because both `dynamic_out_edge` and `dynamic_in_edge` take a single VId. +- The initializer list constructor follows the same pattern. + +### Vertex bidir base changes + +`dynamic_vertex_bidir_base<..., Bidirectional, Traits>`: +- When `Bidirectional=true`, stores graph-derived `in_edges_type` (from `Traits::in_edges_type` if present, else `Traits::edges_type`) +- The `static_assert(Sourced, ...)` is removed since `Sourced` no longer exists. In non-uniform mode, `dynamic_in_edge` always has `source_id()`. In uniform mode, `source_id` is provided by the descriptor. +- In **uniform** mode, `in_edges_type == edges_type` (same container and value type). +- In **non-uniform** mode, `in_edges_type` is a different container type (same container *kind* but parameterized on `in_edge_type` instead of `out_edge_type`). +- The `in_edges()` accessor return type is always graph-derived `in_edges_type&`, regardless of mode. + +### `dynamic_adjacency_graph` alias + +```cpp +template +using dynamic_adjacency_graph = dynamic_graph; +``` + +### `std::hash` specializations + +Collapse from 2 (keyed on `Sourced`) to 2 (keyed on edge class): +- `hash>`: hashes `target_id()` only +- `hash>`: hashes `source_id()` only + +In uniform mode, only the `dynamic_out_edge` hash is used (for both containers). Both hash functions hash a single VId. + +### Test infrastructure changes + +- `graph_test_types.hpp`: Remove `bool Sourced` from tag `traits` aliases. Replace `sourced_void`, `sourced_int`, `sourced_all` with bidirectional configurations: + - `bidir_uniform_void`: `Bidirectional=true`, uniform edges (traits define only `edge_type`; graph derives `in_edge_type = edge_type`) + - `bidir_uniform_int`: same with `EV=int` + - `bidir_nonuniform_void`: `Bidirectional=true`, non-uniform edges (traits define `edge_type` + `in_edge_type = dynamic_in_edge`) + - `bidir_nonuniform_int`: same with `EV=int` +- Individual test files: Remove `xxx_sourced` type aliases. Tests that verified `Sourced=true` behavior should be converted to test bidirectional in-edge behavior (both uniform and non-uniform modes). +- Tests for `source_id(g, uv)` on out-edges: These should still pass — the CPO resolves via the descriptor (Tier 4) in both modes. Behavior is identical. +- New tests needed: + - Verify that uniform and non-uniform bidirectional graphs produce identical traversal results. + - Verify that `source_id(g, uv)` works for out-edges in both modes. + - Verify that `G::in_edge_type` and `G::edge_type` are the same type in uniform mode (traits without `in_edge_type`) and different in non-uniform mode (traits with explicit `in_edge_type`). + +### CPO dispatch interactions + +| Mode | Out-edge `source_id(g,uv)` | Out-edge `target_id(g,uv)` | In-edge `source_id(g,uv)` | In-edge `target_id(g,uv)` | +|------|---------------------------|---------------------------|--------------------------|---------------------------| +| Non-uniform | Tier 4 (descriptor) | Tier 1 (native `.target_id()`) | Tier 1 (native `.source_id()`) | Tier 4 (descriptor → owning vertex) | +| Uniform | Tier 4 (descriptor) | Tier 1 (native `.target_id()`) | Tier 4 (descriptor extracts `.target_id()` from raw edge = actual source) | Tier 4 (descriptor → owning vertex) | + +In **uniform** mode, the in-edge container stores `dynamic_out_edge` which has no `.source_id()` method. So in-edge `source_id(g, uv)` cannot use Tier 1; it falls to Tier 4 where the descriptor extracts `.target_id()` from the raw edge (which stores the source vertex in the in-edge container). The result is correct. + +Both modes produce the same CPO results; only the dispatch tier differs for in-edge `source_id`. No changes to `edge_cpo.hpp` are required. + +### Potential library-level implications + +- **Graph concepts** (`graph_concepts.hpp`): If any concept assumes a single `edge_type`, it may need updating to account for `in_edge_type` as a potentially different type. Review `has_in_edges`, `adjacency_list`, etc. +- **Views** (`incidence.hpp`, `bfs.hpp`, `dfs.hpp`): These work with `edges(g, u)` (out-edges) and are parameterized on edge iterator types. They should work unchanged with either edge type. +- **Algorithms**: Most algorithms operate on out-edges. Bidirectional algorithms (e.g., SCC) that use `in_edges(g, u)` will see `in_edge_type` objects. The CPOs (`source_id`, `target_id`, `edge_value`) abstract away the type difference, so no algorithm changes should be needed. +- **`edge_descriptor_view`**: Already parameterized on `EdgeIter` and `EdgeDirection` (`out_edge_tag`/`in_edge_tag`). Different edge types for in/out containers work naturally because the view is templated on the iterator type of each container. + +## Phased Execution Plan + +### Phase 1: Create `dynamic_in_edge` alongside existing code (additive, no breakage) + +**Goal:** Introduce `dynamic_in_edge` without changing any existing code. + +1. Add `dynamic_in_edge` class with base classes: + - Inherits `dynamic_edge_source` (stores source_id — repurposed from current class, with `Sourced` param removed) + - Inherits `dynamic_edge_value` (optional edge value) + - Provide 2 specializations: `EV!=void`, `EV==void` + - Include `operator<=>` (compares by `source_id`), `operator==`, and `std::hash` specialization (hashes `source_id()` only) + - Constructors: `(source_id)`, `(source_id, value)`, `(source_id, value&&)` + +2. Add graph-side derived aliases in `dynamic_graph_base` for compatibility with existing traits: + - `out_edge_type = edge_type` + - `in_edge_type = Traits::in_edge_type` if present, else `edge_type` + - `in_edges_type = Traits::in_edges_type` if present, else `edges_type` + - This allows existing standard traits to remain minimal (`edge_type` + `edges_type`) while enabling non-uniform traits to opt in by defining `in_edge_type` + `in_edges_type`. + +3. Add forward declaration for `dynamic_in_edge` in `dynamic_graph.hpp`. + +4. **Verify:** Full test suite passes with no changes to existing tests. + +### Phase 2: Wire `dynamic_in_edge` into bidirectional support + +**Goal:** The bidirectional vertex stores graph-derived `in_edges_type` (fallback to `edges_type`), so standard traits need no extra aliases while non-uniform traits can opt in. + +1. Change `dynamic_vertex_bidir_base<..., true, Traits>` to use graph-derived `in_edges_type` for the in-edge container. + +2. Update `load_edges` bidirectional branches to construct `in_edge_type(source_id [, value])` instead of `edge_type(source_id, target_id [, value])` for in-edge emplacement. Both uniform (`dynamic_out_edge`) and non-uniform (`dynamic_in_edge`) take a single VId. + - **Phase ordering note:** In Phase 2, `Sourced` still exists, so some legacy edge types may still require `(source_id, target_id [, value])`. + - Use a temporary constructibility-based dispatch in Phase 2: + - If `in_edge_type` is constructible from `(source_id, target_id [, value])`, use that form. + - Else construct from `(source_id [, value])`. + - This temporary dispatch is removed in Phase 4d after `Sourced` is eliminated and constructors are standardized to one VId. + +3. Update the `in_edges()` ADL friend functions in `dynamic_vertex_base` to use graph-derived `in_edges_type`. + +4. At this point, uniform mode still works because `in_edges_type` defaults to `edges_type` via fallback aliases. Non-uniform mode is not yet active (requires Phase 3–4 to introduce `dynamic_out_edge` and `dynamic_in_edge` as separate types). + +5. **Verify:** Bidirectional tests pass (both uniform behavior preserved). + +### Phase 3: Rename `dynamic_edge` → `dynamic_out_edge` (mechanical rename) + +**Goal:** The out-edge class has the correct name, but `Sourced` still exists as a template parameter (to be removed next). + +1. Rename `dynamic_edge` → `dynamic_out_edge` in: + - Class declarations & definitions (primary + 3 specializations) in `dynamic_graph.hpp` + - All `using edge_type = dynamic_edge<...>` → `using edge_type = dynamic_out_edge<...>` + - Forward declarations in `dynamic_graph.hpp` and all 27 traits headers + - `std::hash` specializations + - Test files that reference `dynamic_edge` directly (if any) + - The `edge_type` alias in traits: `using edge_type = dynamic_out_edge<...>;` (keep as main alias) + +2. Add a deprecated alias: `template<...> using dynamic_edge = dynamic_out_edge<...>;` for transition. + +3. **Verify:** Full test suite passes. + +### Phase 4: Remove `Sourced` template parameter + +**Goal:** Eliminate the `Sourced` bool from every template parameter list. + +This is the highest-risk phase. Work file by file, smallest scope first. + +#### 4a: Remove `Sourced` from edge classes + +1. Repurpose `dynamic_edge_source`: remove `Sourced` template parameter and the `Sourced=false` empty specialization. Keep the primary class (stores `source_id_` unconditionally) as the base for `dynamic_in_edge`. +2. Remove `Sourced` from `dynamic_edge_target`, `dynamic_edge_value` template params. +3. Remove `Sourced` from all 4 `dynamic_out_edge` specializations. Collapse from 4 to 2 (only `EV` matters now): + - `dynamic_out_edge` — stores target_id + value + - `dynamic_out_edge` — stores target_id only + - Remove the `Sourced=true` specializations (the ones that took `(uid, vid)` constructors) +4. Update constructors: all out-edges take `(target_id [, value])` — never `(source_id, target_id [, value])`. + +#### 4b: Remove `Sourced` from vertex and graph classes + +1. Remove `Sourced` from `dynamic_vertex_bidir_base`, `dynamic_vertex_base`, `dynamic_vertex`, `dynamic_graph_base`, `dynamic_graph`. +2. Remove `static constexpr bool sourced = Sourced;` from `dynamic_graph`. +3. Remove `static_assert(Sourced, ...)` from `dynamic_vertex_bidir_base` (replaced by design — `dynamic_in_edge` always has source_id). + +#### 4c: Remove `Sourced` from traits + +1. Update all 27 traits files: remove `bool Sourced` template parameter, remove `static constexpr bool sourced`, update `using edge_type` to use `dynamic_out_edge`. No `out_edge_type` or `in_edge_type` definitions needed — `dynamic_graph` derives them from `edge_type`. + + Clarification: for standard traits, `in_edges_type` may be omitted because `dynamic_graph` fallback derives it from `edges_type`. For non-uniform custom traits, `in_edges_type` must be explicitly provided and must match `in_edge_type`. + +#### 4d: Update `load_edges` + +1. Remove `if constexpr (Sourced)` branches. +2. Out-edge construction is straightforward — `edge_type` is always `dynamic_out_edge`: + ```cpp + emplace_edge(vertex_edges, e.target_id, edge_type(e.target_id [, value])); + ``` +3. In-edge construction (when `Bidirectional`): always `in_edge_type(source_id [, value])`. This works for both uniform (`dynamic_out_edge`) and non-uniform (`dynamic_in_edge`) because both take a single VId. +4. Initializer list constructor: same pattern. + +#### 4e: Update `dynamic_adjacency_graph` alias + +Remove `Traits::sourced` from the template arguments. + +#### 4f: Update `std::hash` specializations + +Remove `Sourced` from hash template params. Two specializations: +- `hash>` — hash `target_id()` +- `hash>` — hash `source_id()` + +Both hash a single VId. In uniform mode, only the `dynamic_out_edge` hash is used. + +#### 4g: **Verify:** Compile all non-test code. + +### Phase 5: Update tests + +1. **`graph_test_types.hpp`:** + - Remove `bool Sourced` from all tag `traits` template aliases. + - Replace `sourced_void`, `sourced_int`, `sourced_all` with bidirectional test configurations: + - `bidir_uniform_void`, `bidir_uniform_int`, `bidir_uniform_all` — bidirectional, uniform edges + - `bidir_nonuniform_void`, `bidir_nonuniform_int` — bidirectional, non-uniform edges + - Each tag needs an additional traits alias for the non-uniform variant (or a second tag template parameter). + +2. **Individual container test files** (48 files): + - Remove `xxx_sourced` type aliases and tests that assert `G::sourced == true/false`. + - Convert tests for "sourced edge" behavior to bidirectional in-edge tests. + - Add parallel tests for both uniform and non-uniform bidirectional modes where applicable. + - Keep `source_id(g, uv)` tests for out-edges — verify they work via descriptor (Tier 4) in both modes. + +3. **Container-native bidirectional tests** (`tests/container/dynamic_graph/test_dynamic_graph_bidirectional.cpp`): + This file tests the `dynamic_graph` implementation directly — construction, `load_edges`, copy/move, `in_edges()` ADL friends, in-edge container behavior. Following the existing separation, it should NOT test CPO dispatch semantics. + - Expand to test both uniform and non-uniform configurations. + - Test `load_edges` populates in-edge containers correctly. + - Test copy/move preserves in-edges. + - Test `in_edges()` accessor returns correct container in both modes. + - Test initializer-list construction with bidirectional edges. + - Test that `in_edges_type == edges_type` in uniform mode and differs in non-uniform mode. + - Non-bidirectional baselines remain as regression checks. + +4. **CPO-only bidirectional tests** (`tests/adj_list/cpo/`): + Following the existing pattern (e.g., `test_in_edges_cpo.cpp`, `test_source_id_cpo.cpp`), these test CPO dispatch against `dynamic_graph` instances without testing construction or mutable actions. The graph is built once and then only read through CPOs. + - **Existing files to update:** + - `test_in_edges_cpo.cpp`: Add `dynamic_graph` bidirectional sections (uniform and non-uniform) alongside the existing stub-graph tests. + - `test_source_id_cpo.cpp`: Add sections verifying `source_id(g, uv)` on out-edges (Tier 4) and in-edges (Tier 1 non-uniform, Tier 4 uniform) for `dynamic_graph`. + - `test_find_in_edge_cpo.cpp`, `test_contains_in_edge_cpo.cpp`: Add `dynamic_graph` bidirectional sections. + - **New CPO test file** (`test_bidir_cpo_consistency.cpp` in `tests/adj_list/cpo/`): + - Build the same graph with both uniform and non-uniform traits. + - Verify that `source_id`, `target_id`, `edge_value`, `in_edges`, `in_degree` produce identical results through CPOs regardless of mode. + - These tests should use pre-built `const` graphs and only exercise read-only CPO access. + +5. **Concept tests** (`tests/adj_list/concepts/test_bidirectional_concepts.cpp`): + - Add `STATIC_REQUIRE` checks that uniform and non-uniform `dynamic_graph` bidirectional types satisfy `bidirectional_adjacency_list`. + - Verify non-bidirectional `dynamic_graph` does NOT satisfy the concept. + +6. **New test: uniform/non-uniform consistency** (housed in the CPO test above, item 4). + +7. **Algorithm test types** (`algorithm_test_types.hpp`) and any algorithm tests: + - Remove `Sourced` from any graph type definitions used in algorithm tests. + +8. **Examples and benchmarks:** + - Update any explicit `Sourced` usage (likely minimal — most use default `Sourced=false`). + +9. **Verify:** Full test suite passes. + +### Phase 6: Cleanup + +1. Remove the deprecated `dynamic_edge` alias (if added in Phase 3). +2. Update all Doxygen comments that reference `Sourced`. +3. Update user-facing docs (`docs/`, `README.md`). +4. Remove old `agents/incoming_edges_design.md` if superseded. + +## Risk Assessment + +| Risk | Impact | Mitigation | +|------|--------|------------| +| `source_id` CPO breaks for out-edges after removing Sourced | Tests fail for source_id on out-edges | In both uniform and non-uniform modes, out-edge `source_id` resolves via descriptor Tier 4. Verify in Phase 4. | +| Traits file mechanical changes (27 files) introduce typos | Compile failures | Script the changes; compile after each trait file. | +| Test files reference `Sourced` template parameter | Compile failures in tests | Phase 5 is dedicated to this. Search for `Sourced` across all test files. | +| In-edge hash collision with out-edge hash | Runtime issues in unordered containers | Both hash functions hash a single VId (`target_id` for out-edges, `source_id` for in-edges). In uniform mode, only the `dynamic_out_edge` hash is used. | +| Uniform mode: `target_id_` stores source in in-edge container | Confusing semantics when inspecting raw edge objects | The descriptor layer handles the semantic reinterpretation. Document clearly: in uniform mode, do NOT interpret `target_id_` directly from in-edge container objects — always use CPOs through descriptors. | +| `dynamic_in_edge` and `dynamic_out_edge` are same size | No memory savings in non-uniform mode | Both store one VId + optional value. The choice between modes is about naming clarity and CPO dispatch path, not memory. The non-uniform mode is more self-documenting; the uniform mode enables type-homogeneous containers. | +| Non-uniform mode: out-edges lack self-contained source_id | Confusing for users who extract raw edge objects | Document clearly: in non-uniform mode, use descriptors to access source_id for out-edges. Uniform mode preserves self-contained edges. | +| Library concepts assume single `edge_type` | Type mismatches for in-edge operations | Review `graph_concepts.hpp` for `has_in_edges` and related concepts. Ensure they work with `in_edge_type` distinct from `edge_type`. | +| Derived aliases (`in_edge_type` / `in_edges_type`) fallback incorrect | Compile/runtime mismatch in bidirectional paths | Add explicit compile-time checks in `dynamic_graph_base`: uniform mode requires `std::same_as` and `std::same_as` when aliases are absent; non-uniform traits must provide both aliases consistently. | +| Existing user code uses `Sourced=true` for standalone edge objects | API breakage | Document migration: use `dynamic_in_edge` for self-contained edges, or use descriptors for out-edges. | +| Two bidirectional modes (uniform/non-uniform) increase test matrix | Longer test times, more test code | Focus non-uniform tests on a representative subset of container types (e.g., vov, vol). Full matrix for uniform only. | + +## Files to Modify (Summary) + +| Category | Files | Count | +|----------|-------|-------| +| Core header | `include/graph/container/dynamic_graph.hpp` | 1 | +| Traits headers | `include/graph/container/traits/*.hpp` | 27 | +| Graph concepts | `include/graph/graph_concepts.hpp` (review for `in_edge_type` compatibility) | 1 | +| Test infrastructure | `tests/common/graph_test_types.hpp` | 1 | +| Test infrastructure | `tests/common/graph_fixtures.hpp` (if it references Sourced) | 1 | +| Test infrastructure | `tests/common/algorithm_test_types.hpp` (if it references Sourced) | 1 | +| Container tests | `tests/container/dynamic_graph/*.cpp` | ~48 | +| Container bidir tests | `tests/container/dynamic_graph/test_dynamic_graph_bidirectional.cpp` (native impl) | 1 | +| CPO tests | `tests/adj_list/cpo/test_source_id_cpo.cpp`, `test_source_cpo.cpp` | 2 | +| CPO bidir tests | `tests/adj_list/cpo/test_in_edges_cpo.cpp`, `test_find_in_edge_cpo.cpp`, `test_contains_in_edge_cpo.cpp` (add dynamic_graph sections) | 3 | +| CPO bidir consistency | `tests/adj_list/cpo/test_bidir_cpo_consistency.cpp` (new) | 1 | +| Bidirectional concepts | `tests/adj_list/concepts/test_bidirectional_concepts.cpp` | 1 | +| Examples | `examples/*.cpp` | ~3 | +| Benchmarks | `benchmark/*.cpp` | ~3 | +| **Total** | | **~90** | + +## Verification Strategy + +- After each phase, run the full test suite (`ctest` or equivalent). +- Use `grep -r "Sourced" include/ tests/` after Phase 4 to confirm complete removal. +- Use `grep -r "dynamic_edge[^_]" include/ tests/` after Phase 3 to confirm complete rename (excluding the deprecated alias). +- Static analysis: ensure no `Sourced` appears in any template parameter list. +- **Uniform/non-uniform parity check:** After Phase 5, run a dedicated test that builds the same graph with both uniform and non-uniform traits and validates that all CPO results match. +- **Memory validation:** Verify `sizeof(dynamic_out_edge)` == `sizeof(dynamic_in_edge)` — both store a single VId. Confirm no accidental padding or extra members. + +## Definition of Done + +- `dynamic_graph` compiles when traits define only `edge_type` + `edges_type` (non-bidirectional and uniform bidirectional). +- Non-uniform traits compile only when `in_edge_type` and `in_edges_type` are provided consistently. +- No references to `Sourced` remain under `include/` and `tests/`. +- All container-native bidirectional tests pass, all CPO-only bidirectional tests pass, and concept tests pass. +- Uniform and non-uniform bidirectional CPO results are identical for `source_id`, `target_id`, `edge_value`, `in_edges`, and `in_degree`. diff --git a/agents/incoming_edges_design.md b/agents/archive/incoming_edges_design.md similarity index 90% rename from agents/incoming_edges_design.md rename to agents/archive/incoming_edges_design.md index f32acf9..7d7e67d 100644 --- a/agents/incoming_edges_design.md +++ b/agents/archive/incoming_edges_design.md @@ -33,36 +33,35 @@ preserving full backward compatibility with existing code. ## 3. Naming Convention -### 3.1 Existing names (no rename required) +### 3.1 Primary outgoing names -The current names remain as-is and **continue to mean "outgoing edges"**: +The explicit directional names are the **primary** CPO definitions: -| Current Name | Stays | Meaning | -|---|---|---| -| `edges(g, u)` | Yes | Outgoing edges from vertex `u` | -| `degree(g, u)` | Yes | Out-degree of `u` | -| `target_id(g, uv)` | Yes | Target vertex of edge `uv` | -| `source_id(g, uv)` | Yes | Source vertex of edge `uv` | -| `target(g, uv)` | Yes | Target vertex descriptor | -| `source(g, uv)` | Yes | Source vertex descriptor | -| `num_edges(g)` | Yes | Total edge count (direction-agnostic) | -| `find_vertex_edge(g, u, v)` | Yes | Find outgoing edge from `u` to `v` | -| `contains_edge(g, u, v)` | Yes | Check outgoing edge from `u` to `v` | -| `has_edge(g)` | Yes | Check whether graph has at least one edge | -| `edge_value(g, uv)` | Yes | Edge property (direction-agnostic) | - -### 3.2 New outgoing aliases (optional clarity) - -For code that wants to be explicit, introduce **aliases** that forward to the -existing CPOs. These are convenience only — not required: - -| New Alias | Forwards To | +| Primary Name | Meaning | +|---|---| +| `out_edges(g, u)` | Outgoing edges from vertex `u` | +| `out_degree(g, u)` | Out-degree of `u` | +| `target_id(g, uv)` | Target vertex of edge `uv` | +| `source_id(g, uv)` | Source vertex of edge `uv` | +| `target(g, uv)` | Target vertex descriptor | +| `source(g, uv)` | Source vertex descriptor | +| `num_edges(g)` | Total edge count (direction-agnostic) | +| `find_out_edge(g, u, v)` | Find outgoing edge from `u` to `v` | +| `contains_edge(g, u, v)` | Check outgoing edge from `u` to `v` | +| `has_edge(g)` | Check whether graph has at least one edge | +| `edge_value(g, uv)` | Edge property (direction-agnostic) | + +### 3.2 Convenience aliases (shorter names) + +For code that prefers brevity, **aliases** forward to the primary CPOs: + +| Alias | Forwards To | |---|---| -| `out_edges(g, u)` | `edges(g, u)` | -| `out_degree(g, u)` | `degree(g, u)` | -| `find_out_edge(g, u, v)` | `find_vertex_edge(g, u, v)` | +| `edges(g, u)` | `out_edges(g, u)` | +| `degree(g, u)` | `out_degree(g, u)` | +| `find_vertex_edge(g, u, v)` | `find_out_edge(g, u, v)` | -These are defined as inline constexpr CPO aliases or thin wrapper functions in +These are defined as inline constexpr CPO aliases in `graph::adj_list` and re-exported to `graph::`. > **Design decision (2026-02-21):** These aliases are **retained**. See @@ -115,7 +114,7 @@ For the `(g, uid)` overload: | 1 | `_adl` | ADL `in_edges(g, uid)` | | 2 | `_default` | `in_edges(g, *find_vertex(g, uid))` | -**Return type:** `in_vertex_edge_range_t` — i.e. the exact range type returned by +**Return type:** `in_edge_range_t` — i.e. the exact range type returned by `in_edges(g, u)` after CPO resolution. This may be an `edge_descriptor_view`, but custom member/ADL implementations may return a different valid range type. @@ -131,23 +130,26 @@ custom member/ADL implementations may return a different valid range type. | 2 | `_adl` | ADL `in_degree(g, u)` | | 3 | `_default` | `size(in_edges(g, u))` or `distance(in_edges(g, u))` | -### 4.3 `out_edges` / `out_degree` / `find_out_edge` (aliases) +### 4.3 `edges` / `degree` / `find_vertex_edge` (convenience aliases) **File:** `include/graph/adj_list/detail/graph_cpo.hpp` +> **Note:** `out_edges`, `out_degree`, and `find_out_edge` are the primary +> CPO instances. The shorter names are convenience aliases. + ```cpp -inline constexpr auto& out_edges = edges; -inline constexpr auto& out_degree = degree; -inline constexpr auto& find_out_edge = find_vertex_edge; +inline constexpr auto& edges = out_edges; +inline constexpr auto& degree = out_degree; +inline constexpr auto& find_vertex_edge = find_out_edge; ``` ### 4.4 `find_in_edge`, `contains_in_edge` -These mirror `find_vertex_edge` (`find_out_edge`) / `contains_edge` but operate +These mirror `find_out_edge` / `contains_edge` but operate on the reverse adjacency structure. Implementation follows the same member → ADL → default cascade pattern. -`find_in_edge` has three overloads mirroring `find_vertex_edge`: +`find_in_edge` has three overloads mirroring `find_out_edge`: - `find_in_edge(g, u, v)` — both vertex descriptors - `find_in_edge(g, u, vid)` — descriptor + vertex ID - `find_in_edge(g, uid, vid)` — both vertex IDs @@ -173,21 +175,29 @@ matches `source_id(g, ie)` against the target vertex (the in-neighbor). The defa | Alias | Definition | |---|---| -| `in_vertex_edge_range_t` | `decltype(in_edges(g, vertex_t))` | -| `in_vertex_edge_iterator_t` | `ranges::iterator_t>` | -| `in_edge_t` | `ranges::range_value_t>` | +| `in_edge_range_t` | `decltype(in_edges(g, vertex_t))` | +| `in_edge_iterator_t` | `ranges::iterator_t>` | +| `in_edge_t` | `ranges::range_value_t>` | `in_edge_t` is **independently deduced** from the `in_edges()` return range. -It is commonly the same type as `edge_t` but this is not required. See +It is commonly the same type as `out_edge_t` but this is not required. See Design Principle 5 and Appendix C for rationale. -Also add outgoing aliases for explicitness: +The outgoing type aliases are the primary definitions: + +| Primary Alias | Definition | +|---|---| +| `out_edge_range_t` | `decltype(out_edges(g, vertex_t))` | +| `out_edge_iterator_t` | `ranges::iterator_t>` | +| `out_edge_t` | `ranges::range_value_t>` | + +Convenience aliases: | Alias | Definition | |---|---| -| `out_vertex_edge_range_t` | Same as `vertex_edge_range_t` | -| `out_vertex_edge_iterator_t` | Same as `vertex_edge_iterator_t` | -| `out_edge_t` | Same as `edge_t` | +| `vertex_edge_range_t` | Same as `out_edge_range_t` | +| `vertex_edge_iterator_t` | Same as `out_edge_iterator_t` | +| `edge_t` | Same as `out_edge_t` | --- @@ -195,11 +205,11 @@ Also add outgoing aliases for explicitness: **File:** `include/graph/adj_list/adjacency_list_concepts.hpp` -### 6.1 `in_vertex_edge_range` +### 6.1 `in_edge_range` ```cpp template -concept in_vertex_edge_range = +concept in_edge_range = std::ranges::forward_range; ``` @@ -210,7 +220,7 @@ not satisfy the full `edge` concept. The minimum requirement is that `source_id(g, ie)` can extract the neighbor vertex ID (see §6.2). When `in_edge_t` and `edge_t` are the same type, the range will naturally -model `vertex_edge_range` as well. +model `out_edge_range` as well. ### 6.2 `bidirectional_adjacency_list` @@ -219,7 +229,7 @@ template concept bidirectional_adjacency_list = adjacency_list && requires(G& g, vertex_t u, in_edge_t ie) { - { in_edges(g, u) } -> in_vertex_edge_range; + { in_edges(g, u) } -> in_edge_range; { source_id(g, ie) } -> std::convertible_to>; // Note: no edge_value() requirement on incoming edges }; @@ -549,7 +559,7 @@ using adj_list::find_in_edge; using adj_list::contains_in_edge; // New concepts -using adj_list::in_vertex_edge_range; +using adj_list::in_edge_range; using adj_list::bidirectional_adjacency_list; using adj_list::index_bidirectional_adjacency_list; @@ -558,11 +568,11 @@ using adj_list::has_in_degree; using adj_list::has_in_degree_v; // New type aliases -using adj_list::in_vertex_edge_range_t; -using adj_list::in_vertex_edge_iterator_t; +using adj_list::in_edge_range_t; +using adj_list::in_edge_iterator_t; using adj_list::in_edge_t; -using adj_list::out_vertex_edge_range_t; -using adj_list::out_vertex_edge_iterator_t; +using adj_list::out_edge_range_t; +using adj_list::out_edge_iterator_t; using adj_list::out_edge_t; ``` @@ -586,9 +596,9 @@ using adj_list::out_edge_t; | `docs/user-guide/views.md` | Add `in_incidence` and `in_neighbors` views; add `out_incidence`/`out_neighbors` aliases | | `docs/user-guide/algorithms.md` | Note which algorithms benefit from bidirectional access | | `docs/user-guide/containers.md` | Document bidirectional dynamic_graph and compressed_graph | -| `docs/reference/concepts.md` | Add `bidirectional_adjacency_list`, `in_vertex_edge_range` | +| `docs/reference/concepts.md` | Add `bidirectional_adjacency_list`, `in_edge_range` | | `docs/reference/cpo-reference.md` | Add `in_edges`, `in_degree`, `out_edges`, `out_degree`, `find_in_edge`, `contains_in_edge` | -| `docs/reference/type-aliases.md` | Add `in_vertex_edge_range_t`, `in_edge_t`, etc. | +| `docs/reference/type-aliases.md` | Add `in_edge_range_t`, `in_edge_t`, etc. | | `docs/reference/adjacency-list-interface.md` | Add incoming edge section | | `docs/contributing/cpo-implementation.md` | Add `in_edges` as an example of the CPO pattern | | `README.md` | Update feature highlights with bidirectional support | @@ -608,7 +618,7 @@ using adj_list::out_edge_t; 1. Implement `in_edges` CPO in `graph_cpo.hpp` (mirror `edges` CPO) 2. Implement `in_degree` CPO (mirror `degree` CPO) 3. Add `out_edges` / `out_degree` aliases -4. Define `in_vertex_edge_range_t`, `in_edge_t`, etc. +4. Define `in_edge_range_t`, `in_edge_t`, etc. 5. Define `bidirectional_adjacency_list` concept 6. Add `has_in_degree` trait 7. Update `graph.hpp` re-exports @@ -707,9 +717,9 @@ Additional phase-specific gates: | Category | New | Modified | Deleted | |---|---|---|---| | **CPOs** | `in_edges`, `in_degree`, `out_edges` (alias), `out_degree` (alias), `find_out_edge` (alias), `find_in_edge`, `contains_in_edge` | — | — | -| **Concepts** | `in_vertex_edge_range`, `bidirectional_adjacency_list`, `index_bidirectional_adjacency_list` | — | — | +| **Concepts** | `in_edge_range`, `bidirectional_adjacency_list`, `index_bidirectional_adjacency_list` | — | — | | **Traits** | `has_in_degree`, `has_in_degree_v`, `has_find_in_edge`, `has_contains_in_edge` | — | — | -| **Type Aliases** | `in_vertex_edge_range_t`, `in_vertex_edge_iterator_t`, `in_edge_t`, `out_vertex_edge_range_t`, `out_vertex_edge_iterator_t`, `out_edge_t` | — | — | +| **Type Aliases** | `in_edge_range_t`, `in_edge_iterator_t`, `in_edge_t`, `out_edge_range_t`, `out_edge_iterator_t`, `out_edge_t` | — | — | | **Views** | `in_incidence.hpp`, `in_neighbors.hpp` | `bfs.hpp`, `dfs.hpp`, `topological_sort.hpp` (add `EdgeAccessor` param), `adaptors.hpp` (add pipe closures) | — | | **Traversal policies** | `out_edges_accessor`, `in_edges_accessor` | — | — | | **Containers** | — | `dynamic_graph.hpp` (add `Bidirectional`), `compressed_graph.hpp` (add CSC), `undirected_adjacency_list.hpp` (add `in_edges` friend) | — | @@ -746,7 +756,7 @@ Several graph libraries (Boost.Graph, NetworkX, LEDA) distinguish `out_edges` / documented and all algorithms depend on it). Renaming `edges()` to `out_edges()` would: - Break every existing user's code -- Require renaming `vertex_edge_range_t` → `out_vertex_edge_range_t` across 50+ files +- Require renaming `vertex_edge_range_t` → `out_edge_range_t` across 50+ files - Create churn in all 14 algorithms Instead we keep `edges()` as the primary outgoing CPO (matching the "default is outgoing" @@ -759,7 +769,7 @@ directionality. ```cpp template using edge_t = ranges::range_value_t>; // from edges(g,u) -template using in_edge_t = ranges::range_value_t>; // from in_edges(g,u) +template using in_edge_t = ranges::range_value_t>; // from in_edges(g,u) ``` They are commonly the same type, but this is **not required**. Scenarios where diff --git a/agents/archive/incoming_edges_plan.md b/agents/archive/incoming_edges_plan.md new file mode 100644 index 0000000..f9aedb9 --- /dev/null +++ b/agents/archive/incoming_edges_plan.md @@ -0,0 +1,1269 @@ +# Incoming Edges — Phased Implementation Plan + +**Source:** `agents/incoming_edges_design.md` +**Branch:** `incoming` +**Build preset:** `linux-gcc-debug` (full test suite) + +--- + +## How to use this plan + +Each phase is a self-contained unit of work that can be completed by an agent in +one session. Phases are strictly ordered — each depends on the prior phase compiling +and passing all tests. Within each phase, steps are listed in execution order. + +**Conventions used throughout:** + +- "Mirror X" means copy the implementation pattern from X, replacing outgoing + names/semantics with incoming equivalents. +- "Full test suite passes" means `cmake --build build/linux-gcc-debug -j$(nproc)` + succeeds and `cd build/linux-gcc-debug && ctest --output-on-failure` reports + zero failures with no regressions. +- File paths are relative to the repository root. + +--- + +## Phase 1 — `in_edges` and `in_degree` CPOs + type aliases ✅ Done + +**Status:** Complete. All 11 test cases pass; 4316/4316 tests pass with zero +regressions. Committed on branch `incoming`. + +**Goal:** Add the two core incoming-edge CPOs, their public instances, the +outgoing aliases (`out_edges`, `out_degree`, `find_out_edge`), the six new type +aliases, and a CPO unit-test file proving all resolution tiers work. + +**Why first:** Every subsequent phase depends on `in_edges` and `in_edge_t` +existing. This phase touches exactly one production header and adds one test +file, so it is low-risk and easy to validate. + +### Files to modify + +| File | Action | +|---|---| +| `include/graph/adj_list/detail/graph_cpo.hpp` | Add CPOs, aliases, public instances | +| `tests/adj_list/CMakeLists.txt` | Register new test file | + +### Files to create + +| File | Content | +|---|---| +| `tests/adj_list/cpo/test_in_edges_cpo.cpp` | Unit tests | + +### Steps + +#### 1.1 Add `in_edges` CPO (graph_cpo.hpp) + +Insert a new section **after** the `edges` public instance and type aliases +(after line ~835, before the `_target` namespace). Follow the exact structural +pattern of `namespace _edges`: + +``` +namespace _cpo_impls { + namespace _in_edges { + // --- (g, u) overload --- + enum class _St_u { _none, _vertex_member, _adl }; + + template + concept _has_vertex_member_u = is_vertex_descriptor_v<...> && + requires(G& g, const U& u) { + { u.inner_value(g).in_edges() } -> std::ranges::forward_range; + }; + + template + concept _has_adl_u = requires(G& g, const U& u) { + { in_edges(g, u) } -> std::ranges::forward_range; + }; + + // NOTE: No _edge_value_pattern tier — in_edges has no implicit default. + // A graph MUST explicitly provide in_edges() via member or ADL. + + template + [[nodiscard]] consteval _Choice_t<_St_u> _Choose_u() noexcept { ... } + + // --- (g, uid) overload --- + enum class _St_uid { _none, _adl, _default }; + // _has_adl_uid, _has_default_uid (find_vertex + in_edges(g, u)) + template + [[nodiscard]] consteval _Choice_t<_St_uid> _Choose_uid() noexcept { ... } + + class _fn { + // operator()(G&& g, const U& u) — vertex descriptor + // operator()(G&& g, const VId& uid) — vertex ID + // Both use _wrap_if_needed() to ensure edge_descriptor_view return + }; + } +} +``` + +Key differences from `edges`: +- Only 2 tiers for `(g, u)`: `_vertex_member` and `_adl`. No `_edge_value_pattern`. +- The `(g, uid)` default delegates to `in_edges(g, *find_vertex(g, uid))`. +- The `_has_default_uid` concept checks `_has_adl_u` (not `_has_edge_value_pattern`). + +#### 1.2 Add `in_edges` public instance and type aliases + +Immediately after the `_in_edges` namespace closing brace: + +```cpp +inline namespace _cpo_instances { + inline constexpr _cpo_impls::_in_edges::_fn in_edges{}; +} + +template +using in_edge_range_t = decltype(in_edges(std::declval(), std::declval>())); + +template +using in_edge_iterator_t = std::ranges::iterator_t>; + +template +using in_edge_t = std::ranges::range_value_t>; +``` + +#### 1.3 Add `in_degree` CPO + +Insert after the `in_edges` section, mirroring `namespace _degree`: + +``` +namespace _cpo_impls { + namespace _in_degree { + // (g, u): _member (g.in_degree(u)), _adl, _default (size(in_edges(g,u))) + // (g, uid): _member, _adl, _default (*find_vertex then in_degree(g,u)) + class _fn { ... }; + } +} + +inline namespace _cpo_instances { + inline constexpr _cpo_impls::_in_degree::_fn in_degree{}; +} +``` + +Key difference from `out_degree`: the `_default` tier calls `in_edges(g, u)` instead +of `out_edges(g, u)`. + +#### 1.4 Add convenience aliases + +> **Note:** As of the primary/alias swap, `out_edges`, `out_degree`, and +> `find_out_edge` are the primary CPO instances. The shorter names `edges`, +> `degree`, and `find_vertex_edge` are convenience aliases. + +```cpp +inline namespace _cpo_instances { + inline constexpr auto& edges = out_edges; + inline constexpr auto& degree = out_degree; + inline constexpr auto& find_vertex_edge = find_out_edge; +} +``` + +#### 1.5 Type aliases (primary → alias) + +> **Note:** `out_edge_range_t`, `out_edge_iterator_t`, and `out_edge_t` are now +> the primary type alias definitions. The old names are convenience aliases. + +```cpp +template +using out_edge_range_t = decltype(out_edges(std::declval(), ...)); + +template +using out_edge_iterator_t = std::ranges::iterator_t>; + +template +using out_edge_t = std::ranges::range_value_t>; + +// Convenience aliases: +template using vertex_edge_range_t = out_edge_range_t; +template using vertex_edge_iterator_t = out_edge_iterator_t; +template using edge_t = out_edge_t; +``` + +#### 1.6 Create test file `tests/adj_list/cpo/test_in_edges_cpo.cpp` + +Test the following scenarios (mirror `test_edges_cpo.cpp` structure): + +1. **Stub graph with `in_edges()` member** — a minimal struct whose vertex + `inner_value(g).in_edges()` returns a `vector`. Verify the CPO + dispatches to `_vertex_member` tier and the return is an + `edge_descriptor_view`. + +2. **Stub graph with ADL `in_edges(g, u)` friend** — verify `_adl` tier. + +3. **`(g, uid)` overload with default** — verify `_default` tier delegates + through `find_vertex` + `in_edges(g, u)`. + +4. **Type alias verification** — `in_edge_range_t`, + `in_edge_iterator_t`, `in_edge_t` all compile and produce + expected types. + +5. **Mixed-type test** — a stub graph where `out_edge_t` is + `pair` but `in_edge_t` is just `int`. Verify both + aliases are independently deduced. + +6. **`edges` / `degree` / `find_vertex_edge` aliases** — verify they + are the exact same object as `out_edges` / `out_degree` / `find_out_edge` + (use `static_assert(&edges == &out_edges)`). + +7. **`in_degree` CPO** — test member, ADL, and default (`size(in_edges)`) + resolution tiers. + +8. **Alias type verification** — verify `edge_t` is `out_edge_t` etc. + +#### 1.7 Register test in CMakeLists + +Add to `tests/adj_list/CMakeLists.txt` under the CPOs section: + +```cmake + cpo/test_in_edges_cpo.cpp +``` + +#### 1.8 Build and run full test suite + +```bash +cmake --build build/linux-gcc-debug -j$(nproc) +cd build/linux-gcc-debug && ctest --output-on-failure +``` + +### Merge gate + +- [x] Full test suite passes with zero regressions (4316/4316). +- [x] All 8 test scenarios pass (11 test cases total). +- [x] `in_edge_t != edge_t` mixed-type test passes. +- [x] `out_edges` alias identity check passes. + +--- + +## Phase 2 — `find_in_edge` and `contains_in_edge` CPOs + traits + +**Goal:** Add the remaining incoming-edge CPOs (`find_in_edge`, +`contains_in_edge`), the new traits (`has_in_degree`, `has_find_in_edge`, +`has_contains_in_edge`), and their unit tests. + +**Why second:** These CPOs depend on `in_edges` (Phase 1). They complete the +full incoming-edge CPO surface before concepts are defined. + +### Files to modify + +| File | Action | +|---|---| +| `include/graph/adj_list/detail/graph_cpo.hpp` | Add `find_in_edge`, `contains_in_edge` CPOs | +| `include/graph/adj_list/adjacency_list_traits.hpp` | Add 3 new traits | +| `tests/adj_list/CMakeLists.txt` | Register new test files | + +### Files to create + +| File | Content | +|---|---| +| `tests/adj_list/cpo/test_find_in_edge_cpo.cpp` | Unit tests for `find_in_edge` | +| `tests/adj_list/cpo/test_contains_in_edge_cpo.cpp` | Unit tests for `contains_in_edge` | +| `tests/adj_list/traits/test_incoming_edge_traits.cpp` | Trait tests | + +### Steps + +#### 2.1 Add `find_in_edge` CPO (graph_cpo.hpp) + +Mirror `namespace _find_vertex_edge` with three overload groups: + +1. `find_in_edge(g, u, v)` — both descriptors. Default iterates + `in_edges(g, u)` and matches `source_id(g, ie)` against `vertex_id(g, v)`. +2. `find_in_edge(g, u, vid)` — descriptor + ID. Default iterates + `in_edges(g, u)` and matches `source_id(g, ie)` against `vid`. +3. `find_in_edge(g, uid, vid)` — both IDs. Default delegates via + `*find_vertex(g, uid)`. + +Key difference from `find_vertex_edge`: iterates `in_edges` and compares +`source_id` instead of `target_id`. + +```cpp +inline namespace _cpo_instances { + inline constexpr _cpo_impls::_find_in_edge::_fn find_in_edge{}; +} +``` + +#### 2.2 Add `contains_in_edge` CPO (graph_cpo.hpp) + +Mirror `namespace _contains_edge` with two overload groups: + +1. `contains_in_edge(g, u, v)` — both descriptors. +2. `contains_in_edge(g, uid, vid)` — both IDs. + +Default implementations iterate `in_edges(g, u)` and match `source_id`. + +```cpp +inline namespace _cpo_instances { + inline constexpr _cpo_impls::_contains_in_edge::_fn contains_in_edge{}; +} +``` + +#### 2.3 Add traits (adjacency_list_traits.hpp) + +Follow the existing `detail::has_X_impl` → `has_X` → `has_X_v` pattern: + +```cpp +// --- has_in_degree --- +namespace detail { + template + concept has_in_degree_impl = requires(G& g, vertex_t u, vertex_id_t uid) { + { in_degree(g, u) } -> std::integral; + { in_degree(g, uid) } -> std::integral; + }; +} +template concept has_in_degree = detail::has_in_degree_impl; +template inline constexpr bool has_in_degree_v = has_in_degree; + +// --- has_find_in_edge --- +namespace detail { + template + concept has_find_in_edge_impl = requires(G& g, vertex_t u, vertex_t v, + vertex_id_t uid, vertex_id_t vid) { + find_in_edge(g, u, v); + find_in_edge(g, u, vid); + find_in_edge(g, uid, vid); + }; +} +template concept has_find_in_edge = detail::has_find_in_edge_impl; +template inline constexpr bool has_find_in_edge_v = has_find_in_edge; + +// --- has_contains_in_edge --- +namespace detail { + template + concept has_contains_in_edge_impl = requires(G& g, vertex_t u, vertex_t v, + vertex_id_t uid, vertex_id_t vid) { + { contains_in_edge(g, u, v) } -> std::convertible_to; + { contains_in_edge(g, uid, vid) } -> std::convertible_to; + }; +} +template concept has_contains_in_edge = detail::has_contains_in_edge_impl; +template inline constexpr bool has_contains_in_edge_v = has_contains_in_edge; +``` + +#### 2.4 Create test files + +**`test_find_in_edge_cpo.cpp`** — test all 3 overloads: +- Stub graph with ADL `in_edges()` friend returning known edges. +- Verify `find_in_edge(g, u, v)` finds correct incoming edge by `source_id`. +- Verify `find_in_edge(g, u, vid)` works. +- Verify `find_in_edge(g, uid, vid)` delegates through `find_vertex`. +- Verify not-found case. + +**`test_contains_in_edge_cpo.cpp`** — test both overloads: +- Verify `contains_in_edge(g, u, v)` returns true/false correctly. +- Verify `contains_in_edge(g, uid, vid)` returns true/false correctly. + +**`test_incoming_edge_traits.cpp`** — test all 3 traits: +- Stub graph that models `in_edges`/`in_degree` → `has_in_degree_v` is true. +- Plain `vector>` → `has_in_degree_v` is false. +- Stub graph with `in_edges` + `find_in_edge` → `has_find_in_edge_v` is true. +- Stub graph with `in_edges` + `contains_in_edge` → `has_contains_in_edge_v` is true. + +#### 2.5 Register tests in CMakeLists + +Add to `tests/adj_list/CMakeLists.txt`: + +```cmake + cpo/test_find_in_edge_cpo.cpp + cpo/test_contains_in_edge_cpo.cpp + traits/test_incoming_edge_traits.cpp +``` + +#### 2.6 Build and run full test suite + +### Merge gate + +- [ ] Full test suite passes. +- [ ] All `find_in_edge` overloads work (member, ADL, default). +- [ ] All `contains_in_edge` overloads work. +- [ ] Traits correctly detect presence/absence of incoming-edge CPOs. + +--- + +## Phase 3 — Concepts + namespace re-exports + +**Goal:** Define `in_edge_range`, `bidirectional_adjacency_list`, +`index_bidirectional_adjacency_list` concepts; update `graph.hpp` with all +re-exports for Phases 1-3. + +**Why third:** Concepts depend on the CPOs and type aliases from Phases 1-2. +Re-exports should be done once after the complete CPO surface exists. + +### Files to modify + +| File | Action | +|---|---| +| `include/graph/adj_list/adjacency_list_concepts.hpp` | Add 3 concepts | +| `include/graph/graph.hpp` | Add re-exports for all new CPOs, aliases, traits, concepts | +| `tests/adj_list/CMakeLists.txt` | Register new test file | + +### Files to create + +| File | Content | +|---|---| +| `tests/adj_list/concepts/test_bidirectional_concepts.cpp` | Concept tests | + +### Steps + +#### 3.1 Add concepts (adjacency_list_concepts.hpp) + +After the `index_adjacency_list` concept: + +```cpp +template +concept in_edge_range = std::ranges::forward_range; + +template +concept bidirectional_adjacency_list = + adjacency_list && + requires(G& g, vertex_t u, in_edge_t ie) { + { in_edges(g, u) } -> in_edge_range; + { source_id(g, ie) } -> std::convertible_to>; + }; + +template +concept index_bidirectional_adjacency_list = + bidirectional_adjacency_list && index_vertex_range; +``` + +#### 3.2 Update graph.hpp re-exports + +In the `namespace graph { ... }` using-declaration block, add: + +```cpp +// Incoming-edge CPOs +using adj_list::in_edges; +using adj_list::in_degree; +using adj_list::find_in_edge; +using adj_list::contains_in_edge; + +// Outgoing aliases +using adj_list::out_edges; +using adj_list::out_degree; +using adj_list::find_out_edge; + +// Incoming-edge type aliases +using adj_list::in_edge_range_t; +using adj_list::in_edge_iterator_t; +using adj_list::in_edge_t; + +// Outgoing type aliases +using adj_list::out_edge_range_t; +using adj_list::out_edge_iterator_t; +using adj_list::out_edge_t; + +// Incoming-edge concepts +using adj_list::in_edge_range; +using adj_list::bidirectional_adjacency_list; +using adj_list::index_bidirectional_adjacency_list; + +// Incoming-edge traits +using adj_list::has_in_degree; +using adj_list::has_in_degree_v; +using adj_list::has_find_in_edge; +using adj_list::has_find_in_edge_v; +using adj_list::has_contains_in_edge; +using adj_list::has_contains_in_edge_v; +``` + +#### 3.3 Create test file `tests/adj_list/concepts/test_bidirectional_concepts.cpp` + +1. **Stub bidirectional graph** — a struct with both `edges()` and `in_edges()` + ADL friends. `static_assert(bidirectional_adjacency_list)`. + +2. **Outgoing-only graph** — a `vector>`. + `static_assert(!bidirectional_adjacency_list)`. + +3. **Index variant** — stub graph with random-access vertices. + `static_assert(index_bidirectional_adjacency_list)`. + +4. **Mixed-type concept** — stub where `in_edge_t` is just `int` + (lightweight back-pointer) but `source_id(g, ie)` works. + `static_assert(bidirectional_adjacency_list)`. + +5. **Re-export test** — verify `graph::bidirectional_adjacency_list` is + accessible (compile-time only). + +#### 3.4 Register test and build + +### Merge gate + +- [ ] Full test suite passes. +- [ ] `bidirectional_adjacency_list` satisfied by stub bidirectional graph. +- [ ] `bidirectional_adjacency_list` not satisfied by `vector>`. +- [ ] Mixed-type (`in_edge_t != edge_t`) graph satisfies concept. +- [ ] All re-exports compile via `#include `. + +--- + +## Phase 4 — `undirected_adjacency_list` incoming-edge support ✅ Done + +**Status:** Complete. 10 new test cases pass (concept, in_edges, in_degree, +find_in_edge, contains_in_edge, integration); 4352/4352 tests pass with zero +regressions. Committed on branch `incoming`. + +**Goal:** Add `in_edges()` ADL friends to `undirected_adjacency_list` that +return the same ranges as `edges()`, making undirected graphs model +`bidirectional_adjacency_list` at zero cost. `in_degree` handled automatically +by CPO default tier (`size(in_edges(g, u))`). + +**Why fourth:** This is the simplest container change (just forwarding to +existing functions) and provides a real container for integration testing. + +### Files modified + +| File | Action | +|---|---| +| `include/graph/container/undirected_adjacency_list.hpp` | Added `in_edges` ADL friends (mutable + const) | +| `tests/container/CMakeLists.txt` | Registered new test file | + +### Files created + +| File | Content | +|---|---| +| `tests/container/undirected_adjacency_list/test_undirected_bidirectional.cpp` | Integration tests | + +### Merge gate + +- [x] Full test suite passes (4352/4352). +- [x] `undirected_adjacency_list` models `bidirectional_adjacency_list`. +- [x] `undirected_adjacency_list` models `index_bidirectional_adjacency_list`. +- [x] `in_edges(g, u)` and `edges(g, u)` produce identical results. +- [x] `in_degree(g, u) == degree(g, u)` for all vertices. +- [x] `find_in_edge` and `contains_in_edge` work correctly on undirected graphs. + +--- + +## Phase 5 — Edge accessor + parameterized `incidence` / `neighbors` views ✅ Done + +**Status:** Complete. All steps 5.1–5.7 done. `edge_accessor.hpp` created, +`Accessor` template param added to incidence/neighbors views, all factory +functions implemented, tests passing (10 in_incidence + 10 in_neighbors +test cases, 57 assertions total). 4405/4405 tests pass. + +**Goal:** Introduce `out_edge_accessor` / `in_edge_accessor` structs in a +new header, then add an `Accessor` template parameter to `incidence_view`, +`basic_incidence_view`, `neighbors_view`, and `basic_neighbors_view`. A +single parameterized class serves both outgoing and incoming iteration — +no class duplication. Factory functions provide the ergonomic names +(`out_incidence`, `in_incidence`, `incidence` alias). + +**Design rationale (edge accessor):** + +An *edge accessor* is a stateless policy object that bundles three operations: + +| Method | `out_edge_accessor` | `in_edge_accessor` | +|---|---|---| +| `edges(g, u)` | `adj_list::edges(g, u)` | `adj_list::in_edges(g, u)` | +| `neighbor_id(g, e)` | `adj_list::target_id(g, e)` | `adj_list::source_id(g, e)` | +| `neighbor(g, e)` | `adj_list::target(g, e)` | `*adj_list::find_vertex(g, adj_list::source_id(g, e))` | + +By defaulting the accessor to `out_edge_accessor`, existing code is +source-compatible. The same accessor type will be reused for BFS / DFS / +topological-sort parameterization in Phase 7. + +**Naming convention** (matches CPO pattern): + +| Factory (primary) | Factory (alias) | View type | +|---|---|---| +| `out_incidence(g, u)` | `incidence(g, u)` | `incidence_view` | +| `in_incidence(g, u)` | — | `incidence_view` | +| `out_neighbors(g, u)` | `neighbors(g, u)` | `neighbors_view` | +| `in_neighbors(g, u)` | — | `neighbors_view` | + +Same pattern for `basic_*` variants and EVF / VVF overloads. + +**Why fifth:** Views consume the CPOs from Phases 1-3 and can be tested +against the undirected container from Phase 4. + +### Files to create + +| File | Content | +|---|---| +| `include/graph/views/edge_accessor.hpp` | `out_edge_accessor`, `in_edge_accessor` | +| `tests/views/test_in_incidence.cpp` | View tests for `in_incidence` | +| `tests/views/test_in_neighbors.cpp` | View tests for `in_neighbors` | + +### Files to modify + +| File | Action | +|---|---| +| `include/graph/views/incidence.hpp` | Add `Accessor` template param to all view classes; derive types from accessor; update iterators | +| `include/graph/views/neighbors.hpp` | Same transformation | +| `include/graph/graph.hpp` | Include `edge_accessor.hpp`; re-export accessor types | +| `tests/views/CMakeLists.txt` | Register new test files | + +### Steps + +#### 5.1 Create `include/graph/views/edge_accessor.hpp` ✅ Done + +```cpp +#pragma once +#include +#include + +namespace graph::views { + +/// Policy for outgoing-edge iteration (default). +struct out_edge_accessor { + template + [[nodiscard]] constexpr auto edges(G& g, adj_list::vertex_t u) const { + return adj_list::edges(g, u); + } + + template + [[nodiscard]] constexpr auto neighbor_id(G& g, adj_list::edge_t e) const { + return adj_list::target_id(g, e); + } + + template + [[nodiscard]] constexpr auto neighbor(G& g, adj_list::edge_t e) const { + return adj_list::target(g, e); + } +}; + +/// Policy for incoming-edge iteration. +struct in_edge_accessor { + template + [[nodiscard]] constexpr auto edges(G& g, adj_list::vertex_t u) const { + return adj_list::in_edges(g, u); + } + + template + [[nodiscard]] constexpr auto neighbor_id(G& g, adj_list::in_edge_t e) const { + return adj_list::source_id(g, e); + } + + template + [[nodiscard]] constexpr auto neighbor(G& g, adj_list::in_edge_t e) const { + return *adj_list::find_vertex(g, adj_list::source_id(g, e)); + } +}; + +} // namespace graph::views +``` + +Notes: +- `out_edge_accessor` is constrained on `adjacency_list` (the minimum). +- `in_edge_accessor` is constrained on `bidirectional_adjacency_list`. +- Both are stateless — `[[no_unique_address]]` in the view makes them zero-cost. +- `in_edge_accessor::neighbor()` uses `find_vertex(g, source_id(g, e))` + because there is no `source()` CPO. + +#### 5.2 Parameterize `incidence_view` with `Accessor` (incidence.hpp) ✅ Done + +Add `#include ` to the includes. + +Change every view class template to accept a trailing `class Accessor`: + +```cpp +template +class incidence_view; +``` + +In the `void` specialization (`incidence_view`): + +1. **Derive types from accessor:** + ```cpp + using edge_range_type = decltype( + std::declval().edges( + std::declval(), std::declval())); + using edge_type = std::ranges::range_value_t; + ``` + +2. **Store accessor:** + ```cpp + [[no_unique_address]] Accessor accessor_{}; + ``` + +3. **Update iterator** — add `const Accessor* acc_` member; use + `acc_->neighbor_id(*g_, current_)` in `operator*()`. + +4. **Update `begin()` / `end()` / `size()`** — call + `accessor_.edges(*g_, source_)` instead of `adj_list::edges(...)`. + +5. **Pass `&accessor_`** to iterator construction. + +Same transformation for the non-void EVF specialization and both +`basic_incidence_view` specializations. + +Deduction guides gain the `Accessor` parameter with default: +```cpp +template +incidence_view(G&, adj_list::vertex_t) + -> incidence_view; + +template +incidence_view(G&, adj_list::vertex_t, EVF) + -> incidence_view; +``` + +#### 5.3 Add `out_incidence` / `in_incidence` factory functions ✅ Done + +After the existing `incidence()` factories (which remain unchanged and +serve as aliases), add: + +```cpp +// --- out_incidence: explicit outgoing factories --- +template +[[nodiscard]] constexpr auto out_incidence(G& g, adj_list::vertex_t u) noexcept { + return incidence_view(g, u); +} +template +requires edge_value_function> +[[nodiscard]] constexpr auto out_incidence(G& g, adj_list::vertex_t u, EVF&& evf) { + return incidence_view, out_edge_accessor>( + g, u, std::forward(evf)); +} +// uid overloads ... + +// --- in_incidence: incoming factories --- +template +[[nodiscard]] constexpr auto in_incidence(G& g, adj_list::vertex_t u) noexcept { + return incidence_view(g, u); +} +template +requires edge_value_function> +[[nodiscard]] constexpr auto in_incidence(G& g, adj_list::vertex_t u, EVF&& evf) { + return incidence_view, in_edge_accessor>( + g, u, std::forward(evf)); +} +// uid overloads ... + +// basic_out_incidence / basic_in_incidence — same pattern ... +``` + +Existing `incidence()` / `basic_incidence()` factories are **unchanged** — +they already create `out_edge_accessor` views by default. + +#### 5.4 Same transformation for `neighbors.hpp` ✅ Done + +- Add `Accessor` template param to `neighbors_view` and + `basic_neighbors_view`. +- Switch from `adj_list::edges(...)` to `accessor_.edges(...)`. +- Switch from `adj_list::target(...)` / `adj_list::vertex_id(...)` to + `accessor_.neighbor(...)` / `accessor_.neighbor_id(...)`. +- Add `out_neighbors()` / `in_neighbors()` factories; keep `neighbors()` + as the alias (unchanged, creates `out_edge_accessor` by default). +- Same for `basic_out_neighbors()` / `basic_in_neighbors()`. + +#### 5.5 Update `graph.hpp` ✅ Done + +- Add `#include `. +- Re-export `out_edge_accessor`, `in_edge_accessor` into `graph::views`. +- Re-export `out_incidence`, `in_incidence`, `out_neighbors`, + `in_neighbors`, `basic_out_incidence`, `basic_in_incidence`, + `basic_out_neighbors`, `basic_in_neighbors` factory functions. + +#### 5.6 Create test files ✅ Done + +**`test_in_incidence.cpp`** — mirror `test_incidence.cpp`: +- Use `undirected_adjacency_list` (from Phase 4) as the bidirectional graph. +- Verify `in_incidence(g, u)` yields `{source_id, edge}` tuples. +- Verify `in_incidence(g, u, evf)` yields `{source_id, edge, value}`. +- Verify `basic_in_incidence(g, uid)` yields `{source_id}`. +- Verify factory function overloads (descriptor and ID). +- Verify `incidence_view` is the same type as + `incidence_view` (compile-time). +- Verify `incidence_view` is a different type. + +**`test_in_neighbors.cpp`** — mirror `test_neighbors.cpp`: +- Verify `in_neighbors(g, u)` yields `{source_id, vertex}` tuples. +- Verify `basic_in_neighbors(g, uid)` yields `{source_id}`. +- Verify alias equivalence. + +#### 5.7 Register tests and build ✅ Done + +### Merge gate + +- [x] Full test suite passes (zero regressions — default `Accessor` preserves + all existing `incidence_view` / `incidence()` usage). +- [x] `in_incidence(g, u)` iterates incoming edges, yields `source_id`. +- [x] `in_neighbors(g, u)` iterates source vertices. +- [x] All factory function overloads work (descriptor + uid, with/without EVF). +- [x] `incidence_view` defaults to `out_edge_accessor` (compile-time). +- [x] Accessor is zero-cost (`sizeof(incidence_view)` unchanged). + +--- + +## Phase 6 — Pipe-syntax adaptors (rename + incoming) ✅ Done + +**Status:** Complete. All steps 6.1–6.3 done. Adaptor closures for +`out_incidence`/`in_incidence`/`out_neighbors`/`in_neighbors` added, +`incidence`/`neighbors` aliases point to outgoing variants, all 8 pipe +expressions tested. Address-equality test confirms aliases. 4405/4405 tests pass. + +**Goal:** Add `out_incidence` / `in_incidence` / `out_neighbors` / +`in_neighbors` adaptor instances with `incidence` / `neighbors` as aliases. +The adaptor closures now forward to the parameterized views from Phase 5. + +**Why sixth:** Adaptors depend on the accessor-parameterized views from Phase 5. + +### Files to modify + +| File | Action | +|---|---| +| `include/graph/views/adaptors.hpp` | Add new adaptor closures using accessor-parameterized views | +| `tests/views/test_adaptors.cpp` | Add pipe-syntax tests for incoming views | + +### Steps + +#### 6.1 Add outgoing-primary and incoming adaptor closures (adaptors.hpp) ✅ Done + +Rename existing adaptor classes and instances to `out_*` primary names. +Then add new `in_*` adaptor closures that construct the incoming accessor +variant of the views. Keep the short names as aliases: + +```cpp +// Renamed adaptor classes: +// incidence_adaptor_fn → out_incidence_adaptor_fn +// basic_incidence_adaptor_fn → basic_out_incidence_adaptor_fn +// neighbors_adaptor_fn → out_neighbors_adaptor_fn +// basic_neighbors_adaptor_fn → basic_out_neighbors_adaptor_fn + +// New incoming adaptor classes — same pattern but create +// in_edge_accessor views: +// in_incidence_adaptor_fn, basic_in_incidence_adaptor_fn +// in_neighbors_adaptor_fn, basic_in_neighbors_adaptor_fn + +namespace graph::views::adaptors { + +// Primary outgoing +inline constexpr out_incidence_adaptor_fn out_incidence{}; +inline constexpr basic_out_incidence_adaptor_fn basic_out_incidence{}; +inline constexpr out_neighbors_adaptor_fn out_neighbors{}; +inline constexpr basic_out_neighbors_adaptor_fn basic_out_neighbors{}; + +// Aliases (incidence = out_incidence, etc.) +inline constexpr auto& incidence = out_incidence; +inline constexpr auto& basic_incidence = basic_out_incidence; +inline constexpr auto& neighbors = out_neighbors; +inline constexpr auto& basic_neighbors = basic_out_neighbors; + +// Incoming +inline constexpr in_incidence_adaptor_fn in_incidence{}; +inline constexpr basic_in_incidence_adaptor_fn basic_in_incidence{}; +inline constexpr in_neighbors_adaptor_fn in_neighbors{}; +inline constexpr basic_in_neighbors_adaptor_fn basic_in_neighbors{}; + +} // namespace graph::views::adaptors +``` + +#### 6.2 Add tests to `test_adaptors.cpp` ✅ Done + +```cpp +TEST_CASE("pipe: g | out_incidence(uid)", "[adaptors][out_incidence]") { ... } +TEST_CASE("pipe: g | in_incidence(uid)", "[adaptors][in_incidence]") { ... } +TEST_CASE("pipe: g | in_incidence(uid, evf)", "[adaptors][in_incidence]") { ... } +TEST_CASE("pipe: g | basic_in_incidence(uid)", "[adaptors][basic_in_incidence]") { ... } +TEST_CASE("pipe: g | in_neighbors(uid)", "[adaptors][in_neighbors]") { ... } +TEST_CASE("pipe: g | basic_in_neighbors(uid)", "[adaptors][in_neighbors]") { ... } +TEST_CASE("pipe: incidence/neighbors aliases", "[adaptors][aliases]") { + // Verify &incidence == &out_incidence, &neighbors == &out_neighbors +} +``` + +#### 6.3 Build and run full test suite ✅ Done + +### Merge gate + +- [x] Full test suite passes (zero regressions — alias forwarding ensures + existing `incidence`/`neighbors` usage still compiles). +- [x] `g | out_incidence(uid)` produces same results as `out_incidence(g, uid)`. +- [x] `g | in_incidence(uid)` produces same results as `in_incidence(g, uid)`. +- [x] `incidence` / `neighbors` aliases resolve to `out_incidence` / `out_neighbors`. +- [x] All 8 pipe expressions from design doc §8.4 work. + +--- + +## Phase 7 — BFS/DFS/topological-sort `Accessor` parameterization ✅ Done + +**Status:** Complete. All 6 traversal view classes (BFS ×2, DFS ×2, topo ×2) +parameterized with `Accessor` template parameter defaulting to +`out_edge_accessor`. 22 new tests in `test_reverse_traversal.cpp`. 4405/4405 +tests pass on GCC. + +**Bug fix discovered:** `in_edge_accessor::neighbor()` was incorrectly calling +`adj_list::source(g, e)` which, for in-edge descriptors, returns the owning +vertex (target) instead of the actual source. Fixed to use +`*adj_list::find_vertex(g, adj_list::source_id(g, e))` which correctly +navigates the in-edge container. + +**Implementation notes:** +- Accessor is the LAST template parameter with default `out_edge_accessor` +- Accessor is NOT stored as a member; instantiated inline as `Accessor{}` (zero-cost) +- Factory function overloads: Accessor as first explicit template param for deduction + (`vertices_bfs(g, seed)`) +- Edge types derived via `Accessor::template edge_t` and `edge_range_t` +- Edge iterators: `std::ranges::iterator_t>` +- Replaced `adj_list::edges()` → `Accessor{}.edges()`, `adj_list::target()` → `Accessor{}.neighbor()` + +### Files to modify + +| File | Action | +|---|---| +| `include/graph/views/bfs.hpp` | Add `Accessor` param to `vertices_bfs_view`, `edges_bfs_view`; replace 5 hardcoded `adj_list::edges()` calls with `accessor_.edges()` | +| `include/graph/views/dfs.hpp` | Same for `vertices_dfs_view`, `edges_dfs_view`; replace 5 calls | +| `include/graph/views/topological_sort.hpp` | Same for `vertices_topological_sort_view`, `edges_topological_sort_view`; replace 7 calls | +| `tests/views/CMakeLists.txt` | Register new test file | + +### Files to create + +| File | Content | +|---|---| +| `tests/views/test_reverse_traversal.cpp` | Reverse BFS/DFS tests | + +### Steps + +#### 7.1 Parameterize BFS views (bfs.hpp) + +For each of `vertices_bfs_view` and `edges_bfs_view`: + +1. Add `class Accessor = out_edge_accessor` as the last template parameter + (before `Alloc` if present, or after the value function type). + +2. Store `[[no_unique_address]] Accessor accessor_;` member (zero-cost for + stateless accessors). + +3. Replace every `adj_list::edges(g, v)` or `adj_list::edges(*g_, v)` with + `accessor_.edges(g, v)` or `accessor_.edges(*g_, v)`. + +4. Add factory function overloads accepting `Accessor`: + ```cpp + vertices_bfs(g, seed, vvf, accessor) + edges_bfs(g, seed, evf, accessor) + ``` + +5. Existing signatures remain unchanged (default `Accessor`). + +#### 7.2 Parameterize DFS views (dfs.hpp) + +Same transformation: 5 `adj_list::edges()` calls → `accessor_.edges()`. + +#### 7.3 Parameterize topological sort views (topological_sort.hpp) + +Same transformation: 7 `adj_list::edges()` calls → `accessor_.edges()`. + +#### 7.4 Create `test_reverse_traversal.cpp` + +Using an undirected_adjacency_list (where `in_edges == edges`): + +1. **Forward BFS** — `vertices_bfs(g, seed)` produces expected order. +2. **Reverse BFS** — `vertices_bfs(g, seed, void_fn, in_edge_accessor{})` + produces expected order (same as forward for undirected). +3. **Forward DFS** vs **Reverse DFS** — same pattern. +4. **Source-compatibility** — existing call sites `vertices_bfs(g, seed)` and + `vertices_bfs(g, seed, vvf)` still compile unchanged. + +#### 7.5 Build and run full test suite + +### Merge gate + +- [x] Full test suite passes (zero regressions in existing BFS/DFS/topo tests). +- [x] Reverse BFS/DFS with `in_edge_accessor` produces correct traversal. +- [x] Existing factory signatures compile without changes. +- [x] No new headers created (reuses `edge_accessor.hpp` from Phase 5). + +--- + +## Phase 8 — `dynamic_graph` bidirectional support ✅ Done + +**Status:** Complete. All steps 8.1–8.10 done. 4391/4391 tests pass on both +GCC-15 and Clang, zero regressions. Direction tags (Option A) fully implemented +with `requires` constraints on tag-dependent member functions. + +**Root cause of original failures (resolved):** The `edge_descriptor` was +exclusively out-edge oriented. For in-edges: +- `source_id()` returned the *owning* vertex's ID (which is the *target* for in-edges) +- `target_id(vertex_data)` navigated `.edges()` (the out-edge container) instead of + `.in_edges()` +- `inner_value(vertex_data)` and `underlying_value(vertex_data)` did the same + +All three CPOs (source_id, target_id, edge_value) produced wrong results when the +descriptor wrapped an in-edge for random-access containers (vov). Forward-iterator +containers (vol) work because Tier 1 (`(*uv.value()).source_id()`) bypasses the +descriptor entirely. + +### Design Decision: Option A — Direction Tag on `edge_descriptor` + +**Chosen approach:** Extend `edge_descriptor` and `edge_descriptor_view` with an +`EdgeDirection` template parameter (defaulting to `out_edge_tag`) rather than +creating separate in-edge descriptor classes. + +**Rationale:** +- Users never see `edge_descriptor` directly — they use `out_edge_t` / `edge_t` + and `in_edge_t`. The descriptor is internal plumbing. +- The ~250 lines of `if constexpr` chains in `inner_value`/`target_id`/`underlying_value` + are the densest code in the project. A single source of truth avoids maintaining two + parallel 680-line files. +- Adding `if constexpr (Direction == in_edge_tag)` branches is one more axis in the + existing `if constexpr` pattern (which already handles random-access vs forward, + void vs non-void, sourced vs unsourced). +- The direction tag enables CPO dispatch via `is_in_edge_descriptor_v`. + +**Mirror principle (A1):** In-edge descriptor methods mirror out-edge descriptor +methods with source↔target roles swapped and `.edges()`↔`.in_edges()` container +access swapped: + +| Method | Out-edge (`out_edge_tag`) | In-edge (`in_edge_tag`) | +|---|---|---| +| `source_id()` | Trivial: `source_.vertex_id()` | Navigates: `.in_edges()[idx].source_id()` | +| `target_id(vd)` | Navigates: `.edges()[idx].target_id()` | Trivial: `source_.vertex_id()` | +| `underlying_value(vd)` | Navigates `.edges()` container | Navigates `.in_edges()` container | +| `inner_value(vd)` | Navigates `.edges()` container | Navigates `.in_edges()` container | + +**Goal:** Fix all three CPOs for in-edges by adding direction-aware branches to +`edge_descriptor`, `edge_descriptor_view`, descriptor traits, and the CPO +resolution tiers. + +**Why eighth:** This is the most complex container change. All prerequisite +infrastructure (CPOs, concepts, views) is already proven by earlier phases. + +### Files to modify + +| File | Action | +|---|---| +| `include/graph/adj_list/edge_descriptor.hpp` | Add `EdgeDirection` tag param; add `source_id(vertex_data)` overload for in-edges; add `if constexpr` branches in `target_id`, `underlying_value`, `inner_value` to navigate `.in_edges()` | +| `include/graph/adj_list/edge_descriptor_view.hpp` | Add `EdgeDirection` tag param; propagate to `edge_descriptor` construction | +| `include/graph/adj_list/descriptor_traits.hpp` | Update `is_edge_descriptor` to match new signature; add `is_in_edge_descriptor` trait; update concepts | +| `include/graph/detail/edge_cpo.hpp` | Update `_adj_list_descriptor` tiers for source_id (add `source_id(vertex_data)` path for in-edges), target_id (use trivial `source_.vertex_id()` for in-edges), edge_value (use `.in_edges()` for in-edges) | +| `include/graph/container/dynamic_graph.hpp` | Add `in_edges(g,u)` ADL friend returning `edge_descriptor_view<..., in_edge_tag>` | +| `tests/container/CMakeLists.txt` | Already registered | + +### Files already created + +| File | Content | +|---|---| +| `tests/container/dynamic_graph/test_dynamic_graph_bidirectional.cpp` | Comprehensive tests (880+ lines, 27 test cases) | + +### Steps + +#### 8.1 Add `Bidirectional` template parameter ✅ Done + +Propagated through `dynamic_graph`, `dynamic_edge`, `dynamic_vertex`, all +specializations, all 9 traits files, and 27 other traits headers. + +#### 8.2 Add reverse edge storage (conditional) ✅ Done + +`dynamic_vertex_bidir_base<..., true, Traits>` stores `edges_type in_edges_` +with `in_edges()` accessor. Empty base for `Bidirectional=false`. + +#### 8.3 Update `load_edges` to populate both containers ✅ Done + +`load_edges` inserts into both `edges()` and `in_edges()` when Bidirectional. + +#### 8.4 Fix `_has_default_uid` in `graph_cpo.hpp` ✅ Done + +Updated `_in_edges::_has_default_uid` concept to accept `_has_vertex_member_u` +in addition to `_has_adl_u`. + +#### 8.5 Add direction tags and update edge_descriptor ✅ Done + +1. **Define direction tags** in `descriptor.hpp` (root definitions): + ```cpp + struct out_edge_tag {}; + struct in_edge_tag {}; + ``` + +2. **Add `EdgeDirection` parameter** to `edge_descriptor`: + - Default: `out_edge_tag` + - Add `static constexpr bool is_in_edge = std::is_same_v` + - Existing `source_id()` (no-arg) behavior unchanged for `out_edge_tag` + +3. **Add `source_id(vertex_data)` overload** for `in_edge_tag`: + - Mirrors `target_id(vertex_data)` — navigates `.in_edges()` container + - For random-access: `in_edges()[edge_storage_].source_id()` + - For forward: `(*edge_storage_).source_id()` + +4. **Add `if constexpr` branches** to `target_id(vertex_data)`: + - `out_edge_tag`: existing logic (navigate `.edges()`) + - `in_edge_tag`: trivial — return `source_.vertex_id()` + +5. **Add `if constexpr` branches** to `underlying_value(vertex_data)` and + `inner_value(vertex_data)`: + - `out_edge_tag`: existing logic (navigate `.edges()`) + - `in_edge_tag`: navigate `.in_edges()` instead + +#### 8.6 Update `edge_descriptor_view` with direction tag ✅ Done + +Added `EdgeDirection` parameter (default `out_edge_tag`), propagated to +`edge_descriptor` construction. Updated `enable_borrowed_range` for 3-param. + +#### 8.7 Update `descriptor_traits.hpp` ✅ Done + +- All 11 `is_edge_descriptor` specializations updated for 3-param +- Added `is_in_edge_descriptor` and `is_in_edge_descriptor_v` traits +- Updated `edge_descriptor_type` concept + +#### 8.8 Update CPO tiers in `edge_cpo.hpp` ✅ Done + +- **source_id** tier 4: `if constexpr (_E::is_in_edge)` with fallback safety + for graphs without `.in_edges()` on vertex data +- **target_id** and **edge_value**: direction-aware via descriptor methods + +#### 8.9 Add `in_edges(g,u)` ADL friend in `dynamic_graph.hpp` ✅ Done + +Added non-const + const `in_edges(g, u)` ADL friends with `requires(Bidirectional)` +returning `edge_descriptor_view<..., in_edge_tag>`. Added `_wrap_in_edge` helper +in `graph_cpo.hpp` `_in_edges` namespace. + +#### 8.10 Build on GCC and Clang, run all tests ✅ Done + +4391/4391 tests pass on both GCC-15 and Clang. Also added `requires` constraints +on tag-dependent member functions (`source_id(vertex_data)` requires `is_in_edge`, +`target_id(vertex_data)` split into two constrained overloads with `requires(is_in_edge)` +and `requires(is_out_edge)`) and added `is_out_edge` complement to `is_in_edge`. + +### Merge gate + +- [x] Full test suite passes (4391/4391 on GCC and Clang). +- [x] All bidirectional test cases pass (69 bidir-related tests). +- [x] Non-bidirectional mode has identical behavior (no regressions). +- [x] `bidirectional_adjacency_list` concept satisfied. +- [x] vov and vol trait types both work correctly. + +--- + +## Phase 9 — Algorithm updates (Kosaraju + transpose_view) + +**Goal:** Optimize Kosaraju's SCC to use `in_edges()` when available; +add `transpose_view` as a zero-cost adaptor. + +**Why ninth:** Algorithms depend on container support (Phase 8) and the +edge accessor infrastructure (Phase 5/7). + +### Files to modify + +| File | Action | +|---|---| +| `include/graph/algorithm/connected_components.hpp` | Add `if constexpr (bidirectional_adjacency_list)` branch using `in_edges_accessor` | +| `tests/algorithms/CMakeLists.txt` | Register new test file | + +### Files to create + +| File | Content | +|---|---| +| `include/graph/views/transpose.hpp` | `transpose_view` wrapper | +| `tests/algorithms/test_scc_bidirectional.cpp` | SCC tests with bidirectional graph | +| `tests/views/test_transpose.cpp` | Transpose view tests | + +### Steps + +#### 9.1 Add `transpose_view` — Done + +Created `include/graph/views/transpose.hpp` — zero-cost bidirectional graph +adaptor that swaps edges↔in_edges, target↔source via ADL friends. +Documented limitation: CPO tier-1 (`_native_edge_member`) bypasses ADL for +forward-iterator containers (vol, dol), so transpose_view works correctly +only for random-access containers (vov, vod, dov, dod, dofl). + +#### 9.2 Optimize Kosaraju's SCC — Done + +Added `kosaraju(G&& g, Component& component)` overload constrained on +`index_bidirectional_adjacency_list`. Uses manual iterative stack-based DFS +with `in_edges`/`source_id` for the second pass — works with ALL container +types (no transpose_view needed). Same O(V+E) complexity with lower constant +factor (no transpose construction). + +#### 9.3 Create tests and build — Done + +- `tests/views/test_transpose.cpp` — 9 test cases, 14 assertions + (vertex forwarding, edge swap, degree swap, double-transpose, edge_value, + empty graph, single vertex) +- `tests/algorithms/test_scc_bidirectional.cpp` — 14 test cases, 68 assertions + (single vertex, cycle, two SCCs, DAG, complex, self-loops, weighted, + disconnected, agreement with two-graph overload — tested on both vov and vol) +- Updated `graph.hpp` to include `transpose.hpp` +- 4405/4405 tests pass on GCC-15 and Clang + +### Merge gate + +- [x] Full test suite passes (4405/4405 on GCC-15 and Clang). +- [x] SCC produces correct results on bidirectional graph (vov + vol). +- [x] `transpose_view` swaps edge directions correctly (vov). + +--- + +## Phase 10 — Documentation ✅ Done + +**Goal:** Update all documentation per design doc §13. + +### Files to modify + +Per the table in design doc §13.1 (11 files), plus: + +| File | Action | +|---|---| +| `docs/index.md` | Add "Bidirectional edge access" feature | +| `docs/getting-started.md` | Add incoming edges section | +| `docs/user-guide/views.md` | Add `in_incidence`, `in_neighbors` | +| `docs/user-guide/algorithms.md` | Note bidirectional benefits | +| `docs/user-guide/containers.md` | Document `Bidirectional` parameter | +| `docs/reference/concepts.md` | Add `bidirectional_adjacency_list` | +| `docs/reference/cpo-reference.md` | Add all incoming CPOs | +| `docs/reference/type-aliases.md` | Add `in_edge_t` etc. | +| `docs/reference/adjacency-list-interface.md` | Add incoming edge section | +| `docs/contributing/cpo-implementation.md` | Add `in_edges` example | +| `README.md` | Update feature highlights | +| `CHANGELOG.md` | Add entry | + +### Files to create + +| File | Content | +|---|---| +| `docs/user-guide/bidirectional-access.md` | Tutorial guide | + +### Steps + +1. Update each file per the table. +2. Create the tutorial guide with examples using `in_edges`, `in_incidence`, + reverse BFS, and bidirectional `dynamic_graph`. +3. Add CHANGELOG entry. +4. Build docs and verify no broken links. + +### Merge gate + +- [x] All docs updated. +- [x] Tutorial compiles (code examples are tested). +- [x] No broken internal links. + +--- + +## Phase summary + +| Phase | Title | New files | Modified files | Key deliverable | Status | +|---|---|---|---|---|---| +| 1 | `in_edges`/`in_degree` CPOs + aliases | 1 test | 2 | Core CPOs + type aliases | **Done** | +| 2 | `find_in_edge`/`contains_in_edge` + traits | 3 tests | 3 | Complete CPO surface | Done | +| 3 | Concepts + re-exports | 1 test | 2 | `bidirectional_adjacency_list` concept | Done | +| 4 | Undirected container support | 1 test | 1 | First real bidirectional container | **Done** | +| 5 | Edge accessor + parameterized views | 1 header + 2 tests | 4 | `out/in_edge_accessor` + accessor-parameterized views | **Done** | +| 6 | Pipe-syntax adaptors | — | 2 | `g \| in_incidence(uid)` | **Done** | +| 7 | BFS/DFS/topo Accessor | 1 test | 3 | Reverse traversal (reuses Phase 5 accessor) | **Done** | +| 8 | `dynamic_graph` bidirectional | 1 test | 1 | Directed bidirectional container | **Done** | +| 9 | Algorithms | 2 headers + 2 tests | 1 | Kosaraju + transpose | **Done** | +| 10 | Documentation | 1 guide | 12 | Complete docs | **Done** | + +**Total estimated effort:** 11-15 days (same as design doc estimate) + +--- + +## Safety principles + +1. **Each phase adds only; nothing is removed or renamed.** + Existing tests pass between every phase. + +2. **Default template arguments preserve source compatibility.** + `Accessor = out_edge_accessor` means no existing call site changes. + +3. **The edge accessor is introduced once (Phase 5) and reused everywhere.** + Phases 5, 6, and 7 all share `out_edge_accessor` / `in_edge_accessor` + from `edge_accessor.hpp`, eliminating class duplication in views and + avoiding a separate accessor header in Phase 7. + +4. **Stub graphs in early phases isolate CPO testing from container code.** + Phases 1-3 don't touch any container — they use lightweight test stubs. + +5. **The simplest container change comes first (Phase 4).** + Undirected just forwards to the existing `edges()` function. + +6. **The most complex change (Phase 8) comes last in the implementation + sequence**, after all the infrastructure it depends on is proven. + +7. **Each phase has an explicit merge gate** — a checklist of conditions + that must pass before proceeding. diff --git a/agents/dynamic_edge_refactor_plan.md b/agents/dynamic_edge_refactor_plan.md new file mode 100644 index 0000000..defac39 --- /dev/null +++ b/agents/dynamic_edge_refactor_plan.md @@ -0,0 +1,916 @@ +# Implementation Plan: dynamic_edge → dynamic_out_edge / dynamic_in_edge Refactor + +> **Strategy source:** [dynamic_edge_refactor_strategy.md](dynamic_edge_refactor_strategy.md) +> +> This plan is designed for safe, incremental, agent-driven implementation. +> Each phase is self-contained and has explicit build/test gates. + +--- + +## Overview + +**Goal:** Rename `dynamic_edge` → `dynamic_out_edge`, introduce `dynamic_in_edge`, remove +the `bool Sourced` template parameter from the entire class hierarchy, and support both +uniform and non-uniform bidirectional edge configurations. + +**Key invariant:** Every phase must meet its defined verification gate before merge. +Where a phase temporarily allows test breakage (Phase 4), that exception is explicit and +the next phase must restore full green status. + +**Files of interest (reference):** +- Core header: `include/graph/container/dynamic_graph.hpp` (2221 lines) +- Traits: 27 files in `include/graph/container/traits/` +- Test infra: `tests/common/graph_test_types.hpp` (460 lines) +- Container tests: ~48 files in `tests/container/dynamic_graph/` +- Edge comparison tests: `tests/container/dynamic_graph/test_dynamic_edge_comparison.cpp` (450 lines) + — directly instantiates all 4 `dynamic_edge` specializations with explicit `Sourced=true/false` +- CPO tests: 26 files in `tests/adj_list/cpo/` +- Concept tests: `tests/adj_list/concepts/test_bidirectional_concepts.cpp` + +--- + +## Phase 1 — Add `dynamic_in_edge` (additive only, no existing code changes) + +**Goal:** Introduce `dynamic_in_edge` and the detection-idiom aliases in `dynamic_graph_base`. +No existing class, trait, or test is modified. The full test suite must remain green. + +### Step 1.1 — Add `dynamic_in_edge` class to `dynamic_graph.hpp` + +**Location:** After the last `dynamic_edge` specialization (after line ~627) and before +`dynamic_vertex_bidir_base` (line ~635). + +**What to add:** + +1. **`dynamic_edge_source` forward-compatible wrapper** — do NOT modify the existing + `dynamic_edge_source` yet. Instead, create a *new* `dynamic_in_edge_source` base class + that unconditionally stores `source_id_` (no `Sourced` parameter). This avoids touching + live code. + + ```cpp + // Temporary base — will replace dynamic_edge_source in Phase 4a. + template + class dynamic_in_edge_source { ... }; // stores source_id_ + ``` + + Members: `source_id()` const/non-const, `vertex_id_type`, constructors for `(source_id)`. + +2. **`dynamic_in_edge` — 2 specializations:** + + a. Primary (`EV != void`): + ```cpp + template + class dynamic_in_edge : public dynamic_in_edge_source<...>, + public dynamic_edge_value + ``` + - Inherits `dynamic_edge_value` with `Sourced=true` as a pass-through (it ignores Sourced). + - Constructors: `(source_id)`, `(source_id, value)`, `(source_id, value&&)`. + - `operator<=>` compares `source_id()`. + - `operator==` compares `source_id()` (and value if present). + + b. Specialization (`EV = void`): + ```cpp + template + class dynamic_in_edge + : public dynamic_in_edge_source<...> + ``` + - Constructor: `(source_id)`. + - `operator<=>` / `operator==` by `source_id()`. + +3. **Forward declaration** — add near line 91: + ```cpp + template + class dynamic_in_edge; + ``` + +**Safety:** This is purely additive — no existing template is modified. + +### Step 1.2 — Add `std::hash` specialization + +**Location:** After the existing `std::hash>` (after line ~2221). + +```cpp +template +struct hash> { + size_t operator()(const ... & edge) const noexcept { + return std::hash{}(edge.source_id()); + } +}; +``` + +### Step 1.3 — Add shared detection-idiom helpers and derived aliases + +**Location:** Add helpers in a shared scope (e.g., `graph::container::detail` in +`dynamic_graph.hpp`), then consume them in both `dynamic_graph_base` and +`dynamic_vertex_bidir_base`. + +Add shared helpers (preferred names are illustrative): + +```cpp +template class Op, class... Args> +using detected_or_t = /* detection-idiom type */; + +template +using detect_in_edge_type = typename T::in_edge_type; + +template +using detect_in_edges_type = typename T::in_edges_type; + +// Derived aliases +using edge_type = typename Traits::edge_type; +using out_edge_type = edge_type; +using in_edge_type = detail::detected_or_t; +using in_edges_type = detail::detected_or_t; +``` + +**Important:** `edge_type` is already used in `dynamic_graph_base` (it comes from `Traits::edge_type`). The new aliases (`out_edge_type`, `in_edge_type`, `in_edges_type`) are added alongside it. Existing code that uses `edge_type` is unchanged. + +### Step 1.4 — Add compile-time safety checks + +In `dynamic_graph_base`, add static assertions: + +```cpp +// Verify alias consistency +static_assert(std::same_as || + !std::same_as, + "Non-uniform traits must also define in_edges_type when in_edge_type differs from edge_type"); +``` + +Also add positive assertions for default uniform behavior when aliases are absent. + +### Step 1.5 — Verify + +- Build the full project: `cmake --build build/linux-gcc-debug` +- Run the full test suite: `ctest --test-dir build/linux-gcc-debug` +- **Expected:** Zero failures, zero compile errors. New code is dead (unreferenced). + +### Step 1.6 — Commit + +Message: `feat: add dynamic_in_edge class and detection-idiom aliases (Phase 1)` + +### Phase 1 Gate (must pass) + +- Build: `cmake --build build/linux-gcc-debug` +- Tests: `ctest --test-dir build/linux-gcc-debug` +- Audit: no runtime behavior changes; additive-only diff in `dynamic_graph.hpp` + +### Doc notes for Phase 1 +- `dynamic_in_edge` structurally mirrors `dynamic_out_edge` will mirror later; at this point `dynamic_edge` is still the live out-edge class. +- `dynamic_in_edge_source` is a temporary class — Phase 4a will repurpose the existing `dynamic_edge_source` and remove this temporary. +- The detection-idiom aliases are in `dynamic_graph_base` but are not yet used by any live path. + +--- + +## Phase 2 — Wire `dynamic_in_edge` into bidirectional support + +**Goal:** `dynamic_vertex_bidir_base<..., true>` uses the graph-derived `in_edges_type` +(which falls back to `edges_type` for all existing traits). Bidirectional `load_edges` +uses `in_edge_type` construction. All existing tests pass unchanged. + +### Step 2.1 — Update `dynamic_vertex_bidir_base<..., true>` to use `in_edges_type` + +**Current** (line ~671): +```cpp +using edges_type = typename Traits::edges_type; // used for in_edges storage +``` + +**Change to:** +```cpp +// Use graph-derived in_edges_type (falls back to edges_type for standard traits) +using in_edges_type_t = detail::detected_or_t; +using edges_type = in_edges_type_t; // rename kept for backward compat of internal member +``` + +**Key insight:** For all existing standard traits (27 files), `detect_in_edges_type` is absent, +so `in_edges_type_t` falls back to `Traits::edges_type`. Behavior is identical to current. The return +type of `in_edges()` is `edges_type&`, which still resolves to the same type. + +### Step 2.2 — Update `load_edges` bidirectional branches + +**Current pattern** (3 occurrences — associative, sequential, fallback paths): +```cpp +if constexpr (Sourced) { + if constexpr (is_void_v) { + if constexpr (Bidirectional) { + emplace_edge(rev, e.source_id, edge_type(e.source_id, e.target_id)); + } + emplace_edge(vertex_edges, e.target_id, edge_type(e.source_id, e.target_id)); + } else { + if constexpr (Bidirectional) { + emplace_edge(rev, e.source_id, edge_type(e.source_id, e.target_id, e.value)); + } + emplace_edge(vertex_edges, e.target_id, edge_type(e.source_id, e.target_id, std::move(e.value))); + } +} +``` + +**Change in-edge construction only** (the out-edge construction within the `Sourced` branch stays the same): + +Use a temporary constructibility-based dispatch. In Phase 2, `Sourced` still exists, +so `edge_type` when `Sourced=true` takes `(source_id, target_id [, value])`. The new +`in_edge_type` (which defaults to `edge_type` for existing traits) must use the same +constructor. But we want to prepare for Phase 4 where `in_edge_type` will take only +`(source_id [, value])`. + +**Approach — helper function in `dynamic_graph_base`:** + +```cpp +private: + // Temporary bridge for in-edge construction (removed in Phase 4d). + // Existing edge_type takes (source_id, target_id [, value]). + // Future in_edge_type takes (source_id [, value]). + template + static constexpr auto make_in_edge(vertex_id_type source_id, vertex_id_type target_id) { + if constexpr (std::constructible_from) { + return InEdge(source_id, target_id); // legacy: (source, target) + } else { + return InEdge(source_id); // new: (source) only + } + } + + template + static constexpr auto make_in_edge(vertex_id_type source_id, vertex_id_type target_id, Val&& val) { + if constexpr (std::constructible_from) { + return InEdge(source_id, target_id, std::forward(val)); // legacy + } else { + return InEdge(source_id, std::forward(val)); // new + } + } +``` + +Then replace the 3 bidirectional in-edge construction sites: + +```cpp +// Before (example — void EV): +emplace_edge(rev, e.source_id, edge_type(e.source_id, e.target_id)); +// After: +emplace_edge(rev, e.source_id, make_in_edge(e.source_id, e.target_id)); + +// Before (example — non-void EV, copy for in-edge): +emplace_edge(rev, e.source_id, edge_type(e.source_id, e.target_id, e.value)); +// After: +emplace_edge(rev, e.source_id, make_in_edge(e.source_id, e.target_id, e.value)); +``` + +**Out-edge construction is unchanged.** The `make_in_edge` helper only affects in-edge emplacement. + +### Step 2.3 — Verify + +- Full build + test suite. +- **Expected:** All existing tests pass. `make_in_edge` dispatches to the legacy (source, target) constructor for all current traits because `in_edge_type == edge_type == dynamic_edge<..., Sourced=true, ...>`. + +### Step 2.4 — Commit + +Message: `feat: wire in_edge_type into bidir vertex base and load_edges (Phase 2)` + +### Phase 2 Gate (must pass) + +- Build: `cmake --build build/linux-gcc-debug` +- Tests: `ctest --test-dir build/linux-gcc-debug` +- Audit: `load_edges` reverse insertion uses `make_in_edge(...)` in all 3 insertion paths + +### Doc notes for Phase 2 +- The `make_in_edge` helper is a temporary bridge. It will be removed in Phase 4d when + `Sourced` is eliminated and all constructors are standardized. +- `in_edges()` return type is still `Traits::edges_type&` for all existing traits. + +--- + +## Phase 3 — Rename `dynamic_edge` → `dynamic_out_edge` (mechanical rename) + +**Goal:** The out-edge class has the correct name. `Sourced` still exists. All tests pass. + +### Step 3.1 — Rename the class in `dynamic_graph.hpp` + +This is a mechanical find-and-replace within `dynamic_graph.hpp`. The renaming targets: + +| What | Location (approx lines) | +|------|------------------------| +| Forward declaration | L91: `class dynamic_edge` → `class dynamic_out_edge` | +| `dynamic_edge_target::edge_type` alias | L175: `using edge_type = dynamic_edge<...>` → `dynamic_out_edge<...>` | +| `dynamic_edge_source::edge_type` alias | L239: same | +| `dynamic_edge_value::edge_type` alias | L322: same | +| Primary class declaration | L405: `class dynamic_edge` → `class dynamic_out_edge` | +| All 4 specialization declarations | L488, L534, L600: same | +| Doxygen comments | `@c dynamic_edge` → `@c dynamic_out_edge` throughout | +| `std::hash` specializations | L2196, L2210: `hash<...dynamic_edge<...>>` → `hash<...dynamic_out_edge<...>>` | + +**Pattern:** `\bdynamic_edge\b` → `dynamic_out_edge` (word-boundary match avoids +`dynamic_edge_target`, `dynamic_edge_source`, `dynamic_edge_value`). + +**Exclusions:** Do NOT rename `dynamic_edge_target`, `dynamic_edge_source`, `dynamic_edge_value` +— these are base classes that will be refactored in Phase 4. + +### Step 3.2 — Add deprecated alias + +After the last `dynamic_out_edge` specialization (after line ~627): + +```cpp +/// @deprecated Use dynamic_out_edge instead. +template +using dynamic_edge = dynamic_out_edge; +``` + +This keeps all 27 traits files and all test files compiling without changes yet. + +### Step 3.3 — Rename forward declarations in all 27 traits files + +Each traits file has a forward declaration block like: +```cpp +template +struct xxx_graph_traits; + +template +class dynamic_edge; +``` + +Change `class dynamic_edge` → `class dynamic_out_edge` in all 27 files. + +**Because the deprecated alias exists**, the `using edge_type = dynamic_edge<...>` in +each traits file still compiles. This will be updated in Phase 4c. + +### Step 3.4 — Update any direct `dynamic_edge` references in test files + +Search: `grep -rn "\bdynamic_edge\b" tests/` (excluding `dynamic_edge_target|source|value`). + +Most test files use `graph_test_types::...` aliases and never name `dynamic_edge` +directly. If any do, update them. The deprecated alias provides a safety net. + +### Step 3.5 — Verify + +- Full build + test suite. +- Run: `grep -rn "\bdynamic_edge\b" include/ tests/ --include="*.hpp" --include="*.cpp" | grep -v "dynamic_edge_target\|dynamic_edge_source\|dynamic_edge_value\|dynamic_edge ="` to audit remaining references (should only be the deprecated alias and traits' `using edge_type`). + +### Step 3.6 — Commit + +Message: `refactor: rename dynamic_edge → dynamic_out_edge with deprecated alias (Phase 3)` + +### Phase 3 Gate (must pass) + +- Build: `cmake --build build/linux-gcc-debug` +- Tests: `ctest --test-dir build/linux-gcc-debug` +- Audit: `grep -rn "\\bdynamic_edge\\b" include/ tests/ --include="*.hpp" --include="*.cpp"` only shows allowed transition references (deprecated alias + intended transitional uses) + +--- + +## Phase 4 — Remove `Sourced` template parameter + +**Goal:** Eliminate `bool Sourced` from every template parameter list. This is the highest-risk phase +— work sub-step by sub-step, compiling frequently. + +### Step 4a — Remove `Sourced` from edge base classes and `dynamic_out_edge` + +**4a.1 — `dynamic_edge_target`** (line ~169): +- Remove `bool Sourced` from template params (7→6 params). +- Update `edge_type` alias to use `dynamic_out_edge` (6 params). + +**4a.2 — `dynamic_edge_source`** (line ~234): +- Remove `bool Sourced` template parameter from the primary class. +- **Delete** the `Sourced=false` empty specialization (line ~298). The primary class now + unconditionally stores `source_id_`. +- Update `edge_type` alias to match new params. +- This class now serves as the base for `dynamic_in_edge` (replacing the temporary + `dynamic_in_edge_source` from Phase 1). + +**4a.3 — `dynamic_edge_value`** (line ~327): +- Remove `bool Sourced` from both specializations (primary and `EV=void`). + +**4a.4 — `dynamic_out_edge`** — collapse from 4 to 2 specializations: +- Remove `bool Sourced` from all template params. +- **Delete** the 2 `Sourced=true` specializations (lines ~421–466 and ~488–509) which took + `(source_id, target_id)` constructors. +- **Promote** the 2 `Sourced=false` specializations (lines ~534–575 and ~600–627) to become + the new primary template and `EV=void` specialization. Remove `Sourced` from their + parameter lists: + - `dynamic_out_edge` — `(target_id)`, `(target_id, val)`. + - `dynamic_out_edge` — `(target_id)`. +- **CRITICAL: Remove `dynamic_edge_source` from `dynamic_out_edge`'s base class list.** + After 4a.2 removes the empty `Sourced=false` specialization, `dynamic_edge_source` + unconditionally stores `source_id_`. Out-edges must NOT inherit this — they only need + `dynamic_edge_target`. Replace the inheritance chain: + - Before: `dynamic_out_edge : dynamic_edge_source<..., false, ...>, dynamic_edge_value<...>` + - After: `dynamic_out_edge : dynamic_edge_target<...>, dynamic_edge_value<...>` + (This mirrors how `dynamic_in_edge` inherits `dynamic_edge_source` + `dynamic_edge_value` + but not `dynamic_edge_target`.) + +**4a.5 — Update `dynamic_in_edge` to use repurposed `dynamic_edge_source`:** +- Change `dynamic_in_edge`'s base from `dynamic_in_edge_source` to the now-Sourced-free + `dynamic_edge_source`. +- Also update `dynamic_in_edge`'s `dynamic_edge_value` base class reference — remove + `Sourced` from its template arguments (it was passed as `Sourced=true` as a pass-through). +- **Delete** the temporary `dynamic_in_edge_source` class from Phase 1. + +**4a.6 — Update forward declarations** (line ~91): +- `class dynamic_out_edge` — remove `bool Sourced` (now 6 params). +- Remove `class dynamic_edge` forward declaration (or the deprecated alias will need updating). + +**4a.7 — Update deprecated alias:** +```cpp +// The deprecated alias can no longer accept Sourced — remove it or update to a 6-param version. +// Since external code is unlikely to use the 7-param form, REMOVE the alias entirely. +``` + +**Do NOT compile yet — vertex/graph classes still reference Sourced. Continue to 4b.** + +### Step 4b — Remove `Sourced` from vertex and graph classes + +**4b.1 — `dynamic_vertex_bidir_base`** (lines ~647, ~665): +- Remove `bool Sourced` from both specializations. +- **Delete** the `static_assert(Sourced, ...)` in the `Bidirectional=true` specialization. +- Update `edges_type` in the bidir specialization: it already uses the detection idiom + from Phase 2 — just remove `Sourced` from the param list. + +**4b.2 — `dynamic_vertex_base`** (line ~701): +- Remove `bool Sourced` from template params. +- Update base class reference to `dynamic_vertex_bidir_base<..., Bidirectional, Traits>` (6 params). +- Update the `edge_type` and `edges_type` aliases defined inside `dynamic_vertex_base` + (lines ~721–722) — these reference `dynamic_out_edge` and must have `Sourced` removed + from their template arguments. + +**4b.3 — `dynamic_vertex`** — both specializations (lines ~836, ~906): +- Remove `bool Sourced`. +- Update base class reference. + +**4b.4 — `dynamic_graph_base`** (line ~956): +- Remove `bool Sourced` from template params. +- Remove `static constexpr bool sourced = Sourced;`. +- Update all internal `edge_type`, `vertex_type` references to 6-param forms. +- **Audit `edge_allocator_type`** (line ~974): currently derived from `edges_type`. In + non-uniform bidirectional mode, `in_edges_type` may use a different element type + (`dynamic_in_edge` vs `dynamic_out_edge`). Add an `in_edge_allocator_type` alias + derived from `in_edges_type` for in-edge container allocation. If both types have + identical layout (same size/alignment), a single allocator suffices, but add a + `static_assert` verifying this. + +**4b.5 — `dynamic_graph`** — both specializations (lines ~1930, ~2088): +- Remove `bool Sourced` from template params. +- Update base class references. +- Update forward declaration defaults (line ~97): remove `bool Sourced = false`. + +**4b.6 — Update `edge_value` friend function** (lines ~1809–1830 in `dynamic_graph_base`): +- This direction-aware friend function dispatches differently for out-edges vs in-edges. +- Remove `Sourced` from template params in the function signature. +- Verify it works for both `dynamic_out_edge` and `dynamic_in_edge`: + - For out-edges: accesses value via `dynamic_edge_value` base (unchanged). + - For in-edges: same mechanism — `dynamic_in_edge` also inherits `dynamic_edge_value`. +- If the function uses `Sourced` in any `if constexpr` branch, replace with a + type-based check (e.g., `is_same_v`). + +**Do NOT compile yet — traits still pass Sourced. Continue to 4c.** + +### Step 4c — Remove `Sourced` from all 27 traits files + +For every file in `include/graph/container/traits/`: + +1. Remove `bool Sourced = false` from the template parameter list (6→5 params). +2. Remove `static constexpr bool sourced = Sourced;`. +3. Update `using edge_type = dynamic_out_edge;` (6 params, no `Sourced`). +4. Update `using vertex_type = dynamic_vertex;` (6 params). +5. Update `using graph_type = dynamic_graph;` (6 params). +6. Update the forward declarations at the top (remove `bool Sourced`). + +**Pattern is identical across all 27 files.** Script this. + +**Standard traits do NOT need `in_edge_type` or `in_edges_type`** — the detection-idiom +fallback in `dynamic_graph_base` (Phase 1) handles it. + +**Complete list of 27 traits files:** +``` +vov, vod, vol, vofl, vos, vous, vom, voum, +dov, dod, dol, dofl, dos, dous, +mov, mod, mol, mofl, mos, mous, mom, +uov, uod, uol, uofl, uos, uous +``` + +### Step 4d — Update `load_edges` and remove `make_in_edge` + +**4d.1 — Simplify out-edge construction.** Each of the 3 insertion paths currently has an +`if constexpr (Sourced)` guard. With `Sourced` removed: +- The `Sourced=true` branch (which passed `(source_id, target_id)` to out-edge constructors) + is deleted — `dynamic_out_edge` no longer accepts `source_id`. +- The `Sourced=false` branch (which passed `(target_id)` only) is **kept** and promoted to + the unconditional path — it was already correct for the new `dynamic_out_edge` API. + +**There are 18 `emplace_edge` calls across the 3 paths** (associative, sequential-forward, +fallback). Each path has both `Sourced=true` and `Sourced=false` branches. Audit all 18. + +Out-edge construction is now always: +```cpp +if constexpr (is_void_v) { + emplace_edge(vertex_edges, e.target_id, edge_type(e.target_id)); +} else { + emplace_edge(vertex_edges, e.target_id, edge_type(e.target_id, std::move(e.value))); +} +``` + +**4d.2 — Simplify in-edge construction.** Replace `make_in_edge` with direct construction: +```cpp +if constexpr (Bidirectional) { + auto& rev = vertices_[...].in_edges(); + if constexpr (is_void_v) { + emplace_edge(rev, e.source_id, in_edge_type(e.source_id)); + } else { + emplace_edge(rev, e.source_id, in_edge_type(e.source_id, e.value)); // copy for in-edge + } +} +``` + +**4d.3 — Delete `make_in_edge` helper** (added in Phase 2). + +**Repeat for all 3 paths:** associative, sequential-forward, fallback. + +### Step 4e — Update `dynamic_adjacency_graph` alias + +**Current** (line ~139): +```cpp +template +using dynamic_adjacency_graph = dynamic_graph<..., Traits::sourced, Traits::bidirectional, Traits>; +``` + +**Change to:** +```cpp +template +using dynamic_adjacency_graph = dynamic_graph<..., Traits::bidirectional, Traits>; +``` +Remove `Traits::sourced`. + +### Step 4f — Update `std::hash` specializations + +- Update `hash>` — remove `Sourced` from template params. + Only one specialization remains (hashes `target_id()` only). The old `Sourced=true` + specialization is deleted (it hashed `source_id() ^ target_id()`). +- `hash>` already has no `Sourced` parameter (added in Phase 1). + +### Step 4g — Compile (library code only — exclude tests) + +There is no library-only CMake target, so use target-level builds to verify headers compile: +```bash +# Build only the example targets (they exercise the full header but not the test code): +cmake --build build/linux-gcc-debug --target basic_usage dijkstra_clrs_example mst_usage_example +``` + +If examples also reference `Sourced`, create a minimal `phase4_smoke.cpp` translation unit +that includes `` and instantiates one `dynamic_graph` +with default traits. Build that single target to verify header correctness. + +**Expected:** Compilation succeeds for library header consumers. Tests will NOT compile yet +(they still reference `Sourced`). That's Phase 5. + +### Step 4h — Commit + +Message: `refactor: remove Sourced template parameter from entire class hierarchy (Phase 4)` + +### Phase 4 Gate (explicit exception) + +- Build: example targets (or Phase 4g smoke-test TU) must compile +- Tests: may fail due to expected test API drift — this is expected and resolved in Phase 5 +- Audit: `grep -rn "Sourced" include/ --include="*.hpp" --include="*.cpp"` returns zero code references + +### Doc notes for Phase 4 +- `dynamic_edge_source` is now the unconditional source-id base (no empty specialization). +- `dynamic_out_edge` has 2 specializations (keyed on `EV`), not 4. +- `dynamic_out_edge` no longer inherits `dynamic_edge_source` — only `dynamic_edge_target` + `dynamic_edge_value`. +- `dynamic_in_edge` inherits `dynamic_edge_source` + `dynamic_edge_value` (no `dynamic_edge_target`). +- `make_in_edge` bridge from Phase 2 is removed. +- Deprecated `dynamic_edge` alias is removed. +- All traits are now 5-param: ``. +- `edge_allocator_type` verified compatible with both `edges_type` and `in_edges_type`. + +--- + +## Phase 5 — Update tests + +**Goal:** All test files compile and pass with the new API. + +### Step 5.1 — Update `graph_test_types.hpp` + +**5.1.1 — Update tag `traits` template aliases** (all ~27 tags): + +Current pattern: +```cpp +struct vov_tag { + static constexpr const char* name = "vov"; + template + using traits = graph::container::vov_graph_traits; +}; +``` + +Change to (replace `Sourced` with `Bidirectional`): +```cpp +struct vov_tag { + static constexpr const char* name = "vov"; + template + using traits = graph::container::vov_graph_traits; +}; +``` + +**Note:** `Sourced` occupied position 5 in the old signature; `Bidirectional` takes the +same position. Callers passing `true`/`false` for `Sourced` must be audited — the boolean +now means `Bidirectional`, which has different semantics. The `sourced_*` aliases in 5.1.2 +below are the primary callers and are deleted. Any other callers passing a 5th argument +must be updated to pass the correct `Bidirectional` value. + +**5.1.2 — Replace `sourced_*` aliases with `bidir_*` aliases:** + +Remove: +```cpp +using sourced_void = dynamic_graph>; +using sourced_int = dynamic_graph>; +using sourced_all = dynamic_graph>; +``` + +Add: +```cpp +// Uniform bidirectional: both containers use dynamic_out_edge (edge_type) +using bidir_void = dynamic_graph>; +using bidir_int = dynamic_graph>; +using bidir_all = dynamic_graph>; +``` + +**5.1.3 — Update non-sourced aliases** (remove `Sourced` parameter): + +Current: `dynamic_graph>` +Change: `dynamic_graph>` + +(Remove the `Sourced` argument; `Bidirectional` takes its position.) + +### Step 5.2 — Update individual container test files (~48 files) + +For each test file in `tests/container/dynamic_graph/`: + +1. **Remove `xxx_sourced` type aliases** (e.g., `using vov_sourced_void = ...`, etc.). +2. **Remove tests that assert `G::sourced == true/false`** (search for `sourced`). +3. **Update remaining type aliases** — remove `Sourced` from any explicit `dynamic_graph<...>` instantiations. +4. **Keep all non-sourced test logic unchanged** — it should compile with the updated `graph_test_types.hpp`. + +**Special attention: `test_dynamic_edge_comparison.cpp`** (~450 lines): +- This file directly instantiates all 4 `dynamic_edge` specializations with explicit + `Sourced=true` and `Sourced=false` template arguments. +- **Rename** `dynamic_edge` → `dynamic_out_edge` in all type aliases. +- **Remove** the `Sourced` template argument from all instantiations. +- **Delete** tests for `Sourced=true` specializations (they test `source_id()` on out-edges, + which no longer exists). +- **Add** `dynamic_in_edge` test cases for `operator<=>`, `operator==`, and `std::hash` + that mirror the deleted `Sourced=true` tests but use `source_id()` on in-edges. + +**Approach:** For each file: +```bash +grep -n "Sourced\|sourced" +``` +If no matches, the file needs no changes beyond what `graph_test_types.hpp` provides. + +**Estimated impact per file:** +- Type alias declaration changes: ~2–6 lines +- Sourced-specific test section deletions: ~20–40 lines per file (where present) +- Logic is unchanged for non-sourced tests + +### Step 5.3 — Update bidirectional container-native tests + +**File:** `tests/container/dynamic_graph/test_dynamic_graph_bidirectional.cpp` + +**5.3.1 — Update type aliases** (lines 33–68): + +Current: +```cpp +using bidir_vov_int = dynamic_graph>; +``` + +Change (remove `Sourced`): +```cpp +using bidir_vov_int = dynamic_graph>; +``` + +Apply to all 7 type aliases in this file. + +**5.3.2 — Add non-uniform bidirectional type aliases and tests:** + +Create a new non-uniform traits struct (either in this file or in a shared header): +```cpp +// Non-uniform traits: out-edges use dynamic_out_edge, in-edges use dynamic_in_edge +template +struct vov_nonuniform_graph_traits { + static constexpr bool bidirectional = true; + using edge_type = dynamic_out_edge; + using in_edge_type = dynamic_in_edge; + using vertex_type = dynamic_vertex; + using graph_type = dynamic_graph; + using edges_type = std::vector; + using in_edges_type = std::vector; + using vertices_type = std::vector; +}; +``` + +Add non-uniform type aliases and parallel tests: +- `using bidir_nu_vov_int = dynamic_graph<..., vov_nonuniform_graph_traits>;` +- `using bidir_nu_vov_void = dynamic_graph<..., vov_nonuniform_graph_traits>;` + +Add test sections that verify uniform and non-uniform produce identical CPO results. + +### Step 5.4 — Update CPO bidirectional tests + +**5.4.1 — `tests/adj_list/cpo/test_in_edges_cpo.cpp`:** +- Add `dynamic_graph` sections (uniform + non-uniform) alongside existing stub tests. + +**5.4.2 — `tests/adj_list/cpo/test_source_id_cpo.cpp`:** +- Add sections verifying `source_id(g, uv)` on out-edges (Tier 4) and in-edges (Tier 1 non-uniform, Tier 4 uniform). + +**5.4.3 — `tests/adj_list/cpo/test_find_in_edge_cpo.cpp`, `test_contains_in_edge_cpo.cpp`:** +- Add `dynamic_graph` sections. + +**5.4.4 — New file: `tests/adj_list/cpo/test_bidir_cpo_consistency.cpp`:** +- Build the same graph with uniform and non-uniform traits. +- Verify `source_id`, `target_id`, `edge_value`, `in_edges`, `in_degree` produce identical results. +- Add to `tests/adj_list/cpo/CMakeLists.txt`. + +**5.4.5 — Centralize non-uniform test traits:** +- Put reusable non-uniform traits in common test infra (`tests/common/graph_test_types.hpp` or + `tests/common/graph_fixtures.hpp`) instead of file-local duplication. +- Reuse the same type in container-native tests, CPO tests, and concept tests. + +### Step 5.5 — Update concept tests + +**File:** `tests/adj_list/concepts/test_bidirectional_concepts.cpp` + +- Add `STATIC_REQUIRE` checks for both uniform and non-uniform `dynamic_graph` types. +- Verify non-bidirectional `dynamic_graph` does NOT satisfy `bidirectional_adjacency_list`. + +### Step 5.6 — Update algorithm test types + +**File:** `tests/common/algorithm_test_types.hpp` + +Current file does NOT reference `Sourced` — but its types use `graph_test_types::int_ev` +which internal type alias will have changed (removed `Sourced` param). Verify it still +compiles. Likely no changes needed. + +### Step 5.7 — Update examples and benchmarks + +- Search for `Sourced` or `dynamic_edge` in `examples/` and `benchmark/`. +- Update any explicit type instantiations. +- Most likely minimal — they tend to use defaults (`Sourced=false`). + +### Step 5.8 — Verify + +```bash +cmake --build build/linux-gcc-debug +ctest --test-dir build/linux-gcc-debug +``` + +**Post-verification audit:** +```bash +grep -rn "Sourced" include/ tests/ --include="*.hpp" --include="*.cpp" +grep -rn "dynamic_edge[^_]" include/ tests/ --include="*.hpp" --include="*.cpp" +``` + +Both should return zero matches (excluding comments/documentation). + +### Step 5.9 — Commit + +Message: `test: update all tests for Sourced removal and add bidir uniform/non-uniform coverage (Phase 5)` + +### Phase 5 Gate (must pass, full green restoration) + +- Build: `cmake --build build/linux-gcc-debug` +- Tests: `ctest --test-dir build/linux-gcc-debug` (no failures) +- Audit: zero `Sourced` code references under `include/` and `tests/` + +### Doc notes for Phase 5 +- `graph_test_types.hpp` tag templates now take `Bidirectional` instead of `Sourced`. +- `sourced_void/int/all` replaced with `bidir_void/int/all` (uniform mode). +- Non-uniform bidirectional tests use a custom `vov_nonuniform_graph_traits` struct. +- CPO consistency test validates that uniform and non-uniform produce identical results. + +--- + +## Phase 6 — Cleanup and documentation + +**Goal:** Remove temporary artifacts, update documentation, verify clean state. + +### Step 6.1 — Remove deprecated alias (if not already removed in Phase 4) + +Verify no `dynamic_edge` alias remains in `dynamic_graph.hpp`. + +### Step 6.2 — Update Doxygen comments + +Audit `dynamic_graph.hpp` for references to: +- `Sourced` parameter (in comments) +- `dynamic_edge` class name (in `@brief`, `@c`, etc.) +- Outdated constructor documentation + +Update all to reference `dynamic_out_edge` / `dynamic_in_edge`. + +### Step 6.3 — Update user-facing documentation + +- `docs/getting-started.md` — if it references edge types +- `docs/reference/` — edge type documentation +- `docs/migration-from-v2.md` — add migration note for `Sourced` removal +- `README.md` — if it references edge construction + +### Step 6.4 — Archive strategy documents + +- Move `agents/incoming_edges_design.md` to `agents/archive/` if superseded. +- Move `agents/dynamic_edge_refactor_strategy.md` to `agents/archive/` after implementation. + +### Step 6.5 — Final verification + +```bash +# Full build on all presets +cmake --build build/linux-gcc-debug && ctest --test-dir build/linux-gcc-debug +cmake --build build/linux-clang-debug && ctest --test-dir build/linux-clang-debug + +# Audit for stale references +grep -rn "Sourced" include/ tests/ --include="*.hpp" --include="*.cpp" +grep -rn "\bdynamic_edge\b" include/ tests/ --include="*.hpp" --include="*.cpp" +grep -rn "dynamic_in_edge_source" include/ --include="*.hpp" # temp class should be gone + +# Memory validation +# (Add a static_assert in a test: sizeof(dynamic_out_edge) == sizeof(dynamic_in_edge)) +``` + +### Step 6.6 — Commit + +Message: `docs: cleanup Doxygen, update user docs, archive strategy (Phase 6)` + +### Documentation Deliverables Checklist + +- API signatures updated in docs for `dynamic_graph` and traits (no `Sourced` parameter). +- Migration note: map old `dynamic_edge<..., Sourced=...>` usage to `dynamic_out_edge` / `dynamic_in_edge`. +- Clarify uniform bidirectional semantics: in-edge container stores `dynamic_out_edge` where raw `target_id_` encodes source. +- Clarify CPO dispatch expectations for `source_id` (Tier 4 for out-edges; Tier 1 or Tier 4 for in-edges depending on mode). +- Update examples showing both default uniform mode and custom non-uniform traits. + +### Phase 6 Gate (must pass) + +- Build: `cmake --build build/linux-gcc-debug` and `cmake --build build/linux-clang-debug` +- Tests: `ctest --test-dir build/linux-gcc-debug` and `ctest --test-dir build/linux-clang-debug` +- Audit: no transitional symbols (`dynamic_in_edge_source`, deprecated aliases) + +--- + +## Risk Mitigations Summary + +| Risk | Mitigation | Phase | +|------|-----------|-------| +| Breaking existing non-bidir tests | Deprecated alias in Phase 3; test updates deferred to Phase 5 | 3, 5 | +| `source_id` CPO breaks for out-edges | Out-edge `source_id` always resolves via descriptor Tier 4 — no code change needed | 4 | +| 27 traits files: mechanical errors | Script the changes; identical pattern in all files | 4c | +| `load_edges` constructor mismatch during transition | `make_in_edge` bridge (Phase 2) removed in Phase 4d | 2, 4d | +| Detection-idiom fallback incorrect | Compile-time `static_assert` in Phase 1.4 | 1 | +| Non-uniform traits missing `in_edges_type` | `static_assert` catches inconsistent traits | 1 | +| Phase 4 is large and risky | Sub-steps 4a–4g; compile at 4g before touching tests | 4 | +| No library-only CMake target | Phase 4g uses example targets or a smoke-test TU as proxy for header-only compilation check | 4g | +| Two bidir modes increase test matrix | Non-uniform tests limited to vov/vol representatives | 5 | + +--- + +## Commit Sequence + +| # | Phase | Commit message | Risk | +|---|-------|---------------|------| +| 1 | 1 | `feat: add dynamic_in_edge class and detection-idiom aliases` | Low (additive only) | +| 2 | 2 | `feat: wire in_edge_type into bidir vertex base and load_edges` | Low (behavioral no-op) | +| 3 | 3 | `refactor: rename dynamic_edge → dynamic_out_edge with deprecated alias` | Low (mechanical + alias safety net) | +| 4 | 4 | `refactor: remove Sourced template parameter from entire class hierarchy` | **High** (breaks tests until Phase 5) | +| 5 | 5 | `test: update all tests for Sourced removal and add bidir coverage` | Medium (many files) | +| 6 | 6 | `docs: cleanup Doxygen, update user docs, archive strategy` | Low (non-functional) | + +> **Note:** Phases 4 and 5 can be combined into one commit if desired — this keeps the +> repo green at every commit. The trade-off is a larger commit. + +--- + +## Status Tracker + +| Phase | Description | Status | Notes | +|-------|-------------|--------|-------| +| 1 | Add `dynamic_in_edge` + detection aliases | COMPLETE | | +| 2 | Wire `in_edge_type` into bidir support | COMPLETE | | +| 3 | Rename `dynamic_edge` → `dynamic_out_edge` | COMPLETE | f560d86 | +| 4a | Remove `Sourced` from edge classes | COMPLETE | 89e1018 | +| 4b | Remove `Sourced` from vertex/graph classes | COMPLETE | 08e6ef6 | +| 4c | Remove `Sourced` from 27 traits files | COMPLETE | 67a0e1f | +| 4d | Update `load_edges`, remove `make_in_edge` | COMPLETE | 7b06988 | +| 4e | Update `dynamic_adjacency_graph` alias | COMPLETE | done in 4b (08e6ef6) | +| 4f | Update `std::hash` specializations | COMPLETE | done in 4a (89e1018) | +| 4g | Compile non-test code | COMPLETE | 01a980a | +| 5.1 | Update `graph_test_types.hpp` | COMPLETE | 9fd9c1f; tags 6-param; sourced_* → bidir_* | +| 5.2 | Update ~48 container test files | COMPLETE | bulk script + manual CPO fixes; `Types::all` → `all_int` | +| 5.3 | Update bidir container-native tests | COMPLETE | non-uniform traits added to bidir/scc/transpose/reverse_traversal | +| 5.4 | Update/add CPO bidir tests | COMPLETE | 5.4.1–5.4.5; 20 new tests; 4329/4329 pass (2026-02-23) | +| 5.5 | Update concept tests | COMPLETE | 8 STATIC_REQUIREs for dynamic_graph; 4337/4337 pass (2026-02-23) | +| 5.6 | Update algorithm test types | COMPLETE | no changes needed; file does not reference `Sourced` | +| 5.7 | Update examples/benchmarks | COMPLETE | no changes needed in examples/; transpose + reverse_traversal fixed | +| 5.8 | Full test suite verification | COMPLETE | 4337/4337 pass (2026-02-23) | +| 5.9 | Commit Phase 5 | COMPLETE | 880ae00 | +| 6.1 | Remove deprecated `dynamic_edge` alias | COMPLETE | alias absent; stale comment in edge_descriptor.hpp fixed | +| 6.2 | Update Doxygen comments in dynamic_graph.hpp | COMPLETE | 4 stale Sourced/dynamic_edge refs fixed | +| 6.3 | Update user-facing documentation | COMPLETE | migration-from-v2.md: table row, code example, trailing sentence | +| 6.4 | Archive strategy documents | COMPLETE | incoming_edges_design/plan + dynamic_edge_refactor_strategy → archive/ | +| 6.5 | Final verification | COMPLETE | 4337/4337 pass; Sourced/dynamic_edge audit clean | +| 6.6 | Commit Phase 6 | COMPLETE | 42e6414 | +| 6 | Cleanup + documentation | COMPLETE | | diff --git a/agents/incoming_edges_plan.md b/agents/incoming_edges_plan.md deleted file mode 100644 index a3b4b1c..0000000 --- a/agents/incoming_edges_plan.md +++ /dev/null @@ -1,1094 +0,0 @@ -# Incoming Edges — Phased Implementation Plan - -**Source:** `agents/incoming_edges_design.md` -**Branch:** `incoming` -**Build preset:** `linux-gcc-debug` (full test suite) - ---- - -## How to use this plan - -Each phase is a self-contained unit of work that can be completed by an agent in -one session. Phases are strictly ordered — each depends on the prior phase compiling -and passing all tests. Within each phase, steps are listed in execution order. - -**Conventions used throughout:** - -- "Mirror X" means copy the implementation pattern from X, replacing outgoing - names/semantics with incoming equivalents. -- "Full test suite passes" means `cmake --build build/linux-gcc-debug -j$(nproc)` - succeeds and `cd build/linux-gcc-debug && ctest --output-on-failure` reports - zero failures with no regressions. -- File paths are relative to the repository root. - ---- - -## Phase 1 — `in_edges` and `in_degree` CPOs + type aliases - -**Goal:** Add the two core incoming-edge CPOs, their public instances, the -outgoing aliases (`out_edges`, `out_degree`, `find_out_edge`), the six new type -aliases, and a CPO unit-test file proving all resolution tiers work. - -**Why first:** Every subsequent phase depends on `in_edges` and `in_edge_t` -existing. This phase touches exactly one production header and adds one test -file, so it is low-risk and easy to validate. - -### Files to modify - -| File | Action | -|---|---| -| `include/graph/adj_list/detail/graph_cpo.hpp` | Add CPOs, aliases, public instances | -| `tests/adj_list/CMakeLists.txt` | Register new test file | - -### Files to create - -| File | Content | -|---|---| -| `tests/adj_list/cpo/test_in_edges_cpo.cpp` | Unit tests | - -### Steps - -#### 1.1 Add `in_edges` CPO (graph_cpo.hpp) - -Insert a new section **after** the `edges` public instance and type aliases -(after line ~835, before the `_target` namespace). Follow the exact structural -pattern of `namespace _edges`: - -``` -namespace _cpo_impls { - namespace _in_edges { - // --- (g, u) overload --- - enum class _St_u { _none, _vertex_member, _adl }; - - template - concept _has_vertex_member_u = is_vertex_descriptor_v<...> && - requires(G& g, const U& u) { - { u.inner_value(g).in_edges() } -> std::ranges::forward_range; - }; - - template - concept _has_adl_u = requires(G& g, const U& u) { - { in_edges(g, u) } -> std::ranges::forward_range; - }; - - // NOTE: No _edge_value_pattern tier — in_edges has no implicit default. - // A graph MUST explicitly provide in_edges() via member or ADL. - - template - [[nodiscard]] consteval _Choice_t<_St_u> _Choose_u() noexcept { ... } - - // --- (g, uid) overload --- - enum class _St_uid { _none, _adl, _default }; - // _has_adl_uid, _has_default_uid (find_vertex + in_edges(g, u)) - template - [[nodiscard]] consteval _Choice_t<_St_uid> _Choose_uid() noexcept { ... } - - class _fn { - // operator()(G&& g, const U& u) — vertex descriptor - // operator()(G&& g, const VId& uid) — vertex ID - // Both use _wrap_if_needed() to ensure edge_descriptor_view return - }; - } -} -``` - -Key differences from `edges`: -- Only 2 tiers for `(g, u)`: `_vertex_member` and `_adl`. No `_edge_value_pattern`. -- The `(g, uid)` default delegates to `in_edges(g, *find_vertex(g, uid))`. -- The `_has_default_uid` concept checks `_has_adl_u` (not `_has_edge_value_pattern`). - -#### 1.2 Add `in_edges` public instance and type aliases - -Immediately after the `_in_edges` namespace closing brace: - -```cpp -inline namespace _cpo_instances { - inline constexpr _cpo_impls::_in_edges::_fn in_edges{}; -} - -template -using in_vertex_edge_range_t = decltype(in_edges(std::declval(), std::declval>())); - -template -using in_vertex_edge_iterator_t = std::ranges::iterator_t>; - -template -using in_edge_t = std::ranges::range_value_t>; -``` - -#### 1.3 Add `in_degree` CPO - -Insert after the `in_edges` section, mirroring `namespace _degree`: - -``` -namespace _cpo_impls { - namespace _in_degree { - // (g, u): _member (g.in_degree(u)), _adl, _default (size(in_edges(g,u))) - // (g, uid): _member, _adl, _default (*find_vertex then in_degree(g,u)) - class _fn { ... }; - } -} - -inline namespace _cpo_instances { - inline constexpr _cpo_impls::_in_degree::_fn in_degree{}; -} -``` - -Key difference from `degree`: the `_default` tier calls `in_edges(g, u)` instead -of `edges(g, u)`. - -#### 1.4 Add outgoing aliases - -After the `in_degree` public instance: - -```cpp -inline namespace _cpo_instances { - inline constexpr auto& out_edges = edges; - inline constexpr auto& out_degree = degree; - inline constexpr auto& find_out_edge = find_vertex_edge; -} -``` - -#### 1.5 Add outgoing type aliases - -```cpp -template -using out_vertex_edge_range_t = vertex_edge_range_t; - -template -using out_vertex_edge_iterator_t = vertex_edge_iterator_t; - -template -using out_edge_t = edge_t; -``` - -#### 1.6 Create test file `tests/adj_list/cpo/test_in_edges_cpo.cpp` - -Test the following scenarios (mirror `test_edges_cpo.cpp` structure): - -1. **Stub graph with `in_edges()` member** — a minimal struct whose vertex - `inner_value(g).in_edges()` returns a `vector`. Verify the CPO - dispatches to `_vertex_member` tier and the return is an - `edge_descriptor_view`. - -2. **Stub graph with ADL `in_edges(g, u)` friend** — verify `_adl` tier. - -3. **`(g, uid)` overload with default** — verify `_default` tier delegates - through `find_vertex` + `in_edges(g, u)`. - -4. **Type alias verification** — `in_vertex_edge_range_t`, - `in_vertex_edge_iterator_t`, `in_edge_t` all compile and produce - expected types. - -5. **Mixed-type test** — a stub graph where `edge_t` is - `pair` but `in_edge_t` is just `int`. Verify both - aliases are independently deduced. - -6. **`out_edges` / `out_degree` / `find_out_edge` aliases** — verify they - are the exact same object as `edges` / `degree` / `find_vertex_edge` - (use `static_assert(&out_edges == &edges)`). - -7. **`in_degree` CPO** — test member, ADL, and default (`size(in_edges)`) - resolution tiers. - -8. **Outgoing type aliases** — verify `out_edge_t` is `edge_t` etc. - -#### 1.7 Register test in CMakeLists - -Add to `tests/adj_list/CMakeLists.txt` under the CPOs section: - -```cmake - cpo/test_in_edges_cpo.cpp -``` - -#### 1.8 Build and run full test suite - -```bash -cmake --build build/linux-gcc-debug -j$(nproc) -cd build/linux-gcc-debug && ctest --output-on-failure -``` - -### Merge gate - -- [ ] Full test suite passes with zero regressions. -- [ ] All 8 test scenarios pass. -- [ ] `in_edge_t != edge_t` mixed-type test passes. -- [ ] `out_edges` alias identity check passes. - ---- - -## Phase 2 — `find_in_edge` and `contains_in_edge` CPOs + traits - -**Goal:** Add the remaining incoming-edge CPOs (`find_in_edge`, -`contains_in_edge`), the new traits (`has_in_degree`, `has_find_in_edge`, -`has_contains_in_edge`), and their unit tests. - -**Why second:** These CPOs depend on `in_edges` (Phase 1). They complete the -full incoming-edge CPO surface before concepts are defined. - -### Files to modify - -| File | Action | -|---|---| -| `include/graph/adj_list/detail/graph_cpo.hpp` | Add `find_in_edge`, `contains_in_edge` CPOs | -| `include/graph/adj_list/adjacency_list_traits.hpp` | Add 3 new traits | -| `tests/adj_list/CMakeLists.txt` | Register new test files | - -### Files to create - -| File | Content | -|---|---| -| `tests/adj_list/cpo/test_find_in_edge_cpo.cpp` | Unit tests for `find_in_edge` | -| `tests/adj_list/cpo/test_contains_in_edge_cpo.cpp` | Unit tests for `contains_in_edge` | -| `tests/adj_list/traits/test_incoming_edge_traits.cpp` | Trait tests | - -### Steps - -#### 2.1 Add `find_in_edge` CPO (graph_cpo.hpp) - -Mirror `namespace _find_vertex_edge` with three overload groups: - -1. `find_in_edge(g, u, v)` — both descriptors. Default iterates - `in_edges(g, u)` and matches `source_id(g, ie)` against `vertex_id(g, v)`. -2. `find_in_edge(g, u, vid)` — descriptor + ID. Default iterates - `in_edges(g, u)` and matches `source_id(g, ie)` against `vid`. -3. `find_in_edge(g, uid, vid)` — both IDs. Default delegates via - `*find_vertex(g, uid)`. - -Key difference from `find_vertex_edge`: iterates `in_edges` and compares -`source_id` instead of `target_id`. - -```cpp -inline namespace _cpo_instances { - inline constexpr _cpo_impls::_find_in_edge::_fn find_in_edge{}; -} -``` - -#### 2.2 Add `contains_in_edge` CPO (graph_cpo.hpp) - -Mirror `namespace _contains_edge` with two overload groups: - -1. `contains_in_edge(g, u, v)` — both descriptors. -2. `contains_in_edge(g, uid, vid)` — both IDs. - -Default implementations iterate `in_edges(g, u)` and match `source_id`. - -```cpp -inline namespace _cpo_instances { - inline constexpr _cpo_impls::_contains_in_edge::_fn contains_in_edge{}; -} -``` - -#### 2.3 Add traits (adjacency_list_traits.hpp) - -Follow the existing `detail::has_X_impl` → `has_X` → `has_X_v` pattern: - -```cpp -// --- has_in_degree --- -namespace detail { - template - concept has_in_degree_impl = requires(G& g, vertex_t u, vertex_id_t uid) { - { in_degree(g, u) } -> std::integral; - { in_degree(g, uid) } -> std::integral; - }; -} -template concept has_in_degree = detail::has_in_degree_impl; -template inline constexpr bool has_in_degree_v = has_in_degree; - -// --- has_find_in_edge --- -namespace detail { - template - concept has_find_in_edge_impl = requires(G& g, vertex_t u, vertex_t v, - vertex_id_t uid, vertex_id_t vid) { - find_in_edge(g, u, v); - find_in_edge(g, u, vid); - find_in_edge(g, uid, vid); - }; -} -template concept has_find_in_edge = detail::has_find_in_edge_impl; -template inline constexpr bool has_find_in_edge_v = has_find_in_edge; - -// --- has_contains_in_edge --- -namespace detail { - template - concept has_contains_in_edge_impl = requires(G& g, vertex_t u, vertex_t v, - vertex_id_t uid, vertex_id_t vid) { - { contains_in_edge(g, u, v) } -> std::convertible_to; - { contains_in_edge(g, uid, vid) } -> std::convertible_to; - }; -} -template concept has_contains_in_edge = detail::has_contains_in_edge_impl; -template inline constexpr bool has_contains_in_edge_v = has_contains_in_edge; -``` - -#### 2.4 Create test files - -**`test_find_in_edge_cpo.cpp`** — test all 3 overloads: -- Stub graph with ADL `in_edges()` friend returning known edges. -- Verify `find_in_edge(g, u, v)` finds correct incoming edge by `source_id`. -- Verify `find_in_edge(g, u, vid)` works. -- Verify `find_in_edge(g, uid, vid)` delegates through `find_vertex`. -- Verify not-found case. - -**`test_contains_in_edge_cpo.cpp`** — test both overloads: -- Verify `contains_in_edge(g, u, v)` returns true/false correctly. -- Verify `contains_in_edge(g, uid, vid)` returns true/false correctly. - -**`test_incoming_edge_traits.cpp`** — test all 3 traits: -- Stub graph that models `in_edges`/`in_degree` → `has_in_degree_v` is true. -- Plain `vector>` → `has_in_degree_v` is false. -- Stub graph with `in_edges` + `find_in_edge` → `has_find_in_edge_v` is true. -- Stub graph with `in_edges` + `contains_in_edge` → `has_contains_in_edge_v` is true. - -#### 2.5 Register tests in CMakeLists - -Add to `tests/adj_list/CMakeLists.txt`: - -```cmake - cpo/test_find_in_edge_cpo.cpp - cpo/test_contains_in_edge_cpo.cpp - traits/test_incoming_edge_traits.cpp -``` - -#### 2.6 Build and run full test suite - -### Merge gate - -- [ ] Full test suite passes. -- [ ] All `find_in_edge` overloads work (member, ADL, default). -- [ ] All `contains_in_edge` overloads work. -- [ ] Traits correctly detect presence/absence of incoming-edge CPOs. - ---- - -## Phase 3 — Concepts + namespace re-exports - -**Goal:** Define `in_vertex_edge_range`, `bidirectional_adjacency_list`, -`index_bidirectional_adjacency_list` concepts; update `graph.hpp` with all -re-exports for Phases 1-3. - -**Why third:** Concepts depend on the CPOs and type aliases from Phases 1-2. -Re-exports should be done once after the complete CPO surface exists. - -### Files to modify - -| File | Action | -|---|---| -| `include/graph/adj_list/adjacency_list_concepts.hpp` | Add 3 concepts | -| `include/graph/graph.hpp` | Add re-exports for all new CPOs, aliases, traits, concepts | -| `tests/adj_list/CMakeLists.txt` | Register new test file | - -### Files to create - -| File | Content | -|---|---| -| `tests/adj_list/concepts/test_bidirectional_concepts.cpp` | Concept tests | - -### Steps - -#### 3.1 Add concepts (adjacency_list_concepts.hpp) - -After the `index_adjacency_list` concept: - -```cpp -template -concept in_vertex_edge_range = std::ranges::forward_range; - -template -concept bidirectional_adjacency_list = - adjacency_list && - requires(G& g, vertex_t u, in_edge_t ie) { - { in_edges(g, u) } -> in_vertex_edge_range; - { source_id(g, ie) } -> std::convertible_to>; - }; - -template -concept index_bidirectional_adjacency_list = - bidirectional_adjacency_list && index_vertex_range; -``` - -#### 3.2 Update graph.hpp re-exports - -In the `namespace graph { ... }` using-declaration block, add: - -```cpp -// Incoming-edge CPOs -using adj_list::in_edges; -using adj_list::in_degree; -using adj_list::find_in_edge; -using adj_list::contains_in_edge; - -// Outgoing aliases -using adj_list::out_edges; -using adj_list::out_degree; -using adj_list::find_out_edge; - -// Incoming-edge type aliases -using adj_list::in_vertex_edge_range_t; -using adj_list::in_vertex_edge_iterator_t; -using adj_list::in_edge_t; - -// Outgoing type aliases -using adj_list::out_vertex_edge_range_t; -using adj_list::out_vertex_edge_iterator_t; -using adj_list::out_edge_t; - -// Incoming-edge concepts -using adj_list::in_vertex_edge_range; -using adj_list::bidirectional_adjacency_list; -using adj_list::index_bidirectional_adjacency_list; - -// Incoming-edge traits -using adj_list::has_in_degree; -using adj_list::has_in_degree_v; -using adj_list::has_find_in_edge; -using adj_list::has_find_in_edge_v; -using adj_list::has_contains_in_edge; -using adj_list::has_contains_in_edge_v; -``` - -#### 3.3 Create test file `tests/adj_list/concepts/test_bidirectional_concepts.cpp` - -1. **Stub bidirectional graph** — a struct with both `edges()` and `in_edges()` - ADL friends. `static_assert(bidirectional_adjacency_list)`. - -2. **Outgoing-only graph** — a `vector>`. - `static_assert(!bidirectional_adjacency_list)`. - -3. **Index variant** — stub graph with random-access vertices. - `static_assert(index_bidirectional_adjacency_list)`. - -4. **Mixed-type concept** — stub where `in_edge_t` is just `int` - (lightweight back-pointer) but `source_id(g, ie)` works. - `static_assert(bidirectional_adjacency_list)`. - -5. **Re-export test** — verify `graph::bidirectional_adjacency_list` is - accessible (compile-time only). - -#### 3.4 Register test and build - -### Merge gate - -- [ ] Full test suite passes. -- [ ] `bidirectional_adjacency_list` satisfied by stub bidirectional graph. -- [ ] `bidirectional_adjacency_list` not satisfied by `vector>`. -- [ ] Mixed-type (`in_edge_t != edge_t`) graph satisfies concept. -- [ ] All re-exports compile via `#include `. - ---- - -## Phase 4 — `undirected_adjacency_list` incoming-edge support - -**Goal:** Add `in_edges()` and `in_degree()` ADL friends to -`undirected_adjacency_list` that return the same ranges as `edges()` and -`degree()`, making undirected graphs model `bidirectional_adjacency_list` -at zero cost. - -**Why fourth:** This is the simplest container change (just forwarding to -existing functions) and provides a real container for integration testing. - -### Files to modify - -| File | Action | -|---|---| -| `include/graph/container/undirected_adjacency_list.hpp` | Add `in_edges`, `in_degree` friends | -| `tests/container/CMakeLists.txt` | Register new test file | - -### Files to create - -| File | Content | -|---|---| -| `tests/container/test_undirected_bidirectional.cpp` | Integration tests | - -### Steps - -#### 4.1 Add ADL friends (undirected_adjacency_list.hpp) - -In `base_undirected_adjacency_list`, immediately after the existing `edges()` -friend functions (around line 1030), add: - -```cpp - // in_edges(g, u) — for undirected graphs, same as edges(g, u) - template - requires adj_list::vertex_descriptor_type - friend constexpr auto in_edges(graph_type& g, const U& u) noexcept { - auto uid = static_cast(u.vertex_id()); - return g.vertices_[uid].edges(g, uid); - } - template - requires adj_list::vertex_descriptor_type - friend constexpr auto in_edges(const graph_type& g, const U& u) noexcept { - auto uid = static_cast(u.vertex_id()); - return g.vertices_[uid].edges(g, uid); - } -``` - -Also add `in_degree` friends if the class has a `degree()` member/friend. -Otherwise, the `in_degree` CPO's default tier (`size(in_edges(g, u))`) will -handle it automatically. - -#### 4.2 Create test file - -Test using actual `undirected_adjacency_list` instances (e.g., `vov_graph`): - -1. **`in_edges` returns same edges as `edges`** — for each vertex, verify - `in_edges(g, u)` and `edges(g, u)` yield the same edge set. - -2. **`in_degree` equals `degree`** — for each vertex. - -3. **Concept satisfaction** — `static_assert(bidirectional_adjacency_list)`. - -4. **`find_in_edge` works** — since `in_edges` returns the same range. - -5. **`contains_in_edge` works** — mirrors `contains_edge`. - -6. Run against multiple trait types (at least `vov` and `vol`). - -#### 4.3 Register test and build - -### Merge gate - -- [ ] Full test suite passes. -- [ ] `undirected_adjacency_list` models `bidirectional_adjacency_list`. -- [ ] `in_edges(g, u)` and `edges(g, u)` produce identical results. - ---- - -## Phase 5 — `in_incidence` and `in_neighbors` views - -**Goal:** Create the incoming-edge view files, their factory functions, -and tests. - -**Why fifth:** Views consume the CPOs from Phases 1-3 and can now also be -tested against the undirected container from Phase 4. - -### Files to modify - -| File | Action | -|---|---| -| `tests/views/CMakeLists.txt` | Register new test files | - -### Files to create - -| File | Content | -|---|---| -| `include/graph/views/in_incidence.hpp` | `in_incidence_view`, `basic_in_incidence_view`, factory functions | -| `include/graph/views/in_neighbors.hpp` | `in_neighbors_view`, `basic_in_neighbors_view`, factory functions | -| `tests/views/test_in_incidence.cpp` | View tests | -| `tests/views/test_in_neighbors.cpp` | View tests | - -### Steps - -#### 5.1 Create `include/graph/views/in_incidence.hpp` - -Copy `incidence.hpp` and make these changes: - -- Rename all classes: `incidence_view` → `in_incidence_view`, - `basic_incidence_view` → `basic_in_incidence_view`. -- Change edge iteration from `edges(g, u)` to `in_edges(g, u)`. -- Change neighbor ID extraction from `target_id(g, e)` to `source_id(g, e)`. -- Change `edge_t` references to `in_edge_t`. -- Change concept constraint from `adjacency_list` to `bidirectional_adjacency_list`. -- Update all doxygen comments. -- Update the `edge_info` yield: `{sid, uv}` / `{sid, uv, val}` / `{sid}` / `{sid, val}`. - -#### 5.2 Create `include/graph/views/in_neighbors.hpp` - -Copy `neighbors.hpp` and apply the same transformation: - -- Rename: `neighbors_view` → `in_neighbors_view`. -- Iterate `in_edges` instead of `edges`. -- Extract `source_id` instead of `target_id`. -- Retrieve source vertex via `source(g, e)` instead of `target(g, e)`. -- Constrain with `bidirectional_adjacency_list`. - -#### 5.3 Add outgoing view aliases - -At the bottom of `in_incidence.hpp`: - -```cpp -namespace graph::views { - inline constexpr auto& out_incidence = incidence; - inline constexpr auto& basic_out_incidence = basic_incidence; -} -``` - -At the bottom of `in_neighbors.hpp`: - -```cpp -namespace graph::views { - inline constexpr auto& out_neighbors = neighbors; - inline constexpr auto& basic_out_neighbors = basic_neighbors; -} -``` - -#### 5.4 Update `graph.hpp` - -Add includes: - -```cpp -#include -#include -``` - -#### 5.5 Create test files - -**`test_in_incidence.cpp`** — mirror `test_incidence.cpp`: -- Use undirected_adjacency_list (from Phase 4) as the bidirectional graph. -- Verify `in_incidence(g, u)` yields `{source_id, edge}` tuples. -- Verify `in_incidence(g, u, evf)` yields `{source_id, edge, value}`. -- Verify `basic_in_incidence(g, uid)` yields `{source_id}`. -- Verify factory function overloads (descriptor and ID). -- Verify `out_incidence` alias is the same as `incidence`. - -**`test_in_neighbors.cpp`** — mirror `test_neighbors.cpp`: -- Verify `in_neighbors(g, u)` yields `{source_id, vertex}` tuples. -- Verify `basic_in_neighbors(g, uid)` yields `{source_id}`. -- Verify `out_neighbors` alias. - -#### 5.6 Register tests and build - -### Merge gate - -- [ ] Full test suite passes. -- [ ] `in_incidence_view` iterates incoming edges, yields `source_id`. -- [ ] `in_neighbors_view` iterates source vertices. -- [ ] All factory function overloads work. -- [ ] Outgoing aliases are identity references. - ---- - -## Phase 6 — Pipe-syntax adaptors - -**Goal:** Add pipe-syntax closures for `in_incidence`, `in_neighbors`, -`basic_in_incidence`, `basic_in_neighbors`, and their `out_` aliases -to `adaptors.hpp`. - -**Why sixth:** Adaptors depend on the view headers from Phase 5. - -### Files to modify - -| File | Action | -|---|---| -| `include/graph/views/adaptors.hpp` | Add adaptor closures and factory fns | -| `tests/views/test_adaptors.cpp` | Add pipe-syntax tests for new views | - -### Steps - -#### 6.1 Add adaptor closures (adaptors.hpp) - -For each new view, follow the existing pattern: - -1. `in_incidence_adaptor_closure` — holds `uid` and optional `evf`. - `operator|(G&& g, closure)` calls `graph::views::in_incidence(g, uid)` or - `graph::views::in_incidence(g, uid, evf)`. - -2. `in_incidence_adaptor_fn` — has `operator()(uid)`, `operator()(uid, evf)`, - `operator()(g, uid)`, `operator()(g, uid, evf)`. - -3. Same for `basic_in_incidence`, `in_neighbors`, `basic_in_neighbors`. - -4. Outgoing aliases: - ```cpp - inline constexpr auto& out_incidence = incidence; - inline constexpr auto& basic_out_incidence = basic_incidence; - inline constexpr auto& out_neighbors = neighbors; - inline constexpr auto& basic_out_neighbors = basic_neighbors; - ``` - -Add to the adaptor namespace block at the bottom: - -```cpp -inline constexpr in_incidence_adaptor_fn in_incidence{}; -inline constexpr basic_in_incidence_adaptor_fn basic_in_incidence{}; -inline constexpr in_neighbors_adaptor_fn in_neighbors{}; -inline constexpr basic_in_neighbors_adaptor_fn basic_in_neighbors{}; -``` - -#### 6.2 Add tests to `test_adaptors.cpp` - -Append new test cases: - -```cpp -TEST_CASE("pipe: g | in_incidence(uid)", "[adaptors][in_incidence]") { ... } -TEST_CASE("pipe: g | in_incidence(uid, evf)", "[adaptors][in_incidence]") { ... } -TEST_CASE("pipe: g | basic_in_incidence(uid)", "[adaptors][basic_in_incidence]") { ... } -TEST_CASE("pipe: g | in_neighbors(uid)", "[adaptors][in_neighbors]") { ... } -TEST_CASE("pipe: g | basic_in_neighbors(uid)", "[adaptors][in_neighbors]") { ... } -TEST_CASE("pipe: out_ aliases forward", "[adaptors][aliases]") { ... } -``` - -#### 6.3 Build and run full test suite - -### Merge gate - -- [ ] Full test suite passes. -- [ ] `g | in_incidence(uid)` produces same results as `in_incidence(g, uid)`. -- [ ] All 8 pipe expressions from design doc §8.4 work. - ---- - -## Phase 7 — BFS/DFS/topological-sort `EdgeAccessor` parameterization - -**Goal:** Add an `EdgeAccessor` template parameter to all 6 traversal view -classes and their factory functions; define `out_edges_accessor` and -`in_edges_accessor` functors. - -**Why seventh:** This is a cross-cutting change to 3 existing view headers. -It must not break any existing call sites (the default accessor preserves -current behavior). Having the undirected bidirectional container (Phase 4) -enables testing reverse traversal on a real graph. - -### Files to modify - -| File | Action | -|---|---| -| `include/graph/views/bfs.hpp` | Add `EdgeAccessor` param to `vertices_bfs_view`, `edges_bfs_view`; replace 5 hardcoded `adj_list::edges()` calls | -| `include/graph/views/dfs.hpp` | Same for `vertices_dfs_view`, `edges_dfs_view`; replace 5 calls | -| `include/graph/views/topological_sort.hpp` | Same for `vertices_topological_sort_view`, `edges_topological_sort_view`; replace 7 calls | -| `tests/views/CMakeLists.txt` | Register new test file | - -### Files to create - -| File | Content | -|---|---| -| `include/graph/views/edge_accessor.hpp` | `out_edges_accessor`, `in_edges_accessor` definitions | -| `tests/views/test_reverse_traversal.cpp` | Reverse BFS/DFS tests | - -### Steps - -#### 7.1 Create `edge_accessor.hpp` - -```cpp -#pragma once -#include - -namespace graph::views { - -struct out_edges_accessor { - template - constexpr auto operator()(G& g, adj_list::vertex_t u) const { - return adj_list::edges(g, u); - } -}; - -struct in_edges_accessor { - template - constexpr auto operator()(G& g, adj_list::vertex_t u) const { - return adj_list::in_edges(g, u); - } -}; - -} // namespace graph::views -``` - -#### 7.2 Parameterize BFS views (bfs.hpp) - -For each of `vertices_bfs_view` and `edges_bfs_view`: - -1. Add `class EdgeAccessor = out_edges_accessor` as the last template parameter - (before `Alloc` if present, or after the value function type). - -2. Store `[[no_unique_address]] EdgeAccessor edge_accessor_;` member. - -3. Replace every `adj_list::edges(g, v)` or `adj_list::edges(*g_, v)` with - `edge_accessor_(g, v)` or `edge_accessor_(*g_, v)`. - -4. Add factory function overloads accepting `EdgeAccessor`: - ```cpp - vertices_bfs(g, seed, vvf, accessor) - edges_bfs(g, seed, evf, accessor) - ``` - -5. Existing signatures remain unchanged (default `EdgeAccessor`). - -#### 7.3 Parameterize DFS views (dfs.hpp) - -Same transformation: 5 `adj_list::edges()` calls → `edge_accessor_()`. - -#### 7.4 Parameterize topological sort views (topological_sort.hpp) - -Same transformation: 7 `adj_list::edges()` calls → `edge_accessor_()`. - -#### 7.5 Create `test_reverse_traversal.cpp` - -Using an undirected_adjacency_list (where `in_edges == edges`): - -1. **Forward BFS** — `vertices_bfs(g, seed)` produces expected order. -2. **Reverse BFS** — `vertices_bfs(g, seed, void_fn, in_edges_accessor{})` - produces expected order (same as forward for undirected). -3. **Forward DFS** vs **Reverse DFS** — same pattern. -4. **Source-compatibility** — existing call sites `vertices_bfs(g, seed)` and - `vertices_bfs(g, seed, vvf)` still compile unchanged. - -#### 7.6 Build and run full test suite - -### Merge gate - -- [ ] Full test suite passes (zero regressions in existing BFS/DFS/topo tests). -- [ ] Reverse BFS/DFS with `in_edges_accessor` produces correct traversal. -- [ ] Existing factory signatures compile without changes. - ---- - -## Phase 8 — `dynamic_graph` bidirectional support - -**Goal:** Add `bool Bidirectional = false` template parameter to -`dynamic_graph_base`, maintain reverse adjacency lists when enabled, -provide `in_edges()` ADL friend. - -**Why eighth:** This is the most complex container change. All prerequisite -infrastructure (CPOs, concepts, views) is already proven by earlier phases. - -### Files to modify - -| File | Action | -|---|---| -| `include/graph/container/dynamic_graph.hpp` | Add `Bidirectional` param, reverse storage, in_edges friend, mutator updates | -| `tests/container/CMakeLists.txt` | Register new test file | - -### Files to create - -| File | Content | -|---|---| -| `tests/container/test_dynamic_graph_bidirectional.cpp` | Comprehensive tests | - -### Steps - -#### 8.1 Add `Bidirectional` template parameter - -Change `dynamic_graph_base` signature: - -```cpp -template -class dynamic_graph_base; -``` - -Propagate through `dynamic_graph` and all using-aliases. - -#### 8.2 Add reverse edge storage (conditional) - -When `Bidirectional = true`, each vertex needs a second edge container. -Use `if constexpr` or a conditional base class: - -```cpp -// In the vertex type: -if constexpr (Bidirectional) { - edges_type in_edges_; // reverse adjacency container -} -``` - -Add `in_edges()` accessor to the vertex type (conditional on `Bidirectional`). - -#### 8.3 Update mutators - -Update `create_edge(u, v, ...)`: -```cpp -if constexpr (Bidirectional) { - // Insert reverse entry in v.in_edges_ with source = u -} -``` - -Similarly update `erase_edge`, `erase_vertex`, `clear_vertex`, `clear`, -copy constructor, move constructor, and swap. Each must maintain -forward↔reverse consistency. - -#### 8.4 Add `in_edges()` ADL friend - -```cpp -template -requires adj_list::vertex_descriptor_type -friend constexpr auto in_edges(graph_type& g, const U& u) noexcept - requires (Bidirectional) -{ - auto uid = static_cast(u.vertex_id()); - return g.vertices_[uid].in_edges(g, uid); -} -``` - -#### 8.5 Create comprehensive test file - -1. **Basic bidirectional graph construction** — create edges, verify `in_edges` - returns expected source vertices. - -2. **`in_degree` matches expected** — for each vertex. - -3. **Concept satisfaction** — `static_assert(bidirectional_adjacency_list)`. - -4. **Mutator invariants** — after `create_edge(u, v)`: - - `contains_edge(g, u, v) == true` - - `contains_in_edge(g, v, u) == true` - - After `erase_edge(...)`: - - Both forward and reverse entries removed. - -5. **`erase_vertex`** — removes all incident edges from both directions. - -6. **`clear_vertex`** — removes all edges for vertex in both directions. - -7. **Copy/move** — verify reverse adjacency is preserved. - -8. **Non-bidirectional unchanged** — `Bidirectional=false` graph compiles and - works identically to before. `static_assert(!bidirectional_adjacency_list)`. - -9. **Views integration** — `in_incidence(g, u)` and `in_neighbors(g, u)` work - on bidirectional dynamic_graph. - -10. Test with at least 2 trait types (e.g., `vov` and `vol`). - -#### 8.6 Register test and build - -### Merge gate - -- [ ] Full test suite passes. -- [ ] All 6 mutator invariants from design doc §10.5 are tested and pass. -- [ ] Non-bidirectional mode has identical behavior (no regressions). -- [ ] `bidirectional_adjacency_list` concept satisfied. - ---- - -## Phase 9 — Algorithm updates (Kosaraju + transpose_view) - -**Goal:** Optimize Kosaraju's SCC to use `in_edges()` when available; -add `transpose_view` as a zero-cost adaptor. - -**Why ninth:** Algorithms depend on container support (Phase 8) and the -edge accessor infrastructure (Phase 7). - -### Files to modify - -| File | Action | -|---|---| -| `include/graph/algorithm/connected_components.hpp` | Add `if constexpr (bidirectional_adjacency_list)` branch using `in_edges_accessor` | -| `tests/algorithms/CMakeLists.txt` | Register new test file | - -### Files to create - -| File | Content | -|---|---| -| `include/graph/views/transpose.hpp` | `transpose_view` wrapper | -| `tests/algorithms/test_scc_bidirectional.cpp` | SCC tests with bidirectional graph | -| `tests/views/test_transpose.cpp` | Transpose view tests | - -### Steps - -#### 9.1 Add `transpose_view` - -A lightweight wrapper that swaps `edges()` ↔ `in_edges()`: - -```cpp -template -class transpose_view { - G* g_; -public: - // ADL friends: edges(tv, u) → in_edges(*g_, u) - // in_edges(tv, u) → edges(*g_, u) - // Forward all other CPOs to underlying graph -}; -``` - -#### 9.2 Optimize Kosaraju's SCC - -Add a compile-time branch: -```cpp -if constexpr (bidirectional_adjacency_list) { - // Use in_edges_accessor for second DFS pass -} else { - // Keep existing edge-rebuild approach -} -``` - -#### 9.3 Create tests and build - -### Merge gate - -- [ ] Full test suite passes. -- [ ] SCC produces correct results on bidirectional graph. -- [ ] `transpose_view` swaps edge directions correctly. - ---- - -## Phase 10 — Documentation - -**Goal:** Update all documentation per design doc §13. - -### Files to modify - -Per the table in design doc §13.1 (11 files), plus: - -| File | Action | -|---|---| -| `docs/index.md` | Add "Bidirectional edge access" feature | -| `docs/getting-started.md` | Add incoming edges section | -| `docs/user-guide/views.md` | Add `in_incidence`, `in_neighbors` | -| `docs/user-guide/algorithms.md` | Note bidirectional benefits | -| `docs/user-guide/containers.md` | Document `Bidirectional` parameter | -| `docs/reference/concepts.md` | Add `bidirectional_adjacency_list` | -| `docs/reference/cpo-reference.md` | Add all incoming CPOs | -| `docs/reference/type-aliases.md` | Add `in_edge_t` etc. | -| `docs/reference/adjacency-list-interface.md` | Add incoming edge section | -| `docs/contributing/cpo-implementation.md` | Add `in_edges` example | -| `README.md` | Update feature highlights | -| `CHANGELOG.md` | Add entry | - -### Files to create - -| File | Content | -|---|---| -| `docs/user-guide/bidirectional-access.md` | Tutorial guide | - -### Steps - -1. Update each file per the table. -2. Create the tutorial guide with examples using `in_edges`, `in_incidence`, - reverse BFS, and bidirectional `dynamic_graph`. -3. Add CHANGELOG entry. -4. Build docs and verify no broken links. - -### Merge gate - -- [ ] All docs updated. -- [ ] Tutorial compiles (code examples are tested). -- [ ] No broken internal links. - ---- - -## Phase summary - -| Phase | Title | New files | Modified files | Key deliverable | -|---|---|---|---|---| -| 1 | `in_edges`/`in_degree` CPOs + aliases | 1 test | 2 | Core CPOs + type aliases | -| 2 | `find_in_edge`/`contains_in_edge` + traits | 3 tests | 3 | Complete CPO surface | -| 3 | Concepts + re-exports | 1 test | 2 | `bidirectional_adjacency_list` concept | -| 4 | Undirected container support | 1 test | 1 | First real bidirectional container | -| 5 | `in_incidence`/`in_neighbors` views | 2 headers + 2 tests | 1 | Incoming-edge views | -| 6 | Pipe-syntax adaptors | — | 2 | `g \| in_incidence(uid)` | -| 7 | BFS/DFS/topo EdgeAccessor | 1 header + 1 test | 3 | Reverse traversal | -| 8 | `dynamic_graph` bidirectional | 1 test | 1 | Directed bidirectional container | -| 9 | Algorithms | 2 headers + 2 tests | 1 | Kosaraju + transpose | -| 10 | Documentation | 1 guide | 12 | Complete docs | - -**Total estimated effort:** 11-15 days (same as design doc estimate) - ---- - -## Safety principles - -1. **Each phase adds only; nothing is removed or renamed.** - Existing tests pass between every phase. - -2. **Default template arguments preserve source compatibility.** - `EdgeAccessor = out_edges_accessor` means no existing call site changes. - -3. **Stub graphs in early phases isolate CPO testing from container code.** - Phases 1-3 don't touch any container — they use lightweight test stubs. - -4. **The simplest container change comes first (Phase 4).** - Undirected just forwards to the existing `edges()` function. - -5. **The most complex change (Phase 8) comes last in the implementation - sequence**, after all the infrastructure it depends on is proven. - -6. **Each phase has an explicit merge gate** — a checklist of conditions - that must pass before proceeding. diff --git a/cmake/CodeCoverage.cmake b/cmake/CodeCoverage.cmake index 447a8ee..401f92a 100644 --- a/cmake/CodeCoverage.cmake +++ b/cmake/CodeCoverage.cmake @@ -23,8 +23,8 @@ function(enable_coverage target_name) add_custom_target(coverage COMMAND ${LCOV} --directory . --zerocounters COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure - COMMAND ${LCOV} --directory . --capture --output-file coverage.info --ignore-errors mismatch - COMMAND ${LCOV} --remove coverage.info '/usr/*' '*/tests/*' --output-file coverage.info --ignore-errors mismatch + COMMAND ${LCOV} --directory . --capture --output-file coverage_raw.info --ignore-errors mismatch + COMMAND ${LCOV} --remove coverage_raw.info '/usr/*' '*/tests/*' --output-file coverage.info --ignore-errors mismatch,unused COMMAND ${GENHTML} coverage.info --output-directory coverage_html --ignore-errors mismatch WORKING_DIRECTORY ${CMAKE_BINARY_DIR} COMMENT "Generating code coverage report" diff --git a/docs/contributing/coding-guidelines.md b/docs/contributing/coding-guidelines.md index dc40b3e..57a2545 100755 --- a/docs/contributing/coding-guidelines.md +++ b/docs/contributing/coding-guidelines.md @@ -53,22 +53,22 @@ These requirements apply to every graph-related component, including implementat | Template Parameter | Type Alias | Variable Name | Description | |-------------------|------------|---------------|-------------| -| `E` | `edge_t` | `uv`, `vw` | Edge descriptor. `uv` is an edge from vertices `u` to `v`. `vw` is an edge from vertices `v` to `w`. | +| `E` | `out_edge_t` | `uv`, `vw` | Edge descriptor. `uv` is an edge from vertices `u` to `v`. `vw` is an edge from vertices `v` to `w`. Alias: `edge_t`. | | `EV` | `edge_value_t` | `val` | Edge Value, value or reference. This can be either the user-defined value on an edge, or a value returned by a function object (e.g. `EVF`) that is related to the edge. | -| `ER` | `vertex_edge_range_t` | | Edge Range for edges of a vertex | -| `EI` | `vertex_edge_iterator_t` | `uvi`, `vwi` | Edge Iterator for an edge of a vertex. `uvi` is an iterator for an edge from vertices `u` to `v`. `vwi` is an iterator for an edge from vertices `v` to `w`. | +| `ER` | `out_edge_range_t` | | Edge Range for outgoing edges of a vertex. Alias: `vertex_edge_range_t`. | +| `EI` | `out_edge_iterator_t` | `uvi`, `vwi` | Edge Iterator for an outgoing edge of a vertex. `uvi` is an iterator for an edge from vertices `u` to `v`. `vwi` is an iterator for an edge from vertices `v` to `w`. Alias: `vertex_edge_iterator_t`. | | `EVF` | | `evf` | Edge Value Function: `evf(g, uv)` → edge value. Graph passed as first parameter for stateless lambdas enabling `std::views` chaining. | | `EProj` | | `eproj` | Edge info projection function: `eproj(uv)` → `edge_info`. | ## Parameterized Type Aliases Types use the `_t` suffix to indicate they are parameterized by graph type `G`: - `vertex_t` - Vertex descriptor type (corresponds to `V`) -- `edge_t` - Edge descriptor type (corresponds to `E`) +- `out_edge_t` - Edge descriptor type (corresponds to `E`). Alias: `edge_t`. - `vertex_id_t`, `vertex_value_t`, `edge_value_t`, etc. - Other derived types - `vertex_iterator_t` - Iterator type for traversing vertices - `vertex_range_t` - Range type that enumerates vertices -- `vertex_edge_range_t` - Range type for the outgoing edges of a vertex -- `edge_iterator_t` - Iterator type for traversing edges +- `out_edge_range_t` - Range type for the outgoing edges of a vertex. Alias: `vertex_edge_range_t`. +- `out_edge_iterator_t` - Iterator type for traversing outgoing edges. Alias: `vertex_edge_iterator_t`. ## Public Interface Requirements All public functions for the graph data model have the following requirements: diff --git a/docs/contributing/cpo-implementation.md b/docs/contributing/cpo-implementation.md index 8c107fd..af59920 100644 --- a/docs/contributing/cpo-implementation.md +++ b/docs/contributing/cpo-implementation.md @@ -961,6 +961,51 @@ auto ids = vertices --- +## Incoming Edge CPOs — `in_edges` Example + +The `in_edges` CPO follows the same `_Choice_t` pattern as `out_edges` but +resolves to the incoming-edge list. This is the pattern used by all four +incoming-edge CPOs (`in_edges`, `in_degree`, `find_in_edge`, `contains_in_edge`). + +```cpp +namespace _in_edges { + enum class _St { _none, _member, _adl }; + + template + concept _has_member = requires(G& g, U& u) { + { g.in_edges(u) } -> std::ranges::forward_range; + }; + + template + concept _has_adl = requires(G& g, U& u) { + { in_edges(g, u) } -> std::ranges::forward_range; + }; + + // No default — in_edges requires explicit container support + template + [[nodiscard]] consteval _Choice_t<_St> _Choose() noexcept { + if constexpr (_has_member) { + return {_St::_member, /* noexcept */ }; + } else if constexpr (_has_adl) { + return {_St::_adl, /* noexcept */ }; + } else { + return {_St::_none, false}; + } + } + // ... operator() dispatches via if constexpr ... +}; +``` + +**Key difference from `out_edges`:** there is no built-in default — a graph +must explicitly provide `in_edges` (via member or ADL). The +`bidirectional_adjacency_list` concept constrains on this CPO being valid. + +The `in_edge_accessor` edge-access policy (see `edge_accessor.hpp`) delegates +to `in_edges`, `source_id`, and `source` to flip any view from outgoing to +incoming iteration. + +--- + ## See Also - [Architecture](architecture.md) — project structure and design principles diff --git a/docs/contributing/cpo-order.md b/docs/contributing/cpo-order.md index 76b666c..273e784 100644 --- a/docs/contributing/cpo-order.md +++ b/docs/contributing/cpo-order.md @@ -23,12 +23,12 @@ The following order should be followed for implementing CPOs and their correspon | # | CPO Function | Type Aliases | Description | |---|--------------|--------------|-------------| -| 4 | `edges(g, u)` | `vertex_edge_range_t`
`vertex_edge_iterator_t`
`edge_descriptor_t`
`edge_t` | Get outgoing edges from vertex (MUST return `edge_descriptor_view`) | +| 4 | `out_edges(g, u)` | `out_edge_range_t`
`out_edge_iterator_t`
`edge_descriptor_t`
`out_edge_t` | Get outgoing edges from vertex (MUST return `edge_descriptor_view`). Aliases: `edges(g, u)`, `vertex_edge_range_t`, `vertex_edge_iterator_t`, `edge_t`. | | 5 | `num_edges(g)` | - | Count total edges in graph | -**IMPORTANT:** `edges(g, u)` MUST always return an `edge_descriptor_view`: +**IMPORTANT:** `out_edges(g, u)` MUST always return an `edge_descriptor_view`: - If `g.edges(u)` exists, it must return `edge_descriptor_view` -- If ADL `edges(g, u)` exists, it must return `edge_descriptor_view` +- If ADL `out_edges(g, u)` exists, it must return `edge_descriptor_view` - Otherwise, if the vertex descriptor's inner value follows edge value patterns, return `edge_descriptor_view(u.inner_value(), u)` ### 3. Edge Target/Source Access @@ -44,8 +44,8 @@ The following order should be followed for implementing CPOs and their correspon | # | CPO Function | Type Aliases | Description | |---|--------------|--------------|-------------| -| 10 | `find_vertex_edge(g, u, vid)` | - | Find edge from u to vid | -| 11 | `contains_edge(g, uid, vid)` | - | Check if edge exists | +| 10 | `find_out_edge(g, u, vid)` | - | Find edge from u to vid. Alias: `find_vertex_edge(g, u, vid)`. | +| 11 | `contains_out_edge(g, uid, vid)` | - | Check if outgoing edge exists. Alias: `contains_edge(g, uid, vid)`. | ### 5. Partition Support @@ -58,7 +58,7 @@ The following order should be followed for implementing CPOs and their correspon | # | CPO Function | Type Aliases | Description | |---|--------------|--------------|-------------| | 13 | `num_vertices(g)` | - | Count vertices in graph | -| 14 | `degree(g, u)` | - | Get degree of vertex | +| 14 | `out_degree(g, u)` | - | Get out-degree of vertex. Alias: `degree(g, u)`. | ### 7. Value Access @@ -73,7 +73,7 @@ The following order should be followed for implementing CPOs and their correspon | # | CPO Function | Type Aliases | Description | |---|--------------|--------------|-------------| | 18 | `num_partitions(g)` | - | Get number of partitions (optional) | -| 19 | `has_edge(g)` | - | Check if graph has any edges | +| 19 | `has_edges(g)` | - | Check if graph has any edges | ## Implementation Priority @@ -92,7 +92,7 @@ The following order should be followed for implementing CPOs and their correspon 4. `u.vertex_id()` - Descriptor default (lowest priority) - Where `u` is a `vertex_descriptor` and `u.inner_value(g)` extracts the actual vertex data from the graph - `find_vertex(g, uid)` -- `edges(g, u)` + type aliases +- `out_edges(g, u)` + type aliases - `target_id(g, uv)` ### Phase 2: Query Functions (High Priority) @@ -100,12 +100,12 @@ The following order should be followed for implementing CPOs and their correspon - `num_vertices(g)` - `num_edges(g)` - `target(g, uv)` -- `degree(g, u)` +- `out_degree(g, u)` ### Phase 3: Edge Queries (Medium Priority) **Required for advanced graph operations** -- `find_vertex_edge(g, u, vid)` -- `contains_edge(g, uid, vid)` +- `find_out_edge(g, u, vid)` +- `contains_out_edge(g, uid, vid)` ### Phase 4: Optional Features (Low Priority) **For specialized graph types** @@ -113,7 +113,7 @@ The following order should be followed for implementing CPOs and their correspon - `source(g, uv)` - For sourced edges - `partition_id(g, u)` - For multipartite graphs - `num_partitions(g)` - For multipartite graphs -- `has_edge(g)` - Convenience function +- `has_edges(g)` - Convenience function ### Phase 5: Value Access (Optional) **For graphs with user-defined values** @@ -134,11 +134,16 @@ using vertex_t = range_value_t>; // vertex_descriptor // After vertex_id CPO using vertex_id_t = decltype(vertex_id(declval(), declval>())); -// After edges CPO -using vertex_edge_range_t = decltype(edges(declval(), declval>())); -using vertex_edge_iterator_t = iterator_t>; -using edge_descriptor_t = range_value_t>; -using edge_t = range_value_t>; // edge_descriptor +// After out_edges CPO (primary names) +using out_edge_range_t = decltype(out_edges(declval(), declval>())); +using out_edge_iterator_t = iterator_t>; +using edge_descriptor_t = range_value_t>; +using out_edge_t = range_value_t>; // edge_descriptor + +// Convenience aliases (old names) +using vertex_edge_range_t = out_edge_range_t; +using vertex_edge_iterator_t = out_edge_iterator_t; +using edge_t = out_edge_t; // After partition_id CPO (optional) using partition_id_t = decltype(partition_id(declval(), declval>())); @@ -147,7 +152,7 @@ using partition_id_t = decltype(partition_id(declval(), declval>( using vertex_value_t = decltype(vertex_value(declval(), declval>())); // After edge_value CPO (optional) -using edge_value_t = decltype(edge_value(declval(), declval>())); +using edge_value_t = decltype(edge_value(declval(), declval>())); // After graph_value CPO (optional) using graph_value_t = decltype(graph_value(declval())); @@ -156,5 +161,5 @@ using graph_value_t = decltype(graph_value(declval())); ## Notes - CPOs marked as "optional" may not be needed for all graph types -- Convenience overloads (e.g., `edges(g, uid)`, `degree(g, uid)`) can be added after core implementations +- Convenience aliases (`edges(g, u)` → `out_edges(g, u)`, `degree(g, u)` → `out_degree(g, u)`, `find_vertex_edge` → `find_out_edge`) can be used interchangeably with the primary names - The order optimizes for dependency resolution (earlier CPOs are used by later ones) diff --git a/docs/getting-started.md b/docs/getting-started.md index 306170a..7f99b4e 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -312,11 +312,75 @@ Edge lists are useful for algorithms that operate on edges directly --- -## 6. Next Steps +## 6. Incoming Edges and Reverse Traversal + +graph-v3 supports **bidirectional edge access** — you can query incoming edges +just as easily as outgoing edges. To enable incoming edges, construct a +`dynamic_graph` with the `Bidirectional` template parameter set to `true`: + +```cpp +#include +#include + +using namespace graph; +using namespace graph::container; + +// Bidirectional = true (sixth template parameter) +using Graph = dynamic_graph>; +``` + +With a bidirectional graph, you can use the incoming-edge CPOs: + +```cpp +Graph g({{0, 1}, {0, 2}, {1, 3}, {2, 3}}); + +// Incoming edges to vertex 3 +for (auto&& e : in_edges(g, *find_vertex(g, 3))) { + std::cout << source_id(g, e) << " -> 3\n"; +} +// Output: 1 -> 3 +// 2 -> 3 + +std::cout << "In-degree of vertex 3: " << in_degree(g, *find_vertex(g, 3)) << "\n"; +// Output: 2 +``` + +### Reverse BFS and DFS + +All search views (BFS, DFS, topological sort) accept an **Accessor** template +parameter that controls traversal direction. Pass `in_edge_accessor` to +traverse in reverse — following incoming edges instead of outgoing: + +```cpp +#include +#include + +using namespace graph::views; + +// Forward BFS from vertex 0 (default — outgoing edges) +for (auto [v] : vertices_bfs(g, 0)) { + std::cout << vertex_id(g, v) << " "; +} +// Output: 1 2 3 + +// Reverse BFS from vertex 3 (incoming edges) +for (auto [v] : vertices_bfs(g, 3)) { + std::cout << vertex_id(g, v) << " "; +} +// Output: 1 2 0 +``` + +For the full guide, see **[Bidirectional Access](user-guide/bidirectional-access.md)**. + +--- + +## 7. Next Steps - **[Adjacency Lists User Guide](user-guide/adjacency-lists.md)** — concepts, CPOs, descriptors - **[Edge Lists User Guide](user-guide/edge-lists.md)** — edge patterns, vertex ID types - **[Views](user-guide/views.md)** — BFS, DFS, topological sort, incidence, neighbors +- **[Bidirectional Access](user-guide/bidirectional-access.md)** — incoming edges, reverse traversal - **[Container Guide](user-guide/containers.md)** — `dynamic_graph`, `compressed_graph`, `undirected_adjacency_list` - **[Algorithm Reference](status/implementation_matrix.md#algorithms)** — all 13 algorithms - **[Migration from v2](migration-from-v2.md)** — what changed from graph-v2 diff --git a/docs/index.md b/docs/index.md index 64b926a..ecc8ac7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,7 +4,7 @@ # graph-v3 Documentation -> A modern C++20 graph library — 13 algorithms, 7 lazy views, 3 containers, 4261+ tests. +> A modern C++20 graph library — 13 algorithms, 7 lazy views, 3 containers, bidirectional edge access, 4405+ tests. @@ -18,6 +18,7 @@ - [Edge Lists](user-guide/edge-lists.md) — flat sourced-edge model, concepts, patterns - [Containers](user-guide/containers.md) — `dynamic_graph`, `compressed_graph`, `undirected_adjacency_list`, 27 trait combinations - [Views](user-guide/views.md) — lazy traversal views (BFS, DFS, topological sort, etc.) +- [Bidirectional Access](user-guide/bidirectional-access.md) — incoming edges, reverse traversal, `in_edge_accessor` - [Algorithms](user-guide/algorithms.md) — Dijkstra, Bellman-Ford, MST, connected components, and more ## Reference diff --git a/docs/migration-from-v2.md b/docs/migration-from-v2.md index 141c1e6..ef1ad7b 100644 --- a/docs/migration-from-v2.md +++ b/docs/migration-from-v2.md @@ -19,13 +19,14 @@ This guide summarizes what changed and what you need to update if you're migrati |------|----------|----------| | Access model | Reference-based | Value-based (descriptors) | | Concepts | 18 | 9 | -| "Sourced" function overloads | Required | Eliminated — source vertex always available via edge descriptor | +| "Sourced" function and view overloads | Required | Eliminated — source vertex always available via edge descriptor | | Vertex ID-only vs reference overloads | Both needed | Single overload via descriptors | | Undirected graph tagging | Required | Not required (verified by `undirected_adjacency_list` tests) | -| Vertex containers | `vector` only | `vector`, `deque`, `map`, `unordered_map` | -| Edge containers | `vector` only | `vector`, `deque`, `forward_list`, `list`, `set`, `unordered_set`, `map` | +| Vertex containers | `vector` and `deque` only | `vector`, `deque`, `map`, `unordered_map` (sparse vertex ids)| +| Edge containers | `vector` and `deque` only | `vector`, `deque`, `forward_list`, `list`, `set`, `unordered_set`, `map` | | Non-integral vertex IDs | Not supported | Supported in `dynamic_graph` | -| Namespaces | Flat `graph::` | `graph::adj_list::`, `graph::edge_list::`, `graph::views::`, `graph::container::` | +| Bidirectional graphs (incoming edges) | Not supported | `dynamic_graph<...,true,true,...>` and `undirected_adjacency_list` satisfy new `bidirectional_adjacency_list` | +| Namespaces | `graph::`, `graph::edge_list::`, `graph::views::`, `graph::container::` | `graph::adj_list::`, `graph::edge_list::`, `graph::views::`, `graph::container::` | --- @@ -45,7 +46,85 @@ edges without holding references to the underlying container. ### Containers -- **`undirected_adjacency_list`** added for undirected graph use cases with O(1) edge removal. +#### `undirected_adjacency_list` (new) + +A dedicated undirected graph container using a **dual-list design**: each edge is physically +stored in two doubly-linked lists — one at each incident vertex. This gives O(1) vertex access +(via contiguous vertex storage) and O(1) edge removal (by unlinking from both lists), without +relying on the general-purpose `dynamic_graph`. + +**Template signature:** + +```cpp +template class VContainer = std::vector, + typename Alloc = std::allocator> +class undirected_adjacency_list; +``` + +**Complexity guarantees:** + +| Operation | Complexity | +|-----------|-----------| +| Vertex access by id | O(1) | +| `create_vertex()` | O(1) amortized | +| `create_edge(u, v)` | O(1) | +| `erase_edge(pos)` | O(1) — unlinks from both vertices' lists | +| `degree(v)` | O(1) — cached per vertex | +| Iterate edges from vertex | O(degree) | +| Iterate all edges | O(V + E) | + +**Edge iteration semantics:** at graph level each undirected edge is visited **twice** — once +from each endpoint. Use `edges_size() / 2` to get the unique edge count. + +**Iterator invalidation:** +- Vertex iterators: invalidated by `create_vertex()` if reallocation occurs, and `clear()`. +- Vertex iterators: **not** invalidated by `create_edge()` or `erase_edge()`. + +**Basic usage:** + +```cpp +#include +using namespace graph::container; + +// Edge value = int (weight), vertex value = std::string (name) +undirected_adjacency_list g; + +auto u = g.create_vertex("Alice"); +auto v = g.create_vertex("Bob"); +auto e = g.create_edge(u, v, 42); // weight 42 + +// Iterate incident edges of u +for (auto&& [uid, vid, uv] : edges(g, *u)) { + std::cout << graph::vertex_value(g, *u) << " -- " + << graph::vertex_value(g, vid) + << " [" << graph::edge_value(g, uv) << "]\n"; +} +``` + +**v2 migration note:** In v2 undirected graphs required tagging the graph type with an +"undirected" marker. In v3 `undirected_adjacency_list` is its own concrete type — no tagging +needed. Replace any v2 undirected `adjacency_list` instantiation with +`undirected_adjacency_list` and remove the tag. + +**When to prefer over `dynamic_graph`:** +- Frequent edge removal (O(1) vs O(degree) for `dynamic_graph`). +- Edges with values that need to be updated in a single place. +- Algorithms that walk incident edges of both endpoints frequently. + +**When to prefer alternatives:** +- Read-only or write-once graphs → use `compressed_graph` (lower memory overhead). +- Directed graphs → use `dynamic_graph`. +- Very high per-vertex degrees (thousands of edges) → cache locality of `dynamic_graph` + edge vectors may outweigh the pointer overhead here. + +--- + +#### `dynamic_graph` (extended) + - **`dynamic_graph`** now supports: - Vertex storage in `map` and `unordered_map` (for sparse vertex IDs). - Edge storage in `map`, `set`, `unordered_set` (for sorted or deduplicated edges). @@ -53,13 +132,122 @@ edges without holding references to the underlying container. - 27 vertex×edge container combinations via traits (see [Containers](user-guide/containers.md)). +### Bidirectional Graphs (new) + +graph-v3 introduces the `bidirectional_adjacency_list` concept, which adds incoming-edge +support on top of the standard `adjacency_list` interface. Two containers satisfy it: + +| Container | How to enable | +|-----------|---------------| +| `dynamic_graph` | Set `Bidirectional = true` (5th template parameter; `Sourced` was removed in v3) | +| `undirected_adjacency_list` | Always satisfied — every edge is its own reverse | + +#### Enabling bidirectional on `dynamic_graph` + +```cpp +#include +#include + +using namespace graph::container; + +// EV=void, VV=void, GV=void, VId=uint32_t, Bidirectional=true +using BiDiGraph = dynamic_graph>; + +BiDiGraph g({{0, 1}, {0, 2}, {1, 3}, {2, 3}}); +``` + +When `Bidirectional = true`, each vertex automatically maintains an **incoming-edge list** +alongside its outgoing-edge list. Adding an edge `u → v` atomically inserts an in-edge +record at `v`. + +#### Incoming-edge CPOs + +Available on any graph satisfying `bidirectional_adjacency_list`: + +| CPO | Signature | Complexity | +|-----|-----------|------------| +| `in_edges(g, u)` | vertex descriptor → range | O(1) | +| `in_edges(g, uid)` | vertex id → range | O(1) | +| `in_degree(g, u)` | vertex descriptor → integral | O(1) | +| `in_degree(g, uid)` | vertex id → integral | O(1) | +| `find_in_edge(g, uid, vid)` | target id, source id → iterator | O(in-degree) | +| `contains_in_edge(g, uid, vid)` | target id, source id → bool | O(in-degree) | +| `source_id(g, e)` | in-edge descriptor → vertex id | O(1) | + +```cpp +#include + +auto v3 = *find_vertex(g, 3u); + +// Iterate incoming edges to vertex 3 +for (auto&& e : in_edges(g, v3)) { + std::cout << source_id(g, e) << " -> 3\n"; +} + +std::cout << "in_degree(3) = " << in_degree(g, v3) << "\n"; + +// Find a specific incoming edge +auto it = find_in_edge(g, 3u, 1u); // edge from vertex 1 to vertex 3 +bool exists = contains_in_edge(g, 3u, 1u); +``` + +#### Incoming-edge views + +The `incidence` and `neighbors` views have incoming-edge variants, parameterized via +the `in_edge_accessor` policy. These are constrained on `bidirectional_adjacency_list`. + +| View | Direction | Structured binding yields | +|------|-----------|---------------------------| +| `incidence(g, u)` | outgoing (default) | `[tid, uv]` | +| `out_incidence(g, u)` | outgoing (explicit) | `[tid, uv]` | +| `in_incidence(g, u)` | incoming | `[sid, uv]` | +| `basic_incidence(g, uid)` | outgoing | `[tid]` | +| `basic_out_incidence(g, uid)` | outgoing (explicit) | `[tid]` | +| `basic_in_incidence(g, uid)` | incoming | `[sid]` | +| `neighbors(g, u)` | outgoing (default) | `[tid, v]` | +| `in_neighbors(g, u)` | incoming | `[sid, v]` | + +```cpp +#include + +using namespace graph::views; + +// Incoming edges to vertex 3 +for (auto [sid, uv] : in_incidence(g, 3u)) { + std::cout << "Edge from " << sid << "\n"; +} + +// Predecessor vertices of vertex 3 +for (auto [sid, v] : in_neighbors(g, 3u)) { + std::cout << "Predecessor: " << sid << "\n"; +} + +// With edge value function +auto evf = [](const auto& g, auto& uv) { return edge_value(g, uv); }; +for (auto [sid, uv, w] : in_incidence(g, 3u, evf)) { + std::cout << sid << " weight " << w << "\n"; +} +``` + +#### Algorithms using bidirectional graphs + +- **`strongly_connected_components`** (Kosaraju's algorithm) requires + `index_bidirectional_adjacency_list` to perform the reverse pass over incoming edges. +- Reverse BFS/DFS is achieved by swapping `incidence` for `in_incidence` in the + traversal loop — no separate reverse-graph construction needed. + +> See the full reference in [Bidirectional Edge Access](user-guide/bidirectional-access.md). + +--- + ### Graph Container Interface - Added support for non-integral vertex IDs. - Extended range types for vertices and edges: - - **Vertices:** bidirectional (e.g. `map`) and forward (e.g. `unordered_map`) ranges. + - **Vertices:** bidirectional (e.g. `map`) and forward (e.g. `unordered_map`) ranges, enabling sparse vertex ids. - **Edges:** bidirectional (`map`, `set`), forward (`unordered_map`, `unordered_set`). - - **Impact:** GCI (P3130), Views (P3129), `dynamic_graph`. **Not** supported by algorithms (P3128). + - **Impact:** GCI (P3130), Views (P3129), `dynamic_graph`. **Not** supported by algorithms (P3128) at this time. ### Views @@ -84,15 +272,15 @@ Definitions specific to adjacency lists moved into `graph::adj_list::` to reflec | v2 namespace | v3 namespace | Contents | |--------------|--------------|----------| -| `graph::` | `graph::` | Root — re-exports `adj_list` types/CPOs for convenience | +| `graph::` | `graph::` | Root — re-exports `adj_list` types/CPOs for convenience. Common edge list definitions. | | *(mixed into `graph::`)* | `graph::adj_list::` | Adjacency list CPOs, descriptors, concepts, traits | -| *(N/A)* | `graph::edge_list::` | Edge list concepts, traits, descriptors | -| *(mixed into `graph::`)* | `graph::views::` | Graph views | -| *(N/A)* | `graph::container::` | Concrete graph containers | +| `graph::edge_list::` | `graph::edge_list::` | Edge list concepts, traits, descriptors. Separate from adjacency lists. | +| `graph::views` | `graph::views::` | Graph views | +| `graph::container::` | `graph::container::` | Concrete graph containers | > **Backward compatibility:** Core `adj_list` types and CPOs are re-exported into `graph::` > via `using` declarations, so most v2 code using `graph::vertices(g)` etc. will continue to -> compile without changes. +> compile without changes. This may be removed in the future. --- @@ -112,6 +300,7 @@ project-wide. There is no target date for that transition. - [ ] Update namespace qualifications if using explicit `graph::` prefixes - [ ] Update value function lambdas to accept `(const auto& g, ...)` as first parameter - [ ] If using undirected graphs: remove "undirected" tagging, use `undirected_adjacency_list` +- [ ] If you need predecessor/incoming-edge queries: use `dynamic_graph` with `Bidirectional=true`, or `undirected_adjacency_list`; replace manual reverse-graph builds with `in_edges`, `in_degree`, `in_incidence`, or `in_neighbors` - [ ] If using custom containers: check trait support in the [container matrix](user-guide/containers.md) diff --git a/docs/reference/adjacency-list-interface.md b/docs/reference/adjacency-list-interface.md index 8e151e7..7f803e4 100644 --- a/docs/reference/adjacency-list-interface.md +++ b/docs/reference/adjacency-list-interface.md @@ -26,7 +26,7 @@ All concepts are in `graph::adj_list`, re-exported to `graph`. | Concept | Parameters | Description | |---------|------------|-------------| -| `vertex_edge_range` | Range `R`, graph `G` | `forward_range` whose values satisfy `edge` | +| `out_edge_range` | Range `R`, graph `G` | `forward_range` whose values satisfy `edge` | ### Vertex Concepts @@ -40,9 +40,10 @@ All concepts are in `graph::adj_list`, re-exported to `graph`. | Concept | Parameters | Description | |---------|------------|-------------| -| `adjacency_list` | Graph `G` | `vertices(g)` → `vertex_range`, `edges(g,u)` → `vertex_edge_range` | +| `adjacency_list` | Graph `G` | `vertices(g)` → `vertex_range`, `edges(g,u)` → `out_edge_range` | | `index_adjacency_list` | Graph `G` | `adjacency_list` + `index_vertex_range` (random-access vertices, integral IDs) | | `ordered_vertex_edges` | Graph `G` | `adjacency_list` + edge ranges sorted by `target_id` | +| `bidirectional_adjacency_list` | Graph `G` | `index_adjacency_list` + `in_edges(g,u)` and `in_degree(g,u)` | > **Note:** All current algorithms require `index_adjacency_list`. The more > general `adjacency_list` supports associative vertex containers and may be @@ -91,9 +92,21 @@ All follow the `_t` naming convention. | Alias | Definition | |-------|------------| -| `vertex_edge_range_t` | `decltype(edges(g, u))` | -| `vertex_edge_iterator_t` | `std::ranges::iterator_t>` | -| `edge_t` | `std::ranges::range_value_t>` | +| `out_edge_range_t` | `decltype(out_edges(g, u))` | +| `out_edge_iterator_t` | `std::ranges::iterator_t>` | +| `out_edge_t` | `std::ranges::range_value_t>` | + +> **Convenience aliases:** `vertex_edge_range_t`, `vertex_edge_iterator_t`, and `edge_t` remain available as aliases for the above. + +### Incoming Edge Types (Bidirectional) + +Available on graphs satisfying `bidirectional_adjacency_list`. + +| Alias | Definition | +|-------|------------| +| `in_edge_range_t` | `decltype(in_edges(g, u))` | +| `in_edge_iterator_t` | `std::ranges::iterator_t>` | +| `in_edge_t` | `std::ranges::range_value_t>` | ### Value Types (Optional) @@ -127,7 +140,7 @@ pattern. See [CPO Reference](cpo-reference.md) for full resolution order. | `num_vertices(g)` | integral | O(1) | `size(vertices(g))` | | `num_vertices(g, pid)` | integral | O(1) | `size(vertices(g))` | | `num_edges(g)` | integral | O(V + E) | Sum of `distance(edges(g, u))` over all vertices | -| `has_edge(g)` | bool | O(V) | First vertex with non-empty edge range | +| `has_edges(g)` | bool | O(V) | First vertex with non-empty edge range | | `num_partitions(g)` | integral | O(1) | `1` | ### Vertex Functions @@ -158,6 +171,20 @@ pattern. See [CPO Reference](cpo-reference.md) for full resolution order. | `find_vertex_edge(g, uid, vid)` | `vertex_edge_iterator_t` | O(degree) | `find_vertex_edge(g, *find_vertex(g, uid), vid)` | | `contains_edge(g, uid, vid)` | bool | O(degree) | `find_vertex_edge(g, uid, vid) != end(edges(g, uid))` | +### Incoming Edge Functions (Bidirectional) + +Available on graphs satisfying `bidirectional_adjacency_list`. + +| Function | Return Type | Default Complexity | Default Implementation | +|----------|-------------|-------------------|------------------------| +| `in_edges(g, u)` | `in_edge_range_t` | O(1) | From container’s incoming-edge list | +| `in_edges(g, uid)` | `in_edge_range_t` | O(1) | `in_edges(g, *find_vertex(g, uid))` | +| `in_degree(g, u)` | integral | O(1) | `size(in_edges(g, u))` | +| `in_degree(g, uid)` | integral | O(1) | `in_degree(g, *find_vertex(g, uid))` | +| `find_in_edge(g, u, vid)` | `in_edge_iterator_t` | O(in-degree) | Linear search through `in_edges(g, u)` | +| `find_in_edge(g, uid, vid)` | `in_edge_iterator_t` | O(in-degree) | `find_in_edge(g, *find_vertex(g, uid), vid)` | +| `contains_in_edge(g, uid, vid)` | bool | O(in-degree) | `find_in_edge(g, uid, vid) != end(in_edges(g, uid))` | + --- ## Descriptor System diff --git a/docs/reference/concepts.md b/docs/reference/concepts.md index 5912db3..9c0ede4 100644 --- a/docs/reference/concepts.md +++ b/docs/reference/concepts.md @@ -16,13 +16,14 @@ Header: `` ``` edge -└── vertex_edge_range +└── out_edge_range └── vertex └── vertex_range └── index_vertex_range └── adjacency_list └── index_adjacency_list - └── ordered_vertex_edges + ├── ordered_vertex_edges + └── bidirectional_adjacency_list ``` ### `edge` @@ -36,13 +37,13 @@ concept edge = requires(G& g, E& e) { }; ``` -### `vertex_edge_range` +### `out_edge_range` -A forward range of edges adjacent to a vertex. +A forward range of outgoing edges adjacent to a vertex. ```cpp template -concept vertex_edge_range = +concept out_edge_range = std::ranges::forward_range && edge>; ``` @@ -54,7 +55,7 @@ A vertex exposes an edge range via the `edges` CPO. ```cpp template concept vertex = requires(G& g, V& u) { - { edges(g, u) } -> vertex_edge_range; + { edges(g, u) } -> out_edge_range; }; ``` @@ -127,6 +128,35 @@ concept ordered_vertex_edges = }; ``` +### `bidirectional_adjacency_list` + +An adjacency list that also provides incoming-edge iteration. + +```cpp +template +concept bidirectional_adjacency_list = + index_adjacency_list && + requires(G& g, vertex_t& u) { + { in_edges(g, u) } -> std::ranges::forward_range; + { in_degree(g, u) } -> std::integral; + }; +``` + +| Requirement | Expression | +|-------------|-----------| +| Incoming edge range | `in_edges(g, u)` returns a `forward_range` of in-edge descriptors | +| In-degree query | `in_degree(g, u)` returns an integral count | +| Find incoming edge | `find_in_edge(g, uid, vid)` returns an in-edge iterator | +| Contains check | `contains_in_edge(g, uid, vid)` returns `bool` | + +**Satisfied by:** +- `dynamic_graph<..., Bidirectional=true, ...>` +- `undirected_adjacency_list` (incoming = outgoing for undirected graphs) + +This concept is required by algorithms that need reverse traversal, such as +Kosaraju's SCC algorithm, and by the `in_edge_accessor` policy used for +reverse BFS/DFS/topological-sort views. + --- ## Edge List Concepts diff --git a/docs/reference/cpo-reference.md b/docs/reference/cpo-reference.md index 2996cd6..110b439 100644 --- a/docs/reference/cpo-reference.md +++ b/docs/reference/cpo-reference.md @@ -11,6 +11,12 @@ structure. Each CPO resolves at compile time via a three-step priority: All CPOs listed below are available in `namespace graph` after `#include `. +> **Convention:** The explicit directional names (`out_edges`, `out_degree`, +> `find_out_edge`, `out_edge_range_t`, etc.) are the primary definitions. +> Shorter convenience aliases (`edges`, `degree`, `find_vertex_edge`, +> `vertex_edge_range_t`, `edge_t`, etc.) are provided and **preferred in +> examples and user code** for brevity. + --- ## Quick Reference Table @@ -21,8 +27,8 @@ All CPOs listed below are available in `namespace graph` after | `vertices` | `(g, pid)` | `vertex_range_t` | O(1) | No | | `vertex_id` | `(g, ui)` | `vertex_id_t` | O(1) | Yes | | `find_vertex` | `(g, uid)` | `vertex_iterator_t` | O(1) | Yes | -| `edges` | `(g, u)` | `vertex_edge_range_t` | O(1) | Yes | -| `edges` | `(g, uid)` | `vertex_edge_range_t` | O(1) | Yes | +| `out_edges` | `(g, u)` / `(g, uid)` | `out_edge_range_t` | O(1) | Yes | +| `in_edges` | `(g, u)` / `(g, uid)` | `in_edge_range_t` | O(1) | Yes | | `target_id` | `(g, uv)` | `vertex_id_t` | O(1) | Yes | | `target` | `(g, uv)` | `vertex_t&` | O(1) | Yes | | `source_id` | `(g, uv)` | `vertex_id_t` | O(1) | Yes | @@ -31,16 +37,40 @@ All CPOs listed below are available in `namespace graph` after | `num_vertices` | `(g, pid)` | integral | O(1) | No | | `num_edges` | `(g)` | integral | O(V) | Yes | | `num_edges` | `(g, u)` / `(g, uid)` | integral | O(1) | Yes | -| `degree` | `(g, u)` / `(g, uid)` | integral | O(1) | Yes | -| `find_vertex_edge` | `(g, u, v)` / `(g, uid, vid)` | `vertex_edge_iterator_t` | O(deg) | Yes | -| `contains_edge` | `(g, uid, vid)` | `bool` | O(deg) | Yes | -| `has_edge` | `(g)` | `bool` | O(V) | Yes | +| `out_degree` | `(g, u)` / `(g, uid)` | integral | O(1) | Yes | +| `in_degree` | `(g, u)` / `(g, uid)` | integral | O(1) | Yes | +| `find_out_edge` | `(g, u, v)` / `(g, uid, vid)` | `out_edge_iterator_t` | O(deg) | Yes | +| `find_in_edge` | `(g, u, v)` / `(g, uid, vid)` | `in_edge_iterator_t` | O(deg) | Yes | +| `contains_out_edge` | `(g, uid, vid)` | `bool` | O(deg) | Yes | +| `contains_in_edge` | `(g, uid, vid)` | `bool` | O(deg) | Yes | +| `has_edges` | `(g)` | `bool` | O(V) | Yes | | `vertex_value` | `(g, u)` | `decltype(auto)` | O(1) | No | | `edge_value` | `(g, uv)` | `decltype(auto)` | O(1) | Yes | | `graph_value` | `(g)` | `decltype(auto)` | O(1) | No | | `partition_id` | `(g, u)` | `partition_id_t` | O(1) | No | | `num_partitions` | `(g)` | integral | O(1) | Yes (1) | +### Convenience Aliases + +Several CPOs have shorter alias names that forward to the primary definition: + +| Alias | Forwards To | +|-------|-------------| +| `edges(g, u)` / `edges(g, uid)` | `out_edges(g, u)` / `out_edges(g, uid)` | +| `degree(g, u)` / `degree(g, uid)` | `out_degree(g, u)` / `out_degree(g, uid)` | +| `find_vertex_edge(g, ...)` | `find_out_edge(g, ...)` | +| `contains_edge(g, uid, vid)` | `contains_out_edge(g, uid, vid)` | + +Similarly for type aliases: + +| Alias | Primary | +|-------|---------| +| `vertex_edge_range_t` | `out_edge_range_t` | +| `vertex_edge_iterator_t` | `out_edge_iterator_t` | +| `edge_t` | `out_edge_t` | + +> Note: It's tempting to replace `find_vertex_edge(g, ...)` and `vertex_edge_range_t` with the simpler `find_edge(g, ...)` and `edge_range_t`. However, that could run into name conflicts with the abstract edge data structure and its types. They are in different namespaces to isolate the names, but it's safer to keep them the way they are until more thought is put into it. -PR 2/22/2026 + --- ## Vertex CPOs @@ -108,25 +138,27 @@ Returns the number of vertices. ## Edge CPOs -### `edges(g, u)` / `edges(g, uid)` +### `out_edges(g, u)` / `out_edges(g, uid)` + +Alias: `edges(g, u)` / `edges(g, uid)` ```cpp -auto edges(G& g, vertex_t& u) -> vertex_edge_range_t; -auto edges(G& g, vertex_id_t uid) -> vertex_edge_range_t; +auto out_edges(G& g, vertex_t& u) -> out_edge_range_t; +auto out_edges(G& g, vertex_id_t uid) -> out_edge_range_t; ``` -Returns the edge range for vertex `u` (by descriptor or ID). +Returns the outgoing edge range for vertex `u` (by descriptor or ID). | Property | Value | |----------|-------| -| **Constraint** | `vertex_edge_range` | -| **Default** | Descriptor: dereference `u` (range-of-ranges). ID: `edges(g, *find_vertex(g, uid))` | +| **Constraint** | `out_edge_range` | +| **Default** | Descriptor: dereference `u` (range-of-ranges). ID: `out_edges(g, *find_vertex(g, uid))` | | **Complexity** | O(1) | ### `target_id(g, uv)` ```cpp -auto target_id(G& g, edge_t& uv) -> vertex_id_t; +auto target_id(G& g, out_edge_t& uv) -> vertex_id_t; ``` Returns the target vertex ID for edge `uv`. @@ -140,7 +172,7 @@ Returns the target vertex ID for edge `uv`. ### `target(g, uv)` ```cpp -auto target(G& g, edge_t& uv) -> vertex_t&; +auto target(G& g, out_edge_t& uv) -> vertex_t&; ``` Returns a reference to the target vertex for edge `uv`. @@ -154,7 +186,7 @@ Returns a reference to the target vertex for edge `uv`. ### `source_id(g, uv)` ```cpp -auto source_id(G& g, edge_t& uv) -> vertex_id_t; +auto source_id(G& g, out_edge_t& uv) -> vertex_id_t; ``` Returns the source vertex ID for edge `uv`. Not all graph models support this @@ -169,7 +201,7 @@ Returns the source vertex ID for edge `uv`. Not all graph models support this ### `source(g, uv)` ```cpp -auto source(G& g, edge_t& uv) -> vertex_t&; +auto source(G& g, out_edge_t& uv) -> vertex_t&; ``` Returns a reference to the source vertex for edge `uv`. @@ -194,52 +226,58 @@ Returns the number of edges (whole graph or per vertex). | **Default (whole graph)** | Sum of `ranges::size(edges(g, u))` over all vertices — **O(V)** | | **Default (per vertex)** | `ranges::size(edges(g, u))` — **O(1)** | -### `degree(g, u)` / `degree(g, uid)` +### `out_degree(g, u)` / `out_degree(g, uid)` + +Alias: `degree(g, u)` / `degree(g, uid)` ```cpp -auto degree(G& g, vertex_t& u) -> integral; -auto degree(G& g, vertex_id_t uid) -> integral; +auto out_degree(G& g, vertex_t& u) -> integral; +auto out_degree(G& g, vertex_id_t uid) -> integral; ``` Returns the out-degree of vertex `u`. | Property | Value | |----------|-------| -| **Default** | `ranges::size(edges(g, u))` | +| **Default** | `ranges::size(out_edges(g, u))` | | **Complexity** | O(1) | -### `find_vertex_edge(g, ...)` +### `find_out_edge(g, ...)` + +Alias: `find_vertex_edge(g, ...)` ```cpp -auto find_vertex_edge(G& g, vertex_t& u, vertex_t& v) -> vertex_edge_iterator_t; -auto find_vertex_edge(G& g, vertex_t& u, vertex_id_t vid) -> vertex_edge_iterator_t; -auto find_vertex_edge(G& g, vertex_id_t uid, vertex_id_t vid) -> vertex_edge_iterator_t; +auto find_out_edge(G& g, vertex_t& u, vertex_t& v) -> out_edge_iterator_t; +auto find_out_edge(G& g, vertex_t& u, vertex_id_t vid) -> out_edge_iterator_t; +auto find_out_edge(G& g, vertex_id_t uid, vertex_id_t vid) -> out_edge_iterator_t; ``` -Finds the edge from `u` to `v` (or `uid` to `vid`). +Finds the outgoing edge from `u` to `v` (or `uid` to `vid`). | Property | Value | |----------|-------| -| **Default** | Linear scan: `ranges::find_if(edges(g, u), [&](auto& uv) { return target_id(g, uv) == vid; })` | +| **Default** | Linear scan: `ranges::find_if(out_edges(g, u), [&](auto& uv) { return target_id(g, uv) == vid; })` | | **Complexity** | O(degree(u)) | -### `contains_edge(g, uid, vid)` +### `contains_out_edge(g, uid, vid)` + +Alias: `contains_edge(g, uid, vid)` ```cpp -auto contains_edge(G& g, vertex_id_t uid, vertex_id_t vid) -> bool; +auto contains_out_edge(G& g, vertex_id_t uid, vertex_id_t vid) -> bool; ``` -Tests whether an edge from `uid` to `vid` exists. +Tests whether an outgoing edge from `uid` to `vid` exists. | Property | Value | |----------|-------| | **Default** | `find_vertex_edge(g, uid, vid) != ranges::end(edges(g, uid))` | | **Complexity** | O(degree(uid)) | -### `has_edge(g)` +### `has_edges(g)` ```cpp -auto has_edge(G& g) -> bool; +auto has_edges(G& g) -> bool; ``` Tests whether the graph has at least one edge. @@ -251,6 +289,67 @@ Tests whether the graph has at least one edge. --- +## Incoming Edge CPOs + +### `in_edges(g, u)` / `in_edges(g, uid)` + +```cpp +auto in_edges(G& g, vertex_t& u) -> in_edge_range_t; +auto in_edges(G& g, vertex_id_t uid) -> in_edge_range_t; +``` + +Returns the incoming edge range for vertex `u` (by descriptor or ID). + +| Property | Value | +|----------|-------| +| **Constraint** | `in_edge_range` | +| **Default** | Descriptor: `u.in_edges(g)` → ADL. ID: `in_edges(g, *find_vertex(g, uid))` | +| **Complexity** | O(1) | + +### `in_degree(g, u)` / `in_degree(g, uid)` + +```cpp +auto in_degree(G& g, vertex_t& u) -> integral; +auto in_degree(G& g, vertex_id_t uid) -> integral; +``` + +Returns the in-degree of vertex `u`. + +| Property | Value | +|----------|-------| +| **Default** | `ranges::size(in_edges(g, u))` | +| **Complexity** | O(1) | + +### `find_in_edge(g, ...)` + +```cpp +auto find_in_edge(G& g, vertex_t& u, vertex_t& v) -> in_edge_iterator_t; +auto find_in_edge(G& g, vertex_t& u, vertex_id_t vid) -> in_edge_iterator_t; +auto find_in_edge(G& g, vertex_id_t uid, vertex_id_t vid) -> in_edge_iterator_t; +``` + +Finds the incoming edge to `u` from `v` (or `uid` from `vid`). + +| Property | Value | +|----------|-------| +| **Default** | Linear scan: `ranges::find_if(in_edges(g, u), [&](auto& uv) { return source_id(g, uv) == vid; })` | +| **Complexity** | O(in_degree(u)) | + +### `contains_in_edge(g, uid, vid)` + +```cpp +auto contains_in_edge(G& g, vertex_id_t uid, vertex_id_t vid) -> bool; +``` + +Tests whether an incoming edge to `uid` from `vid` exists. + +| Property | Value | +|----------|-------| +| **Default** | `find_in_edge(g, uid, vid) != ranges::end(in_edges(g, uid))` | +| **Complexity** | O(in_degree(uid)) | + +--- + ## Value CPOs All three value CPOs (`vertex_value`, `edge_value`, `graph_value`) return diff --git a/docs/reference/edge-list-interface.md b/docs/reference/edge-list-interface.md index c5dec85..6766119 100644 --- a/docs/reference/edge-list-interface.md +++ b/docs/reference/edge-list-interface.md @@ -74,7 +74,7 @@ adjacency list edges and edge list edges. | `target_id(el, uv)` | `vertex_id_t` | O(1) | Automatic for tuple and edge_descriptor patterns | | `edge_value(el, uv)` | `edge_value_t` | O(1) | Automatic for 3+-element tuples and edge_descriptor patterns | | `num_edges(el)` | integral | O(1) | `std::ranges::size(el)` | -| `has_edge(el)` | bool | O(1) | `num_edges(el) > 0` | +| `has_edges(el)` | bool | O(1) | `num_edges(el) > 0` | | `contains_edge(el, uid, vid)` | bool | O(E) | Linear search for matching `source_id`/`target_id` | --- diff --git a/docs/reference/type-aliases.md b/docs/reference/type-aliases.md index 73147b1..08ae60a 100644 --- a/docs/reference/type-aliases.md +++ b/docs/reference/type-aliases.md @@ -19,9 +19,22 @@ concepts. | `vertex_iterator_t` | `std::ranges::iterator_t>` | `vertex_range` | | `vertex_t` | `std::ranges::range_value_t>` | `vertex_range` | | `vertex_id_t` | Result type of `vertex_id(g, ui)` — typically `std::size_t` | `index_vertex_range` | -| `vertex_edge_range_t` | Type of the range returned by `edges(g, u)` | `vertex` | -| `vertex_edge_iterator_t` | `std::ranges::iterator_t>` | `vertex` | -| `edge_t` | `std::ranges::range_value_t>` | `vertex` | +| `out_edge_range_t` | Type of the range returned by `out_edges(g, u)` | `vertex` | +| `out_edge_iterator_t` | `std::ranges::iterator_t>` | `vertex` | +| `out_edge_t` | `std::ranges::range_value_t>` | `vertex` | + +> **Convenience aliases:** `vertex_edge_range_t` = `out_edge_range_t`, `vertex_edge_iterator_t` = `out_edge_iterator_t`, `edge_t` = `out_edge_t`. The old names remain available. + +### Incoming Edge Type Aliases + +Available on graphs satisfying `bidirectional_adjacency_list` (e.g., +`dynamic_graph` with `Bidirectional = true`, or `undirected_adjacency_list`). + +| Alias | Definition | Required Concept | +|-------|-----------|-----------------| +| `in_edge_range_t` | Type of the range returned by `in_edges(g, u)` | `bidirectional_adjacency_list` | +| `in_edge_iterator_t` | `std::ranges::iterator_t>` | `bidirectional_adjacency_list` | +| `in_edge_t` | `std::ranges::range_value_t>` | `bidirectional_adjacency_list` | ### Usage @@ -31,7 +44,7 @@ concepts. template void example(G& g) { using VId = graph::vertex_id_t; // e.g. size_t - using Edge = graph::edge_t; // edge value type + using Edge = graph::edge_t; // edge value type for (auto& u : graph::vertices(g)) { VId uid = graph::vertex_id(g, std::ranges::begin(graph::vertices(g))); @@ -95,9 +108,13 @@ vertices(g) ──→ vertex_range_t vertex_id(g, ui) ──→ vertex_id_t -edges(g, u) ──→ vertex_edge_range_t - ├── iterator_t ──→ vertex_edge_iterator_t - └── range_value_t ──→ edge_t +edges(g, u) ──→ vertex_edge_range_t (primary: out_edge_range_t) + ├── iterator_t ──→ vertex_edge_iterator_t (primary: out_edge_iterator_t) + └── range_value_t ──→ edge_t (primary: out_edge_t) + +in_edges(g, u) ──→ in_edge_range_t (bidirectional graphs only) + ├── iterator_t ──→ in_edge_iterator_t + └── range_value_t ──→ in_edge_t ``` --- diff --git a/docs/status/coverage.md b/docs/status/coverage.md index 9872b8d..31c1bad 100644 --- a/docs/status/coverage.md +++ b/docs/status/coverage.md @@ -1,9 +1,9 @@ # Code Coverage Report -> **Generated:** 2026-02-21 | **Compiler:** GCC 15.1.0 | **Preset:** `linux-gcc-coverage` -> **Tests:** 4261 passed, 0 failed (100% pass rate) -> **Overall line coverage:** 95.8% (3300 / 3446 lines) -> **Overall function coverage:** 92.0% (27193 / 29567 functions) +> **Generated:** 2026-02-23 | **Compiler:** GCC 15.1.0 | **Preset:** `linux-gcc-coverage` +> **Tests:** 4343 passed, 0 failed (100% pass rate) +> **Overall line coverage:** 96.3% (3624 / 3764 lines) +> **Overall function coverage:** 91.8% (29030 / 31639 functions) --- @@ -11,12 +11,12 @@ | Category | Lines Hit | Lines Total | Line Coverage | |----------|-----------|-------------|---------------| -| Adjacency list infrastructure | 321 | 321 | **100.0%** | -| Algorithms | 739 | 788 | **93.8%** | -| Containers | 902 | 962 | **93.8%** | -| Detail / CPOs | 21 | 21 | **100.0%** | +| Adjacency list infrastructure | 375 | 375 | **100.0%** | +| Algorithms | 795 | 833 | **95.4%** | +| Containers | 938 | 1000 | **93.8%** | +| Detail / CPOs | 24 | 24 | **100.0%** | | Edge list | 15 | 16 | **93.8%** | -| Views | 1302 | 1336 | **97.5%** | +| Views | 1477 | 1514 | **97.6%** | --- @@ -29,11 +29,11 @@ All adjacency list descriptor and CPO support headers reach **100% line coverage | File | Lines Hit / Total | Line % | Funcs Hit / Total | Func % | |------|-------------------|--------|-------------------|--------| | `descriptor_traits.hpp` | 10 / 10 | 100.0% | 8 / 8 | 100.0% | -| `detail/graph_cpo.hpp` | 135 / 135 | 100.0% | 4250 / 4253 | 99.9% | -| `edge_descriptor.hpp` | 74 / 74 | 100.0% | 1205 / 1205 | 100.0% | -| `edge_descriptor_view.hpp` | 37 / 37 | 100.0% | 2291 / 2292 | 100.0% | -| `vertex_descriptor.hpp` | 35 / 35 | 100.0% | 1305 / 1306 | 99.9% | -| `vertex_descriptor_view.hpp` | 30 / 30 | 100.0% | 2321 / 2390 | 97.1% | +| `detail/graph_cpo.hpp` | 179 / 179 | 100.0% | 4500 / 4503 | 99.9% | +| `edge_descriptor.hpp` | 84 / 84 | 100.0% | 1293 / 1293 | 100.0% | +| `edge_descriptor_view.hpp` | 37 / 37 | 100.0% | 2453 / 2454 | 100.0% | +| `vertex_descriptor.hpp` | 35 / 35 | 100.0% | 1379 / 1380 | 99.9% | +| `vertex_descriptor_view.hpp` | 30 / 30 | 100.0% | 2410 / 2481 | 97.1% | ### Algorithms (`algorithm/`) @@ -43,9 +43,9 @@ All adjacency list descriptor and CPO support headers reach **100% line coverage | `bellman_ford_shortest_paths.hpp` | 54 / 63 | 85.7% | 31 / 31 | 100.0% | | `biconnected_components.hpp` | 68 / 68 | 100.0% | 4 / 4 | 100.0% | | `breadth_first_search.hpp` | 29 / 29 | 100.0% | 12 / 12 | 100.0% | -| `connected_components.hpp` | 123 / 137 | 89.8% | 10 / 10 | 100.0% | +| `connected_components.hpp` | 168 / 182 | 92.3% | 18 / 18 | 100.0% | | `depth_first_search.hpp` | 38 / 38 | 100.0% | 5 / 5 | 100.0% | -| `dijkstra_shortest_paths.hpp` | 54 / 65 | 83.1% | 32 / 36 | 88.9% | +| `dijkstra_shortest_paths.hpp` | 65 / 65 | 100.0% | 36 / 36 | 100.0% | | `jaccard.hpp` | 24 / 24 | 100.0% | 5 / 5 | 100.0% | | `label_propagation.hpp` | 66 / 69 | 95.7% | 3 / 3 | 100.0% | | `mis.hpp` | 25 / 28 | 89.3% | 1 / 1 | 100.0% | @@ -59,30 +59,32 @@ All adjacency list descriptor and CPO support headers reach **100% line coverage | File | Lines Hit / Total | Line % | Funcs Hit / Total | Func % | |------|-------------------|--------|-------------------|--------| | `compressed_graph.hpp` | 198 / 223 | 88.8% | 580 / 606 | 95.7% | -| `container_utility.hpp` | 6 / 6 | 100.0% | 382 / 382 | 100.0% | +| `container_utility.hpp` | 6 / 6 | 100.0% | 393 / 393 | 100.0% | | `detail/undirected_adjacency_list_impl.hpp` | 312 / 321 | 97.2% | 158 / 163 | 96.9% | -| `dynamic_graph.hpp` | 286 / 311 | 92.0% | 11429 / 13626 | 83.9% | -| `undirected_adjacency_list.hpp` | 100 / 101 | 99.0% | 126 / 128 | 98.4% | +| `dynamic_graph.hpp` | 316 / 343 | 92.1% | 11986 / 14415 | 83.1% | +| `undirected_adjacency_list.hpp` | 106 / 107 | 99.1% | 132 / 134 | 98.5% | ### Views (`views/`) | File | Lines Hit / Total | Line % | Funcs Hit / Total | Func % | |------|-------------------|--------|-------------------|--------| -| `adaptors.hpp` | 109 / 109 | 100.0% | 85 / 85 | 100.0% | -| `bfs.hpp` | 211 / 227 | 93.0% | 146 / 157 | 93.0% | -| `dfs.hpp` | 225 / 239 | 94.1% | 257 / 268 | 95.9% | +| `adaptors.hpp` | 146 / 146 | 100.0% | 113 / 113 | 100.0% | +| `bfs.hpp` | 218 / 234 | 93.2% | 193 / 204 | 94.6% | +| `dfs.hpp` | 232 / 246 | 94.3% | 316 / 327 | 96.6% | +| `edge_accessor.hpp` | 12 / 12 | 100.0% | 55 / 55 | 100.0% | | `edgelist.hpp` | 219 / 221 | 99.1% | 556 / 578 | 96.2% | -| `incidence.hpp` | 89 / 89 | 100.0% | 463 / 469 | 98.7% | -| `neighbors.hpp` | 102 / 102 | 100.0% | 282 / 286 | 98.6% | +| `incidence.hpp` | 124 / 124 | 100.0% | 613 / 619 | 99.0% | +| `neighbors.hpp` | 134 / 134 | 100.0% | 386 / 390 | 99.0% | | `search_base.hpp` | 5 / 5 | 100.0% | 11 / 11 | 100.0% | -| `topological_sort.hpp` | 230 / 232 | 99.1% | 248 / 256 | 96.9% | -| `vertexlist.hpp` | 112 / 112 | 100.0% | 467 / 469 | 99.6% | +| `topological_sort.hpp` | 252 / 257 | 98.1% | 296 / 305 | 97.0% | +| `transpose.hpp` | 23 / 23 | 100.0% | 22 / 22 | 100.0% | +| `vertexlist.hpp` | 112 / 112 | 100.0% | 507 / 509 | 99.6% | ### Other | File | Lines Hit / Total | Line % | Funcs Hit / Total | Func % | |------|-------------------|--------|-------------------|--------| -| `detail/edge_cpo.hpp` | 21 / 21 | 100.0% | 461 / 461 | 100.0% | +| `detail/edge_cpo.hpp` | 24 / 24 | 100.0% | 500 / 500 | 100.0% | | `edge_list/edge_list_descriptor.hpp` | 15 / 16 | 93.8% | 21 / 21 | 100.0% | | `graph_info.hpp` | 0 / 2 | 0.0% | 0 / 2 | 0.0% | @@ -95,11 +97,9 @@ Files below 90% line coverage, ranked by gap size: | File | Line % | Lines Missed | Notes | |------|--------|--------------|-------| | `graph_info.hpp` | 0.0% | 2 | Metadata-only header; no executable paths exercised | -| `dijkstra_shortest_paths.hpp` | 83.1% | 11 | Some overload / error-path branches not reached | | `bellman_ford_shortest_paths.hpp` | 85.7% | 9 | Negative-cycle detection path not fully exercised | | `compressed_graph.hpp` | 88.8% | 25 | Some construction / accessor overloads untested | -| `connected_components.hpp` | 89.8% | 14 | Sparse-graph and edge-case branches | -| `mis.hpp` | 89.3% | 3 | Minor branch not reached | +| `mis.hpp` | 89.3% | 3 | Some branches not fully exercised | --- diff --git a/docs/status/implementation_matrix.md b/docs/status/implementation_matrix.md index 6e6d985..8b0dae8 100644 --- a/docs/status/implementation_matrix.md +++ b/docs/status/implementation_matrix.md @@ -16,7 +16,7 @@ | Breadth-first search | `breadth_first_search.hpp` | `test_breadth_first_search.cpp` | Implemented | | Depth-first search | `depth_first_search.hpp` | `test_depth_first_search.cpp` | Implemented | | Topological sort | `topological_sort.hpp` | `test_topological_sort.cpp` | Implemented | -| Connected components | `connected_components.hpp` | `test_connected_components.cpp` | Implemented | +| Connected components (Kosaraju SCC) | `connected_components.hpp` | `test_connected_components.cpp`, `test_scc_bidirectional.cpp` | Implemented | | Articulation points | `articulation_points.hpp` | `test_articulation_points.cpp` | Implemented | | Biconnected components | `biconnected_components.hpp` | `test_biconnected_components.cpp` | Implemented | | MST (Prim / Kruskal) | `mst.hpp` | `test_mst.cpp` | Implemented | @@ -29,19 +29,20 @@ ## Views -7 user-facing views in `include/graph/views/` (excluding infrastructure headers): +10 user-facing views in `include/graph/views/` (excluding infrastructure headers): | View | Header | Category | |------|--------|----------| | vertexlist | `vertexlist.hpp` | Basic | | edgelist | `edgelist.hpp` | Basic | -| incidence | `incidence.hpp` | Basic | -| neighbors | `neighbors.hpp` | Basic | +| incidence / out_incidence / in_incidence | `incidence.hpp` | Basic | +| neighbors / in_neighbors | `neighbors.hpp` | Basic | | BFS (vertices + edges) | `bfs.hpp` | Search | | DFS (vertices + edges) | `dfs.hpp` | Search | | Topological sort (vertices + edges) | `topological_sort.hpp` | Search | +| Transpose adaptor | `transpose.hpp` | Adaptor | -Infrastructure headers (not user views): `view_concepts.hpp`, `adaptors.hpp`, `basic_views.hpp`, `search_base.hpp`. +Infrastructure headers (not user views): `view_concepts.hpp`, `adaptors.hpp`, `basic_views.hpp`, `search_base.hpp`, `edge_accessor.hpp`. --- @@ -61,7 +62,7 @@ Utility header: `container_utility.hpp`. ## `dynamic_graph` Trait Combinations -26 combinations in `include/graph/container/traits/`: +27 combinations in `include/graph/container/traits/`: ### Vertex containers @@ -135,7 +136,7 @@ Naming convention: `{vertex}o{edge}_graph_traits.hpp` | Namespace | Purpose | |-----------|---------| -| `graph::` | Root — re-exports `adj_list` types/CPOs for convenience | +| `graph::` | Root — common edge list CPOs, re-exports `adj_list` types/CPOs for convenience | | `graph::adj_list::` | Adjacency list CPOs, descriptors, concepts, traits | | `graph::edge_list::` | Edge list concepts, traits, descriptors | | `graph::views::` | Graph views (vertexlist, edgelist, neighbors, BFS, DFS, etc.) | @@ -150,6 +151,3 @@ Naming convention: `{vertex}o{edge}_graph_traits.hpp` find_package(graph3 REQUIRED) target_link_libraries(your_target PRIVATE graph::graph3) ``` - -> **Note:** The README (as of Phase 0) incorrectly says `graph::graph`. The actual exported target -> is `graph::graph3`, as defined in `cmake/InstallConfig.cmake`. This will be fixed in Phase 2. diff --git a/docs/status/metrics.md b/docs/status/metrics.md index 25551b5..319446c 100644 --- a/docs/status/metrics.md +++ b/docs/status/metrics.md @@ -8,13 +8,13 @@ | Metric | Value | Source | |--------|-------|--------| | Algorithms | 13 | `include/graph/algorithm/` (excluding `traversal_common.hpp`) | -| Views | 7 | vertexlist, edgelist, incidence, neighbors, bfs, dfs, topological_sort | +| Views | 10 | vertexlist, edgelist, incidence, in_incidence, neighbors, in_neighbors, bfs, dfs, topological_sort, transpose | | Containers | 3 | `dynamic_graph`, `compressed_graph`, `undirected_adjacency_list` | -| Trait combinations | 26 | `include/graph/container/traits/` | -| Test count | 4261 | `ctest --preset linux-gcc-debug` (100% pass, 2026-02-19) | +| Trait combinations | 27 | `include/graph/container/traits/` | +| Test count | 4405 | `ctest --preset linux-gcc-debug` (100% pass, 2026-02-23) | | C++ standard | C++20 | `CMakeLists.txt` line 26 | -| Line coverage | 95.8% (3300 / 3446) | `docs/status/coverage.md` (2026-02-21) | -| Function coverage | 92.0% (27193 / 29567) | `docs/status/coverage.md` (2026-02-21) | +| Line coverage | 96.0% (3613 / 3764) | `docs/status/coverage.md` (2026-02-22) | +| Function coverage | 91.8% (29030 / 31639) | `docs/status/coverage.md` (2026-02-22) | | License | BSL-1.0 | `LICENSE` | | CMake minimum | 3.20 | `CMakeLists.txt` line 1 | | Consumer target | `graph::graph3` | `cmake/InstallConfig.cmake` (`find_package(graph3)`) | diff --git a/docs/user-guide/adjacency-lists.md b/docs/user-guide/adjacency-lists.md index 2d6e1f6..8686634 100644 --- a/docs/user-guide/adjacency-lists.md +++ b/docs/user-guide/adjacency-lists.md @@ -42,7 +42,7 @@ graph-v3 defines 9 concepts in `graph::adj_list` (re-exported into `graph::`). | Concept | Description | |---------|-------------| | `edge` | Edge descriptor with `source_id`, `source`, `target_id`, and `target` | -| `vertex_edge_range` | Forward range whose elements satisfy `edge` | +| `out_edge_range` | Forward range whose elements satisfy `edge` | ### Vertex concepts @@ -56,7 +56,7 @@ graph-v3 defines 9 concepts in `graph::adj_list` (re-exported into `graph::`). | Concept | Description | |---------|-------------| -| `adjacency_list` | `vertices(g)` returns a `vertex_range`; `edges(g, u)` returns a `vertex_edge_range` | +| `adjacency_list` | `vertices(g)` returns a `vertex_range`; `edges(g, u)` returns an `out_edge_range` | | `index_adjacency_list` | An `adjacency_list` whose vertex range is also an `index_vertex_range` (O(1) vertex lookup) | | `ordered_vertex_edges` | An `adjacency_list` whose edge ranges are sorted by `target_id` (enables algorithms like triangle counting) | @@ -90,9 +90,9 @@ three tiers: custom member → ADL free function → built-in default. | CPO | Description | |-----|-------------| -| `find_vertex_edge(g, u, v)` | Find edge from vertex `u` to vertex `v` | +| `find_vertex_edge(g, u, v)` | Find outgoing edge from vertex `u` to vertex `v` | | `contains_edge(g, u, v)` | Returns `true` if edge u→v exists | -| `has_edge(g, uid, vid)` | Returns `true` if edge uid→vid exists | +| `has_edges(g)` | Returns `true` if graph has at least one edge | ### Partition CPOs @@ -296,9 +296,9 @@ Convenience aliases extracted from a graph type `G`: | `vertex_iterator_t` | Iterator type of the vertex range | | `vertex_t` | Value type of the vertex range (a vertex descriptor) | | `vertex_id_t` | Vertex ID type (`size_t` for indexed, key type for map-based) | -| `vertex_edge_range_t` | Return type of `edges(g, u)` (an `edge_descriptor_view`)| -| `vertex_edge_iterator_t` | Iterator type of the edge range | -| `edge_t` | Value type of the edge range (an edge descriptor) | +| `vertex_edge_range_t` | Return type of `edges(g, u)` (an `edge_descriptor_view`). Primary: `out_edge_range_t` | +| `vertex_edge_iterator_t` | Iterator type of the edge range. Primary: `out_edge_iterator_t` | +| `edge_t` | Value type of the edge range (an edge descriptor). Primary: `out_edge_t` | --- diff --git a/docs/user-guide/algorithms.md b/docs/user-guide/algorithms.md index b0bf86c..4e3da56 100644 --- a/docs/user-guide/algorithms.md +++ b/docs/user-guide/algorithms.md @@ -32,6 +32,12 @@ Algorithms follow a consistent pattern: - **Initialization**: use `init_shortest_paths()` to properly set up distance and predecessor vectors before calling shortest-path algorithms +> **Bidirectional graphs:** Algorithms that benefit from incoming-edge access +> (e.g., Kosaraju SCC, transpose graph) can use bidirectional `dynamic_graph` +> containers. Search views (BFS, DFS, topological sort) also accept an +> `in_edge_accessor` for reverse traversal — see +> [Bidirectional Access](bidirectional-access.md). + ```cpp #include @@ -99,6 +105,7 @@ All headers are under `include/graph/algorithm/`. | [BFS](algorithms/bfs.md) | Traversal | `breadth_first_search.hpp` | O(V+E) | O(V) | | [Biconnected Components](algorithms/biconnected_components.md) | Components | `biconnected_components.hpp` | O(V+E) | O(V+E) | | [Connected Components](algorithms/connected_components.md) | Components | `connected_components.hpp` | O(V+E) | O(V) | +| [Kosaraju SCC](algorithms/connected_components.md) | Components | `connected_components.hpp` | O(V+E) | O(V) | | [DFS](algorithms/dfs.md) | Traversal | `depth_first_search.hpp` | O(V+E) | O(V) | | [Dijkstra](algorithms/dijkstra.md) | Shortest Paths | `dijkstra_shortest_paths.hpp` | O((V+E) log V) | O(V) | | [Jaccard Coefficient](algorithms/jaccard.md) | Analytics | `jaccard.hpp` | O(V + E·d) | O(V+E) | @@ -164,8 +171,19 @@ full-graph, single-source, and multi-source variants. ### [Connected Components](algorithms/connected_components.md) Three algorithms in one header: `connected_components` (DFS-based, undirected), -`kosaraju` (two-pass DFS for directed SCC, requires transpose graph), and -`afforest` (union-find with neighbor sampling, parallel-friendly). +`kosaraju` (two-pass DFS for directed SCC, uses `in_edge_accessor` for the +reverse pass on bidirectional graphs), and `afforest` (union-find with neighbor +sampling, parallel-friendly). + +Kosaraju’s algorithm works with any graph that supports both outgoing and +incoming edge iteration (i.e., `bidirectional_adjacency_list` concept). +A **`transpose_graph`** view is also available for algorithms that need a +transposed adjacency structure: + +```cpp +#include +#include +``` **Time:** O(V+E) — **Space:** O(V) — **Header:** `connected_components.hpp` diff --git a/docs/user-guide/bidirectional-access.md b/docs/user-guide/bidirectional-access.md new file mode 100644 index 0000000..012e293 --- /dev/null +++ b/docs/user-guide/bidirectional-access.md @@ -0,0 +1,426 @@ +# Bidirectional Edge Access + +> [← Back to Documentation Index](../index.md) · [Views](views.md) · [Containers](containers.md) + +- [Overview](#overview) +- [Creating a Bidirectional Graph](#creating-a-bidirectional-graph) +- [Incoming Edge CPOs](#incoming-edge-cpos) +- [Incoming Edge Views](#incoming-edge-views) +- [Reverse BFS](#reverse-bfs) +- [Reverse DFS](#reverse-dfs) +- [Reverse Topological Sort](#reverse-topological-sort) +- [The Edge Accessor Pattern](#the-edge-accessor-pattern) +- [Undirected Graphs](#undirected-graphs) +- [Algorithms](#algorithms) +- [Performance Notes](#performance-notes) +- [API Reference Summary](#api-reference-summary) + +## Overview + +graph-v3 supports **bidirectional edge access** — querying both outgoing and +incoming edges of any vertex. This enables: + +- Finding all predecessors of a vertex +- Reverse BFS/DFS traversal (from sinks to sources) +- Reverse topological sort +- Strongly connected components via Kosaraju's algorithm +- Transpose graph construction + +Bidirectional access is opt-in. Graphs must explicitly store incoming-edge +lists to support the incoming-edge CPOs. Two containers provide this: + +| Container | How | +|-----------|-----| +| `dynamic_graph` | Set `Bidirectional = true` (sixth template parameter) | +| `undirected_adjacency_list` | Incoming edges = outgoing edges (symmetric) | + +--- + +## Creating a Bidirectional Graph + +### `dynamic_graph` with Bidirectional + +```cpp +#include +#include + +using namespace graph; +using namespace graph::container; + +// Key: Bidirectional = true (sixth template parameter) +// EV=void, VV=void, GV=void, VId=uint32_t, Sourced=true, Bidirectional=true +using Graph = dynamic_graph>; + +Graph g({{0, 1}, {0, 2}, {1, 3}, {2, 3}}); + +// 0 ──→ 1 +// │ │ +// ↓ ↓ +// 2 ──→ 3 +``` + +When `Bidirectional` is `true`, each vertex maintains an **incoming-edge list** +in addition to the outgoing-edge list. Adding an edge from `u` to `v` +automatically inserts an in-edge record at vertex `v`. + +> **Note:** `Sourced` must also be `true` for bidirectional graphs, because +> in-edge descriptors need to resolve their source vertex. + +### `undirected_adjacency_list` + +```cpp +#include + +using namespace graph::container; + +undirected_adjacency_list g; +// ... populate ... +``` + +For undirected graphs, every edge appears in both endpoints' edge lists, so +`in_edges(g, u)` returns the same edges as `out_edges(g, u)`. The +`bidirectional_adjacency_list` concept is automatically satisfied. + +--- + +## Incoming Edge CPOs + +Once you have a bidirectional graph, the following CPOs are available: + +```cpp +#include + +Graph g({{0, 1}, {0, 2}, {1, 3}, {2, 3}}); + +auto v3 = *find_vertex(g, 3); + +// Iterate incoming edges to vertex 3 +for (auto&& e : in_edges(g, v3)) { + std::cout << source_id(g, e) << " -> 3\n"; +} +// Output: +// 1 -> 3 +// 2 -> 3 + +// In-degree +std::cout << "in_degree(3) = " << in_degree(g, v3) << "\n"; // 2 + +// Find a specific incoming edge +auto it = find_in_edge(g, 3u, 1u); // edge from 1 to 3 +if (it != std::ranges::end(in_edges(g, v3))) { + std::cout << "Found edge from " << source_id(g, *it) << "\n"; +} + +// Check existence +bool has_edge = contains_in_edge(g, 3u, 1u); // true +``` + +| CPO | Parameters | Return | Complexity | +|-----|-----------|--------|------------| +| `in_edges(g, u)` | graph, vertex descriptor | `in_edge_range_t` | O(1) | +| `in_edges(g, uid)` | graph, vertex id | `in_edge_range_t` | O(1) | +| `in_degree(g, u)` | graph, vertex descriptor | integral | O(1) | +| `in_degree(g, uid)` | graph, vertex id | integral | O(1) | +| `find_in_edge(g, uid, vid)` | graph, target id, source id | `in_edge_iterator_t` | O(in-degree) | +| `contains_in_edge(g, uid, vid)` | graph, target id, source id | `bool` | O(in-degree) | + +--- + +## Incoming Edge Views + +The library provides `in_incidence` and `in_neighbors` views that iterate over +incoming edges, mirroring the outgoing `incidence` and `neighbors` views: + +```cpp +#include + +using namespace graph::views; + +// Incoming edges to vertex 3, with structured bindings +for (auto [sid, uv] : in_incidence(g, 3)) { + std::cout << "Edge from " << sid << "\n"; +} + +// Predecessor vertices of vertex 3 +for (auto [sid, n] : in_neighbors(g, 3)) { + std::cout << "Predecessor: " << sid << "\n"; +} + +// With value function +auto evf = [](const auto& g, auto& uv) { return edge_value(g, uv); }; +for (auto [sid, uv, w] : in_incidence(g, 3, evf)) { + std::cout << sid << " weight " << w << "\n"; +} +``` + +### Pipe syntax + +```cpp +using namespace graph::views::adaptors; + +auto view = g | in_incidence(3); +auto view = g | in_neighbors(3); +``` + +--- + +## Reverse BFS + +Pass `in_edge_accessor` as the first explicit template argument to any BFS +view factory to traverse **incoming** edges: + +```cpp +#include +#include + +using namespace graph::views; + +// Forward BFS from vertex 0 (default — outgoing edges) +std::cout << "Forward BFS from 0: "; +for (auto [v] : vertices_bfs(g, 0)) { + std::cout << vertex_id(g, v) << " "; +} +// Output: 1 2 3 + +// Reverse BFS from vertex 3 (incoming edges) +std::cout << "\nReverse BFS from 3: "; +for (auto [v] : vertices_bfs(g, 3)) { + std::cout << vertex_id(g, v) << " "; +} +// Output: 1 2 0 +``` + +**Edge variant:** + +```cpp +for (auto [uv] : edges_bfs(g, 3)) { + std::cout << source_id(g, uv) << " <- " << target_id(g, uv) << "\n"; +} +``` + +**With value function:** + +```cpp +auto vvf = [](const auto& g, auto& v) { return vertex_id(g, v); }; +for (auto [v, id] : vertices_bfs(g, 3, vvf)) { + std::cout << "Visited vertex " << id << "\n"; +} +``` + +--- + +## Reverse DFS + +The same pattern works for DFS: + +```cpp +#include +#include + +using namespace graph::views; + +// Reverse DFS from vertex 3 +for (auto [v] : vertices_dfs(g, 3)) { + std::cout << vertex_id(g, v) << " "; +} + +// Reverse DFS edges +for (auto [uv] : edges_dfs(g, 3)) { + std::cout << source_id(g, uv) << " <- " << target_id(g, uv) << "\n"; +} +``` + +--- + +## Reverse Topological Sort + +Topological sort views also accept an Accessor: + +```cpp +#include +#include + +using namespace graph::views; + +// Standard topological sort (outgoing edges) +for (auto [v] : vertices_topological_sort(g)) { + std::cout << vertex_id(g, v) << " "; +} + +// Reverse topological sort (incoming edges) +for (auto [v] : vertices_topological_sort(g)) { + std::cout << vertex_id(g, v) << " "; +} +``` + +**Safe variant with cycle detection:** + +```cpp +auto result = vertices_topological_sort_safe(g); +if (result) { + for (auto [v] : *result) { + std::cout << vertex_id(g, v) << " "; + } +} else { + std::cerr << "Cycle detected at vertex " << result.error() << "\n"; +} +``` + +--- + +## The Edge Accessor Pattern + +The `out_edge_accessor` and `in_edge_accessor` are **stateless policy structs** +that bundle three operations: + +| Operation | `out_edge_accessor` | `in_edge_accessor` | +|-----------|--------------------|--------------------| +| `edges(g, u)` | `out_edges(g, u)` | `in_edges(g, u)` | +| `neighbor_id(g, e)` | `target_id(g, e)` | `source_id(g, e)` | +| `neighbor(g, e)` | `target(g, e)` | `source(g, e)` | + +Each accessor also exposes type aliases: + +```cpp +template +using edge_range_t = ...; // out_edge_range_t or in_edge_range_t + +template +using edge_t = ...; // out_edge_t or in_edge_t +``` + +**Header:** `` + +**Key design properties:** + +- **Zero-cost** — accessors are stateless (`sizeof == 1`, stored with + `[[no_unique_address]]`) and instantiated inline as `Accessor{}`. +- **Source-compatible** — all view classes default `Accessor = out_edge_accessor`, + so existing code compiles unchanged. +- **Deducible** — pass the Accessor as the first explicit template argument: + `vertices_bfs(g, seed)`. The graph type `G` is deduced. + +--- + +## Undirected Graphs + +`undirected_adjacency_list` automatically satisfies `bidirectional_adjacency_list` +because every edge appears at both endpoints. All incoming-edge CPOs and +reverse traversal views work without any special setup: + +```cpp +#include +#include +#include + +undirected_adjacency_list g; +// ... populate ... + +// These are equivalent for undirected graphs +for (auto [v] : views::vertices_bfs(g, 0)) { ... } +for (auto [v] : views::vertices_bfs(g, 0)) { ... } +``` + +--- + +## Algorithms + +### Kosaraju's Strongly Connected Components + +Kosaraju's algorithm finds strongly connected components in directed graphs +using two DFS passes — the second pass traverses edges in reverse. With +bidirectional graphs, this operates efficiently using `in_edge_accessor`: + +```cpp +#include + +std::vector component(num_vertices(g)); +size_t num_components = kosaraju(g, component); +``` + +### Transpose Graph + +A `transpose_graph` view provides a transposed adjacency structure where +outgoing edges become incoming and vice versa: + +```cpp +#include + +auto gt = transpose_graph(g); +// gt.out_edges(v) returns g.in_edges(v) +``` + +--- + +## Performance Notes + +| Operation | Bidirectional Cost | Non-Bidirectional Cost | +|-----------|-------------------|----------------------| +| Memory per edge | ~2× (out-edge + in-edge records) | 1× | +| Edge insertion | O(1) amortized (two list inserts) | O(1) amortized | +| `in_edges(g, u)` | O(1) | Not available | +| `in_degree(g, u)` | O(1) | Not available | +| `find_in_edge(g, uid, vid)` | O(in-degree) | Not available | +| Reverse BFS/DFS | Same as forward | Not available | + +The Accessor parameter is compiled away entirely — `Accessor{}` is stateless and +all calls are resolved at compile time via `if constexpr` or inlining. There is +**zero runtime overhead** compared to directly calling the underlying CPOs. + +--- + +## API Reference Summary + +### CPOs + +| CPO | Header | Description | +|-----|--------|-------------| +| `in_edges(g, u)` | `` | Incoming edge range | +| `in_degree(g, u)` | `` | Number of incoming edges | +| `find_in_edge(g, uid, vid)` | `` | Find specific incoming edge | +| `contains_in_edge(g, uid, vid)` | `` | Test incoming edge existence | + +### Views + +| View | Header | Description | +|------|--------|-------------| +| `in_incidence(g, uid)` | `` | Incoming edges with structured bindings | +| `in_neighbors(g, uid)` | `` | Predecessor vertices | +| `vertices_bfs(g, seed)` | `` | Reverse BFS | +| `edges_bfs(g, seed)` | `` | Reverse BFS edges | +| `vertices_dfs(g, seed)` | `` | Reverse DFS | +| `edges_dfs(g, seed)` | `` | Reverse DFS edges | +| `vertices_topological_sort(g)` | `` | Reverse topo sort | + +### Concepts + +| Concept | Header | Description | +|---------|--------|-------------| +| `bidirectional_adjacency_list` | `` | Graph supports incoming edges | + +### Type Aliases + +| Alias | Description | +|-------|-------------| +| `in_edge_range_t` | Range type of `in_edges(g, u)` | +| `in_edge_iterator_t` | Iterator type for incoming edges | +| `in_edge_t` | Edge descriptor type for incoming edges | + +### Edge Accessors + +| Accessor | Header | Description | +|----------|--------|-------------| +| `out_edge_accessor` | `` | Default — outgoing edges | +| `in_edge_accessor` | `` | Incoming edges | + +--- + +## See Also + +- [Views User Guide](views.md) — all graph views including `in_incidence`, `in_neighbors` +- [Containers User Guide](containers.md) — `dynamic_graph` with `Bidirectional` parameter +- [Concepts Reference](../reference/concepts.md) — `bidirectional_adjacency_list` +- [CPO Reference](../reference/cpo-reference.md) — incoming edge CPO signatures +- [Type Aliases Reference](../reference/type-aliases.md) — `in_edge_*` type aliases +- [Getting Started](../getting-started.md) — quick introduction to incoming edges diff --git a/docs/user-guide/containers.md b/docs/user-guide/containers.md index 9d086e2..1fc4a1f 100644 --- a/docs/user-guide/containers.md +++ b/docs/user-guide/containers.md @@ -33,6 +33,7 @@ template > class dynamic_graph; } @@ -43,6 +44,12 @@ determined entirely by the **Traits** parameter, which names the concrete `vertices_type` and `edges_type` (e.g., `std::vector` and `std::forward_list`). +When **`Bidirectional`** is `true`, each vertex maintains an additional +incoming-edge list, enabling the `in_edges`, `in_degree`, `find_in_edge`, and +`contains_in_edge` CPOs in O(1) / O(in-degree). This is required by the +`bidirectional_adjacency_list` concept and enables reverse traversal via +`in_edge_accessor`. + ### Properties | Property | Value | @@ -80,7 +87,7 @@ Complexity depends on the vertex container, the edge container, or neither. |-----------|------------| | `num_vertices(g)` | O(1) | | `num_edges(g)` | O(1) | -| `has_edge(g)` | O(1) | +| `has_edges(g)` | O(1) | ### Template parameters @@ -91,6 +98,7 @@ Complexity depends on the vertex container, the edge container, or neither. | `GV` | `void` | Graph value type (`void` → no graph value) | | `VId` | `uint32_t` | Vertex ID type (integral for indexed traits, any ordered/hashable type for map-based traits) | | `Sourced` | `false` | When `true`, each edge stores a source vertex ID. This does not affect the ability to use `source_id(g,uv)`. | +| `Bidirectional` | `false` | When `true`, each vertex maintains an incoming-edge list, enabling `in_edges(g,u)`, `in_degree(g,u)`, `find_in_edge(g,...)`, and `contains_in_edge(g,...)`. Satisfies `bidirectional_adjacency_list`. | | `Traits` | `vofl_graph_traits<…>` | Trait struct that defines the vertex and edge container types | ### Quick usage diff --git a/docs/user-guide/views.md b/docs/user-guide/views.md index b56539c..973aa5d 100644 --- a/docs/user-guide/views.md +++ b/docs/user-guide/views.md @@ -5,8 +5,10 @@ - [Overview](#overview) - [Quick Start](#quick-start) - [Basic Views](#basic-views) — vertexlist, incidence, neighbors, edgelist +- [Incoming Edge Views](#incoming-edge-views) — in_incidence, in_neighbors - [Simplified Views (basic\_)](#simplified-views-basic_) — id-only variants - [Search Views](#search-views) — DFS, BFS, topological sort +- [Reverse Search (Accessor Parameter)](#reverse-search-accessor-parameter) — BFS/DFS/topo over incoming edges - [Range Adaptor Syntax](#range-adaptor-syntax) - [Value Functions](#value-functions) - [Chaining with std::views](#chaining-with-stdviews) @@ -194,6 +196,85 @@ for (auto [uid, vid, uv, w] : views::edgelist(g, evf)) { } ``` +## Incoming Edge Views + +For graphs that support bidirectional edge access (e.g., `dynamic_graph` with +`Bidirectional = true`, or `undirected_adjacency_list`), the library provides +incoming-edge view variants. + +### in_incidence + +Iterates over **incoming** edges to a specific vertex. + +**Signature**: +```cpp +auto in_incidence(G&& g, UID uid); +auto in_incidence(G&& g, UID uid, EVF&& evf); // with value function +``` + +**Structured Bindings**: + +| Variant | Binding | +|---------|---------| +| `in_incidence(g, uid)` | `[sid, uv]` | +| `in_incidence(g, uid, evf)` | `[sid, uv, val]` | + +**Example**: +```cpp +// Incoming edges to vertex 3 +for (auto [sid, uv] : views::in_incidence(g, 3)) { + std::cout << "Edge from " << sid << "\n"; +} + +// With value function +auto evf = [](const auto& g, auto& uv) { return edge_value(g, uv); }; +for (auto [sid, uv, w] : views::in_incidence(g, 3, evf)) { + std::cout << "Edge from " << sid << " weight " << w << "\n"; +} +``` + +**Pipe syntax**: +```cpp +using namespace graph::views::adaptors; +auto view = g | in_incidence(3); +``` + +--- + +### in_neighbors + +Iterates over vertices that have edges **to** a specific vertex (predecessor +vertices). + +**Signature**: +```cpp +auto in_neighbors(G&& g, UID uid); +auto in_neighbors(G&& g, UID uid, VVF&& vvf); // with value function +``` + +**Structured Bindings**: + +| Variant | Binding | +|---------|---------| +| `in_neighbors(g, uid)` | `[sid, n]` | +| `in_neighbors(g, uid, vvf)` | `[sid, n, val]` | + +**Example**: +```cpp +// Predecessors of vertex 3 +for (auto [sid, n] : views::in_neighbors(g, 3)) { + std::cout << "Predecessor: " << sid << "\n"; +} +``` + +**Pipe syntax**: +```cpp +using namespace graph::views::adaptors; +auto view = g | in_neighbors(3); +``` + +--- + ## Simplified Views (basic\_) Each basic view has a `basic_` variant that returns **ids only** (no vertex/edge descriptors). @@ -378,6 +459,68 @@ if (result) { } ``` +## Reverse Search (Accessor Parameter) + +All search views (BFS, DFS, topological sort) accept an **Accessor** template +parameter that controls which edges are traversed. By default, views use +`out_edge_accessor` (outgoing edges). Pass `in_edge_accessor` as the first +explicit template argument to traverse **incoming** edges instead. + +This requires a graph that supports bidirectional edge access. + +**Header**: `` + +| Accessor | `edges()` | `neighbor_id()` | `neighbor()` | +|----------|-----------|------------------|--------------| +| `out_edge_accessor` (default) | `out_edges(g, u)` | `target_id(g, e)` | `target(g, e)` | +| `in_edge_accessor` | `in_edges(g, u)` | `source_id(g, e)` | `source(g, e)` | + +**Reverse BFS**: +```cpp +#include +#include + +using namespace graph::views; + +// Forward BFS (default) +for (auto [v] : vertices_bfs(g, 0)) { ... } + +// Reverse BFS — follow incoming edges +for (auto [v] : vertices_bfs(g, 3)) { ... } + +// Reverse BFS with value function +auto vvf = [](const auto& g, auto& v) { return vertex_id(g, v); }; +for (auto [v, id] : vertices_bfs(g, 3, vvf)) { ... } +``` + +**Reverse DFS**: +```cpp +#include + +for (auto [v] : vertices_dfs(g, 3)) { ... } +for (auto [uv] : edges_dfs(g, 3)) { ... } +``` + +**Reverse Topological Sort**: +```cpp +#include + +for (auto [v] : vertices_topological_sort(g)) { ... } + +// Safe variant with cycle detection +auto result = vertices_topological_sort_safe(g); +if (result) { + for (auto [v] : *result) { ... } +} +``` + +**Key points**: +- The Accessor is a **zero-cost abstraction** — it is stateless and compiles away entirely. +- Existing code is **source-compatible** — the default `out_edge_accessor` matches prior behavior. +- All value function and allocator overloads work with both accessors. + +--- + ## Range Adaptor Syntax All views support both direct call and pipe syntax: @@ -533,6 +676,8 @@ auto dfs = g | vertices_dfs(0); // Allocates visited tracker | edges_bfs | O(V) | Visited tracker + queue | | vertices_topological_sort | O(V) | Visited tracker + result | | edges_topological_sort | O(V) | Visited tracker + result | +| in_incidence | O(1) | References graph | +| in_neighbors | O(1) | References graph | ### Optimization Tips @@ -782,6 +927,7 @@ for (auto [uid, u] : views::vertexlist(g)) { > iteration is safe — only structural changes (adding/removing vertices or edges) are prohibited. - [Adjacency Lists User Guide](adjacency-lists.md) — concepts, CPOs, descriptors +- [Bidirectional Access](bidirectional-access.md) — incoming edges, reverse traversal, `in_edge_accessor` - [Containers User Guide](containers.md) — graph container options - [Edge Lists User Guide](edge-lists.md) — edge list input for graph construction - [CPO Reference](../reference/cpo-reference.md) — customization point objects diff --git a/examples/dijkstra_clrs_example.cpp b/examples/dijkstra_clrs_example.cpp index 9c1e0da..d6b4c45 100644 --- a/examples/dijkstra_clrs_example.cpp +++ b/examples/dijkstra_clrs_example.cpp @@ -25,7 +25,7 @@ using Graph = container::dynamic_graph>; + container::vol_graph_traits>; void print_path(const vector& predecessor, unsigned source, unsigned target) { if (target == source) { diff --git a/include/graph/adj_list/adjacency_list_concepts.hpp b/include/graph/adj_list/adjacency_list_concepts.hpp index 9437034..b30e706 100644 --- a/include/graph/adj_list/adjacency_list_concepts.hpp +++ b/include/graph/adj_list/adjacency_list_concepts.hpp @@ -54,9 +54,9 @@ concept edge = is_edge_descriptor_v> && requires(G& g, co // ============================================================================= /** - * @brief Concept for a forward range of edges + * @brief Concept for a forward range of outgoing edges * - * A vertex_edge_range is a range where each element satisfies the edge concept. + * An out_edge_range is a range where each element satisfies the edge concept. * This is used to represent the outgoing edges from a vertex in an adjacency list, * or all edges in an edge list. * @@ -73,7 +73,7 @@ concept edge = is_edge_descriptor_v> && requires(G& g, co * @tparam G Graph type */ template -concept vertex_edge_range = std::ranges::forward_range && edge>; +concept out_edge_range = std::ranges::forward_range && edge>; // ============================================================================= // Vertex Concepts @@ -178,11 +178,11 @@ concept index_vertex_range = * * An adjacency_list is a graph where: * - Vertices can be iterated as a vertex_range (forward) - * - Each vertex has outgoing edges as a vertex_edge_range + * - Each vertex has outgoing edges as an out_edge_range * * Requirements: * - vertices(g) returns a vertex_range - * - edges(g, u) returns a vertex_edge_range for vertex u + * - out_edges(g, u) returns an out_edge_range for vertex u * - Supports vertex_id(g, u) for each vertex * - Supports source_id(g, e), source(g, e), target_id(g, e), and target(g, e) for each edge * @@ -196,7 +196,7 @@ concept index_vertex_range = template concept adjacency_list = requires(G& g, vertex_t u) { { vertices(g) } -> vertex_range; - { edges(g, u) } -> vertex_edge_range; + { out_edges(g, u) } -> out_edge_range; }; /** @@ -251,8 +251,74 @@ concept index_adjacency_list = adjacency_list && index_vertex_range; */ template concept ordered_vertex_edges = adjacency_list && requires(G& g, vertex_t u) { - requires std::forward_iterator; + requires std::forward_iterator; }; +// ============================================================================= +// Incoming Edge Concepts +// ============================================================================= + +/** + * @brief Concept for a forward range of incoming edges + * + * An in_edge_range is a range where each element satisfies the edge concept. + * This is used to represent incoming edges to a vertex in a bidirectional graph. + * The incoming edge descriptor type (in_edge_t) may differ from the outgoing + * edge descriptor type (edge_t), but both must support the full edge interface + * (source_id, source, target_id, target). + * + * Requirements: + * - Must be a std::ranges::forward_range + * - Range value type must satisfy the edge concept + * + * @tparam R Range type + * @tparam G Graph type + */ +template +concept in_edge_range = std::ranges::forward_range && edge>; + +/** + * @brief Concept for graphs with bidirectional adjacency list structure + * + * A bidirectional_adjacency_list is an adjacency_list that also provides + * access to incoming edges for each vertex. This enables traversing the + * graph in reverse (from targets back to sources). + * + * Requirements: + * - Must satisfy adjacency_list + * - in_edges(g, u) must return an in_edge_range + * - source_id(g, ie) must return a value convertible to vertex_id_t + * for incoming edge elements + * + * Note: source_id on an incoming edge returns the ID of the vertex from + * which the edge originates (the "source" in the original directed sense). + * + * @tparam G Graph type + */ +template +concept bidirectional_adjacency_list = + adjacency_list && + requires(G& g, vertex_t u, in_edge_t ie) { + { in_edges(g, u) } -> in_edge_range; + { source_id(g, ie) } -> std::convertible_to>; + }; + +/** + * @brief Concept for bidirectional graphs with index-based vertex access + * + * An index_bidirectional_adjacency_list combines bidirectional traversal + * with O(1) index-based vertex lookup. + * + * Requirements: + * - Must satisfy bidirectional_adjacency_list + * - Must satisfy index_vertex_range (random access vertices) + * + * @tparam G Graph type + */ +template +concept index_bidirectional_adjacency_list = + bidirectional_adjacency_list && index_vertex_range; + + } // namespace graph::adj_list diff --git a/include/graph/adj_list/adjacency_list_traits.hpp b/include/graph/adj_list/adjacency_list_traits.hpp index d857488..a27bab8 100644 --- a/include/graph/adj_list/adjacency_list_traits.hpp +++ b/include/graph/adj_list/adjacency_list_traits.hpp @@ -167,6 +167,116 @@ concept has_contains_edge = template inline constexpr bool has_contains_edge_v = has_contains_edge; +// ============================================================================= +// In-Degree Trait +// ============================================================================= + +namespace detail { + // Helper to detect if in_degree(g, u) is valid for a vertex descriptor + template + concept has_in_degree_impl = requires(G& g, vertex_t& u) { + { in_degree(g, u) } -> std::integral; + }; +} // namespace detail + +/** + * @brief Trait to check if a graph supports in_degree operations + * + * A graph has in_degree support if in_degree(g, u) is valid with a vertex descriptor. + * + * @tparam G Graph type + */ +template +concept has_in_degree = detail::has_in_degree_impl; + +// Convenience variable template +template +inline constexpr bool has_in_degree_v = has_in_degree; + +// ============================================================================= +// Find In-Edge Trait +// ============================================================================= + +namespace detail { + // Helper to detect if find_in_edge(g, u, v) is valid with vertex descriptors + template + concept has_find_in_edge_uu_impl = requires(G& g, vertex_t u, vertex_t v) { + { find_in_edge(g, u, v) }; + }; + + // Helper to detect if find_in_edge(g, u, vid) is valid with descriptor + id + template + concept has_find_in_edge_uid_impl = requires(G& g, vertex_t u, vertex_id_t vid) { + { find_in_edge(g, u, vid) }; + }; + + // Helper to detect if find_in_edge(g, uid, vid) is valid with vertex IDs + template + concept has_find_in_edge_uidvid_impl = requires(G& g, vertex_id_t uid, vertex_id_t vid) { + { find_in_edge(g, uid, vid) }; + }; +} // namespace detail + +/** + * @brief Trait to check if a graph supports find_in_edge operations + * + * A graph has find_in_edge support if all three overloads are valid: + * - find_in_edge(g, u, v) where u, v are vertex descriptors + * - find_in_edge(g, u, vid) where u is a descriptor and vid is a vertex ID + * - find_in_edge(g, uid, vid) where both are vertex IDs + * + * @tparam G Graph type + */ +template +concept has_find_in_edge = + detail::has_find_in_edge_uu_impl && detail::has_find_in_edge_uid_impl && + detail::has_find_in_edge_uidvid_impl; + +// Convenience variable template +template +inline constexpr bool has_find_in_edge_v = has_find_in_edge; + +// ============================================================================= +// Contains In-Edge Trait +// ============================================================================= + +namespace detail { + // Helper to detect if contains_in_edge(g, u, v) is valid + template + concept has_contains_in_edge_uv_impl = requires(G& g, V u, V v) { + { contains_in_edge(g, u, v) } -> std::same_as; + }; + + // Helper to detect if contains_in_edge(g, uid, vid) is valid with vertex IDs + template + concept has_contains_in_edge_uidvid_impl = + std::same_as, vertex_id_t> && requires(G& g, V uid, V vid) { + { contains_in_edge(g, uid, vid) } -> std::same_as; + }; +} // namespace detail + +/** + * @brief Trait to check if a graph supports contains_in_edge operations + * + * A graph has contains_in_edge support with vertex type V if both overloads are valid: + * - contains_in_edge(g, u, v) where u, v are of type V + * - contains_in_edge(g, uid, vid) if V is vertex_id_t + * + * Requirements: + * - Both overloads must return bool + * + * @tparam G Graph type + * @tparam V Vertex or vertex ID type + */ +template +concept has_contains_in_edge = + detail::has_contains_in_edge_uv_impl && + (std::same_as, vertex_t> || detail::has_contains_in_edge_uidvid_impl); + +// Convenience variable template +template +inline constexpr bool has_contains_in_edge_v = has_contains_in_edge; + // ============================================================================= // Combined Trait Queries // ============================================================================= diff --git a/include/graph/adj_list/descriptor.hpp b/include/graph/adj_list/descriptor.hpp index 15b8fa4..2d1bac6 100644 --- a/include/graph/adj_list/descriptor.hpp +++ b/include/graph/adj_list/descriptor.hpp @@ -12,6 +12,11 @@ namespace graph::adj_list { +/// @brief Tag type indicating an out-edge (default) descriptor direction +struct out_edge_tag {}; +/// @brief Tag type indicating an in-edge descriptor direction +struct in_edge_tag {}; + /** * @brief Concept to check if a type is pair-like (has at least 2 members accessible via tuple protocol) * diff --git a/include/graph/adj_list/descriptor_traits.hpp b/include/graph/adj_list/descriptor_traits.hpp index 26aedcb..3c07a72 100644 --- a/include/graph/adj_list/descriptor_traits.hpp +++ b/include/graph/adj_list/descriptor_traits.hpp @@ -15,13 +15,13 @@ namespace graph::adj_list { template class vertex_descriptor; -template +template class edge_descriptor; template class vertex_descriptor_view; -template +template class edge_descriptor_view; // ============================================================================= @@ -56,8 +56,8 @@ struct is_edge_descriptor : std::false_type {}; /** * @brief Specialization for edge_descriptor */ -template -struct is_edge_descriptor> : std::true_type {}; +template +struct is_edge_descriptor> : std::true_type {}; /** * @brief Helper variable template for is_edge_descriptor @@ -66,6 +66,26 @@ struct is_edge_descriptor> : std::true_typ template inline constexpr bool is_edge_descriptor_v = is_edge_descriptor>::value; +/** + * @brief Primary template - not an in-edge descriptor + */ +template +struct is_in_edge_descriptor : std::false_type {}; + +/** + * @brief Specialization: true when EdgeDirection is in_edge_tag + */ +template +struct is_in_edge_descriptor> + : std::bool_constant> {}; + +/** + * @brief Helper variable template for is_in_edge_descriptor + * Removes cv-qualifiers before checking + */ +template +inline constexpr bool is_in_edge_descriptor_v = is_in_edge_descriptor>::value; + /** * @brief Primary template - not a descriptor */ @@ -81,8 +101,8 @@ struct is_descriptor> : std::true_type {}; /** * @brief Specialization for edge_descriptor */ -template -struct is_descriptor> : std::true_type {}; +template +struct is_descriptor> : std::true_type {}; /** * @brief Helper variable template for is_descriptor @@ -123,8 +143,8 @@ struct is_edge_descriptor_view : std::false_type {}; /** * @brief Specialization for edge_descriptor_view */ -template -struct is_edge_descriptor_view> : std::true_type {}; +template +struct is_edge_descriptor_view> : std::true_type {}; /** * @brief Helper variable template for is_edge_descriptor_view @@ -148,8 +168,8 @@ struct is_descriptor_view> : std::true_type { /** * @brief Specialization for edge_descriptor_view */ -template -struct is_descriptor_view> : std::true_type {}; +template +struct is_descriptor_view> : std::true_type {}; /** * @brief Helper variable template for is_descriptor_view @@ -190,13 +210,13 @@ using descriptor_iterator_type_t = typename descriptor_iterator_type::type; template struct edge_descriptor_edge_iterator_type; -template -struct edge_descriptor_edge_iterator_type> { +template +struct edge_descriptor_edge_iterator_type> { using type = EdgeIter; }; -template -struct edge_descriptor_edge_iterator_type> { +template +struct edge_descriptor_edge_iterator_type> { using type = EdgeIter; }; @@ -212,13 +232,13 @@ using edge_descriptor_edge_iterator_type_t = typename edge_descriptor_edge_itera template struct edge_descriptor_vertex_iterator_type; -template -struct edge_descriptor_vertex_iterator_type> { +template +struct edge_descriptor_vertex_iterator_type> { using type = VertexIter; }; -template -struct edge_descriptor_vertex_iterator_type> { +template +struct edge_descriptor_vertex_iterator_type> { using type = VertexIter; }; @@ -251,9 +271,9 @@ using descriptor_storage_type_t = typename descriptor_storage_type::type; template struct edge_descriptor_storage_type; -template -struct edge_descriptor_storage_type> { - using type = typename edge_descriptor::edge_storage_type; +template +struct edge_descriptor_storage_type> { + using type = typename edge_descriptor::edge_storage_type; }; /** @@ -276,8 +296,8 @@ template struct is_random_access_descriptor> : std::bool_constant> {}; -template -struct is_random_access_descriptor> +template +struct is_random_access_descriptor> : std::bool_constant> {}; /** @@ -297,8 +317,8 @@ template struct is_iterator_based_descriptor> : std::bool_constant> {}; -template -struct is_iterator_based_descriptor> +template +struct is_iterator_based_descriptor> : std::bool_constant> {}; /** diff --git a/include/graph/adj_list/detail/graph_cpo.hpp b/include/graph/adj_list/detail/graph_cpo.hpp index f0237a1..e21fc6d 100644 --- a/include/graph/adj_list/detail/graph_cpo.hpp +++ b/include/graph/adj_list/detail/graph_cpo.hpp @@ -786,49 +786,412 @@ namespace _cpo_impls { } // namespace _cpo_impls // ============================================================================= -// edges(g, u) - Public CPO instance and type aliases +// out_edges(g, u) - Public CPO instance and type aliases // ============================================================================= inline namespace _cpo_instances { /** * @brief CPO for getting outgoing edges from a vertex * - * Usage: auto vertex_edges = graph::edges(my_graph, vertex_descriptor); + * Usage: auto vertex_edges = graph::out_edges(my_graph, vertex_descriptor); * * Returns: edge_descriptor_view */ - inline constexpr _cpo_impls::_edges::_fn edges{}; + inline constexpr _cpo_impls::_edges::_fn out_edges{}; + + /// @brief Alias for out_edges — provided for convenience. + inline constexpr auto& edges = out_edges; } // namespace _cpo_instances /** - * @brief Range type returned by edges(g, u) + * @brief Range type returned by out_edges(g, u) * * This is always edge_descriptor_view where EdgeIter * is the iterator type of the underlying edge container and VertexIter is * the iterator type of the vertex container. */ template -using vertex_edge_range_t = decltype(edges(std::declval(), std::declval>())); +using out_edge_range_t = decltype(out_edges(std::declval(), std::declval>())); /** - * @brief Iterator type for traversing edges from a vertex + * @brief Iterator type for traversing outgoing edges from a vertex * - * Iterator over the edge_descriptor_view returned by edges(g, u). + * Iterator over the edge_descriptor_view returned by out_edges(g, u). */ template -using vertex_edge_iterator_t = std::ranges::iterator_t>; +using out_edge_iterator_t = std::ranges::iterator_t>; /** - * @brief Edge descriptor type for graph G + * @brief Outgoing edge descriptor type for graph G * * This is the value_type of the edge range - an edge_descriptor * that wraps an edge and maintains its source vertex. */ template -using edge_t = std::ranges::range_value_t>; +using out_edge_t = std::ranges::range_value_t>; + +/// @brief Alias for out_edge_range_t — provided for convenience. +template +using vertex_edge_range_t = out_edge_range_t; + +/// @brief Alias for out_edge_iterator_t — provided for convenience. +template +using vertex_edge_iterator_t = out_edge_iterator_t; + +/// @brief Alias for out_edge_t — provided for convenience. +template +using edge_t = out_edge_t; namespace _cpo_impls {} // namespace _cpo_impls +// ============================================================================= +// in_edges(g, u) and in_edges(g, uid) CPO +// ============================================================================= + +namespace _cpo_impls { + + namespace _in_edges { + // Use the public CPO instances (already declared above) + using _cpo_instances::find_vertex; + + // --- (g, u) overload: vertex descriptor --- + enum class _St_u { _none, _vertex_member, _adl }; + + // Check for u.inner_value(g).in_edges() member function + template + concept _has_vertex_member_u = is_vertex_descriptor_v> && std::ranges::forward_range && + requires(G& g, const U& u) { + { u.inner_value(g) }; + { u.inner_value(g).in_edges() } -> std::ranges::forward_range; + }; + + // Check for ADL in_edges(g, u) + template + concept _has_adl_u = requires(G& g, const U& u) { + { in_edges(g, u) } -> std::ranges::forward_range; + }; + + // NOTE: No _edge_value_pattern tier — in_edges has no implicit default. + // A graph MUST explicitly provide in_edges() via member or ADL. + + template + [[nodiscard]] consteval _Choice_t<_St_u> _Choose_u() noexcept { + if constexpr (_has_vertex_member_u) { + return {_St_u::_vertex_member, noexcept(std::declval().inner_value(std::declval()).in_edges())}; + } else if constexpr (_has_adl_u) { + return {_St_u::_adl, noexcept(in_edges(std::declval(), std::declval()))}; + } else { + return {_St_u::_none, false}; + } + } + + // --- (g, uid) overload: vertex ID --- + enum class _St_uid { _none, _adl, _default }; + + // Check for ADL in_edges(g, uid) - vertex ID + template + concept _has_adl_uid = requires(G& g, const VId& uid) { + { in_edges(g, uid) } -> std::ranges::forward_range; + }; + + // Check if we can use default: find_vertex + in_edges(g, u) + // Accepts both vertex_member and ADL tiers for the vertex descriptor overload + template + concept _has_default_uid = + requires(G& g, const VId& uid) { + { find_vertex(g, uid) } -> std::input_iterator; + requires vertex_descriptor_type; + } && (_has_vertex_member_u(), std::declval()))> || + _has_adl_u(), std::declval()))>); + + template + [[nodiscard]] consteval _Choice_t<_St_uid> _Choose_uid() noexcept { + if constexpr (_has_adl_uid) { + return {_St_uid::_adl, noexcept(in_edges(std::declval(), std::declval()))}; + } else if constexpr (_has_default_uid) { + return {_St_uid::_default, false}; + } else { + return {_St_uid::_none, false}; + } + } + + // Helper to wrap result in edge_descriptor_view with in_edge_tag. + // Unlike _edges::_wrap_if_needed (which defaults to out_edge_tag), + // this always produces in-edge-tagged descriptors. + template + [[nodiscard]] constexpr auto _wrap_in_edge(Result&& result, const U& source_vertex) noexcept { + using ResultType = std::remove_cvref_t; + if constexpr (is_edge_descriptor_view_v) { + // Already an edge_descriptor_view — trust the caller's tag + return std::forward(result); + } else { + // Wrap raw range/container in edge_descriptor_view<..., in_edge_tag> + using edge_iter_t = std::ranges::iterator_t; + using vertex_iter_t = typename std::remove_cvref_t::iterator_type; + return edge_descriptor_view( + std::forward(result), source_vertex); + } + } + + class _fn { + private: + template + static constexpr _Choice_t<_St_u> _Choice_u = _Choose_u, std::remove_cvref_t>(); + + template + static constexpr _Choice_t<_St_uid> _Choice_uid = _Choose_uid, std::remove_cvref_t>(); + + public: + /** + * @brief Get range of incoming edges to a vertex (by descriptor) + * + * Resolution order: + * 1. If u.inner_value(g).in_edges() exists -> use it (wrap if needed) + * 2. If ADL in_edges(g, u) exists -> use it (wrap if needed) + */ + template + [[nodiscard]] constexpr auto operator()(G&& g, const U& u) const + noexcept(_Choice_u, std::remove_cvref_t>._No_throw) + requires(_Choice_u, std::remove_cvref_t>._Strategy != _St_u::_none) + { + using _G = std::remove_cvref_t; + using _U = std::remove_cvref_t; + if constexpr (_Choice_u<_G, _U>._Strategy == _St_u::_vertex_member) { + return _wrap_in_edge(u.inner_value(g).in_edges(), u); + } else if constexpr (_Choice_u<_G, _U>._Strategy == _St_u::_adl) { + return _wrap_in_edge(in_edges(g, u), u); + } + } + + /** + * @brief Get range of incoming edges to a vertex (by ID) + * + * Resolution order: + * 1. ADL in_edges(g, uid) (highest priority) + * 2. in_edges(g, *find_vertex(g, uid)) (default) + */ + template + requires(!vertex_descriptor_type) + [[nodiscard]] constexpr auto operator()(G&& g, const VId& uid) const + noexcept(_Choice_uid, std::remove_cvref_t>._No_throw) + requires(_Choice_uid, std::remove_cvref_t>._Strategy != _St_uid::_none) + { + using _G = std::remove_cvref_t; + using _VId = std::remove_cvref_t; + + if constexpr (_Choice_uid<_G, _VId>._Strategy == _St_uid::_adl) { + auto result = [&g, &uid]() { + if constexpr (requires { typename _G::vertex_id_type; }) { + return in_edges(g, static_cast(uid)); + } else { + return in_edges(g, static_cast>(uid)); + } + }(); + auto v = *find_vertex(g, static_cast>(uid)); + return _wrap_in_edge(std::move(result), v); + } else if constexpr (_Choice_uid<_G, _VId>._Strategy == _St_uid::_default) { + auto v = *find_vertex(std::forward(g), static_cast>(uid)); + return (*this)(std::forward(g), v); + } + } + }; + } // namespace _in_edges + +} // namespace _cpo_impls + +// ============================================================================= +// in_edges(g, u) - Public CPO instance and type aliases +// ============================================================================= + +inline namespace _cpo_instances { + /** + * @brief CPO for getting incoming edges to a vertex + * + * Usage: auto in_edge_range = graph::in_edges(my_graph, vertex_descriptor); + * auto in_edge_range = graph::in_edges(my_graph, vertex_id); + * + * Returns: edge_descriptor_view + */ + inline constexpr _cpo_impls::_in_edges::_fn in_edges{}; +} // namespace _cpo_instances + +/** + * @brief Range type returned by in_edges(g, u) + */ +template +using in_edge_range_t = decltype(in_edges(std::declval(), std::declval>())); + +/** + * @brief Iterator type for traversing incoming edges to a vertex + */ +template +using in_edge_iterator_t = std::ranges::iterator_t>; + +/** + * @brief Incoming edge descriptor type for graph G + */ +template +using in_edge_t = std::ranges::range_value_t>; + +// ============================================================================= +// in_degree(g, u) and in_degree(g, uid) CPO +// ============================================================================= + +namespace _cpo_impls { + + namespace _in_degree { + // Strategy enum for in_degree(g, u) - vertex descriptor version + enum class _St_u { _none, _member, _adl, _default }; + + // Check for g.in_degree(u) member function - vertex descriptor + template + concept _has_member_u = requires(G& g, const U& u) { + { g.in_degree(u) } -> std::integral; + }; + + // Check for ADL in_degree(g, u) - vertex descriptor + template + concept _has_adl_u = requires(G& g, const U& u) { + { in_degree(g, u) } -> std::integral; + }; + + // Check if we can use default: count in_edges via size() or distance() + template + concept _has_default_u = requires(G& g, const U& u) { + { in_edges(g, u) } -> std::ranges::forward_range; + }; + + template + [[nodiscard]] consteval _Choice_t<_St_u> _Choose_u() noexcept { + if constexpr (_has_member_u) { + return {_St_u::_member, noexcept(std::declval().in_degree(std::declval()))}; + } else if constexpr (_has_adl_u) { + return {_St_u::_adl, noexcept(in_degree(std::declval(), std::declval()))}; + } else if constexpr (_has_default_u) { + return {_St_u::_default, true}; + } else { + return {_St_u::_none, false}; + } + } + + // Strategy enum for in_degree(g, uid) - vertex ID version + enum class _St_uid { _none, _member, _adl, _default }; + + // Check for g.in_degree(uid) member function - vertex ID + template + concept _has_member_uid = requires(G& g, const VId& uid) { + { g.in_degree(uid) } -> std::integral; + }; + + // Check for ADL in_degree(g, uid) - vertex ID + template + concept _has_adl_uid = requires(G& g, const VId& uid) { + { in_degree(g, uid) } -> std::integral; + }; + + // Check if we can use default: in_degree(g, *find_vertex(g, uid)) + template + concept _has_default_uid = requires(G& g, const VId& uid) { + { find_vertex(g, uid) } -> std::input_iterator; + requires vertex_descriptor_type; + requires _has_default_u; + }; + + template + [[nodiscard]] consteval _Choice_t<_St_uid> _Choose_uid() noexcept { + if constexpr (_has_member_uid) { + return {_St_uid::_member, noexcept(std::declval().in_degree(std::declval()))}; + } else if constexpr (_has_adl_uid) { + return {_St_uid::_adl, noexcept(in_degree(std::declval(), std::declval()))}; + } else if constexpr (_has_default_uid) { + return {_St_uid::_default, false}; + } else { + return {_St_uid::_none, false}; + } + } + + class _fn { + private: + template + static constexpr _Choice_t<_St_u> _Choice_u = _Choose_u, std::remove_cvref_t>(); + + template + static constexpr _Choice_t<_St_uid> _Choice_uid = _Choose_uid, std::remove_cvref_t>(); + + public: + // in_degree(g, u) - vertex descriptor version + template + [[nodiscard]] constexpr auto operator()(G&& g, const U& u) const noexcept(_Choice_u._No_throw) + requires(_Choice_u, std::remove_cvref_t>._Strategy != _St_u::_none) + { + using _G = std::remove_cvref_t; + using _U = std::remove_cvref_t; + + if constexpr (_Choice_u<_G, _U>._Strategy == _St_u::_member) { + return g.in_degree(u); + } else if constexpr (_Choice_u<_G, _U>._Strategy == _St_u::_adl) { + return in_degree(g, u); + } else if constexpr (_Choice_u<_G, _U>._Strategy == _St_u::_default) { + auto edge_range = in_edges(std::forward(g), u); + if constexpr (std::ranges::sized_range) { + return std::ranges::size(edge_range); + } else { + return std::ranges::distance(edge_range); + } + } + } + + // in_degree(g, uid) - vertex ID version + template + requires(!vertex_descriptor_type) + [[nodiscard]] constexpr auto operator()(G&& g, const VId& uid) const noexcept(_Choice_uid._No_throw) + requires(_Choice_uid, std::remove_cvref_t>._Strategy != _St_uid::_none) + { + using _G = std::remove_cvref_t; + using _VId = std::remove_cvref_t; + + if constexpr (_Choice_uid<_G, _VId>._Strategy == _St_uid::_member) { + if constexpr (requires { typename _G::vertex_id_type; }) { + return g.in_degree(static_cast(uid)); + } else { + return g.in_degree(uid); + } + } else if constexpr (_Choice_uid<_G, _VId>._Strategy == _St_uid::_adl) { + if constexpr (requires { typename _G::vertex_id_type; }) { + return in_degree(g, static_cast(uid)); + } else { + return in_degree(g, uid); + } + } else if constexpr (_Choice_uid<_G, _VId>._Strategy == _St_uid::_default) { + auto v = *find_vertex(std::forward(g), static_cast>(uid)); + return (*this)(std::forward(g), v); + } + } + }; + } // namespace _in_degree +} // namespace _cpo_impls + +// ============================================================================= +// in_degree(g, u) - Public CPO instance +// ============================================================================= + +inline namespace _cpo_instances { + /** + * @brief CPO for getting the in-degree (number of incoming edges) of a vertex + * + * Usage: + * auto deg = graph::in_degree(my_graph, vertex_descriptor); + * auto deg = graph::in_degree(my_graph, vertex_id); + * + * Returns: Number of incoming edges to the vertex (integral type) + */ + inline constexpr _cpo_impls::_in_degree::_fn in_degree{}; +} // namespace _cpo_instances + +// Outgoing aliases (out_edges, out_degree, find_out_edge) and type aliases are +// declared later in this file, after all referenced CPO instances exist. +// Search for "Outgoing aliases" below. + namespace _cpo_impls { // ========================================================================= @@ -1585,20 +1948,23 @@ namespace _cpo_impls { } // namespace _cpo_impls // ============================================================================= -// degree(g, u) and degree(g, uid) - Public CPO instances +// out_degree(g, u) and out_degree(g, uid) - Public CPO instances // ============================================================================= inline namespace _cpo_instances { /** - * @brief CPO for getting the degree (number of outgoing edges) of a vertex + * @brief CPO for getting the out-degree (number of outgoing edges) of a vertex * * Usage: - * auto deg = graph::degree(my_graph, vertex_descriptor); - * auto deg = graph::degree(my_graph, vertex_id); + * auto deg = graph::out_degree(my_graph, vertex_descriptor); + * auto deg = graph::out_degree(my_graph, vertex_id); * * Returns: Number of outgoing edges from the vertex (integral type) */ - inline constexpr _cpo_impls::_degree::_fn degree{}; + inline constexpr _cpo_impls::_degree::_fn out_degree{}; + + /// @brief Alias for out_degree — provided for convenience. + inline constexpr auto& degree = out_degree; } // namespace _cpo_instances namespace _cpo_impls { @@ -1831,21 +2197,454 @@ namespace _cpo_impls { } // namespace _cpo_impls // ============================================================================= -// find_vertex_edge(g, u, v/vid) and find_vertex_edge(g, uid, vid) - Public CPO instances +// find_out_edge(g, u, v/vid) and find_out_edge(g, uid, vid) - Public CPO instances // ============================================================================= inline namespace _cpo_instances { /** - * @brief CPO for finding an edge from source vertex u to target vertex v + * @brief CPO for finding an outgoing edge from source vertex u to target vertex v * * Usage: - * auto e = graph::find_vertex_edge(my_graph, u_descriptor, v_descriptor); - * auto e = graph::find_vertex_edge(my_graph, u_descriptor, target_id); - * auto e = graph::find_vertex_edge(my_graph, source_id, target_id); + * auto e = graph::find_out_edge(my_graph, u_descriptor, v_descriptor); + * auto e = graph::find_out_edge(my_graph, u_descriptor, target_id); + * auto e = graph::find_out_edge(my_graph, source_id, target_id); * * Returns: Edge descriptor if found, or end iterator/sentinel if not found */ - inline constexpr _cpo_impls::_find_vertex_edge::_fn find_vertex_edge{}; + inline constexpr _cpo_impls::_find_vertex_edge::_fn find_out_edge{}; + + /// @brief Alias for find_out_edge — provided for convenience. + inline constexpr auto& find_vertex_edge = find_out_edge; +} // namespace _cpo_instances + +// ============================================================================= +// find_in_edge(g, u, v), find_in_edge(g, u, vid), find_in_edge(g, uid, vid) CPO +// ============================================================================= +// +// Default tier strategy: "find incoming edge to u from v" is equivalent to +// "find outgoing edge from v to u". We delegate to find_vertex_edge(g, v, u) +// which correctly accesses v's outgoing edges. +// This avoids issues with edge_descriptor_view wrapping for incoming edges, +// where target_id/source_id would access the wrong underlying container. +// ============================================================================= + +namespace _cpo_impls { + + namespace _find_in_edge { + using _cpo_instances::edges; + using _cpo_instances::find_vertex; + using _cpo_instances::find_vertex_edge; + using graph::target_id; + + // --- (g, u, v) overload: both descriptors --- + enum class _St_uu { _none, _member, _adl, _default }; + + template + concept _has_member_uu = requires(G& g, const U& u, const V& v) { + { g.find_in_edge(u, v) }; + }; + + template + concept _has_adl_uu = requires(G& g, const U& u, const V& v) { + { find_in_edge(g, u, v) }; + }; + + // Default: find outgoing edge from v to u via find_vertex_edge(g, v, u) + template + concept _has_default_uu = requires(G& g, const U& u, const V& v) { + { find_vertex_edge(g, v, u) }; + }; + + template + [[nodiscard]] consteval _Choice_t<_St_uu> _Choose_uu() noexcept { + if constexpr (_has_member_uu) { + return {_St_uu::_member, + noexcept(std::declval().find_in_edge(std::declval(), std::declval()))}; + } else if constexpr (_has_adl_uu) { + return {_St_uu::_adl, + noexcept(find_in_edge(std::declval(), std::declval(), std::declval()))}; + } else if constexpr (_has_default_uu) { + return {_St_uu::_default, false}; + } else { + return {_St_uu::_none, false}; + } + } + + // --- (g, u, vid) overload: descriptor + source ID --- + enum class _St_uid { _none, _member, _adl, _default }; + + template + concept _has_member_uid = requires(G& g, const U& u, const VId& vid) { + { g.find_in_edge(u, vid) }; + }; + + template + concept _has_adl_uid = requires(G& g, const U& u, const VId& vid) { + { find_in_edge(g, u, vid) }; + }; + + // Default: find outgoing edge from vid's vertex to u + // Resolve vid → vertex descriptor, then find_vertex_edge(g, v, u) + template + concept _has_default_uid = requires(G& g, const U& u, const VId& vid) { + { find_vertex(g, vid) } -> std::input_iterator; + requires vertex_descriptor_type; + { edges(g, *find_vertex(g, vid)) } -> std::ranges::input_range; + { target_id(g, *std::ranges::begin(edges(g, *find_vertex(g, vid)))) }; + { vertex_id(g, u) }; + }; + + template + [[nodiscard]] consteval _Choice_t<_St_uid> _Choose_uid() noexcept { + if constexpr (_has_member_uid) { + return {_St_uid::_member, + noexcept(std::declval().find_in_edge(std::declval(), std::declval()))}; + } else if constexpr (_has_adl_uid) { + return {_St_uid::_adl, + noexcept(find_in_edge(std::declval(), std::declval(), std::declval()))}; + } else if constexpr (_has_default_uid) { + return {_St_uid::_default, false}; + } else { + return {_St_uid::_none, false}; + } + } + + // --- (g, uid, vid) overload: both IDs --- + enum class _St_uidvid { _none, _member, _adl, _default }; + + template + concept _has_member_uidvid = requires(G& g, const UId& uid, const VId& vid) { + { g.find_in_edge(uid, vid) }; + }; + + template + concept _has_adl_uidvid = requires(G& g, const UId& uid, const VId& vid) { + { find_in_edge(g, uid, vid) }; + }; + + // Default: delegate to find_vertex_edge(g, vid, uid) — swap source and target + template + concept _has_default_uidvid = requires(G& g, const UId& uid, const VId& vid) { + { find_vertex_edge(g, vid, uid) }; + }; + + template + [[nodiscard]] consteval _Choice_t<_St_uidvid> _Choose_uidvid() noexcept { + if constexpr (_has_member_uidvid) { + return {_St_uidvid::_member, + noexcept(std::declval().find_in_edge(std::declval(), std::declval()))}; + } else if constexpr (_has_adl_uidvid) { + return {_St_uidvid::_adl, + noexcept(find_in_edge(std::declval(), std::declval(), std::declval()))}; + } else if constexpr (_has_default_uidvid) { + return {_St_uidvid::_default, false}; + } else { + return {_St_uidvid::_none, false}; + } + } + + class _fn { + private: + template + static constexpr _Choice_t<_St_uu> _Choice_uu = + _Choose_uu, std::remove_cvref_t, std::remove_cvref_t>(); + + template + static constexpr _Choice_t<_St_uid> _Choice_uid = + _Choose_uid, std::remove_cvref_t, std::remove_cvref_t>(); + + template + static constexpr _Choice_t<_St_uidvid> _Choice_uidvid = + _Choose_uidvid, std::remove_cvref_t, std::remove_cvref_t>(); + + public: + // find_in_edge(g, u, v) - both vertex descriptors + template + [[nodiscard]] constexpr auto operator()(G&& g, const U& u, const V& v) const + noexcept(_Choice_uu._No_throw) + requires(_Choice_uu, std::remove_cvref_t, std::remove_cvref_t>._Strategy != + _St_uu::_none) + { + using _G = std::remove_cvref_t; + using _U = std::remove_cvref_t; + using _V = std::remove_cvref_t; + + if constexpr (_Choice_uu<_G, _U, _V>._Strategy == _St_uu::_member) { + return g.find_in_edge(u, v); + } else if constexpr (_Choice_uu<_G, _U, _V>._Strategy == _St_uu::_adl) { + return find_in_edge(g, u, v); + } else if constexpr (_Choice_uu<_G, _U, _V>._Strategy == _St_uu::_default) { + // "find incoming to u from v" = "find outgoing from v to u" + return find_vertex_edge(std::forward(g), v, u); + } + } + + // find_in_edge(g, u, vid) - target descriptor + source ID + template + requires(!vertex_descriptor_type) + [[nodiscard]] constexpr auto operator()(G&& g, const U& u, const VId& vid) const + noexcept(_Choice_uid._No_throw) + requires(_Choice_uid, std::remove_cvref_t, std::remove_cvref_t>._Strategy != + _St_uid::_none) + { + using _G = std::remove_cvref_t; + using _U = std::remove_cvref_t; + using _VId = std::remove_cvref_t; + + if constexpr (_Choice_uid<_G, _U, _VId>._Strategy == _St_uid::_member) { + return g.find_in_edge(u, vid); + } else if constexpr (_Choice_uid<_G, _U, _VId>._Strategy == _St_uid::_adl) { + return find_in_edge(g, u, vid); + } else if constexpr (_Choice_uid<_G, _U, _VId>._Strategy == _St_uid::_default) { + // "find incoming to u from vid" = "find outgoing from vid's vertex to u" + auto v = *find_vertex(std::forward(g), static_cast>(vid)); + auto target_uid = vertex_id(std::forward(g), u); + auto edge_range = edges(std::forward(g), v); + auto it = std::ranges::find_if(edge_range, [&](const auto& e) { + return static_cast>(target_id(std::forward(g), e)) == + static_cast>(target_uid); + }); + return *it; + } + } + + // find_in_edge(g, uid, vid) - both vertex IDs + template + requires(!vertex_descriptor_type) && + (!vertex_descriptor_type) + [[nodiscard]] constexpr auto operator()(G&& g, const UId& uid, const VId& vid) const + noexcept(_Choice_uidvid._No_throw) + requires( + _Choice_uidvid, std::remove_cvref_t, std::remove_cvref_t>._Strategy != + _St_uidvid::_none) + { + using _G = std::remove_cvref_t; + using _UId = std::remove_cvref_t; + using _VId = std::remove_cvref_t; + + if constexpr (_Choice_uidvid<_G, _UId, _VId>._Strategy == _St_uidvid::_member) { + if constexpr (requires { typename _G::vertex_id_type; }) { + return g.find_in_edge(static_cast(uid), + static_cast(vid)); + } else { + return g.find_in_edge(uid, vid); + } + } else if constexpr (_Choice_uidvid<_G, _UId, _VId>._Strategy == _St_uidvid::_adl) { + if constexpr (requires { typename _G::vertex_id_type; }) { + return find_in_edge(g, static_cast(uid), + static_cast(vid)); + } else { + return find_in_edge(g, uid, vid); + } + } else if constexpr (_Choice_uidvid<_G, _UId, _VId>._Strategy == _St_uidvid::_default) { + // "find incoming to uid from vid" = "find outgoing from vid to uid" + return find_vertex_edge(std::forward(g), vid, uid); + } + } + }; + } // namespace _find_in_edge + +} // namespace _cpo_impls + +// ============================================================================= +// find_in_edge - Public CPO instance +// ============================================================================= + +inline namespace _cpo_instances { + /** + * @brief CPO for finding an incoming edge to vertex u from source vertex v + * + * Usage: + * auto e = graph::find_in_edge(my_graph, u_descriptor, v_descriptor); + * auto e = graph::find_in_edge(my_graph, u_descriptor, source_id); + * auto e = graph::find_in_edge(my_graph, target_id, source_id); + * + * Returns: Edge descriptor (from source's outgoing edge list) if found + */ + inline constexpr _cpo_impls::_find_in_edge::_fn find_in_edge{}; +} // namespace _cpo_instances + +// ============================================================================= +// contains_in_edge(g, u, v) and contains_in_edge(g, uid, vid) CPO +// ============================================================================= +// +// Default: "does incoming edge to u from v exist?" = "does edge from v to u exist?" +// Iterate edges(g, v) and check if target_id matches vertex_id(g, u). +// ============================================================================= + +namespace _cpo_impls { + + namespace _contains_in_edge { + using _cpo_instances::edges; + using _cpo_instances::find_vertex; + using graph::target_id; + + // --- (g, u, v) overload: both descriptors --- + enum class _St_uv { _none, _member, _adl, _default }; + + template + concept _has_member_uv = requires(G& g, const U& u, const V& v) { + { g.contains_in_edge(u, v) } -> std::convertible_to; + }; + + template + concept _has_adl_uv = requires(G& g, const U& u, const V& v) { + { contains_in_edge(g, u, v) } -> std::convertible_to; + }; + + // Default: iterate edges(g, v) and match target_id against vertex_id(g, u) + template + concept _has_default_uv = requires(G& g, const U& u, const V& v) { + { edges(g, v) } -> std::ranges::input_range; + { target_id(g, *std::ranges::begin(edges(g, v))) }; + { vertex_id(g, u) }; + }; + + template + [[nodiscard]] consteval _Choice_t<_St_uv> _Choose_uv() noexcept { + if constexpr (_has_member_uv) { + return {_St_uv::_member, + noexcept(std::declval().contains_in_edge(std::declval(), std::declval()))}; + } else if constexpr (_has_adl_uv) { + return {_St_uv::_adl, + noexcept(contains_in_edge(std::declval(), std::declval(), std::declval()))}; + } else if constexpr (_has_default_uv) { + return {_St_uv::_default, false}; + } else { + return {_St_uv::_none, false}; + } + } + + // --- (g, uid, vid) overload: both IDs --- + enum class _St_uidvid { _none, _member, _adl, _default }; + + template + concept _has_member_uidvid = requires(G& g, const UId& uid, const VId& vid) { + { g.contains_in_edge(uid, vid) } -> std::convertible_to; + }; + + template + concept _has_adl_uidvid = requires(G& g, const UId& uid, const VId& vid) { + { contains_in_edge(g, uid, vid) } -> std::convertible_to; + }; + + // Default: find_vertex(g, vid), then iterate edges(g, v) and compare target_id against uid + template + concept _has_default_uidvid = requires(G& g, const UId& uid, const VId& vid) { + { find_vertex(g, vid) } -> std::input_iterator; + requires vertex_descriptor_type; + { edges(g, *find_vertex(g, vid)) } -> std::ranges::input_range; + { target_id(g, *std::ranges::begin(edges(g, *find_vertex(g, vid)))) }; + }; + + template + [[nodiscard]] consteval _Choice_t<_St_uidvid> _Choose_uidvid() noexcept { + if constexpr (_has_member_uidvid) { + return {_St_uidvid::_member, + noexcept(std::declval().contains_in_edge(std::declval(), std::declval()))}; + } else if constexpr (_has_adl_uidvid) { + return {_St_uidvid::_adl, + noexcept(contains_in_edge(std::declval(), std::declval(), std::declval()))}; + } else if constexpr (_has_default_uidvid) { + return {_St_uidvid::_default, false}; + } else { + return {_St_uidvid::_none, false}; + } + } + + template + inline constexpr _Choice_t<_St_uv> _Choice_uv = _Choose_uv(); + + template + inline constexpr _Choice_t<_St_uidvid> _Choice_uidvid = _Choose_uidvid(); + + struct _fn { + // contains_in_edge(g, u, v) - both vertex descriptors + template + [[nodiscard]] constexpr bool operator()(G&& g, const U& u, const V& v) const + noexcept(_Choice_uv._No_throw) + requires(_Choice_uv, std::remove_cvref_t, std::remove_cvref_t>._Strategy != + _St_uv::_none) + { + using _G = std::remove_cvref_t; + using _U = std::remove_cvref_t; + using _V = std::remove_cvref_t; + + if constexpr (_Choice_uv<_G, _U, _V>._Strategy == _St_uv::_member) { + return g.contains_in_edge(u, v); + } else if constexpr (_Choice_uv<_G, _U, _V>._Strategy == _St_uv::_adl) { + return contains_in_edge(g, u, v); + } else if constexpr (_Choice_uv<_G, _U, _V>._Strategy == _St_uv::_default) { + // "contains incoming to u from v" = "edge from v to u exists" + auto target_uid = vertex_id(std::forward(g), u); + auto edge_range = edges(std::forward(g), v); + auto it = std::ranges::find_if(edge_range, [&](const auto& e) { + return static_cast>(target_id(std::forward(g), e)) == + static_cast>(target_uid); + }); + return it != std::ranges::end(edge_range); + } + } + + // contains_in_edge(g, uid, vid) - both vertex IDs + template + requires(!vertex_descriptor_type) && + (!vertex_descriptor_type) + [[nodiscard]] constexpr bool operator()(G&& g, const UId& uid, const VId& vid) const + noexcept(_Choice_uidvid._No_throw) + requires( + _Choice_uidvid, std::remove_cvref_t, std::remove_cvref_t>._Strategy != + _St_uidvid::_none) + { + using _G = std::remove_cvref_t; + using _UId = std::remove_cvref_t; + using _VId = std::remove_cvref_t; + + if constexpr (_Choice_uidvid<_G, _UId, _VId>._Strategy == _St_uidvid::_member) { + if constexpr (requires { typename _G::vertex_id_type; }) { + return g.contains_in_edge(static_cast(uid), + static_cast(vid)); + } else { + return g.contains_in_edge(uid, vid); + } + } else if constexpr (_Choice_uidvid<_G, _UId, _VId>._Strategy == _St_uidvid::_adl) { + if constexpr (requires { typename _G::vertex_id_type; }) { + return contains_in_edge(g, static_cast(uid), + static_cast(vid)); + } else { + return contains_in_edge(g, uid, vid); + } + } else if constexpr (_Choice_uidvid<_G, _UId, _VId>._Strategy == _St_uidvid::_default) { + // "contains incoming to uid from vid" = "edge from vid to uid exists" + auto v = *find_vertex(std::forward(g), static_cast>(vid)); + auto edge_range = edges(std::forward(g), v); + auto it = std::ranges::find_if(edge_range, [&](const auto& e) { + return static_cast>(target_id(std::forward(g), e)) == + static_cast>(uid); + }); + return it != std::ranges::end(edge_range); + } + } + }; + } // namespace _contains_in_edge + +} // namespace _cpo_impls + +// ============================================================================= +// contains_in_edge - Public CPO instance +// ============================================================================= + +inline namespace _cpo_instances { + /** + * @brief CPO for checking if an incoming edge exists to vertex u from source vertex v + * + * Usage: + * bool exists = graph::contains_in_edge(my_graph, u_descriptor, v_descriptor); + * bool exists = graph::contains_in_edge(my_graph, target_id, source_id); + * + * Returns: true if incoming edge exists, false otherwise + */ + inline constexpr _cpo_impls::_contains_in_edge::_fn contains_in_edge{}; } // namespace _cpo_instances namespace _cpo_impls { @@ -2017,12 +2816,13 @@ inline namespace _cpo_instances { * @brief CPO for checking if an edge exists from source vertex u to target vertex v * * Usage: - * bool exists = graph::contains_edge(my_graph, u_descriptor, v_descriptor); - * bool exists = graph::contains_edge(my_graph, source_id, target_id); + * bool exists = graph::contains_out_edge(my_graph, u_descriptor, v_descriptor); + * bool exists = graph::contains_out_edge(my_graph, source_id, target_id); * * Returns: true if edge exists, false otherwise */ - inline constexpr _cpo_impls::_contains_edge::_fn contains_edge{}; + inline constexpr _cpo_impls::_contains_edge::_fn contains_out_edge{}; + inline constexpr auto& contains_edge = contains_out_edge; } // namespace _cpo_instances namespace _cpo_impls { @@ -2118,7 +2918,7 @@ namespace _cpo_impls { } // namespace _cpo_impls // ============================================================================= -// has_edge(g) - Public CPO instance +// has_edges(g) - Public CPO instance // ============================================================================= inline namespace _cpo_instances { @@ -2126,11 +2926,11 @@ inline namespace _cpo_instances { * @brief CPO for checking if the graph has any edges * * Usage: - * bool has_edges = graph::has_edge(my_graph); + * bool result = graph::has_edges(my_graph); * * Returns: true if graph has at least one edge, false otherwise */ - inline constexpr _cpo_impls::_has_edge::_fn has_edge{}; + inline constexpr _cpo_impls::_has_edge::_fn has_edges{}; } // namespace _cpo_instances namespace _cpo_impls { diff --git a/include/graph/adj_list/edge_descriptor.hpp b/include/graph/adj_list/edge_descriptor.hpp index 0a070f4..ae9de25 100644 --- a/include/graph/adj_list/edge_descriptor.hpp +++ b/include/graph/adj_list/edge_descriptor.hpp @@ -18,15 +18,27 @@ namespace graph::adj_list { * Provides a lightweight, type-safe handle to edges stored in various container types. * Maintains both the edge location and the source vertex descriptor. * + * The EdgeDirection tag controls source/target semantics: + * - out_edge_tag (default): source_ is the source vertex, target navigates .edges() + * - in_edge_tag: source_ is the target vertex, source navigates .in_edges() + * * @tparam EdgeIter Iterator type of the underlying edge container * @tparam VertexIter Iterator type of the vertex container + * @tparam EdgeDirection Direction tag: out_edge_tag or in_edge_tag */ -template +template class edge_descriptor { public: using edge_iterator_type = EdgeIter; using vertex_iterator_type = VertexIter; using vertex_desc = vertex_descriptor; + using edge_direction = EdgeDirection; + + /// True when this descriptor wraps an in-edge (source/target semantics are swapped). + static constexpr bool is_in_edge = std::is_same_v; + + /// True when this descriptor wraps an out-edge (default direction). + static constexpr bool is_out_edge = std::is_same_v; // Conditional storage type for edge: size_t for random access, iterator for forward using edge_storage_type = std::conditional_t, std::size_t, EdgeIter>; @@ -93,8 +105,87 @@ class edge_descriptor { * 3. Wrap return expressions in parentheses: return (edge_val.first); * See descriptor.md "Lambda Reference Binding Issues" section for details. */ + /** + * @brief Get the source vertex ID by navigating the edge container (in-edge only) + * @param vertex_data The vertex/edge data structure passed from the CPO + * @return The source vertex identifier extracted from the native in-edge + * + * For in-edge descriptors, source_id() (no args) returns the owning vertex ID + * which is actually the target. This overload navigates the in_edges container + * to retrieve the native edge's source_id — the actual source vertex. + * + * Only available when EdgeDirection is in_edge_tag. + */ + template + [[nodiscard]] constexpr auto source_id(const VertexData& vertex_data) const noexcept + requires(is_in_edge) { + const auto& edge_container = [&]() -> decltype(auto) { + if constexpr (requires { vertex_data.in_edges(); }) { + return vertex_data.in_edges(); + } else if constexpr (requires { + vertex_data.first; + vertex_data.second; + }) { + if constexpr (requires { vertex_data.second.in_edges(); }) { + return vertex_data.second.in_edges(); + } else { + return vertex_data.second; + } + } else { + return vertex_data; + } + }(); + + const auto& edge_val = [&]() -> decltype(auto) { + if constexpr (std::random_access_iterator) { + return edge_container[edge_storage_]; + } else { + return *edge_storage_; + } + }(); + + // Extract source_id from the native edge + if constexpr (requires { edge_val.second.source_id(); }) { + return edge_val.second.source_id(); + } else if constexpr (requires { edge_val.source_id(); }) { + return edge_val.source_id(); + } else if constexpr (requires { edge_val.first; }) { + return edge_val.first; + } else if constexpr (requires { std::get<0>(edge_val); }) { + return std::get<0>(edge_val); + } else { + return edge_val; + } + } + + /** + * @brief Get the target vertex ID for an in-edge descriptor + * @param vertex_data The vertex/edge data structure passed from the CPO (unused) + * @return The target vertex identifier (the owning vertex) + * + * For in-edges, the owning vertex IS the target — no container navigation needed. + * The vertex_data parameter is accepted for interface consistency with the CPO. + * + * Only available when EdgeDirection is in_edge_tag. + */ + template + [[nodiscard]] constexpr auto target_id(const VertexData& /*vertex_data*/) const noexcept + requires(is_in_edge) { + return source_.vertex_id(); + } + + /** + * @brief Get the target vertex ID for an out-edge descriptor + * @param vertex_data The vertex/edge data structure passed from the CPO + * @return The target vertex identifier extracted from the edge + * + * Navigates the out-edge container (.edges()) to extract the target vertex ID. + * + * Only available when EdgeDirection is out_edge_tag. + */ template - [[nodiscard]] constexpr auto target_id(const VertexData& vertex_data) const noexcept { + [[nodiscard]] constexpr auto target_id(const VertexData& vertex_data) const noexcept + requires(is_out_edge) { using edge_value_type = typename std::iterator_traits::value_type; // Extract the actual edge container from vertex_data: @@ -140,7 +231,7 @@ class edge_descriptor { // Access the edge through .second, then call target_id() return edge_val.second.target_id(); } else if constexpr (requires { edge_val.target_id(); }) { - // Edge object with target_id() member (e.g., dynamic_edge) + // Edge object with target_id() member (e.g., dynamic_out_edge) return edge_val.target_id(); } else if constexpr (requires { edge_val.first; }) { // Pair-like: .first is the target ID @@ -164,9 +255,13 @@ class edge_descriptor { */ template [[nodiscard]] constexpr decltype(auto) underlying_value(VertexData& vertex_data) const noexcept { - // Extract the actual edge container from vertex_data (see target_id for details) + // Extract the actual edge container from vertex_data + // For in-edge descriptors, navigate .in_edges() instead of .edges() if constexpr (requires { vertex_data.edges(); }) { - auto& edge_container = vertex_data.edges(); + auto& edge_container = [&]() -> decltype(auto) { + if constexpr (is_in_edge && requires { vertex_data.in_edges(); }) return (vertex_data.in_edges()); + else return (vertex_data.edges()); + }(); if constexpr (std::random_access_iterator) { return (edge_container[edge_storage_]); } else { @@ -177,7 +272,10 @@ class edge_descriptor { vertex_data.second; }) { if constexpr (requires { vertex_data.second.edges(); }) { - auto& edge_container = vertex_data.second.edges(); + auto& edge_container = [&]() -> decltype(auto) { + if constexpr (is_in_edge && requires { vertex_data.second.in_edges(); }) return (vertex_data.second.in_edges()); + else return (vertex_data.second.edges()); + }(); if constexpr (std::random_access_iterator) { return (edge_container[edge_storage_]); } else { @@ -207,9 +305,12 @@ class edge_descriptor { */ template [[nodiscard]] constexpr decltype(auto) underlying_value(const VertexData& vertex_data) const noexcept { - // Extract the actual edge container from vertex_data (see target_id for details) + // Extract the actual edge container from vertex_data if constexpr (requires { vertex_data.edges(); }) { - const auto& edge_container = vertex_data.edges(); + const auto& edge_container = [&]() -> decltype(auto) { + if constexpr (is_in_edge && requires { vertex_data.in_edges(); }) return (vertex_data.in_edges()); + else return (vertex_data.edges()); + }(); if constexpr (std::random_access_iterator) { return (edge_container[edge_storage_]); } else { @@ -220,7 +321,10 @@ class edge_descriptor { vertex_data.second; }) { if constexpr (requires { vertex_data.second.edges(); }) { - const auto& edge_container = vertex_data.second.edges(); + const auto& edge_container = [&]() -> decltype(auto) { + if constexpr (is_in_edge && requires { vertex_data.second.in_edges(); }) return (vertex_data.second.in_edges()); + else return (vertex_data.second.edges()); + }(); if constexpr (std::random_access_iterator) { return (edge_container[edge_storage_]); } else { @@ -260,9 +364,13 @@ class edge_descriptor { [[nodiscard]] constexpr decltype(auto) inner_value(VertexData& vertex_data) const noexcept { using edge_value_type = typename std::iterator_traits::value_type; - // Extract the actual edge container from vertex_data (see target_id for details) + // Extract the actual edge container from vertex_data + // For in-edge descriptors, navigate .in_edges() instead of .edges() if constexpr (requires { vertex_data.edges(); }) { - auto& edge_container = vertex_data.edges(); + auto& edge_container = [&]() -> decltype(auto) { + if constexpr (is_in_edge && requires { vertex_data.in_edges(); }) return (vertex_data.in_edges()); + else return (vertex_data.edges()); + }(); // Simple type: just the target ID, return it (no separate property) if constexpr (std::integral) { if constexpr (std::random_access_iterator) { @@ -450,9 +558,13 @@ class edge_descriptor { [[nodiscard]] constexpr decltype(auto) inner_value(const VertexData& vertex_data) const noexcept { using edge_value_type = typename std::iterator_traits::value_type; - // Extract the actual edge container from vertex_data (see target_id for details) + // Extract the actual edge container from vertex_data + // For in-edge descriptors, navigate .in_edges() instead of .edges() if constexpr (requires { vertex_data.edges(); }) { - const auto& edge_container = vertex_data.edges(); + const auto& edge_container = [&]() -> decltype(auto) { + if constexpr (is_in_edge && requires { vertex_data.in_edges(); }) return (vertex_data.in_edges()); + else return (vertex_data.edges()); + }(); // Simple type: just the target ID, return it (no separate property) if constexpr (std::integral) { if constexpr (std::random_access_iterator) { @@ -655,9 +767,9 @@ class edge_descriptor { // Hash specialization for std::unordered containers namespace std { -template -struct hash> { - [[nodiscard]] size_t operator()(const graph::adj_list::edge_descriptor& ed) const noexcept { +template +struct hash> { + [[nodiscard]] size_t operator()(const graph::adj_list::edge_descriptor& ed) const noexcept { // Combine hash of edge storage and source vertex size_t h1 = [&ed]() { if constexpr (std::random_access_iterator) { diff --git a/include/graph/adj_list/edge_descriptor_view.hpp b/include/graph/adj_list/edge_descriptor_view.hpp index b652c90..07527fb 100644 --- a/include/graph/adj_list/edge_descriptor_view.hpp +++ b/include/graph/adj_list/edge_descriptor_view.hpp @@ -20,11 +20,12 @@ namespace graph::adj_list { * * @tparam EdgeIter Iterator type of the underlying edge container * @tparam VertexIter Iterator type of the vertex container + * @tparam EdgeDirection Direction tag: out_edge_tag (default) or in_edge_tag */ -template -class edge_descriptor_view : public std::ranges::view_interface> { +template +class edge_descriptor_view : public std::ranges::view_interface> { public: - using edge_desc = edge_descriptor; + using edge_desc = edge_descriptor; using vertex_desc = vertex_descriptor; using edge_storage_type = typename edge_desc::edge_storage_type; @@ -181,6 +182,6 @@ edge_descriptor_view(Range&&, VertexDesc) } // namespace graph::adj_list // Enable borrowed_range for edge_descriptor_view to allow std::ranges operations on temporaries -template -inline constexpr bool std::ranges::enable_borrowed_range> = +template +inline constexpr bool std::ranges::enable_borrowed_range> = true; diff --git a/include/graph/adj_list/vertex_descriptor.hpp b/include/graph/adj_list/vertex_descriptor.hpp index 07336c0..71fb169 100644 --- a/include/graph/adj_list/vertex_descriptor.hpp +++ b/include/graph/adj_list/vertex_descriptor.hpp @@ -80,11 +80,6 @@ class vertex_descriptor { } } - /** - * @brief Get the underlying container value (const version) - * @param container The underlying vertex container - * @return Const reference to the vertex data from the container - */ template [[nodiscard]] constexpr decltype(auto) underlying_value(const Container& container) const noexcept { if constexpr (std::random_access_iterator) { diff --git a/include/graph/algorithm/connected_components.hpp b/include/graph/algorithm/connected_components.hpp index 72688e2..a7a822c 100644 --- a/include/graph/algorithm/connected_components.hpp +++ b/include/graph/algorithm/connected_components.hpp @@ -33,6 +33,7 @@ namespace graph { // Using declarations for new namespace structure using adj_list::index_adjacency_list; +using adj_list::index_bidirectional_adjacency_list; using adj_list::adjacency_list; using adj_list::vertex_id_t; using adj_list::edge_t; @@ -214,6 +215,132 @@ void kosaraju(G&& g, // graph } } +//============================================================================= +// kosaraju (bidirectional) - Single-graph SCC using in_edges +//============================================================================= + +/** + * @brief Finds strongly connected components using in_edges (no transpose needed). + * + * When the graph satisfies `index_bidirectional_adjacency_list`, the second DFS + * pass can traverse incoming edges directly instead of requiring a separate + * transpose graph. This eliminates the O(V + E) cost of constructing and + * storing the transpose. + * + * @par Complexity Analysis + * + * | Case | Time | Space | + * |------|------|-------| + * | All cases | O(V + E) | O(V) | + * + * Same asymptotic cost as the two-graph overload, but with lower constant + * factor due to avoided transpose construction. + * + * @par Container Requirements + * + * - Requires: `index_bidirectional_adjacency_list` (in_edges + index vertices) + * - Requires: `random_access_range` + * - Works with: All bidirectional `dynamic_graph` container combinations + * + * @tparam G Graph type (must satisfy index_bidirectional_adjacency_list concept) + * @tparam Component Random access range for component IDs + * + * @param g The directed bidirectional graph to analyze + * @param component Output: component[v] = component ID for vertex v + * + * @pre `component.size() >= num_vertices(g)` + * + * @post `component[v]` contains the SCC ID for vertex v + * @post Component IDs are assigned 0, 1, 2, ..., num_components-1 + * @post Vertices in the same SCC have the same component ID + * + * @par Example + * @code + * // Create bidirectional directed graph: 0->1->2->0 (cycle), 2->3 + * using Traits = container::vov_graph_traits; + * container::dynamic_graph g({{0,1}, {1,2}, {2,0}, {2,3}}); + * + * std::vector component(num_vertices(g)); + * kosaraju(g, component); // No transpose needed! + * @endcode + * + * @see kosaraju(G&&, GT&&, Component&) For non-bidirectional graphs + */ +template +void kosaraju(G&& g, // bidirectional graph + Component& component // out: strongly connected component assignment +) { + size_t N(num_vertices(g)); + std::vector visited(N, false); + using CT = typename std::decay::type; + std::fill(component.begin(), component.end(), std::numeric_limits::max()); + std::vector> order; + + auto& g_ref = g; + + // First pass: iterative DFS to compute finish times (same as two-graph version) + auto dfs_finish_order = [&](vertex_id_t start) { + std::stack, bool>> stack; + stack.push({start, false}); + visited[start] = true; + + while (!stack.empty()) { + auto [uid, children_visited] = stack.top(); + stack.pop(); + + if (children_visited) { + order.push_back(uid); + } else { + stack.push({uid, true}); + + auto uid_vertex = *find_vertex(g_ref, uid); + for (auto&& [vid, e] : views::incidence(g_ref, uid_vertex)) { + if (!visited[vid]) { + visited[vid] = true; + stack.push({vid, false}); + } + } + } + } + }; + + for (auto&& vinfo : views::vertexlist(g_ref)) { + auto uid = vertex_id(g_ref, vinfo.vertex); + if (!visited[uid]) { + dfs_finish_order(uid); + } + } + + // Second pass: DFS on reverse edges (via in_edges) in reverse finish order. + // Each DFS tree corresponds to exactly one SCC. + size_t cid = 0; + std::ranges::reverse_view reverse{order}; + for (auto& uid : reverse) { + if (component[uid] == std::numeric_limits::max()) { + // Manual iterative DFS using in_edges + source_id + std::stack> dfs_stack; + dfs_stack.push(uid); + component[uid] = cid; + + while (!dfs_stack.empty()) { + auto current = dfs_stack.top(); + dfs_stack.pop(); + + auto v = *adj_list::find_vertex(g_ref, current); + for (auto&& ie : adj_list::in_edges(g_ref, v)) { + auto src = adj_list::source_id(g_ref, ie); + if (component[src] == std::numeric_limits::max()) { + component[src] = cid; + dfs_stack.push(src); + } + } + } + ++cid; + } + } +} + //============================================================================= // connected_components - Connected Components (Undirected Graphs) //============================================================================= diff --git a/include/graph/container/compressed_graph.hpp b/include/graph/container/compressed_graph.hpp index 55d7c06..5c2be1c 100644 --- a/include/graph/container/compressed_graph.hpp +++ b/include/graph/container/compressed_graph.hpp @@ -1326,7 +1326,7 @@ class compressed_graph_base * @param g The graph (forwarding reference) * @return true if the graph has at least one edge, false otherwise * @note Complexity: O(1) - direct size check - * @note This is the ADL customization point for the has_edge(g) CPO + * @note This is the ADL customization point for the has_edges(g) CPO */ template requires std::derived_from, compressed_graph_base> diff --git a/include/graph/container/dynamic_graph.hpp b/include/graph/container/dynamic_graph.hpp index f7059a9..cccfd1e 100644 --- a/include/graph/container/dynamic_graph.hpp +++ b/include/graph/container/dynamic_graph.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include "graph/graph.hpp" @@ -38,6 +39,7 @@ using adj_list::vertex_descriptor_view; using adj_list::edge_descriptor_view; using adj_list::vertex_descriptor_type; using adj_list::edge_descriptor_type; +using adj_list::in_edge_tag; //-------------------------------------------------------------------------------------------------- // dynamic_graph traits forward references @@ -53,31 +55,31 @@ using adj_list::edge_descriptor_type; // #include // map + forward_list // -template +template struct vofl_graph_traits; -template +template struct vol_graph_traits; -template +template struct vov_graph_traits; -template +template struct vod_graph_traits; -template +template struct dofl_graph_traits; -template +template struct dol_graph_traits; -template +template struct dov_graph_traits; -template +template struct dod_graph_traits; -template +template struct mofl_graph_traits; @@ -85,18 +87,21 @@ struct mofl_graph_traits; // dynamic_graph forward references // -template -class dynamic_edge; +template +class dynamic_out_edge; -template +template +class dynamic_in_edge; + +template class dynamic_vertex; -template > +template > class dynamic_graph; //-------------------------------------------------------------------------------------------------- @@ -132,22 +137,52 @@ using dynamic_adjacency_graph = dynamic_graph; //-------------------------------------------------------------------------------------------------- -// dynamic_edge +// detection-idiom helpers (shared by dynamic_graph_base and dynamic_vertex_bidir_base) +// +namespace detail { + + // Minimal detection idiom (no Boost dependency). + namespace _detect { + template class Op, class... Args> + struct detector { using type = Default; }; + + template class Op, class... Args> + struct detector>, Op, Args...> { + using type = Op; + }; + } // namespace _detect + + /// Returns Op if it is well-formed, otherwise Default. + template class Op, class... Args> + using detected_or_t = typename _detect::detector::type; + + // Detectors for optional Traits members added by non-uniform bidirectional traits. + template + using detect_in_edge_type = typename T::in_edge_type; + + template + using detect_in_edges_type = typename T::in_edges_type; + +} // namespace detail + +//-------------------------------------------------------------------------------------------------- +// dynamic_out_edge // /** * @ingroup graph_containers - * @brief Implementation of the @c target_id(g,uv) property of a @c dynamic_edge in a @c dynamic_graph. + * @brief Implementation of the @c target_id(g,uv) property of a @c dynamic_out_edge in a @c dynamic_graph. * - * This is one of three composable classes used to define properties for a @c dynamic_edge, the - * others being dynamic_edge_source for the source id and dynamic_edge_value for an edge value. + * This is one of two composable classes used to define properties for a @c dynamic_out_edge, the + * other being @c dynamic_edge_value for an edge value. (@c dynamic_edge_source is used by + * @c dynamic_in_edge to store the source id.) * * Unlike the other composable edge property classes, this class doesn't have an option for not - * existing. The target id always exists. It could easily be merged into the dynamic_edge class, + * existing. The target id always exists. It could easily be merged into the dynamic_out_edge class, * but was kept separate for design symmetry with the other property classes. * * @tparam EV The edge value type. If "void" is used no user value is stored on the edge and @@ -156,19 +191,17 @@ using dynamic_adjacency_graph = dynamic_graph +template class dynamic_edge_target { public: using vertex_id_type = VId; using value_type = EV; - using graph_type = dynamic_graph; - using vertex_type = dynamic_vertex; - using edge_type = dynamic_edge; + using graph_type = dynamic_graph; + using vertex_type = dynamic_vertex; + using edge_type = dynamic_out_edge; public: constexpr dynamic_edge_target(vertex_id_type targ_id) : target_id_(targ_id) {} @@ -202,17 +235,13 @@ class dynamic_edge_target { /** * @ingroup graph_containers - * @brief Implementation of the @c source_id(g,uv) property of a @c dynamic_edge in a @c dynamic_graph. + * @brief Implementation of the @c source_id(g,uv) property of an edge in a @c dynamic_graph. * - * This is one of three composable classes used to define properties for a @c dynamic_edge, the + * This is one of three composable classes used to define properties for an edge, the * others being dynamic_edge_target for the target id and dynamic_edge_value for an edge value. * - * A specialization for @c Sourced=false exists as an empty class so any call to @c source_id(g,uv) will - * generate a compile error. - * - * The edge descriptor provides a reliable way to get the source vertex id, so storing it on the edge - * is optional for most use cases. Howeever, it's useful to support it to provide a fully self-contained - * edge object that can be used in the context of an independent graph that is effective in its own right. + * This class unconditionally stores source_id and serves as the base for @c dynamic_in_edge. + * It is not used by @c dynamic_out_edge (which stores only a target id and optional edge value). * * @tparam EV The edge value type. If "void" is used no user value is stored on the edge and * calls to @c edge_value(g,uv) will generate a compile error. @@ -220,19 +249,17 @@ class dynamic_edge_target { * and calls to @c vertex_value(g,u) will generate a compile error. * @tparam GV The graph value type. If "void" is used no user value is stored on the graph * and calls to @c graph_value(g) will generate a compile error. - * @tparam Sourced Is a source vertex id stored on the edge? If false, calls to @c source_id(g,uv) - * and @c source(g,uv) will generate a compile error. * @tparam VId Vertex id type * @tparam Traits Defines the types for vertex and edge containers. */ -template +template class dynamic_edge_source { public: using vertex_id_type = VId; using value_type = EV; - using graph_type = dynamic_graph; - using vertex_type = dynamic_vertex; - using edge_type = dynamic_edge; + using graph_type = dynamic_graph; + using vertex_type = dynamic_vertex; + using edge_type = dynamic_out_edge; public: constexpr dynamic_edge_source(vertex_id_type source_id) : source_id_(source_id) {} @@ -265,34 +292,9 @@ class dynamic_edge_source { /** * @ingroup graph_containers - * @brief Implementation of the @c source_id(g,uv) property of a @c dynamic_edge in a @c dynamic_graph. - * - * This is one of three composable classes used to define properties for a @c dynamic_edge, the - * others being dynamic_edge_target for the target id and dynamic_edge_value for an edge value. - * - * This specialization for @c Sourced=false is a simple placeholder that doesn't define anything. - * If the user attempts to call to @c source_id(g,uv) or @c source(g,uv) will generate a compile - * error so they can resolve the incorrect usage. + * @brief Implementation of the @c edge_value(g,uv) property of a @c dynamic_out_edge in a @c dynamic_graph. * - * @tparam EV The edge value type. If "void" is used no user value is stored on the edge and - * calls to @c edge_value(g,uv) will generate a compile error. - * @tparam VV The vertex value type. If "void" is used no user value is stored on the vertex - * and calls to @c vertex_value(g,u) will generate a compile error. - * @tparam GV The graph value type. If "void" is used no user value is stored on the graph - * and calls to @c graph_value(g) will generate a compile error. - * @tparam Sourced [false] Source id is not stored on the edge. Use of @c source_id(g,uv) or - * source(g,uv) will generate an error. - * @tparam VId Vertex id type - * @tparam Traits Defines the types for vertex and edge containers. -*/ -template -class dynamic_edge_source {}; - -/** - * @ingroup graph_containers - * @brief Implementation of the @c edge_value(g,uv) property of a @c dynamic_edge in a @c dynamic_graph. - * - * This is one of three composable classes used to define properties for a @c dynamic_edge, the + * This is one of three composable classes used to define properties for a @c dynamic_out_edge, the * others being dynamic_edge_target for the target id and dynamic_edge_source for source id. * * A specialization for @c EV=void exists as an empty class so any call to @c edge_value(g,uv) will @@ -304,18 +306,16 @@ class dynamic_edge_source {}; * and calls to @c vertex_value(g,u) will generate a compile error. * @tparam GV The graph value type. If "void" is used no user value is stored on the graph * and calls to @c graph_value(g) will generate a compile error. - * @tparam Sourced Is a source vertex id stored on the edge? If false, calls to @c source_id(g,uv) - * and @c source(g,uv) will generate a compile error. * @tparam VId Vertex id type * @tparam Traits Defines the types for vertex and edge containers. */ -template +template class dynamic_edge_value { public: using value_type = EV; - using graph_type = dynamic_graph; - using vertex_type = dynamic_vertex; - using edge_type = dynamic_edge; + using graph_type = dynamic_graph; + using vertex_type = dynamic_vertex; + using edge_type = dynamic_out_edge; public: constexpr dynamic_edge_value(const value_type& value) : value_(value) {} @@ -345,9 +345,9 @@ class dynamic_edge_value { /** * @ingroup graph_containers - * @brief Implementation of the @c edge_value(g,uv) property of a @c dynamic_edge in a @c dynamic_graph. + * @brief Implementation of the @c edge_value(g,uv) property of a @c dynamic_out_edge in a @c dynamic_graph. * - * This is one of three composable classes used to define properties for a @c dynamic_edge, the + * This is one of three composable classes used to define properties for a @c dynamic_out_edge, the * others being dynamic_edge_target for the target id and dynamic_edge_source for source id. * * This specialization for @c EV=void is a simple placeholder that doesn't define anything. @@ -360,31 +360,24 @@ class dynamic_edge_value { * and calls to @c vertex_value(g,u) will generate a compile error. * @tparam GV The graph value type. If "void" is used no user value is stored on the graph * and calls to @c graph_value(g) will generate a compile error. - * @tparam Sourced Is a source vertex id stored on the edge? If false, calls to @c source_id(g,uv) - * and @c source(g,uv) will generate a compile error. * @tparam VId Vertex id type * @tparam Traits Defines the types for vertex and edge containers. */ -template -class dynamic_edge_value {}; +template +class dynamic_edge_value {}; /** * @ingroup graph_containers - * @brief The edge class for a @c dynamic_graph. + * @brief The out-edge class for a @c dynamic_graph. * - * The edge is a composition of a target id, optional source id (@c Sourced) and optional edge value. - * This implementation supports a source id and edge value, where additional specializations exist - * for different combinations. The real functionality for properties is implemented in the - * @c dynamic_edge_target, @c dynamic_edge_source and @c dynamic_edge_value base classes. + * The out-edge stores a target id and an optional edge value. It does NOT store a source id — + * that role belongs to @c dynamic_in_edge (via @c dynamic_edge_source). The functionality for + * properties is implemented in the @c dynamic_edge_target and @c dynamic_edge_value base classes. * - * The following combinations of EV and Sourced are supported in @c dynamic_edge specializations. - * The only difference between them are the values taken by the constructors to match the - * inclusion/exclusion of the source Id and/or value properties. - * * < @c EV not void, @c Sourced=true > (this implementation) - * * < @c EV not void, @c Sourced=false > - * * < @c EV is void, @c Sourced=true > - * * < @c EV is void, @c Sourced=false > + * Two specializations exist keyed on @c EV: + * * < @c EV not void > (this implementation — stores target_id and edge value) + * * < @c EV is void > (stores target_id only) * * @tparam EV The edge value type. If "void" is used no user value is stored on the edge and * calls to @c edge_value(g,uv) will generate a compile error. @@ -392,65 +385,53 @@ class dynamic_edge_value {}; * and calls to @c vertex_value(g,u) will generate a compile error. * @tparam GV The graph value type. If "void" is used no user value is stored on the graph * and calls to @c graph_value(g) will generate a compile error. - * @tparam Sourced Is a source vertex id stored on the edge? If false, calls to @c source_id(g,uv) - * and @c source(g,uv) will generate a compile error. * @tparam VId Vertex id type * @tparam Traits Defines the types for vertex and edge containers. */ -template -class dynamic_edge - : public dynamic_edge_target - , public dynamic_edge_source - , public dynamic_edge_value { +template +class dynamic_out_edge + : public dynamic_edge_target + , public dynamic_edge_value { public: - using base_target_type = dynamic_edge_target; - using base_source_type = dynamic_edge_source; - using base_value_type = dynamic_edge_value; + using base_target_type = dynamic_edge_target; + using base_value_type = dynamic_edge_value; using vertex_id_type = VId; using value_type = EV; - using graph_type = dynamic_graph; - using vertex_type = dynamic_vertex; - using edge_type = dynamic_edge; + using graph_type = dynamic_graph; + using vertex_type = dynamic_vertex; + using edge_type = dynamic_out_edge; public: - constexpr dynamic_edge(vertex_id_type uid, vertex_id_type vid) : base_target_type(vid), base_source_type(uid) {} - constexpr dynamic_edge(vertex_id_type uid, vertex_id_type vid, const value_type& val) - : base_target_type(vid), base_source_type(uid), base_value_type(val) {} - constexpr dynamic_edge(vertex_id_type uid, vertex_id_type vid, value_type&& val) - : base_target_type(vid), base_source_type(uid), base_value_type(move(val)) {} + constexpr dynamic_out_edge(vertex_id_type targ_id) : base_target_type(targ_id) {} + constexpr dynamic_out_edge(vertex_id_type targ_id, const value_type& val) + : base_target_type(targ_id), base_value_type(val) {} + constexpr dynamic_out_edge(vertex_id_type targ_id, value_type&& val) + : base_target_type(targ_id), base_value_type(std::move(val)) {} - constexpr dynamic_edge() = default; - constexpr dynamic_edge(const dynamic_edge&) = default; - constexpr dynamic_edge(dynamic_edge&&) = default; - constexpr ~dynamic_edge() = default; + constexpr dynamic_out_edge() = default; + constexpr dynamic_out_edge(const dynamic_out_edge&) = default; + constexpr dynamic_out_edge(dynamic_out_edge&&) = default; + constexpr ~dynamic_out_edge() = default; - constexpr dynamic_edge& operator=(const dynamic_edge&) = default; - constexpr dynamic_edge& operator=(dynamic_edge&&) = default; + constexpr dynamic_out_edge& operator=(const dynamic_out_edge&) = default; + constexpr dynamic_out_edge& operator=(dynamic_out_edge&&) = default; // Comparison operators for ordered/unordered set containers - // Compare by (source_id, target_id) - edge value is intentionally excluded - constexpr auto operator<=>(const dynamic_edge& rhs) const noexcept { - if (auto cmp = base_source_type::source_id() <=> rhs.base_source_type::source_id(); cmp != 0) - return cmp; + // Compare by target_id only — edge value is intentionally excluded + constexpr auto operator<=>(const dynamic_out_edge& rhs) const noexcept { return base_target_type::target_id() <=> rhs.base_target_type::target_id(); } - constexpr bool operator==(const dynamic_edge& rhs) const noexcept { - return base_source_type::source_id() == rhs.base_source_type::source_id() && - base_target_type::target_id() == rhs.base_target_type::target_id(); + constexpr bool operator==(const dynamic_out_edge& rhs) const noexcept { + return base_target_type::target_id() == rhs.base_target_type::target_id(); } }; /** * @ingroup graph_containers - * @brief The edge class for a @c dynamic_graph. + * @brief The out-edge class for a @c dynamic_graph (EV=void specialization). * - * The edge is a composition of a target id, optional source id (@c Sourced) and optional edge value. - * This implementation supports a source id and edge value, where additional specializations exist - * for different combinations. The real functionality for properties is implemented in the - * @c dynamic_edge_target, @c dynamic_edge_source and @c dynamic_edge_value base classes. - * - * This is a specialization definition for @c EV=void and @c Sourced=true. + * Stores target_id only — no edge value. * * @tparam EV [void] A value type is not defined for the edge. Calling @c edge_value(g,uv) * will generate a compile error. @@ -458,174 +439,197 @@ class dynamic_edge * and calls to @c vertex_value(g,u) will generate a compile error. * @tparam GV The graph value type. If "void" is used no user value is stored on the graph * and calls to @c graph_value(g) will generate a compile error. - * @tparam Sourced [true] Source id is stored on the edge. Use of @c source_id(g,uv) and - * @c source(g,uv) will return a value without error. * @tparam VId Vertex id type * @tparam Traits Defines the types for vertex and edge containers. */ -template -class dynamic_edge - : public dynamic_edge_target - , public dynamic_edge_source - , public dynamic_edge_value { +template +class dynamic_out_edge + : public dynamic_edge_target + , public dynamic_edge_value { public: - using base_target_type = dynamic_edge_target; - using base_source_type = dynamic_edge_source; - using base_value_type = dynamic_edge_value; + using base_target_type = dynamic_edge_target; + using base_value_type = dynamic_edge_value; using vertex_id_type = VId; using value_type = void; - using graph_type = dynamic_graph; - using vertex_type = dynamic_vertex; - using edge_type = dynamic_edge; + using graph_type = dynamic_graph; + using vertex_type = dynamic_vertex; + using edge_type = dynamic_out_edge; public: - constexpr dynamic_edge(vertex_id_type src_id, vertex_id_type tgt_id) - : base_target_type(tgt_id), base_source_type(src_id) {} + constexpr dynamic_out_edge(vertex_id_type vid) : base_target_type(vid) {} - constexpr dynamic_edge() = default; - constexpr dynamic_edge(const dynamic_edge&) = default; - constexpr dynamic_edge(dynamic_edge&&) = default; - constexpr ~dynamic_edge() = default; + constexpr dynamic_out_edge() = default; + constexpr dynamic_out_edge(const dynamic_out_edge&) = default; + constexpr dynamic_out_edge(dynamic_out_edge&&) = default; + constexpr ~dynamic_out_edge() = default; - constexpr dynamic_edge& operator=(const dynamic_edge&) = default; - constexpr dynamic_edge& operator=(dynamic_edge&&) = default; + constexpr dynamic_out_edge& operator=(const dynamic_out_edge&) = default; + constexpr dynamic_out_edge& operator=(dynamic_out_edge&&) = default; // Comparison operators for ordered/unordered set containers - // Compare by (source_id, target_id) - constexpr auto operator<=>(const dynamic_edge& rhs) const noexcept { - if (auto cmp = base_source_type::source_id() <=> rhs.base_source_type::source_id(); cmp != 0) - return cmp; + // Compare by target_id only + constexpr auto operator<=>(const dynamic_out_edge& rhs) const noexcept { return base_target_type::target_id() <=> rhs.base_target_type::target_id(); } - constexpr bool operator==(const dynamic_edge& rhs) const noexcept { - return base_source_type::source_id() == rhs.base_source_type::source_id() && - base_target_type::target_id() == rhs.base_target_type::target_id(); + constexpr bool operator==(const dynamic_out_edge& rhs) const noexcept { + return base_target_type::target_id() == rhs.base_target_type::target_id(); } }; +//-------------------------------------------------------------------------------------------------- +// dynamic_in_edge — the in-edge class for bidirectional dynamic_graph. +// +// Stores source_id (via dynamic_edge_source) and an optional edge value (via dynamic_edge_value). +// Two specializations keyed on EV (as with dynamic_out_edge). +// /** * @ingroup graph_containers - * @brief The edge class for a @c dynamic_graph. - * - * The edge is a composition of a target id, optional source id (@c Sourced) and optional edge value. - * This implementation supports a source id and edge value, where additional specializations exist - * for different combinations. The real functionality for properties is implemented in the - * @c dynamic_edge_target, @c dynamic_edge_source and @c dynamic_edge_value base classes. + * @brief In-edge class for a bidirectional @c dynamic_graph (EV != void specialization). * - * This is a specialization definition for @c EV!=void and @c Sourced=false. - * - * @tparam EV The edge value type. If "void" is used no user value is stored on the edge. - * @tparam VV The vertex value type. If "void" is used no user value is stored on the vertex - * and calls to @c vertex_value(g,u) will generate a compile error. - * @tparam GV The graph value type. If "void" is used no user value is stored on the graph - * and calls to @c graph_value(g) will generate a compile error. - * @tparam Sourced [false] Source id is not stored on the edge. Use of @c source_id(g,uv) or - * @c source(g,uv) will generate a compile error. - * @tparam VId Vertex id type - * @tparam Traits Defines the types for vertex and edge containers. -*/ -template -class dynamic_edge - : public dynamic_edge_target - , public dynamic_edge_source - , public dynamic_edge_value { + * Stores @c source_id and an edge value. Used in the per-vertex reverse-adjacency list when + * non-uniform bidirectional traits define @c in_edge_type = dynamic_in_edge<...>. + * + * @tparam EV Edge value type (not void). + * @tparam VV Vertex value type. + * @tparam GV Graph value type. + * @tparam VId Vertex id type. + * @tparam Bidirectional Whether the graph is bidirectional. + * @tparam Traits Traits struct. + */ +template +class dynamic_in_edge + : public dynamic_edge_source + , public dynamic_edge_value { public: - using base_target_type = dynamic_edge_target; - using base_source_type = dynamic_edge_source; - using base_value_type = dynamic_edge_value; + using base_source_type = dynamic_edge_source; + using base_value_type = dynamic_edge_value; using vertex_id_type = VId; using value_type = EV; - using graph_type = dynamic_graph; - using vertex_type = dynamic_vertex; - using edge_type = dynamic_edge; public: - constexpr dynamic_edge(vertex_id_type targ_id) : base_target_type(targ_id) {} - constexpr dynamic_edge(vertex_id_type targ_id, const value_type& val) - : base_target_type(targ_id), base_value_type(val) {} - constexpr dynamic_edge(vertex_id_type targ_id, value_type&& val) - : base_target_type(targ_id), base_value_type(std::move(val)) {} - - constexpr dynamic_edge() = default; - constexpr dynamic_edge(const dynamic_edge&) = default; - constexpr dynamic_edge(dynamic_edge&&) = default; - constexpr ~dynamic_edge() = default; - - constexpr dynamic_edge& operator=(const dynamic_edge&) = default; - constexpr dynamic_edge& operator=(dynamic_edge&&) = default; - - // Comparison operators for ordered/unordered set containers - // Compare by target_id only (source_id not stored when Sourced=false) - constexpr auto operator<=>(const dynamic_edge& rhs) const noexcept { - return base_target_type::target_id() <=> rhs.base_target_type::target_id(); + constexpr dynamic_in_edge(vertex_id_type src_id) : base_source_type(src_id) {} + constexpr dynamic_in_edge(vertex_id_type src_id, const value_type& val) + : base_source_type(src_id), base_value_type(val) {} + constexpr dynamic_in_edge(vertex_id_type src_id, value_type&& val) + : base_source_type(src_id), base_value_type(std::move(val)) {} + + constexpr dynamic_in_edge() = default; + constexpr dynamic_in_edge(const dynamic_in_edge&) = default; + constexpr dynamic_in_edge(dynamic_in_edge&&) = default; + constexpr ~dynamic_in_edge() = default; + + constexpr dynamic_in_edge& operator=(const dynamic_in_edge&) = default; + constexpr dynamic_in_edge& operator=(dynamic_in_edge&&) = default; + + // Compare by source_id (edge value intentionally excluded, consistent with dynamic_out_edge) + constexpr auto operator<=>(const dynamic_in_edge& rhs) const noexcept { + return base_source_type::source_id() <=> rhs.base_source_type::source_id(); } - constexpr bool operator==(const dynamic_edge& rhs) const noexcept { - return base_target_type::target_id() == rhs.base_target_type::target_id(); + constexpr bool operator==(const dynamic_in_edge& rhs) const noexcept { + return base_source_type::source_id() == rhs.base_source_type::source_id(); } }; /** * @ingroup graph_containers - * @brief The edge class for a @c dynamic_graph. - * - * The edge is a composition of a target id, optional source id (@c Sourced) and optional edge value. - * This implementation supports a source id and edge value, where additional specializations exist - * for different combinations. The real functionality for properties is implemented in the - * @c dynamic_edge_target, @c dynamic_edge_source and @c dynamic_edge_value base classes. + * @brief In-edge class for a bidirectional @c dynamic_graph (EV = void specialization). * - * This is a specialization definition for @c EV=void and @c Sourced=false. - * - * @tparam EV [void] A value type is not defined for the edge. Calling @c edge_value(g,uv) - * will generate a compile error. - * @tparam VV The vertex value type. If "void" is used no user value is stored on the vertex - * and calls to @c vertex_value(g,u) will generate a compile error. - * @tparam GV The graph value type. If "void" is used no user value is stored on the graph - * and calls to @c graph_value(g) will generate a compile error. - * @tparam Sourced [false] Source id is not stored on the edge. Use of @c source_id(g,uv) or - * @c source(g,uv) will generate a compile error. - * @tparam VId Vertex id type - * @tparam Traits Defines the types for vertex and edge containers. -*/ -template -class dynamic_edge - : public dynamic_edge_target - , public dynamic_edge_source - , public dynamic_edge_value { + * Stores @c source_id only. Used in the per-vertex reverse-adjacency list. + * + * @tparam VV Vertex value type. + * @tparam GV Graph value type. + * @tparam VId Vertex id type. + * @tparam Bidirectional Whether the graph is bidirectional. + * @tparam Traits Traits struct. + */ +template +class dynamic_in_edge + : public dynamic_edge_source { public: - using base_target_type = dynamic_edge_target; - using base_source_type = dynamic_edge_source; - using base_value_type = dynamic_edge_value; + using base_source_type = dynamic_edge_source; using vertex_id_type = VId; using value_type = void; - using graph_type = dynamic_graph; - using vertex_type = dynamic_vertex; - using edge_type = dynamic_edge; public: - constexpr dynamic_edge(vertex_id_type vid) : base_target_type(vid) {} + constexpr dynamic_in_edge(vertex_id_type src_id) : base_source_type(src_id) {} - constexpr dynamic_edge() = default; - constexpr dynamic_edge(const dynamic_edge&) = default; - constexpr dynamic_edge(dynamic_edge&&) = default; - constexpr ~dynamic_edge() = default; + constexpr dynamic_in_edge() = default; + constexpr dynamic_in_edge(const dynamic_in_edge&) = default; + constexpr dynamic_in_edge(dynamic_in_edge&&) = default; + constexpr ~dynamic_in_edge() = default; - constexpr dynamic_edge& operator=(const dynamic_edge&) = default; - constexpr dynamic_edge& operator=(dynamic_edge&&) = default; + constexpr dynamic_in_edge& operator=(const dynamic_in_edge&) = default; + constexpr dynamic_in_edge& operator=(dynamic_in_edge&&) = default; - // Comparison operators for ordered/unordered set containers - // Compare by target_id only (source_id not stored when Sourced=false) - constexpr auto operator<=>(const dynamic_edge& rhs) const noexcept { - return base_target_type::target_id() <=> rhs.base_target_type::target_id(); + constexpr auto operator<=>(const dynamic_in_edge& rhs) const noexcept { + return base_source_type::source_id() <=> rhs.base_source_type::source_id(); } - constexpr bool operator==(const dynamic_edge& rhs) const noexcept { - return base_target_type::target_id() == rhs.base_target_type::target_id(); + constexpr bool operator==(const dynamic_in_edge& rhs) const noexcept { + return base_source_type::source_id() == rhs.base_source_type::source_id(); } }; +//-------------------------------------------------------------------------------------------------- +// dynamic_vertex_bidir_base — conditional base class for bidirectional vertex support +// +// When Bidirectional=false (default), this base is an empty class with no storage cost. +// When Bidirectional=true, it stores the reverse (incoming) edge container directly on the vertex. +// The in_edges() member function is automatically discovered by the in_edges CPO via the +// _vertex_member dispatch tier (u.inner_value(g).in_edges()), so no ADL friend is needed. +// +// Bidirectional support: the source_id CPO correctly identifies the origin +// vertex of each incoming edge via the dynamic_in_edge's native source_id member. +//-------------------------------------------------------------------------------------------------- + +/// @brief Empty base for non-bidirectional vertices (zero storage cost). +template +class dynamic_vertex_bidir_base { +public: + constexpr dynamic_vertex_bidir_base() = default; + constexpr dynamic_vertex_bidir_base(const dynamic_vertex_bidir_base&) = default; + constexpr dynamic_vertex_bidir_base(dynamic_vertex_bidir_base&&) = default; + constexpr ~dynamic_vertex_bidir_base() = default; + + constexpr dynamic_vertex_bidir_base& operator=(const dynamic_vertex_bidir_base&) = default; + constexpr dynamic_vertex_bidir_base& operator=(dynamic_vertex_bidir_base&&) = default; + + /// Accept allocator but ignore it (empty base). + template + constexpr dynamic_vertex_bidir_base(Alloc) {} +}; + +/// @brief Populated base for bidirectional vertices — stores incoming edges. +template +class dynamic_vertex_bidir_base { + +public: + // Use detected in_edges_type if the traits define it; otherwise fall back to edges_type. + // For all standard traits (27 files) this is identical to edges_type — zero behaviour change. + using edges_type = detail::detected_or_t; + using allocator_type = typename edges_type::allocator_type; + + constexpr dynamic_vertex_bidir_base() = default; + constexpr dynamic_vertex_bidir_base(const dynamic_vertex_bidir_base&) = default; + constexpr dynamic_vertex_bidir_base(dynamic_vertex_bidir_base&&) = default; + constexpr ~dynamic_vertex_bidir_base() = default; + + constexpr dynamic_vertex_bidir_base& operator=(const dynamic_vertex_bidir_base&) = default; + constexpr dynamic_vertex_bidir_base& operator=(dynamic_vertex_bidir_base&&) = default; + + constexpr dynamic_vertex_bidir_base(allocator_type alloc) : in_edges_(alloc) {} + + constexpr edges_type& in_edges() noexcept { return in_edges_; } + constexpr const edges_type& in_edges() const noexcept { return in_edges_; } + +private: + edges_type in_edges_; +}; + //-------------------------------------------------------------------------------------------------- // dynamic_vertex // @@ -642,19 +646,20 @@ class dynamic_edge * and calls to @c vertex_value(g,u) will generate a compile error. * @tparam GV The graph value type. If "void" is used no user value is stored on the graph * and calls to @c graph_value(g) will generate a compile error. - * @tparam Sourced Is a source vertex id stored on the edge? If false, calls to @c source_id(g,uv) - * and @c source(g,uv) will generate a compile error. * @tparam VId Vertex id type * @tparam Traits Defines the types for vertex and edge containers. */ -template -class dynamic_vertex_base { +template +class dynamic_vertex_base + : public dynamic_vertex_bidir_base { + using bidir_base = dynamic_vertex_bidir_base; + public: using vertex_id_type = VId; using value_type = VV; - using graph_type = dynamic_graph; - using vertex_type = dynamic_vertex; - using edge_type = dynamic_edge; + using graph_type = dynamic_graph; + using vertex_type = dynamic_vertex; + using edge_type = dynamic_out_edge; using edges_type = typename Traits::edges_type; using allocator_type = typename edges_type::allocator_type; @@ -667,7 +672,7 @@ class dynamic_vertex_base { constexpr dynamic_vertex_base& operator=(const dynamic_vertex_base&) = default; constexpr dynamic_vertex_base& operator=(dynamic_vertex_base&&) = default; - constexpr dynamic_vertex_base(allocator_type alloc) : edges_(alloc) {} + constexpr dynamic_vertex_base(allocator_type alloc) : bidir_base(alloc), edges_(alloc) {} public: constexpr edges_type& edges() noexcept { return edges_; } @@ -719,6 +724,29 @@ class dynamic_vertex_base { return edge_descriptor_view(edges_ref, u); } + // ADL friend functions for in-edges — only available for bidirectional graphs + template + requires vertex_descriptor_type + [[nodiscard]] friend constexpr auto in_edges(graph_type& g, const U& u) noexcept + requires(Bidirectional) + { + using vertex_iter_t = typename U::iterator_type; + auto& in_edges_ref = u.inner_value(g).in_edges(); + using edge_iter_t = decltype(in_edges_ref.begin()); + return edge_descriptor_view(in_edges_ref, u); + } + + template + requires vertex_descriptor_type + [[nodiscard]] friend constexpr auto in_edges(const graph_type& g, const U& u) noexcept + requires(Bidirectional) + { + using vertex_iter_t = typename U::iterator_type; + const auto& in_edges_ref = u.inner_value(g).in_edges(); + using edge_iter_t = decltype(in_edges_ref.begin()); + return edge_descriptor_view(in_edges_ref, u); + } + // friend constexpr typename edges_type::iterator // find_vertex_edge(graph_type& g, vertex_id_type uid, vertex_id_type vid) { // return std::ranges::find(g[uid].edges_, @@ -746,20 +774,18 @@ class dynamic_vertex_base { * and calls to @c vertex_value(g,u) will generate a compile error. * @tparam GV The graph value type. If "void" is used no user value is stored on the graph * and calls to @c graph_value(g) will generate a compile error. - * @tparam Sourced Is a source vertex id stored on the edge? If false, calls to @c source_id(g,uv) - * and @c source(g,uv) will generate a compile error. * @tparam VId Vertex id type * @tparam Traits Defines the types for vertex and edge containers. */ -template -class dynamic_vertex : public dynamic_vertex_base { +template +class dynamic_vertex : public dynamic_vertex_base { public: - using base_type = dynamic_vertex_base; + using base_type = dynamic_vertex_base; using vertex_id_type = VId; using value_type = remove_cvref_t; - using graph_type = dynamic_graph; - using vertex_type = dynamic_vertex; - using edge_type = dynamic_edge; + using graph_type = dynamic_graph; + using vertex_type = dynamic_vertex; + using edge_type = dynamic_out_edge; using edges_type = typename Traits::edges_type; using allocator_type = typename edges_type::allocator_type; @@ -806,21 +832,19 @@ class dynamic_vertex : public dynamic_vertex_base -class dynamic_vertex - : public dynamic_vertex_base { +template +class dynamic_vertex + : public dynamic_vertex_base { public: - using base_type = dynamic_vertex_base; + using base_type = dynamic_vertex_base; using vertex_id_type = VId; using value_type = void; - using graph_type = dynamic_graph; - using vertex_type = dynamic_vertex; - using edge_type = dynamic_edge; + using graph_type = dynamic_graph; + using vertex_type = dynamic_vertex; + using edge_type = dynamic_out_edge; using edges_type = typename Traits::edges_type; using allocator_type = typename edges_type::allocator_type; @@ -839,8 +863,8 @@ class dynamic_vertex /**------------------------------------------------------------------------------------------------- * @ingroup graph_containers * @brief dynamic_graph_base defines the core implementation for a graph with a variety - * characteristics including edge, vertex and graph value types, whether edges are sourced or not, - * the vertex id type, and selection of the containers used for vertices and edges. + * of characteristics including edge, vertex and graph value types, whether bidirectional in-edge + * support is enabled, the vertex id type, and selection of the containers used for vertices and edges. * * This class includes the vertices and functions to access them, and the constructors and * functions to load the vertices and edges into the graph. @@ -854,35 +878,51 @@ class dynamic_vertex * It has been added to assure the same interface as the compressed_graph, but is not implemented * at this time. * - * @tparam EV The edge value type. If "void" is used no user value is stored on the edge and - * calls to @c edge_value(g,uv) will generate a compile error. - * @tparam VV The vertex value type. If "void" is used no user value is stored on the vertex - * and calls to @c vertex_value(g,u) will generate a compile error. - * @tparam GV The graph value type. If "void" is used no user value is stored on the graph - * and calls to @c graph_value(g) will generate a compile error. - * @tparam Sourced Is a source vertex id stored on the edge? If false, calls to @c source_id(g,uv) - * and @c source(g,uv) will generate a compile error. - * @tparam VId The type used for the vertex id. - * @tparam Traits Defines the types for vertex and edge containers. + * @tparam EV The edge value type. If "void" is used no user value is stored on the edge and + * calls to @c edge_value(g,uv) will generate a compile error. + * @tparam VV The vertex value type. If "void" is used no user value is stored on the vertex + * and calls to @c vertex_value(g,u) will generate a compile error. + * @tparam GV The graph value type. If "void" is used no user value is stored on the graph + * and calls to @c graph_value(g) will generate a compile error. + * @tparam VId The type used for the vertex id. + * @tparam Traits Defines the types for vertex and edge containers. + * @tparam Bidirectional If true, maintains per-vertex reverse adjacency lists so that + * @c in_edges(g,u) is available. */ -template +template class dynamic_graph_base { + public: // types - using graph_type = dynamic_graph; + using graph_type = dynamic_graph; using graph_traits = Traits; using partition_id_type = VId; using partition_vector = std::vector; using vertex_id_type = VId; - using vertex_type = dynamic_vertex; + using vertex_type = dynamic_vertex; using vertices_type = typename Traits::vertices_type; using vertex_allocator_type = typename vertices_type::allocator_type; using size_type = typename vertices_type::size_type; using edges_type = typename Traits::edges_type; using edge_allocator_type = typename edges_type::allocator_type; - using edge_type = dynamic_edge; + using edge_type = dynamic_out_edge; + + // Detection-idiom derived aliases (Phase 1 — dead code until Phase 2). + // For standard traits that don't define in_edge_type/in_edges_type, these fall back + // to edge_type and edges_type respectively, keeping runtime behavior unchanged. + using out_edge_type = edge_type; + using in_edge_type = detail::detected_or_t; + using in_edges_type = detail::detected_or_t; + + // Verify alias consistency: non-uniform traits that define in_edge_type must also define + // in_edges_type so the in-edge container element type matches. + static_assert(std::same_as || + !std::same_as, + "Non-uniform traits must also define in_edges_type when in_edge_type " + "differs from edge_type"); public: // Construction/Destruction/Assignment constexpr dynamic_graph_base() = default; @@ -1269,18 +1309,18 @@ class dynamic_graph_base { // We need to ensure both source and target vertices exist (void)vertices_[e.target_id]; // ensure target vertex exists auto& vertex_edges = vertices_[e.source_id].edges(); - if constexpr (Sourced) { - if constexpr (is_void_v) { - emplace_edge(vertex_edges, e.target_id, edge_type(e.source_id, e.target_id)); - } else { - emplace_edge(vertex_edges, e.target_id, edge_type(e.source_id, e.target_id, std::move(e.value))); + if constexpr (is_void_v) { + if constexpr (Bidirectional) { + auto& rev = vertices_[e.target_id].in_edges(); + emplace_edge(rev, e.source_id, in_edge_type(e.source_id)); } + emplace_edge(vertex_edges, e.target_id, edge_type(e.target_id)); } else { - if constexpr (is_void_v) { - emplace_edge(vertex_edges, e.target_id, edge_type(e.target_id)); - } else { - emplace_edge(vertex_edges, e.target_id, edge_type(e.target_id, std::move(e.value))); + if constexpr (Bidirectional) { + auto& rev = vertices_[e.target_id].in_edges(); + emplace_edge(rev, e.source_id, in_edge_type(e.source_id, e.value)); // copy } + emplace_edge(vertex_edges, e.target_id, edge_type(e.target_id, std::move(e.value))); } edge_count_ += 1; } @@ -1314,8 +1354,9 @@ class dynamic_graph_base { // Only resize if we collected edges; empty input should not create vertices if (!projected.empty()) { if constexpr (resizable) { - if (vertices_.size() <= max_id) + if (vertices_.size() <= max_id) { vertices_.resize(max_id + 1, vertex_type(vertices_.get_allocator())); + } } } // If adjacency edge container is vector we can reserve per-vertex capacity now. @@ -1333,6 +1374,20 @@ class dynamic_graph_base { ec.reserve(degrees[vid]); } } + // Also reserve reverse edge capacity for bidirectional graphs + if constexpr (Bidirectional) { + std::vector in_degrees(vertices_.size(), size_type{0}); + for (auto const& e : projected) { + in_degrees[static_cast(e.target_id)]++; + } + for (size_t vid = 0; vid < in_degrees.size(); ++vid) { + auto& rc = vertices_[vid].in_edges(); + if constexpr (requires { rc.reserve(in_degrees[vid]); }) { + if (in_degrees[vid]) + rc.reserve(in_degrees[vid]); + } + } + } } // Insert from cached list for (auto& e : projected) { @@ -1341,18 +1396,18 @@ class dynamic_graph_base { if (static_cast(e.target_id) >= vertices_.size()) throw std::runtime_error("target id exceeds the number of vertices in load_edges"); auto& vertex_edges = vertices_[static_cast(e.source_id)].edges(); - if constexpr (Sourced) { - if constexpr (is_void_v) { - emplace_edge(vertex_edges, e.target_id, edge_type(e.source_id, e.target_id)); - } else { - emplace_edge(vertex_edges, e.target_id, edge_type(e.source_id, e.target_id, std::move(e.value))); + if constexpr (is_void_v) { + if constexpr (Bidirectional) { + auto& rev = vertices_[static_cast(e.target_id)].in_edges(); + emplace_edge(rev, e.source_id, in_edge_type(e.source_id)); } + emplace_edge(vertex_edges, e.target_id, edge_type(e.target_id)); } else { - if constexpr (is_void_v) { - emplace_edge(vertex_edges, e.target_id, edge_type(e.target_id)); - } else { - emplace_edge(vertex_edges, e.target_id, edge_type(e.target_id, std::move(e.value))); + if constexpr (Bidirectional) { + auto& rev = vertices_[static_cast(e.target_id)].in_edges(); + emplace_edge(rev, e.source_id, in_edge_type(e.source_id, e.value)); // copy } + emplace_edge(vertex_edges, e.target_id, edge_type(e.target_id, std::move(e.value))); } edge_count_ += 1; } @@ -1371,18 +1426,18 @@ class dynamic_graph_base { if (static_cast(e.target_id) >= vertices_.size()) throw std::runtime_error("target id exceeds the number of vertices in load_edges"); auto& vertex_edges = vertices_[static_cast(e.source_id)].edges(); - if constexpr (Sourced) { - if constexpr (is_void_v) { - emplace_edge(vertex_edges, e.target_id, edge_type(e.source_id, e.target_id)); - } else { - emplace_edge(vertex_edges, e.target_id, edge_type(e.source_id, e.target_id, std::move(e.value))); + if constexpr (is_void_v) { + if constexpr (Bidirectional) { + auto& rev = vertices_[static_cast(e.target_id)].in_edges(); + emplace_edge(rev, e.source_id, in_edge_type(e.source_id)); } + emplace_edge(vertex_edges, e.target_id, edge_type(e.target_id)); } else { - if constexpr (is_void_v) { - emplace_edge(vertex_edges, e.target_id, edge_type(e.target_id)); - } else { - emplace_edge(vertex_edges, e.target_id, edge_type(e.target_id, std::move(e.value))); + if constexpr (Bidirectional) { + auto& rev = vertices_[static_cast(e.target_id)].in_edges(); + emplace_edge(rev, e.source_id, in_edge_type(e.source_id, e.value)); // copy } + emplace_edge(vertex_edges, e.target_id, edge_type(e.target_id, std::move(e.value))); } edge_count_ += 1; } @@ -1392,7 +1447,6 @@ class dynamic_graph_base { // --------------------------------------------------------------------------- // (Removed deprecated legacy parameter order bridge overload) -private: constexpr void terminate_partitions() { // Partitions are only meaningful for sequential containers with numeric IDs // For associative containers (map/unordered_map), partition functionality is not supported @@ -1678,23 +1732,35 @@ class dynamic_graph_base { requires std::derived_from, dynamic_graph_base> && edge_descriptor_type && (!std::is_void_v) [[nodiscard]] friend constexpr decltype(auto) edge_value(G&& g, E&& uv) noexcept { + // Direction-aware edge container access: + // For in-edge descriptors, navigate .in_edges(); for out-edges, navigate .edges() + auto&& edge_container = [&]() -> decltype(auto) { + if constexpr (std::remove_cvref_t::is_in_edge) { + return (std::forward(uv).source().inner_value(std::forward(g).vertices_).in_edges()); + } else { + return (std::forward(uv).source().inner_value(std::forward(g).vertices_).edges()); + } + }(); + // Get the edge object by chaining calls to avoid dangling reference warnings // For map-based containers, edge_obj is pair, so access .second if constexpr (requires { - std::forward(uv) - .inner_value(std::forward(uv).source().inner_value(std::forward(g).vertices_).edges()) - .second.value(); + std::forward(uv).inner_value(edge_container).second.value(); }) { - return std::forward(uv) - .inner_value(std::forward(uv).source().inner_value(std::forward(g).vertices_).edges()) - .second.value(); + return std::forward(uv).inner_value(edge_container).second.value(); } else { - return std::forward(uv) - .inner_value(std::forward(uv).source().inner_value(std::forward(g).vertices_).edges()) - .value(); + return std::forward(uv).inner_value(edge_container).value(); } } + // ========================================================================= + // in_edges(g, u) — incoming edge CPO + // ========================================================================= + // Bidirectional in_edges are stored directly on each vertex via dynamic_vertex_bidir_base. + // The in_edges CPO discovers them through the _vertex_member dispatch tier which calls + // u.inner_value(g).in_edges() and wraps automatically in edge_descriptor_view. + // No ADL friend function is needed here. + // friend constexpr vertex_id_type vertex_id(const dynamic_graph_base& g, typename vertices_type::const_iterator ui) { // return static_cast(ui - g.vertices_.begin()); // } @@ -1730,9 +1796,9 @@ class dynamic_graph_base { /** * @ingroup graph_containers - * @brief dynamic_graph defines a graph with a variety characteristics including edge, vertex - * and graph value types, whether edges are sourced or not, the vertex id type, and selection - * of the containers used for vertices and edges. + * @brief dynamic_graph defines a graph with a variety of characteristics including edge, vertex + * and graph value types, whether bidirectional in-edge support is enabled, the vertex id type, + * and selection of the containers used for vertices and edges. * * dynamic_graph provides the interface that includes or excludes (GV=void) a graph value. * dynamic_graph_base has the core implementation for the graph. @@ -1760,17 +1826,16 @@ class dynamic_graph_base { * and calls to @c vertex_value(g,u) will generate a compile error. VV must be default-constructable. * @tparam GV @showinitializer =void The graph value type. If "void" is used no user value is stored on the graph * and calls to @c graph_value(g) will generate a compile error. - * @tparam Sourced @showinitializer =false Is a source vertex id stored on the edge? If false, calls to @c source_id(g,uv) - * and @c source(g,uv) will generate a compile error. * @tparam VId @showinitializer =uint32_t Vertex id type - * @tparam Traits @showinitializer =vofl_graph_traits Defines the types for vertex and edge containers. + * @tparam Traits @showinitializer =vofl_graph_traits Defines the types for vertex and edge containers. + * @tparam Bidirectional @showinitializer =false If true, maintains reverse adjacency lists for in_edges(g,u). */ -template -class dynamic_graph : public dynamic_graph_base { +template +class dynamic_graph : public dynamic_graph_base { public: // Types & Constants - using base_type = dynamic_graph_base; - using graph_type = dynamic_graph; + using base_type = dynamic_graph_base; + using graph_type = dynamic_graph; using graph_traits = Traits; using vertex_id_type = VId; using value_type = GV; @@ -1778,9 +1843,7 @@ class dynamic_graph : public dynamic_graph_base; - - constexpr inline const static bool sourced = Sourced; + using edge_type = dynamic_out_edge; private: // Members [[no_unique_address]] GV graph_value_{}; @@ -1934,17 +1997,17 @@ class dynamic_graph : public dynamic_graph_base -class dynamic_graph - : public dynamic_graph_base { +template +class dynamic_graph + : public dynamic_graph_base { public: // Types & Constants - using base_type = dynamic_graph_base; - using graph_type = dynamic_graph; + using base_type = dynamic_graph_base; + using graph_type = dynamic_graph; using graph_traits = Traits; using vertex_id_type = VId; using value_type = void; @@ -1952,9 +2015,7 @@ class dynamic_graph using edges_type = typename Traits::edges_type; using edge_allocator_type = typename edges_type::allocator_type; - using edge_type = dynamic_edge; - - constexpr inline const static bool sourced = Sourced; + using edge_type = dynamic_out_edge; public: // Construction/Destruction/Assignment dynamic_graph() = default; @@ -2047,30 +2108,28 @@ class dynamic_graph namespace std { /** - * @brief Hash specialization for dynamic_edge with Sourced = true + * @brief Hash specialization for dynamic_out_edge. * - * Hashes based on source_id and target_id only (edge value excluded, works for any EV) + * Hashes based on target_id only (out-edges no longer store source_id). Works for any EV. */ -template -struct hash> { +template +struct hash> { [[nodiscard]] size_t - operator()(const graph::container::dynamic_edge& edge) const noexcept { - size_t h1 = std::hash{}(edge.source_id()); - size_t h2 = std::hash{}(edge.target_id()); - return h1 ^ (h2 << 1); + operator()(const graph::container::dynamic_out_edge& edge) const noexcept { + return std::hash{}(edge.target_id()); } }; /** - * @brief Hash specialization for dynamic_edge with Sourced = false - * - * Hashes based on target_id only (no source_id available, works for any EV) + * @brief Hash specialization for dynamic_in_edge. + * + * Hashes based on source_id only (edge value excluded, works for any EV). */ -template -struct hash> { +template +struct hash> { [[nodiscard]] size_t - operator()(const graph::container::dynamic_edge& edge) const noexcept { - return std::hash{}(edge.target_id()); + operator()(const graph::container::dynamic_in_edge& edge) const noexcept { + return std::hash{}(edge.source_id()); } }; diff --git a/include/graph/container/traits/dod_graph_traits.hpp b/include/graph/container/traits/dod_graph_traits.hpp index 143c400..44da931 100644 --- a/include/graph/container/traits/dod_graph_traits.hpp +++ b/include/graph/container/traits/dod_graph_traits.hpp @@ -5,30 +5,30 @@ namespace graph::container { // Forward declarations -template -class dynamic_edge; +template +class dynamic_out_edge; -template +template class dynamic_vertex; -template +template class dynamic_graph; // dod_graph_traits // Vertices: std::deque (stable iterators) // Edges: std::deque (stable iterators with random access) // Parameter semantics mirror vofl_graph_traits. -template +template struct dod_graph_traits { using edge_value_type = EV; using vertex_value_type = VV; using graph_value_type = GV; using vertex_id_type = VId; - static constexpr bool sourced = Sourced; + static constexpr bool bidirectional = Bidirectional; - using edge_type = dynamic_edge; - using vertex_type = dynamic_vertex; - using graph_type = dynamic_graph; + using edge_type = dynamic_out_edge; + using vertex_type = dynamic_vertex; + using graph_type = dynamic_graph; using vertices_type = std::deque; using edges_type = std::deque; diff --git a/include/graph/container/traits/dofl_graph_traits.hpp b/include/graph/container/traits/dofl_graph_traits.hpp index 4aa3c6c..d69955f 100644 --- a/include/graph/container/traits/dofl_graph_traits.hpp +++ b/include/graph/container/traits/dofl_graph_traits.hpp @@ -6,30 +6,30 @@ namespace graph::container { // Forward declarations -template -class dynamic_edge; +template +class dynamic_out_edge; -template +template class dynamic_vertex; -template +template class dynamic_graph; // dofl_graph_traits // Vertices: std::deque (stable iterators) // Edges: std::forward_list // Parameter semantics mirror vofl_graph_traits. -template +template struct dofl_graph_traits { using edge_value_type = EV; using vertex_value_type = VV; using graph_value_type = GV; using vertex_id_type = VId; - static constexpr bool sourced = Sourced; + static constexpr bool bidirectional = Bidirectional; - using edge_type = dynamic_edge; - using vertex_type = dynamic_vertex; - using graph_type = dynamic_graph; + using edge_type = dynamic_out_edge; + using vertex_type = dynamic_vertex; + using graph_type = dynamic_graph; using vertices_type = std::deque; using edges_type = std::forward_list; diff --git a/include/graph/container/traits/dol_graph_traits.hpp b/include/graph/container/traits/dol_graph_traits.hpp index 80bee38..3881dea 100644 --- a/include/graph/container/traits/dol_graph_traits.hpp +++ b/include/graph/container/traits/dol_graph_traits.hpp @@ -6,30 +6,30 @@ namespace graph::container { // Forward declarations -template -class dynamic_edge; +template +class dynamic_out_edge; -template +template class dynamic_vertex; -template +template class dynamic_graph; // dol_graph_traits // Vertices: std::deque (stable iterators) // Edges: std::list // Parameter semantics mirror vofl_graph_traits. -template +template struct dol_graph_traits { using edge_value_type = EV; using vertex_value_type = VV; using graph_value_type = GV; using vertex_id_type = VId; - static constexpr bool sourced = Sourced; + static constexpr bool bidirectional = Bidirectional; - using edge_type = dynamic_edge; - using vertex_type = dynamic_vertex; - using graph_type = dynamic_graph; + using edge_type = dynamic_out_edge; + using vertex_type = dynamic_vertex; + using graph_type = dynamic_graph; using vertices_type = std::deque; using edges_type = std::list; diff --git a/include/graph/container/traits/dos_graph_traits.hpp b/include/graph/container/traits/dos_graph_traits.hpp index 527bf68..c4c7773 100644 --- a/include/graph/container/traits/dos_graph_traits.hpp +++ b/include/graph/container/traits/dos_graph_traits.hpp @@ -6,13 +6,13 @@ namespace graph::container { // Forward declarations -template -class dynamic_edge; +template +class dynamic_out_edge; -template +template class dynamic_vertex; -template +template class dynamic_graph; // dos_graph_traits @@ -22,23 +22,23 @@ class dynamic_graph; // Key characteristics: // - Vertices have stable references/pointers on push_back/push_front (unlike vector) // - Edges are automatically deduplicated (no parallel edges with same endpoints) -// - Edges are stored in sorted order (by source_id if Sourced, then target_id) +// - Edges are stored in sorted order (by target_id) // - O(log n) edge insertion, lookup, and deletion // - Bidirectional iterators for edges (no random access to edges) -// - Requires operator<=> on dynamic_edge (implemented in dynamic_graph.hpp) +// - Requires operator<=> on dynamic_out_edge (implemented in dynamic_graph.hpp) // // Parameter semantics mirror vofl_graph_traits. -template +template struct dos_graph_traits { using edge_value_type = EV; using vertex_value_type = VV; using graph_value_type = GV; using vertex_id_type = VId; - static constexpr bool sourced = Sourced; + static constexpr bool bidirectional = Bidirectional; - using edge_type = dynamic_edge; - using vertex_type = dynamic_vertex; - using graph_type = dynamic_graph; + using edge_type = dynamic_out_edge; + using vertex_type = dynamic_vertex; + using graph_type = dynamic_graph; using vertices_type = std::deque; using edges_type = std::set; diff --git a/include/graph/container/traits/dous_graph_traits.hpp b/include/graph/container/traits/dous_graph_traits.hpp index 72f53c3..e4de54c 100644 --- a/include/graph/container/traits/dous_graph_traits.hpp +++ b/include/graph/container/traits/dous_graph_traits.hpp @@ -6,13 +6,13 @@ namespace graph::container { // Forward declarations -template -class dynamic_edge; +template +class dynamic_out_edge; -template +template class dynamic_vertex; -template +template class dynamic_graph; // dous_graph_traits @@ -24,7 +24,7 @@ class dynamic_graph; // - Edges are stored in unordered fashion (insertion order not preserved) // - O(1) average edge insertion, lookup, and deletion // - Forward iterators only (no bidirectional or random access) -// - Requires operator== and std::hash on dynamic_edge (implemented in dynamic_graph.hpp) +// - Requires operator== and std::hash on dynamic_out_edge (implemented in dynamic_graph.hpp) // - Stable vertex iterators (unlike vector, iterators remain valid during insertions) // // Compared to dos_graph_traits: @@ -36,17 +36,17 @@ class dynamic_graph; // - dous: deque vertices (stable iterators, efficient push_front/push_back) // // Parameter semantics mirror dofl_graph_traits. -template +template struct dous_graph_traits { using edge_value_type = EV; using vertex_value_type = VV; using graph_value_type = GV; using vertex_id_type = VId; - static constexpr bool sourced = Sourced; + static constexpr bool bidirectional = Bidirectional; - using edge_type = dynamic_edge; - using vertex_type = dynamic_vertex; - using graph_type = dynamic_graph; + using edge_type = dynamic_out_edge; + using vertex_type = dynamic_vertex; + using graph_type = dynamic_graph; using vertices_type = std::deque; using edges_type = std::unordered_set; diff --git a/include/graph/container/traits/dov_graph_traits.hpp b/include/graph/container/traits/dov_graph_traits.hpp index 69a81b2..a840023 100644 --- a/include/graph/container/traits/dov_graph_traits.hpp +++ b/include/graph/container/traits/dov_graph_traits.hpp @@ -6,30 +6,30 @@ namespace graph::container { // Forward declarations -template -class dynamic_edge; +template +class dynamic_out_edge; -template +template class dynamic_vertex; -template +template class dynamic_graph; // dov_graph_traits // Vertices: std::deque (stable iterators) // Edges: std::vector (random access) // Parameter semantics mirror vofl_graph_traits. -template +template struct dov_graph_traits { using edge_value_type = EV; using vertex_value_type = VV; using graph_value_type = GV; using vertex_id_type = VId; - static constexpr bool sourced = Sourced; + static constexpr bool bidirectional = Bidirectional; - using edge_type = dynamic_edge; - using vertex_type = dynamic_vertex; - using graph_type = dynamic_graph; + using edge_type = dynamic_out_edge; + using vertex_type = dynamic_vertex; + using graph_type = dynamic_graph; using vertices_type = std::deque; using edges_type = std::vector; diff --git a/include/graph/container/traits/mod_graph_traits.hpp b/include/graph/container/traits/mod_graph_traits.hpp index 3b25532..8147556 100644 --- a/include/graph/container/traits/mod_graph_traits.hpp +++ b/include/graph/container/traits/mod_graph_traits.hpp @@ -7,13 +7,13 @@ namespace graph::container { // Forward declarations -template -class dynamic_edge; +template +class dynamic_out_edge; -template +template class dynamic_vertex; -template +template class dynamic_graph; // mod_graph_traits @@ -26,18 +26,18 @@ class dynamic_graph; // The std::deque edge container provides random access iteration with // efficient insertion at both ends (unlike vector which is only efficient at back). // Template parameters: EV (edge value or void), VV (vertex value or void), GV (graph value or void), -// VId (vertex id - any ordered type with operator<), Sourced (store source id on edge when true). -template +// VId (vertex id - any ordered type with operator<). +template struct mod_graph_traits { using edge_value_type = EV; using vertex_value_type = VV; using graph_value_type = GV; using vertex_id_type = VId; - static constexpr bool sourced = Sourced; + static constexpr bool bidirectional = Bidirectional; - using edge_type = dynamic_edge; - using vertex_type = dynamic_vertex; - using graph_type = dynamic_graph; + using edge_type = dynamic_out_edge; + using vertex_type = dynamic_vertex; + using graph_type = dynamic_graph; using vertices_type = std::map; using edges_type = std::deque; diff --git a/include/graph/container/traits/mofl_graph_traits.hpp b/include/graph/container/traits/mofl_graph_traits.hpp index 2a5cd87..001a7c6 100644 --- a/include/graph/container/traits/mofl_graph_traits.hpp +++ b/include/graph/container/traits/mofl_graph_traits.hpp @@ -7,13 +7,13 @@ namespace graph::container { // Forward declarations -template -class dynamic_edge; +template +class dynamic_out_edge; -template +template class dynamic_vertex; -template +template class dynamic_graph; // mofl_graph_traits @@ -24,18 +24,18 @@ class dynamic_graph; // created - no auto-extension when edges reference undefined vertices. // Vertex IDs can be any ordered type (int, string, custom struct with operator<). // Template parameters: EV (edge value or void), VV (vertex value or void), GV (graph value or void), -// VId (vertex id - any ordered type with operator<), Sourced (store source id on edge when true). -template +// VId (vertex id - any ordered type with operator<). +template struct mofl_graph_traits { using edge_value_type = EV; using vertex_value_type = VV; using graph_value_type = GV; using vertex_id_type = VId; - static constexpr bool sourced = Sourced; + static constexpr bool bidirectional = Bidirectional; - using edge_type = dynamic_edge; - using vertex_type = dynamic_vertex; - using graph_type = dynamic_graph; + using edge_type = dynamic_out_edge; + using vertex_type = dynamic_vertex; + using graph_type = dynamic_graph; using vertices_type = std::map; using edges_type = std::forward_list; diff --git a/include/graph/container/traits/mol_graph_traits.hpp b/include/graph/container/traits/mol_graph_traits.hpp index 4736d57..e0608ac 100644 --- a/include/graph/container/traits/mol_graph_traits.hpp +++ b/include/graph/container/traits/mol_graph_traits.hpp @@ -7,13 +7,13 @@ namespace graph::container { // Forward declarations -template -class dynamic_edge; +template +class dynamic_out_edge; -template +template class dynamic_vertex; -template +template class dynamic_graph; // mol_graph_traits @@ -25,18 +25,18 @@ class dynamic_graph; // Vertex IDs can be any ordered type (int, string, custom struct with operator<). // The std::list edge container provides bidirectional iteration (unlike forward_list). // Template parameters: EV (edge value or void), VV (vertex value or void), GV (graph value or void), -// VId (vertex id - any ordered type with operator<), Sourced (store source id on edge when true). -template +// VId (vertex id - any ordered type with operator<). +template struct mol_graph_traits { using edge_value_type = EV; using vertex_value_type = VV; using graph_value_type = GV; using vertex_id_type = VId; - static constexpr bool sourced = Sourced; + static constexpr bool bidirectional = Bidirectional; - using edge_type = dynamic_edge; - using vertex_type = dynamic_vertex; - using graph_type = dynamic_graph; + using edge_type = dynamic_out_edge; + using vertex_type = dynamic_vertex; + using graph_type = dynamic_graph; using vertices_type = std::map; using edges_type = std::list; diff --git a/include/graph/container/traits/mom_graph_traits.hpp b/include/graph/container/traits/mom_graph_traits.hpp index 26b24ca..d64320f 100644 --- a/include/graph/container/traits/mom_graph_traits.hpp +++ b/include/graph/container/traits/mom_graph_traits.hpp @@ -5,13 +5,13 @@ namespace graph::container { // Forward declarations -template -class dynamic_edge; +template +class dynamic_out_edge; -template +template class dynamic_vertex; -template +template class dynamic_graph; // mom_graph_traits @@ -35,17 +35,17 @@ class dynamic_graph; // - Memory overhead: ~24-32 bytes per vertex + ~8-16 bytes per edge // // Parameter semantics mirror vom_graph_traits. -template +template struct mom_graph_traits { using edge_value_type = EV; using vertex_value_type = VV; using graph_value_type = GV; using vertex_id_type = VId; - static constexpr bool sourced = Sourced; + static constexpr bool bidirectional = Bidirectional; - using edge_type = dynamic_edge; - using vertex_type = dynamic_vertex; - using graph_type = dynamic_graph; + using edge_type = dynamic_out_edge; + using vertex_type = dynamic_vertex; + using graph_type = dynamic_graph; using vertices_type = std::map; // Map keyed by vertex ID using edges_type = std::map; // Map keyed by target vertex ID diff --git a/include/graph/container/traits/mos_graph_traits.hpp b/include/graph/container/traits/mos_graph_traits.hpp index c56d52c..924efe1 100644 --- a/include/graph/container/traits/mos_graph_traits.hpp +++ b/include/graph/container/traits/mos_graph_traits.hpp @@ -6,13 +6,13 @@ namespace graph::container { // Forward declarations -template -class dynamic_edge; +template +class dynamic_out_edge; -template +template class dynamic_vertex; -template +template class dynamic_graph; // mos_graph_traits @@ -23,24 +23,24 @@ class dynamic_graph; // - Sparse, non-contiguous vertex IDs with key-based access // - Vertex IDs can be any ordered type (int, string, custom struct with operator<) // - Edges are automatically deduplicated (no parallel edges with same endpoints) -// - Edges are stored in sorted order (by source_id if Sourced, then target_id) +// - Edges are stored in sorted order (by target_id) // - O(log n) vertex and edge insertion, lookup, and deletion // - Bidirectional iterators for both vertices and edges // - Unlike sequential containers, vertices must be explicitly created // // Template parameters: EV (edge value or void), VV (vertex value or void), GV (graph value or void), -// VId (vertex id - any ordered type with operator<), Sourced (store source id on edge when true). -template +// VId (vertex id - any ordered type with operator<). +template struct mos_graph_traits { using edge_value_type = EV; using vertex_value_type = VV; using graph_value_type = GV; using vertex_id_type = VId; - static constexpr bool sourced = Sourced; + static constexpr bool bidirectional = Bidirectional; - using edge_type = dynamic_edge; - using vertex_type = dynamic_vertex; - using graph_type = dynamic_graph; + using edge_type = dynamic_out_edge; + using vertex_type = dynamic_vertex; + using graph_type = dynamic_graph; using vertices_type = std::map; using edges_type = std::set; diff --git a/include/graph/container/traits/mous_graph_traits.hpp b/include/graph/container/traits/mous_graph_traits.hpp index 0fce030..745fc45 100644 --- a/include/graph/container/traits/mous_graph_traits.hpp +++ b/include/graph/container/traits/mous_graph_traits.hpp @@ -6,13 +6,13 @@ namespace graph::container { // Forward declarations -template -class dynamic_edge; +template +class dynamic_out_edge; -template +template class dynamic_vertex; -template +template class dynamic_graph; // mous_graph_traits @@ -28,25 +28,25 @@ class dynamic_graph; // - O(1) average edge insertion, lookup, and deletion // - Bidirectional iterators for vertices, forward iterators only for edges // - Unlike sequential containers, vertices must be explicitly created -// - Requires operator== and std::hash on dynamic_edge (implemented in dynamic_graph.hpp) +// - Requires operator== and std::hash on dynamic_out_edge (implemented in dynamic_graph.hpp) // // Compared to mos_graph_traits: // - mos: O(log n) edge operations, sorted order, bidirectional iterators // - mous: O(1) average edge operations, unordered, forward iterators only // // Template parameters: EV (edge value or void), VV (vertex value or void), GV (graph value or void), -// VId (vertex id - any ordered type with operator<), Sourced (store source id on edge when true). -template +// VId (vertex id - any ordered type with operator<). +template struct mous_graph_traits { using edge_value_type = EV; using vertex_value_type = VV; using graph_value_type = GV; using vertex_id_type = VId; - static constexpr bool sourced = Sourced; + static constexpr bool bidirectional = Bidirectional; - using edge_type = dynamic_edge; - using vertex_type = dynamic_vertex; - using graph_type = dynamic_graph; + using edge_type = dynamic_out_edge; + using vertex_type = dynamic_vertex; + using graph_type = dynamic_graph; using vertices_type = std::map; using edges_type = std::unordered_set; diff --git a/include/graph/container/traits/mov_graph_traits.hpp b/include/graph/container/traits/mov_graph_traits.hpp index 3442474..2c041bf 100644 --- a/include/graph/container/traits/mov_graph_traits.hpp +++ b/include/graph/container/traits/mov_graph_traits.hpp @@ -7,13 +7,13 @@ namespace graph::container { // Forward declarations -template -class dynamic_edge; +template +class dynamic_out_edge; -template +template class dynamic_vertex; -template +template class dynamic_graph; // mov_graph_traits @@ -25,18 +25,18 @@ class dynamic_graph; // Vertex IDs can be any ordered type (int, string, custom struct with operator<). // The std::vector edge container provides random access iteration. // Template parameters: EV (edge value or void), VV (vertex value or void), GV (graph value or void), -// VId (vertex id - any ordered type with operator<), Sourced (store source id on edge when true). -template +// VId (vertex id - any ordered type with operator<). +template struct mov_graph_traits { using edge_value_type = EV; using vertex_value_type = VV; using graph_value_type = GV; using vertex_id_type = VId; - static constexpr bool sourced = Sourced; + static constexpr bool bidirectional = Bidirectional; - using edge_type = dynamic_edge; - using vertex_type = dynamic_vertex; - using graph_type = dynamic_graph; + using edge_type = dynamic_out_edge; + using vertex_type = dynamic_vertex; + using graph_type = dynamic_graph; using vertices_type = std::map; using edges_type = std::vector; diff --git a/include/graph/container/traits/uod_graph_traits.hpp b/include/graph/container/traits/uod_graph_traits.hpp index 47a67c4..340d20f 100644 --- a/include/graph/container/traits/uod_graph_traits.hpp +++ b/include/graph/container/traits/uod_graph_traits.hpp @@ -7,13 +7,13 @@ namespace graph::container { // Forward declarations -template -class dynamic_edge; +template +class dynamic_out_edge; -template +template class dynamic_vertex; -template +template class dynamic_graph; // uod_graph_traits @@ -26,18 +26,18 @@ class dynamic_graph; // Unlike std::map, iteration order is NOT sorted - it's based on hash buckets. // Edges use std::deque for stable iterators and efficient front/back insertion. // Template parameters: EV (edge value or void), VV (vertex value or void), GV (graph value or void), -// VId (vertex id - any hashable type with std::hash specialization), Sourced (store source id on edge when true). -template +// VId (vertex id - any hashable type with std::hash specialization). +template struct uod_graph_traits { using edge_value_type = EV; using vertex_value_type = VV; using graph_value_type = GV; using vertex_id_type = VId; - static constexpr bool sourced = Sourced; + static constexpr bool bidirectional = Bidirectional; - using edge_type = dynamic_edge; - using vertex_type = dynamic_vertex; - using graph_type = dynamic_graph; + using edge_type = dynamic_out_edge; + using vertex_type = dynamic_vertex; + using graph_type = dynamic_graph; using vertices_type = std::unordered_map; using edges_type = std::deque; diff --git a/include/graph/container/traits/uofl_graph_traits.hpp b/include/graph/container/traits/uofl_graph_traits.hpp index a33c083..e35d371 100644 --- a/include/graph/container/traits/uofl_graph_traits.hpp +++ b/include/graph/container/traits/uofl_graph_traits.hpp @@ -7,13 +7,13 @@ namespace graph::container { // Forward declarations -template -class dynamic_edge; +template +class dynamic_out_edge; -template +template class dynamic_vertex; -template +template class dynamic_graph; // uofl_graph_traits @@ -25,18 +25,18 @@ class dynamic_graph; // Vertex IDs can be any hashable type (int, string, custom struct with hash). // Unlike std::map, iteration order is NOT sorted - it's based on hash buckets. // Template parameters: EV (edge value or void), VV (vertex value or void), GV (graph value or void), -// VId (vertex id - any hashable type with std::hash specialization), Sourced (store source id on edge when true). -template +// VId (vertex id - any hashable type with std::hash specialization). +template struct uofl_graph_traits { using edge_value_type = EV; using vertex_value_type = VV; using graph_value_type = GV; using vertex_id_type = VId; - static constexpr bool sourced = Sourced; + static constexpr bool bidirectional = Bidirectional; - using edge_type = dynamic_edge; - using vertex_type = dynamic_vertex; - using graph_type = dynamic_graph; + using edge_type = dynamic_out_edge; + using vertex_type = dynamic_vertex; + using graph_type = dynamic_graph; using vertices_type = std::unordered_map; using edges_type = std::forward_list; diff --git a/include/graph/container/traits/uol_graph_traits.hpp b/include/graph/container/traits/uol_graph_traits.hpp index 85c631d..1931259 100644 --- a/include/graph/container/traits/uol_graph_traits.hpp +++ b/include/graph/container/traits/uol_graph_traits.hpp @@ -7,13 +7,13 @@ namespace graph::container { // Forward declarations -template -class dynamic_edge; +template +class dynamic_out_edge; -template +template class dynamic_vertex; -template +template class dynamic_graph; // uol_graph_traits @@ -26,18 +26,18 @@ class dynamic_graph; // Unlike std::map, iteration order is NOT sorted - it's based on hash buckets. // Edges use std::list for stable iterators and bidirectional traversal. // Template parameters: EV (edge value or void), VV (vertex value or void), GV (graph value or void), -// VId (vertex id - any hashable type with std::hash specialization), Sourced (store source id on edge when true). -template +// VId (vertex id - any hashable type with std::hash specialization). +template struct uol_graph_traits { using edge_value_type = EV; using vertex_value_type = VV; using graph_value_type = GV; using vertex_id_type = VId; - static constexpr bool sourced = Sourced; + static constexpr bool bidirectional = Bidirectional; - using edge_type = dynamic_edge; - using vertex_type = dynamic_vertex; - using graph_type = dynamic_graph; + using edge_type = dynamic_out_edge; + using vertex_type = dynamic_vertex; + using graph_type = dynamic_graph; using vertices_type = std::unordered_map; using edges_type = std::list; diff --git a/include/graph/container/traits/uos_graph_traits.hpp b/include/graph/container/traits/uos_graph_traits.hpp index 49bee61..2dfe48b 100644 --- a/include/graph/container/traits/uos_graph_traits.hpp +++ b/include/graph/container/traits/uos_graph_traits.hpp @@ -6,13 +6,13 @@ namespace graph::container { // Forward declarations -template -class dynamic_edge; +template +class dynamic_out_edge; -template +template class dynamic_vertex; -template +template class dynamic_graph; // uos_graph_traits @@ -24,30 +24,30 @@ class dynamic_graph; // - Vertex IDs can be any hashable type (int, string, custom struct with std::hash) // - Edges are automatically deduplicated (no parallel edges with same endpoints) // - Vertices stored in unordered fashion (insertion order not preserved) -// - Edges stored in sorted order (by source_id if Sourced, then target_id) +// - Edges stored in sorted order (by target_id) // - O(1) average vertex insertion, lookup, and deletion // - O(log n) edge insertion, lookup, and deletion // - Forward iterators only for vertices, bidirectional iterators for edges // - Unlike sequential containers, vertices must be explicitly created -// - Requires operator== and std::hash on VId, operator< on dynamic_edge (implemented in dynamic_graph.hpp) +// - Requires operator== and std::hash on VId, operator< on dynamic_out_edge (implemented in dynamic_graph.hpp) // // Compared to uous_graph_traits: // - uous: O(1) average edge operations, unordered edges, forward edge iterators // - uos: O(log n) edge operations, sorted edges, bidirectional edge iterators // // Template parameters: EV (edge value or void), VV (vertex value or void), GV (graph value or void), -// VId (vertex id - any hashable type with operator== and std::hash), Sourced (store source id on edge when true). -template +// VId (vertex id - any hashable type with operator== and std::hash). +template struct uos_graph_traits { using edge_value_type = EV; using vertex_value_type = VV; using graph_value_type = GV; using vertex_id_type = VId; - static constexpr bool sourced = Sourced; + static constexpr bool bidirectional = Bidirectional; - using edge_type = dynamic_edge; - using vertex_type = dynamic_vertex; - using graph_type = dynamic_graph; + using edge_type = dynamic_out_edge; + using vertex_type = dynamic_vertex; + using graph_type = dynamic_graph; using vertices_type = std::unordered_map; using edges_type = std::set; diff --git a/include/graph/container/traits/uous_graph_traits.hpp b/include/graph/container/traits/uous_graph_traits.hpp index c05bef8..a180ade 100644 --- a/include/graph/container/traits/uous_graph_traits.hpp +++ b/include/graph/container/traits/uous_graph_traits.hpp @@ -6,13 +6,13 @@ namespace graph::container { // Forward declarations -template -class dynamic_edge; +template +class dynamic_out_edge; -template +template class dynamic_vertex; -template +template class dynamic_graph; // uous_graph_traits @@ -28,25 +28,25 @@ class dynamic_graph; // - O(1) average edge insertion, lookup, and deletion // - Forward iterators only for both vertices and edges // - Unlike sequential containers, vertices must be explicitly created -// - Requires operator== and std::hash on both VId and dynamic_edge (implemented in dynamic_graph.hpp) +// - Requires operator== and std::hash on both VId and dynamic_out_edge (implemented in dynamic_graph.hpp) // // Compared to mous_graph_traits: // - mous: O(log n) vertex operations, sorted vertex order, bidirectional vertex iterators // - uous: O(1) average vertex operations, unordered, forward iterators only // // Template parameters: EV (edge value or void), VV (vertex value or void), GV (graph value or void), -// VId (vertex id - any hashable type with operator== and std::hash), Sourced (store source id on edge when true). -template +// VId (vertex id - any hashable type with operator== and std::hash). +template struct uous_graph_traits { using edge_value_type = EV; using vertex_value_type = VV; using graph_value_type = GV; using vertex_id_type = VId; - static constexpr bool sourced = Sourced; + static constexpr bool bidirectional = Bidirectional; - using edge_type = dynamic_edge; - using vertex_type = dynamic_vertex; - using graph_type = dynamic_graph; + using edge_type = dynamic_out_edge; + using vertex_type = dynamic_vertex; + using graph_type = dynamic_graph; using vertices_type = std::unordered_map; using edges_type = std::unordered_set; diff --git a/include/graph/container/traits/uov_graph_traits.hpp b/include/graph/container/traits/uov_graph_traits.hpp index 8561d09..e3f3a69 100644 --- a/include/graph/container/traits/uov_graph_traits.hpp +++ b/include/graph/container/traits/uov_graph_traits.hpp @@ -7,13 +7,13 @@ namespace graph::container { // Forward declarations -template -class dynamic_edge; +template +class dynamic_out_edge; -template +template class dynamic_vertex; -template +template class dynamic_graph; // uov_graph_traits @@ -26,18 +26,18 @@ class dynamic_graph; // Unlike std::map, iteration order is NOT sorted - it's based on hash buckets. // Edges use std::vector for cache-friendly access and O(1) size(). // Template parameters: EV (edge value or void), VV (vertex value or void), GV (graph value or void), -// VId (vertex id - any hashable type with std::hash specialization), Sourced (store source id on edge when true). -template +// VId (vertex id - any hashable type with std::hash specialization). +template struct uov_graph_traits { using edge_value_type = EV; using vertex_value_type = VV; using graph_value_type = GV; using vertex_id_type = VId; - static constexpr bool sourced = Sourced; + static constexpr bool bidirectional = Bidirectional; - using edge_type = dynamic_edge; - using vertex_type = dynamic_vertex; - using graph_type = dynamic_graph; + using edge_type = dynamic_out_edge; + using vertex_type = dynamic_vertex; + using graph_type = dynamic_graph; using vertices_type = std::unordered_map; using edges_type = std::vector; diff --git a/include/graph/container/traits/vod_graph_traits.hpp b/include/graph/container/traits/vod_graph_traits.hpp index e3f77e6..1844ebe 100644 --- a/include/graph/container/traits/vod_graph_traits.hpp +++ b/include/graph/container/traits/vod_graph_traits.hpp @@ -6,30 +6,30 @@ namespace graph::container { // Forward declarations -template -class dynamic_edge; +template +class dynamic_out_edge; -template +template class dynamic_vertex; -template +template class dynamic_graph; // vod_graph_traits // Vertices: std::vector // Edges: std::deque (stable iterators with random access). // Parameter semantics mirror vofl_graph_traits. -template +template struct vod_graph_traits { using edge_value_type = EV; using vertex_value_type = VV; using graph_value_type = GV; using vertex_id_type = VId; - static constexpr bool sourced = Sourced; + static constexpr bool bidirectional = Bidirectional; - using edge_type = dynamic_edge; - using vertex_type = dynamic_vertex; - using graph_type = dynamic_graph; + using edge_type = dynamic_out_edge; + using vertex_type = dynamic_vertex; + using graph_type = dynamic_graph; using vertices_type = std::vector; using edges_type = std::deque; diff --git a/include/graph/container/traits/vofl_graph_traits.hpp b/include/graph/container/traits/vofl_graph_traits.hpp index 86a3d99..b9894dc 100644 --- a/include/graph/container/traits/vofl_graph_traits.hpp +++ b/include/graph/container/traits/vofl_graph_traits.hpp @@ -6,13 +6,13 @@ namespace graph::container { // Forward declarations -template -class dynamic_edge; +template +class dynamic_out_edge; -template +template class dynamic_vertex; -template +template class dynamic_graph; // vofl_graph_traits @@ -20,18 +20,18 @@ class dynamic_graph; // Edges: std::forward_list (singly-linked; forward iteration only) // Notes: Lightweight singly-linked adjacency for cheap edge insertion. // Template parameters: EV (edge value or void), VV (vertex value or void), GV (graph value or void), -// VId (integral vertex id), Sourced (store source id on edge when true). -template +// VId (integral vertex id). +template struct vofl_graph_traits { using edge_value_type = EV; using vertex_value_type = VV; using graph_value_type = GV; using vertex_id_type = VId; - static constexpr bool sourced = Sourced; + static constexpr bool bidirectional = Bidirectional; - using edge_type = dynamic_edge; - using vertex_type = dynamic_vertex; - using graph_type = dynamic_graph; + using edge_type = dynamic_out_edge; + using vertex_type = dynamic_vertex; + using graph_type = dynamic_graph; using vertices_type = std::vector; using edges_type = std::forward_list; diff --git a/include/graph/container/traits/vol_graph_traits.hpp b/include/graph/container/traits/vol_graph_traits.hpp index a26fa1b..88237c7 100644 --- a/include/graph/container/traits/vol_graph_traits.hpp +++ b/include/graph/container/traits/vol_graph_traits.hpp @@ -6,29 +6,29 @@ namespace graph::container { // Forward declarations -template -class dynamic_edge; +template +class dynamic_out_edge; -template +template class dynamic_vertex; -template +template class dynamic_graph; // vol_graph_traits // Vertices: std::vector // Edges: std::list (bidirectional). Parameter semantics mirror vofl_graph_traits. -template +template struct vol_graph_traits { using edge_value_type = EV; using vertex_value_type = VV; using graph_value_type = GV; using vertex_id_type = VId; - static constexpr bool sourced = Sourced; + static constexpr bool bidirectional = Bidirectional; - using edge_type = dynamic_edge; - using vertex_type = dynamic_vertex; - using graph_type = dynamic_graph; + using edge_type = dynamic_out_edge; + using vertex_type = dynamic_vertex; + using graph_type = dynamic_graph; using vertices_type = std::vector; using edges_type = std::list; diff --git a/include/graph/container/traits/vom_graph_traits.hpp b/include/graph/container/traits/vom_graph_traits.hpp index a362be4..e0b051e 100644 --- a/include/graph/container/traits/vom_graph_traits.hpp +++ b/include/graph/container/traits/vom_graph_traits.hpp @@ -6,13 +6,13 @@ namespace graph::container { // Forward declarations -template -class dynamic_edge; +template +class dynamic_out_edge; -template +template class dynamic_vertex; -template +template class dynamic_graph; // vom_graph_traits @@ -33,17 +33,17 @@ class dynamic_graph; // - Memory overhead: ~8-16 bytes per edge (pair) // // Parameter semantics mirror vofl_graph_traits. -template +template struct vom_graph_traits { using edge_value_type = EV; using vertex_value_type = VV; using graph_value_type = GV; using vertex_id_type = VId; - static constexpr bool sourced = Sourced; + static constexpr bool bidirectional = Bidirectional; - using edge_type = dynamic_edge; - using vertex_type = dynamic_vertex; - using graph_type = dynamic_graph; + using edge_type = dynamic_out_edge; + using vertex_type = dynamic_vertex; + using graph_type = dynamic_graph; using vertices_type = std::vector; using edges_type = std::map; // Map keyed by target vertex ID diff --git a/include/graph/container/traits/vos_graph_traits.hpp b/include/graph/container/traits/vos_graph_traits.hpp index e0a8182..cf3c17f 100644 --- a/include/graph/container/traits/vos_graph_traits.hpp +++ b/include/graph/container/traits/vos_graph_traits.hpp @@ -6,13 +6,13 @@ namespace graph::container { // Forward declarations -template -class dynamic_edge; +template +class dynamic_out_edge; -template +template class dynamic_vertex; -template +template class dynamic_graph; // vos_graph_traits @@ -21,23 +21,23 @@ class dynamic_graph; // // Key characteristics: // - Edges are automatically deduplicated (no parallel edges with same endpoints) -// - Edges are stored in sorted order (by source_id if Sourced, then target_id) +// - Edges are stored in sorted order (by target_id) // - O(log n) edge insertion, lookup, and deletion // - Bidirectional iterators (no random access to edges) -// - Requires operator<=> on dynamic_edge (implemented in dynamic_graph.hpp) +// - Requires operator<=> on dynamic_out_edge (implemented in dynamic_graph.hpp) // // Parameter semantics mirror vofl_graph_traits. -template +template struct vos_graph_traits { using edge_value_type = EV; using vertex_value_type = VV; using graph_value_type = GV; using vertex_id_type = VId; - static constexpr bool sourced = Sourced; + static constexpr bool bidirectional = Bidirectional; - using edge_type = dynamic_edge; - using vertex_type = dynamic_vertex; - using graph_type = dynamic_graph; + using edge_type = dynamic_out_edge; + using vertex_type = dynamic_vertex; + using graph_type = dynamic_graph; using vertices_type = std::vector; using edges_type = std::set; diff --git a/include/graph/container/traits/voum_graph_traits.hpp b/include/graph/container/traits/voum_graph_traits.hpp index 7c24f3e..b3d2c77 100644 --- a/include/graph/container/traits/voum_graph_traits.hpp +++ b/include/graph/container/traits/voum_graph_traits.hpp @@ -6,13 +6,13 @@ namespace graph::container { // Forward declarations -template -class dynamic_edge; +template +class dynamic_out_edge; -template +template class dynamic_vertex; -template +template class dynamic_graph; // voum_graph_traits @@ -37,17 +37,17 @@ class dynamic_graph; // - Memory overhead: ~8-16 bytes per edge (pair) // // Parameter semantics mirror vom_graph_traits. -template +template struct voum_graph_traits { using edge_value_type = EV; using vertex_value_type = VV; using graph_value_type = GV; using vertex_id_type = VId; - static constexpr bool sourced = Sourced; + static constexpr bool bidirectional = Bidirectional; - using edge_type = dynamic_edge; - using vertex_type = dynamic_vertex; - using graph_type = dynamic_graph; + using edge_type = dynamic_out_edge; + using vertex_type = dynamic_vertex; + using graph_type = dynamic_graph; using vertices_type = std::vector; using edges_type = std::unordered_map; // Hash map keyed by target vertex ID diff --git a/include/graph/container/traits/vous_graph_traits.hpp b/include/graph/container/traits/vous_graph_traits.hpp index 4cb3994..be6234b 100644 --- a/include/graph/container/traits/vous_graph_traits.hpp +++ b/include/graph/container/traits/vous_graph_traits.hpp @@ -6,13 +6,13 @@ namespace graph::container { // Forward declarations -template -class dynamic_edge; +template +class dynamic_out_edge; -template +template class dynamic_vertex; -template +template class dynamic_graph; // vous_graph_traits @@ -24,24 +24,24 @@ class dynamic_graph; // - Edges are stored in unordered fashion (insertion order not preserved) // - O(1) average edge insertion, lookup, and deletion // - Forward iterators only (no bidirectional or random access) -// - Requires operator== and std::hash on dynamic_edge (implemented in dynamic_graph.hpp) +// - Requires operator== and std::hash on dynamic_out_edge (implemented in dynamic_graph.hpp) // // Compared to vos_graph_traits: // - vos: O(log n) operations, sorted order, bidirectional iterators // - vous: O(1) average operations, unordered, forward iterators only // // Parameter semantics mirror vofl_graph_traits. -template +template struct vous_graph_traits { using edge_value_type = EV; using vertex_value_type = VV; using graph_value_type = GV; using vertex_id_type = VId; - static constexpr bool sourced = Sourced; + static constexpr bool bidirectional = Bidirectional; - using edge_type = dynamic_edge; - using vertex_type = dynamic_vertex; - using graph_type = dynamic_graph; + using edge_type = dynamic_out_edge; + using vertex_type = dynamic_vertex; + using graph_type = dynamic_graph; using vertices_type = std::vector; using edges_type = std::unordered_set; diff --git a/include/graph/container/traits/vov_graph_traits.hpp b/include/graph/container/traits/vov_graph_traits.hpp index b132c29..fff6c7a 100644 --- a/include/graph/container/traits/vov_graph_traits.hpp +++ b/include/graph/container/traits/vov_graph_traits.hpp @@ -5,30 +5,30 @@ namespace graph::container { // Forward declarations -template -class dynamic_edge; +template +class dynamic_out_edge; -template +template class dynamic_vertex; -template +template class dynamic_graph; // vov_graph_traits // Vertices: std::vector // Edges: std::vector (contiguous; best for random access & cache locality). // Parameter semantics mirror vofl_graph_traits. -template +template struct vov_graph_traits { using edge_value_type = EV; using vertex_value_type = VV; using graph_value_type = GV; using vertex_id_type = VId; - static constexpr bool sourced = Sourced; + static constexpr bool bidirectional = Bidirectional; - using edge_type = dynamic_edge; - using vertex_type = dynamic_vertex; - using graph_type = dynamic_graph; + using edge_type = dynamic_out_edge; + using vertex_type = dynamic_vertex; + using graph_type = dynamic_graph; using vertices_type = std::vector; using edges_type = std::vector; diff --git a/include/graph/container/undirected_adjacency_list.hpp b/include/graph/container/undirected_adjacency_list.hpp index 2c64533..5df5994 100644 --- a/include/graph/container/undirected_adjacency_list.hpp +++ b/include/graph/container/undirected_adjacency_list.hpp @@ -1029,6 +1029,28 @@ class base_undirected_adjacency_list { return g.vertices_[uid].edges(g, uid); } + /// @brief Get incoming edges to a vertex descriptor (CPO: in_edges(g, u)). + /// @details For undirected graphs, incoming edges are the same as outgoing edges, + /// so this simply delegates to the same underlying edge range. + /// This makes undirected_adjacency_list model bidirectional_adjacency_list. + /// @tparam U Vertex descriptor type. + /// @param g The graph. + /// @param u The vertex descriptor. + /// @return Native vertex_edge_range (CPO will wrap in edge_descriptor_view). + /// @complexity O(1) + template + requires adj_list::vertex_descriptor_type + friend constexpr auto in_edges(graph_type& g, const U& u) noexcept { + auto uid = static_cast(u.vertex_id()); + return g.vertices_[uid].edges(g, uid); + } + template + requires adj_list::vertex_descriptor_type + friend constexpr auto in_edges(const graph_type& g, const U& u) noexcept { + auto uid = static_cast(u.vertex_id()); + return g.vertices_[uid].edges(g, uid); + } + public: /// @brief Get range of all edges. /// @note Each undirected edge appears twice in iteration (once from each endpoint). diff --git a/include/graph/detail/edge_cpo.hpp b/include/graph/detail/edge_cpo.hpp index 3e28137..407dafa 100644 --- a/include/graph/detail/edge_cpo.hpp +++ b/include/graph/detail/edge_cpo.hpp @@ -334,7 +334,19 @@ namespace _cpo_impls { } else if constexpr (_Choice<_G, _E>._Strategy == _St::_adl) { return source_id(g, uv); } else if constexpr (_Choice<_G, _E>._Strategy == _St::_adj_list_descriptor) { - return uv.source_id(); + if constexpr (_E::is_in_edge) { + // For in-edge descriptors: navigate the in_edges container if available. + // If the vertex data doesn't expose .in_edges(), fall back to the + // no-arg source_id() (returns the owning / queried vertex ID). + auto&& vd = uv.source().underlying_value(std::forward(g)); + if constexpr (requires { vd.in_edges(); }) { + return uv.source_id(vd); + } else { + return uv.source_id(); + } + } else { + return uv.source_id(); + } } else if constexpr (_Choice<_G, _E>._Strategy == _St::_edge_list_descriptor) { return uv.source_id(); } else if constexpr (_Choice<_G, _E>._Strategy == _St::_edge_info_member) { diff --git a/include/graph/graph.hpp b/include/graph/graph.hpp index 45a4894..b839255 100644 --- a/include/graph/graph.hpp +++ b/include/graph/graph.hpp @@ -51,6 +51,7 @@ #include #include #include +#include #include #include #include @@ -58,6 +59,7 @@ #include #include #include +#include // Graph containers are intentionally NOT included here to avoid heavy dependencies. // Include specific container headers directly: @@ -106,12 +108,15 @@ using adj_list::is_edge_descriptor_view_v; // Adjacency list concepts using adj_list::edge; using adj_list::vertex; -using adj_list::vertex_edge_range; +using adj_list::out_edge_range; using adj_list::vertex_range; using adj_list::index_vertex_range; using adj_list::adjacency_list; using adj_list::index_adjacency_list; using adj_list::ordered_vertex_edges; +using adj_list::in_edge_range; +using adj_list::bidirectional_adjacency_list; +using adj_list::index_bidirectional_adjacency_list; // Adjacency list traits using adj_list::has_degree; @@ -126,6 +131,12 @@ using adj_list::has_basic_queries; using adj_list::has_basic_queries_v; using adj_list::has_full_queries; using adj_list::has_full_queries_v; +using adj_list::has_in_degree; +using adj_list::has_in_degree_v; +using adj_list::has_find_in_edge; +using adj_list::has_find_in_edge_v; +using adj_list::has_contains_in_edge; +using adj_list::has_contains_in_edge_v; // CPOs (Customization Point Objects) using adj_list::vertices; @@ -138,8 +149,9 @@ using adj_list::num_vertices; using adj_list::num_edges; using adj_list::degree; using adj_list::find_vertex_edge; +using adj_list::contains_out_edge; using adj_list::contains_edge; -using adj_list::has_edge; +using adj_list::has_edges; using adj_list::vertex_value; using adj_list::edge_value; using adj_list::graph_value; @@ -147,6 +159,13 @@ using adj_list::source_id; using adj_list::source; using adj_list::partition_id; using adj_list::num_partitions; +using adj_list::in_edges; +using adj_list::in_degree; +using adj_list::find_in_edge; +using adj_list::contains_in_edge; +using adj_list::out_edges; +using adj_list::out_degree; +using adj_list::find_out_edge; // Type aliases using adj_list::vertex_range_t; @@ -156,4 +175,10 @@ using adj_list::vertex_id_t; using adj_list::vertex_edge_range_t; using adj_list::vertex_edge_iterator_t; using adj_list::edge_t; +using adj_list::in_edge_range_t; +using adj_list::in_edge_iterator_t; +using adj_list::in_edge_t; +using adj_list::out_edge_range_t; +using adj_list::out_edge_iterator_t; +using adj_list::out_edge_t; } // namespace graph diff --git a/include/graph/views.hpp b/include/graph/views.hpp index ed76b73..8e76107 100644 --- a/include/graph/views.hpp +++ b/include/graph/views.hpp @@ -87,7 +87,7 @@ using adj_list::num_edges; using adj_list::degree; using adj_list::find_vertex_edge; using adj_list::contains_edge; -using adj_list::has_edge; +using adj_list::has_edges; using adj_list::vertex_value; using adj_list::edge_value; using adj_list::graph_value; diff --git a/include/graph/views/adaptors.hpp b/include/graph/views/adaptors.hpp index 2a78e0f..f211851 100644 --- a/include/graph/views/adaptors.hpp +++ b/include/graph/views/adaptors.hpp @@ -54,7 +54,7 @@ using adj_list::num_edges; using adj_list::degree; using adj_list::find_vertex_edge; using adj_list::contains_edge; -using adj_list::has_edge; +using adj_list::has_edges; using adj_list::vertex_value; using adj_list::edge_value; using adj_list::graph_value; @@ -112,7 +112,7 @@ struct vertexlist_adaptor_fn { }; //============================================================================= -// incidence adaptor +// incidence adaptor (outgoing — default) //============================================================================= template @@ -161,8 +161,56 @@ struct incidence_adaptor_fn { } }; +// Alias: out_incidence_adaptor_fn == incidence_adaptor_fn +using out_incidence_adaptor_fn = incidence_adaptor_fn; + //============================================================================= -// neighbors adaptor +// in_incidence adaptor (incoming) +//============================================================================= + +template +struct in_incidence_adaptor_closure { + UID uid; + [[no_unique_address]] std::conditional_t, monostate, EVF> evf; + + template + friend auto operator|(G&& g, in_incidence_adaptor_closure adaptor) { + auto u = *adj_list::find_vertex(g, std::move(adaptor.uid)); + if constexpr (std::is_void_v) { + return graph::views::in_incidence(std::forward(g), u); + } else { + return graph::views::in_incidence(std::forward(g), u, std::move(adaptor.evf)); + } + } +}; + +struct in_incidence_adaptor_fn { + template + auto operator()(UID&& uid) const { + return in_incidence_adaptor_closure, void>{std::forward(uid), monostate{}}; + } + + template + auto operator()(UID&& uid, EVF&& evf) const { + return in_incidence_adaptor_closure, std::decay_t>{std::forward(uid), + std::forward(evf)}; + } + + template + requires adj_list::index_bidirectional_adjacency_list> + auto operator()(G&& g, UID&& uid) const { + return graph::views::in_incidence(std::forward(g), adj_list::vertex_id_t>(std::forward(uid))); + } + + template + requires adj_list::index_bidirectional_adjacency_list> + auto operator()(G&& g, UID&& uid, EVF&& evf) const { + return graph::views::in_incidence(std::forward(g), adj_list::vertex_id_t>(std::forward(uid)), std::forward(evf)); + } +}; + +//============================================================================= +// neighbors adaptor (outgoing — default) //============================================================================= template @@ -210,6 +258,53 @@ struct neighbors_adaptor_fn { } }; +// Alias: out_neighbors_adaptor_fn == neighbors_adaptor_fn +using out_neighbors_adaptor_fn = neighbors_adaptor_fn; + +//============================================================================= +// in_neighbors adaptor (incoming) +//============================================================================= + +template +struct in_neighbors_adaptor_closure { + UID uid; + [[no_unique_address]] std::conditional_t, monostate, VVF> vvf; + + template + friend auto operator|(G&& g, in_neighbors_adaptor_closure adaptor) { + if constexpr (std::is_void_v) { + return graph::views::in_neighbors(std::forward(g), std::move(adaptor.uid)); + } else { + return graph::views::in_neighbors(std::forward(g), std::move(adaptor.uid), std::move(adaptor.vvf)); + } + } +}; + +struct in_neighbors_adaptor_fn { + template + auto operator()(UID&& uid) const { + return in_neighbors_adaptor_closure, void>{std::forward(uid), monostate{}}; + } + + template + auto operator()(UID&& uid, VVF&& vvf) const { + return in_neighbors_adaptor_closure, std::decay_t>{std::forward(uid), + std::forward(vvf)}; + } + + template + requires adj_list::index_bidirectional_adjacency_list> + auto operator()(G&& g, UID&& uid) const { + return graph::views::in_neighbors(std::forward(g), adj_list::vertex_id_t>(std::forward(uid))); + } + + template + requires adj_list::index_bidirectional_adjacency_list> + auto operator()(G&& g, UID&& uid, VVF&& vvf) const { + return graph::views::in_neighbors(std::forward(g), adj_list::vertex_id_t>(std::forward(uid)), std::forward(vvf)); + } +}; + //============================================================================= // edgelist adaptor //============================================================================= @@ -290,7 +385,7 @@ struct basic_vertexlist_adaptor_fn { }; //============================================================================= -// basic_incidence adaptor +// basic_incidence adaptor (outgoing — default) //============================================================================= template @@ -333,8 +428,55 @@ struct basic_incidence_adaptor_fn { } }; +// Alias: basic_out_incidence_adaptor_fn == basic_incidence_adaptor_fn +using basic_out_incidence_adaptor_fn = basic_incidence_adaptor_fn; + +//============================================================================= +// basic_in_incidence adaptor (incoming) +//============================================================================= + +template +struct basic_in_incidence_adaptor_closure { + UID uid; + [[no_unique_address]] std::conditional_t, monostate, EVF> evf; + + template + friend auto operator|(G&& g, basic_in_incidence_adaptor_closure adaptor) { + if constexpr (std::is_void_v) { + return graph::views::basic_in_incidence(std::forward(g), std::move(adaptor.uid)); + } else { + return graph::views::basic_in_incidence(std::forward(g), std::move(adaptor.uid), std::move(adaptor.evf)); + } + } +}; + +struct basic_in_incidence_adaptor_fn { + template + auto operator()(UID&& uid) const { + return basic_in_incidence_adaptor_closure, void>{std::forward(uid), monostate{}}; + } + + template + auto operator()(UID&& uid, EVF&& evf) const { + return basic_in_incidence_adaptor_closure, std::decay_t>{std::forward(uid), + std::forward(evf)}; + } + + template + requires adj_list::index_bidirectional_adjacency_list> + auto operator()(G&& g, UID&& uid) const { + return graph::views::basic_in_incidence(std::forward(g), adj_list::vertex_id_t>(std::forward(uid))); + } + + template + requires adj_list::index_bidirectional_adjacency_list> + auto operator()(G&& g, UID&& uid, EVF&& evf) const { + return graph::views::basic_in_incidence(std::forward(g), adj_list::vertex_id_t>(std::forward(uid)), std::forward(evf)); + } +}; + //============================================================================= -// basic_neighbors adaptor +// basic_neighbors adaptor (outgoing — default) //============================================================================= template @@ -377,6 +519,53 @@ struct basic_neighbors_adaptor_fn { } }; +// Alias: basic_out_neighbors_adaptor_fn == basic_neighbors_adaptor_fn +using basic_out_neighbors_adaptor_fn = basic_neighbors_adaptor_fn; + +//============================================================================= +// basic_in_neighbors adaptor (incoming) +//============================================================================= + +template +struct basic_in_neighbors_adaptor_closure { + UID uid; + [[no_unique_address]] std::conditional_t, monostate, VVF> vvf; + + template + friend auto operator|(G&& g, basic_in_neighbors_adaptor_closure adaptor) { + if constexpr (std::is_void_v) { + return graph::views::basic_in_neighbors(std::forward(g), std::move(adaptor.uid)); + } else { + return graph::views::basic_in_neighbors(std::forward(g), std::move(adaptor.uid), std::move(adaptor.vvf)); + } + } +}; + +struct basic_in_neighbors_adaptor_fn { + template + auto operator()(UID&& uid) const { + return basic_in_neighbors_adaptor_closure, void>{std::forward(uid), monostate{}}; + } + + template + auto operator()(UID&& uid, VVF&& vvf) const { + return basic_in_neighbors_adaptor_closure, std::decay_t>{std::forward(uid), + std::forward(vvf)}; + } + + template + requires adj_list::index_bidirectional_adjacency_list> + auto operator()(G&& g, UID&& uid) const { + return graph::views::basic_in_neighbors(std::forward(g), adj_list::vertex_id_t>(std::forward(uid))); + } + + template + requires adj_list::index_bidirectional_adjacency_list> + auto operator()(G&& g, UID&& uid, VVF&& vvf) const { + return graph::views::basic_in_neighbors(std::forward(g), adj_list::vertex_id_t>(std::forward(uid)), std::forward(vvf)); + } +}; + //============================================================================= // basic_edgelist adaptor //============================================================================= @@ -788,17 +977,37 @@ struct edges_topological_sort_adaptor_fn { //============================================================================= namespace graph::views::adaptors { -// Basic views -inline constexpr vertexlist_adaptor_fn vertexlist{}; -inline constexpr incidence_adaptor_fn incidence{}; -inline constexpr neighbors_adaptor_fn neighbors{}; -inline constexpr edgelist_adaptor_fn edgelist{}; - -// Basic views (simplified bindings) -inline constexpr basic_vertexlist_adaptor_fn basic_vertexlist{}; -inline constexpr basic_incidence_adaptor_fn basic_incidence{}; -inline constexpr basic_neighbors_adaptor_fn basic_neighbors{}; -inline constexpr basic_edgelist_adaptor_fn basic_edgelist{}; +// Full views — outgoing primary +inline constexpr out_incidence_adaptor_fn out_incidence{}; +inline constexpr out_neighbors_adaptor_fn out_neighbors{}; + +// Full views — incoming +inline constexpr in_incidence_adaptor_fn in_incidence{}; +inline constexpr in_neighbors_adaptor_fn in_neighbors{}; + +// Full views — short aliases (outgoing by default) +inline constexpr auto& incidence = out_incidence; +inline constexpr auto& neighbors = out_neighbors; + +// Other full views (no direction variant) +inline constexpr vertexlist_adaptor_fn vertexlist{}; +inline constexpr edgelist_adaptor_fn edgelist{}; + +// Basic views — outgoing primary +inline constexpr basic_out_incidence_adaptor_fn basic_out_incidence{}; +inline constexpr basic_out_neighbors_adaptor_fn basic_out_neighbors{}; + +// Basic views — incoming +inline constexpr basic_in_incidence_adaptor_fn basic_in_incidence{}; +inline constexpr basic_in_neighbors_adaptor_fn basic_in_neighbors{}; + +// Basic views — short aliases (outgoing by default) +inline constexpr auto& basic_incidence = basic_out_incidence; +inline constexpr auto& basic_neighbors = basic_out_neighbors; + +// Other basic views (no direction variant) +inline constexpr basic_vertexlist_adaptor_fn basic_vertexlist{}; +inline constexpr basic_edgelist_adaptor_fn basic_edgelist{}; // Search views inline constexpr vertices_dfs_adaptor_fn vertices_dfs{}; diff --git a/include/graph/views/bfs.hpp b/include/graph/views/bfs.hpp index 177a005..6ba95e1 100644 --- a/include/graph/views/bfs.hpp +++ b/include/graph/views/bfs.hpp @@ -128,14 +128,17 @@ #include #include #include +#include namespace graph::views { // Forward declarations -template > +template , + class Accessor = out_edge_accessor> class vertices_bfs_view; -template > +template , + class Accessor = out_edge_accessor> class edges_bfs_view; namespace bfs_detail { @@ -199,12 +202,12 @@ namespace bfs_detail { * @par Complexity * Time: O(V + E). Space: O(V). */ - template + template struct bfs_edge_state { using graph_type = G; using vertex_type = adj_list::vertex_t; using vertex_id_type = adj_list::vertex_id_t; - using edge_iter_type = adj_list::vertex_edge_iterator_t; + using edge_iter_type = std::ranges::iterator_t>; using allocator_type = Alloc; using entry_type = edge_queue_entry; using queue_alloc = typename std::allocator_traits::template rebind_alloc; @@ -218,7 +221,7 @@ namespace bfs_detail { bfs_edge_state(G& g, vertex_type seed_vertex, std::size_t num_vertices, Alloc alloc = {}) : queue_(std::deque(alloc)), visited_(num_vertices, alloc) { - auto edge_range = adj_list::edges(g, seed_vertex); + auto edge_range = Accessor{}.edges(g, seed_vertex); auto edge_begin = std::ranges::begin(edge_range); auto edge_end = std::ranges::end(edge_range); queue_.push({seed_vertex, 0, edge_end, edge_begin}); @@ -244,13 +247,13 @@ namespace bfs_detail { * @see vertices_bfs_view — with value function * @see edges_bfs_view — edge-oriented BFS */ -template -class vertices_bfs_view : public std::ranges::view_interface> { +template +class vertices_bfs_view : public std::ranges::view_interface> { public: using graph_type = G; using vertex_type = adj_list::vertex_t; using vertex_id_type = adj_list::vertex_id_t; - using edge_type = adj_list::edge_t; + using edge_type = typename Accessor::template edge_t; using allocator_type = Alloc; using info_type = vertex_info; @@ -318,9 +321,9 @@ class vertices_bfs_view : public std::ranges::view_interfacevisited_.is_visited(target_vid)) { @@ -396,13 +399,13 @@ class vertices_bfs_view : public std::ranges::view_interface — without value function * @see edges_bfs_view — edge-oriented BFS */ -template -class vertices_bfs_view : public std::ranges::view_interface> { +template +class vertices_bfs_view : public std::ranges::view_interface> { public: using graph_type = G; using vertex_type = adj_list::vertex_t; using vertex_id_type = adj_list::vertex_id_t; - using edge_type = adj_list::edge_t; + using edge_type = typename Accessor::template edge_t; using allocator_type = Alloc; using value_result_type = std::invoke_result_t; using info_type = vertex_info; @@ -470,9 +473,9 @@ class vertices_bfs_view : public std::ranges::view_interfacevisited_.is_visited(target_vid)) { @@ -698,18 +701,18 @@ requires vertex_value_function> * @see edges_bfs_view — with value function * @see vertices_bfs_view — vertex-oriented BFS */ -template -class edges_bfs_view : public std::ranges::view_interface> { +template +class edges_bfs_view : public std::ranges::view_interface> { public: using graph_type = G; using vertex_type = adj_list::vertex_t; using vertex_id_type = adj_list::vertex_id_t; - using edge_type = adj_list::edge_t; + using edge_type = typename Accessor::template edge_t; using allocator_type = Alloc; using info_type = edge_info; private: - using state_type = bfs_detail::bfs_edge_state; + using state_type = bfs_detail::bfs_edge_state; public: /** @@ -788,14 +791,14 @@ class edges_bfs_view : public std::ranges::view_interfacevisited_.is_visited(target_vid)) { state_->visited_.mark_visited(target_vid); // Add target vertex to queue with its edge range - auto target_edge_range = adj_list::edges(*g_, target_v); + auto target_edge_range = Accessor{}.edges(*g_, target_v); auto target_begin = std::ranges::begin(target_edge_range); auto target_end = std::ranges::end(target_edge_range); state_->queue_.push({target_v, current.depth + 1, target_end, target_begin}); @@ -883,19 +886,19 @@ class edges_bfs_view : public std::ranges::view_interface — without value function * @see vertices_bfs_view — vertex-oriented BFS */ -template -class edges_bfs_view : public std::ranges::view_interface> { +template +class edges_bfs_view : public std::ranges::view_interface> { public: using graph_type = G; using vertex_type = adj_list::vertex_t; using vertex_id_type = adj_list::vertex_id_t; - using edge_type = adj_list::edge_t; + using edge_type = typename Accessor::template edge_t; using allocator_type = Alloc; using value_result_type = std::invoke_result_t; using info_type = edge_info; private: - using state_type = bfs_detail::bfs_edge_state; + using state_type = bfs_detail::bfs_edge_state; public: /** @@ -975,14 +978,14 @@ class edges_bfs_view : public std::ranges::view_interfacevisited_.is_visited(target_vid)) { state_->visited_.mark_visited(target_vid); // Add target vertex to queue with its edge range - auto target_edge_range = adj_list::edges(*g_, target_v); + auto target_edge_range = Accessor{}.edges(*g_, target_v); auto target_begin = std::ranges::begin(target_edge_range); auto target_end = std::ranges::end(target_edge_range); state_->queue_.push({target_v, current.depth + 1, target_end, target_begin}); @@ -1195,4 +1198,66 @@ requires edge_value_function> return edges_bfs_view, Alloc>(g, seed_vertex, std::forward(evf), alloc); } +//============================================================================= +// Accessor-parameterized factory functions +//============================================================================= +// Usage: vertices_bfs(g, seed) +// edges_bfs(g, seed) + +/// BFS vertex traversal with explicit Accessor, from vertex ID. +template +[[nodiscard]] auto vertices_bfs(G& g, adj_list::vertex_id_t seed) { + return vertices_bfs_view, Accessor>(g, seed, std::allocator{}); +} + +/// BFS vertex traversal with explicit Accessor, from vertex descriptor. +template +[[nodiscard]] auto vertices_bfs(G& g, adj_list::vertex_t seed_vertex) { + return vertices_bfs_view, Accessor>(g, seed_vertex, std::allocator{}); +} + +/// BFS vertex traversal with explicit Accessor and value function, from vertex ID. +template +requires vertex_value_function> +[[nodiscard]] auto vertices_bfs(G& g, adj_list::vertex_id_t seed, VVF&& vvf) { + return vertices_bfs_view, std::allocator, Accessor>(g, seed, std::forward(vvf), + std::allocator{}); +} + +/// BFS vertex traversal with explicit Accessor and value function, from vertex descriptor. +template +requires vertex_value_function> +[[nodiscard]] auto vertices_bfs(G& g, adj_list::vertex_t seed_vertex, VVF&& vvf) { + return vertices_bfs_view, std::allocator, Accessor>(g, seed_vertex, + std::forward(vvf), + std::allocator{}); +} + +/// BFS edge traversal with explicit Accessor, from vertex ID. +template +[[nodiscard]] auto edges_bfs(G& g, adj_list::vertex_id_t seed) { + return edges_bfs_view, Accessor>(g, seed); +} + +/// BFS edge traversal with explicit Accessor, from vertex descriptor. +template +[[nodiscard]] auto edges_bfs(G& g, adj_list::vertex_t seed_vertex) { + return edges_bfs_view, Accessor>(g, seed_vertex); +} + +/// BFS edge traversal with explicit Accessor and value function, from vertex ID. +template +requires edge_value_function> +[[nodiscard]] auto edges_bfs(G& g, adj_list::vertex_id_t seed, EVF&& evf) { + return edges_bfs_view, std::allocator, Accessor>(g, seed, std::forward(evf)); +} + +/// BFS edge traversal with explicit Accessor and value function, from vertex descriptor. +template +requires edge_value_function> +[[nodiscard]] auto edges_bfs(G& g, adj_list::vertex_t seed_vertex, EVF&& evf) { + return edges_bfs_view, std::allocator, Accessor>(g, seed_vertex, + std::forward(evf)); +} + } // namespace graph::views diff --git a/include/graph/views/dfs.hpp b/include/graph/views/dfs.hpp index 5655083..5ca16d5 100644 --- a/include/graph/views/dfs.hpp +++ b/include/graph/views/dfs.hpp @@ -127,14 +127,17 @@ #include #include #include +#include namespace graph::views { // Forward declarations -template > +template , + class Accessor = out_edge_accessor> class vertices_dfs_view; -template > +template , + class Accessor = out_edge_accessor> class edges_dfs_view; namespace dfs_detail { @@ -170,12 +173,12 @@ namespace dfs_detail { * reachable edge once. Space: O(V) — stack ≤ V entries, visited * tracker = V bits. */ - template + template struct dfs_state { using graph_type = G; using vertex_type = adj_list::vertex_t; using vertex_id_type = adj_list::vertex_id_t; - using edge_iterator_type = adj_list::vertex_edge_iterator_t; + using edge_iterator_type = std::ranges::iterator_t>; using allocator_type = Alloc; using entry_type = stack_entry; using stack_alloc = typename std::allocator_traits::template rebind_alloc; @@ -188,7 +191,7 @@ namespace dfs_detail { dfs_state(G& g, vertex_type seed_vertex, std::size_t num_vertices, Alloc alloc = {}) : stack_(std::vector(alloc)), visited_(num_vertices, alloc) { - auto edge_range = adj_list::edges(g, seed_vertex); + auto edge_range = Accessor{}.edges(g, seed_vertex); stack_.push({seed_vertex, std::ranges::begin(edge_range), std::ranges::end(edge_range)}); visited_.mark_visited(adj_list::vertex_id(g, seed_vertex)); // Note: count_ is not incremented here. It's incremented in advance() when @@ -214,18 +217,18 @@ namespace dfs_detail { * @see vertices_dfs_view — with value function * @see edges_dfs_view — edge-oriented DFS */ -template -class vertices_dfs_view : public std::ranges::view_interface> { +template +class vertices_dfs_view : public std::ranges::view_interface> { public: using graph_type = G; using vertex_type = adj_list::vertex_t; using vertex_id_type = adj_list::vertex_id_t; - using edge_type = adj_list::edge_t; + using edge_type = typename Accessor::template edge_t; using allocator_type = Alloc; using info_type = vertex_info; private: - using state_type = dfs_detail::dfs_state; + using state_type = dfs_detail::dfs_state; public: /** @@ -296,15 +299,15 @@ class vertices_dfs_view : public std::ranges::view_interfacevisited_.is_visited(target_vid)) { state_->visited_.mark_visited(target_vid); // Push target vertex with its edge range - auto edge_range = adj_list::edges(*g_, target_v); + auto edge_range = Accessor{}.edges(*g_, target_v); state_->stack_.push({target_v, std::ranges::begin(edge_range), std::ranges::end(edge_range)}); ++state_->depth_; ++state_->count_; @@ -386,19 +389,19 @@ class vertices_dfs_view : public std::ranges::view_interface — without value function * @see edges_dfs_view — edge-oriented DFS */ -template -class vertices_dfs_view : public std::ranges::view_interface> { +template +class vertices_dfs_view : public std::ranges::view_interface> { public: using graph_type = G; using vertex_type = adj_list::vertex_t; using vertex_id_type = adj_list::vertex_id_t; - using edge_type = adj_list::edge_t; + using edge_type = typename Accessor::template edge_t; using allocator_type = Alloc; using value_result_type = std::invoke_result_t; using info_type = vertex_info; private: - using state_type = dfs_detail::dfs_state; + using state_type = dfs_detail::dfs_state; public: /** @@ -470,15 +473,15 @@ class vertices_dfs_view : public std::ranges::view_interfacevisited_.is_visited(target_vid)) { state_->visited_.mark_visited(target_vid); // Push target vertex with its edge range - auto edge_range = adj_list::edges(*g_, target_v); + auto edge_range = Accessor{}.edges(*g_, target_v); state_->stack_.push({target_v, std::ranges::begin(edge_range), std::ranges::end(edge_range)}); ++state_->depth_; ++state_->count_; @@ -749,18 +752,18 @@ requires vertex_value_function> * @see edges_dfs_view — with value function * @see vertices_dfs_view — vertex-oriented DFS */ -template -class edges_dfs_view : public std::ranges::view_interface> { +template +class edges_dfs_view : public std::ranges::view_interface> { public: using graph_type = G; using vertex_type = adj_list::vertex_t; using vertex_id_type = adj_list::vertex_id_t; - using edge_type = adj_list::edge_t; + using edge_type = typename Accessor::template edge_t; using allocator_type = Alloc; using info_type = edge_info; private: - using state_type = dfs_detail::dfs_state; + using state_type = dfs_detail::dfs_state; public: /** @@ -833,15 +836,15 @@ class edges_dfs_view : public std::ranges::view_interfacevisited_.is_visited(target_vid)) { state_->visited_.mark_visited(target_vid); // Push target vertex with its edge range - auto edge_range = adj_list::edges(*g_, target_v); + auto edge_range = Accessor{}.edges(*g_, target_v); state_->stack_.push({target_v, std::ranges::begin(edge_range), std::ranges::end(edge_range)}); ++state_->depth_; ++state_->count_; @@ -924,19 +927,19 @@ class edges_dfs_view : public std::ranges::view_interface — without value function * @see vertices_dfs_view — vertex-oriented DFS */ -template -class edges_dfs_view : public std::ranges::view_interface> { +template +class edges_dfs_view : public std::ranges::view_interface> { public: using graph_type = G; using vertex_type = adj_list::vertex_t; using vertex_id_type = adj_list::vertex_id_t; - using edge_type = adj_list::edge_t; + using edge_type = typename Accessor::template edge_t; using allocator_type = Alloc; using value_result_type = std::invoke_result_t; using info_type = edge_info; private: - using state_type = dfs_detail::dfs_state; + using state_type = dfs_detail::dfs_state; public: /** @@ -1007,13 +1010,13 @@ class edges_dfs_view : public std::ranges::view_interfacevisited_.is_visited(target_vid)) { state_->visited_.mark_visited(target_vid); - auto edge_range = adj_list::edges(*g_, target_v); + auto edge_range = Accessor{}.edges(*g_, target_v); state_->stack_.push({target_v, std::ranges::begin(edge_range), std::ranges::end(edge_range)}); ++state_->depth_; ++state_->count_; @@ -1262,4 +1265,68 @@ requires edge_value_function> return edges_dfs_view, Alloc>(g, seed_vertex, std::forward(evf), alloc); } +//============================================================================= +// Accessor-parameterized factory functions +//============================================================================= +// Usage: vertices_dfs(g, seed) +// edges_dfs(g, seed) + +/// DFS vertex traversal with explicit Accessor, from vertex ID. +template +[[nodiscard]] auto vertices_dfs(G& g, adj_list::vertex_id_t seed) { + return vertices_dfs_view, Accessor>(g, seed, std::allocator{}); +} + +/// DFS vertex traversal with explicit Accessor, from vertex descriptor. +template +[[nodiscard]] auto vertices_dfs(G& g, adj_list::vertex_t seed_vertex) { + return vertices_dfs_view, Accessor>(g, seed_vertex, std::allocator{}); +} + +/// DFS vertex traversal with explicit Accessor and value function, from vertex ID. +template +requires vertex_value_function> +[[nodiscard]] auto vertices_dfs(G& g, adj_list::vertex_id_t seed, VVF&& vvf) { + return vertices_dfs_view, std::allocator, Accessor>(g, seed, std::forward(vvf), + std::allocator{}); +} + +/// DFS vertex traversal with explicit Accessor and value function, from vertex descriptor. +template +requires vertex_value_function> +[[nodiscard]] auto vertices_dfs(G& g, adj_list::vertex_t seed_vertex, VVF&& vvf) { + return vertices_dfs_view, std::allocator, Accessor>(g, seed_vertex, + std::forward(vvf), + std::allocator{}); +} + +/// DFS edge traversal with explicit Accessor, from vertex ID. +template +[[nodiscard]] auto edges_dfs(G& g, adj_list::vertex_id_t seed) { + return edges_dfs_view, Accessor>(g, seed, std::allocator{}); +} + +/// DFS edge traversal with explicit Accessor, from vertex descriptor. +template +[[nodiscard]] auto edges_dfs(G& g, adj_list::vertex_t seed_vertex) { + return edges_dfs_view, Accessor>(g, seed_vertex, std::allocator{}); +} + +/// DFS edge traversal with explicit Accessor and value function, from vertex ID. +template +requires edge_value_function> +[[nodiscard]] auto edges_dfs(G& g, adj_list::vertex_id_t seed, EVF&& evf) { + return edges_dfs_view, std::allocator, Accessor>(g, seed, std::forward(evf), + std::allocator{}); +} + +/// DFS edge traversal with explicit Accessor and value function, from vertex descriptor. +template +requires edge_value_function> +[[nodiscard]] auto edges_dfs(G& g, adj_list::vertex_t seed_vertex, EVF&& evf) { + return edges_dfs_view, std::allocator, Accessor>(g, seed_vertex, + std::forward(evf), + std::allocator{}); +} + } // namespace graph::views diff --git a/include/graph/views/edge_accessor.hpp b/include/graph/views/edge_accessor.hpp new file mode 100644 index 0000000..25e1524 --- /dev/null +++ b/include/graph/views/edge_accessor.hpp @@ -0,0 +1,133 @@ +/** + * @file edge_accessor.hpp + * @brief Edge accessor policies for parameterizing graph views. + * + * Defines stateless policy objects that bundle the three operations needed + * by view iterators: fetching the edge range from a vertex, extracting + * the neighbor vertex id from an edge, and obtaining the neighbor vertex + * descriptor. By defaulting to @c out_edge_accessor, existing code is + * source-compatible. Passing @c in_edge_accessor flips views to iterate + * over incoming edges. + * + * @section accessors Provided Accessors + * + * | Accessor | edges() | neighbor_id() | neighbor() | + * |-----------------------|-------------------|-----------------|----------------| + * | @c out_edge_accessor | @c edges(g,u) | @c target_id | @c target | + * | @c in_edge_accessor | @c in_edges(g,u) | @c source_id | @c source | + * + * @section type_traits Type Traits + * + * Each accessor exposes two member alias templates: + * - @c edge_range_t — the range type returned by @c edges() + * - @c edge_t — the edge descriptor type (range value type) + * + * Views use these to derive their internal type aliases, ensuring that + * @c incidence_view correctly uses + * @c in_edge_range_t and @c in_edge_t. + * + * @section usage Usage + * + * @code + * // Forward (outgoing) incidence — default: + * for (auto [tid, e] : incidence(g, u)) { ... } + * + * // Reverse (incoming) incidence: + * for (auto [sid, e] : in_incidence(g, u)) { ... } + * @endcode + * + * @see incidence.hpp — incidence views parameterized by accessor + * @see neighbors.hpp — neighbor views parameterized by accessor + */ + +#pragma once + +#include +#include + +namespace graph::views { + +// ============================================================================= +// out_edge_accessor — outgoing edge policy (default) +// ============================================================================= + +/** + * @brief Policy for outgoing-edge iteration. + * + * This is the default accessor used by all view classes. It delegates to + * @c edges(g,u) / @c target_id(g,e) / @c target(g,e). + * + * Stateless — @c [[no_unique_address]] storage costs zero bytes. + */ +struct out_edge_accessor { + /// Edge range type for a graph @c G. + template + using edge_range_t = adj_list::out_edge_range_t; + + /// Edge descriptor type for a graph @c G. + template + using edge_t = adj_list::out_edge_t; + + /// Return the outgoing edge range for vertex @c u. + template + [[nodiscard]] constexpr auto edges(G& g, adj_list::vertex_t u) const { + return adj_list::out_edges(g, u); + } + + /// Return the neighbor (target) vertex id from edge @c e. + template + [[nodiscard]] constexpr auto neighbor_id(G& g, adj_list::edge_t e) const { + return adj_list::target_id(g, e); + } + + /// Return the neighbor (target) vertex descriptor from edge @c e. + template + [[nodiscard]] constexpr auto neighbor(G& g, adj_list::edge_t e) const { + return adj_list::target(g, e); + } +}; + +// ============================================================================= +// in_edge_accessor — incoming edge policy +// ============================================================================= + +/** + * @brief Policy for incoming-edge iteration. + * + * Delegates to @c in_edges(g,u) / @c source_id(g,e) / @c source(g,e). + * Constrained on @c bidirectional_adjacency_list so that it only compiles + * for graphs that provide incoming-edge support. + * + * Stateless — @c [[no_unique_address]] storage costs zero bytes. + */ +struct in_edge_accessor { + /// Edge range type for incoming edges of graph @c G. + template + using edge_range_t = adj_list::in_edge_range_t; + + /// Edge descriptor type for incoming edges of graph @c G. + template + using edge_t = adj_list::in_edge_t; + + /// Return the incoming edge range for vertex @c u. + template + [[nodiscard]] constexpr auto edges(G& g, adj_list::vertex_t u) const { + return adj_list::in_edges(g, u); + } + + /// Return the neighbor (source) vertex id from incoming edge @c e. + template + [[nodiscard]] constexpr auto neighbor_id(G& g, adj_list::in_edge_t e) const { + return adj_list::source_id(g, e); + } + + /// Return the neighbor (source) vertex descriptor from incoming edge @c e. + /// Uses source_id + find_vertex because the source() CPO on in-edge + /// descriptors returns the owning vertex (target), not the actual source. + template + [[nodiscard]] constexpr auto neighbor(G& g, adj_list::in_edge_t e) const { + return *adj_list::find_vertex(g, adj_list::source_id(g, e)); + } +}; + +} // namespace graph::views diff --git a/include/graph/views/incidence.hpp b/include/graph/views/incidence.hpp index 26fd250..654fadf 100644 --- a/include/graph/views/incidence.hpp +++ b/include/graph/views/incidence.hpp @@ -114,14 +114,15 @@ #include #include #include +#include namespace graph::views { // Forward declarations -template +template class incidence_view; -template +template class basic_incidence_view; /** @@ -146,15 +147,16 @@ class basic_incidence_view; * @see incidence(G&, vertex_t) — factory function * @see basic_incidence_view — simplified target-id-only variant */ -template -class incidence_view : public std::ranges::view_interface> { +template +class incidence_view : public std::ranges::view_interface> { public: using graph_type = G; + using accessor_type = Accessor; using vertex_type = adj_list::vertex_t; using vertex_id_type = adj_list::vertex_id_t; - using edge_range_type = adj_list::vertex_edge_range_t; - using edge_iterator_type = adj_list::vertex_edge_iterator_t; - using edge_type = adj_list::edge_t; + using edge_range_type = typename Accessor::template edge_range_t; + using edge_iterator_type = std::ranges::iterator_t; + using edge_type = typename Accessor::template edge_t; using info_type = edge_info; /** @@ -175,7 +177,7 @@ class incidence_view : public std::ranges::view_interface(adj_list::target_id(*g_, current_)), current_}; + return info_type{static_cast(Accessor{}.neighbor_id(*g_, current_)), current_}; } constexpr iterator& operator++() noexcept { @@ -203,12 +205,12 @@ class incidence_view : public std::ranges::view_interface : public std::ranges::view_interface { - return std::ranges::size(adj_list::edges(*g_, source_)); + return std::ranges::size(Accessor{}.edges(*g_, source_)); } private: @@ -256,15 +258,16 @@ class incidence_view : public std::ranges::view_interface, EVF&&) — factory function * @see basic_incidence_view — simplified target-id-only variant */ -template -class incidence_view : public std::ranges::view_interface> { +template +class incidence_view : public std::ranges::view_interface> { public: using graph_type = G; + using accessor_type = Accessor; using vertex_type = adj_list::vertex_t; using vertex_id_type = adj_list::vertex_id_t; - using edge_range_type = adj_list::vertex_edge_range_t; - using edge_iterator_type = adj_list::vertex_edge_iterator_t; - using edge_type = adj_list::edge_t; + using edge_range_type = typename Accessor::template edge_range_t; + using edge_iterator_type = std::ranges::iterator_t; + using edge_type = typename Accessor::template edge_t; using value_type_result = std::invoke_result_t; using info_type = edge_info; @@ -286,7 +289,7 @@ class incidence_view : public std::ranges::view_interface constexpr iterator(G* g, edge_type e, EVF* evf) noexcept : g_(g), current_(e), evf_(evf) {} [[nodiscard]] constexpr value_type operator*() const { - return value_type{static_cast(adj_list::target_id(*g_, current_)), current_, std::invoke(*evf_, std::as_const(*g_), current_)}; + return value_type{static_cast(Accessor{}.neighbor_id(*g_, current_)), current_, std::invoke(*evf_, std::as_const(*g_), current_)}; } constexpr iterator& operator++() noexcept { @@ -316,19 +319,19 @@ class incidence_view : public std::ranges::view_interface : g_(&g), source_(u), evf_(std::move(evf)) {} [[nodiscard]] constexpr iterator begin() noexcept { - auto edge_range = adj_list::edges(*g_, source_); + auto edge_range = Accessor{}.edges(*g_, source_); return iterator(g_, *std::ranges::begin(edge_range), &evf_); } [[nodiscard]] constexpr iterator end() noexcept { - auto edge_range = adj_list::edges(*g_, source_); + auto edge_range = Accessor{}.edges(*g_, source_); return iterator(g_, *std::ranges::end(edge_range), &evf_); } [[nodiscard]] constexpr auto size() const noexcept requires std::ranges::sized_range { - return std::ranges::size(adj_list::edges(*g_, source_)); + return std::ranges::size(Accessor{}.edges(*g_, source_)); } private: @@ -339,10 +342,10 @@ class incidence_view : public std::ranges::view_interface // Deduction guides template -incidence_view(G&, adj_list::vertex_t) -> incidence_view; +incidence_view(G&, adj_list::vertex_t) -> incidence_view; template -incidence_view(G&, adj_list::vertex_t, EVF) -> incidence_view; +incidence_view(G&, adj_list::vertex_t, EVF) -> incidence_view; /** * @brief Create an incidence view over edges from a vertex (no value function). @@ -476,15 +479,16 @@ requires edge_value_function> * @see basic_incidence(G&, vertex_id_t) — factory function * @see incidence_view — standard variant with edge descriptor */ -template -class basic_incidence_view : public std::ranges::view_interface> { +template +class basic_incidence_view : public std::ranges::view_interface> { public: using graph_type = G; + using accessor_type = Accessor; using vertex_type = adj_list::vertex_t; using vertex_id_type = adj_list::vertex_id_t; - using edge_range_type = adj_list::vertex_edge_range_t; - using edge_iterator_type = adj_list::vertex_edge_iterator_t; - using edge_type = adj_list::edge_t; + using edge_range_type = typename Accessor::template edge_range_t; + using edge_iterator_type = std::ranges::iterator_t; + using edge_type = typename Accessor::template edge_t; using info_type = edge_info; /** @@ -505,7 +509,7 @@ class basic_incidence_view : public std::ranges::view_interface(adj_list::target_id(*g_, current_))}; + return value_type{static_cast(Accessor{}.neighbor_id(*g_, current_))}; } constexpr iterator& operator++() noexcept { @@ -533,19 +537,19 @@ class basic_incidence_view : public std::ranges::view_interface { - return std::ranges::size(adj_list::edges(*g_, source_)); + return std::ranges::size(Accessor{}.edges(*g_, source_)); } private: @@ -585,15 +589,16 @@ class basic_incidence_view : public std::ranges::view_interface, EVF&&) — factory function * @see incidence_view — standard variant with edge descriptor */ -template -class basic_incidence_view : public std::ranges::view_interface> { +template +class basic_incidence_view : public std::ranges::view_interface> { public: using graph_type = G; + using accessor_type = Accessor; using vertex_type = adj_list::vertex_t; using vertex_id_type = adj_list::vertex_id_t; - using edge_range_type = adj_list::vertex_edge_range_t; - using edge_iterator_type = adj_list::vertex_edge_iterator_t; - using edge_type = adj_list::edge_t; + using edge_range_type = typename Accessor::template edge_range_t; + using edge_iterator_type = std::ranges::iterator_t; + using edge_type = typename Accessor::template edge_t; using value_type_result = std::invoke_result_t; using info_type = edge_info; @@ -615,7 +620,7 @@ class basic_incidence_view : public std::ranges::view_interface(adj_list::target_id(*g_, current_)), std::invoke(*evf_, std::as_const(*g_), current_)}; + return value_type{static_cast(Accessor{}.neighbor_id(*g_, current_)), std::invoke(*evf_, std::as_const(*g_), current_)}; } constexpr iterator& operator++() noexcept { @@ -651,19 +656,19 @@ class basic_incidence_view : public std::ranges::view_interface { - return std::ranges::size(adj_list::edges(*g_, source_)); + return std::ranges::size(Accessor{}.edges(*g_, source_)); } private: @@ -674,10 +679,10 @@ class basic_incidence_view : public std::ranges::view_interface -basic_incidence_view(G&, adj_list::vertex_t) -> basic_incidence_view; +basic_incidence_view(G&, adj_list::vertex_t) -> basic_incidence_view; template -basic_incidence_view(G&, adj_list::vertex_t, EVF) -> basic_incidence_view; +basic_incidence_view(G&, adj_list::vertex_t, EVF) -> basic_incidence_view; // ============================================================================= // Factory functions: basic_incidence @@ -730,4 +735,98 @@ requires edge_value_function> return basic_incidence_view>(g, u, std::forward(evf)); } +// ============================================================================= +// Explicit outgoing factories: out_incidence / basic_out_incidence +// ============================================================================= + +/// @brief Create an outgoing incidence view (no value function). +template +[[nodiscard]] constexpr auto out_incidence(G& g, adj_list::vertex_t u) noexcept { + return incidence_view(g, u); +} + +/// @brief Create an outgoing incidence view with edge value function. +template +requires edge_value_function> +[[nodiscard]] constexpr auto out_incidence(G& g, adj_list::vertex_t u, EVF&& evf) { + return incidence_view, out_edge_accessor>(g, u, std::forward(evf)); +} + +/// @brief Create an outgoing incidence view from vertex id. +template +[[nodiscard]] constexpr auto out_incidence(G& g, adj_list::vertex_id_t uid) { + auto u = *adj_list::find_vertex(g, uid); + return out_incidence(g, u); +} + +/// @brief Create an outgoing incidence view with EVF from vertex id. +template +requires edge_value_function> +[[nodiscard]] constexpr auto out_incidence(G& g, adj_list::vertex_id_t uid, EVF&& evf) { + auto u = *adj_list::find_vertex(g, uid); + return out_incidence(g, u, std::forward(evf)); +} + +/// @brief Create a basic outgoing incidence view (target id only). +template +[[nodiscard]] constexpr auto basic_out_incidence(G& g, adj_list::vertex_id_t uid) { + auto u = *adj_list::find_vertex(g, uid); + return basic_incidence_view(g, u); +} + +/// @brief Create a basic outgoing incidence view with EVF. +template +requires edge_value_function> +[[nodiscard]] constexpr auto basic_out_incidence(G& g, adj_list::vertex_id_t uid, EVF&& evf) { + auto u = *adj_list::find_vertex(g, uid); + return basic_incidence_view, out_edge_accessor>(g, u, std::forward(evf)); +} + +// ============================================================================= +// Incoming factories: in_incidence / basic_in_incidence +// ============================================================================= + +/// @brief Create an incoming incidence view (no value function). +template +[[nodiscard]] constexpr auto in_incidence(G& g, adj_list::vertex_t u) noexcept { + return incidence_view(g, u); +} + +/// @brief Create an incoming incidence view with edge value function. +template +requires edge_value_function> +[[nodiscard]] constexpr auto in_incidence(G& g, adj_list::vertex_t u, EVF&& evf) { + return incidence_view, in_edge_accessor>(g, u, std::forward(evf)); +} + +/// @brief Create an incoming incidence view from vertex id. +template +[[nodiscard]] constexpr auto in_incidence(G& g, adj_list::vertex_id_t uid) { + auto u = *adj_list::find_vertex(g, uid); + return in_incidence(g, u); +} + +/// @brief Create an incoming incidence view with EVF from vertex id. +template +requires edge_value_function> +[[nodiscard]] constexpr auto in_incidence(G& g, adj_list::vertex_id_t uid, EVF&& evf) { + auto u = *adj_list::find_vertex(g, uid); + return in_incidence(g, u, std::forward(evf)); +} + +/// @brief Create a basic incoming incidence view (source id only). +template +[[nodiscard]] constexpr auto basic_in_incidence(G& g, adj_list::vertex_id_t uid) { + auto u = *adj_list::find_vertex(g, uid); + return basic_incidence_view(g, u); +} + +/// @brief Create a basic incoming incidence view with EVF. +template +requires edge_value_function> +[[nodiscard]] constexpr auto basic_in_incidence(G& g, adj_list::vertex_id_t uid, EVF&& evf) { + auto u = *adj_list::find_vertex(g, uid); + return basic_incidence_view, in_edge_accessor>(g, u, std::forward(evf)); +} + } // namespace graph::views diff --git a/include/graph/views/neighbors.hpp b/include/graph/views/neighbors.hpp index 07c140f..1081c5f 100644 --- a/include/graph/views/neighbors.hpp +++ b/include/graph/views/neighbors.hpp @@ -119,14 +119,15 @@ #include #include #include +#include namespace graph::views { // Forward declarations -template +template class neighbors_view; -template +template class basic_neighbors_view; /** @@ -152,15 +153,16 @@ class basic_neighbors_view; * @see basic_neighbors_view — simplified target-id-only variant * @see incidence_view — edge-descriptor variant */ -template -class neighbors_view : public std::ranges::view_interface> { +template +class neighbors_view : public std::ranges::view_interface> { public: using graph_type = G; + using accessor_type = Accessor; using vertex_type = adj_list::vertex_t; using vertex_id_type = adj_list::vertex_id_t; - using edge_range_type = adj_list::vertex_edge_range_t; - using edge_iterator_type = adj_list::vertex_edge_iterator_t; - using edge_type = adj_list::edge_t; + using edge_range_type = typename Accessor::template edge_range_t; + using edge_iterator_type = std::ranges::iterator_t; + using edge_type = typename Accessor::template edge_t; using info_type = neighbor_info; /** @@ -181,10 +183,9 @@ class neighbors_view : public std::ranges::view_interface(Accessor{}.neighbor_id(*g_, current_edge_)); + return value_type{nbr_id, nbr}; } constexpr iterator& operator++() noexcept { @@ -214,19 +215,19 @@ class neighbors_view : public std::ranges::view_interface { - return std::ranges::size(adj_list::edges(*g_, source_)); + return std::ranges::size(Accessor{}.edges(*g_, source_)); } private: @@ -265,15 +266,16 @@ class neighbors_view : public std::ranges::view_interface, VVF&&) — factory function * @see basic_neighbors_view — simplified target-id-only variant */ -template -class neighbors_view : public std::ranges::view_interface> { +template +class neighbors_view : public std::ranges::view_interface> { public: using graph_type = G; + using accessor_type = Accessor; using vertex_type = adj_list::vertex_t; using vertex_id_type = adj_list::vertex_id_t; - using edge_range_type = adj_list::vertex_edge_range_t; - using edge_iterator_type = adj_list::vertex_edge_iterator_t; - using edge_type = adj_list::edge_t; + using edge_range_type = typename Accessor::template edge_range_t; + using edge_iterator_type = std::ranges::iterator_t; + using edge_type = typename Accessor::template edge_t; using value_type_result = std::invoke_result_t; using info_type = neighbor_info; @@ -295,9 +297,9 @@ class neighbors_view : public std::ranges::view_interface constexpr iterator(G* g, edge_type e, VVF* vvf) noexcept : g_(g), current_edge_(e), vvf_(vvf) {} [[nodiscard]] constexpr value_type operator*() const { - auto target = adj_list::target(*g_, current_edge_); - auto target_id = adj_list::vertex_id(*g_, target); - return value_type{target_id, target, std::invoke(*vvf_, std::as_const(*g_), target)}; + auto nbr = Accessor{}.neighbor(*g_, current_edge_); + auto nbr_id = static_cast(Accessor{}.neighbor_id(*g_, current_edge_)); + return value_type{nbr_id, nbr, std::invoke(*vvf_, std::as_const(*g_), nbr)}; } constexpr iterator& operator++() noexcept { @@ -329,19 +331,19 @@ class neighbors_view : public std::ranges::view_interface : g_(&g), source_(u), vvf_(std::move(vvf)) {} [[nodiscard]] constexpr iterator begin() noexcept { - auto edge_range = adj_list::edges(*g_, source_); + auto edge_range = Accessor{}.edges(*g_, source_); return iterator(g_, *std::ranges::begin(edge_range), &vvf_); } [[nodiscard]] constexpr iterator end() noexcept { - auto edge_range = adj_list::edges(*g_, source_); + auto edge_range = Accessor{}.edges(*g_, source_); return iterator(g_, *std::ranges::end(edge_range), &vvf_); } [[nodiscard]] constexpr auto size() const noexcept requires std::ranges::sized_range { - return std::ranges::size(adj_list::edges(*g_, source_)); + return std::ranges::size(Accessor{}.edges(*g_, source_)); } private: @@ -352,10 +354,10 @@ class neighbors_view : public std::ranges::view_interface // Deduction guides template -neighbors_view(G&, adj_list::vertex_t) -> neighbors_view; +neighbors_view(G&, adj_list::vertex_t) -> neighbors_view; template -neighbors_view(G&, adj_list::vertex_t, VVF) -> neighbors_view; +neighbors_view(G&, adj_list::vertex_t, VVF) -> neighbors_view; /** * @brief Create a neighbors view over adjacent vertices (no value function). @@ -489,15 +491,16 @@ requires vertex_value_function> * @see basic_neighbors(G&, vertex_id_t) — factory function * @see neighbors_view — standard variant with target descriptor */ -template -class basic_neighbors_view : public std::ranges::view_interface> { +template +class basic_neighbors_view : public std::ranges::view_interface> { public: using graph_type = G; + using accessor_type = Accessor; using vertex_type = adj_list::vertex_t; using vertex_id_type = adj_list::vertex_id_t; - using edge_range_type = adj_list::vertex_edge_range_t; - using edge_iterator_type = adj_list::vertex_edge_iterator_t; - using edge_type = adj_list::edge_t; + using edge_range_type = typename Accessor::template edge_range_t; + using edge_iterator_type = std::ranges::iterator_t; + using edge_type = typename Accessor::template edge_t; using info_type = neighbor_info; /** @@ -518,7 +521,7 @@ class basic_neighbors_view : public std::ranges::view_interface(adj_list::target_id(*g_, current_edge_))}; + return value_type{static_cast(Accessor{}.neighbor_id(*g_, current_edge_))}; } constexpr iterator& operator++() noexcept { @@ -548,19 +551,19 @@ class basic_neighbors_view : public std::ranges::view_interface { - return std::ranges::size(adj_list::edges(*g_, source_)); + return std::ranges::size(Accessor{}.edges(*g_, source_)); } private: @@ -600,15 +603,16 @@ class basic_neighbors_view : public std::ranges::view_interface, VVF&&) — factory function * @see neighbors_view — standard variant with target descriptor */ -template -class basic_neighbors_view : public std::ranges::view_interface> { +template +class basic_neighbors_view : public std::ranges::view_interface> { public: using graph_type = G; + using accessor_type = Accessor; using vertex_type = adj_list::vertex_t; using vertex_id_type = adj_list::vertex_id_t; - using edge_range_type = adj_list::vertex_edge_range_t; - using edge_iterator_type = adj_list::vertex_edge_iterator_t; - using edge_type = adj_list::edge_t; + using edge_range_type = typename Accessor::template edge_range_t; + using edge_iterator_type = std::ranges::iterator_t; + using edge_type = typename Accessor::template edge_t; using value_type_result = std::invoke_result_t; using info_type = neighbor_info; @@ -630,9 +634,9 @@ class basic_neighbors_view : public std::ranges::view_interface(adj_list::vertex_id(*g_, target)); - return value_type{target_id, std::invoke(*vvf_, std::as_const(*g_), target)}; + auto nbr = Accessor{}.neighbor(*g_, current_edge_); + auto nbr_id = static_cast(Accessor{}.neighbor_id(*g_, current_edge_)); + return value_type{nbr_id, std::invoke(*vvf_, std::as_const(*g_), nbr)}; } constexpr iterator& operator++() noexcept { @@ -670,19 +674,19 @@ class basic_neighbors_view : public std::ranges::view_interface { - return std::ranges::size(adj_list::edges(*g_, source_)); + return std::ranges::size(Accessor{}.edges(*g_, source_)); } private: @@ -693,10 +697,10 @@ class basic_neighbors_view : public std::ranges::view_interface -basic_neighbors_view(G&, adj_list::vertex_t) -> basic_neighbors_view; +basic_neighbors_view(G&, adj_list::vertex_t) -> basic_neighbors_view; template -basic_neighbors_view(G&, adj_list::vertex_t, VVF) -> basic_neighbors_view; +basic_neighbors_view(G&, adj_list::vertex_t, VVF) -> basic_neighbors_view; // ============================================================================= // Factory functions: basic_neighbors @@ -749,4 +753,98 @@ requires vertex_value_function> return basic_neighbors_view>(g, u, std::forward(vvf)); } +// ============================================================================= +// Explicit outgoing factories: out_neighbors / basic_out_neighbors +// ============================================================================= + +/// @brief Create an outgoing neighbors view (no value function). +template +[[nodiscard]] constexpr auto out_neighbors(G& g, adj_list::vertex_t u) noexcept { + return neighbors_view(g, u); +} + +/// @brief Create an outgoing neighbors view with vertex value function. +template +requires vertex_value_function> +[[nodiscard]] constexpr auto out_neighbors(G& g, adj_list::vertex_t u, VVF&& vvf) { + return neighbors_view, out_edge_accessor>(g, u, std::forward(vvf)); +} + +/// @brief Create an outgoing neighbors view from vertex id. +template +[[nodiscard]] constexpr auto out_neighbors(G& g, adj_list::vertex_id_t uid) { + auto u = *adj_list::find_vertex(g, uid); + return out_neighbors(g, u); +} + +/// @brief Create an outgoing neighbors view with VVF from vertex id. +template +requires vertex_value_function> +[[nodiscard]] constexpr auto out_neighbors(G& g, adj_list::vertex_id_t uid, VVF&& vvf) { + auto u = *adj_list::find_vertex(g, uid); + return out_neighbors(g, u, std::forward(vvf)); +} + +/// @brief Create a basic outgoing neighbors view (target id only). +template +[[nodiscard]] constexpr auto basic_out_neighbors(G& g, adj_list::vertex_id_t uid) { + auto u = *adj_list::find_vertex(g, uid); + return basic_neighbors_view(g, u); +} + +/// @brief Create a basic outgoing neighbors view with VVF. +template +requires vertex_value_function> +[[nodiscard]] constexpr auto basic_out_neighbors(G& g, adj_list::vertex_id_t uid, VVF&& vvf) { + auto u = *adj_list::find_vertex(g, uid); + return basic_neighbors_view, out_edge_accessor>(g, u, std::forward(vvf)); +} + +// ============================================================================= +// Incoming factories: in_neighbors / basic_in_neighbors +// ============================================================================= + +/// @brief Create an incoming neighbors view (no value function). +template +[[nodiscard]] constexpr auto in_neighbors(G& g, adj_list::vertex_t u) noexcept { + return neighbors_view(g, u); +} + +/// @brief Create an incoming neighbors view with vertex value function. +template +requires vertex_value_function> +[[nodiscard]] constexpr auto in_neighbors(G& g, adj_list::vertex_t u, VVF&& vvf) { + return neighbors_view, in_edge_accessor>(g, u, std::forward(vvf)); +} + +/// @brief Create an incoming neighbors view from vertex id. +template +[[nodiscard]] constexpr auto in_neighbors(G& g, adj_list::vertex_id_t uid) { + auto u = *adj_list::find_vertex(g, uid); + return in_neighbors(g, u); +} + +/// @brief Create an incoming neighbors view with VVF from vertex id. +template +requires vertex_value_function> +[[nodiscard]] constexpr auto in_neighbors(G& g, adj_list::vertex_id_t uid, VVF&& vvf) { + auto u = *adj_list::find_vertex(g, uid); + return in_neighbors(g, u, std::forward(vvf)); +} + +/// @brief Create a basic incoming neighbors view (source id only). +template +[[nodiscard]] constexpr auto basic_in_neighbors(G& g, adj_list::vertex_id_t uid) { + auto u = *adj_list::find_vertex(g, uid); + return basic_neighbors_view(g, u); +} + +/// @brief Create a basic incoming neighbors view with VVF. +template +requires vertex_value_function> +[[nodiscard]] constexpr auto basic_in_neighbors(G& g, adj_list::vertex_id_t uid, VVF&& vvf) { + auto u = *adj_list::find_vertex(g, uid); + return basic_neighbors_view, in_edge_accessor>(g, u, std::forward(vvf)); +} + } // namespace graph::views diff --git a/include/graph/views/topological_sort.hpp b/include/graph/views/topological_sort.hpp index 1534d68..3e8ead3 100644 --- a/include/graph/views/topological_sort.hpp +++ b/include/graph/views/topological_sort.hpp @@ -161,14 +161,17 @@ #include #include #include +#include namespace graph::views { // Forward declarations -template > +template , + class Accessor = out_edge_accessor> class vertices_topological_sort_view; -template > +template , + class Accessor = out_edge_accessor> class edges_topological_sort_view; namespace topo_detail { @@ -187,7 +190,7 @@ namespace topo_detail { * @par Complexity * Time: O(V + E). Space: O(V) (O(2V) with cycle detection). */ - template + template struct topo_state { using graph_type = G; using vertex_type = adj_list::vertex_t; @@ -239,8 +242,8 @@ namespace topo_detail { } // Visit all children - for (auto edge : adj_list::edges(g, v)) { - auto target_v = adj_list::target(g, edge); + for (auto edge : Accessor{}.edges(g, v)) { + auto target_v = Accessor{}.neighbor(g, edge); auto target_vid = adj_list::vertex_id(g, target_v); if (detect_cycles && rec_stack_[target_vid]) { @@ -284,9 +287,9 @@ namespace topo_detail { * @see vertices_topological_sort_view — with value function * @see edges_topological_sort_view — edge-oriented variant */ -template -class vertices_topological_sort_view - : public std::ranges::view_interface> { +template +class vertices_topological_sort_view + : public std::ranges::view_interface> { public: using graph_type = G; using vertex_type = adj_list::vertex_t; @@ -295,7 +298,7 @@ class vertices_topological_sort_view using info_type = vertex_info; private: - using state_type = topo_detail::topo_state; + using state_type = topo_detail::topo_state; public: /** @@ -407,9 +410,9 @@ class vertices_topological_sort_view * @see vertices_topological_sort_view — without value function * @see edges_topological_sort_view — edge-oriented variant */ -template +template class vertices_topological_sort_view - : public std::ranges::view_interface> { + : public std::ranges::view_interface> { public: using graph_type = G; using vertex_type = adj_list::vertex_t; @@ -419,7 +422,7 @@ class vertices_topological_sort_view using info_type = vertex_info; private: - using state_type = topo_detail::topo_state; + using state_type = topo_detail::topo_state; public: /** @@ -544,18 +547,19 @@ vertices_topological_sort_view(G&, VVF, Alloc) -> vertices_topological_sort_view * @see edges_topological_sort_view — with value function * @see vertices_topological_sort_view — vertex-oriented variant */ -template -class edges_topological_sort_view - : public std::ranges::view_interface> { +template +class edges_topological_sort_view + : public std::ranges::view_interface> { public: using graph_type = G; using vertex_type = adj_list::vertex_t; - using edge_type = adj_list::edge_t; + using edge_type = typename Accessor::template edge_t; using allocator_type = Alloc; using info_type = edge_info; private: - using state_type = topo_detail::topo_state; + using state_type = topo_detail::topo_state; + using edge_iter_type = std::ranges::iterator_t>; public: /** @@ -578,7 +582,7 @@ class edges_topological_sort_view : g_(g), state_(std::move(state)), vertex_index_(v_idx) { if (!at_end()) { auto v = state_->post_order_[vertex_index_]; - auto edge_range = adj_list::edges(*g_, v); + auto edge_range = Accessor{}.edges(*g_, v); edge_it_ = std::ranges::begin(edge_range); edge_end_ = std::ranges::end(edge_range); skip_to_first_edge(); // Position without incrementing count_ @@ -622,7 +626,7 @@ class edges_topological_sort_view ++vertex_index_; if (vertex_index_ < state_->post_order_.size()) { auto v = state_->post_order_[vertex_index_]; - auto edge_range = adj_list::edges(*g_, v); + auto edge_range = Accessor{}.edges(*g_, v); edge_it_ = std::ranges::begin(edge_range); edge_end_ = std::ranges::end(edge_range); } @@ -642,18 +646,18 @@ class edges_topological_sort_view ++vertex_index_; if (vertex_index_ < state_->post_order_.size()) { auto v = state_->post_order_[vertex_index_]; - auto edge_range = adj_list::edges(*g_, v); + auto edge_range = Accessor{}.edges(*g_, v); edge_it_ = std::ranges::begin(edge_range); edge_end_ = std::ranges::end(edge_range); } } } - G* g_ = nullptr; - std::shared_ptr state_; - std::size_t vertex_index_ = 0; - adj_list::vertex_edge_iterator_t edge_it_{}; - adj_list::vertex_edge_iterator_t edge_end_{}; + G* g_ = nullptr; + std::shared_ptr state_; + std::size_t vertex_index_ = 0; + edge_iter_type edge_it_{}; + edge_iter_type edge_end_{}; }; struct sentinel { @@ -708,18 +712,19 @@ class edges_topological_sort_view * @see edges_topological_sort_view — without value function * @see vertices_topological_sort_view — vertex-oriented variant */ -template -class edges_topological_sort_view : public std::ranges::view_interface> { +template +class edges_topological_sort_view : public std::ranges::view_interface> { public: using graph_type = G; using vertex_type = adj_list::vertex_t; - using edge_type = adj_list::edge_t; + using edge_type = typename Accessor::template edge_t; using allocator_type = Alloc; using value_result_type = std::invoke_result_t; using info_type = edge_info; private: - using state_type = topo_detail::topo_state; + using state_type = topo_detail::topo_state; + using edge_iter_type = std::ranges::iterator_t>; public: /** @@ -741,7 +746,7 @@ class edges_topological_sort_view : public std::ranges::view_interfacepost_order_[vertex_index_]; - auto edge_range = adj_list::edges(*g_, v); + auto edge_range = Accessor{}.edges(*g_, v); edge_it_ = std::ranges::begin(edge_range); edge_end_ = std::ranges::end(edge_range); skip_to_first_edge(); // Position without incrementing count_ @@ -787,7 +792,7 @@ class edges_topological_sort_view : public std::ranges::view_interfacepost_order_.size()) { auto v = state_->post_order_[vertex_index_]; - auto edge_range = adj_list::edges(*g_, v); + auto edge_range = Accessor{}.edges(*g_, v); edge_it_ = std::ranges::begin(edge_range); edge_end_ = std::ranges::end(edge_range); } @@ -805,19 +810,19 @@ class edges_topological_sort_view : public std::ranges::view_interfacepost_order_.size()) { auto v = state_->post_order_[vertex_index_]; - auto edge_range = adj_list::edges(*g_, v); + auto edge_range = Accessor{}.edges(*g_, v); edge_it_ = std::ranges::begin(edge_range); edge_end_ = std::ranges::end(edge_range); } } } - G* g_ = nullptr; - std::shared_ptr state_; - std::size_t vertex_index_ = 0; - adj_list::vertex_edge_iterator_t edge_it_{}; - adj_list::vertex_edge_iterator_t edge_end_{}; - EVF* evf_ = nullptr; + G* g_ = nullptr; + std::shared_ptr state_; + std::size_t vertex_index_ = 0; + edge_iter_type edge_it_{}; + edge_iter_type edge_end_{}; + EVF* evf_ = nullptr; }; struct sentinel { @@ -1198,4 +1203,110 @@ requires edge_value_function> return view_type(g, std::forward(evf), std::move(state)); } +//============================================================================= +// Accessor-parameterized factory functions +//============================================================================= +// Usage: vertices_topological_sort(g) +// edges_topological_sort(g) + +/// Topological vertex traversal with explicit Accessor. +template +[[nodiscard]] auto vertices_topological_sort(G& g) { + return vertices_topological_sort_view, Accessor>(g, std::allocator{}); +} + +/// Topological vertex traversal with explicit Accessor and value function. +template +requires vertex_value_function> +[[nodiscard]] auto vertices_topological_sort(G& g, VVF&& vvf) { + return vertices_topological_sort_view, std::allocator, Accessor>( + g, std::forward(vvf), std::allocator{}); +} + +/// Topological edge traversal with explicit Accessor. +template +[[nodiscard]] auto edges_topological_sort(G& g) { + return edges_topological_sort_view, Accessor>(g, std::allocator{}); +} + +/// Topological edge traversal with explicit Accessor and value function. +template +requires edge_value_function> +[[nodiscard]] auto edges_topological_sort(G& g, EVF&& evf) { + return edges_topological_sort_view, std::allocator, Accessor>( + g, std::forward(evf), std::allocator{}); +} + +//============================================================================= +// Accessor-parameterized safe factory functions +//============================================================================= + +/// Topological vertex traversal with explicit Accessor and cycle detection. +template +[[nodiscard]] auto vertices_topological_sort_safe(G& g) + -> tl::expected, Accessor>, adj_list::vertex_t> { + using view_type = vertices_topological_sort_view, Accessor>; + + auto state = + std::make_shared, Accessor>>(g, std::allocator{}, true); + + if (state->has_cycle()) { + return tl::unexpected(state->cycle_vertex().value()); + } + + return view_type(g, std::move(state)); +} + +/// Topological vertex traversal with explicit Accessor, value function, and cycle detection. +template +requires vertex_value_function> +[[nodiscard]] auto vertices_topological_sort_safe(G& g, VVF&& vvf) + -> tl::expected, std::allocator, Accessor>, + adj_list::vertex_t> { + using view_type = vertices_topological_sort_view, std::allocator, Accessor>; + + auto state = + std::make_shared, Accessor>>(g, std::allocator{}, true); + + if (state->has_cycle()) { + return tl::unexpected(state->cycle_vertex().value()); + } + + return view_type(g, std::forward(vvf), std::move(state)); +} + +/// Topological edge traversal with explicit Accessor and cycle detection. +template +[[nodiscard]] auto edges_topological_sort_safe(G& g) + -> tl::expected, Accessor>, adj_list::vertex_t> { + using view_type = edges_topological_sort_view, Accessor>; + + auto state = + std::make_shared, Accessor>>(g, std::allocator{}, true); + + if (state->has_cycle()) { + return tl::unexpected(state->cycle_vertex().value()); + } + + return view_type(g, std::move(state)); +} + +/// Topological edge traversal with explicit Accessor, value function, and cycle detection. +template +requires edge_value_function> +[[nodiscard]] auto edges_topological_sort_safe(G& g, EVF&& evf) + -> tl::expected, std::allocator, Accessor>, + adj_list::vertex_t> { + using view_type = edges_topological_sort_view, std::allocator, Accessor>; + + auto state = + std::make_shared, Accessor>>(g, std::allocator{}, true); + + if (state->has_cycle()) { + return tl::unexpected(state->cycle_vertex().value()); + } + + return view_type(g, std::forward(evf), std::move(state)); +} + } // namespace graph::views diff --git a/include/graph/views/transpose.hpp b/include/graph/views/transpose.hpp new file mode 100644 index 0000000..1cfc55c --- /dev/null +++ b/include/graph/views/transpose.hpp @@ -0,0 +1,238 @@ +/** + * @file transpose.hpp + * @brief Zero-cost transpose adaptor for bidirectional graphs. + * + * Wraps a bidirectional graph so that the roles of outgoing and incoming + * edges are exchanged: + * + * | Original CPO | Transpose CPO | + * |---------------------|----------------------------| + * | edges(g, u) | in_edges(underlying, u) | + * | in_edges(g, u) | edges(underlying, u) | + * | target_id(g, e) | source_id(underlying, e) | + * | source_id(g, e) | target_id(underlying, e) | + * | target(g, e) | source(underlying, e) | + * | source(g, e) | target(underlying, e) | + * | degree(g, v) | in_degree(underlying, v) | + * | in_degree(g, v) | degree(underlying, v) | + * + * All other CPOs (vertices, num_vertices, find_vertex, vertex_id, + * edge_value, graph_value, etc.) forward to the underlying graph. + * + * @section concept_satisfaction Concept Satisfaction + * + * When the underlying graph satisfies `index_bidirectional_adjacency_list`, + * `transpose_view` also satisfies `index_bidirectional_adjacency_list`. + * + * @section limitations Known Limitations + * + * The `target_id` / `source_id` ADL overrides resolve correctly for + * graphs whose edge descriptors use index-based storage (random-access + * containers: vov, vod, dov, dod, dofl, etc.). For forward-iterator + * containers (vol, dol), the CPO's tier-1 ("native edge member") fires + * before the ADL tier, bypassing the swap. A future accessor-parameterised + * DFS/BFS (Phases 5 + 7) will eliminate this limitation. + * + * For algorithm use with ALL container types, prefer the single-graph + * `kosaraju(g, component)` overload, which handles reverse traversal + * internally without needing transpose_view. + * + * @section usage Usage + * + * @code + * auto tv = graph::views::transpose(bidir_graph); + * + * // Iterate "forward" edges of the transpose = in-edges of original + * for (auto&& [uid, u] : views::vertexlist(tv)) { + * for (auto&& [tid, e] : views::incidence(tv, u)) { + * // tid is the source vertex in the original graph + * } + * } + * @endcode + * + * @see connected_components.hpp — kosaraju(g, component) bidirectional overload + */ + +#pragma once + +#include +#include + +namespace graph::views { + +// ============================================================================ +// transpose_view — bidirectional graph adaptor +// ============================================================================ + +/** + * @brief Adaptor that presents a bidirectional graph with edges reversed. + * + * Stores a pointer to the underlying graph. All ADL friend functions + * delegate to the underlying graph, swapping outgoing/incoming roles. + * + * @tparam G Underlying graph type (must satisfy bidirectional_adjacency_list) + */ +template +class transpose_view { +public: + using graph_type = G; + + /// Construct from a mutable reference to the underlying graph. + explicit constexpr transpose_view(G& g) noexcept : g_(&g) {} + + /// Access the underlying graph. + [[nodiscard]] constexpr G& base() noexcept { return *g_; } + [[nodiscard]] constexpr const G& base() const noexcept { return *g_; } + + // =========================================================================== + // Vertex CPO friends — forwarded unchanged + // =========================================================================== + + friend constexpr auto vertices(transpose_view& tv) { return adj_list::vertices(*tv.g_); } + friend constexpr auto vertices(const transpose_view& tv) { return adj_list::vertices(*tv.g_); } + + friend constexpr auto num_vertices(const transpose_view& tv) { return adj_list::num_vertices(*tv.g_); } + + friend constexpr auto find_vertex(transpose_view& tv, adj_list::vertex_id_t uid) { + return adj_list::find_vertex(*tv.g_, uid); + } + friend constexpr auto find_vertex(const transpose_view& tv, adj_list::vertex_id_t uid) { + return adj_list::find_vertex(*tv.g_, uid); + } + + template + friend constexpr auto vertex_id(const transpose_view& tv, const V& v) { + return adj_list::vertex_id(*tv.g_, v); + } + + template + friend constexpr auto has_edges(const transpose_view& tv, const V& v) { + return adj_list::has_edges(*tv.g_, v); + } + + // =========================================================================== + // Edge CPO friends — directions swapped + // =========================================================================== + + /// edges(transpose, v) → in_edges(underlying, v) + template + friend constexpr auto edges(transpose_view& tv, V&& v) { + return adj_list::in_edges(*tv.g_, std::forward(v)); + } + template + friend constexpr auto edges(const transpose_view& tv, V&& v) { + return adj_list::in_edges(*tv.g_, std::forward(v)); + } + + /// in_edges(transpose, v) → edges(underlying, v) + template + friend constexpr auto in_edges(transpose_view& tv, V&& v) { + return adj_list::edges(*tv.g_, std::forward(v)); + } + template + friend constexpr auto in_edges(const transpose_view& tv, V&& v) { + return adj_list::edges(*tv.g_, std::forward(v)); + } + + // =========================================================================== + // Edge property CPO friends — source/target swapped + // =========================================================================== + + /// target_id(transpose, e) → source_id(underlying, e) + template + friend constexpr auto target_id(const transpose_view& tv, const E& e) { + return adj_list::source_id(*tv.g_, e); + } + template + friend constexpr auto target_id(transpose_view& tv, const E& e) { + return adj_list::source_id(*tv.g_, e); + } + + /// source_id(transpose, e) → target_id(underlying, e) + template + friend constexpr auto source_id(const transpose_view& tv, const E& e) { + return adj_list::target_id(*tv.g_, e); + } + template + friend constexpr auto source_id(transpose_view& tv, const E& e) { + return adj_list::target_id(*tv.g_, e); + } + + /// target(transpose, e) → source(underlying, e) + template + friend constexpr auto target(const transpose_view& tv, const E& e) { + return adj_list::source(*tv.g_, e); + } + template + friend constexpr auto target(transpose_view& tv, const E& e) { + return adj_list::source(*tv.g_, e); + } + + /// source(transpose, e) → target(underlying, e) + template + friend constexpr auto source(const transpose_view& tv, const E& e) { + return adj_list::target(*tv.g_, e); + } + template + friend constexpr auto source(transpose_view& tv, const E& e) { + return adj_list::target(*tv.g_, e); + } + + /// edge_value — forwarded unchanged + template + friend constexpr decltype(auto) edge_value(const transpose_view& tv, const E& e) { + return adj_list::edge_value(*tv.g_, e); + } + template + friend constexpr decltype(auto) edge_value(transpose_view& tv, const E& e) { + return adj_list::edge_value(*tv.g_, e); + } + + // =========================================================================== + // Degree CPO friends — swapped + // =========================================================================== + + /// degree(transpose, v) → in_degree(underlying, v) + template + friend constexpr auto degree(const transpose_view& tv, const V& v) { + return adj_list::in_degree(*tv.g_, v); + } + + /// in_degree(transpose, v) → degree(underlying, v) + template + friend constexpr auto in_degree(const transpose_view& tv, const V& v) { + return adj_list::degree(*tv.g_, v); + } + + // =========================================================================== + // num_edges — forwarded + // =========================================================================== + + template + friend constexpr auto num_edges(const transpose_view& tv, const V& v) { + return adj_list::num_edges(*tv.g_, v); + } + + friend constexpr auto num_edges(const transpose_view& tv) { return adj_list::num_edges(*tv.g_); } + +private: + G* g_; +}; + +// ============================================================================ +// Factory function +// ============================================================================ + +/** + * @brief Create a transpose view of a bidirectional graph. + * + * @tparam G Graph type (must satisfy bidirectional_adjacency_list) + * @param g The graph to transpose (by reference; must outlive the view) + * @return transpose_view + */ +template +[[nodiscard]] constexpr transpose_view transpose(G& g) noexcept { + return transpose_view(g); +} + +} // namespace graph::views diff --git a/include/graph/views/vertexlist.hpp b/include/graph/views/vertexlist.hpp index b53a2f3..1c81d4f 100644 --- a/include/graph/views/vertexlist.hpp +++ b/include/graph/views/vertexlist.hpp @@ -85,10 +85,34 @@ * @section supported_graphs Supported Graph Properties * * - Requires: @c adjacency_list concept - * (subrange overloads require @c index_adjacency_list) * - Works with all @c dynamic_graph container combinations * - Works with directed and undirected graphs * + * @subsection index_adjacency_list_requirement Subrange Overloads Require @c index_adjacency_list + * + * The subrange factory overloads — @c vertexlist(g,first_u,last_u), + * @c vertexlist(g,first_u,last_u,vvf), @c basic_vertexlist(g,first_uid,last_uid), + * and @c basic_vertexlist(g,first_uid,last_uid,vvf) — require + * @c index_adjacency_list rather than plain @c adjacency_list because: + * + * 1. **O(1) @c size()** — The view's @c size() member is computed at construction + * as `vertex_id(g,last_u) - vertex_id(g,first_u)` (descriptor overloads) or + * `last_uid - first_uid` (id overloads). Subtraction on @c vertex_id_t + * requires an @c std::integral vertex ID type. @c index_adjacency_list + * enforces this via @c index_vertex_range. + * + * 2. **O(1) @c find_vertex()** — The id-based @c basic_vertexlist overloads call + * @c find_vertex(g, first_uid) and @c find_vertex(g, last_uid) to convert IDs + * back to descriptors. On random-access containers (e.g. @c vector, @c deque) + * this is O(1); on map-based containers it is O(log N) or O(1) amortised but + * the result is not a contiguous-range iterator. @c index_adjacency_list + * ensures the underlying container supports random-access iteration, keeping + * construction O(1). + * + * Relaxing to `adjacency_list && std::integral>` would allow + * non-random-access containers (e.g. @c map-vertex graphs) at the cost of + * making @c find_vertex O(log N). The current stricter constraint is intentional. + * * @section exception_safety Exception Safety * * Construction is @c noexcept when VVF is nothrow move-constructible (or diff --git a/scripts/remove_sourced.py b/scripts/remove_sourced.py new file mode 100644 index 0000000..d689267 --- /dev/null +++ b/scripts/remove_sourced.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +""" +Phase 5.2 — Remove Sourced references from test files. + +Transformations applied to each file: +1. Remove `using xxx_sourced... = ...;` alias declarations (multi-line) +2. Remove TEST_CASE blocks tagged with [sourced] +3. Remove SECTION("sourced...") blocks (inside non-sourced TEST_CASEs) +4. Remove STATIC_REQUIRE(traits::sourced == ...) lines +5. Remove [[maybe_unused]] XXX_sourced variable declarations +6. Substitute `, false, false, ` → `, false, ` in remaining aliases (drop Sourced=false) + and the corresponding `, false>` in trailing traits args (they stay — Bidirectionl=false) + +Brace-counting logic handles nested {}. +""" + +import re +import sys +from pathlib import Path + + +def remove_block(lines: list[str], start: int) -> int: + """Remove a brace-delimited block starting at lines[start]. + Returns the index of the first line AFTER the block.""" + depth = 0 + i = start + while i < len(lines): + line = lines[i] + depth += line.count('{') - line.count('}') + i += 1 + if depth <= 0 and i > start + 1: + break + return i + + +def is_sourced_test_case(line: str) -> bool: + """Return True if the line starts a TEST_CASE or TEMPLATE_TEST_CASE tagged [sourced]. + NOTE: only checks the single line; for multi-line, use is_sourced_test_case_multiline.""" + if not re.match(r'\s*(TEMPLATE_)?TEST_CASE\s*\(', line): + return False + return bool(re.search(r'\[sourced\]', line)) + + +def is_sourced_test_case_multiline(lines: list[str], i: int) -> bool: + """Return True if lines[i] starts a TEST_CASE/TEMPLATE_TEST_CASE and the [sourced] + tag appears within the next few lines of the argument list (closing paren).""" + line = lines[i] + if not re.match(r'\s*(TEMPLATE_)?TEST_CASE\s*\(', line): + return False + # Check up to 5 lines for the closing ')' and [sourced] tag + for j in range(i, min(i + 6, len(lines))): + if re.search(r'\[sourced\]', lines[j]): + return True + if ')' in lines[j] and j > i: # closing paren of TEST_CASE args + break + return False + + +def is_sourced_section(line: str) -> bool: + """Return True if line is SECTION("sourced...).""" + return bool(re.match(r'\s*SECTION\s*\(\s*"sourced', line, re.IGNORECASE)) + + +def find_block_end(lines: list[str], start: int) -> int: + """Find the end of the {} block that begins on or after lines[start]. + The block starts with the first '{' encountered. + Returns the index after the closing '}'.""" + i = start + depth = 0 + found_open = False + while i < len(lines): + line = lines[i] + opens = line.count('{') + closes = line.count('}') + if not found_open and opens > 0: + found_open = True + if found_open: + depth += opens - closes + if depth <= 0: + return i + 1 + i += 1 + return len(lines) + + +def remove_multiline_alias(lines: list[str], start: int) -> int: + """Remove `using xxx_sourced = ...;` starting at lines[start]. + Returns index after the removed declaration.""" + i = start + while i < len(lines): + if lines[i].rstrip().endswith(';'): + return i + 1 + i += 1 + return i + 1 + + +def process_lines(lines: list[str]) -> list[str]: + result = [] + i = 0 + while i < len(lines): + line = lines[i] + + # 1. Sourced type alias declarations: `using xxx_sourced... =` + if re.match(r'\s*using \w*[Ss]ourced\w*\s*=', line): + i = remove_multiline_alias(lines, i) + continue + + # 2. TEST_CASE / TEMPLATE_TEST_CASE blocks tagged [sourced] + if is_sourced_test_case_multiline(lines, i): + i = find_block_end(lines, i) + continue + + # 3. SECTION("sourced ...") blocks + if is_sourced_section(line): + i = find_block_end(lines, i) + continue + + # 4. STATIC_REQUIRE(traits::sourced == ...) and static_assert(G::sourced ...) lines + if re.search(r'(STATIC_REQUIRE|static_assert)\s*\(\s*(traits|G)::sourced\s*==', line): + i += 1 + continue + + # 5. [[maybe_unused]] XXX_sourced... variable declarations + if re.match(r'\s*\[\[maybe_unused\]\]\s+\w*[Ss]ourced\w*\b', line): + i += 1 + continue + + # 6. In template-arg lists, drop the Sourced=false position: + # `, false, false,` → `, false,` (remove Sourced's slot; Bidir stays) + # Also handles continuation lines like `false, false, Traits<...>` + if ', false, false,' in line: + line = line.replace(', false, false,', ', false,', 1) + elif re.match(r'^(\s*)false, false,', line): + # Multi-line alias continuation: leading `false, false, ...` → `false, ...` + line = re.sub(r'^(\s*)false, false,', r'\1false,', line, count=1) + + result.append(line) + i += 1 + + return result + + +def process_file(file_path: Path, dry_run: bool = False) -> bool: + """Process a single file. Returns True if changed.""" + with open(file_path) as f: + content = f.read() + + lines = content.splitlines(keepends=True) + new_lines = process_lines(lines) + new_content = ''.join(new_lines) + + if new_content == content: + return False + + if not dry_run: + with open(file_path, 'w') as f: + f.write(new_content) + + return True + + +def main(): + dry_run = '--dry-run' in sys.argv + paths = [p for p in sys.argv[1:] if not p.startswith('--')] + + if not paths: + print("Usage: remove_sourced.py [--dry-run] ...") + sys.exit(1) + + changed = 0 + for path_str in paths: + p = Path(path_str) + if not p.exists(): + print(f" SKIP (not found): {p}") + continue + modified = process_file(p, dry_run=dry_run) + if modified: + print(f" CHANGED: {p.name}") + changed += 1 + else: + print(f" unchanged: {p.name}") + + print(f"\nTotal changed: {changed}/{len(paths)}") + + +if __name__ == '__main__': + main() diff --git a/tests/adj_list/CMakeLists.txt b/tests/adj_list/CMakeLists.txt index 3057a59..61292c9 100644 --- a/tests/adj_list/CMakeLists.txt +++ b/tests/adj_list/CMakeLists.txt @@ -5,6 +5,7 @@ add_executable(graph3_adj_list_tests concepts/test_vertex_concepts.cpp concepts/test_adjacency_list_edge_concepts.cpp concepts/test_adjacency_list_vertex_concepts.cpp + concepts/test_bidirectional_concepts.cpp # Descriptors descriptors/test_vertex_descriptor.cpp @@ -24,9 +25,12 @@ add_executable(graph3_adj_list_tests cpo/test_num_vertices_cpo.cpp cpo/test_num_edges_cpo.cpp cpo/test_degree_cpo.cpp + cpo/test_in_edges_cpo.cpp + cpo/test_find_in_edge_cpo.cpp + cpo/test_contains_in_edge_cpo.cpp cpo/test_find_vertex_edge_cpo.cpp cpo/test_contains_edge_cpo.cpp - cpo/test_has_edge_cpo.cpp + cpo/test_has_edges_cpo.cpp cpo/test_vertex_value_cpo.cpp cpo/test_edge_value_cpo.cpp cpo/test_edge_value_cpo_value_method.cpp @@ -35,9 +39,11 @@ add_executable(graph3_adj_list_tests cpo/test_num_partitions_cpo.cpp cpo/test_vertices_pid_cpo.cpp cpo/test_num_vertices_pid_cpo.cpp + cpo/test_bidir_cpo_consistency.cpp # Traits traits/test_type_aliases.cpp + traits/test_incoming_edge_traits.cpp traits/test_adjacency_list_traits.cpp ) diff --git a/tests/adj_list/concepts/test_adjacency_list_edge_concepts.cpp b/tests/adj_list/concepts/test_adjacency_list_edge_concepts.cpp index f07d8ee..2296ac9 100644 --- a/tests/adj_list/concepts/test_adjacency_list_edge_concepts.cpp +++ b/tests/adj_list/concepts/test_adjacency_list_edge_concepts.cpp @@ -82,17 +82,17 @@ TEST_CASE("edge concept - edge_descriptor from deque>", "[adjacency_l } // ============================================================================= -// vertex_edge_range Concept Tests +// out_edge_range Concept Tests // ============================================================================= -TEST_CASE("vertex_edge_range concept - edge_descriptor_view from vector>", +TEST_CASE("out_edge_range concept - edge_descriptor_view from vector>", "[adjacency_list][concepts][range]") { using Graph = std::vector>; using VertexIter = typename Graph::iterator; using EdgeIter = typename std::vector::iterator; using EdgeDescView = edge_descriptor_view; - STATIC_REQUIRE(vertex_edge_range); + STATIC_REQUIRE(out_edge_range); Graph g = {{1, 2, 3}, {0, 2}, {0, 1}}; auto verts = vertices(g); @@ -111,14 +111,14 @@ TEST_CASE("vertex_edge_range concept - edge_descriptor_view from vector>", +TEST_CASE("out_edge_range concept - edge_descriptor_view from vector>", "[adjacency_list][concepts][range]") { using Graph = std::vector>>; using VertexIter = typename Graph::iterator; using EdgeIter = typename std::vector>::iterator; using EdgeDescView = edge_descriptor_view; - STATIC_REQUIRE(vertex_edge_range); + STATIC_REQUIRE(out_edge_range); Graph g = {{{1, 1.5}, {2, 2.5}}, {{0, 0.5}}, {}}; auto verts = vertices(g); @@ -136,13 +136,13 @@ TEST_CASE("vertex_edge_range concept - edge_descriptor_view from vector>; using VertexIter = typename Graph::iterator; using EdgeIter = typename std::vector::iterator; using EdgeDescView = edge_descriptor_view; - STATIC_REQUIRE(vertex_edge_range); + STATIC_REQUIRE(out_edge_range); Graph g = {{1, 2}, {2, 3}, {0, 1}}; auto verts = vertices(g); @@ -191,7 +191,7 @@ TEST_CASE("Edge concepts - concept requirements documented", "[adjacency_list][c } TEST_CASE("Edge range concepts - range requirements documented", "[adjacency_list][concepts][range][documentation]") { - // vertex_edge_range requires: + // out_edge_range requires: // - R is a forward_range // - range_value_t satisfies edge @@ -210,5 +210,5 @@ TEST_CASE("Edge range concepts - range requirements documented", "[adjacency_lis auto v = *vertices(g).begin(); auto edge_range = edges(g, v); - STATIC_REQUIRE(vertex_edge_range); + STATIC_REQUIRE(out_edge_range); } diff --git a/tests/adj_list/concepts/test_bidirectional_concepts.cpp b/tests/adj_list/concepts/test_bidirectional_concepts.cpp new file mode 100644 index 0000000..a115797 --- /dev/null +++ b/tests/adj_list/concepts/test_bidirectional_concepts.cpp @@ -0,0 +1,290 @@ +/** + * @file test_bidirectional_concepts.cpp + * @brief Unit tests for in_edge_range, bidirectional_adjacency_list, + * and index_bidirectional_adjacency_list concepts + */ + +#include +#include +#include +#include +#include +#include "../../common/graph_test_types.hpp" +#include + +using namespace graph; +using namespace graph::adj_list; + +// ============================================================================= +// Stub bidirectional graph with ADL in_edges +// ============================================================================= + +namespace test_bidir_concepts { + +struct BidirGraph : std::vector> { + std::vector> in_adj; + + explicit BidirGraph(size_t n) : std::vector>(n), in_adj(n) {} + + void add_edge(size_t from, size_t to) { + (*this)[from].push_back(static_cast(to)); + in_adj[to].push_back(static_cast(from)); + } +}; + +// ADL in_edges for vertex descriptor +template + requires vertex_descriptor_type +auto in_edges(BidirGraph& g, const U& u) -> const std::vector& { + return g.in_adj[u.vertex_id()]; +} + +} // namespace test_bidir_concepts + +// ============================================================================= +// in_edge_range Concept Tests +// ============================================================================= + +TEST_CASE("in_edge_range concept satisfied by wrapped incoming edge range", + "[adjacency_list][concepts][in_edge_range]") { + // The in_edges CPO wraps raw ranges into edge_descriptor_views, + // so in_edge_range_t satisfies both forward_range and edge + using Graph = test_bidir_concepts::BidirGraph; + using InRange = in_edge_range_t; + STATIC_REQUIRE(in_edge_range); +} + +TEST_CASE("in_edge_range concept NOT satisfied by raw non-edge ranges", + "[adjacency_list][concepts][in_edge_range]") { + using Graph = std::vector>; + // Raw vector is a forward_range but int doesn't satisfy edge + STATIC_REQUIRE_FALSE(in_edge_range, Graph>); + // vector similarly fails the edge requirement + STATIC_REQUIRE_FALSE(in_edge_range, Graph>); +} + +// ============================================================================= +// bidirectional_adjacency_list Concept Tests +// ============================================================================= + +TEST_CASE("bidirectional_adjacency_list concept satisfied by bidirectional graph", + "[adjacency_list][concepts][bidirectional_adjacency_list]") { + using Graph = test_bidir_concepts::BidirGraph; + STATIC_REQUIRE(bidirectional_adjacency_list); +} + +TEST_CASE("bidirectional_adjacency_list concept NOT satisfied by outgoing-only graph", + "[adjacency_list][concepts][bidirectional_adjacency_list]") { + // Plain vector> has edges but no in_edges + using Graph = std::vector>; + STATIC_REQUIRE_FALSE(bidirectional_adjacency_list); +} + +TEST_CASE("bidirectional_adjacency_list implies adjacency_list", + "[adjacency_list][concepts][bidirectional_adjacency_list]") { + using Graph = test_bidir_concepts::BidirGraph; + // If bidirectional, then also adjacency_list + STATIC_REQUIRE(adjacency_list); + STATIC_REQUIRE(bidirectional_adjacency_list); +} + +TEST_CASE("bidirectional_adjacency_list runtime validation", + "[adjacency_list][concepts][bidirectional_adjacency_list]") { + test_bidir_concepts::BidirGraph g(4); + g.add_edge(0, 1); + g.add_edge(0, 2); + g.add_edge(1, 2); + g.add_edge(3, 0); + + auto verts = vertices(g); + auto it = verts.begin(); + auto v0 = *it++; + auto v1 = *it++; + auto v2 = *it++; + + // Verify in_edges returns correct incoming edges + auto in0 = in_edges(g, v0); + REQUIRE(std::ranges::distance(in0) == 1); // from vertex 3 + + auto in2 = in_edges(g, v2); + REQUIRE(std::ranges::distance(in2) == 2); // from vertices 0, 1 + + // Verify source_id on incoming edges compiles and returns a vertex ID. + // For BidirGraph (which stores in-edges as plain ints without .in_edges() + // on the vertex), source_id() returns the owning/target vertex ID. + // Graphs that store in-edges with .in_edges() on the vertex (like + // dynamic_graph) get the actual source vertex ID instead. + for (auto ie : in_edges(g, v2)) { + auto sid = source_id(g, ie); + REQUIRE(sid == 2); // The vertex we called in_edges on (owning vertex) + } +} + +// ============================================================================= +// index_bidirectional_adjacency_list Concept Tests +// ============================================================================= + +TEST_CASE("index_bidirectional_adjacency_list concept satisfied by indexed bidirectional graph", + "[adjacency_list][concepts][index_bidirectional_adjacency_list]") { + using Graph = test_bidir_concepts::BidirGraph; + // BidirGraph inherits from vector>, which is index-based + STATIC_REQUIRE(index_vertex_range); + STATIC_REQUIRE(index_bidirectional_adjacency_list); +} + +TEST_CASE("index_bidirectional_adjacency_list NOT satisfied by non-bidirectional graph", + "[adjacency_list][concepts][index_bidirectional_adjacency_list]") { + using Graph = std::vector>; + // Has index_vertex_range but not bidirectional + STATIC_REQUIRE(index_vertex_range); + STATIC_REQUIRE_FALSE(index_bidirectional_adjacency_list); +} + +// ============================================================================= +// Re-export Tests (graph:: namespace) +// ============================================================================= + +TEST_CASE("bidirectional concepts accessible via graph:: namespace", + "[adjacency_list][concepts][re-export]") { + using Graph = test_bidir_concepts::BidirGraph; + // Verify concepts are re-exported to graph:: namespace + STATIC_REQUIRE(graph::bidirectional_adjacency_list); + STATIC_REQUIRE(graph::index_bidirectional_adjacency_list); + STATIC_REQUIRE_FALSE(graph::bidirectional_adjacency_list>>); +} + +TEST_CASE("incoming-edge CPOs accessible via graph:: namespace", + "[adjacency_list][concepts][re-export]") { + test_bidir_concepts::BidirGraph g(3); + g.add_edge(0, 1); + g.add_edge(2, 1); + + auto verts = graph::vertices(g); + auto it = verts.begin(); + auto v0 = *it++; + auto v1 = *it++; + + // in_edges via graph:: namespace + auto in1 = graph::in_edges(g, v1); + REQUIRE(std::ranges::distance(in1) == 2); + + // in_degree via graph:: namespace + REQUIRE(graph::in_degree(g, v1) == 2); + + // contains_in_edge via graph:: namespace + REQUIRE(graph::contains_in_edge(g, v1, v0) == true); +} + +TEST_CASE("incoming-edge type aliases accessible via graph:: namespace", + "[adjacency_list][concepts][re-export]") { + using Graph = test_bidir_concepts::BidirGraph; + + // Verify type aliases are accessible from graph:: namespace + STATIC_REQUIRE(std::is_same_v, + adj_list::in_edge_range_t>); + STATIC_REQUIRE(std::is_same_v, + adj_list::in_edge_t>); + + // Outgoing aliases + STATIC_REQUIRE(std::is_same_v, + adj_list::out_edge_range_t>); + STATIC_REQUIRE(std::is_same_v, + adj_list::out_edge_t>); +} + +TEST_CASE("incoming-edge traits accessible via graph:: namespace", + "[adjacency_list][concepts][re-export]") { + using Graph = test_bidir_concepts::BidirGraph; + + STATIC_REQUIRE(graph::has_in_degree); + STATIC_REQUIRE(graph::has_in_degree_v); + STATIC_REQUIRE(graph::has_find_in_edge); + STATIC_REQUIRE(graph::has_find_in_edge_v); +} + +// ============================================================================= +// dynamic_graph concept checks +// ============================================================================= + +// Non-uniform bidirectional: in_edge_type = dynamic_in_edge, which has +// source_id() — CPO Tier 1 fires, satisfying bidirectional_adjacency_list. +using DynBidirVov = graph::container::dynamic_graph>; +using DynBidirVovInt = graph::container::dynamic_graph>; +using DynBidirVol = graph::container::dynamic_graph>; + +// Non-bidirectional: Bidirectional=false → no in_edges at all. +using DynNonBidir = graph::container::dynamic_graph>; + +TEST_CASE("bidirectional_adjacency_list satisfied by dynamic_graph with non-uniform bidir traits", + "[adjacency_list][concepts][bidirectional_adjacency_list][dynamic_graph]") { + // vov (vector edges) non-uniform bidir + STATIC_REQUIRE(bidirectional_adjacency_list); + STATIC_REQUIRE(bidirectional_adjacency_list); + // vol (list edges) non-uniform bidir + STATIC_REQUIRE(bidirectional_adjacency_list); +} + +TEST_CASE("bidirectional_adjacency_list NOT satisfied by non-bidirectional dynamic_graph", + "[adjacency_list][concepts][bidirectional_adjacency_list][dynamic_graph]") { + STATIC_REQUIRE_FALSE(bidirectional_adjacency_list); +} + +TEST_CASE("dynamic_graph with non-uniform bidir traits also satisfies adjacency_list", + "[adjacency_list][concepts][adjacency_list][dynamic_graph]") { + // bidirectional implies adjacency_list + STATIC_REQUIRE(adjacency_list); + STATIC_REQUIRE(adjacency_list); +} + +TEST_CASE("index_bidirectional_adjacency_list satisfied by non-uniform bidir dynamic_graph", + "[adjacency_list][concepts][index_bidirectional_adjacency_list][dynamic_graph]") { + STATIC_REQUIRE(index_bidirectional_adjacency_list); + STATIC_REQUIRE(index_bidirectional_adjacency_list); + STATIC_REQUIRE(index_bidirectional_adjacency_list); +} + +TEST_CASE("index_bidirectional_adjacency_list NOT satisfied by non-bidirectional dynamic_graph", + "[adjacency_list][concepts][index_bidirectional_adjacency_list][dynamic_graph]") { + STATIC_REQUIRE_FALSE(index_bidirectional_adjacency_list); +} + +TEST_CASE("dynamic_graph non-uniform bidir concept checks via graph:: namespace", + "[adjacency_list][concepts][dynamic_graph][re-export]") { + STATIC_REQUIRE(graph::bidirectional_adjacency_list); + STATIC_REQUIRE(graph::bidirectional_adjacency_list); + STATIC_REQUIRE_FALSE(graph::bidirectional_adjacency_list); + STATIC_REQUIRE(graph::index_bidirectional_adjacency_list); + STATIC_REQUIRE_FALSE(graph::index_bidirectional_adjacency_list); +} + +TEST_CASE("dynamic_graph non-uniform bidir runtime validation", + "[adjacency_list][concepts][bidirectional_adjacency_list][dynamic_graph][runtime]") { + // 0->1, 0->2, 1->2, 2->3 + DynBidirVov g({{0, 1}, {0, 2}, {1, 2}, {2, 3}}); + + // vertex 0: 0 in-edges; vertex 2: 2 in-edges (from 0 and 1) + REQUIRE(in_degree(g, uint32_t(0)) == 0); + REQUIRE(in_degree(g, uint32_t(1)) == 1); + REQUIRE(in_degree(g, uint32_t(2)) == 2); + REQUIRE(in_degree(g, uint32_t(3)) == 1); + + // source_id on in-edges returns the actual source vertex, not the target + auto u2 = *find_vertex(g, uint32_t(2)); + for (auto ie : in_edges(g, u2)) { + auto sid = source_id(g, ie); + REQUIRE((sid == 0 || sid == 1)); + } +} + +TEST_CASE("in_edge_t for dynamic_graph bidir differs from edge_t (out-edge)", + "[adjacency_list][concepts][dynamic_graph][type]") { + // in_edge_t is an edge_descriptor over dynamic_in_edge iterators; + // edge_t is an edge_descriptor over dynamic_out_edge iterators. + // They are distinct types because the underlying iterators differ. + STATIC_REQUIRE_FALSE(std::is_same_v, edge_t>); + STATIC_REQUIRE_FALSE(std::is_same_v, edge_t>); +} diff --git a/tests/adj_list/cpo/test_bidir_cpo_consistency.cpp b/tests/adj_list/cpo/test_bidir_cpo_consistency.cpp new file mode 100644 index 0000000..175f34d --- /dev/null +++ b/tests/adj_list/cpo/test_bidir_cpo_consistency.cpp @@ -0,0 +1,254 @@ +/** + * @file test_bidir_cpo_consistency.cpp + * @brief Consistency tests for bidirectional dynamic_graph CPOs + * + * Verifies that CPO results for non-uniform bidirectional dynamic_graph are + * internally consistent: + * - In-edge (source,target) pairs from in_edges() mirror out-edge pairs from edges() + * - in_degree(g,u) matches the counted number of in_edges(g,u) + * - source_id(g,ie) on in-edges is stable across multiple iterations + * - Results are identical between vov_bidir_graph_traits (vector) and + * vol_bidir_graph_traits (list) containers + * + * Terminology: + * - "out-edge pair": {source_vid, target_vid} from edges(g, u) + * - "in-edge pair": {source_vid, target_vid} from in_edges(g, u) + * They should be equal sets for any bidirectional graph. + */ + +#include +#include +#include +#include +#include +#include +#include "../../common/graph_test_types.hpp" +#include +#include +#include + +using namespace graph; +using namespace graph::adj_list; + +using VovBiDirVoid = + graph::container::dynamic_graph>; +using VovBiDirInt = + graph::container::dynamic_graph>; +using VolBiDirVoid = + graph::container::dynamic_graph>; + +// ============================================================================= +// Helpers: collect (source, target) pairs via CPOs +// ============================================================================= + +template +std::set> +collect_out_edge_pairs(const G& g) { + std::set> result; + for (auto u : vertices(g)) { + auto uid = static_cast(vertex_id(g, u)); + for (auto oe : edges(g, u)) { + auto tid = static_cast(target_id(g, oe)); + result.insert({uid, tid}); + } + } + return result; +} + +template +std::set> +collect_in_edge_pairs(const G& g) { + std::set> result; + for (auto u : vertices(g)) { + auto uid = static_cast(vertex_id(g, u)); + for (auto ie : in_edges(g, u)) { + auto sid = static_cast(source_id(g, ie)); + result.insert({sid, uid}); + } + } + return result; +} + +// ============================================================================= +// Symmetry: in-edge pairs == out-edge pairs +// ============================================================================= + +TEST_CASE("bidir CPO consistency: in-edges mirror out-edges (triangle)", + "[bidir][cpo][consistency]") { + // 0->1, 0->2, 1->2 + VovBiDirVoid g({{0, 1}, {0, 2}, {1, 2}}); + + auto out_pairs = collect_out_edge_pairs(g); + auto in_pairs = collect_in_edge_pairs(g); + + REQUIRE(out_pairs == in_pairs); +} + +TEST_CASE("bidir CPO consistency: in-edges mirror out-edges (star)", + "[bidir][cpo][consistency][star]") { + // Star: 0->1, 0->2, 0->3, 0->4 + VovBiDirVoid g({{0, 1}, {0, 2}, {0, 3}, {0, 4}}); + + auto out_pairs = collect_out_edge_pairs(g); + auto in_pairs = collect_in_edge_pairs(g); + + REQUIRE(out_pairs == in_pairs); +} + +TEST_CASE("bidir CPO consistency: in-edges mirror out-edges (path)", + "[bidir][cpo][consistency][path]") { + // Path: 0->1->2->3 + VovBiDirVoid g({{0, 1}, {1, 2}, {2, 3}}); + + auto out_pairs = collect_out_edge_pairs(g); + auto in_pairs = collect_in_edge_pairs(g); + + REQUIRE(out_pairs == in_pairs); +} + +// ============================================================================= +// in_degree matches manual count of incoming out-edges +// ============================================================================= + +TEST_CASE("bidir CPO consistency: in_degree matches incoming out-edge count", + "[bidir][cpo][consistency][degree]") { + // Has vertices with different in-degrees: 0->1, 0->2, 1->2, 2->3, 3->2 + VovBiDirVoid g({{0, 1}, {0, 2}, {1, 2}, {2, 3}, {3, 2}}); + + for (auto u : vertices(g)) { + auto uid = vertex_id(g, u); + size_t indeg = in_degree(g, u); + + // Count by walking all out-edges and checking target + size_t manual = 0; + for (auto v : vertices(g)) + for (auto oe : edges(g, v)) + if (target_id(g, oe) == uid) + ++manual; + + REQUIRE(indeg == manual); + } +} + +TEST_CASE("bidir CPO consistency: in_degree via uid overload", + "[bidir][cpo][consistency][degree][uid]") { + // 0->1, 0->2, 1->2 + VovBiDirVoid g({{0, 1}, {0, 2}, {1, 2}}); + + REQUIRE(in_degree(g, uint32_t(0)) == 0); // nothing points to 0 + REQUIRE(in_degree(g, uint32_t(1)) == 1); // only 0->1 + REQUIRE(in_degree(g, uint32_t(2)) == 2); // 0->2 and 1->2 +} + +// ============================================================================= +// source_id stability across multiple iterations +// ============================================================================= + +TEST_CASE("bidir CPO consistency: source_id on in-edges is stable across iterations", + "[bidir][cpo][consistency][stable]") { + VovBiDirVoid g({{0, 1}, {0, 2}, {1, 2}}); + + auto u2 = *find_vertex(g, uint32_t(2)); // vertex with 2 in-edges + + // Two iterations should yield the same source IDs in the same order + std::vector first_pass, second_pass; + for (auto ie : in_edges(g, u2)) + first_pass.push_back(source_id(g, ie)); + for (auto ie : in_edges(g, u2)) + second_pass.push_back(source_id(g, ie)); + + REQUIRE(first_pass == second_pass); +} + +// ============================================================================= +// Weighted graph: source_id still works +// ============================================================================= + +TEST_CASE("bidir CPO consistency: weighted graph in-edge pairs mirror out-edge pairs", + "[bidir][cpo][consistency][weighted]") { + // 0->1 (w:10), 0->2 (w:20), 1->2 (w:30) + VovBiDirInt g({{0, 1, 10}, {0, 2, 20}, {1, 2, 30}}); + + auto out_pairs = collect_out_edge_pairs(g); + auto in_pairs = collect_in_edge_pairs(g); + + REQUIRE(out_pairs == in_pairs); +} + +// ============================================================================= +// Container type independence: vov and vol produce identical CPO results +// ============================================================================= + +TEST_CASE("bidir CPO consistency: vov and vol in-edge pairs are identical", + "[bidir][cpo][consistency][vol]") { + VovBiDirVoid g_vov({{0, 1}, {0, 2}, {1, 2}, {2, 3}}); + VolBiDirVoid g_vol({{0, 1}, {0, 2}, {1, 2}, {2, 3}}); + + SECTION("in-edge pairs identical") { + REQUIRE(collect_in_edge_pairs(g_vov) == collect_in_edge_pairs(g_vol)); + } + + SECTION("out-edge pairs identical") { + REQUIRE(collect_out_edge_pairs(g_vov) == collect_out_edge_pairs(g_vol)); + } + + SECTION("in_degree values identical for all vertices") { + REQUIRE(num_vertices(g_vov) == num_vertices(g_vol)); + for (uint32_t i = 0; i < 4; ++i) + REQUIRE(in_degree(g_vov, i) == in_degree(g_vol, i)); + } +} + +// ============================================================================= +// Empty graph +// ============================================================================= + +TEST_CASE("bidir CPO consistency: empty graph has no in-edges", + "[bidir][cpo][consistency][empty]") { + VovBiDirVoid g; + g.resize_vertices(4); + + SECTION("all in_degrees are zero") { + for (auto u : vertices(g)) + REQUIRE(in_degree(g, u) == 0); + } + + SECTION("all in_edges ranges are empty") { + for (auto u : vertices(g)) + REQUIRE(std::ranges::empty(in_edges(g, u))); + } + + SECTION("no in-edge or out-edge pairs") { + REQUIRE(collect_out_edge_pairs(g).empty()); + REQUIRE(collect_in_edge_pairs(g).empty()); + } +} + +// ============================================================================= +// Single-vertex self-loop +// ============================================================================= + +TEST_CASE("bidir CPO consistency: self-loop edge", + "[bidir][cpo][consistency][selfloop]") { + // 0->0 (self-loop), 0->1 + VovBiDirVoid g({{0, 0}, {0, 1}}); + + SECTION("in-edge and out-edge pairs mirror each other") { + auto out_pairs = collect_out_edge_pairs(g); + auto in_pairs = collect_in_edge_pairs(g); + REQUIRE(out_pairs == in_pairs); + } + + SECTION("vertex 0 has self-loop in in-edges") { + auto u0 = *find_vertex(g, uint32_t(0)); + bool found_self = false; + for (auto ie : in_edges(g, u0)) { + if (source_id(g, ie) == 0) + found_self = true; + } + REQUIRE(found_self); + } +} diff --git a/tests/adj_list/cpo/test_contains_in_edge_cpo.cpp b/tests/adj_list/cpo/test_contains_in_edge_cpo.cpp new file mode 100644 index 0000000..da8f4a2 --- /dev/null +++ b/tests/adj_list/cpo/test_contains_in_edge_cpo.cpp @@ -0,0 +1,205 @@ +/** + * @file test_contains_in_edge_cpo.cpp + * @brief Comprehensive tests for contains_in_edge(g,u,v) and contains_in_edge(g,uid,vid) CPOs + */ + +#include +#include +#include +#include +#include +#include +#include "../../common/graph_test_types.hpp" +#include + +using namespace graph; +using namespace graph::adj_list; + +// ============================================================================= +// Stub graph with ADL in_edges — same pattern as test_in_edges_cpo.cpp +// ============================================================================= + +namespace test_contains_in_edge { + +struct ADLGraph : std::vector> { + std::vector> in_adj; + + explicit ADLGraph(size_t n) : std::vector>(n), in_adj(n) {} + + void add_edge(size_t from, size_t to) { + (*this)[from].push_back(static_cast(to)); + in_adj[to].push_back(static_cast(from)); + } +}; + +// ADL in_edges for vertex descriptor +template + requires vertex_descriptor_type +auto in_edges(ADLGraph& g, const U& u) -> const std::vector& { + return g.in_adj[u.vertex_id()]; +} + +} // namespace test_contains_in_edge + +// ============================================================================= +// Tests: contains_in_edge(g, u, v) — both vertex descriptors +// ============================================================================= + +TEST_CASE("contains_in_edge(g, u, v) detects existing incoming edges", "[contains_in_edge][cpo][uv]") { + // Graph: 0->1, 0->2, 1->2, 3->0 + test_contains_in_edge::ADLGraph graph(4); + graph.add_edge(0, 1); + graph.add_edge(0, 2); + graph.add_edge(1, 2); + graph.add_edge(3, 0); + + auto verts = vertices(graph); + auto it = verts.begin(); + auto v0 = *it++; + auto v1 = *it++; + auto v2 = *it++; + auto v3 = *it; + + SECTION("Existing incoming edges return true") { + // v0 has incoming from v3 + REQUIRE(contains_in_edge(graph, v0, v3) == true); + // v1 has incoming from v0 + REQUIRE(contains_in_edge(graph, v1, v0) == true); + // v2 has incoming from v0 and v1 + REQUIRE(contains_in_edge(graph, v2, v0) == true); + REQUIRE(contains_in_edge(graph, v2, v1) == true); + } + + SECTION("Non-existing incoming edges return false") { + // v0 has no incoming from v0, v1, v2 + REQUIRE(contains_in_edge(graph, v0, v0) == false); + REQUIRE(contains_in_edge(graph, v0, v1) == false); + REQUIRE(contains_in_edge(graph, v0, v2) == false); + // v1 has no incoming from v1, v2, v3 + REQUIRE(contains_in_edge(graph, v1, v1) == false); + REQUIRE(contains_in_edge(graph, v1, v2) == false); + REQUIRE(contains_in_edge(graph, v1, v3) == false); + // v3 has no incoming edges at all + REQUIRE(contains_in_edge(graph, v3, v0) == false); + REQUIRE(contains_in_edge(graph, v3, v1) == false); + REQUIRE(contains_in_edge(graph, v3, v2) == false); + REQUIRE(contains_in_edge(graph, v3, v3) == false); + } +} + +TEST_CASE("contains_in_edge(g, u, v) handles vertex with no incoming edges", "[contains_in_edge][cpo][empty][uv]") { + // Graph: 0->1. Vertex 0 has no incoming edges. + test_contains_in_edge::ADLGraph graph(2); + graph.add_edge(0, 1); + + auto verts = vertices(graph); + auto it = verts.begin(); + auto v0 = *it++; + auto v1 = *it; + + REQUIRE(contains_in_edge(graph, v0, v0) == false); + REQUIRE(contains_in_edge(graph, v0, v1) == false); + // v1 has incoming from v0 + REQUIRE(contains_in_edge(graph, v1, v0) == true); +} + +// ============================================================================= +// Tests: contains_in_edge(g, uid, vid) — both IDs +// ============================================================================= + +TEST_CASE("contains_in_edge(g, uid, vid) detects existing incoming edges by ID", + "[contains_in_edge][cpo][uidvid]") { + // Graph: 0->1, 0->2, 1->2, 3->0 + test_contains_in_edge::ADLGraph graph(4); + graph.add_edge(0, 1); + graph.add_edge(0, 2); + graph.add_edge(1, 2); + graph.add_edge(3, 0); + + SECTION("Existing incoming edges return true") { + REQUIRE(contains_in_edge(graph, size_t(0), size_t(3)) == true); + REQUIRE(contains_in_edge(graph, size_t(1), size_t(0)) == true); + REQUIRE(contains_in_edge(graph, size_t(2), size_t(0)) == true); + REQUIRE(contains_in_edge(graph, size_t(2), size_t(1)) == true); + } + + SECTION("Non-existing incoming edges return false") { + REQUIRE(contains_in_edge(graph, size_t(0), size_t(0)) == false); + REQUIRE(contains_in_edge(graph, size_t(0), size_t(1)) == false); + REQUIRE(contains_in_edge(graph, size_t(0), size_t(2)) == false); + REQUIRE(contains_in_edge(graph, size_t(3), size_t(0)) == false); + REQUIRE(contains_in_edge(graph, size_t(3), size_t(1)) == false); + REQUIRE(contains_in_edge(graph, size_t(3), size_t(2)) == false); + REQUIRE(contains_in_edge(graph, size_t(3), size_t(3)) == false); + } +} + +TEST_CASE("contains_in_edge(g, uid, vid) handles empty incoming edges by ID", + "[contains_in_edge][cpo][empty][uidvid]") { + test_contains_in_edge::ADLGraph graph(3); + // No edges added - all should be false + REQUIRE(contains_in_edge(graph, size_t(0), size_t(1)) == false); + REQUIRE(contains_in_edge(graph, size_t(1), size_t(0)) == false); + REQUIRE(contains_in_edge(graph, size_t(2), size_t(0)) == false); +} + +// ============================================================================= +// dynamic_graph with non-uniform bidirectional traits +// ============================================================================= + +using DynBiDirContainsGraph = + graph::container::dynamic_graph>; + +TEST_CASE("contains_in_edge(g, u, v) - dynamic_graph non-uniform bidir", + "[contains_in_edge][cpo][dynamic_graph]") { + using namespace graph; + using namespace graph::container; + + // Graph: 0->1, 0->2, 1->2, 3->0 + DynBiDirContainsGraph g({{0, 1}, {0, 2}, {1, 2}, {3, 0}}); + + auto verts = vertices(g); + auto it = verts.begin(); + auto v0 = *it++; + auto v1 = *it++; + auto v2 = *it++; + auto v3 = *it; + + SECTION("existing in-edges return true") { + REQUIRE(contains_in_edge(g, v0, v3) == true); // 3->0 + REQUIRE(contains_in_edge(g, v1, v0) == true); // 0->1 + REQUIRE(contains_in_edge(g, v2, v0) == true); // 0->2 + REQUIRE(contains_in_edge(g, v2, v1) == true); // 1->2 + } + + SECTION("non-existing in-edges return false") { + REQUIRE(contains_in_edge(g, v0, v0) == false); + REQUIRE(contains_in_edge(g, v0, v1) == false); + REQUIRE(contains_in_edge(g, v3, v0) == false); // v3 has no incoming edges + REQUIRE(contains_in_edge(g, v3, v2) == false); + } +} + +TEST_CASE("contains_in_edge(g, uid, vid) - dynamic_graph non-uniform bidir", + "[contains_in_edge][cpo][dynamic_graph][uidvid]") { + using namespace graph; + using namespace graph::container; + + // Graph: 0->1, 0->2, 1->2, 3->0 + DynBiDirContainsGraph g({{0, 1}, {0, 2}, {1, 2}, {3, 0}}); + + SECTION("existing in-edges return true by ID") { + REQUIRE(contains_in_edge(g, uint32_t(0), uint32_t(3)) == true); + REQUIRE(contains_in_edge(g, uint32_t(1), uint32_t(0)) == true); + REQUIRE(contains_in_edge(g, uint32_t(2), uint32_t(0)) == true); + REQUIRE(contains_in_edge(g, uint32_t(2), uint32_t(1)) == true); + } + + SECTION("non-existing in-edges return false by ID") { + REQUIRE(contains_in_edge(g, uint32_t(0), uint32_t(0)) == false); + REQUIRE(contains_in_edge(g, uint32_t(0), uint32_t(1)) == false); + REQUIRE(contains_in_edge(g, uint32_t(3), uint32_t(0)) == false); + REQUIRE(contains_in_edge(g, uint32_t(3), uint32_t(2)) == false); + } +} diff --git a/tests/adj_list/cpo/test_find_in_edge_cpo.cpp b/tests/adj_list/cpo/test_find_in_edge_cpo.cpp new file mode 100644 index 0000000..9fd1306 --- /dev/null +++ b/tests/adj_list/cpo/test_find_in_edge_cpo.cpp @@ -0,0 +1,240 @@ +/** + * @file test_find_in_edge_cpo.cpp + * @brief Comprehensive tests for find_in_edge(g,u,v), find_in_edge(g,u,vid), and find_in_edge(g,uid,vid) CPOs + */ + +#include +#include +#include +#include +#include +#include +#include "../../common/graph_test_types.hpp" +#include + +using namespace graph; +using namespace graph::adj_list; + +// ============================================================================= +// Stub graph with ADL in_edges (reusable) — same pattern as test_in_edges_cpo.cpp +// ============================================================================= + +namespace test_find_in_edge { + +struct ADLGraph : std::vector> { + std::vector> in_adj; + + explicit ADLGraph(size_t n) : std::vector>(n), in_adj(n) {} + + void add_edge(size_t from, size_t to) { + (*this)[from].push_back(static_cast(to)); + in_adj[to].push_back(static_cast(from)); + } +}; + +// ADL in_edges for vertex descriptor +template + requires vertex_descriptor_type +auto in_edges(ADLGraph& g, const U& u) -> const std::vector& { + return g.in_adj[u.vertex_id()]; +} + +} // namespace test_find_in_edge + +// ============================================================================= +// Tests: find_in_edge(g, u, v) — both vertex descriptors +// ============================================================================= + +TEST_CASE("find_in_edge(g, u, v) finds incoming edge by descriptor", "[find_in_edge][cpo][uu]") { + // Build: 0->2, 1->2, 3->2. Vertex 2 has in-edges from {0, 1, 3}. + test_find_in_edge::ADLGraph graph(4); + graph.add_edge(0, 2); + graph.add_edge(1, 2); + graph.add_edge(3, 2); + + auto verts = vertices(graph); + auto it = verts.begin(); + auto v0 = *it++; + auto v1 = *it++; + auto v2 = *it++; + auto v3 = *it; + + SECTION("Find existing incoming edge from v1 to v2") { + auto e = find_in_edge(graph, v2, v1); + // Returns outgoing edge from v1 to v2; source_id == v1's vertex id + REQUIRE(source_id(graph, e) == 1); + } + + SECTION("Find existing incoming edge from v0 to v2") { + auto e = find_in_edge(graph, v2, v0); + REQUIRE(source_id(graph, e) == 0); + } + + SECTION("Find existing incoming edge from v3 to v2") { + auto e = find_in_edge(graph, v2, v3); + REQUIRE(source_id(graph, e) == 3); + } +} + +// ============================================================================= +// Tests: find_in_edge(g, u, vid) — descriptor + source ID +// ============================================================================= + +TEST_CASE("find_in_edge(g, u, vid) finds incoming edge by source ID", "[find_in_edge][cpo][uid]") { + test_find_in_edge::ADLGraph graph(4); + graph.add_edge(0, 2); + graph.add_edge(1, 2); + graph.add_edge(3, 2); + + auto verts = vertices(graph); + auto it = verts.begin(); + std::advance(it, 2); + auto v2 = *it; + + SECTION("Find incoming edge from source ID 1") { + auto e = find_in_edge(graph, v2, 1); + REQUIRE(source_id(graph, e) == 1); + } + + SECTION("Find incoming edge from source ID 0") { + auto e = find_in_edge(graph, v2, 0); + REQUIRE(source_id(graph, e) == 0); + } + + SECTION("Find incoming edge from source ID 3") { + auto e = find_in_edge(graph, v2, 3); + REQUIRE(source_id(graph, e) == 3); + } +} + +// ============================================================================= +// Tests: find_in_edge(g, uid, vid) — both IDs +// ============================================================================= + +TEST_CASE("find_in_edge(g, uid, vid) finds incoming edge by both IDs", "[find_in_edge][cpo][uidvid]") { + test_find_in_edge::ADLGraph graph(4); + graph.add_edge(0, 2); + graph.add_edge(1, 2); + graph.add_edge(3, 2); + + SECTION("Find incoming edge to vertex 2 from vertex 1") { + auto e = find_in_edge(graph, size_t(2), size_t(1)); + REQUIRE(source_id(graph, e) == 1); + } + + SECTION("Find incoming edge to vertex 2 from vertex 0") { + auto e = find_in_edge(graph, size_t(2), size_t(0)); + REQUIRE(source_id(graph, e) == 0); + } + + SECTION("Find incoming edge to vertex 2 from vertex 3") { + auto e = find_in_edge(graph, size_t(2), size_t(3)); + REQUIRE(source_id(graph, e) == 3); + } +} + +// ============================================================================= +// Tests: Vertex with no incoming edges +// ============================================================================= + +TEST_CASE("find_in_edge on vertex with single incoming edge", "[find_in_edge][cpo][single]") { + test_find_in_edge::ADLGraph graph(3); + graph.add_edge(0, 1); // Only edge: 0 -> 1 + + auto verts = vertices(graph); + auto it = verts.begin(); + auto v0 = *it++; + auto v1 = *it; + + auto e = find_in_edge(graph, v1, v0); + REQUIRE(source_id(graph, e) == 0); +} + +// ============================================================================= +// dynamic_graph with non-uniform bidirectional traits +// ============================================================================= + +using DynBiDirFindGraph = + graph::container::dynamic_graph>; + +TEST_CASE("find_in_edge(g, u, v) - dynamic_graph non-uniform bidir", + "[find_in_edge][cpo][dynamic_graph]") { + using namespace graph; + using namespace graph::container; + + // Graph: 0->2, 1->2, 3->2 + DynBiDirFindGraph g({{0, 2}, {1, 2}, {3, 2}}); + + auto verts = vertices(g); + auto it = verts.begin(); + auto v0 = *it++; + auto v1 = *it++; + auto v2 = *it++; + auto v3 = *it; + + SECTION("find in-edge from v0 to v2 — source_id matches") { + auto ie = find_in_edge(g, v2, v0); + REQUIRE(source_id(g, ie) == 0); + REQUIRE(target_id(g, ie) == 2); + } + + SECTION("find in-edge from v1 to v2") { + auto ie = find_in_edge(g, v2, v1); + REQUIRE(source_id(g, ie) == 1); + REQUIRE(target_id(g, ie) == 2); + } + + SECTION("find in-edge from v3 to v2") { + auto ie = find_in_edge(g, v2, v3); + REQUIRE(source_id(g, ie) == 3); + REQUIRE(target_id(g, ie) == 2); + } +} + +TEST_CASE("find_in_edge(g, u, vid) - dynamic_graph non-uniform bidir", + "[find_in_edge][cpo][dynamic_graph][uid]") { + using namespace graph; + using namespace graph::container; + + DynBiDirFindGraph g({{0, 2}, {1, 2}, {3, 2}}); + auto v2 = *find_vertex(g, uint32_t(2)); + + SECTION("find in-edge from source ID 0") { + auto ie = find_in_edge(g, v2, uint32_t(0)); + REQUIRE(source_id(g, ie) == 0); + } + + SECTION("find in-edge from source ID 1") { + auto ie = find_in_edge(g, v2, uint32_t(1)); + REQUIRE(source_id(g, ie) == 1); + } + + SECTION("find in-edge from source ID 3") { + auto ie = find_in_edge(g, v2, uint32_t(3)); + REQUIRE(source_id(g, ie) == 3); + } +} + +TEST_CASE("find_in_edge(g, uid, vid) - dynamic_graph non-uniform bidir", + "[find_in_edge][cpo][dynamic_graph][uidvid]") { + using namespace graph; + using namespace graph::container; + + DynBiDirFindGraph g({{0, 2}, {1, 2}, {3, 2}}); + + SECTION("find in-edge to vertex 2 from vertex 1") { + auto ie = find_in_edge(g, uint32_t(2), uint32_t(1)); + REQUIRE(source_id(g, ie) == 1); + } + + SECTION("find in-edge to vertex 2 from vertex 3") { + auto ie = find_in_edge(g, uint32_t(2), uint32_t(3)); + REQUIRE(source_id(g, ie) == 3); + } + + SECTION("find in-edge to vertex 2 from vertex 0") { + auto ie = find_in_edge(g, uint32_t(2), uint32_t(0)); + REQUIRE(source_id(g, ie) == 0); + } +} diff --git a/tests/adj_list/cpo/test_has_edge_cpo.cpp b/tests/adj_list/cpo/test_has_edges_cpo.cpp similarity index 63% rename from tests/adj_list/cpo/test_has_edge_cpo.cpp rename to tests/adj_list/cpo/test_has_edges_cpo.cpp index ac905d8..a1a7839 100644 --- a/tests/adj_list/cpo/test_has_edge_cpo.cpp +++ b/tests/adj_list/cpo/test_has_edges_cpo.cpp @@ -1,6 +1,6 @@ /** - * @file test_has_edge_cpo.cpp - * @brief Comprehensive tests for has_edge(g) CPO + * @file test_has_edges_cpo.cpp + * @brief Comprehensive tests for has_edges(g) CPO * * Tests all resolution paths (member, ADL, default) and various scenarios */ @@ -22,49 +22,49 @@ using namespace graph::adj_list; // Simple vector-based graph (adjacency list with vector of vectors) using SimpleGraph = std::vector>>; -TEST_CASE("has_edge - Empty graph", "[has_edge][default]") { +TEST_CASE("has_edges - Empty graph", "[has_edges][default]") { SimpleGraph g; - REQUIRE_FALSE(has_edge(g)); + REQUIRE_FALSE(has_edges(g)); } -TEST_CASE("has_edge - Single vertex with no edges", "[has_edge][default]") { +TEST_CASE("has_edges - Single vertex with no edges", "[has_edges][default]") { SimpleGraph g(1); // One vertex, no edges - REQUIRE_FALSE(has_edge(g)); + REQUIRE_FALSE(has_edges(g)); } -TEST_CASE("has_edge - Multiple vertices with no edges", "[has_edge][default]") { +TEST_CASE("has_edges - Multiple vertices with no edges", "[has_edges][default]") { SimpleGraph g(5); // Five vertices, no edges - REQUIRE_FALSE(has_edge(g)); + REQUIRE_FALSE(has_edges(g)); } -TEST_CASE("has_edge - Single edge", "[has_edge][default]") { +TEST_CASE("has_edges - Single edge", "[has_edges][default]") { SimpleGraph g(2); g[0].push_back({1, 10}); // Edge from 0 to 1 - REQUIRE(has_edge(g)); + REQUIRE(has_edges(g)); } -TEST_CASE("has_edge - Multiple edges from first vertex", "[has_edge][default]") { +TEST_CASE("has_edges - Multiple edges from first vertex", "[has_edges][default]") { SimpleGraph g(4); g[0].push_back({1, 10}); g[0].push_back({2, 20}); g[0].push_back({3, 30}); - REQUIRE(has_edge(g)); + REQUIRE(has_edges(g)); } -TEST_CASE("has_edge - Edge from middle vertex", "[has_edge][default]") { +TEST_CASE("has_edges - Edge from middle vertex", "[has_edges][default]") { SimpleGraph g(5); // First two vertices have no edges g[2].push_back({3, 10}); // First edge is on vertex 2 - REQUIRE(has_edge(g)); + REQUIRE(has_edges(g)); } -TEST_CASE("has_edge - Edge from last vertex only", "[has_edge][default]") { +TEST_CASE("has_edges - Edge from last vertex only", "[has_edges][default]") { SimpleGraph g(5); g[4].push_back({3, 10}); // Only vertex 4 has an edge - REQUIRE(has_edge(g)); + REQUIRE(has_edges(g)); } -TEST_CASE("has_edge - Complete graph", "[has_edge][default]") { +TEST_CASE("has_edges - Complete graph", "[has_edges][default]") { SimpleGraph g(3); // Complete graph: all vertices connected to all others g[0].push_back({1, 10}); @@ -73,31 +73,31 @@ TEST_CASE("has_edge - Complete graph", "[has_edge][default]") { g[1].push_back({2, 30}); g[2].push_back({0, 20}); g[2].push_back({1, 30}); - REQUIRE(has_edge(g)); + REQUIRE(has_edges(g)); } -TEST_CASE("has_edge - Self-loop only", "[has_edge][default]") { +TEST_CASE("has_edges - Self-loop only", "[has_edges][default]") { SimpleGraph g(3); g[1].push_back({1, 10}); // Self-loop on vertex 1 - REQUIRE(has_edge(g)); + REQUIRE(has_edges(g)); } -TEST_CASE("has_edge - Linear chain", "[has_edge][default]") { +TEST_CASE("has_edges - Linear chain", "[has_edges][default]") { SimpleGraph g(4); g[0].push_back({1, 10}); g[1].push_back({2, 20}); g[2].push_back({3, 30}); - REQUIRE(has_edge(g)); + REQUIRE(has_edges(g)); } -TEST_CASE("has_edge - Star graph", "[has_edge][default]") { +TEST_CASE("has_edges - Star graph", "[has_edges][default]") { SimpleGraph g(5); // Star: center vertex connected to all others g[0].push_back({1, 10}); g[0].push_back({2, 20}); g[0].push_back({3, 30}); g[0].push_back({4, 40}); - REQUIRE(has_edge(g)); + REQUIRE(has_edges(g)); } // ============================================================================= @@ -106,29 +106,29 @@ TEST_CASE("has_edge - Star graph", "[has_edge][default]") { using MapGraph = std::map>>; -TEST_CASE("has_edge - Map graph with no edges", "[has_edge][default][map]") { +TEST_CASE("has_edges - Map graph with no edges", "[has_edges][default][map]") { MapGraph g; g[0] = {}; g[1] = {}; g[2] = {}; - REQUIRE_FALSE(has_edge(g)); + REQUIRE_FALSE(has_edges(g)); } -TEST_CASE("has_edge - Map graph with edge", "[has_edge][default][map]") { +TEST_CASE("has_edges - Map graph with edge", "[has_edges][default][map]") { MapGraph g; g[0] = {}; g[1] = {{2, 10}}; g[2] = {}; - REQUIRE(has_edge(g)); + REQUIRE(has_edges(g)); } -TEST_CASE("has_edge - Map graph sparse vertices", "[has_edge][default][map]") { +TEST_CASE("has_edges - Map graph sparse vertices", "[has_edges][default][map]") { MapGraph g; g[0] = {}; g[10] = {}; g[20] = {{30, 10}}; g[30] = {}; - REQUIRE(has_edge(g)); + REQUIRE(has_edges(g)); } // ============================================================================= @@ -137,10 +137,10 @@ TEST_CASE("has_edge - Map graph sparse vertices", "[has_edge][default][map]") { struct GraphWithMember { std::vector>> data; - bool has_edges_flag; + bool has_edge_flag; // Custom member function that takes precedence - bool has_edge() const { return has_edges_flag; } + bool has_edge() const { return has_edge_flag; } // Required for vertices(g) to work auto begin() { return data.begin(); } @@ -149,20 +149,20 @@ struct GraphWithMember { auto end() const { return data.end(); } }; -TEST_CASE("has_edge - Custom member function returns true", "[has_edge][member]") { +TEST_CASE("has_edges - Custom member function returns true", "[has_edges][member]") { GraphWithMember g; g.data.resize(3); - g.has_edges_flag = true; // Custom implementation returns true + g.has_edge_flag = true; // Custom implementation returns true // Note: actual graph has no edges, but member takes precedence - REQUIRE(has_edge(g)); + REQUIRE(has_edges(g)); } -TEST_CASE("has_edge - Custom member function returns false", "[has_edge][member]") { +TEST_CASE("has_edges - Custom member function returns false", "[has_edges][member]") { GraphWithMember g; g.data.resize(3); g.data[0].push_back({1, 10}); // Graph has edge - g.has_edges_flag = false; // But custom implementation returns false - REQUIRE_FALSE(has_edge(g)); + g.has_edge_flag = false; // But custom implementation returns false + REQUIRE_FALSE(has_edges(g)); } // ============================================================================= @@ -184,94 +184,94 @@ struct Graph { bool has_edge(const Graph& g) { return g.adl_result; } } // namespace custom -TEST_CASE("has_edge - ADL function returns true", "[has_edge][adl]") { +TEST_CASE("has_edges - ADL function returns true", "[has_edges][adl]") { custom::Graph g; g.data.resize(3); g.adl_result = true; - REQUIRE(has_edge(g)); + REQUIRE(has_edges(g)); } -TEST_CASE("has_edge - ADL function returns false", "[has_edge][adl]") { +TEST_CASE("has_edges - ADL function returns false", "[has_edges][adl]") { custom::Graph g; g.data.resize(3); g.data[0].push_back({1, 10}); // Graph has edge g.adl_result = false; // But ADL returns false - REQUIRE_FALSE(has_edge(g)); + REQUIRE_FALSE(has_edges(g)); } // ============================================================================= // Test Const Correctness // ============================================================================= -TEST_CASE("has_edge - Const graph with edges", "[has_edge][const]") { +TEST_CASE("has_edges - Const graph with edges", "[has_edges][const]") { SimpleGraph g_mutable(3); g_mutable[0].push_back({1, 10}); const SimpleGraph& g = g_mutable; - REQUIRE(has_edge(g)); + REQUIRE(has_edges(g)); } -TEST_CASE("has_edge - Const graph without edges", "[has_edge][const]") { +TEST_CASE("has_edges - Const graph without edges", "[has_edges][const]") { SimpleGraph g_mutable(3); const SimpleGraph& g = g_mutable; - REQUIRE_FALSE(has_edge(g)); + REQUIRE_FALSE(has_edges(g)); } // ============================================================================= // Test Edge Cases // ============================================================================= -TEST_CASE("has_edge - Large graph with many vertices, first has edge", "[has_edge][large]") { +TEST_CASE("has_edges - Large graph with many vertices, first has edge", "[has_edges][large]") { SimpleGraph g(1000); g[0].push_back({1, 10}); // First vertex has edge - REQUIRE(has_edge(g)); + REQUIRE(has_edges(g)); } -TEST_CASE("has_edge - Large graph with many vertices, last has edge", "[has_edge][large]") { +TEST_CASE("has_edges - Large graph with many vertices, last has edge", "[has_edges][large]") { SimpleGraph g(1000); g[999].push_back({998, 10}); // Only last vertex has edge - REQUIRE(has_edge(g)); + REQUIRE(has_edges(g)); } -TEST_CASE("has_edge - Large graph with many vertices, none have edges", "[has_edge][large]") { +TEST_CASE("has_edges - Large graph with many vertices, none have edges", "[has_edges][large]") { SimpleGraph g(1000); // Many vertices, no edges - REQUIRE_FALSE(has_edge(g)); + REQUIRE_FALSE(has_edges(g)); } -TEST_CASE("has_edge - Multigraph with parallel edges", "[has_edge][multigraph]") { +TEST_CASE("has_edges - Multigraph with parallel edges", "[has_edges][multigraph]") { SimpleGraph g(2); // Multiple edges between same vertices g[0].push_back({1, 10}); g[0].push_back({1, 20}); g[0].push_back({1, 30}); - REQUIRE(has_edge(g)); + REQUIRE(has_edges(g)); } // ============================================================================= // Test Integration with Other CPOs // ============================================================================= -TEST_CASE("has_edge - Consistent with num_edges for empty graph", "[has_edge][integration]") { +TEST_CASE("has_edges - Consistent with num_edges for empty graph", "[has_edges][integration]") { SimpleGraph g(3); - REQUIRE_FALSE(has_edge(g)); + REQUIRE_FALSE(has_edges(g)); REQUIRE(num_edges(g) == 0); } -TEST_CASE("has_edge - Consistent with num_edges for graph with edges", "[has_edge][integration]") { +TEST_CASE("has_edges - Consistent with num_edges for graph with edges", "[has_edges][integration]") { SimpleGraph g(3); g[0].push_back({1, 10}); g[1].push_back({2, 20}); - REQUIRE(has_edge(g)); + REQUIRE(has_edges(g)); REQUIRE(num_edges(g) > 0); } -TEST_CASE("has_edge - After adding edge with edges CPO", "[has_edge][integration]") { +TEST_CASE("has_edges - After adding edge with edges CPO", "[has_edges][integration]") { SimpleGraph g(3); - REQUIRE_FALSE(has_edge(g)); + REQUIRE_FALSE(has_edges(g)); // Add edge g[0].push_back({1, 10}); - REQUIRE(has_edge(g)); + REQUIRE(has_edges(g)); // Verify we can find it auto u = *vertices(g).begin(); // First vertex @@ -279,14 +279,14 @@ TEST_CASE("has_edge - After adding edge with edges CPO", "[has_edge][integration REQUIRE(!std::ranges::empty(edge_range)); } -TEST_CASE("has_edge - Verify short-circuit behavior", "[has_edge][performance]") { - // This test verifies that has_edge stops at first vertex with edges +TEST_CASE("has_edges - Verify short-circuit behavior", "[has_edges][performance]") { + // This test verifies that has_edges stops at first vertex with edges // We can't directly test performance, but we can verify behavior SimpleGraph g(100); g[0].push_back({1, 10}); // First vertex has edge // All other vertices empty - REQUIRE(has_edge(g)); + REQUIRE(has_edges(g)); // If implementation is correct, it should stop at first vertex } @@ -294,7 +294,7 @@ TEST_CASE("has_edge - Verify short-circuit behavior", "[has_edge][performance]") // Test Different Graph Topologies // ============================================================================= -TEST_CASE("has_edge - Directed acyclic graph", "[has_edge][topology]") { +TEST_CASE("has_edges - Directed acyclic graph", "[has_edges][topology]") { SimpleGraph g(5); // DAG topology g[0].push_back({1, 10}); @@ -302,19 +302,19 @@ TEST_CASE("has_edge - Directed acyclic graph", "[has_edge][topology]") { g[1].push_back({3, 30}); g[2].push_back({3, 40}); g[3].push_back({4, 50}); - REQUIRE(has_edge(g)); + REQUIRE(has_edges(g)); } -TEST_CASE("has_edge - Cyclic graph", "[has_edge][topology]") { +TEST_CASE("has_edges - Cyclic graph", "[has_edges][topology]") { SimpleGraph g(3); // Cycle: 0 -> 1 -> 2 -> 0 g[0].push_back({1, 10}); g[1].push_back({2, 20}); g[2].push_back({0, 30}); - REQUIRE(has_edge(g)); + REQUIRE(has_edges(g)); } -TEST_CASE("has_edge - Disconnected components, some have edges", "[has_edge][topology]") { +TEST_CASE("has_edges - Disconnected components, some have edges", "[has_edges][topology]") { SimpleGraph g(6); // Component 1: 0-1 (has edge) g[0].push_back({1, 10}); @@ -322,16 +322,16 @@ TEST_CASE("has_edge - Disconnected components, some have edges", "[has_edge][top // Component 3: 3-4-5 (has edges) g[3].push_back({4, 20}); g[4].push_back({5, 30}); - REQUIRE(has_edge(g)); + REQUIRE(has_edges(g)); } -TEST_CASE("has_edge - Disconnected components, none have edges", "[has_edge][topology]") { +TEST_CASE("has_edges - Disconnected components, none have edges", "[has_edges][topology]") { SimpleGraph g(5); // All vertices isolated, no edges - REQUIRE_FALSE(has_edge(g)); + REQUIRE_FALSE(has_edges(g)); } -TEST_CASE("has_edge - Tree structure", "[has_edge][topology]") { +TEST_CASE("has_edges - Tree structure", "[has_edges][topology]") { SimpleGraph g(7); // Tree rooted at 0 g[0].push_back({1, 10}); @@ -340,5 +340,5 @@ TEST_CASE("has_edge - Tree structure", "[has_edge][topology]") { g[1].push_back({4, 40}); g[2].push_back({5, 50}); g[2].push_back({6, 60}); - REQUIRE(has_edge(g)); + REQUIRE(has_edges(g)); } diff --git a/tests/adj_list/cpo/test_in_edges_cpo.cpp b/tests/adj_list/cpo/test_in_edges_cpo.cpp new file mode 100644 index 0000000..14cfcb2 --- /dev/null +++ b/tests/adj_list/cpo/test_in_edges_cpo.cpp @@ -0,0 +1,403 @@ +/** + * @file test_in_edges_cpo.cpp + * @brief Comprehensive tests for in_edges, in_degree CPOs, outgoing aliases, and type aliases + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include "../../common/graph_test_types.hpp" +#include + +using namespace graph; +using namespace graph::adj_list; + +// ============================================================================= +// Scenario 1: Stub graph with in_edges() vertex member +// +// The graph IS a vector. Each vertex's inner value has an +// in_edges() member. The _vertex_member tier should fire. +// ============================================================================= + +namespace test_vertex_member { + +struct InEdgeVertex { + std::vector edges_out; // outgoing — satisfies default edges() pattern + std::vector in_list; // incoming + + auto in_edges() const -> const std::vector& { return in_list; } +}; + +using MemberGraph = std::vector; + +} // namespace test_vertex_member + +TEST_CASE("in_edges(g,u) - vertex member tier", "[in_edges][cpo][member]") { + test_vertex_member::MemberGraph graph = { + {{3}, {1, 2}}, // vertex 0: out={3}, in={1,2} + {{0, 2}, {0}}, // vertex 1: out={0,2}, in={0} + {{}, {}}, // vertex 2: out={}, in={} + }; + + auto verts = vertices(graph); + auto v0 = *verts.begin(); + + auto in_range = in_edges(graph, v0); + REQUIRE(std::ranges::forward_range); + + // Should have 2 incoming edges (from vertices 1 and 2) + size_t count = 0; + for ([[maybe_unused]] auto e : in_range) { + ++count; + } + REQUIRE(count == 2); + + // Vertex 2 has no incoming edges + auto it = verts.begin(); + std::advance(it, 2); + auto v2 = *it; + auto in_range_v2 = in_edges(graph, v2); + REQUIRE(std::ranges::empty(in_range_v2)); +} + +// ============================================================================= +// Scenario 2: Stub graph with ADL in_edges(g, u) +// +// The graph inherits from vector> (so edges/vertices work) and +// provides a separate in_adj member. ADL in_edges accesses it. +// ============================================================================= + +namespace test_adl_in_edges { + +struct ADLGraph : std::vector> { + std::vector> in_adj; + + explicit ADLGraph(size_t n) : std::vector>(n), in_adj(n) {} + + void add_edge(size_t from, size_t to) { + (*this)[from].push_back(static_cast(to)); + in_adj[to].push_back(static_cast(from)); + } +}; + +// ADL in_edges for vertex descriptor +template + requires vertex_descriptor_type +auto in_edges(ADLGraph& g, const U& u) -> const std::vector& { + return g.in_adj[u.vertex_id()]; +} + +} // namespace test_adl_in_edges + +TEST_CASE("in_edges(g,u) - ADL tier", "[in_edges][cpo][adl]") { + test_adl_in_edges::ADLGraph graph(4); + graph.add_edge(0, 1); + graph.add_edge(0, 2); + graph.add_edge(1, 2); + graph.add_edge(3, 0); + + // Vertex 2 should have in-edges from 0 and 1 + auto verts = vertices(graph); + auto it = verts.begin(); + std::advance(it, 2); + auto v2 = *it; + + auto in_range = in_edges(graph, v2); + REQUIRE(std::ranges::forward_range); + + size_t count = 0; + for ([[maybe_unused]] auto e : in_range) { + ++count; + } + REQUIRE(count == 2); +} + +// ============================================================================= +// Scenario 3: (g, uid) overload with default tier +// +// Uses ADLGraph. The (g, uid) default delegates through find_vertex + +// in_edges(g, u). +// ============================================================================= + +TEST_CASE("in_edges(g,uid) - default tier via find_vertex + in_edges(g,u)", "[in_edges][cpo][uid][default]") { + test_adl_in_edges::ADLGraph graph(3); + graph.add_edge(0, 1); + graph.add_edge(2, 1); + + // Vertex 1 has in-edges from 0 and 2 + auto in_range = in_edges(graph, size_t(1)); + REQUIRE(std::ranges::forward_range); + + size_t count = 0; + for ([[maybe_unused]] auto e : in_range) { + ++count; + } + REQUIRE(count == 2); +} + +// ============================================================================= +// Scenario 4: Type alias verification +// ============================================================================= + +TEST_CASE("in_edges type aliases compile correctly", "[in_edges][cpo][type_alias]") { + using Graph = test_adl_in_edges::ADLGraph; + + // These should compile without error + using InRange = in_edge_range_t; + using InIter = in_edge_iterator_t; + using InEdge = in_edge_t; + + STATIC_REQUIRE(std::ranges::forward_range); + STATIC_REQUIRE(std::input_iterator); + STATIC_REQUIRE(std::is_same_v>); +} + +// ============================================================================= +// Scenario 5: Mixed-type test - in_edge_t != edge_t +// +// The graph inherits from vector>> so edges() uses +// the _edge_value_pattern tier (pair = weighted). ADL in_edges() returns +// plain vector& (unweighted source IDs). The two edge types differ. +// ============================================================================= + +namespace test_mixed_types { + +struct MixedGraph : std::vector>> { + std::vector> in_adj; + + explicit MixedGraph(size_t n) : std::vector>>(n), in_adj(n) {} + + void add_edge(size_t from, size_t to, double w) { + (*this)[from].push_back({static_cast(to), w}); + in_adj[to].push_back(static_cast(from)); + } +}; + +template + requires vertex_descriptor_type +auto in_edges(MixedGraph& g, const U& u) -> const std::vector& { + return g.in_adj[u.vertex_id()]; +} + +} // namespace test_mixed_types + +TEST_CASE("in_edge_t differs from edge_t when edges differ", "[in_edges][cpo][mixed_types]") { + using Graph = test_mixed_types::MixedGraph; + + // edge_t wraps pair (from outgoing edges via _edge_value_pattern) + using OutEdge = edge_t; + // in_edge_t wraps int (from incoming edges via ADL) + using InEdge = in_edge_t; + + // They should be different types since the underlying containers differ + STATIC_REQUIRE(!std::is_same_v); +} + +// ============================================================================= +// Scenario 6: out_edges / out_degree / find_out_edge alias identity +// ============================================================================= + +TEST_CASE("out_edges, out_degree, find_out_edge are aliases for edges, degree, find_vertex_edge", + "[out_edges][cpo][alias]") { + STATIC_REQUIRE(&out_edges == &edges); + STATIC_REQUIRE(&out_degree == °ree); + STATIC_REQUIRE(&find_out_edge == &find_vertex_edge); +} + +// ============================================================================= +// Scenario 7: in_degree CPO — member, ADL, default tiers +// ============================================================================= + +namespace test_in_degree_member { + +// Graph where g.in_degree(u) member returns doubled count +struct Graph : std::vector> { + std::vector> in_adj; + + explicit Graph(size_t n) : std::vector>(n), in_adj(n) {} + + template + requires vertex_descriptor_type + size_t in_degree(const U& u) const { + return in_adj[u.vertex_id()].size() * 2; // doubled for testing + } +}; + +// Also provide ADL in_edges so default tier would also be available +template + requires vertex_descriptor_type +auto in_edges(Graph& g, const U& u) -> const std::vector& { + return g.in_adj[u.vertex_id()]; +} + +} // namespace test_in_degree_member + +namespace test_in_degree_adl { + +struct Graph : std::vector> { + std::vector> in_adj; + + explicit Graph(size_t n) : std::vector>(n), in_adj(n) {} +}; + +// ADL in_degree — tripled count +template + requires vertex_descriptor_type +size_t in_degree(Graph& g, const U& u) { + return g.in_adj[u.vertex_id()].size() * 3; // tripled for testing +} + +// Also provide ADL in_edges +template + requires vertex_descriptor_type +auto in_edges(Graph& g, const U& u) -> const std::vector& { + return g.in_adj[u.vertex_id()]; +} + +} // namespace test_in_degree_adl + +TEST_CASE("in_degree(g,u) - member tier", "[in_degree][cpo][member]") { + test_in_degree_member::Graph graph(3); + graph.in_adj[0] = {1, 2}; + graph.in_adj[1] = {0}; + graph.in_adj[2] = {}; + + auto verts = vertices(graph); + auto v0 = *verts.begin(); + + // Member returns doubled: 2 * 2 = 4 + REQUIRE(in_degree(graph, v0) == 4); +} + +TEST_CASE("in_degree(g,u) - ADL tier", "[in_degree][cpo][adl]") { + test_in_degree_adl::Graph graph(3); + graph.in_adj[0] = {1, 2}; + graph.in_adj[1] = {0}; + graph.in_adj[2] = {}; + + auto verts = vertices(graph); + auto v0 = *verts.begin(); + + // ADL returns tripled: 2 * 3 = 6 + REQUIRE(in_degree(graph, v0) == 6); +} + +TEST_CASE("in_degree(g,u) - default tier via size(in_edges)", "[in_degree][cpo][default]") { + test_adl_in_edges::ADLGraph graph(4); + graph.add_edge(0, 2); + graph.add_edge(1, 2); + graph.add_edge(3, 2); + + auto verts = vertices(graph); + auto it = verts.begin(); + std::advance(it, 2); + auto v2 = *it; + + // Default: count in_edges(g, u) => 3 + REQUIRE(in_degree(graph, v2) == 3); +} + +TEST_CASE("in_degree(g,uid) - default tier", "[in_degree][cpo][uid][default]") { + test_adl_in_edges::ADLGraph graph(3); + graph.add_edge(0, 1); + graph.add_edge(2, 1); + + // uid overload: find_vertex then in_degree(g, u) + REQUIRE(in_degree(graph, size_t(1)) == 2); + REQUIRE(in_degree(graph, size_t(0)) == 0); +} + +// ============================================================================= +// Scenario 8: Outgoing type aliases +// ============================================================================= + +TEST_CASE("out_edge_range_t, out_edge_iterator_t, out_edge_t match existing aliases", + "[out_edges][cpo][type_alias]") { + using Graph = std::vector>; + + STATIC_REQUIRE(std::is_same_v, vertex_edge_range_t>); + STATIC_REQUIRE(std::is_same_v, vertex_edge_iterator_t>); + STATIC_REQUIRE(std::is_same_v, edge_t>); +} + +// ============================================================================= +// Scenario 9: dynamic_graph with non-uniform bidirectional traits +// +// dynamic_graph> satisfies the +// bidirectional_adjacency_list concept because its in_edge_type is +// dynamic_in_edge, which has source_id() — enabling CPO Tier 1 dispatch. +// ============================================================================= + +using DynBiDirGraph = + graph::container::dynamic_graph>; + +TEST_CASE("in_edges(g,u) - dynamic_graph non-uniform bidir traits", + "[in_edges][cpo][dynamic_graph]") { + using namespace graph; + using namespace graph::container; + + // Graph: 0->1, 0->2, 1->2, 2->3 + // Expected in-edges: 0↦{}, 1↦{0→1}, 2↦{0→2,1→2}, 3↦{2→3} + DynBiDirGraph g({{0, 1}, {0, 2}, {1, 2}, {2, 3}}); + + SECTION("vertex 2 has two incoming edges") { + auto u2 = *find_vertex(g, uint32_t(2)); + size_t cnt = 0; + for ([[maybe_unused]] auto ie : in_edges(g, u2)) + ++cnt; + REQUIRE(cnt == 2); + } + + SECTION("vertex 0 has no incoming edges") { + auto u0 = *find_vertex(g, uint32_t(0)); + REQUIRE(std::ranges::empty(in_edges(g, u0))); + } + + SECTION("vertex 3 has one incoming edge") { + auto u3 = *find_vertex(g, uint32_t(3)); + size_t cnt = 0; + for ([[maybe_unused]] auto ie : in_edges(g, u3)) + ++cnt; + REQUIRE(cnt == 1); + } + + SECTION("in_degree matches in_edges count for all vertices") { + for (auto u : vertices(g)) { + size_t deg = in_degree(g, u); + size_t cnt = 0; + for ([[maybe_unused]] auto ie : in_edges(g, u)) + ++cnt; + REQUIRE(deg == cnt); + } + } + + SECTION("in_edges(g, uid) overload") { + size_t cnt = 0; + for ([[maybe_unused]] auto ie : in_edges(g, uint32_t(2))) + ++cnt; + REQUIRE(cnt == 2); + } + + SECTION("in_degree(g, uid) overload") { + REQUIRE(in_degree(g, uint32_t(2)) == 2); + REQUIRE(in_degree(g, uint32_t(0)) == 0); + } +} + +TEST_CASE("in_edges type aliases for dynamic_graph non-uniform bidir", + "[in_edges][cpo][dynamic_graph][type_alias]") { + using G = DynBiDirGraph; + using InRange = in_edge_range_t; + using InIter = in_edge_iterator_t; + using InEdge = in_edge_t; + + STATIC_REQUIRE(std::ranges::forward_range); + STATIC_REQUIRE(std::input_iterator); + STATIC_REQUIRE(std::is_same_v>); +} diff --git a/tests/adj_list/cpo/test_source_id_cpo.cpp b/tests/adj_list/cpo/test_source_id_cpo.cpp index 0ef3177..1721819 100644 --- a/tests/adj_list/cpo/test_source_id_cpo.cpp +++ b/tests/adj_list/cpo/test_source_id_cpo.cpp @@ -7,6 +7,9 @@ #include #include #include +#include +#include +#include "../../common/graph_test_types.hpp" #include #include #include @@ -483,3 +486,92 @@ TEST_CASE("source_id(g,uv) - self-loops", "[source_id][cpo][selfloop]") { REQUIRE(sources[0] == 1); // self-loop source } } + +// ============================================================================= +// Test: dynamic_graph non-uniform bidir — source_id on in-edges (Tier 1) +// +// Non-uniform bidir traits define in_edge_type = dynamic_in_edge, which has a +// source_id() member. The CPO resolves via Tier 1 (native edge member), not +// the descriptor-based Tier 4 used for generic adj-list out-edges. +// ============================================================================= + +using DynBidirSrcId = + graph::container::dynamic_graph>; + +TEST_CASE("source_id(g,ie) - dynamic_graph in-edges via non-uniform bidir (Tier 1)", + "[source_id][cpo][dynamic_graph][bidir][in_edges]") { + using namespace graph; + using namespace graph::container; + + // Graph: 0->1, 0->2, 1->2, 2->3 + // In-edges of vertex 2: from 0 and 1 + DynBidirSrcId g({{0, 1}, {0, 2}, {1, 2}, {2, 3}}); + + auto u2 = *find_vertex(g, uint32_t(2)); + + SECTION("source_id of in-edges to vertex 2 are 0 and 1") { + std::vector sources; + for (auto ie : in_edges(g, u2)) + sources.push_back(source_id(g, ie)); + std::sort(sources.begin(), sources.end()); + REQUIRE(sources.size() == 2); + REQUIRE(sources[0] == 0); + REQUIRE(sources[1] == 1); + } + + SECTION("source_id of in-edge to vertex 3 is 2") { + auto u3 = *find_vertex(g, uint32_t(3)); + for (auto ie : in_edges(g, u3)) + REQUIRE(source_id(g, ie) == 2); + } + + SECTION("all source_ids from in-edges are valid vertex IDs") { + for (auto u : vertices(g)) { + for (auto ie : in_edges(g, u)) { + auto sid = source_id(g, ie); + REQUIRE(sid < num_vertices(g)); + } + } + } +} + +TEST_CASE("source_id(g,oe) - dynamic_graph out-edges in bidir graph (Tier 4)", + "[source_id][cpo][dynamic_graph][bidir][out_edges]") { + using namespace graph; + using namespace graph::container; + + // Out-edge source_id on a bidir graph works the same as non-bidir (Tier 4). + DynBidirSrcId g({{0, 1}, {0, 2}, {1, 2}}); + + SECTION("source_id of out-edges matches the source vertex for all") { + for (auto u : vertices(g)) { + auto vid = vertex_id(g, u); + for (auto oe : edges(g, u)) + REQUIRE(source_id(g, oe) == vid); + } + } +} + +TEST_CASE("source_id(g,ie) - dynamic_graph weighted non-uniform bidir", + "[source_id][cpo][dynamic_graph][bidir][weighted]") { + using namespace graph; + using namespace graph::container; + + using WeightedG = + graph::container::dynamic_graph>; + + WeightedG g({{0, 1, 10}, {0, 2, 20}, {1, 2, 30}}); + + SECTION("source_id of in-edges to vertex 2 are 0 and 1") { + auto u2 = *find_vertex(g, uint32_t(2)); + std::vector sources; + for (auto ie : in_edges(g, u2)) + sources.push_back(source_id(g, ie)); + std::sort(sources.begin(), sources.end()); + REQUIRE(sources.size() == 2); + REQUIRE(sources[0] == 0); + REQUIRE(sources[1] == 1); + } +} diff --git a/tests/adj_list/traits/test_incoming_edge_traits.cpp b/tests/adj_list/traits/test_incoming_edge_traits.cpp new file mode 100644 index 0000000..50f9292 --- /dev/null +++ b/tests/adj_list/traits/test_incoming_edge_traits.cpp @@ -0,0 +1,95 @@ +/** + * @file test_incoming_edge_traits.cpp + * @brief Unit tests for incoming edge traits: has_in_degree, has_find_in_edge, has_contains_in_edge + */ + +#include +#include +#include +#include +#include + +using namespace graph; +using namespace graph::adj_list; + +// ============================================================================= +// Stub graph with ADL in_edges — provides incoming edge support +// ============================================================================= + +namespace test_incoming_traits { + +struct IncomingGraph : std::vector> { + std::vector> in_adj; + + explicit IncomingGraph(size_t n) : std::vector>(n), in_adj(n) {} + + void add_edge(size_t from, size_t to) { + (*this)[from].push_back(static_cast(to)); + in_adj[to].push_back(static_cast(from)); + } +}; + +// ADL in_edges for vertex descriptor +template + requires vertex_descriptor_type +auto in_edges(IncomingGraph& g, const U& u) -> const std::vector& { + return g.in_adj[u.vertex_id()]; +} + +} // namespace test_incoming_traits + +// Simple graph without in_edges support +using PlainGraph = std::vector>; + +// ============================================================================= +// has_in_degree Tests +// ============================================================================= + +TEST_CASE("has_in_degree trait for graph with in_edges support", + "[adjacency_list_traits][has_in_degree]") { + STATIC_REQUIRE(has_in_degree); + STATIC_REQUIRE(has_in_degree_v); +} + +TEST_CASE("has_in_degree trait is false for plain graph without in_edges", + "[adjacency_list_traits][has_in_degree]") { + STATIC_REQUIRE_FALSE(has_in_degree); + STATIC_REQUIRE_FALSE(has_in_degree_v); +} + +// ============================================================================= +// has_find_in_edge Tests +// ============================================================================= + +TEST_CASE("has_find_in_edge trait for graph with in_edges support", + "[adjacency_list_traits][has_find_in_edge]") { + STATIC_REQUIRE(has_find_in_edge); + STATIC_REQUIRE(has_find_in_edge_v); +} + +TEST_CASE("has_find_in_edge trait is also true for plain graph (default delegates to outgoing edges)", + "[adjacency_list_traits][has_find_in_edge]") { + STATIC_REQUIRE(has_find_in_edge); + STATIC_REQUIRE(has_find_in_edge_v); +} + +// ============================================================================= +// has_contains_in_edge Tests +// ============================================================================= + +TEST_CASE("has_contains_in_edge trait for graph with in_edges and vertex descriptors", + "[adjacency_list_traits][has_contains_in_edge]") { + using Graph = test_incoming_traits::IncomingGraph; + using V = vertex_t; + + STATIC_REQUIRE(has_contains_in_edge); + STATIC_REQUIRE(has_contains_in_edge_v); +} + +TEST_CASE("has_contains_in_edge trait is also true for plain graph (default delegates to outgoing edges)", + "[adjacency_list_traits][has_contains_in_edge]") { + using V = vertex_t; + + STATIC_REQUIRE(has_contains_in_edge); + STATIC_REQUIRE(has_contains_in_edge_v); +} diff --git a/tests/algorithms/CMakeLists.txt b/tests/algorithms/CMakeLists.txt index 0fe2905..557cf55 100644 --- a/tests/algorithms/CMakeLists.txt +++ b/tests/algorithms/CMakeLists.txt @@ -16,6 +16,7 @@ add_executable(test_algorithms test_articulation_points.cpp test_biconnected_components.cpp test_jaccard.cpp + test_scc_bidirectional.cpp ) target_link_libraries(test_algorithms diff --git a/tests/algorithms/test_dijkstra_shortest_paths.cpp b/tests/algorithms/test_dijkstra_shortest_paths.cpp index f4a11a9..b428d96 100644 --- a/tests/algorithms/test_dijkstra_shortest_paths.cpp +++ b/tests/algorithms/test_dijkstra_shortest_paths.cpp @@ -282,3 +282,98 @@ TEST_CASE("dijkstra_shortest_paths - vertex id visitor", "[algorithm][dijkstra_s REQUIRE(distance[0] == 0); REQUIRE(distance[1] == clrs_dijkstra_results::distances_from_0[1]); } + +// ============================================================================= +// Error / Exception Condition Tests +// ============================================================================= + +TEST_CASE("dijkstra_shortest_paths - distances too small throws", "[algorithm][dijkstra_shortest_paths][error]") { + using Graph = vov_weighted; + + auto g = clrs_dijkstra_graph(); // 5 vertices + std::vector distances(2); // too small + std::vector> predecessor(num_vertices(g)); + init_shortest_paths(distances, predecessor); + + CHECK_THROWS_AS(dijkstra_shortest_paths(g, vertex_id_t(0), distances, predecessor, + [](const auto& g, const auto& uv) { return edge_value(g, uv); }), + std::out_of_range); +} + +TEST_CASE("dijkstra_shortest_paths - predecessor too small throws", "[algorithm][dijkstra_shortest_paths][error]") { + using Graph = vov_weighted; + + auto g = clrs_dijkstra_graph(); // 5 vertices + std::vector distances(num_vertices(g)); + std::vector> predecessor(2); // too small + init_shortest_paths(distances, predecessor); + + CHECK_THROWS_AS(dijkstra_shortest_paths(g, vertex_id_t(0), distances, predecessor, + [](const auto& g, const auto& uv) { return edge_value(g, uv); }), + std::out_of_range); +} + +TEST_CASE("dijkstra_shortest_paths - source vertex out of range throws", "[algorithm][dijkstra_shortest_paths][error]") { + using Graph = vov_weighted; + + auto g = path_graph_4_weighted(); // 4 vertices (ids 0-3) + std::vector distances(num_vertices(g)); + std::vector> predecessor(num_vertices(g)); + init_shortest_paths(distances, predecessor); + + CHECK_THROWS_AS(dijkstra_shortest_paths(g, vertex_id_t(99), distances, predecessor, + [](const auto& g, const auto& uv) { return edge_value(g, uv); }), + std::out_of_range); +} + +TEST_CASE("dijkstra_shortest_paths - negative edge weight throws", "[algorithm][dijkstra_shortest_paths][error]") { + using Graph = vov_weighted; // int edge values (signed) + + auto g = path_graph_4_weighted(); // 0->1->2->3 + std::vector distances(num_vertices(g)); + std::vector> predecessor(num_vertices(g)); + init_shortest_paths(distances, predecessor); + + // Weight function that always returns a negative value triggers the signed-weight guard + CHECK_THROWS_AS(dijkstra_shortest_paths(g, vertex_id_t(0), distances, predecessor, + [](const auto&, const auto&) { return -1; }), + std::out_of_range); +} + +TEST_CASE("dijkstra_shortest_paths - infinite weight edge triggers logic_error", "[algorithm][dijkstra_shortest_paths][error]") { + using Graph = vov_weighted; + using distance_type = int; + + // When the edge weight equals the infinity sentinel, combine(0, INF) = INF which is + // NOT strictly less than INF, so relax_target returns false for an undiscovered vertex. + // The algorithm treats this as an internal invariant violation and throws std::logic_error. + auto g = path_graph_4_weighted(); // 0->1->2->3 + std::vector distances(num_vertices(g)); + std::vector> predecessor(num_vertices(g)); + init_shortest_paths(distances, predecessor); + + const auto INF = shortest_path_infinite_distance(); + CHECK_THROWS_AS(dijkstra_shortest_paths(g, vertex_id_t(0), distances, predecessor, + [INF](const auto&, const auto&) { return INF; }), + std::logic_error); +} + +TEST_CASE("dijkstra_shortest_paths - on_edge_not_relaxed visitor callback", "[algorithm][dijkstra_shortest_paths][visitor]") { + using Graph = vov_weighted; + + // The CLRS graph has multiple paths to the same vertex; revisiting an already-optimal + // vertex triggers on_edge_not_relaxed (e.g. z->s, t->y, x->z are not relaxed). + auto g = clrs_dijkstra_graph(); + std::vector distances(num_vertices(g)); + std::vector> predecessor(num_vertices(g)); + init_shortest_paths(distances, predecessor); + + CountingVisitor visitor; + dijkstra_shortest_paths(g, vertex_id_t(0), distances, predecessor, + [](const auto& g, const auto& uv) { return edge_value(g, uv); }, visitor); + + CHECK(visitor.edges_not_relaxed > 0); + REQUIRE(distances[0] == 0); + REQUIRE(distances[1] == clrs_dijkstra_results::distances_from_0[1]); // t: 8 + REQUIRE(distances[3] == clrs_dijkstra_results::distances_from_0[3]); // y: 5 +} diff --git a/tests/algorithms/test_scc_bidirectional.cpp b/tests/algorithms/test_scc_bidirectional.cpp new file mode 100644 index 0000000..fa0963f --- /dev/null +++ b/tests/algorithms/test_scc_bidirectional.cpp @@ -0,0 +1,331 @@ +/** + * @file test_scc_bidirectional.cpp + * @brief Tests for kosaraju() bidirectional overload (single-graph SCC). + * + * Verifies: + * - Correctness of SCC detection using in_edges (no separate transpose graph) + * - Agreement with the two-graph kosaraju overload + * - Works with both vov (random-access) and vol (forward-iterator) containers + * + * Uses NON-UNIFORM bidirectional traits so that in_edges store dynamic_in_edge + * (which has source_id()), satisfying the bidirectional_adjacency_list concept. + */ + +#include +#include +#include +#include +#include +#include "../common/graph_fixtures.hpp" +#include +#include +#include +#include + +using namespace graph; +using namespace graph::container; +using namespace graph::test::fixtures; + +// ============================================================================= +// Non-uniform bidirectional traits +// Uniform traits (vov_graph_traits<..., true>) store dynamic_out_edge for in-edges, +// which lacks source_id(). Non-uniform traits define in_edge_type = dynamic_in_edge +// so that source_id(g, ie) works via the native edge member tier. +// ============================================================================= + +template +struct vov_bidir_graph_traits { + using edge_value_type = EV; + using vertex_value_type = VV; + using graph_value_type = GV; + using vertex_id_type = VId; + static constexpr bool bidirectional = true; + + using edge_type = dynamic_out_edge; + using in_edge_type = dynamic_in_edge; + using vertex_type = dynamic_vertex; + using graph_type = dynamic_graph; + + using edges_type = std::vector; + using in_edges_type = std::vector; + using vertices_type = std::vector; +}; + +template +struct vol_bidir_graph_traits { + using edge_value_type = EV; + using vertex_value_type = VV; + using graph_value_type = GV; + using vertex_id_type = VId; + static constexpr bool bidirectional = true; + + using edge_type = dynamic_out_edge; + using in_edge_type = dynamic_in_edge; + using vertex_type = dynamic_vertex; + using graph_type = dynamic_graph; + + using edges_type = std::list; + using in_edges_type = std::list; + using vertices_type = std::vector; +}; + +// Bidirectional graph types using non-uniform traits +using bidir_vov_void = dynamic_graph>; + +using bidir_vol_void = dynamic_graph>; + +using bidir_vov_int = dynamic_graph>; + +// ============================================================================= +// Helpers (mirrored from test_connected_components.cpp) +// ============================================================================= + +template +bool all_same_component(const Component& component, const std::vector& vertices) { + if (vertices.empty()) + return true; + auto first_comp = component[vertices[0]]; + return std::all_of(vertices.begin(), vertices.end(), [&](size_t v) { return component[v] == first_comp; }); +} + +template +bool different_components(const Component& component, size_t u, size_t v) { + return component[u] != component[v]; +} + +template +size_t count_unique_components(const Component& component) { + std::set unique(component.begin(), component.end()); + return unique.size(); +} + +// ============================================================================= +// Single vertex +// ============================================================================= + +TEST_CASE("kosaraju bidir - single vertex (vov)", "[algorithm][kosaraju][scc][bidirectional]") { + auto g = single_vertex(); + + std::vector component(num_vertices(g)); + kosaraju(g, component); + + REQUIRE(component[0] == 0); + REQUIRE(count_unique_components(component) == 1); +} + +TEST_CASE("kosaraju bidir - single vertex (vol)", "[algorithm][kosaraju][scc][bidirectional]") { + auto g = single_vertex(); + + std::vector component(num_vertices(g)); + kosaraju(g, component); + + REQUIRE(component[0] == 0); + REQUIRE(count_unique_components(component) == 1); +} + +// ============================================================================= +// Simple cycle — all vertices in one SCC +// ============================================================================= + +TEST_CASE("kosaraju bidir - simple cycle (vov)", "[algorithm][kosaraju][scc][bidirectional]") { + // 0 → 1 → 2 → 0 + bidir_vov_void g({{0, 1}, {1, 2}, {2, 0}}); + std::vector component(num_vertices(g)); + + kosaraju(g, component); + + REQUIRE(all_same_component(component, {0, 1, 2})); + REQUIRE(count_unique_components(component) == 1); +} + +TEST_CASE("kosaraju bidir - simple cycle (vol)", "[algorithm][kosaraju][scc][bidirectional]") { + bidir_vol_void g({{0, 1}, {1, 2}, {2, 0}}); + std::vector component(num_vertices(g)); + + kosaraju(g, component); + + REQUIRE(all_same_component(component, {0, 1, 2})); + REQUIRE(count_unique_components(component) == 1); +} + +// ============================================================================= +// Two SCCs +// ============================================================================= + +TEST_CASE("kosaraju bidir - two SCCs (vov)", "[algorithm][kosaraju][scc][bidirectional]") { + // SCC1: {0,1} (0 ↔ 1) + // SCC2: {2,3} (2 ↔ 3) + // Cross: 1 → 2 (one-way, so no merge) + bidir_vov_void g({{0, 1}, {1, 0}, {1, 2}, {2, 3}, {3, 2}}); + std::vector component(num_vertices(g)); + + kosaraju(g, component); + + REQUIRE(all_same_component(component, {0, 1})); + REQUIRE(all_same_component(component, {2, 3})); + REQUIRE(different_components(component, 0, 2)); + REQUIRE(count_unique_components(component) == 2); +} + +TEST_CASE("kosaraju bidir - two SCCs (vol)", "[algorithm][kosaraju][scc][bidirectional]") { + bidir_vol_void g({{0, 1}, {1, 0}, {1, 2}, {2, 3}, {3, 2}}); + std::vector component(num_vertices(g)); + + kosaraju(g, component); + + REQUIRE(all_same_component(component, {0, 1})); + REQUIRE(all_same_component(component, {2, 3})); + REQUIRE(different_components(component, 0, 2)); + REQUIRE(count_unique_components(component) == 2); +} + +// ============================================================================= +// DAG — every vertex is its own SCC +// ============================================================================= + +TEST_CASE("kosaraju bidir - DAG (vov)", "[algorithm][kosaraju][scc][bidirectional]") { + // 0 → 1 → 2 → 3 + bidir_vov_void g({{0, 1}, {1, 2}, {2, 3}}); + std::vector component(num_vertices(g)); + + kosaraju(g, component); + + REQUIRE(count_unique_components(component) == 4); + for (size_t i = 0; i < 4; ++i) + for (size_t j = i + 1; j < 4; ++j) + REQUIRE(different_components(component, i, j)); +} + +TEST_CASE("kosaraju bidir - DAG (vol)", "[algorithm][kosaraju][scc][bidirectional]") { + bidir_vol_void g({{0, 1}, {1, 2}, {2, 3}}); + std::vector component(num_vertices(g)); + + kosaraju(g, component); + + REQUIRE(count_unique_components(component) == 4); + for (size_t i = 0; i < 4; ++i) + for (size_t j = i + 1; j < 4; ++j) + REQUIRE(different_components(component, i, j)); +} + +// ============================================================================= +// Complex SCC structure (3 SCCs) +// ============================================================================= + +TEST_CASE("kosaraju bidir - complex SCCs (vov)", "[algorithm][kosaraju][scc][bidirectional]") { + // SCC1: {0,1,2} cycle 0→1→2→0 + // SCC2: {3,4} cycle 3→4→3 + // SCC3: {5} singleton + // Cross: 2→3, 4→5 + bidir_vov_void g({{0, 1}, {1, 2}, {2, 0}, {2, 3}, {3, 4}, {4, 3}, {4, 5}}); + + std::vector component(num_vertices(g)); + kosaraju(g, component); + + REQUIRE(count_unique_components(component) == 3); + REQUIRE(all_same_component(component, {0, 1, 2})); + REQUIRE(all_same_component(component, {3, 4})); + REQUIRE(different_components(component, 0, 3)); + REQUIRE(different_components(component, 0, 5)); + REQUIRE(different_components(component, 3, 5)); +} + +TEST_CASE("kosaraju bidir - complex SCCs (vol)", "[algorithm][kosaraju][scc][bidirectional]") { + bidir_vol_void g({{0, 1}, {1, 2}, {2, 0}, {2, 3}, {3, 4}, {4, 3}, {4, 5}}); + + std::vector component(num_vertices(g)); + kosaraju(g, component); + + REQUIRE(count_unique_components(component) == 3); + REQUIRE(all_same_component(component, {0, 1, 2})); + REQUIRE(all_same_component(component, {3, 4})); + REQUIRE(different_components(component, 0, 3)); + REQUIRE(different_components(component, 0, 5)); + REQUIRE(different_components(component, 3, 5)); +} + +// ============================================================================= +// Self-loops +// ============================================================================= + +TEST_CASE("kosaraju bidir - self loops", "[algorithm][kosaraju][scc][bidirectional]") { + // 0 self-loop, 1 self-loop, 0→1 (one-way) + bidir_vov_void g({{0, 0}, {1, 1}, {0, 1}}); + std::vector component(num_vertices(g)); + + kosaraju(g, component); + + // Each vertex is its own SCC (self-loop doesn't merge with others) + REQUIRE(count_unique_components(component) == 2); + REQUIRE(different_components(component, 0, 1)); +} + +// ============================================================================= +// Agreement with two-graph overload +// ============================================================================= + +TEST_CASE("kosaraju bidir - agrees with two-graph overload", "[algorithm][kosaraju][scc][bidirectional]") { + // Build same graph for both overloads + // Graph: 0→1→2→0 (SCC), 2→3→4→3 (SCC), 4→5 + bidir_vov_void g_bidir({{0, 1}, {1, 2}, {2, 0}, {2, 3}, {3, 4}, {4, 3}, {4, 5}}); + + // Non-bidir version + transpose + using vov_dir = dynamic_graph>; + vov_dir g_fwd({{0, 1}, {1, 2}, {2, 0}, {2, 3}, {3, 4}, {4, 3}, {4, 5}}); + vov_dir g_rev({{1, 0}, {2, 1}, {0, 2}, {3, 2}, {4, 3}, {3, 4}, {5, 4}}); + + std::vector comp_bidir(num_vertices(g_bidir)); + std::vector comp_twog(num_vertices(g_fwd)); + + kosaraju(g_bidir, comp_bidir); + kosaraju(g_fwd, g_rev, comp_twog); + + // Both should find the same number of SCCs + REQUIRE(count_unique_components(comp_bidir) == count_unique_components(comp_twog)); + + // Both should agree on which vertices are in the same SCC + for (size_t i = 0; i < comp_bidir.size(); ++i) + for (size_t j = i + 1; j < comp_bidir.size(); ++j) { + INFO("vertices " << i << " and " << j); + REQUIRE((comp_bidir[i] == comp_bidir[j]) == (comp_twog[i] == comp_twog[j])); + } +} + +// ============================================================================= +// Weighted bidirectional graph +// ============================================================================= + +TEST_CASE("kosaraju bidir - weighted edges ignored", "[algorithm][kosaraju][scc][bidirectional]") { + // Same structure as simple cycle but with weights + bidir_vov_int g({{0, 1, 10}, {1, 2, 20}, {2, 0, 30}}); + std::vector component(num_vertices(g)); + + kosaraju(g, component); + + REQUIRE(all_same_component(component, {0, 1, 2})); + REQUIRE(count_unique_components(component) == 1); +} + +// ============================================================================= +// Disconnected graph +// ============================================================================= + +TEST_CASE("kosaraju bidir - disconnected graph", "[algorithm][kosaraju][scc][bidirectional]") { + // 0→1→0 (SCC), 2 isolated, 3→4→3 (SCC) + bidir_vov_void g({{0, 1}, {1, 0}, {3, 4}, {4, 3}}); + + // Need 5 vertices (0-4). Graph constructor infers from max vertex id in edge list. + std::vector component(num_vertices(g)); + kosaraju(g, component); + + REQUIRE(count_unique_components(component) == 3); + REQUIRE(all_same_component(component, {0, 1})); + REQUIRE(all_same_component(component, {3, 4})); + REQUIRE(different_components(component, 0, 2)); + REQUIRE(different_components(component, 0, 3)); + REQUIRE(different_components(component, 2, 3)); +} diff --git a/tests/common/algorithm_test_types.hpp b/tests/common/algorithm_test_types.hpp index 73591a6..a04e55a 100644 --- a/tests/common/algorithm_test_types.hpp +++ b/tests/common/algorithm_test_types.hpp @@ -158,14 +158,14 @@ concept ordered_vertex_edges = requires(G& g, vertex_id_t u) { template struct is_sparse_vertex_container : std::false_type {}; -template -struct is_sparse_vertex_container> - : std::bool_constant> || - std::same_as> || - std::same_as> || - std::same_as> || - std::same_as> || - std::same_as>> {}; +template +struct is_sparse_vertex_container> + : std::bool_constant> || + std::same_as> || + std::same_as> || + std::same_as> || + std::same_as> || + std::same_as>> {}; template inline constexpr bool is_sparse_vertex_container_v = is_sparse_vertex_container::value; diff --git a/tests/common/graph_test_types.hpp b/tests/common/graph_test_types.hpp index c26665c..fbee06c 100644 --- a/tests/common/graph_test_types.hpp +++ b/tests/common/graph_test_types.hpp @@ -4,7 +4,7 @@ * * This header provides a tag-based type generation system that allows Catch2's * TEMPLATE_TEST_CASE to test multiple container types (vov, vod, dov, dod, vol, dol) with - * multiple value configurations (void, int, string, sourced) in a single test file. + * multiple value configurations (void, int, string, bidirectional) in a single test file. * * Usage: * TEMPLATE_TEST_CASE("test name", "[tags]", vov_tag, vod_tag, dov_tag, dod_tag, vol_tag, dol_tag) { @@ -54,6 +54,7 @@ #include // Edge unordered_map containers (hash-based, deduplicated by target_id) #include +#include // for vol_bidir_graph_traits::in_edges_type #include namespace graph::test { @@ -67,8 +68,8 @@ namespace graph::test { struct vov_tag { static constexpr const char* name = "vov"; - template - using traits = graph::container::vov_graph_traits; + template + using traits = graph::container::vov_graph_traits; }; /** @@ -77,8 +78,8 @@ struct vov_tag { struct vod_tag { static constexpr const char* name = "vod"; - template - using traits = graph::container::vod_graph_traits; + template + using traits = graph::container::vod_graph_traits; }; /** @@ -87,8 +88,8 @@ struct vod_tag { struct dov_tag { static constexpr const char* name = "dov"; - template - using traits = graph::container::dov_graph_traits; + template + using traits = graph::container::dov_graph_traits; }; /** @@ -97,8 +98,8 @@ struct dov_tag { struct dod_tag { static constexpr const char* name = "dod"; - template - using traits = graph::container::dod_graph_traits; + template + using traits = graph::container::dod_graph_traits; }; /** @@ -107,8 +108,8 @@ struct dod_tag { struct vol_tag { static constexpr const char* name = "vol"; - template - using traits = graph::container::vol_graph_traits; + template + using traits = graph::container::vol_graph_traits; }; /** @@ -117,8 +118,8 @@ struct vol_tag { struct dol_tag { static constexpr const char* name = "dol"; - template - using traits = graph::container::dol_graph_traits; + template + using traits = graph::container::dol_graph_traits; }; // ============================================================================= @@ -132,8 +133,8 @@ struct dol_tag { struct vofl_tag { static constexpr const char* name = "vofl"; - template - using traits = graph::container::vofl_graph_traits; + template + using traits = graph::container::vofl_graph_traits; }; /** @@ -143,8 +144,8 @@ struct vofl_tag { struct dofl_tag { static constexpr const char* name = "dofl"; - template - using traits = graph::container::dofl_graph_traits; + template + using traits = graph::container::dofl_graph_traits; }; /** @@ -154,8 +155,8 @@ struct dofl_tag { struct mofl_tag { static constexpr const char* name = "mofl"; - template - using traits = graph::container::mofl_graph_traits; + template + using traits = graph::container::mofl_graph_traits; }; // ============================================================================= @@ -169,8 +170,8 @@ struct mofl_tag { struct vos_tag { static constexpr const char* name = "vos"; - template - using traits = graph::container::vos_graph_traits; + template + using traits = graph::container::vos_graph_traits; }; /** @@ -180,8 +181,8 @@ struct vos_tag { struct dos_tag { static constexpr const char* name = "dos"; - template - using traits = graph::container::dos_graph_traits; + template + using traits = graph::container::dos_graph_traits; }; /** @@ -191,8 +192,8 @@ struct dos_tag { struct mos_tag { static constexpr const char* name = "mos"; - template - using traits = graph::container::mos_graph_traits; + template + using traits = graph::container::mos_graph_traits; }; /** @@ -202,8 +203,8 @@ struct mos_tag { struct uos_tag { static constexpr const char* name = "uos"; - template - using traits = graph::container::uos_graph_traits; + template + using traits = graph::container::uos_graph_traits; }; // ============================================================================= @@ -217,8 +218,8 @@ struct uos_tag { struct vous_tag { static constexpr const char* name = "vous"; - template - using traits = graph::container::vous_graph_traits; + template + using traits = graph::container::vous_graph_traits; }; /** @@ -228,8 +229,8 @@ struct vous_tag { struct dous_tag { static constexpr const char* name = "dous"; - template - using traits = graph::container::dous_graph_traits; + template + using traits = graph::container::dous_graph_traits; }; /** @@ -239,8 +240,8 @@ struct dous_tag { struct mous_tag { static constexpr const char* name = "mous"; - template - using traits = graph::container::mous_graph_traits; + template + using traits = graph::container::mous_graph_traits; }; /** @@ -250,8 +251,8 @@ struct mous_tag { struct uous_tag { static constexpr const char* name = "uous"; - template - using traits = graph::container::uous_graph_traits; + template + using traits = graph::container::uous_graph_traits; }; // ============================================================================= @@ -266,8 +267,8 @@ struct uous_tag { struct mol_tag { static constexpr const char* name = "mol"; - template - using traits = graph::container::mol_graph_traits; + template + using traits = graph::container::mol_graph_traits; }; /** @@ -277,8 +278,8 @@ struct mol_tag { struct mov_tag { static constexpr const char* name = "mov"; - template - using traits = graph::container::mov_graph_traits; + template + using traits = graph::container::mov_graph_traits; }; /** @@ -288,8 +289,8 @@ struct mov_tag { struct mod_tag { static constexpr const char* name = "mod"; - template - using traits = graph::container::mod_graph_traits; + template + using traits = graph::container::mod_graph_traits; }; // ============================================================================= @@ -304,8 +305,8 @@ struct mod_tag { struct uol_tag { static constexpr const char* name = "uol"; - template - using traits = graph::container::uol_graph_traits; + template + using traits = graph::container::uol_graph_traits; }; /** @@ -315,8 +316,8 @@ struct uol_tag { struct uov_tag { static constexpr const char* name = "uov"; - template - using traits = graph::container::uov_graph_traits; + template + using traits = graph::container::uov_graph_traits; }; /** @@ -326,8 +327,8 @@ struct uov_tag { struct uod_tag { static constexpr const char* name = "uod"; - template - using traits = graph::container::uod_graph_traits; + template + using traits = graph::container::uod_graph_traits; }; /** @@ -338,8 +339,8 @@ struct uod_tag { struct uofl_tag { static constexpr const char* name = "uofl"; - template - using traits = graph::container::uofl_graph_traits; + template + using traits = graph::container::uofl_graph_traits; }; // ============================================================================= @@ -354,8 +355,8 @@ struct uofl_tag { struct vom_tag { static constexpr const char* name = "vom"; - template - using traits = graph::container::vom_graph_traits; + template + using traits = graph::container::vom_graph_traits; }; /** @@ -365,8 +366,8 @@ struct vom_tag { struct mom_tag { static constexpr const char* name = "mom"; - template - using traits = graph::container::mom_graph_traits; + template + using traits = graph::container::mom_graph_traits; }; /** @@ -376,8 +377,8 @@ struct mom_tag { struct voum_tag { static constexpr const char* name = "voum"; - template - using traits = graph::container::voum_graph_traits; + template + using traits = graph::container::voum_graph_traits; }; // ============================================================================= @@ -387,23 +388,23 @@ struct voum_tag { /** * @brief Generates all standard graph type configurations from a container tag * - * @tparam Tag One of vov_tag, vod_tag, dov_tag, dod_tag + * @tparam Tag One of vov_tag, vod_tag, dov_tag, dod_tag, etc. * * Provides the 8 standard type aliases: - * - void_type: EV=void, VV=void, GV=void, Sourced=false - * - int_ev: EV=int, VV=void, GV=void, Sourced=false - * - int_vv: EV=void, VV=int, GV=void, Sourced=false - * - all_int: EV=int, VV=int, GV=int, Sourced=false - * - string_type: EV/VV/GV=string, Sourced=false - * - sourced_void: EV=void, VV=void, GV=void, Sourced=true - * - sourced_int: EV=int, VV=void, GV=void, Sourced=true - * - sourced_all: EV=int, VV=int, GV=int, Sourced=true + * - void_type: EV=void, VV=void, GV=void, Bidirectional=false + * - int_ev: EV=int, VV=void, GV=void, Bidirectional=false + * - int_vv: EV=void, VV=int, GV=void, Bidirectional=false + * - all_int: EV=int, VV=int, GV=int, Bidirectional=false + * - string_type: EV/VV/GV=string, Bidirectional=false + * - bidir_void: EV=void, VV=void, GV=void, Bidirectional=true + * - bidir_int: EV=int, VV=void, GV=void, Bidirectional=true + * - bidir_all: EV=int, VV=int, GV=int, Bidirectional=true */ template struct graph_test_types { using VId = uint32_t; - // Non-sourced configurations + // Non-bidirectional configurations using void_type = graph::container:: dynamic_graph>; @@ -424,14 +425,14 @@ struct graph_test_types { false, typename Tag::template traits>; - // Sourced configurations (for source_id/source CPO tests) - using sourced_void = graph::container:: + // Bidirectional configurations (for in_edges/source_id CPO tests) + using bidir_void = graph::container:: dynamic_graph>; - using sourced_int = graph::container:: + using bidir_int = graph::container:: dynamic_graph>; - using sourced_all = graph::container:: + using bidir_all = graph::container:: dynamic_graph>; // Container name for test output @@ -451,6 +452,70 @@ constexpr const char* container_name() { // Convenience aliases for Catch2 TEMPLATE_TEST_CASE // ============================================================================= +// ============================================================================= +// Non-uniform bidirectional traits +// +// These traits define in_edge_type = dynamic_in_edge so that source_id(g, ie) +// is resolved via the native edge member CPO tier (Tier 1) rather than the +// descriptor-based Tier 4. This is required to satisfy the +// bidirectional_adjacency_list concept, which requires source_id(g, ie) to +// compile for in_edge_t. +// ============================================================================= + +/** + * @brief Non-uniform bidir traits: vector + vector + vector + * + * Uses dynamic_in_edge for in_edges_type so that source_id(g, ie) dispatches + * to dynamic_in_edge::source_id() (Tier 1), not the descriptor-based Tier 4. + */ +template +struct vov_bidir_graph_traits { + using edge_value_type = EV; + using vertex_value_type = VV; + using graph_value_type = GV; + using vertex_id_type = VId; + static constexpr bool bidirectional = true; + + using edge_type = graph::container::dynamic_out_edge; + using in_edge_type = graph::container::dynamic_in_edge; + using vertex_type = graph::container::dynamic_vertex; + using graph_type = graph::container::dynamic_graph; + + using edges_type = std::vector; + using in_edges_type = std::vector; + using vertices_type = std::vector; +}; + +/** + * @brief Non-uniform bidir traits: vector + list + list + * + * List-based edges for tests that need non-random-access edge containers. + */ +template +struct vol_bidir_graph_traits { + using edge_value_type = EV; + using vertex_value_type = VV; + using graph_value_type = GV; + using vertex_id_type = VId; + static constexpr bool bidirectional = true; + + using edge_type = graph::container::dynamic_out_edge; + using in_edge_type = graph::container::dynamic_in_edge; + using vertex_type = graph::container::dynamic_vertex; + using graph_type = graph::container::dynamic_graph; + + using edges_type = std::list; + using in_edges_type = std::list; + using vertices_type = std::vector; +}; + +// Convenience type aliases +using bidir_nu_vov_void = graph::container::dynamic_graph>; +using bidir_nu_vov_int = graph::container::dynamic_graph>; +using bidir_nu_vov_all = graph::container::dynamic_graph>; +using bidir_nu_vol_void = graph::container::dynamic_graph>; +using bidir_nu_vol_int = graph::container::dynamic_graph>; + // Random-access containers (support num_edges(g,u), sized_range edges) using random_access_container_tags = std::tuple; diff --git a/tests/container/CMakeLists.txt b/tests/container/CMakeLists.txt index ab3993d..b88d555 100644 --- a/tests/container/CMakeLists.txt +++ b/tests/container/CMakeLists.txt @@ -46,6 +46,9 @@ add_executable(graph3_container_tests dynamic_graph/test_dynamic_graph_conversions.cpp dynamic_graph/test_dynamic_graph_heterogeneous.cpp + # dynamic_graph - bidirectional (in_edges) tests + dynamic_graph/test_dynamic_graph_bidirectional.cpp + # dynamic_graph - CPO tests (consolidated) dynamic_graph/test_dynamic_graph_cpo_random_access.cpp dynamic_graph/test_dynamic_graph_cpo_forward_list.cpp @@ -60,6 +63,7 @@ add_executable(graph3_container_tests # undirected_adjacency_list undirected_adjacency_list/test_undirected_adjacency_list.cpp undirected_adjacency_list/test_undirected_adjacency_list_cpo.cpp + undirected_adjacency_list/test_undirected_bidirectional.cpp ) target_link_libraries(graph3_container_tests diff --git a/tests/container/compressed_graph/test_compressed_graph_cpo.cpp b/tests/container/compressed_graph/test_compressed_graph_cpo.cpp index 116f7e4..6c65239 100644 --- a/tests/container/compressed_graph/test_compressed_graph_cpo.cpp +++ b/tests/container/compressed_graph/test_compressed_graph_cpo.cpp @@ -2339,59 +2339,59 @@ TEST_CASE("contains_edge(g, u, v) with high degree vertex", "[contains_edge][api } // ============================================================================= -// has_edge(g) CPO Tests +// has_edges(g) CPO Tests // ============================================================================= -TEST_CASE("has_edge(g) with graph containing edges", "[has_edge][api]") { +TEST_CASE("has_edges(g) with graph containing edges", "[has_edges][api]") { using Graph = compressed_graph; vector> edges_data = {{0, 1, 10}, {1, 2, 20}, {2, 3, 30}}; Graph g; g.load_edges(edges_data); - REQUIRE(has_edge(g) == true); + REQUIRE(has_edges(g) == true); } -TEST_CASE("has_edge(g) with empty graph", "[has_edge][api]") { +TEST_CASE("has_edges(g) with empty graph", "[has_edges][api]") { using Graph = compressed_graph; Graph g; // No edges loaded - REQUIRE(has_edge(g) == false); + REQUIRE(has_edges(g) == false); } -TEST_CASE("has_edge(g) with graph with vertices but no edges", "[has_edge][api]") { +TEST_CASE("has_edges(g) with graph with vertices but no edges", "[has_edges][api]") { using Graph = compressed_graph; vector> edges_data = {}; Graph g; g.load_edges(edges_data); - REQUIRE(has_edge(g) == false); + REQUIRE(has_edges(g) == false); } -TEST_CASE("has_edge(g) with single edge", "[has_edge][api]") { +TEST_CASE("has_edges(g) with single edge", "[has_edges][api]") { using Graph = compressed_graph; vector> edges_data = {{0, 1, 10}}; Graph g; g.load_edges(edges_data); - REQUIRE(has_edge(g) == true); + REQUIRE(has_edges(g) == true); } -TEST_CASE("has_edge(g) with self-loop", "[has_edge][api]") { +TEST_CASE("has_edges(g) with self-loop", "[has_edges][api]") { using Graph = compressed_graph; vector> edges_data = {{0, 0, 10}}; Graph g; g.load_edges(edges_data); - REQUIRE(has_edge(g) == true); + REQUIRE(has_edges(g) == true); } -TEST_CASE("has_edge(g) with const graph", "[has_edge][api]") { +TEST_CASE("has_edges(g) with const graph", "[has_edges][api]") { using Graph = compressed_graph; vector> edges_data = {{0, 1, 10}}; @@ -2399,29 +2399,29 @@ TEST_CASE("has_edge(g) with const graph", "[has_edge][api]") { temp_g.load_edges(edges_data); const Graph g = std::move(temp_g); - REQUIRE(has_edge(g) == true); + REQUIRE(has_edges(g) == true); } -TEST_CASE("has_edge(g) with const empty graph", "[has_edge][api]") { +TEST_CASE("has_edges(g) with const empty graph", "[has_edges][api]") { using Graph = compressed_graph; Graph temp_g; const Graph g = std::move(temp_g); - REQUIRE(has_edge(g) == false); + REQUIRE(has_edges(g) == false); } -TEST_CASE("has_edge(g) with void vertex values", "[has_edge][api]") { +TEST_CASE("has_edges(g) with void vertex values", "[has_edges][api]") { using Graph = compressed_graph; vector> edges_data = {{0, 1, 10}, {1, 2, 20}}; Graph g; g.load_edges(edges_data); - REQUIRE(has_edge(g) == true); + REQUIRE(has_edges(g) == true); } -TEST_CASE("has_edge(g) with string values", "[has_edge][api]") { +TEST_CASE("has_edges(g) with string values", "[has_edges][api]") { using Graph = compressed_graph; vector> edges_data = {{0, 1, "edge01"}}; vector> vertex_values = {{0, "v0"}, {1, "v1"}}; @@ -2430,22 +2430,22 @@ TEST_CASE("has_edge(g) with string values", "[has_edge][api]") { g.load_edges(edges_data); g.load_vertices(vertex_values); - REQUIRE(has_edge(g) == true); + REQUIRE(has_edges(g) == true); } -TEST_CASE("has_edge(g) return type is bool", "[has_edge][api]") { +TEST_CASE("has_edges(g) return type is bool", "[has_edges][api]") { using Graph = compressed_graph; vector> edges_data = {{0, 1, 10}}; Graph g; g.load_edges(edges_data); - auto result = has_edge(g); + auto result = has_edges(g); REQUIRE(std::is_same_v); REQUIRE(result == true); } -TEST_CASE("has_edge(g) with many edges", "[has_edge][api]") { +TEST_CASE("has_edges(g) with many edges", "[has_edges][api]") { using Graph = compressed_graph; vector> edges_data = {{0, 1, 1}, {0, 2, 1}, {1, 2, 1}, {1, 3, 1}, {2, 3, 1}, {3, 4, 1}, {4, 0, 1}}; @@ -2453,10 +2453,10 @@ TEST_CASE("has_edge(g) with many edges", "[has_edge][api]") { Graph g; g.load_edges(edges_data); - REQUIRE(has_edge(g) == true); + REQUIRE(has_edges(g) == true); } -TEST_CASE("has_edge(g) with disconnected components", "[has_edge][api]") { +TEST_CASE("has_edges(g) with disconnected components", "[has_edges][api]") { using Graph = compressed_graph; vector> edges_data = { {0, 1, 10}, {2, 3, 20} // Two separate components @@ -2465,10 +2465,10 @@ TEST_CASE("has_edge(g) with disconnected components", "[has_edge][api]") { Graph g; g.load_edges(edges_data); - REQUIRE(has_edge(g) == true); + REQUIRE(has_edges(g) == true); } -TEST_CASE("has_edge(g) with isolated vertex at beginning", "[has_edge][api]") { +TEST_CASE("has_edges(g) with isolated vertex at beginning", "[has_edges][api]") { using Graph = compressed_graph; // Vertex 0 has no edges, but vertex 1 does vector> edges_data = {{1, 2, 10}}; @@ -2476,10 +2476,10 @@ TEST_CASE("has_edge(g) with isolated vertex at beginning", "[has_edge][api]") { Graph g; g.load_edges(edges_data); - REQUIRE(has_edge(g) == true); + REQUIRE(has_edges(g) == true); } -TEST_CASE("has_edge(g) with complete graph", "[has_edge][api]") { +TEST_CASE("has_edges(g) with complete graph", "[has_edges][api]") { using Graph = compressed_graph; // Complete graph on 3 vertices vector> edges_data = {{0, 1, 1}, {0, 2, 1}, {1, 0, 1}, {1, 2, 1}, {2, 0, 1}, {2, 1, 1}}; @@ -2487,47 +2487,47 @@ TEST_CASE("has_edge(g) with complete graph", "[has_edge][api]") { Graph g; g.load_edges(edges_data); - REQUIRE(has_edge(g) == true); + REQUIRE(has_edges(g) == true); } -TEST_CASE("has_edge(g) with bidirectional edges", "[has_edge][api]") { +TEST_CASE("has_edges(g) with bidirectional edges", "[has_edges][api]") { using Graph = compressed_graph; vector> edges_data = {{0, 1, 10}, {1, 0, 20}}; Graph g; g.load_edges(edges_data); - REQUIRE(has_edge(g) == true); + REQUIRE(has_edges(g) == true); } -TEST_CASE("has_edge(g) with linear chain", "[has_edge][api]") { +TEST_CASE("has_edges(g) with linear chain", "[has_edges][api]") { using Graph = compressed_graph; vector> edges_data = {{0, 1, 10}, {1, 2, 20}, {2, 3, 30}}; Graph g; g.load_edges(edges_data); - REQUIRE(has_edge(g) == true); + REQUIRE(has_edges(g) == true); } -TEST_CASE("has_edge(g) with star graph", "[has_edge][api]") { +TEST_CASE("has_edges(g) with star graph", "[has_edges][api]") { using Graph = compressed_graph; vector> edges_data = {{0, 1, 10}, {0, 2, 20}, {0, 3, 30}}; Graph g; g.load_edges(edges_data); - REQUIRE(has_edge(g) == true); + REQUIRE(has_edges(g) == true); } -TEST_CASE("has_edge(g) with cycle", "[has_edge][api]") { +TEST_CASE("has_edges(g) with cycle", "[has_edges][api]") { using Graph = compressed_graph; vector> edges_data = {{0, 1, 10}, {1, 2, 20}, {2, 0, 30}}; Graph g; g.load_edges(edges_data); - REQUIRE(has_edge(g) == true); + REQUIRE(has_edges(g) == true); } // ============================================================================= diff --git a/tests/container/dynamic_graph/test_dynamic_edge_comparison.cpp b/tests/container/dynamic_graph/test_dynamic_edge_comparison.cpp index 318620f..3eafa02 100644 --- a/tests/container/dynamic_graph/test_dynamic_edge_comparison.cpp +++ b/tests/container/dynamic_graph/test_dynamic_edge_comparison.cpp @@ -1,11 +1,13 @@ /** * @file test_dynamic_edge_comparison.cpp - * @brief Tests for dynamic_edge comparison operators and std::hash - * - * Phase 4.1: Set Container Support Prerequisites - * Tests operator<=>, operator==, and std::hash for dynamic_edge - * - * These operators are required for using dynamic_edge with std::set (ordered) + * @brief Tests for dynamic_out_edge and dynamic_in_edge comparison operators and std::hash + * + * Phase 4.1 / Phase 5 refactor: Set Container Support Prerequisites + * Tests operator<=>, operator==, and std::hash for: + * - dynamic_out_edge (out-edges: compare by target_id) + * - dynamic_in_edge (in-edges: compare by source_id) + * + * These operators are required for using edges with std::set (ordered) * and std::unordered_set (unordered) edge containers. */ @@ -19,34 +21,38 @@ using namespace graph::container; -//================================================================================================== -// Test edge type definitions for all 4 specializations -//================================================================================================== +//============================================================================== +// Type aliases — dynamic_out_edge (out-edges, compare by target_id) +//============================================================================== -// EV != void, Sourced = true (primary template) -using edge_ev_sourced = - dynamic_edge>; +// EV != void +using edge_ev_out = + dynamic_out_edge>; -// EV = void, Sourced = true -using edge_void_sourced = - dynamic_edge>; +// EV = void +using edge_void_out = + dynamic_out_edge>; -// EV != void, Sourced = false -using edge_ev_unsourced = - dynamic_edge>; +//============================================================================== +// Type aliases — dynamic_in_edge (in-edges, compare by source_id) +//============================================================================== -// EV = void, Sourced = false -using edge_void_unsourced = - dynamic_edge>; +// EV != void +using edge_ev_in = + dynamic_in_edge>; -//================================================================================================== -// 1. operator<=> Tests - Sourced edges (compare by source_id, then target_id) -//================================================================================================== +// EV = void +using edge_void_in = + dynamic_in_edge>; -TEST_CASE("dynamic_edge operator<=> with Sourced=true, EV!=void", "[edge][comparison][spaceship]") { +//============================================================================== +// 1. operator<=> Tests - dynamic_out_edge (compare by target_id only) +//============================================================================== + +TEST_CASE("dynamic_out_edge operator<=> EV!=void", "[edge][comparison][spaceship]") { SECTION("equal edges") { - edge_ev_sourced e1(1, 2, 100); - edge_ev_sourced e2(1, 2, 200); // Different value, same ids + edge_ev_out e1(2, 100); + edge_ev_out e2(2, 200); // Different value, same target_id REQUIRE((e1 <=> e2) == std::strong_ordering::equal); REQUIRE(!(e1 < e2)); @@ -55,9 +61,9 @@ TEST_CASE("dynamic_edge operator<=> with Sourced=true, EV!=void", "[edge][compar REQUIRE(e1 >= e2); } - SECTION("less by source_id") { - edge_ev_sourced e1(1, 5, 100); - edge_ev_sourced e2(2, 3, 100); // Larger source_id but smaller target_id + SECTION("less by target_id") { + edge_ev_out e1(2, 100); + edge_ev_out e2(5, 100); REQUIRE((e1 <=> e2) == std::strong_ordering::less); REQUIRE(e1 < e2); @@ -66,9 +72,9 @@ TEST_CASE("dynamic_edge operator<=> with Sourced=true, EV!=void", "[edge][compar REQUIRE(!(e1 >= e2)); } - SECTION("greater by source_id") { - edge_ev_sourced e1(3, 1, 100); - edge_ev_sourced e2(2, 5, 100); + SECTION("greater by target_id") { + edge_ev_out e1(7, 100); + edge_ev_out e2(3, 100); REQUIRE((e1 <=> e2) == std::strong_ordering::greater); REQUIRE(!(e1 < e2)); @@ -76,374 +82,382 @@ TEST_CASE("dynamic_edge operator<=> with Sourced=true, EV!=void", "[edge][compar REQUIRE(!(e1 <= e2)); REQUIRE(e1 >= e2); } - - SECTION("same source_id, less by target_id") { - edge_ev_sourced e1(1, 2, 100); - edge_ev_sourced e2(1, 3, 100); - - REQUIRE((e1 <=> e2) == std::strong_ordering::less); - REQUIRE(e1 < e2); - } - - SECTION("same source_id, greater by target_id") { - edge_ev_sourced e1(1, 5, 100); - edge_ev_sourced e2(1, 3, 100); - - REQUIRE((e1 <=> e2) == std::strong_ordering::greater); - REQUIRE(e1 > e2); - } } -TEST_CASE("dynamic_edge operator<=> with Sourced=true, EV=void", "[edge][comparison][spaceship]") { +TEST_CASE("dynamic_out_edge operator<=> EV=void", "[edge][comparison][spaceship]") { SECTION("equal edges") { - edge_void_sourced e1(1, 2); - edge_void_sourced e2(1, 2); + edge_void_out e1(5); + edge_void_out e2(5); REQUIRE((e1 <=> e2) == std::strong_ordering::equal); } - SECTION("ordering by source_id first, then target_id") { - edge_void_sourced e1(1, 2); - edge_void_sourced e2(2, 1); - - REQUIRE(e1 < e2); // source_id 1 < 2 - } - - SECTION("same source, different target") { - edge_void_sourced e1(1, 3); - edge_void_sourced e2(1, 2); + SECTION("ordering by target_id") { + edge_void_out e1(3); + edge_void_out e2(7); - REQUIRE(e1 > e2); // target_id 3 > 2 + REQUIRE(e1 < e2); } } -//================================================================================================== -// 2. operator<=> Tests - Unsourced edges (compare by target_id only) -//================================================================================================== +//============================================================================== +// 2. operator<=> Tests - dynamic_in_edge (compare by source_id only) +//============================================================================== -TEST_CASE("dynamic_edge operator<=> with Sourced=false, EV!=void", "[edge][comparison][spaceship]") { +TEST_CASE("dynamic_in_edge operator<=> EV!=void", "[edge][comparison][spaceship]") { SECTION("equal edges") { - edge_ev_unsourced e1(2, 100); - edge_ev_unsourced e2(2, 200); // Different value, same target_id + edge_ev_in e1(1, 100); + edge_ev_in e2(1, 200); // Different value, same source_id REQUIRE((e1 <=> e2) == std::strong_ordering::equal); + REQUIRE(!(e1 < e2)); + REQUIRE(!(e1 > e2)); + REQUIRE(e1 <= e2); + REQUIRE(e1 >= e2); } - SECTION("less by target_id") { - edge_ev_unsourced e1(2, 100); - edge_ev_unsourced e2(5, 100); + SECTION("less by source_id") { + edge_ev_in e1(1, 100); + edge_ev_in e2(2, 100); + REQUIRE((e1 <=> e2) == std::strong_ordering::less); REQUIRE(e1 < e2); + REQUIRE(!(e1 > e2)); + REQUIRE(e1 <= e2); + REQUIRE(!(e1 >= e2)); } - SECTION("greater by target_id") { - edge_ev_unsourced e1(7, 100); - edge_ev_unsourced e2(3, 100); + SECTION("greater by source_id") { + edge_ev_in e1(3, 100); + edge_ev_in e2(2, 100); + REQUIRE((e1 <=> e2) == std::strong_ordering::greater); + REQUIRE(!(e1 < e2)); REQUIRE(e1 > e2); + REQUIRE(!(e1 <= e2)); + REQUIRE(e1 >= e2); } } -TEST_CASE("dynamic_edge operator<=> with Sourced=false, EV=void", "[edge][comparison][spaceship]") { +TEST_CASE("dynamic_in_edge operator<=> EV=void", "[edge][comparison][spaceship]") { SECTION("equal edges") { - edge_void_unsourced e1(5); - edge_void_unsourced e2(5); + edge_void_in e1(2); + edge_void_in e2(2); REQUIRE((e1 <=> e2) == std::strong_ordering::equal); } - SECTION("ordering by target_id") { - edge_void_unsourced e1(3); - edge_void_unsourced e2(7); + SECTION("ordering by source_id") { + edge_void_in e1(1); + edge_void_in e2(2); - REQUIRE(e1 < e2); + REQUIRE(e1 < e2); // source_id 1 < 2 } -} - -//================================================================================================== -// 3. operator== Tests -//================================================================================================== -TEST_CASE("dynamic_edge operator== with Sourced=true", "[edge][comparison][equality]") { - SECTION("EV != void - equal edges with different values") { - edge_ev_sourced e1(1, 2, 100); - edge_ev_sourced e2(1, 2, 999); // Different value + SECTION("reverse order") { + edge_void_in e1(3); + edge_void_in e2(1); - REQUIRE(e1 == e2); // Compares only source_id and target_id + REQUIRE(e1 > e2); } +} - SECTION("EV != void - unequal by source_id") { - edge_ev_sourced e1(1, 2, 100); - edge_ev_sourced e2(3, 2, 100); +//============================================================================== +// 3. operator== Tests - dynamic_out_edge +//============================================================================== - REQUIRE(!(e1 == e2)); - REQUIRE(e1 != e2); +TEST_CASE("dynamic_out_edge operator==", "[edge][comparison][equality]") { + SECTION("EV != void - equal edges with different values") { + edge_ev_out e1(2, 100); + edge_ev_out e2(2, 999); // Different value + + REQUIRE(e1 == e2); // compares only target_id } SECTION("EV != void - unequal by target_id") { - edge_ev_sourced e1(1, 2, 100); - edge_ev_sourced e2(1, 5, 100); + edge_ev_out e1(2, 100); + edge_ev_out e2(5, 100); REQUIRE(!(e1 == e2)); REQUIRE(e1 != e2); } SECTION("EV = void - equal edges") { - edge_void_sourced e1(1, 2); - edge_void_sourced e2(1, 2); + edge_void_out e1(5); + edge_void_out e2(5); REQUIRE(e1 == e2); } SECTION("EV = void - unequal edges") { - edge_void_sourced e1(1, 2); - edge_void_sourced e2(1, 3); + edge_void_out e1(5); + edge_void_out e2(7); REQUIRE(e1 != e2); } } -TEST_CASE("dynamic_edge operator== with Sourced=false", "[edge][comparison][equality]") { +//============================================================================== +// 4. operator== Tests - dynamic_in_edge +//============================================================================== + +TEST_CASE("dynamic_in_edge operator==", "[edge][comparison][equality]") { SECTION("EV != void - equal edges with different values") { - edge_ev_unsourced e1(2, 100); - edge_ev_unsourced e2(2, 999); + edge_ev_in e1(1, 100); + edge_ev_in e2(1, 999); // Different value - REQUIRE(e1 == e2); + REQUIRE(e1 == e2); // compares only source_id } - SECTION("EV != void - unequal by target_id") { - edge_ev_unsourced e1(2, 100); - edge_ev_unsourced e2(5, 100); + SECTION("EV != void - unequal by source_id") { + edge_ev_in e1(1, 100); + edge_ev_in e2(3, 100); + REQUIRE(!(e1 == e2)); REQUIRE(e1 != e2); } SECTION("EV = void - equal edges") { - edge_void_unsourced e1(5); - edge_void_unsourced e2(5); + edge_void_in e1(2); + edge_void_in e2(2); REQUIRE(e1 == e2); } SECTION("EV = void - unequal edges") { - edge_void_unsourced e1(5); - edge_void_unsourced e2(7); + edge_void_in e1(2); + edge_void_in e2(3); REQUIRE(e1 != e2); } } -//================================================================================================== -// 4. std::hash Tests -//================================================================================================== +//============================================================================== +// 5. std::hash Tests - dynamic_out_edge +//============================================================================== -TEST_CASE("std::hash for dynamic_edge with Sourced=true", "[edge][hash]") { +TEST_CASE("std::hash for dynamic_out_edge", "[edge][hash]") { SECTION("EV != void - equal edges have same hash") { - edge_ev_sourced e1(1, 2, 100); - edge_ev_sourced e2(1, 2, 999); // Different value + edge_ev_out e1(2, 100); + edge_ev_out e2(2, 999); // Different value - std::hash hasher; + std::hash hasher; REQUIRE(hasher(e1) == hasher(e2)); } - SECTION("EV != void - different edges likely have different hash") { - edge_ev_sourced e1(1, 2, 100); - edge_ev_sourced e2(1, 3, 100); // Different target - edge_ev_sourced e3(2, 2, 100); // Different source + SECTION("EV != void - different target_ids likely have different hash") { + edge_ev_out e1(2, 100); + edge_ev_out e2(5, 100); - std::hash hasher; - // Note: Different hash is not guaranteed but highly likely + std::hash hasher; REQUIRE(hasher(e1) != hasher(e2)); - REQUIRE(hasher(e1) != hasher(e3)); } SECTION("EV = void - equal edges have same hash") { - edge_void_sourced e1(1, 2); - edge_void_sourced e2(1, 2); + edge_void_out e1(5); + edge_void_out e2(5); - std::hash hasher; + std::hash hasher; REQUIRE(hasher(e1) == hasher(e2)); } + + SECTION("EV = void - different edges likely have different hash") { + edge_void_out e1(5); + edge_void_out e2(7); + + std::hash hasher; + REQUIRE(hasher(e1) != hasher(e2)); + } } -TEST_CASE("std::hash for dynamic_edge with Sourced=false", "[edge][hash]") { +//============================================================================== +// 6. std::hash Tests - dynamic_in_edge +//============================================================================== + +TEST_CASE("std::hash for dynamic_in_edge", "[edge][hash]") { SECTION("EV != void - equal edges have same hash") { - edge_ev_unsourced e1(2, 100); - edge_ev_unsourced e2(2, 999); + edge_ev_in e1(1, 100); + edge_ev_in e2(1, 999); // Different value - std::hash hasher; + std::hash hasher; REQUIRE(hasher(e1) == hasher(e2)); } - SECTION("EV = void - equal edges have same hash") { - edge_void_unsourced e1(5); - edge_void_unsourced e2(5); + SECTION("EV != void - different source_ids likely have different hash") { + edge_ev_in e1(1, 100); + edge_ev_in e2(3, 100); - std::hash hasher; - REQUIRE(hasher(e1) == hasher(e2)); + std::hash hasher; + REQUIRE(hasher(e1) != hasher(e2)); } - SECTION("EV = void - different edges likely have different hash") { - edge_void_unsourced e1(5); - edge_void_unsourced e2(7); + SECTION("EV = void - equal edges have same hash") { + edge_void_in e1(2); + edge_void_in e2(2); - std::hash hasher; - REQUIRE(hasher(e1) != hasher(e2)); + std::hash hasher; + REQUIRE(hasher(e1) == hasher(e2)); } } -//================================================================================================== -// 5. Integration with std::set (requires operator<=>) -//================================================================================================== +//============================================================================== +// 7. Integration with std::set (requires operator<=>) +//============================================================================== -TEST_CASE("dynamic_edge works with std::set", "[edge][set][integration]") { - SECTION("Sourced edges - deduplicates by source_id and target_id") { - std::set edge_set; +TEST_CASE("dynamic_out_edge works with std::set", "[edge][set][integration]") { + SECTION("deduplicates by target_id") { + std::set edge_set; - edge_set.insert(edge_ev_sourced(1, 2, 100)); - edge_set.insert(edge_ev_sourced(1, 2, 999)); // Duplicate (same ids) - edge_set.insert(edge_ev_sourced(1, 3, 100)); - edge_set.insert(edge_ev_sourced(2, 1, 100)); + edge_set.insert(edge_ev_out(2, 100)); + edge_set.insert(edge_ev_out(2, 999)); // Duplicate (same target_id) + edge_set.insert(edge_ev_out(5, 100)); + edge_set.insert(edge_ev_out(3, 100)); - REQUIRE(edge_set.size() == 3); // Only 3 unique edges + REQUIRE(edge_set.size() == 3); } - SECTION("Sourced edges - maintains sorted order") { - std::set edge_set; + SECTION("maintains sorted order by target_id") { + std::set edge_set; - edge_set.insert(edge_void_sourced(2, 3)); - edge_set.insert(edge_void_sourced(1, 2)); - edge_set.insert(edge_void_sourced(1, 3)); - edge_set.insert(edge_void_sourced(2, 1)); + edge_set.insert(edge_void_out(5)); + edge_set.insert(edge_void_out(2)); + edge_set.insert(edge_void_out(8)); + edge_set.insert(edge_void_out(1)); - std::vector> expected = {{1, 2}, {1, 3}, {2, 1}, {2, 3}}; + std::vector expected = {1, 2, 5, 8}; size_t i = 0; for (const auto& e : edge_set) { - REQUIRE(e.source_id() == expected[i].first); - REQUIRE(e.target_id() == expected[i].second); + REQUIRE(e.target_id() == expected[i]); ++i; } } +} - SECTION("Unsourced edges - deduplicates by target_id") { - std::set edge_set; +TEST_CASE("dynamic_in_edge works with std::set", "[edge][set][integration]") { + SECTION("deduplicates by source_id") { + std::set edge_set; - edge_set.insert(edge_ev_unsourced(2, 100)); - edge_set.insert(edge_ev_unsourced(2, 999)); // Duplicate - edge_set.insert(edge_ev_unsourced(5, 100)); - edge_set.insert(edge_ev_unsourced(3, 100)); + edge_set.insert(edge_ev_in(1, 100)); + edge_set.insert(edge_ev_in(1, 999)); // Duplicate (same source_id) + edge_set.insert(edge_ev_in(2, 100)); + edge_set.insert(edge_ev_in(3, 100)); REQUIRE(edge_set.size() == 3); } - SECTION("Unsourced edges - maintains sorted order") { - std::set edge_set; + SECTION("maintains sorted order by source_id") { + std::set edge_set; - edge_set.insert(edge_void_unsourced(5)); - edge_set.insert(edge_void_unsourced(2)); - edge_set.insert(edge_void_unsourced(8)); - edge_set.insert(edge_void_unsourced(1)); + edge_set.insert(edge_void_in(3)); + edge_set.insert(edge_void_in(1)); + edge_set.insert(edge_void_in(4)); + edge_set.insert(edge_void_in(2)); - std::vector expected = {1, 2, 5, 8}; + std::vector expected = {1, 2, 3, 4}; size_t i = 0; for (const auto& e : edge_set) { - REQUIRE(e.target_id() == expected[i]); + REQUIRE(e.source_id() == expected[i]); ++i; } } } -//================================================================================================== -// 6. Integration with std::unordered_set (requires operator== and std::hash) -//================================================================================================== +//============================================================================== +// 8. Integration with std::unordered_set +//============================================================================== -TEST_CASE("dynamic_edge works with std::unordered_set", "[edge][unordered_set][integration]") { - SECTION("Sourced edges - deduplicates by source_id and target_id") { - std::unordered_set edge_set; +TEST_CASE("dynamic_out_edge works with std::unordered_set", + "[edge][unordered_set][integration]") { + SECTION("deduplicates by target_id") { + std::unordered_set edge_set; - edge_set.insert(edge_ev_sourced(1, 2, 100)); - edge_set.insert(edge_ev_sourced(1, 2, 999)); // Duplicate - edge_set.insert(edge_ev_sourced(1, 3, 100)); - edge_set.insert(edge_ev_sourced(2, 1, 100)); + edge_set.insert(edge_ev_out(2, 100)); + edge_set.insert(edge_ev_out(2, 999)); // Duplicate + edge_set.insert(edge_ev_out(5, 100)); - REQUIRE(edge_set.size() == 3); + REQUIRE(edge_set.size() == 2); } - SECTION("Sourced edges - find works correctly") { - std::unordered_set edge_set; + SECTION("find works correctly") { + std::unordered_set edge_set; - edge_set.insert(edge_void_sourced(1, 2)); - edge_set.insert(edge_void_sourced(2, 3)); + edge_set.insert(edge_void_out(3)); + edge_set.insert(edge_void_out(7)); - REQUIRE(edge_set.find(edge_void_sourced(1, 2)) != edge_set.end()); - REQUIRE(edge_set.find(edge_void_sourced(1, 5)) == edge_set.end()); + REQUIRE(edge_set.find(edge_void_out(3)) != edge_set.end()); + REQUIRE(edge_set.find(edge_void_out(5)) == edge_set.end()); } +} - SECTION("Unsourced edges - deduplicates by target_id") { - std::unordered_set edge_set; +TEST_CASE("dynamic_in_edge works with std::unordered_set", + "[edge][unordered_set][integration]") { + SECTION("deduplicates by source_id") { + std::unordered_set edge_set; - edge_set.insert(edge_ev_unsourced(2, 100)); - edge_set.insert(edge_ev_unsourced(2, 999)); // Duplicate - edge_set.insert(edge_ev_unsourced(5, 100)); + edge_set.insert(edge_ev_in(1, 100)); + edge_set.insert(edge_ev_in(1, 999)); // Duplicate + edge_set.insert(edge_ev_in(3, 100)); REQUIRE(edge_set.size() == 2); } - SECTION("Unsourced edges - find works correctly") { - std::unordered_set edge_set; + SECTION("find works correctly") { + std::unordered_set edge_set; - edge_set.insert(edge_void_unsourced(3)); - edge_set.insert(edge_void_unsourced(7)); + edge_set.insert(edge_void_in(1)); + edge_set.insert(edge_void_in(2)); - REQUIRE(edge_set.find(edge_void_unsourced(3)) != edge_set.end()); - REQUIRE(edge_set.find(edge_void_unsourced(5)) == edge_set.end()); + REQUIRE(edge_set.find(edge_void_in(1)) != edge_set.end()); + REQUIRE(edge_set.find(edge_void_in(5)) == edge_set.end()); } } -//================================================================================================== -// 7. Edge case tests -//================================================================================================== +//============================================================================== +// 9. Edge case tests +//============================================================================== -TEST_CASE("dynamic_edge comparison edge cases", "[edge][comparison][edge-cases]") { - SECTION("default constructed edges are equal") { - edge_void_unsourced e1; - edge_void_unsourced e2; +TEST_CASE("dynamic edge comparison edge cases", "[edge][comparison][edge-cases]") { + SECTION("default constructed out-edges are equal") { + edge_void_out e1; + edge_void_out e2; REQUIRE(e1 == e2); REQUIRE((e1 <=> e2) == std::strong_ordering::equal); } - SECTION("edge with id 0") { - edge_void_unsourced e1(0); - edge_void_unsourced e2(0); + SECTION("out-edge with target_id 0") { + edge_void_out e1(0); + edge_void_out e2(0); REQUIRE(e1 == e2); - REQUIRE(std::hash{}(e1) == std::hash{}(e2)); + REQUIRE(std::hash{}(e1) == std::hash{}(e2)); } - SECTION("large vertex ids") { - uint32_t max_id = std::numeric_limits::max(); - edge_void_sourced e1(max_id, max_id); - edge_void_sourced e2(max_id, max_id); + SECTION("in-edge with source_id 0") { + edge_void_in e1(0); + edge_void_in e2(0); REQUIRE(e1 == e2); - REQUIRE(std::hash{}(e1) == std::hash{}(e2)); + REQUIRE(std::hash{}(e1) == std::hash{}(e2)); } - SECTION("self-loop edges") { - edge_void_sourced e1(5, 5); - edge_void_sourced e2(5, 5); + SECTION("out-edge large vertex ids") { + uint32_t max_id = std::numeric_limits::max(); + edge_void_out e1(max_id); + edge_void_out e2(max_id); REQUIRE(e1 == e2); + REQUIRE(std::hash{}(e1) == std::hash{}(e2)); } - SECTION("reverse edges are not equal for sourced") { - edge_void_sourced e1(1, 2); - edge_void_sourced e2(2, 1); + SECTION("in-edge large vertex ids") { + uint32_t max_id = std::numeric_limits::max(); + edge_void_in e1(max_id); + edge_void_in e2(max_id); - REQUIRE(e1 != e2); - REQUIRE(e1 < e2); // (1, 2) < (2, 1) by source_id + REQUIRE(e1 == e2); + REQUIRE(std::hash{}(e1) == std::hash{}(e2)); } } diff --git a/tests/container/dynamic_graph/test_dynamic_graph_bidirectional.cpp b/tests/container/dynamic_graph/test_dynamic_graph_bidirectional.cpp new file mode 100644 index 0000000..eabb2b9 --- /dev/null +++ b/tests/container/dynamic_graph/test_dynamic_graph_bidirectional.cpp @@ -0,0 +1,926 @@ +/** + * @file test_dynamic_graph_bidirectional.cpp + * @brief Comprehensive tests for dynamic_graph with Bidirectional=true + * + * Phase 8.5: Verifies that dynamic_graph<..., Bidirectional=true> satisfies + * bidirectional_adjacency_list, populates in_edges during load_edges, + * works with in_edges/in_degree CPOs, views (in_incidence, in_neighbors), + * and that Bidirectional=false mode is completely unchanged. + * + * Tests use two trait types (vov and vol) as required by the plan. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace graph::container; +using graph::copyable_vertex_t; + +// ============================================================================ +// Non-uniform bidirectional traits +// +// Uniform traits (vov_graph_traits<..., true>) store dynamic_out_edge for +// in-edges, which lacks source_id(). Non-uniform traits define +// in_edge_type = dynamic_in_edge so that source_id(g, ie) works via the +// native edge member CPO tier and the bidirectional_adjacency_list concept +// is satisfied. +// ============================================================================ + +template +struct vov_bidir_graph_traits { + using edge_value_type = EV; + using vertex_value_type = VV; + using graph_value_type = GV; + using vertex_id_type = VId; + static constexpr bool bidirectional = true; + + using edge_type = dynamic_out_edge; + using in_edge_type = dynamic_in_edge; + using vertex_type = dynamic_vertex; + using graph_type = dynamic_graph; + + using edges_type = std::vector; + using in_edges_type = std::vector; + using vertices_type = std::vector; +}; + +template +struct vol_bidir_graph_traits { + using edge_value_type = EV; + using vertex_value_type = VV; + using graph_value_type = GV; + using vertex_id_type = VId; + static constexpr bool bidirectional = true; + + using edge_type = dynamic_out_edge; + using in_edge_type = dynamic_in_edge; + using vertex_type = dynamic_vertex; + using graph_type = dynamic_graph; + + using edges_type = std::list; + using in_edges_type = std::list; + using vertices_type = std::vector; +}; + +// ============================================================================ +// Type aliases — Bidirectional graphs using non-uniform traits +// ============================================================================ + +// vov: vector vertices + vector edges — EV=int for weighted edges +using bidir_vov_int = + dynamic_graph>; + +// vov: void edge value (unweighted) +using bidir_vov_void = + dynamic_graph>; + +// vol: vector vertices + list edges — EV=int for weighted edges +using bidir_vol_int = + dynamic_graph>; + +// vol: void edge value (unweighted) +using bidir_vol_void = + dynamic_graph>; + +// vov with vertex value +using bidir_vov_int_vv = + dynamic_graph>; + +// ============================================================================ +// Non-bidirectional baselines for regression comparison +// ============================================================================ + +using nonbidir_vov = + dynamic_graph>; + +using nonbidir_vol = + dynamic_graph>; + +// ============================================================================ +// CPOs in scope +// ============================================================================ +using graph::adj_list::vertices; +using graph::adj_list::num_vertices; +using graph::adj_list::find_vertex; +using graph::adj_list::vertex_id; +using graph::adj_list::edges; +using graph::adj_list::degree; +using graph::adj_list::target_id; +using graph::adj_list::source_id; +using graph::adj_list::edge_value; +using graph::adj_list::in_edges; +using graph::adj_list::in_degree; +using graph::adj_list::find_in_edge; +using graph::adj_list::contains_in_edge; +using graph::adj_list::find_vertex_edge; +using graph::adj_list::contains_edge; + +// ============================================================================ +// Helper: build a small directed graph for testing +// +// 0 --10--> 1 --20--> 2 +// | ^ +// +--------30----------+ +// +// Edges: (0,1,10), (0,2,30), (1,2,20) +// Expected in_edges: +// vertex 0: none +// vertex 1: {from 0, weight 10} +// vertex 2: {from 0, weight 30}, {from 1, weight 20} +// ============================================================================ + +struct test_edge { + uint32_t source_id; + uint32_t target_id; + int value; +}; + +static const std::vector triangle_edges = { + {0, 1, 10}, + {0, 2, 30}, + {1, 2, 20}, +}; + +template +G make_triangle_graph() { + G g; + g.load_edges(triangle_edges, std::identity{}); + return g; +} + +// ============================================================================ +// 1. Concept satisfaction +// ============================================================================ + +TEST_CASE("bidir dynamic_graph models bidirectional_adjacency_list", + "[dynamic_graph][bidirectional][concept]") { + + SECTION("vov Bidirectional=true satisfies concept") { + static_assert(graph::adj_list::bidirectional_adjacency_list, + "bidir_vov_int must model bidirectional_adjacency_list"); + static_assert(graph::adj_list::index_bidirectional_adjacency_list, + "bidir_vov_int must model index_bidirectional_adjacency_list"); + } + + SECTION("vol Bidirectional=true satisfies concept") { + static_assert(graph::adj_list::bidirectional_adjacency_list, + "bidir_vol_int must model bidirectional_adjacency_list"); + static_assert(graph::adj_list::index_bidirectional_adjacency_list, + "bidir_vol_int must model index_bidirectional_adjacency_list"); + } + + SECTION("void edge value Bidirectional=true satisfies concept") { + static_assert(graph::adj_list::bidirectional_adjacency_list); + static_assert(graph::adj_list::bidirectional_adjacency_list); + } + + SECTION("graph namespace re-exports") { + static_assert(graph::bidirectional_adjacency_list); + static_assert(graph::index_bidirectional_adjacency_list); + } +} + +// ============================================================================ +// 2. Non-bidirectional unchanged (no regressions) +// ============================================================================ + +TEST_CASE("non-bidir dynamic_graph does NOT model bidirectional_adjacency_list", + "[dynamic_graph][bidirectional][concept][nonbidir]") { + + static_assert(!graph::adj_list::bidirectional_adjacency_list, + "Bidirectional=false must not model bidirectional_adjacency_list"); + static_assert(!graph::adj_list::bidirectional_adjacency_list, + "Bidirectional=false must not model bidirectional_adjacency_list"); +} + +TEST_CASE("non-bidir dynamic_graph works identically to before", + "[dynamic_graph][bidirectional][nonbidir]") { + nonbidir_vov g; + g.load_edges(triangle_edges, std::identity{}); + + REQUIRE(num_vertices(g) == 3); + + // Outgoing edges work as usual + size_t edge_count = 0; + for (auto v : vertices(g)) { + for ([[maybe_unused]] auto e : edges(g, v)) { + ++edge_count; + } + } + REQUIRE(edge_count == 3); + + // source_id works (bidirectional graph stores source in out-edges) + for (auto v : vertices(g)) { + auto uid = vertex_id(g, v); + for (auto e : edges(g, v)) { + REQUIRE(source_id(g, e) == uid); + } + } +} + +// ============================================================================ +// 3. Basic bidirectional graph construction +// ============================================================================ + +TEMPLATE_TEST_CASE("bidir basic construction and in_edges", + "[dynamic_graph][bidirectional][in_edges]", + bidir_vov_int, bidir_vol_int) { + auto g = make_triangle_graph(); + + REQUIRE(num_vertices(g) == 3); + + SECTION("vertex 0 has no incoming edges") { + auto uid = uint32_t{0}; + auto u = *find_vertex(g, uid); + size_t count = 0; + for ([[maybe_unused]] auto ie : in_edges(g, u)) { + ++count; + } + REQUIRE(count == 0); + } + + SECTION("vertex 1 has 1 incoming edge from vertex 0") { + auto uid = uint32_t{1}; + auto u = *find_vertex(g, uid); + std::vector sources; + for (auto ie : in_edges(g, u)) { + sources.push_back(source_id(g, ie)); + } + REQUIRE(sources.size() == 1); + REQUIRE(sources[0] == 0); + } + + SECTION("vertex 2 has 2 incoming edges from vertices 0 and 1") { + auto uid = uint32_t{2}; + auto u = *find_vertex(g, uid); + std::set sources; + for (auto ie : in_edges(g, u)) { + sources.insert(source_id(g, ie)); + } + REQUIRE(sources.size() == 2); + REQUIRE(sources.count(0) == 1); + REQUIRE(sources.count(1) == 1); + } +} + +// ============================================================================ +// 4. in_edges by vertex id +// ============================================================================ + +TEMPLATE_TEST_CASE("bidir in_edges by vertex id", + "[dynamic_graph][bidirectional][in_edges][uid]", + bidir_vov_int, bidir_vol_int) { + auto g = make_triangle_graph(); + + auto uid = uint32_t{2}; + std::set sources; + for (auto ie : in_edges(g, uid)) { + sources.insert(source_id(g, ie)); + } + REQUIRE(sources.size() == 2); + REQUIRE(sources.count(0) == 1); + REQUIRE(sources.count(1) == 1); +} + +// ============================================================================ +// 5. in_degree matches expected +// ============================================================================ + +TEMPLATE_TEST_CASE("bidir in_degree matches expected", + "[dynamic_graph][bidirectional][in_degree]", + bidir_vov_int, bidir_vol_int) { + auto g = make_triangle_graph(); + + SECTION("in_degree by vertex descriptor") { + std::vector expected_in_deg = {0, 1, 2}; + for (auto v : vertices(g)) { + auto uid = vertex_id(g, v); + REQUIRE(in_degree(g, v) == expected_in_deg[uid]); + } + } + + SECTION("in_degree by vertex id") { + REQUIRE(in_degree(g, uint32_t{0}) == 0); + REQUIRE(in_degree(g, uint32_t{1}) == 1); + REQUIRE(in_degree(g, uint32_t{2}) == 2); + } +} + +// ============================================================================ +// 6. source_id and target_id on in_edges +// ============================================================================ + +TEMPLATE_TEST_CASE("bidir in_edges carry correct source_id and target_id", + "[dynamic_graph][bidirectional][source_id][target_id]", + bidir_vov_int, bidir_vol_int) { + auto g = make_triangle_graph(); + + // For each vertex, the in_edges should have target_id == vertex's own id + // and source_id == the origin of the original forward edge + for (auto v : vertices(g)) { + auto uid = vertex_id(g, v); + for (auto ie : in_edges(g, v)) { + REQUIRE(target_id(g, ie) == uid); + // source_id should be a valid vertex + auto sid = source_id(g, ie); + REQUIRE(sid < num_vertices(g)); + REQUIRE(sid != uid); // no self-loops in our test graph + } + } +} + +// ============================================================================ +// 7. edge_value on in_edges (weighted graph) +// ============================================================================ + +TEST_CASE("bidir in_edges carry correct edge values", + "[dynamic_graph][bidirectional][edge_value]") { + auto g = make_triangle_graph(); + + // Build expected: for each (src, tgt) -> weight + std::map, int> expected; + for (auto& e : triangle_edges) { + expected[{e.source_id, e.target_id}] = e.value; + } + + // Check in_edges carry the same values + for (auto v : vertices(g)) { + auto uid = vertex_id(g, v); + for (auto ie : in_edges(g, v)) { + auto sid = source_id(g, ie); + auto key = std::pair{sid, uid}; + REQUIRE(expected.count(key) == 1); + REQUIRE(edge_value(g, ie) == expected[key]); + } + } +} + +// ============================================================================ +// 8. void edge value (unweighted bidir) +// ============================================================================ + +TEST_CASE("bidir void edge value works (unweighted)", + "[dynamic_graph][bidirectional][void_ev]") { + struct edge_data { + uint32_t source_id; + uint32_t target_id; + }; + std::vector edge_list = {{0, 1}, {0, 2}, {1, 2}}; + + bidir_vov_void g; + g.load_edges(edge_list, std::identity{}); + + REQUIRE(num_vertices(g) == 3); + REQUIRE(in_degree(g, uint32_t{0}) == 0); + REQUIRE(in_degree(g, uint32_t{1}) == 1); + REQUIRE(in_degree(g, uint32_t{2}) == 2); + + // source_id still works on in_edges + auto u2 = *find_vertex(g, uint32_t{2}); + std::set sources; + for (auto ie : in_edges(g, u2)) { + sources.insert(source_id(g, ie)); + } + REQUIRE(sources == std::set{0, 1}); +} + +// ============================================================================ +// 9. find_in_edge and contains_in_edge +// ============================================================================ + +TEMPLATE_TEST_CASE("bidir find_in_edge and contains_in_edge", + "[dynamic_graph][bidirectional][find_in_edge]", + bidir_vov_int, bidir_vol_int) { + auto g = make_triangle_graph(); + + SECTION("contains_in_edge for existing edges") { + // Edge (0,1) exists => vertex 1 should contain in_edge from 0 + REQUIRE(contains_in_edge(g, uint32_t{1}, uint32_t{0}) == true); + // Edge (0,2) exists => vertex 2 should contain in_edge from 0 + REQUIRE(contains_in_edge(g, uint32_t{2}, uint32_t{0}) == true); + // Edge (1,2) exists => vertex 2 should contain in_edge from 1 + REQUIRE(contains_in_edge(g, uint32_t{2}, uint32_t{1}) == true); + } + + SECTION("contains_in_edge for non-existing edges") { + // No edge (1,0) => vertex 0 should NOT contain in_edge from 1 + REQUIRE(contains_in_edge(g, uint32_t{0}, uint32_t{1}) == false); + // No edge (2,0) => vertex 0 should NOT contain in_edge from 2 + REQUIRE(contains_in_edge(g, uint32_t{0}, uint32_t{2}) == false); + // No edge (2,1) => vertex 1 should NOT contain in_edge from 2 + REQUIRE(contains_in_edge(g, uint32_t{1}, uint32_t{2}) == false); + } + + SECTION("find_in_edge returns valid edge for existing edge") { + auto ie = find_in_edge(g, uint32_t{2}, uint32_t{0}); + REQUIRE(source_id(g, ie) == 0); + REQUIRE(target_id(g, ie) == 2); + } + + SECTION("find_in_edge for a different existing edge") { + auto ie = find_in_edge(g, uint32_t{2}, uint32_t{1}); + REQUIRE(source_id(g, ie) == 1); + REQUIRE(target_id(g, ie) == 2); + } +} + +// ============================================================================ +// 10. Const graph access +// ============================================================================ + +TEST_CASE("bidir in_edges on const graph", + "[dynamic_graph][bidirectional][const]") { + auto g = make_triangle_graph(); + + const auto& cg = g; + + // in_edges on const graph + auto u2 = *find_vertex(cg, uint32_t{2}); + size_t count = 0; + for ([[maybe_unused]] auto ie : in_edges(cg, u2)) { + ++count; + } + REQUIRE(count == 2); + + // in_degree on const graph + REQUIRE(in_degree(cg, u2) == 2); +} + +// ============================================================================ +// 11. Copy and move semantics +// ============================================================================ + +TEST_CASE("bidir copy preserves reverse adjacency", + "[dynamic_graph][bidirectional][copy]") { + auto g1 = make_triangle_graph(); + + // Copy constructor + bidir_vov_int g2 = g1; + + REQUIRE(num_vertices(g2) == 3); + REQUIRE(in_degree(g2, uint32_t{0}) == 0); + REQUIRE(in_degree(g2, uint32_t{1}) == 1); + REQUIRE(in_degree(g2, uint32_t{2}) == 2); + + // Verify source_ids survive the copy + auto u2 = *find_vertex(g2, uint32_t{2}); + std::set sources; + for (auto ie : in_edges(g2, u2)) { + sources.insert(source_id(g2, ie)); + } + REQUIRE(sources == std::set{0, 1}); +} + +TEST_CASE("bidir move preserves reverse adjacency", + "[dynamic_graph][bidirectional][move]") { + auto g1 = make_triangle_graph(); + + // Move constructor + bidir_vov_int g2 = std::move(g1); + + REQUIRE(num_vertices(g2) == 3); + REQUIRE(in_degree(g2, uint32_t{0}) == 0); + REQUIRE(in_degree(g2, uint32_t{1}) == 1); + REQUIRE(in_degree(g2, uint32_t{2}) == 2); + + auto u2 = *find_vertex(g2, uint32_t{2}); + std::set sources; + for (auto ie : in_edges(g2, u2)) { + sources.insert(source_id(g2, ie)); + } + REQUIRE(sources == std::set{0, 1}); +} + +TEST_CASE("bidir copy assignment preserves reverse adjacency", + "[dynamic_graph][bidirectional][copy_assign]") { + auto g1 = make_triangle_graph(); + bidir_vov_int g2; + + g2 = g1; + + REQUIRE(num_vertices(g2) == 3); + REQUIRE(in_degree(g2, uint32_t{2}) == 2); +} + +TEST_CASE("bidir move assignment preserves reverse adjacency", + "[dynamic_graph][bidirectional][move_assign]") { + auto g1 = make_triangle_graph(); + bidir_vov_int g2; + + g2 = std::move(g1); + + REQUIRE(num_vertices(g2) == 3); + REQUIRE(in_degree(g2, uint32_t{2}) == 2); +} + +// ============================================================================ +// 12. Initializer-list construction +// ============================================================================ + +TEST_CASE("bidir initializer_list construction", + "[dynamic_graph][bidirectional][init_list]") { + // bidir + EV=int: initializer_list> + bidir_vov_int g({{0, 1, 10}, {0, 2, 30}, {1, 2, 20}}); + + REQUIRE(num_vertices(g) == 3); + REQUIRE(in_degree(g, uint32_t{0}) == 0); + REQUIRE(in_degree(g, uint32_t{1}) == 1); + REQUIRE(in_degree(g, uint32_t{2}) == 2); + + // Verify source_ids + auto u1 = *find_vertex(g, uint32_t{1}); + for (auto ie : in_edges(g, u1)) { + REQUIRE(source_id(g, ie) == 0); + REQUIRE(edge_value(g, ie) == 10); + } +} + +// ============================================================================ +// 13. Larger graph — fan-in topology +// ============================================================================ + +TEST_CASE("bidir fan-in topology", + "[dynamic_graph][bidirectional][fan_in]") { + // All vertices 0..4 point to vertex 5 + std::vector fan_edges; + for (uint32_t i = 0; i < 5; ++i) { + fan_edges.push_back({i, 5, static_cast(i * 10)}); + } + + bidir_vov_int g; + g.load_edges(fan_edges, std::identity{}); + + REQUIRE(num_vertices(g) == 6); + + // vertex 5 should have 5 incoming edges + REQUIRE(in_degree(g, uint32_t{5}) == 5); + + // All other vertices have 0 incoming edges + for (uint32_t i = 0; i < 5; ++i) { + REQUIRE(in_degree(g, i) == 0); + } + + // Check all source_ids for vertex 5 + auto u5 = *find_vertex(g, uint32_t{5}); + std::set sources; + for (auto ie : in_edges(g, u5)) { + sources.insert(source_id(g, ie)); + } + REQUIRE(sources == std::set{0, 1, 2, 3, 4}); +} + +// ============================================================================ +// 14. Self-loop handling +// ============================================================================ + +TEST_CASE("bidir self-loop appears in both edges and in_edges", + "[dynamic_graph][bidirectional][self_loop]") { + struct edge_data { + uint32_t source_id; + uint32_t target_id; + }; + std::vector el = {{0, 0}, {0, 1}}; + + bidir_vov_void g; + g.load_edges(el, std::identity{}); + + REQUIRE(num_vertices(g) == 2); + + // vertex 0: out-degree=2 (self-loop + edge to 1), in-degree=1 (self-loop) + REQUIRE(degree(g, uint32_t{0}) == 2); + REQUIRE(in_degree(g, uint32_t{0}) == 1); + + // The in-edge on vertex 0 should have source_id==0 + auto u0 = *find_vertex(g, uint32_t{0}); + for (auto ie : in_edges(g, u0)) { + REQUIRE(source_id(g, ie) == 0); + } + + // vertex 1: out-degree=0, in-degree=1 (from 0) + REQUIRE(degree(g, uint32_t{1}) == 0); + REQUIRE(in_degree(g, uint32_t{1}) == 1); +} + +// ============================================================================ +// 15. Forward-reverse consistency +// ============================================================================ + +TEMPLATE_TEST_CASE("bidir forward-reverse consistency", + "[dynamic_graph][bidirectional][consistency]", + bidir_vov_int, bidir_vol_int) { + auto g = make_triangle_graph(); + + // For every forward edge (u -> v), there must be an in_edge on vertex v + // with source_id == u + for (auto u : vertices(g)) { + auto uid = vertex_id(g, u); + for (auto e : edges(g, u)) { + auto tid = target_id(g, e); + // vertex tid should have an in_edge from uid + REQUIRE(contains_in_edge(g, tid, uid)); + } + } + + // For every in_edge on vertex v with source_id == u, + // vertex u should have a forward edge to v + for (auto v : vertices(g)) { + auto vid = vertex_id(g, v); + for (auto ie : in_edges(g, v)) { + auto sid = source_id(g, ie); + // vertex sid should have a forward edge to vid + REQUIRE(contains_edge(g, sid, vid)); + } + } +} + +// ============================================================================ +// 16. Total in_degree == total out_degree == edge count +// ============================================================================ + +TEMPLATE_TEST_CASE("bidir total in_degree equals total out_degree", + "[dynamic_graph][bidirectional][degree_sum]", + bidir_vov_int, bidir_vol_int) { + auto g = make_triangle_graph(); + + size_t total_out = 0, total_in = 0; + for (auto v : vertices(g)) { + total_out += degree(g, v); + total_in += in_degree(g, v); + } + REQUIRE(total_out == total_in); + REQUIRE(total_out == 3); // 3 edges +} + +// ============================================================================ +// 17. Views integration — in_incidence +// ============================================================================ + +TEST_CASE("bidir in_incidence view", + "[dynamic_graph][bidirectional][views][in_incidence]") { + auto g = make_triangle_graph(); + + SECTION("in_incidence by vertex id") { + // vertex 2 has in-edges from 0 and 1 + std::set sources; + for (auto [sid, ie] : graph::views::in_incidence(g, uint32_t{2})) { + sources.insert(static_cast(sid)); + } + REQUIRE(sources == std::set{0, 1}); + } + + SECTION("in_incidence with edge value function") { + std::vector weights; + for (auto [sid, ie] : graph::views::in_incidence(g, uint32_t{2})) { + weights.push_back(edge_value(g, ie)); + } + std::sort(weights.begin(), weights.end()); + REQUIRE(weights == std::vector{20, 30}); + } + + SECTION("in_incidence on vertex with no in-edges") { + size_t count = 0; + for ([[maybe_unused]] auto entry : graph::views::in_incidence(g, uint32_t{0})) { + ++count; + } + REQUIRE(count == 0); + } +} + +// ============================================================================ +// 18. Views integration — in_neighbors +// ============================================================================ + +TEST_CASE("bidir in_neighbors view", + "[dynamic_graph][bidirectional][views][in_neighbors]") { + auto g = make_triangle_graph(); + + SECTION("in_neighbors by vertex id") { + // vertex 2 has in-neighbors 0 and 1 + std::set nbrs; + for (auto [nid, nv] : graph::views::in_neighbors(g, uint32_t{2})) { + nbrs.insert(static_cast(nid)); + } + REQUIRE(nbrs == std::set{0, 1}); + } + + SECTION("in_neighbors on vertex with no in-edges") { + size_t count = 0; + for ([[maybe_unused]] auto entry : graph::views::in_neighbors(g, uint32_t{0})) { + ++count; + } + REQUIRE(count == 0); + } +} + +// ============================================================================ +// 19. basic_in_incidence and basic_in_neighbors +// ============================================================================ + +TEST_CASE("bidir basic_in_incidence view", + "[dynamic_graph][bidirectional][views][basic_in_incidence]") { + auto g = make_triangle_graph(); + + std::set sources; + for (auto [sid] : graph::views::basic_in_incidence(g, uint32_t{2})) { + sources.insert(static_cast(sid)); + } + REQUIRE(sources == std::set{0, 1}); +} + +TEST_CASE("bidir basic_in_neighbors view", + "[dynamic_graph][bidirectional][views][basic_in_neighbors]") { + auto g = make_triangle_graph(); + + std::set nbrs; + for (auto [nid] : graph::views::basic_in_neighbors(g, uint32_t{2})) { + nbrs.insert(static_cast(nid)); + } + REQUIRE(nbrs == std::set{0, 1}); +} + +// ============================================================================ +// 20. Vertex value type with bidirectional +// ============================================================================ + +TEST_CASE("bidir with vertex values", + "[dynamic_graph][bidirectional][vertex_value]") { + bidir_vov_int_vv g; + + // Load vertices with values, then edges + using vertex_data = copyable_vertex_t; + std::vector vv = {{0, 100}, {1, 200}, {2, 300}}; + g.load_vertices(vv, std::identity{}); + g.load_edges(triangle_edges, std::identity{}, 3); + + REQUIRE(num_vertices(g) == 3); + + // Vertex values accessible + using graph::adj_list::vertex_value; + auto u0 = *find_vertex(g, uint32_t{0}); + REQUIRE(vertex_value(g, u0) == 100); + + // in_edges work alongside vertex values + REQUIRE(in_degree(g, uint32_t{2}) == 2); +} + +// ============================================================================ +// 21. Empty graph +// ============================================================================ + +TEST_CASE("bidir empty graph", + "[dynamic_graph][bidirectional][empty]") { + bidir_vov_int g; + + REQUIRE(num_vertices(g) == 0); + // No crashes, just an empty graph +} + +// ============================================================================ +// 22. Single vertex, no edges +// ============================================================================ + +TEST_CASE("bidir single vertex no edges", + "[dynamic_graph][bidirectional][single_vertex]") { + bidir_vov_int g; + g.resize_vertices(1); + + REQUIRE(num_vertices(g) == 1); + auto u0 = *find_vertex(g, uint32_t{0}); + REQUIRE(in_degree(g, u0) == 0); + REQUIRE(degree(g, u0) == 0); +} + +// ============================================================================ +// 23. Clear resets everything +// ============================================================================ + +TEST_CASE("bidir clear resets in_edges", + "[dynamic_graph][bidirectional][clear]") { + auto g = make_triangle_graph(); + REQUIRE(num_vertices(g) == 3); + REQUIRE(in_degree(g, uint32_t{2}) == 2); + + g.clear(); + REQUIRE(num_vertices(g) == 0); +} + +// ============================================================================ +// 24. Multiple load_edges calls accumulate correctly +// ============================================================================ + +TEST_CASE("bidir multiple load_edges accumulate", + "[dynamic_graph][bidirectional][load_edges][accumulate]") { + bidir_vov_int g; + + // First batch: (0,1,10) + std::vector batch1 = {{0, 1, 10}}; + g.load_edges(batch1, std::identity{}); + + REQUIRE(num_vertices(g) == 2); + REQUIRE(in_degree(g, uint32_t{1}) == 1); + + // Second batch: (0,2,30), (1,2,20) + std::vector batch2 = {{0, 2, 30}, {1, 2, 20}}; + g.load_edges(batch2, std::identity{}); + + REQUIRE(num_vertices(g) == 3); + REQUIRE(in_degree(g, uint32_t{2}) == 2); + + // Original in_edges still present + REQUIRE(in_degree(g, uint32_t{1}) == 1); +} + +// ============================================================================ +// 25. Explicit vertex_count in load_edges +// ============================================================================ + +TEST_CASE("bidir load_edges with explicit vertex_count", + "[dynamic_graph][bidirectional][load_edges][vertex_count]") { + bidir_vov_int g; + g.load_edges(triangle_edges, std::identity{}, /*vertex_count=*/5); + + // Should have 5 vertices (some with no edges) + REQUIRE(num_vertices(g) == 5); + REQUIRE(in_degree(g, uint32_t{0}) == 0); + REQUIRE(in_degree(g, uint32_t{1}) == 1); + REQUIRE(in_degree(g, uint32_t{2}) == 2); + REQUIRE(in_degree(g, uint32_t{3}) == 0); + REQUIRE(in_degree(g, uint32_t{4}) == 0); +} + +// ============================================================================ +// 26. Dense graph — complete K4 +// ============================================================================ + +TEST_CASE("bidir complete graph K4", + "[dynamic_graph][bidirectional][complete]") { + // Complete directed graph on 4 vertices + std::vector k4_edges; + int w = 1; + for (uint32_t i = 0; i < 4; ++i) { + for (uint32_t j = 0; j < 4; ++j) { + if (i != j) { + k4_edges.push_back({i, j, w++}); + } + } + } + + bidir_vov_int g; + g.load_edges(k4_edges, std::identity{}); + + REQUIRE(num_vertices(g) == 4); + + // Each vertex: out-degree=3, in-degree=3 + for (auto v : vertices(g)) { + REQUIRE(degree(g, v) == 3); + REQUIRE(in_degree(g, v) == 3); + } + + // Total edges: 12 forward, 12 reverse + size_t total_out = 0, total_in = 0; + for (auto v : vertices(g)) { + total_out += degree(g, v); + total_in += in_degree(g, v); + } + REQUIRE(total_out == 12); + REQUIRE(total_in == 12); +} + +// ============================================================================ +// 27. vol trait type works identically to vov +// ============================================================================ + +TEST_CASE("bidir vol trait type works", + "[dynamic_graph][bidirectional][vol]") { + bidir_vol_int g({{0, 1, 10}, {0, 2, 30}, {1, 2, 20}}); + + REQUIRE(num_vertices(g) == 3); + REQUIRE(in_degree(g, uint32_t{0}) == 0); + REQUIRE(in_degree(g, uint32_t{1}) == 1); + REQUIRE(in_degree(g, uint32_t{2}) == 2); + + // edge values preserved + auto u2 = *find_vertex(g, uint32_t{2}); + std::set weights; + for (auto ie : in_edges(g, u2)) { + weights.insert(edge_value(g, ie)); + } + REQUIRE(weights == std::set{20, 30}); +} diff --git a/tests/container/dynamic_graph/test_dynamic_graph_common.cpp b/tests/container/dynamic_graph/test_dynamic_graph_common.cpp index 8373375..e5725f1 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_common.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_common.cpp @@ -1255,121 +1255,10 @@ TEMPLATE_TEST_CASE("vertex value modification", // TEMPLATE_TEST_CASE: Sourced Edges //================================================================================================== -TEMPLATE_TEST_CASE("sourced edges construction", - "[common][sourced]", - (vofl_graph_traits), - (vol_graph_traits), - (vov_graph_traits), - (vod_graph_traits), - (dofl_graph_traits), - (dol_graph_traits), - (dov_graph_traits), - (dod_graph_traits)) { - using Graph = dynamic_graph; - std::vector> edges = {{0, 1}, {1, 2}}; - Graph g; - g.load_edges(edges, std::identity{}); - auto& v0 = g[0]; - for (auto& e : v0.edges()) { - REQUIRE(e.source_id() == 0); - REQUIRE(e.target_id() == 1); - } -} -TEMPLATE_TEST_CASE("sourced edges with values", - "[common][sourced]", - (vofl_graph_traits), - (vol_graph_traits), - (vov_graph_traits), - (vod_graph_traits), - (dofl_graph_traits), - (dol_graph_traits), - (dov_graph_traits), - (dod_graph_traits)) { - using Graph = dynamic_graph; - std::vector> edges = {{0, 1, 10}, {1, 2, 20}}; - Graph g; - g.load_edges(edges, std::identity{}); - - auto& v1 = g[1]; - for (auto& e : v1.edges()) { - REQUIRE(e.source_id() == 1); - REQUIRE(e.target_id() == 2); - REQUIRE(e.value() == 20); - } -} - -TEMPLATE_TEST_CASE("sourced self-loops", - "[common][sourced]", - (vofl_graph_traits), - (vol_graph_traits), - (vov_graph_traits), - (vod_graph_traits), - (dofl_graph_traits), - (dol_graph_traits), - (dov_graph_traits), - (dod_graph_traits)) { - using Graph = dynamic_graph; - - std::vector> edges = {{0, 0}, {1, 1}}; - Graph g; - g.load_edges(edges, std::identity{}); - - auto& v0 = g[0]; - for (auto& e : v0.edges()) { - REQUIRE(e.source_id() == 0); - REQUIRE(e.target_id() == 0); - } -} - -TEMPLATE_TEST_CASE("sourced multiple edges from vertex", - "[common][sourced]", - (vofl_graph_traits), - (vol_graph_traits), - (vov_graph_traits), - (vod_graph_traits), - (dofl_graph_traits), - (dol_graph_traits), - (dov_graph_traits), - (dod_graph_traits)) { - using Graph = dynamic_graph; - - std::vector> edges = {{0, 1}, {0, 2}, {0, 3}}; - Graph g; - g.load_edges(edges, std::identity{}); - - auto& v0 = g[0]; - for (auto& e : v0.edges()) { - REQUIRE(e.source_id() == 0); - } -} - -TEMPLATE_TEST_CASE("sourced edge iteration consistency", - "[common][sourced]", - (vofl_graph_traits), - (vol_graph_traits), - (vov_graph_traits), - (vod_graph_traits), - (dofl_graph_traits), - (dol_graph_traits), - (dov_graph_traits), - (dod_graph_traits)) { - using Graph = dynamic_graph; - - std::vector> edges = {{0, 1, 10}, {1, 2, 20}, {2, 0, 30}}; - Graph g; - g.load_edges(edges, std::identity{}); - - for (size_t i = 0; i < 3; ++i) { - auto& v = g[i]; - for (auto& e : v.edges()) { - REQUIRE(e.source_id() == i); - } - } -} //================================================================================================== // TEMPLATE_TEST_CASE: Value Types @@ -1543,36 +1432,6 @@ TEMPLATE_TEST_CASE("const value access", } } -TEMPLATE_TEST_CASE("sourced edges with parallel edges", - "[common][sourced]", - (vofl_graph_traits), - (vol_graph_traits), - (vov_graph_traits), - (vod_graph_traits), - (dofl_graph_traits), - (dol_graph_traits), - (dov_graph_traits), - (dod_graph_traits)) { - using Graph = dynamic_graph; - - std::vector> edges = { - {0, 1, 10}, - {0, 1, 20}, // parallel edge - {0, 1, 30} // another parallel edge - }; - Graph g; - g.load_edges(edges, std::identity{}); - - auto& v0 = g[0]; - size_t count = 0; - for (const auto& e : v0.edges()) { - if (e.target_id() == 1) { - ++count; - } - } - - REQUIRE(count >= 1); // At least one edge to vertex 1 -} //================================================================================================== // TEMPLATE_TEST_CASE: Graph Properties diff --git a/tests/container/dynamic_graph/test_dynamic_graph_cpo_edge_map.cpp b/tests/container/dynamic_graph/test_dynamic_graph_cpo_edge_map.cpp index 7265a48..ba3d5ec 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_cpo_edge_map.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_cpo_edge_map.cpp @@ -397,17 +397,17 @@ TEMPLATE_TEST_CASE("edge_map CPO num_edges(g)", "[dynamic_graph][cpo][num_edges] } //================================================================================================== -// 6. has_edge(g) CPO Tests +// 6. has_edges(g) CPO Tests //================================================================================================== -TEMPLATE_TEST_CASE("edge_map CPO has_edge(g)", "[dynamic_graph][cpo][has_edge][edge_map]", vom_tag, mom_tag, voum_tag) { +TEMPLATE_TEST_CASE("edge_map CPO has_edges(g)", "[dynamic_graph][cpo][has_edges][edge_map]", vom_tag, mom_tag, voum_tag) { using Types = graph_test_types; using Graph_void = typename Types::void_type; using Graph_int_ev = typename Types::int_ev; SECTION("empty graph") { Graph_void g; - REQUIRE(has_edge(g) == false); + REQUIRE(has_edges(g) == false); } SECTION("graph with edges") { @@ -420,7 +420,7 @@ TEMPLATE_TEST_CASE("edge_map CPO has_edge(g)", "[dynamic_graph][cpo][has_edge][e g.load_edges(edgelist); } - REQUIRE(has_edge(g) == true); + REQUIRE(has_edges(g) == true); } SECTION("const correctness") { @@ -434,7 +434,7 @@ TEMPLATE_TEST_CASE("edge_map CPO has_edge(g)", "[dynamic_graph][cpo][has_edge][e } const auto& cg = g; - REQUIRE(has_edge(cg) == true); + REQUIRE(has_edges(cg) == true); } SECTION("with edge values") { @@ -447,7 +447,7 @@ TEMPLATE_TEST_CASE("edge_map CPO has_edge(g)", "[dynamic_graph][cpo][has_edge][e g.load_edges(edgelist); } - REQUIRE(has_edge(g) == true); + REQUIRE(has_edges(g) == true); } } @@ -1160,16 +1160,16 @@ TEMPLATE_TEST_CASE("edge_map CPO graph_value(g)", "[dynamic_graph][cpo][graph_va } //================================================================================================== -// 16. source_id(g, uv) CPO Tests (Sourced=true) +// 16. source_id(g, uv) CPO Tests //================================================================================================== TEMPLATE_TEST_CASE("edge_map CPO source_id(g, uv)", "[dynamic_graph][cpo][source_id][edge_map]", vom_tag, mom_tag, voum_tag) { - using Types = graph_test_types; - using Graph_sourced = typename Types::sourced_void; - using Graph_sourced_int = typename Types::sourced_int; + using Types = graph_test_types; + using Graph_void = typename Types::void_type; + using Graph_int_ev = typename Types::int_ev; SECTION("basic source IDs") { - Graph_sourced g; + Graph_void g; std::vector edgelist{{0, 1}, {0, 2}}; if constexpr (is_map_based_v) { g.load_edges(edgelist); @@ -1185,7 +1185,7 @@ TEMPLATE_TEST_CASE("edge_map CPO source_id(g, uv)", "[dynamic_graph][cpo][source } SECTION("different sources") { - Graph_sourced g; + Graph_void g; std::vector edgelist{{0, 1}, {1, 2}}; if constexpr (is_map_based_v) { g.load_edges(edgelist); @@ -1203,7 +1203,7 @@ TEMPLATE_TEST_CASE("edge_map CPO source_id(g, uv)", "[dynamic_graph][cpo][source } SECTION("const correctness") { - Graph_sourced g; + Graph_void g; std::vector edgelist{{0, 1}}; if constexpr (is_map_based_v) { g.load_edges(edgelist); @@ -1220,7 +1220,7 @@ TEMPLATE_TEST_CASE("edge_map CPO source_id(g, uv)", "[dynamic_graph][cpo][source } SECTION("with edge values") { - Graph_sourced_int g; + Graph_int_ev g; std::vector edgelist{{0, 1, 10}}; if constexpr (is_map_based_v) { g.load_edges(edgelist); @@ -1237,16 +1237,16 @@ TEMPLATE_TEST_CASE("edge_map CPO source_id(g, uv)", "[dynamic_graph][cpo][source } //================================================================================================== -// 17. source(g, uv) CPO Tests (Sourced=true) +// 17. source(g, uv) CPO Tests //================================================================================================== TEMPLATE_TEST_CASE("edge_map CPO source(g, uv)", "[dynamic_graph][cpo][source][edge_map]", vom_tag, mom_tag, voum_tag) { using Types = graph_test_types; - using Graph_sourced = typename Types::sourced_void; - using Graph_sourced_int = typename Types::sourced_int; + using Graph_void = typename Types::void_type; + using Graph_int_ev = typename Types::int_ev; SECTION("basic usage") { - Graph_sourced g; + Graph_void g; std::vector edgelist{{0, 1}, {0, 2}}; if constexpr (is_map_based_v) { g.load_edges(edgelist); @@ -1263,7 +1263,7 @@ TEMPLATE_TEST_CASE("edge_map CPO source(g, uv)", "[dynamic_graph][cpo][source][e } SECTION("consistency with source_id") { - Graph_sourced g; + Graph_void g; std::vector edgelist{{0, 1}, {1, 2}}; if constexpr (is_map_based_v) { g.load_edges(edgelist); @@ -1281,7 +1281,7 @@ TEMPLATE_TEST_CASE("edge_map CPO source(g, uv)", "[dynamic_graph][cpo][source][e } SECTION("const correctness") { - Graph_sourced g; + Graph_void g; std::vector edgelist{{0, 1}}; if constexpr (is_map_based_v) { g.load_edges(edgelist); @@ -1299,7 +1299,7 @@ TEMPLATE_TEST_CASE("edge_map CPO source(g, uv)", "[dynamic_graph][cpo][source][e } SECTION("with edge values") { - Graph_sourced_int g; + Graph_int_ev g; std::vector edgelist{{0, 1, 10}}; if constexpr (is_map_based_v) { g.load_edges(edgelist); diff --git a/tests/container/dynamic_graph/test_dynamic_graph_cpo_forward_list.cpp b/tests/container/dynamic_graph/test_dynamic_graph_cpo_forward_list.cpp index 0c691f1..8322799 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_cpo_forward_list.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_cpo_forward_list.cpp @@ -261,29 +261,29 @@ TEMPLATE_TEST_CASE("forward_list CPO num_edges(g)", "[dynamic_graph][cpo][num_ed } //================================================================================================== -// 6. has_edge(g) CPO Tests +// 6. has_edges(g) CPO Tests //================================================================================================== -TEMPLATE_TEST_CASE("forward_list CPO has_edge(g)", "[dynamic_graph][cpo][has_edge]", vofl_tag, dofl_tag) { +TEMPLATE_TEST_CASE("forward_list CPO has_edges(g)", "[dynamic_graph][cpo][has_edges]", vofl_tag, dofl_tag) { using Types = graph_test_types; using Graph_void = typename Types::void_type; SECTION("empty graph") { Graph_void g; - REQUIRE(!has_edge(g)); + REQUIRE(!has_edges(g)); } SECTION("with edges") { Graph_void g({{0, 1}}); - REQUIRE(has_edge(g)); + REQUIRE(has_edges(g)); } SECTION("matches num_edges") { Graph_void g1; Graph_void g2({{0, 1}}); - REQUIRE(has_edge(g1) == (num_edges(g1) > 0)); - REQUIRE(has_edge(g2) == (num_edges(g2) > 0)); + REQUIRE(has_edges(g1) == (num_edges(g1) > 0)); + REQUIRE(has_edges(g2) == (num_edges(g2) > 0)); } } @@ -526,6 +526,7 @@ TEMPLATE_TEST_CASE("forward_list CPO edges(g, u)", "[dynamic_graph][cpo][edges]" TEMPLATE_TEST_CASE("forward_list CPO edges(g, uid)", "[dynamic_graph][cpo][edges]", vofl_tag, dofl_tag) { using Types = graph_test_types; + using Graph_int_ev = typename Types::int_ev; using Graph_void = typename Types::void_type; SECTION("with vertex ID") { @@ -2580,11 +2581,11 @@ TEMPLATE_TEST_CASE("forward_list CPO num_vertices(g, pid)", TEMPLATE_TEST_CASE("forward_list CPO source_id(g, uv)", "[dynamic_graph][cpo][source_id]", vofl_tag, dofl_tag) { using Types = graph_test_types; - using Graph_sourced_void = typename Types::sourced_void; - using Graph_sourced_int = typename Types::sourced_int; + using Graph_void = typename Types::void_type; + using Graph_int_ev = typename Types::int_ev; SECTION("basic usage") { - Graph_sourced_void g({{0, 1}, {1, 2}, {0, 2}}); + Graph_void g({{0, 1}, {1, 2}, {0, 2}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -2593,7 +2594,7 @@ TEMPLATE_TEST_CASE("forward_list CPO source_id(g, uv)", "[dynamic_graph][cpo][so } SECTION("different sources") { - Graph_sourced_void g({{0, 1}, {1, 2}, {2, 3}}); + Graph_void g({{0, 1}, {1, 2}, {2, 3}}); for (size_t i = 0; i < 3; ++i) { auto u = *find_vertex(g, i); @@ -2604,7 +2605,7 @@ TEMPLATE_TEST_CASE("forward_list CPO source_id(g, uv)", "[dynamic_graph][cpo][so } SECTION("const correctness") { - const Graph_sourced_void g({{0, 1}, {1, 2}}); + const Graph_void g({{0, 1}, {1, 2}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -2613,7 +2614,7 @@ TEMPLATE_TEST_CASE("forward_list CPO source_id(g, uv)", "[dynamic_graph][cpo][so } SECTION("with edge values") { - Graph_sourced_int g({{0, 1, 10}, {1, 2, 20}}); + Graph_int_ev g({{0, 1, 10}, {1, 2, 20}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -2623,7 +2624,7 @@ TEMPLATE_TEST_CASE("forward_list CPO source_id(g, uv)", "[dynamic_graph][cpo][so } SECTION("with self-loop") { - Graph_sourced_void g({{0, 0}, {0, 1}}); + Graph_void g({{0, 0}, {0, 1}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -2632,7 +2633,7 @@ TEMPLATE_TEST_CASE("forward_list CPO source_id(g, uv)", "[dynamic_graph][cpo][so } SECTION("with parallel edges") { - Graph_sourced_int g; + Graph_int_ev g; g.resize_vertices(3); std::vector> edge_data = {{0, 1, 10}, {0, 1, 20}, {0, 1, 30}}; @@ -2645,7 +2646,7 @@ TEMPLATE_TEST_CASE("forward_list CPO source_id(g, uv)", "[dynamic_graph][cpo][so } SECTION("large graph") { - Graph_sourced_void g; + Graph_void g; g.resize_vertices(100); std::vector> edge_data; @@ -2661,7 +2662,7 @@ TEMPLATE_TEST_CASE("forward_list CPO source_id(g, uv)", "[dynamic_graph][cpo][so } SECTION("multiple edges from same source") { - Graph_sourced_void g({{0, 1}, {0, 2}, {0, 3}}); + Graph_void g({{0, 1}, {0, 2}, {0, 3}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -2670,7 +2671,7 @@ TEMPLATE_TEST_CASE("forward_list CPO source_id(g, uv)", "[dynamic_graph][cpo][so } SECTION("star graph") { - Graph_sourced_void g; + Graph_void g; g.resize_vertices(6); std::vector> edge_data; @@ -2686,7 +2687,7 @@ TEMPLATE_TEST_CASE("forward_list CPO source_id(g, uv)", "[dynamic_graph][cpo][so } SECTION("chain graph") { - Graph_sourced_void g({{0, 1}, {1, 2}, {2, 3}}); + Graph_void g({{0, 1}, {1, 2}, {2, 3}}); for (size_t i = 0; i < 3; ++i) { auto u = *find_vertex(g, i); @@ -2697,7 +2698,7 @@ TEMPLATE_TEST_CASE("forward_list CPO source_id(g, uv)", "[dynamic_graph][cpo][so } SECTION("cycle graph") { - Graph_sourced_void g({{0, 1}, {1, 2}, {2, 3}, {3, 0}}); + Graph_void g({{0, 1}, {1, 2}, {2, 3}, {3, 0}}); for (size_t i = 0; i < 4; ++i) { auto u = *find_vertex(g, i); @@ -2708,7 +2709,7 @@ TEMPLATE_TEST_CASE("forward_list CPO source_id(g, uv)", "[dynamic_graph][cpo][so } SECTION("consistency with source(g, uv)") { - Graph_sourced_void g({{0, 1}, {1, 2}, {2, 3}}); + Graph_void g({{0, 1}, {1, 2}, {2, 3}}); for (auto u : vertices(g)) { for (auto uv : edges(g, u)) { @@ -2719,7 +2720,7 @@ TEMPLATE_TEST_CASE("forward_list CPO source_id(g, uv)", "[dynamic_graph][cpo][so } SECTION("self-loops") { - Graph_sourced_void g({{0, 0}, {1, 1}}); + Graph_void g({{0, 0}, {1, 1}}); // Self-loops: source and target are the same auto u0 = *find_vertex(g, 0); @@ -2742,11 +2743,12 @@ TEMPLATE_TEST_CASE("forward_list CPO source_id(g, uv)", "[dynamic_graph][cpo][so TEMPLATE_TEST_CASE("forward_list CPO source(g, uv)", "[dynamic_graph][cpo][source]", vofl_tag, dofl_tag) { using Types = graph_test_types; - using Graph_sourced_void = typename Types::sourced_void; - using Graph_sourced_int = typename Types::sourced_int; + using Graph_void = typename Types::void_type; + using Graph_int_ev = typename Types::int_ev; + using Graph_all = typename Types::all_int; SECTION("basic usage") { - Graph_sourced_void g({{0, 1}, {1, 2}}); + Graph_void g({{0, 1}, {1, 2}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -2756,7 +2758,7 @@ TEMPLATE_TEST_CASE("forward_list CPO source(g, uv)", "[dynamic_graph][cpo][sourc } SECTION("consistency with source_id") { - Graph_sourced_void g({{0, 1}, {1, 2}, {2, 3}}); + Graph_void g({{0, 1}, {1, 2}, {2, 3}}); for (auto u : vertices(g)) { for (auto uv : edges(g, u)) { @@ -2767,7 +2769,7 @@ TEMPLATE_TEST_CASE("forward_list CPO source(g, uv)", "[dynamic_graph][cpo][sourc } SECTION("const correctness") { - const Graph_sourced_void g({{0, 1}, {1, 2}}); + const Graph_void g({{0, 1}, {1, 2}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -2777,7 +2779,7 @@ TEMPLATE_TEST_CASE("forward_list CPO source(g, uv)", "[dynamic_graph][cpo][sourc } SECTION("with edge values") { - Graph_sourced_int g({{0, 1, 100}, {1, 2, 200}}); + Graph_int_ev g({{0, 1, 100}, {1, 2, 200}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -2788,7 +2790,7 @@ TEMPLATE_TEST_CASE("forward_list CPO source(g, uv)", "[dynamic_graph][cpo][sourc } SECTION("with self-loop") { - Graph_sourced_void g({{0, 0}, {0, 1}}); + Graph_void g({{0, 0}, {0, 1}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -2798,7 +2800,7 @@ TEMPLATE_TEST_CASE("forward_list CPO source(g, uv)", "[dynamic_graph][cpo][sourc } SECTION("different sources") { - Graph_sourced_void g({{0, 1}, {1, 2}, {2, 3}, {3, 4}}); + Graph_void g({{0, 1}, {1, 2}, {2, 3}, {3, 4}}); for (size_t i = 0; i < 4; ++i) { auto u = *find_vertex(g, i); @@ -2810,7 +2812,7 @@ TEMPLATE_TEST_CASE("forward_list CPO source(g, uv)", "[dynamic_graph][cpo][sourc } SECTION("large graph") { - Graph_sourced_void g; + Graph_void g; g.resize_vertices(100); std::vector> edge_data; @@ -2827,7 +2829,7 @@ TEMPLATE_TEST_CASE("forward_list CPO source(g, uv)", "[dynamic_graph][cpo][sourc } SECTION("returns valid descriptor") { - Graph_sourced_void g({{0, 1}, {1, 2}}); + Graph_void g({{0, 1}, {1, 2}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -2838,10 +2840,9 @@ TEMPLATE_TEST_CASE("forward_list CPO source(g, uv)", "[dynamic_graph][cpo][sourc } SECTION("with vertex values") { - // Use sourced_all which has VV=int and Sourced=true - using Graph_sourced_all = typename Types::sourced_all; + // Use all_int which has EV=int, VV=int, GV=int - Graph_sourced_all g({{0, 1, 10}, {1, 2, 20}}); + Graph_all g({{0, 1, 10}, {1, 2, 20}}); int val = 100; for (auto u : vertices(g)) { @@ -2857,7 +2858,7 @@ TEMPLATE_TEST_CASE("forward_list CPO source(g, uv)", "[dynamic_graph][cpo][sourc } SECTION("parallel edges") { - Graph_sourced_int g; + Graph_int_ev g; g.resize_vertices(2); std::vector> edge_data = {{0, 1, 10}, {0, 1, 20}, {0, 1, 30}}; @@ -2871,7 +2872,7 @@ TEMPLATE_TEST_CASE("forward_list CPO source(g, uv)", "[dynamic_graph][cpo][sourc } SECTION("chain graph") { - Graph_sourced_void g({{0, 1}, {1, 2}, {2, 3}}); + Graph_void g({{0, 1}, {1, 2}, {2, 3}}); for (size_t i = 0; i < 3; ++i) { auto u = *find_vertex(g, i); @@ -2883,7 +2884,7 @@ TEMPLATE_TEST_CASE("forward_list CPO source(g, uv)", "[dynamic_graph][cpo][sourc } SECTION("star graph") { - Graph_sourced_void g; + Graph_void g; g.resize_vertices(6); std::vector> edge_data; @@ -2900,7 +2901,7 @@ TEMPLATE_TEST_CASE("forward_list CPO source(g, uv)", "[dynamic_graph][cpo][sourc } SECTION("can traverse from source to target") { - Graph_sourced_void g({{0, 1}, {1, 2}}); + Graph_void g({{0, 1}, {1, 2}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -2913,7 +2914,7 @@ TEMPLATE_TEST_CASE("forward_list CPO source(g, uv)", "[dynamic_graph][cpo][sourc } SECTION("accumulate values from edges") { - Graph_sourced_int g({{0, 1, 10}, {0, 2, 20}, {1, 2, 30}}); + Graph_int_ev g({{0, 1, 10}, {0, 2, 20}, {1, 2, 30}}); int sum = 0; for (auto u : vertices(g)) { @@ -2926,7 +2927,7 @@ TEMPLATE_TEST_CASE("forward_list CPO source(g, uv)", "[dynamic_graph][cpo][sourc } SECTION("self-loops") { - Graph_sourced_void g({{0, 0}, {1, 1}}); + Graph_void g({{0, 0}, {1, 1}}); // For self-loops, source and target should be the same vertex auto u0 = *find_vertex(g, 0); @@ -2958,7 +2959,7 @@ TEMPLATE_TEST_CASE("forward_list CPO integration", "[dynamic_graph][cpo][integra REQUIRE(num_vertices(g) == 3); REQUIRE(num_edges(g) == 2); - REQUIRE(has_edge(g)); + REQUIRE(has_edges(g)); } SECTION("empty graph properties") { @@ -2966,7 +2967,7 @@ TEMPLATE_TEST_CASE("forward_list CPO integration", "[dynamic_graph][cpo][integra REQUIRE(num_vertices(g) == 0); REQUIRE(num_edges(g) == 0); - REQUIRE(!has_edge(g)); + REQUIRE(!has_edges(g)); } SECTION("find vertex by id") { diff --git a/tests/container/dynamic_graph/test_dynamic_graph_cpo_map_vertices.cpp b/tests/container/dynamic_graph/test_dynamic_graph_cpo_map_vertices.cpp index 1b863d4..cc88cc3 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_cpo_map_vertices.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_cpo_map_vertices.cpp @@ -379,11 +379,11 @@ TEMPLATE_TEST_CASE("map CPO num_edges(g)", } //================================================================================================== -// 6. has_edge(g) CPO Tests +// 6. has_edges(g) CPO Tests //================================================================================================== -TEMPLATE_TEST_CASE("map CPO has_edge(g)", - "[dynamic_graph][cpo][has_edge][map]", +TEMPLATE_TEST_CASE("map CPO has_edges(g)", + "[dynamic_graph][cpo][has_edges][map]", mol_tag, mov_tag, mod_tag, @@ -396,22 +396,22 @@ TEMPLATE_TEST_CASE("map CPO has_edge(g)", SECTION("graph with edges") { auto g = make_basic_graph_void(); - REQUIRE(has_edge(g) == true); + REQUIRE(has_edges(g) == true); } SECTION("empty graph") { Graph_void g; - REQUIRE(has_edge(g) == false); + REQUIRE(has_edges(g) == false); } SECTION("const correctness") { const auto g = make_sparse_graph_void(); - REQUIRE(has_edge(g) == true); + REQUIRE(has_edges(g) == true); } SECTION("with edge values") { auto g = make_sparse_graph_int(); - REQUIRE(has_edge(g) == true); + REQUIRE(has_edges(g) == true); } } @@ -1351,7 +1351,7 @@ TEMPLATE_TEST_CASE("map CPO graph_value(g)", } //================================================================================================== -// 21. source_id(g, uv) CPO Tests (Sourced=true) +// 21. source_id(g, uv) CPO Tests //================================================================================================== TEMPLATE_TEST_CASE("map CPO source_id(g, uv)", @@ -1363,11 +1363,11 @@ TEMPLATE_TEST_CASE("map CPO source_id(g, uv)", mos_tag, mous_tag) { using Types = graph_test_types; - using Graph_sourced = typename Types::sourced_void; - using Graph_sourced_int = typename Types::sourced_int; + using Graph_void = typename Types::void_type; + using Graph_int_ev = typename Types::int_ev; SECTION("sparse source IDs") { - auto g = make_sparse_graph_void(); + auto g = make_sparse_graph_void(); auto v100 = *find_vertex(g, uint32_t(100)); @@ -1377,7 +1377,7 @@ TEMPLATE_TEST_CASE("map CPO source_id(g, uv)", } SECTION("different sources") { - auto g = make_sparse_graph_void(); + auto g = make_sparse_graph_void(); for (auto u : vertices(g)) { auto uid = vertex_id(g, u); @@ -1388,7 +1388,7 @@ TEMPLATE_TEST_CASE("map CPO source_id(g, uv)", } SECTION("const correctness") { - const auto g = make_sparse_graph_void(); + const auto g = make_sparse_graph_void(); auto v100 = *find_vertex(g, uint32_t(100)); for (auto uv : edges(g, v100)) { @@ -1397,7 +1397,7 @@ TEMPLATE_TEST_CASE("map CPO source_id(g, uv)", } SECTION("with edge values") { - auto g = make_sparse_graph_int(); + auto g = make_sparse_graph_int(); auto v100 = *find_vertex(g, uint32_t(100)); for (auto uv : edges(g, v100)) { @@ -1406,7 +1406,7 @@ TEMPLATE_TEST_CASE("map CPO source_id(g, uv)", } SECTION("self-loop") { - auto g = make_self_loop_graph(); + auto g = make_self_loop_graph(); auto v100 = *find_vertex(g, uint32_t(100)); for (auto uv : edges(g, v100)) { @@ -1416,7 +1416,7 @@ TEMPLATE_TEST_CASE("map CPO source_id(g, uv)", } //================================================================================================== -// 22. source(g, uv) CPO Tests (Sourced=true) +// 22. source(g, uv) CPO Tests //================================================================================================== TEMPLATE_TEST_CASE("map CPO source(g, uv)", @@ -1428,11 +1428,11 @@ TEMPLATE_TEST_CASE("map CPO source(g, uv)", mos_tag, mous_tag) { using Types = graph_test_types; - using Graph_sourced = typename Types::sourced_void; - using Graph_sourced_int = typename Types::sourced_int; + using Graph_void = typename Types::void_type; + using Graph_int_ev = typename Types::int_ev; SECTION("basic usage") { - auto g = make_sparse_graph_void(); + auto g = make_sparse_graph_void(); auto v100 = *find_vertex(g, uint32_t(100)); for (auto uv : edges(g, v100)) { @@ -1442,7 +1442,7 @@ TEMPLATE_TEST_CASE("map CPO source(g, uv)", } SECTION("consistency with source_id") { - auto g = make_sparse_graph_void(); + auto g = make_sparse_graph_void(); for (auto u : vertices(g)) { for (auto uv : edges(g, u)) { @@ -1453,7 +1453,7 @@ TEMPLATE_TEST_CASE("map CPO source(g, uv)", } SECTION("const correctness") { - const auto g = make_sparse_graph_void(); + const auto g = make_sparse_graph_void(); auto v100 = *find_vertex(g, uint32_t(100)); for (auto uv : edges(g, v100)) { @@ -1463,7 +1463,7 @@ TEMPLATE_TEST_CASE("map CPO source(g, uv)", } SECTION("with edge values") { - auto g = make_sparse_graph_int(); + auto g = make_sparse_graph_int(); auto v100 = *find_vertex(g, uint32_t(100)); for (auto uv : edges(g, v100)) { @@ -1473,7 +1473,7 @@ TEMPLATE_TEST_CASE("map CPO source(g, uv)", } SECTION("different sources") { - auto g = make_sparse_graph_void(); + auto g = make_sparse_graph_void(); for (auto u : vertices(g)) { auto uid = vertex_id(g, u); diff --git a/tests/container/dynamic_graph/test_dynamic_graph_cpo_random_access.cpp b/tests/container/dynamic_graph/test_dynamic_graph_cpo_random_access.cpp index 0123196..00385c9 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_cpo_random_access.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_cpo_random_access.cpp @@ -297,11 +297,11 @@ TEMPLATE_TEST_CASE("random_access CPO num_edges(g)", } //================================================================================================== -// 6. has_edge(g) CPO Tests +// 6. has_edges(g) CPO Tests //================================================================================================== -TEMPLATE_TEST_CASE("random_access CPO has_edge(g)", - "[dynamic_graph][cpo][has_edge]", +TEMPLATE_TEST_CASE("random_access CPO has_edges(g)", + "[dynamic_graph][cpo][has_edges]", vov_tag, vod_tag, dov_tag, @@ -313,20 +313,20 @@ TEMPLATE_TEST_CASE("random_access CPO has_edge(g)", SECTION("empty graph") { Graph_void g; - REQUIRE(!has_edge(g)); + REQUIRE(!has_edges(g)); } SECTION("with edges") { Graph_void g({{0, 1}}); - REQUIRE(has_edge(g)); + REQUIRE(has_edges(g)); } SECTION("matches num_edges") { Graph_void g1; Graph_void g2({{0, 1}}); - REQUIRE(has_edge(g1) == (num_edges(g1) > 0)); - REQUIRE(has_edge(g2) == (num_edges(g2) > 0)); + REQUIRE(has_edges(g1) == (num_edges(g1) > 0)); + REQUIRE(has_edges(g2) == (num_edges(g2) > 0)); } } @@ -663,6 +663,7 @@ TEMPLATE_TEST_CASE("random_access CPO edges(g, uid)", vol_tag, dol_tag) { using Types = graph_test_types; + using Graph_int_ev = typename Types::int_ev; using Graph_void = typename Types::void_type; SECTION("with vertex ID") { @@ -2799,11 +2800,11 @@ TEMPLATE_TEST_CASE("random_access CPO source_id(g, uv)", vol_tag, dol_tag) { using Types = graph_test_types; - using Graph_sourced_void = typename Types::sourced_void; - using Graph_sourced_int = typename Types::sourced_int; + using Graph_void = typename Types::void_type; + using Graph_int_ev = typename Types::int_ev; SECTION("basic usage") { - Graph_sourced_void g({{0, 1}, {1, 2}, {0, 2}}); + Graph_void g({{0, 1}, {1, 2}, {0, 2}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -2812,7 +2813,7 @@ TEMPLATE_TEST_CASE("random_access CPO source_id(g, uv)", } SECTION("different sources") { - Graph_sourced_void g({{0, 1}, {1, 2}, {2, 3}}); + Graph_void g({{0, 1}, {1, 2}, {2, 3}}); for (size_t i = 0; i < 3; ++i) { auto u = *find_vertex(g, i); @@ -2823,7 +2824,7 @@ TEMPLATE_TEST_CASE("random_access CPO source_id(g, uv)", } SECTION("const correctness") { - const Graph_sourced_void g({{0, 1}, {1, 2}}); + const Graph_void g({{0, 1}, {1, 2}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -2832,7 +2833,7 @@ TEMPLATE_TEST_CASE("random_access CPO source_id(g, uv)", } SECTION("with edge values") { - Graph_sourced_int g({{0, 1, 10}, {1, 2, 20}}); + Graph_int_ev g({{0, 1, 10}, {1, 2, 20}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -2842,7 +2843,7 @@ TEMPLATE_TEST_CASE("random_access CPO source_id(g, uv)", } SECTION("with self-loop") { - Graph_sourced_void g({{0, 0}, {0, 1}}); + Graph_void g({{0, 0}, {0, 1}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -2851,7 +2852,7 @@ TEMPLATE_TEST_CASE("random_access CPO source_id(g, uv)", } SECTION("with parallel edges") { - Graph_sourced_int g; + Graph_int_ev g; g.resize_vertices(3); std::vector> edge_data = {{0, 1, 10}, {0, 1, 20}, {0, 1, 30}}; @@ -2864,7 +2865,7 @@ TEMPLATE_TEST_CASE("random_access CPO source_id(g, uv)", } SECTION("large graph") { - Graph_sourced_void g; + Graph_void g; g.resize_vertices(100); std::vector> edge_data; @@ -2880,7 +2881,7 @@ TEMPLATE_TEST_CASE("random_access CPO source_id(g, uv)", } SECTION("multiple edges from same source") { - Graph_sourced_void g({{0, 1}, {0, 2}, {0, 3}}); + Graph_void g({{0, 1}, {0, 2}, {0, 3}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -2889,7 +2890,7 @@ TEMPLATE_TEST_CASE("random_access CPO source_id(g, uv)", } SECTION("star graph") { - Graph_sourced_void g; + Graph_void g; g.resize_vertices(6); std::vector> edge_data; @@ -2905,7 +2906,7 @@ TEMPLATE_TEST_CASE("random_access CPO source_id(g, uv)", } SECTION("chain graph") { - Graph_sourced_void g({{0, 1}, {1, 2}, {2, 3}}); + Graph_void g({{0, 1}, {1, 2}, {2, 3}}); for (size_t i = 0; i < 3; ++i) { auto u = *find_vertex(g, i); @@ -2916,7 +2917,7 @@ TEMPLATE_TEST_CASE("random_access CPO source_id(g, uv)", } SECTION("cycle graph") { - Graph_sourced_void g({{0, 1}, {1, 2}, {2, 3}, {3, 0}}); + Graph_void g({{0, 1}, {1, 2}, {2, 3}, {3, 0}}); for (size_t i = 0; i < 4; ++i) { auto u = *find_vertex(g, i); @@ -2927,7 +2928,7 @@ TEMPLATE_TEST_CASE("random_access CPO source_id(g, uv)", } SECTION("consistency with source(g, uv)") { - Graph_sourced_void g({{0, 1}, {1, 2}, {2, 3}}); + Graph_void g({{0, 1}, {1, 2}, {2, 3}}); for (auto u : vertices(g)) { for (auto uv : edges(g, u)) { @@ -2938,7 +2939,7 @@ TEMPLATE_TEST_CASE("random_access CPO source_id(g, uv)", } SECTION("self-loops") { - Graph_sourced_void g({{0, 0}, {1, 1}}); + Graph_void g({{0, 0}, {1, 1}}); // Self-loops: source and target are the same auto u0 = *find_vertex(g, 0); @@ -2968,11 +2969,12 @@ TEMPLATE_TEST_CASE("random_access CPO source(g, uv)", vol_tag, dol_tag) { using Types = graph_test_types; - using Graph_sourced_void = typename Types::sourced_void; - using Graph_sourced_int = typename Types::sourced_int; + using Graph_void = typename Types::void_type; + using Graph_int_ev = typename Types::int_ev; + using Graph_all = typename Types::all_int; SECTION("basic usage") { - Graph_sourced_void g({{0, 1}, {1, 2}}); + Graph_void g({{0, 1}, {1, 2}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -2982,7 +2984,7 @@ TEMPLATE_TEST_CASE("random_access CPO source(g, uv)", } SECTION("consistency with source_id") { - Graph_sourced_void g({{0, 1}, {1, 2}, {2, 3}}); + Graph_void g({{0, 1}, {1, 2}, {2, 3}}); for (auto u : vertices(g)) { for (auto uv : edges(g, u)) { @@ -2993,7 +2995,7 @@ TEMPLATE_TEST_CASE("random_access CPO source(g, uv)", } SECTION("const correctness") { - const Graph_sourced_void g({{0, 1}, {1, 2}}); + const Graph_void g({{0, 1}, {1, 2}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -3003,7 +3005,7 @@ TEMPLATE_TEST_CASE("random_access CPO source(g, uv)", } SECTION("with edge values") { - Graph_sourced_int g({{0, 1, 100}, {1, 2, 200}}); + Graph_int_ev g({{0, 1, 100}, {1, 2, 200}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -3014,7 +3016,7 @@ TEMPLATE_TEST_CASE("random_access CPO source(g, uv)", } SECTION("with self-loop") { - Graph_sourced_void g({{0, 0}, {0, 1}}); + Graph_void g({{0, 0}, {0, 1}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -3024,7 +3026,7 @@ TEMPLATE_TEST_CASE("random_access CPO source(g, uv)", } SECTION("different sources") { - Graph_sourced_void g({{0, 1}, {1, 2}, {2, 3}, {3, 4}}); + Graph_void g({{0, 1}, {1, 2}, {2, 3}, {3, 4}}); for (size_t i = 0; i < 4; ++i) { auto u = *find_vertex(g, i); @@ -3036,7 +3038,7 @@ TEMPLATE_TEST_CASE("random_access CPO source(g, uv)", } SECTION("large graph") { - Graph_sourced_void g; + Graph_void g; g.resize_vertices(100); std::vector> edge_data; @@ -3053,7 +3055,7 @@ TEMPLATE_TEST_CASE("random_access CPO source(g, uv)", } SECTION("returns valid descriptor") { - Graph_sourced_void g({{0, 1}, {1, 2}}); + Graph_void g({{0, 1}, {1, 2}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -3064,10 +3066,9 @@ TEMPLATE_TEST_CASE("random_access CPO source(g, uv)", } SECTION("with vertex values") { - // Use sourced_all which has VV=int and Sourced=true - using Graph_sourced_all = typename Types::sourced_all; + // Use all_int which has EV=int, VV=int, GV=int - Graph_sourced_all g({{0, 1, 10}, {1, 2, 20}}); + Graph_all g({{0, 1, 10}, {1, 2, 20}}); int val = 100; for (auto u : vertices(g)) { @@ -3083,7 +3084,7 @@ TEMPLATE_TEST_CASE("random_access CPO source(g, uv)", } SECTION("parallel edges") { - Graph_sourced_int g; + Graph_int_ev g; g.resize_vertices(2); std::vector> edge_data = {{0, 1, 10}, {0, 1, 20}, {0, 1, 30}}; @@ -3097,7 +3098,7 @@ TEMPLATE_TEST_CASE("random_access CPO source(g, uv)", } SECTION("chain graph") { - Graph_sourced_void g({{0, 1}, {1, 2}, {2, 3}}); + Graph_void g({{0, 1}, {1, 2}, {2, 3}}); for (size_t i = 0; i < 3; ++i) { auto u = *find_vertex(g, i); @@ -3109,7 +3110,7 @@ TEMPLATE_TEST_CASE("random_access CPO source(g, uv)", } SECTION("star graph") { - Graph_sourced_void g; + Graph_void g; g.resize_vertices(6); std::vector> edge_data; @@ -3126,7 +3127,7 @@ TEMPLATE_TEST_CASE("random_access CPO source(g, uv)", } SECTION("can traverse from source to target") { - Graph_sourced_void g({{0, 1}, {1, 2}}); + Graph_void g({{0, 1}, {1, 2}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -3139,7 +3140,7 @@ TEMPLATE_TEST_CASE("random_access CPO source(g, uv)", } SECTION("accumulate values from edges") { - Graph_sourced_int g({{0, 1, 10}, {0, 2, 20}, {1, 2, 30}}); + Graph_int_ev g({{0, 1, 10}, {0, 2, 20}, {1, 2, 30}}); int sum = 0; for (auto u : vertices(g)) { @@ -3152,7 +3153,7 @@ TEMPLATE_TEST_CASE("random_access CPO source(g, uv)", } SECTION("self-loops") { - Graph_sourced_void g({{0, 0}, {1, 1}}); + Graph_void g({{0, 0}, {1, 1}}); // For self-loops, source and target should be the same vertex auto u0 = *find_vertex(g, 0); @@ -3191,7 +3192,7 @@ TEMPLATE_TEST_CASE("random_access CPO integration", REQUIRE(num_vertices(g) == 3); REQUIRE(num_edges(g) == 2); - REQUIRE(has_edge(g)); + REQUIRE(has_edges(g)); } SECTION("empty graph properties") { @@ -3199,7 +3200,7 @@ TEMPLATE_TEST_CASE("random_access CPO integration", REQUIRE(num_vertices(g) == 0); REQUIRE(num_edges(g) == 0); - REQUIRE(!has_edge(g)); + REQUIRE(!has_edges(g)); } SECTION("find vertex by id") { diff --git a/tests/container/dynamic_graph/test_dynamic_graph_cpo_sorted.cpp b/tests/container/dynamic_graph/test_dynamic_graph_cpo_sorted.cpp index 5082ee0..aa45c23 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_cpo_sorted.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_cpo_sorted.cpp @@ -266,29 +266,29 @@ TEMPLATE_TEST_CASE("sorted CPO num_edges(g)", "[dynamic_graph][cpo][num_edges]", } //================================================================================================== -// 6. has_edge(g) CPO Tests +// 6. has_edges(g) CPO Tests //================================================================================================== -TEMPLATE_TEST_CASE("sorted CPO has_edge(g)", "[dynamic_graph][cpo][has_edge]", vos_tag, dos_tag) { +TEMPLATE_TEST_CASE("sorted CPO has_edges(g)", "[dynamic_graph][cpo][has_edges]", vos_tag, dos_tag) { using Types = graph_test_types; using Graph_void = typename Types::void_type; SECTION("empty graph") { Graph_void g; - REQUIRE(!has_edge(g)); + REQUIRE(!has_edges(g)); } SECTION("with edges") { Graph_void g({{0, 1}}); - REQUIRE(has_edge(g)); + REQUIRE(has_edges(g)); } SECTION("matches num_edges") { Graph_void g1; Graph_void g2({{0, 1}}); - REQUIRE(has_edge(g1) == (num_edges(g1) > 0)); - REQUIRE(has_edge(g2) == (num_edges(g2) > 0)); + REQUIRE(has_edges(g1) == (num_edges(g1) > 0)); + REQUIRE(has_edges(g2) == (num_edges(g2) > 0)); } } @@ -597,6 +597,7 @@ TEMPLATE_TEST_CASE("sorted CPO edges(g, u)", "[dynamic_graph][cpo][edges]", vos_ TEMPLATE_TEST_CASE("sorted CPO edges(g, uid)", "[dynamic_graph][cpo][edges]", vos_tag, dos_tag) { using Types = graph_test_types; + using Graph_int_ev = typename Types::int_ev; using Graph_void = typename Types::void_type; SECTION("with vertex ID") { @@ -2624,11 +2625,11 @@ TEMPLATE_TEST_CASE("sorted CPO num_vertices(g, pid)", TEMPLATE_TEST_CASE("sorted CPO source_id(g, uv)", "[dynamic_graph][cpo][source_id]", vos_tag, dos_tag) { using Types = graph_test_types; - using Graph_sourced_void = typename Types::sourced_void; - using Graph_sourced_int = typename Types::sourced_int; + using Graph_void = typename Types::void_type; + using Graph_int_ev = typename Types::int_ev; SECTION("basic usage") { - Graph_sourced_void g({{0, 1}, {1, 2}, {0, 2}}); + Graph_void g({{0, 1}, {1, 2}, {0, 2}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -2637,7 +2638,7 @@ TEMPLATE_TEST_CASE("sorted CPO source_id(g, uv)", "[dynamic_graph][cpo][source_i } SECTION("different sources") { - Graph_sourced_void g({{0, 1}, {1, 2}, {2, 3}}); + Graph_void g({{0, 1}, {1, 2}, {2, 3}}); for (size_t i = 0; i < 3; ++i) { auto u = *find_vertex(g, i); @@ -2648,7 +2649,7 @@ TEMPLATE_TEST_CASE("sorted CPO source_id(g, uv)", "[dynamic_graph][cpo][source_i } SECTION("const correctness") { - const Graph_sourced_void g({{0, 1}, {1, 2}}); + const Graph_void g({{0, 1}, {1, 2}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -2657,7 +2658,7 @@ TEMPLATE_TEST_CASE("sorted CPO source_id(g, uv)", "[dynamic_graph][cpo][source_i } SECTION("with edge values") { - Graph_sourced_int g({{0, 1, 10}, {1, 2, 20}}); + Graph_int_ev g({{0, 1, 10}, {1, 2, 20}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -2667,7 +2668,7 @@ TEMPLATE_TEST_CASE("sorted CPO source_id(g, uv)", "[dynamic_graph][cpo][source_i } SECTION("with self-loop") { - Graph_sourced_void g({{0, 0}, {0, 1}}); + Graph_void g({{0, 0}, {0, 1}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -2676,7 +2677,7 @@ TEMPLATE_TEST_CASE("sorted CPO source_id(g, uv)", "[dynamic_graph][cpo][source_i } SECTION("with parallel edges") { - Graph_sourced_int g; + Graph_int_ev g; g.resize_vertices(3); std::vector> edge_data = {{0, 1, 10}, {0, 1, 20}, {0, 1, 30}}; @@ -2689,7 +2690,7 @@ TEMPLATE_TEST_CASE("sorted CPO source_id(g, uv)", "[dynamic_graph][cpo][source_i } SECTION("large graph") { - Graph_sourced_void g; + Graph_void g; g.resize_vertices(100); std::vector> edge_data; @@ -2705,7 +2706,7 @@ TEMPLATE_TEST_CASE("sorted CPO source_id(g, uv)", "[dynamic_graph][cpo][source_i } SECTION("multiple edges from same source") { - Graph_sourced_void g({{0, 1}, {0, 2}, {0, 3}}); + Graph_void g({{0, 1}, {0, 2}, {0, 3}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -2714,7 +2715,7 @@ TEMPLATE_TEST_CASE("sorted CPO source_id(g, uv)", "[dynamic_graph][cpo][source_i } SECTION("star graph") { - Graph_sourced_void g; + Graph_void g; g.resize_vertices(6); std::vector> edge_data; @@ -2730,7 +2731,7 @@ TEMPLATE_TEST_CASE("sorted CPO source_id(g, uv)", "[dynamic_graph][cpo][source_i } SECTION("chain graph") { - Graph_sourced_void g({{0, 1}, {1, 2}, {2, 3}}); + Graph_void g({{0, 1}, {1, 2}, {2, 3}}); for (size_t i = 0; i < 3; ++i) { auto u = *find_vertex(g, i); @@ -2741,7 +2742,7 @@ TEMPLATE_TEST_CASE("sorted CPO source_id(g, uv)", "[dynamic_graph][cpo][source_i } SECTION("cycle graph") { - Graph_sourced_void g({{0, 1}, {1, 2}, {2, 3}, {3, 0}}); + Graph_void g({{0, 1}, {1, 2}, {2, 3}, {3, 0}}); for (size_t i = 0; i < 4; ++i) { auto u = *find_vertex(g, i); @@ -2752,7 +2753,7 @@ TEMPLATE_TEST_CASE("sorted CPO source_id(g, uv)", "[dynamic_graph][cpo][source_i } SECTION("consistency with source(g, uv)") { - Graph_sourced_void g({{0, 1}, {1, 2}, {2, 3}}); + Graph_void g({{0, 1}, {1, 2}, {2, 3}}); for (auto u : vertices(g)) { for (auto uv : edges(g, u)) { @@ -2763,7 +2764,7 @@ TEMPLATE_TEST_CASE("sorted CPO source_id(g, uv)", "[dynamic_graph][cpo][source_i } SECTION("self-loops") { - Graph_sourced_void g({{0, 0}, {1, 1}}); + Graph_void g({{0, 0}, {1, 1}}); // Self-loops: source and target are the same auto u0 = *find_vertex(g, 0); @@ -2786,11 +2787,12 @@ TEMPLATE_TEST_CASE("sorted CPO source_id(g, uv)", "[dynamic_graph][cpo][source_i TEMPLATE_TEST_CASE("sorted CPO source(g, uv)", "[dynamic_graph][cpo][source]", vos_tag, dos_tag) { using Types = graph_test_types; - using Graph_sourced_void = typename Types::sourced_void; - using Graph_sourced_int = typename Types::sourced_int; + using Graph_void = typename Types::void_type; + using Graph_int_ev = typename Types::int_ev; + using Graph_all = typename Types::all_int; SECTION("basic usage") { - Graph_sourced_void g({{0, 1}, {1, 2}}); + Graph_void g({{0, 1}, {1, 2}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -2800,7 +2802,7 @@ TEMPLATE_TEST_CASE("sorted CPO source(g, uv)", "[dynamic_graph][cpo][source]", v } SECTION("consistency with source_id") { - Graph_sourced_void g({{0, 1}, {1, 2}, {2, 3}}); + Graph_void g({{0, 1}, {1, 2}, {2, 3}}); for (auto u : vertices(g)) { for (auto uv : edges(g, u)) { @@ -2811,7 +2813,7 @@ TEMPLATE_TEST_CASE("sorted CPO source(g, uv)", "[dynamic_graph][cpo][source]", v } SECTION("const correctness") { - const Graph_sourced_void g({{0, 1}, {1, 2}}); + const Graph_void g({{0, 1}, {1, 2}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -2821,7 +2823,7 @@ TEMPLATE_TEST_CASE("sorted CPO source(g, uv)", "[dynamic_graph][cpo][source]", v } SECTION("with edge values") { - Graph_sourced_int g({{0, 1, 100}, {1, 2, 200}}); + Graph_int_ev g({{0, 1, 100}, {1, 2, 200}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -2832,7 +2834,7 @@ TEMPLATE_TEST_CASE("sorted CPO source(g, uv)", "[dynamic_graph][cpo][source]", v } SECTION("with self-loop") { - Graph_sourced_void g({{0, 0}, {0, 1}}); + Graph_void g({{0, 0}, {0, 1}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -2842,7 +2844,7 @@ TEMPLATE_TEST_CASE("sorted CPO source(g, uv)", "[dynamic_graph][cpo][source]", v } SECTION("different sources") { - Graph_sourced_void g({{0, 1}, {1, 2}, {2, 3}, {3, 4}}); + Graph_void g({{0, 1}, {1, 2}, {2, 3}, {3, 4}}); for (size_t i = 0; i < 4; ++i) { auto u = *find_vertex(g, i); @@ -2854,7 +2856,7 @@ TEMPLATE_TEST_CASE("sorted CPO source(g, uv)", "[dynamic_graph][cpo][source]", v } SECTION("large graph") { - Graph_sourced_void g; + Graph_void g; g.resize_vertices(100); std::vector> edge_data; @@ -2871,7 +2873,7 @@ TEMPLATE_TEST_CASE("sorted CPO source(g, uv)", "[dynamic_graph][cpo][source]", v } SECTION("returns valid descriptor") { - Graph_sourced_void g({{0, 1}, {1, 2}}); + Graph_void g({{0, 1}, {1, 2}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -2882,10 +2884,9 @@ TEMPLATE_TEST_CASE("sorted CPO source(g, uv)", "[dynamic_graph][cpo][source]", v } SECTION("with vertex values") { - // Use sourced_all which has VV=int and Sourced=true - using Graph_sourced_all = typename Types::sourced_all; + // Use all_int which has EV=int, VV=int, GV=int - Graph_sourced_all g({{0, 1, 10}, {1, 2, 20}}); + Graph_all g({{0, 1, 10}, {1, 2, 20}}); int val = 100; for (auto u : vertices(g)) { @@ -2901,7 +2902,7 @@ TEMPLATE_TEST_CASE("sorted CPO source(g, uv)", "[dynamic_graph][cpo][source]", v } SECTION("parallel edges") { - Graph_sourced_int g; + Graph_int_ev g; g.resize_vertices(2); std::vector> edge_data = {{0, 1, 10}, {0, 1, 20}, {0, 1, 30}}; @@ -2915,7 +2916,7 @@ TEMPLATE_TEST_CASE("sorted CPO source(g, uv)", "[dynamic_graph][cpo][source]", v } SECTION("chain graph") { - Graph_sourced_void g({{0, 1}, {1, 2}, {2, 3}}); + Graph_void g({{0, 1}, {1, 2}, {2, 3}}); for (size_t i = 0; i < 3; ++i) { auto u = *find_vertex(g, i); @@ -2927,7 +2928,7 @@ TEMPLATE_TEST_CASE("sorted CPO source(g, uv)", "[dynamic_graph][cpo][source]", v } SECTION("star graph") { - Graph_sourced_void g; + Graph_void g; g.resize_vertices(6); std::vector> edge_data; @@ -2944,7 +2945,7 @@ TEMPLATE_TEST_CASE("sorted CPO source(g, uv)", "[dynamic_graph][cpo][source]", v } SECTION("can traverse from source to target") { - Graph_sourced_void g({{0, 1}, {1, 2}}); + Graph_void g({{0, 1}, {1, 2}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -2957,7 +2958,7 @@ TEMPLATE_TEST_CASE("sorted CPO source(g, uv)", "[dynamic_graph][cpo][source]", v } SECTION("accumulate values from edges") { - Graph_sourced_int g({{0, 1, 10}, {0, 2, 20}, {1, 2, 30}}); + Graph_int_ev g({{0, 1, 10}, {0, 2, 20}, {1, 2, 30}}); int sum = 0; for (auto u : vertices(g)) { @@ -2970,7 +2971,7 @@ TEMPLATE_TEST_CASE("sorted CPO source(g, uv)", "[dynamic_graph][cpo][source]", v } SECTION("self-loops") { - Graph_sourced_void g({{0, 0}, {1, 1}}); + Graph_void g({{0, 0}, {1, 1}}); // For self-loops, source and target should be the same vertex auto u0 = *find_vertex(g, 0); @@ -3002,7 +3003,7 @@ TEMPLATE_TEST_CASE("sorted CPO integration", "[dynamic_graph][cpo][integration]" REQUIRE(num_vertices(g) == 3); REQUIRE(num_edges(g) == 2); - REQUIRE(has_edge(g)); + REQUIRE(has_edges(g)); } SECTION("empty graph properties") { @@ -3010,7 +3011,7 @@ TEMPLATE_TEST_CASE("sorted CPO integration", "[dynamic_graph][cpo][integration]" REQUIRE(num_vertices(g) == 0); REQUIRE(num_edges(g) == 0); - REQUIRE(!has_edge(g)); + REQUIRE(!has_edges(g)); } SECTION("find vertex by id") { diff --git a/tests/container/dynamic_graph/test_dynamic_graph_cpo_unordered.cpp b/tests/container/dynamic_graph/test_dynamic_graph_cpo_unordered.cpp index e2dd736..5e4f433 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_cpo_unordered.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_cpo_unordered.cpp @@ -266,29 +266,29 @@ TEMPLATE_TEST_CASE("unordered CPO num_edges(g)", "[dynamic_graph][cpo][num_edges } //================================================================================================== -// 6. has_edge(g) CPO Tests +// 6. has_edges(g) CPO Tests //================================================================================================== -TEMPLATE_TEST_CASE("unordered CPO has_edge(g)", "[dynamic_graph][cpo][has_edge]", vous_tag, dous_tag) { +TEMPLATE_TEST_CASE("unordered CPO has_edges(g)", "[dynamic_graph][cpo][has_edges]", vous_tag, dous_tag) { using Types = graph_test_types; using Graph_void = typename Types::void_type; SECTION("empty graph") { Graph_void g; - REQUIRE(!has_edge(g)); + REQUIRE(!has_edges(g)); } SECTION("with edges") { Graph_void g({{0, 1}}); - REQUIRE(has_edge(g)); + REQUIRE(has_edges(g)); } SECTION("matches num_edges") { Graph_void g1; Graph_void g2({{0, 1}}); - REQUIRE(has_edge(g1) == (num_edges(g1) > 0)); - REQUIRE(has_edge(g2) == (num_edges(g2) > 0)); + REQUIRE(has_edges(g1) == (num_edges(g1) > 0)); + REQUIRE(has_edges(g2) == (num_edges(g2) > 0)); } } @@ -603,6 +603,7 @@ TEMPLATE_TEST_CASE("unordered CPO edges(g, u)", "[dynamic_graph][cpo][edges]", v TEMPLATE_TEST_CASE("unordered CPO edges(g, uid)", "[dynamic_graph][cpo][edges]", vous_tag, dous_tag) { using Types = graph_test_types; + using Graph_int_ev = typename Types::int_ev; using Graph_void = typename Types::void_type; SECTION("with vertex ID") { @@ -2650,11 +2651,11 @@ TEMPLATE_TEST_CASE("unordered CPO num_vertices(g, pid)", TEMPLATE_TEST_CASE("unordered CPO source_id(g, uv)", "[dynamic_graph][cpo][source_id]", vous_tag, dous_tag) { using Types = graph_test_types; - using Graph_sourced_void = typename Types::sourced_void; - using Graph_sourced_int = typename Types::sourced_int; + using Graph_void = typename Types::void_type; + using Graph_int_ev = typename Types::int_ev; SECTION("basic usage") { - Graph_sourced_void g({{0, 1}, {1, 2}, {0, 2}}); + Graph_void g({{0, 1}, {1, 2}, {0, 2}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -2663,7 +2664,7 @@ TEMPLATE_TEST_CASE("unordered CPO source_id(g, uv)", "[dynamic_graph][cpo][sourc } SECTION("different sources") { - Graph_sourced_void g({{0, 1}, {1, 2}, {2, 3}}); + Graph_void g({{0, 1}, {1, 2}, {2, 3}}); for (size_t i = 0; i < 3; ++i) { auto u = *find_vertex(g, i); @@ -2674,7 +2675,7 @@ TEMPLATE_TEST_CASE("unordered CPO source_id(g, uv)", "[dynamic_graph][cpo][sourc } SECTION("const correctness") { - const Graph_sourced_void g({{0, 1}, {1, 2}}); + const Graph_void g({{0, 1}, {1, 2}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -2683,7 +2684,7 @@ TEMPLATE_TEST_CASE("unordered CPO source_id(g, uv)", "[dynamic_graph][cpo][sourc } SECTION("with edge values") { - Graph_sourced_int g({{0, 1, 10}, {1, 2, 20}}); + Graph_int_ev g({{0, 1, 10}, {1, 2, 20}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -2693,7 +2694,7 @@ TEMPLATE_TEST_CASE("unordered CPO source_id(g, uv)", "[dynamic_graph][cpo][sourc } SECTION("with self-loop") { - Graph_sourced_void g({{0, 0}, {0, 1}}); + Graph_void g({{0, 0}, {0, 1}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -2702,7 +2703,7 @@ TEMPLATE_TEST_CASE("unordered CPO source_id(g, uv)", "[dynamic_graph][cpo][sourc } SECTION("with parallel edges") { - Graph_sourced_int g; + Graph_int_ev g; g.resize_vertices(3); std::vector> edge_data = {{0, 1, 10}, {0, 1, 20}, {0, 1, 30}}; @@ -2715,7 +2716,7 @@ TEMPLATE_TEST_CASE("unordered CPO source_id(g, uv)", "[dynamic_graph][cpo][sourc } SECTION("large graph") { - Graph_sourced_void g; + Graph_void g; g.resize_vertices(100); std::vector> edge_data; @@ -2731,7 +2732,7 @@ TEMPLATE_TEST_CASE("unordered CPO source_id(g, uv)", "[dynamic_graph][cpo][sourc } SECTION("multiple edges from same source") { - Graph_sourced_void g({{0, 1}, {0, 2}, {0, 3}}); + Graph_void g({{0, 1}, {0, 2}, {0, 3}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -2740,7 +2741,7 @@ TEMPLATE_TEST_CASE("unordered CPO source_id(g, uv)", "[dynamic_graph][cpo][sourc } SECTION("star graph") { - Graph_sourced_void g; + Graph_void g; g.resize_vertices(6); std::vector> edge_data; @@ -2756,7 +2757,7 @@ TEMPLATE_TEST_CASE("unordered CPO source_id(g, uv)", "[dynamic_graph][cpo][sourc } SECTION("chain graph") { - Graph_sourced_void g({{0, 1}, {1, 2}, {2, 3}}); + Graph_void g({{0, 1}, {1, 2}, {2, 3}}); for (size_t i = 0; i < 3; ++i) { auto u = *find_vertex(g, i); @@ -2767,7 +2768,7 @@ TEMPLATE_TEST_CASE("unordered CPO source_id(g, uv)", "[dynamic_graph][cpo][sourc } SECTION("cycle graph") { - Graph_sourced_void g({{0, 1}, {1, 2}, {2, 3}, {3, 0}}); + Graph_void g({{0, 1}, {1, 2}, {2, 3}, {3, 0}}); for (size_t i = 0; i < 4; ++i) { auto u = *find_vertex(g, i); @@ -2778,7 +2779,7 @@ TEMPLATE_TEST_CASE("unordered CPO source_id(g, uv)", "[dynamic_graph][cpo][sourc } SECTION("consistency with source(g, uv)") { - Graph_sourced_void g({{0, 1}, {1, 2}, {2, 3}}); + Graph_void g({{0, 1}, {1, 2}, {2, 3}}); for (auto u : vertices(g)) { for (auto uv : edges(g, u)) { @@ -2789,7 +2790,7 @@ TEMPLATE_TEST_CASE("unordered CPO source_id(g, uv)", "[dynamic_graph][cpo][sourc } SECTION("self-loops") { - Graph_sourced_void g({{0, 0}, {1, 1}}); + Graph_void g({{0, 0}, {1, 1}}); // Self-loops: source and target are the same auto u0 = *find_vertex(g, 0); @@ -2812,11 +2813,12 @@ TEMPLATE_TEST_CASE("unordered CPO source_id(g, uv)", "[dynamic_graph][cpo][sourc TEMPLATE_TEST_CASE("unordered CPO source(g, uv)", "[dynamic_graph][cpo][source]", vous_tag, dous_tag) { using Types = graph_test_types; - using Graph_sourced_void = typename Types::sourced_void; - using Graph_sourced_int = typename Types::sourced_int; + using Graph_void = typename Types::void_type; + using Graph_int_ev = typename Types::int_ev; + using Graph_all = typename Types::all_int; SECTION("basic usage") { - Graph_sourced_void g({{0, 1}, {1, 2}}); + Graph_void g({{0, 1}, {1, 2}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -2826,7 +2828,7 @@ TEMPLATE_TEST_CASE("unordered CPO source(g, uv)", "[dynamic_graph][cpo][source]" } SECTION("consistency with source_id") { - Graph_sourced_void g({{0, 1}, {1, 2}, {2, 3}}); + Graph_void g({{0, 1}, {1, 2}, {2, 3}}); for (auto u : vertices(g)) { for (auto uv : edges(g, u)) { @@ -2837,7 +2839,7 @@ TEMPLATE_TEST_CASE("unordered CPO source(g, uv)", "[dynamic_graph][cpo][source]" } SECTION("const correctness") { - const Graph_sourced_void g({{0, 1}, {1, 2}}); + const Graph_void g({{0, 1}, {1, 2}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -2847,7 +2849,7 @@ TEMPLATE_TEST_CASE("unordered CPO source(g, uv)", "[dynamic_graph][cpo][source]" } SECTION("with edge values") { - Graph_sourced_int g({{0, 1, 100}, {1, 2, 200}}); + Graph_int_ev g({{0, 1, 100}, {1, 2, 200}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -2858,7 +2860,7 @@ TEMPLATE_TEST_CASE("unordered CPO source(g, uv)", "[dynamic_graph][cpo][source]" } SECTION("with self-loop") { - Graph_sourced_void g({{0, 0}, {0, 1}}); + Graph_void g({{0, 0}, {0, 1}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -2868,7 +2870,7 @@ TEMPLATE_TEST_CASE("unordered CPO source(g, uv)", "[dynamic_graph][cpo][source]" } SECTION("different sources") { - Graph_sourced_void g({{0, 1}, {1, 2}, {2, 3}, {3, 4}}); + Graph_void g({{0, 1}, {1, 2}, {2, 3}, {3, 4}}); for (size_t i = 0; i < 4; ++i) { auto u = *find_vertex(g, i); @@ -2880,7 +2882,7 @@ TEMPLATE_TEST_CASE("unordered CPO source(g, uv)", "[dynamic_graph][cpo][source]" } SECTION("large graph") { - Graph_sourced_void g; + Graph_void g; g.resize_vertices(100); std::vector> edge_data; @@ -2897,7 +2899,7 @@ TEMPLATE_TEST_CASE("unordered CPO source(g, uv)", "[dynamic_graph][cpo][source]" } SECTION("returns valid descriptor") { - Graph_sourced_void g({{0, 1}, {1, 2}}); + Graph_void g({{0, 1}, {1, 2}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -2908,10 +2910,9 @@ TEMPLATE_TEST_CASE("unordered CPO source(g, uv)", "[dynamic_graph][cpo][source]" } SECTION("with vertex values") { - // Use sourced_all which has VV=int and Sourced=true - using Graph_sourced_all = typename Types::sourced_all; + // Use all_int which has EV=int, VV=int, GV=int - Graph_sourced_all g({{0, 1, 10}, {1, 2, 20}}); + Graph_all g({{0, 1, 10}, {1, 2, 20}}); int val = 100; for (auto u : vertices(g)) { @@ -2927,7 +2928,7 @@ TEMPLATE_TEST_CASE("unordered CPO source(g, uv)", "[dynamic_graph][cpo][source]" } SECTION("parallel edges") { - Graph_sourced_int g; + Graph_int_ev g; g.resize_vertices(2); std::vector> edge_data = {{0, 1, 10}, {0, 1, 20}, {0, 1, 30}}; @@ -2941,7 +2942,7 @@ TEMPLATE_TEST_CASE("unordered CPO source(g, uv)", "[dynamic_graph][cpo][source]" } SECTION("chain graph") { - Graph_sourced_void g({{0, 1}, {1, 2}, {2, 3}}); + Graph_void g({{0, 1}, {1, 2}, {2, 3}}); for (size_t i = 0; i < 3; ++i) { auto u = *find_vertex(g, i); @@ -2953,7 +2954,7 @@ TEMPLATE_TEST_CASE("unordered CPO source(g, uv)", "[dynamic_graph][cpo][source]" } SECTION("star graph") { - Graph_sourced_void g; + Graph_void g; g.resize_vertices(6); std::vector> edge_data; @@ -2970,7 +2971,7 @@ TEMPLATE_TEST_CASE("unordered CPO source(g, uv)", "[dynamic_graph][cpo][source]" } SECTION("can traverse from source to target") { - Graph_sourced_void g({{0, 1}, {1, 2}}); + Graph_void g({{0, 1}, {1, 2}}); auto u0 = *find_vertex(g, 0); for (auto uv : edges(g, u0)) { @@ -2983,7 +2984,7 @@ TEMPLATE_TEST_CASE("unordered CPO source(g, uv)", "[dynamic_graph][cpo][source]" } SECTION("accumulate values from edges") { - Graph_sourced_int g({{0, 1, 10}, {0, 2, 20}, {1, 2, 30}}); + Graph_int_ev g({{0, 1, 10}, {0, 2, 20}, {1, 2, 30}}); int sum = 0; for (auto u : vertices(g)) { @@ -2996,7 +2997,7 @@ TEMPLATE_TEST_CASE("unordered CPO source(g, uv)", "[dynamic_graph][cpo][source]" } SECTION("self-loops") { - Graph_sourced_void g({{0, 0}, {1, 1}}); + Graph_void g({{0, 0}, {1, 1}}); // For self-loops, source and target should be the same vertex auto u0 = *find_vertex(g, 0); @@ -3028,7 +3029,7 @@ TEMPLATE_TEST_CASE("unordered CPO integration", "[dynamic_graph][cpo][integratio REQUIRE(num_vertices(g) == 3); REQUIRE(num_edges(g) == 2); - REQUIRE(has_edge(g)); + REQUIRE(has_edges(g)); } SECTION("empty graph properties") { @@ -3036,7 +3037,7 @@ TEMPLATE_TEST_CASE("unordered CPO integration", "[dynamic_graph][cpo][integratio REQUIRE(num_vertices(g) == 0); REQUIRE(num_edges(g) == 0); - REQUIRE(!has_edge(g)); + REQUIRE(!has_edges(g)); } SECTION("find vertex by id") { diff --git a/tests/container/dynamic_graph/test_dynamic_graph_cpo_unordered_map_vertices.cpp b/tests/container/dynamic_graph/test_dynamic_graph_cpo_unordered_map_vertices.cpp index e7a57f7..5334492 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_cpo_unordered_map_vertices.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_cpo_unordered_map_vertices.cpp @@ -384,11 +384,11 @@ TEMPLATE_TEST_CASE("unordered_map CPO num_edges(g)", } //================================================================================================== -// 6. has_edge(g) CPO Tests +// 6. has_edges(g) CPO Tests //================================================================================================== -TEMPLATE_TEST_CASE("unordered_map CPO has_edge(g)", - "[dynamic_graph][cpo][has_edge][unordered_map]", +TEMPLATE_TEST_CASE("unordered_map CPO has_edges(g)", + "[dynamic_graph][cpo][has_edges][unordered_map]", uol_tag, uov_tag, uod_tag, @@ -401,22 +401,22 @@ TEMPLATE_TEST_CASE("unordered_map CPO has_edge(g)", SECTION("graph with edges") { auto g = make_basic_graph_void(); - REQUIRE(has_edge(g) == true); + REQUIRE(has_edges(g) == true); } SECTION("empty graph") { Graph_void g; - REQUIRE(has_edge(g) == false); + REQUIRE(has_edges(g) == false); } SECTION("const correctness") { const auto g = make_sparse_graph_void(); - REQUIRE(has_edge(g) == true); + REQUIRE(has_edges(g) == true); } SECTION("with edge values") { auto g = make_sparse_graph_int(); - REQUIRE(has_edge(g) == true); + REQUIRE(has_edges(g) == true); } } @@ -1357,7 +1357,7 @@ TEMPLATE_TEST_CASE("unordered_map CPO graph_value(g)", } //================================================================================================== -// 21. source_id(g, uv) CPO Tests (Sourced=true) +// 21. source_id(g, uv) CPO Tests //================================================================================================== TEMPLATE_TEST_CASE("unordered_map CPO source_id(g, uv)", @@ -1369,11 +1369,11 @@ TEMPLATE_TEST_CASE("unordered_map CPO source_id(g, uv)", uos_tag, uous_tag) { using Types = graph_test_types; - using Graph_sourced = typename Types::sourced_void; - using Graph_sourced_int = typename Types::sourced_int; + using Graph_void = typename Types::void_type; + using Graph_int_ev = typename Types::int_ev; SECTION("sparse source IDs") { - auto g = make_sparse_graph_void(); + auto g = make_sparse_graph_void(); auto v100 = *find_vertex(g, uint32_t(100)); @@ -1383,7 +1383,7 @@ TEMPLATE_TEST_CASE("unordered_map CPO source_id(g, uv)", } SECTION("different sources") { - auto g = make_sparse_graph_void(); + auto g = make_sparse_graph_void(); for (auto u : vertices(g)) { auto uid = vertex_id(g, u); @@ -1394,7 +1394,7 @@ TEMPLATE_TEST_CASE("unordered_map CPO source_id(g, uv)", } SECTION("const correctness") { - const auto g = make_sparse_graph_void(); + const auto g = make_sparse_graph_void(); auto v100 = *find_vertex(g, uint32_t(100)); for (auto uv : edges(g, v100)) { @@ -1403,7 +1403,7 @@ TEMPLATE_TEST_CASE("unordered_map CPO source_id(g, uv)", } SECTION("with edge values") { - auto g = make_sparse_graph_int(); + auto g = make_sparse_graph_int(); auto v100 = *find_vertex(g, uint32_t(100)); for (auto uv : edges(g, v100)) { @@ -1412,7 +1412,7 @@ TEMPLATE_TEST_CASE("unordered_map CPO source_id(g, uv)", } SECTION("self-loop") { - auto g = make_self_loop_graph(); + auto g = make_self_loop_graph(); auto v100 = *find_vertex(g, uint32_t(100)); for (auto uv : edges(g, v100)) { @@ -1422,7 +1422,7 @@ TEMPLATE_TEST_CASE("unordered_map CPO source_id(g, uv)", } //================================================================================================== -// 22. source(g, uv) CPO Tests (Sourced=true) +// 22. source(g, uv) CPO Tests //================================================================================================== TEMPLATE_TEST_CASE("unordered_map CPO source(g, uv)", @@ -1434,11 +1434,11 @@ TEMPLATE_TEST_CASE("unordered_map CPO source(g, uv)", uos_tag, uous_tag) { using Types = graph_test_types; - using Graph_sourced = typename Types::sourced_void; - using Graph_sourced_int = typename Types::sourced_int; + using Graph_void = typename Types::void_type; + using Graph_int_ev = typename Types::int_ev; SECTION("basic usage") { - auto g = make_sparse_graph_void(); + auto g = make_sparse_graph_void(); auto v100 = *find_vertex(g, uint32_t(100)); for (auto uv : edges(g, v100)) { @@ -1448,7 +1448,7 @@ TEMPLATE_TEST_CASE("unordered_map CPO source(g, uv)", } SECTION("consistency with source_id") { - auto g = make_sparse_graph_void(); + auto g = make_sparse_graph_void(); for (auto u : vertices(g)) { for (auto uv : edges(g, u)) { @@ -1459,7 +1459,7 @@ TEMPLATE_TEST_CASE("unordered_map CPO source(g, uv)", } SECTION("const correctness") { - const auto g = make_sparse_graph_void(); + const auto g = make_sparse_graph_void(); auto v100 = *find_vertex(g, uint32_t(100)); for (auto uv : edges(g, v100)) { @@ -1469,7 +1469,7 @@ TEMPLATE_TEST_CASE("unordered_map CPO source(g, uv)", } SECTION("with edge values") { - auto g = make_sparse_graph_int(); + auto g = make_sparse_graph_int(); auto v100 = *find_vertex(g, uint32_t(100)); for (auto uv : edges(g, v100)) { @@ -1479,7 +1479,7 @@ TEMPLATE_TEST_CASE("unordered_map CPO source(g, uv)", } SECTION("different sources") { - auto g = make_sparse_graph_void(); + auto g = make_sparse_graph_void(); for (auto u : vertices(g)) { auto uid = vertex_id(g, u); diff --git a/tests/container/dynamic_graph/test_dynamic_graph_dod.cpp b/tests/container/dynamic_graph/test_dynamic_graph_dod.cpp index 05ec158..c5bc88a 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_dod.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_dod.cpp @@ -47,12 +47,8 @@ using dod_string_string_string = std::string, std::string, uint32_t, - false, - dod_graph_traits>; + false, dod_graph_traits>; -using dod_sourced = dynamic_graph>; -using dod_int_sourced = - dynamic_graph>; //================================================================================================== // 1. Construction Tests @@ -140,17 +136,6 @@ TEST_CASE("dod copy and move construction", "[dod][construction]") { } } -TEST_CASE("dod sourced construction", "[dod][construction][sourced]") { - SECTION("sourced edge construction") { - dod_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced with edge value construction") { - dod_int_sourced g; - REQUIRE(g.size() == 0); - } -} //================================================================================================== // 2. Basic Properties Tests @@ -291,13 +276,8 @@ TEST_CASE("dod_graph_traits", "[dod][traits]") { STATIC_REQUIRE(std::is_same_v); STATIC_REQUIRE(std::is_same_v); STATIC_REQUIRE(std::is_same_v); - STATIC_REQUIRE(traits::sourced == false); } - SECTION("sourced = true") { - using traits = dod_graph_traits; - STATIC_REQUIRE(traits::sourced == true); - } SECTION("vertex_id_type variations") { using traits_u64 = dod_graph_traits; @@ -422,22 +402,19 @@ TEST_CASE("dod value types", "[dod][value_types]") { } SECTION("with string edge value type") { - using graph_t = dynamic_graph>; + using graph_t = dynamic_graph>; graph_t g; REQUIRE(g.size() == 0); } SECTION("with string vertex value type") { - using graph_t = dynamic_graph>; + using graph_t = dynamic_graph>; graph_t g; REQUIRE(g.size() == 0); } SECTION("with string graph value type") { - using graph_t = dynamic_graph>; + using graph_t = dynamic_graph>; graph_t g(std::string("test")); REQUIRE(g.graph_value() == "test"); } @@ -490,41 +467,6 @@ TEST_CASE("dod vertex ID types", "[dod][vertex_id]") { // 9. Sourced Edge Tests //================================================================================================== -TEST_CASE("dod sourced edges", "[dod][sourced]") { - SECTION("sourced=false by default") { - dod_void_void_void g; - using traits = dod_graph_traits; - STATIC_REQUIRE(traits::sourced == false); - } - - SECTION("sourced=true explicit") { - dod_sourced g; - using traits = dod_graph_traits; - STATIC_REQUIRE(traits::sourced == true); - } - - SECTION("sourced with void values") { - dod_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced with int edge value") { - dod_int_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced copy construction") { - dod_sourced g1; - dod_sourced g2 = g1; - REQUIRE(g2.size() == 0); - } - - SECTION("sourced move construction") { - dod_sourced g1; - dod_sourced g2 = std::move(g1); - REQUIRE(g2.size() == 0); - } -} //================================================================================================== // 10. Const Correctness Tests @@ -622,8 +564,6 @@ TEST_CASE("dod various template instantiations compile", "[dod][compilation]") { [[maybe_unused]] dod_void_void_int g5; [[maybe_unused]] dod_int_int_int g6; [[maybe_unused]] dod_string_string_string g7; - [[maybe_unused]] dod_sourced g8; - [[maybe_unused]] dod_int_sourced g9; REQUIRE(true); // Just ensuring compilation } @@ -845,22 +785,6 @@ TEST_CASE("dod initializer_list constructor with all value types", "[dod][constr } } -TEST_CASE("dod initializer_list constructor with sourced edges", "[dod][construction][initializer_list][sourced]") { - using G = dod_sourced; - - SECTION("construct sourced graph with initializer list") { - G g({{0, 1}, {1, 2}, {2, 0}}); - REQUIRE(g.size() == 3); - - // Verify sourced edges have source_id - auto& v0 = g[0]; - auto edges0 = v0.edges(); - REQUIRE(std::ranges::distance(edges0) == 1); - auto e0 = edges0.begin(); - REQUIRE(e0->source_id() == 0); - REQUIRE(e0->target_id() == 1); - } -} TEST_CASE("dod initializer_list complex graph patterns", "[dod][construction][initializer_list]") { using G = dod_int_void_void; @@ -950,8 +874,7 @@ TEST_CASE("dod load_vertices", "[dynamic_graph][dod][load_vertices]") { } SECTION("with custom projection from struct") { - using G2 = dynamic_graph>; + using G2 = dynamic_graph>; using vertex_data2 = copyable_vertex_t; struct Person { @@ -1088,8 +1011,7 @@ TEST_CASE("dod load_edges", "[dynamic_graph][dod][load_edges]") { } SECTION("with custom projection") { - using G2 = dynamic_graph>; + using G2 = dynamic_graph>; using vertex_data2 = copyable_vertex_t; using edge_data2 = copyable_edge_t; diff --git a/tests/container/dynamic_graph/test_dynamic_graph_dofl.cpp b/tests/container/dynamic_graph/test_dynamic_graph_dofl.cpp index 492d19a..6fb9fbb 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_dofl.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_dofl.cpp @@ -39,13 +39,8 @@ using dofl_string_string_string = std::string, std::string, uint32_t, - false, - dofl_graph_traits>; + false, dofl_graph_traits>; -using dofl_sourced = - dynamic_graph>; -using dofl_int_sourced = - dynamic_graph>; //================================================================================================== // 1. Construction Tests (40 tests) @@ -133,17 +128,6 @@ TEST_CASE("dofl construction", "[dynamic_graph][dofl][construction]") { } } -TEST_CASE("dofl construction sourced", "[dynamic_graph][dofl][construction][sourced]") { - SECTION("sourced edge construction") { - dofl_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced with edge value construction") { - dofl_int_sourced g; - REQUIRE(g.size() == 0); - } -} //================================================================================================== // 2. Basic Properties Tests (20 tests) @@ -409,12 +393,10 @@ TEST_CASE("dofl traits", "[dynamic_graph][dofl][traits]") { STATIC_REQUIRE(std::is_same_v); STATIC_REQUIRE(std::is_same_v); STATIC_REQUIRE(std::is_same_v); - STATIC_REQUIRE(traits::sourced == false); } SECTION("dofl_graph_traits sourced = true") { using traits = dofl_graph_traits; - STATIC_REQUIRE(traits::sourced == true); } SECTION("vertex_id_type variations") { @@ -541,22 +523,19 @@ TEST_CASE("dofl value_types", "[dynamic_graph][dofl][value_types]") { } SECTION("with string edge value type") { - using graph_t = dynamic_graph>; + using graph_t = dynamic_graph>; graph_t g; REQUIRE(g.size() == 0); } SECTION("with string vertex value type") { - using graph_t = dynamic_graph>; + using graph_t = dynamic_graph>; graph_t g; REQUIRE(g.size() == 0); } SECTION("with string graph value type") { - using graph_t = dynamic_graph>; + using graph_t = dynamic_graph>; graph_t g(std::string("test")); REQUIRE(g.graph_value() == "test"); } @@ -610,41 +589,6 @@ TEST_CASE("dofl vertex_id", "[dynamic_graph][dofl][vertex_id]") { // 9. Sourced Edge Tests (15 tests) //================================================================================================== -TEST_CASE("dofl sourced", "[dynamic_graph][dofl][sourced]") { - SECTION("sourced=false by default") { - dofl_void_void_void g; - using traits = dofl_graph_traits; - STATIC_REQUIRE(traits::sourced == false); - } - - SECTION("sourced=true explicit") { - dofl_sourced g; - using traits = dofl_graph_traits; - STATIC_REQUIRE(traits::sourced == true); - } - - SECTION("sourced with void values") { - dofl_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced with int edge value") { - dofl_int_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced copy construction") { - dofl_sourced g1; - dofl_sourced g2 = g1; - REQUIRE(g2.size() == 0); - } - - SECTION("sourced move construction") { - dofl_sourced g1; - dofl_sourced g2 = std::move(g1); - REQUIRE(g2.size() == 0); - } -} //================================================================================================== // 10. Const Correctness Tests (15 tests) @@ -742,8 +686,6 @@ TEST_CASE("dofl various template instantiations compile", "[dynamic_graph][dofl] [[maybe_unused]] dofl_void_void_int g5; [[maybe_unused]] dofl_int_int_int g6; [[maybe_unused]] dofl_string_string_string g7; - [[maybe_unused]] dofl_sourced g8; - [[maybe_unused]] dofl_int_sourced g9; REQUIRE(true); // Just ensuring compilation } @@ -1012,22 +954,6 @@ TEST_CASE("dofl construction initializer_list", "[dynamic_graph][dofl][construct } } -TEST_CASE("dofl construction initializer_list sourced", - "[dynamic_graph][dofl][construction][initializer_list][sourced]") { - SECTION("construct sourced graph with initializer list") { - using G = dofl_sourced; - G g({{0, 1}, {1, 2}, {2, 0}}); - REQUIRE(g.size() == 3); - - // Verify sourced edges have source_id - auto& v0 = g[0]; - auto edges0 = v0.edges(); - REQUIRE(std::ranges::distance(edges0) == 1); - auto e0 = edges0.begin(); - REQUIRE(e0->source_id() == 0); - REQUIRE(e0->target_id() == 1); - } -} //================================================================================================== //================================================================================================== @@ -1069,8 +995,7 @@ TEST_CASE("dofl load_vertices", "[dynamic_graph][dofl][load_vertices]") { } SECTION("custom projection - load with projection from struct") { - using G = dynamic_graph>; + using G = dynamic_graph>; using vertex_data = copyable_vertex_t; struct Person { uint32_t id; @@ -1215,8 +1140,7 @@ TEST_CASE("dofl load_edges", "[dynamic_graph][dofl][load_edges]") { } SECTION("custom projection - load with projection from custom struct") { - using G = dynamic_graph>; + using G = dynamic_graph>; using vertex_data = copyable_vertex_t; using edge_data = copyable_edge_t; struct Edge { diff --git a/tests/container/dynamic_graph/test_dynamic_graph_dol.cpp b/tests/container/dynamic_graph/test_dynamic_graph_dol.cpp index ad73f86..a5858eb 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_dol.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_dol.cpp @@ -40,12 +40,8 @@ using dol_string_string_string = std::string, std::string, uint32_t, - false, - dol_graph_traits>; + false, dol_graph_traits>; -using dol_sourced = dynamic_graph>; -using dol_int_sourced = - dynamic_graph>; //================================================================================================== // 1. Construction Tests @@ -133,17 +129,6 @@ TEST_CASE("dol copy and move construction", "[dol][construction]") { } } -TEST_CASE("dol sourced construction", "[dol][construction][sourced]") { - SECTION("sourced edge construction") { - dol_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced with edge value construction") { - dol_int_sourced g; - REQUIRE(g.size() == 0); - } -} //================================================================================================== // 2. Basic Properties Tests @@ -284,13 +269,8 @@ TEST_CASE("dol_graph_traits", "[dol][traits]") { STATIC_REQUIRE(std::is_same_v); STATIC_REQUIRE(std::is_same_v); STATIC_REQUIRE(std::is_same_v); - STATIC_REQUIRE(traits::sourced == false); } - SECTION("sourced = true") { - using traits = dol_graph_traits; - STATIC_REQUIRE(traits::sourced == true); - } SECTION("vertex_id_type variations") { using traits_u64 = dol_graph_traits; @@ -415,22 +395,19 @@ TEST_CASE("dol value types", "[dol][value_types]") { } SECTION("with string edge value type") { - using graph_t = dynamic_graph>; + using graph_t = dynamic_graph>; graph_t g; REQUIRE(g.size() == 0); } SECTION("with string vertex value type") { - using graph_t = dynamic_graph>; + using graph_t = dynamic_graph>; graph_t g; REQUIRE(g.size() == 0); } SECTION("with string graph value type") { - using graph_t = dynamic_graph>; + using graph_t = dynamic_graph>; graph_t g(std::string("test")); REQUIRE(g.graph_value() == "test"); } @@ -483,41 +460,6 @@ TEST_CASE("dol vertex ID types", "[dol][vertex_id]") { // 9. Sourced Edge Tests //================================================================================================== -TEST_CASE("dol sourced edges", "[dol][sourced]") { - SECTION("sourced=false by default") { - dol_void_void_void g; - using traits = dol_graph_traits; - STATIC_REQUIRE(traits::sourced == false); - } - - SECTION("sourced=true explicit") { - dol_sourced g; - using traits = dol_graph_traits; - STATIC_REQUIRE(traits::sourced == true); - } - - SECTION("sourced with void values") { - dol_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced with int edge value") { - dol_int_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced copy construction") { - dol_sourced g1; - dol_sourced g2 = g1; - REQUIRE(g2.size() == 0); - } - - SECTION("sourced move construction") { - dol_sourced g1; - dol_sourced g2 = std::move(g1); - REQUIRE(g2.size() == 0); - } -} //================================================================================================== // 10. Const Correctness Tests @@ -615,8 +557,6 @@ TEST_CASE("dol various template instantiations compile", "[dol][compilation]") { [[maybe_unused]] dol_void_void_int g5; [[maybe_unused]] dol_int_int_int g6; [[maybe_unused]] dol_string_string_string g7; - [[maybe_unused]] dol_sourced g8; - [[maybe_unused]] dol_int_sourced g9; REQUIRE(true); // Just ensuring compilation } @@ -840,22 +780,6 @@ TEST_CASE("dol initializer_list constructor with all value types", "[dol][constr } } -TEST_CASE("dol initializer_list constructor with sourced edges", "[dol][construction][initializer_list][sourced]") { - using G = dol_sourced; - - SECTION("construct sourced graph with initializer list") { - G g({{0, 1}, {1, 2}, {2, 0}}); - REQUIRE(g.size() == 3); - - // Verify sourced edges have source_id - auto& v0 = g[0]; - auto edges0 = v0.edges(); - REQUIRE(std::ranges::distance(edges0) == 1); - auto e0 = edges0.begin(); - REQUIRE(e0->source_id() == 0); - REQUIRE(e0->target_id() == 1); - } -} TEST_CASE("dol initializer_list complex graph patterns", "[dol][construction][initializer_list]") { using G = dol_int_void_void; @@ -945,8 +869,7 @@ TEST_CASE("dol load_vertices", "[dynamic_graph][dol][load_vertices]") { } SECTION("load with custom projection from struct") { - using G2 = dynamic_graph>; + using G2 = dynamic_graph>; using vertex_data2 = copyable_vertex_t; struct Person { @@ -1080,8 +1003,7 @@ TEST_CASE("dol load_edges", "[dynamic_graph][dol][load_edges]") { } SECTION("load with custom projection from struct") { - using G3 = dynamic_graph>; + using G3 = dynamic_graph>; using vertex_data3 = copyable_vertex_t; using edge_data3 = copyable_edge_t; diff --git a/tests/container/dynamic_graph/test_dynamic_graph_dos.cpp b/tests/container/dynamic_graph/test_dynamic_graph_dos.cpp index 2327c01..885c510 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_dos.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_dos.cpp @@ -44,12 +44,8 @@ using dos_string_string_string = std::string, std::string, uint32_t, - false, - dos_graph_traits>; + false, dos_graph_traits>; -using dos_sourced = dynamic_graph>; -using dos_int_sourced = - dynamic_graph>; // Edge and vertex data types for loading using edge_void = copyable_edge_t; @@ -217,19 +213,6 @@ TEST_CASE("dos edge deduplication", "[dos][set][deduplication]") { REQUIRE(v0.edges().begin()->value() == 100); } - SECTION("sourced edges - deduplication by (source_id, target_id)") { - dos_sourced g; - std::vector ee = { - {0, 1}, - {0, 1}, // Duplicates - {1, 0}, - {1, 0} // Different direction, also duplicates - }; - g.load_edges(ee, std::identity{}); - - // Should have exactly 2 unique edges (0->1 and 1->0) - REQUIRE(count_all_edges(g) == 2); - } } //================================================================================================== @@ -253,21 +236,6 @@ TEST_CASE("dos edges are sorted by target_id", "[dos][set][sorted]") { REQUIRE(target_ids == std::vector{1, 2, 3, 5, 8}); } - SECTION("sourced edges sorted by target_id (source is same per vertex)") { - dos_sourced g; - std::vector ee = {{0, 7}, {0, 3}, {0, 9}, {0, 1}}; - g.load_edges(ee, std::identity{}); - - auto& v0 = g[0]; - std::vector target_ids; - for (const auto& edge : v0.edges()) { - target_ids.push_back(edge.target_id()); - } - - // Note: For sourced edges, comparison is (source_id, target_id) - // Since source_id is same (0) for all edges from v0, they sort by target_id - REQUIRE(target_ids == std::vector{1, 3, 7, 9}); - } } //================================================================================================== @@ -428,39 +396,6 @@ TEST_CASE("dos edge values", "[dos][edge][value]") { // 9. Sourced Edge Tests //================================================================================================== -TEST_CASE("dos sourced edges", "[dos][sourced]") { - SECTION("source_id access") { - dos_sourced g({{0, 1}, {0, 2}, {1, 0}}); - - auto& v0 = g[0]; - for (const auto& edge : v0.edges()) { - REQUIRE(edge.source_id() == 0); - } - - auto& v1 = g[1]; - for (const auto& edge : v1.edges()) { - REQUIRE(edge.source_id() == 1); - } - } - - SECTION("sourced edge with values") { - dos_int_sourced g; - std::vector ee = {{0, 1, 100}, {1, 0, 200}}; - g.load_edges(ee, std::identity{}); - - auto& v0 = g[0]; - auto it0 = v0.edges().begin(); - REQUIRE(it0->source_id() == 0); - REQUIRE(it0->target_id() == 1); - REQUIRE(it0->value() == 100); - - auto& v1 = g[1]; - auto it1 = v1.edges().begin(); - REQUIRE(it1->source_id() == 1); - REQUIRE(it1->target_id() == 0); - REQUIRE(it1->value() == 200); - } -} //================================================================================================== // 10. Self-Loop Tests @@ -707,13 +642,6 @@ TEST_CASE("dos type traits", "[dos][traits]") { static_assert(requires(vertices_t& v) { v.push_front(std::declval()); }); } - SECTION("sourced trait") { - using traits_unsourced = dos_graph_traits; - using traits_sourced = dos_graph_traits; - - static_assert(traits_unsourced::sourced == false); - static_assert(traits_sourced::sourced == true); - } } //================================================================================================== diff --git a/tests/container/dynamic_graph/test_dynamic_graph_dous.cpp b/tests/container/dynamic_graph/test_dynamic_graph_dous.cpp index cce619b..e2b1622 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_dous.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_dous.cpp @@ -49,13 +49,8 @@ using dous_string_string_string = std::string, std::string, uint32_t, - false, - dous_graph_traits>; + false, dous_graph_traits>; -using dous_sourced = - dynamic_graph>; -using dous_int_sourced = - dynamic_graph>; // Edge and vertex data types for loading using edge_void = copyable_edge_t; @@ -296,34 +291,6 @@ TEST_CASE("dous value access", "[dous][values]") { // 6. Sourced Edge Tests //================================================================================================== -TEST_CASE("dous sourced edges", "[dous][sourced]") { - SECTION("source_id access") { - dous_sourced g; - std::deque ee = {{0, 1}, {1, 2}, {0, 2}}; - g.load_edges(ee, std::identity{}); - - auto& v0 = g[0]; - for (auto& e : v0.edges()) { - REQUIRE(e.source_id() == 0); - } - - auto& v1 = g[1]; - for (auto& e : v1.edges()) { - REQUIRE(e.source_id() == 1); - } - } - - SECTION("sourced edge deduplication") { - dous_int_sourced g; - // Multiple edges from 0 to 1 with different values - std::deque ee = {{0, 1, 100}, {0, 1, 200}, {0, 1, 300}}; - g.load_edges(ee, std::identity{}); - - auto& v0 = g[0]; - // unordered_set deduplicates by (source_id, target_id) pair - REQUIRE(std::ranges::distance(v0.edges()) == 1); - } -} //================================================================================================== // 7. Unordered Set Specific Behavior diff --git a/tests/container/dynamic_graph/test_dynamic_graph_dov.cpp b/tests/container/dynamic_graph/test_dynamic_graph_dov.cpp index d33fdaa..be50c74 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_dov.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_dov.cpp @@ -42,12 +42,8 @@ using dov_string_string_string = std::string, std::string, uint32_t, - false, - dov_graph_traits>; + false, dov_graph_traits>; -using dov_sourced = dynamic_graph>; -using dov_int_sourced = - dynamic_graph>; //================================================================================================== // 1. Construction Tests @@ -135,17 +131,6 @@ TEST_CASE("dov copy and move construction", "[dov][construction]") { } } -TEST_CASE("dov sourced construction", "[dov][construction][sourced]") { - SECTION("sourced edge construction") { - dov_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced with edge value construction") { - dov_int_sourced g; - REQUIRE(g.size() == 0); - } -} //================================================================================================== // 2. Basic Properties Tests @@ -286,13 +271,8 @@ TEST_CASE("dov_graph_traits", "[dov][traits]") { STATIC_REQUIRE(std::is_same_v); STATIC_REQUIRE(std::is_same_v); STATIC_REQUIRE(std::is_same_v); - STATIC_REQUIRE(traits::sourced == false); } - SECTION("sourced = true") { - using traits = dov_graph_traits; - STATIC_REQUIRE(traits::sourced == true); - } SECTION("vertex_id_type variations") { using traits_u64 = dov_graph_traits; @@ -417,22 +397,19 @@ TEST_CASE("dov value types", "[dov][value_types]") { } SECTION("with string edge value type") { - using graph_t = dynamic_graph>; + using graph_t = dynamic_graph>; graph_t g; REQUIRE(g.size() == 0); } SECTION("with string vertex value type") { - using graph_t = dynamic_graph>; + using graph_t = dynamic_graph>; graph_t g; REQUIRE(g.size() == 0); } SECTION("with string graph value type") { - using graph_t = dynamic_graph>; + using graph_t = dynamic_graph>; graph_t g(std::string("test")); REQUIRE(g.graph_value() == "test"); } @@ -485,41 +462,6 @@ TEST_CASE("dov vertex ID types", "[dov][vertex_id]") { // 9. Sourced Edge Tests //================================================================================================== -TEST_CASE("dov sourced edges", "[dov][sourced]") { - SECTION("sourced=false by default") { - dov_void_void_void g; - using traits = dov_graph_traits; - STATIC_REQUIRE(traits::sourced == false); - } - - SECTION("sourced=true explicit") { - dov_sourced g; - using traits = dov_graph_traits; - STATIC_REQUIRE(traits::sourced == true); - } - - SECTION("sourced with void values") { - dov_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced with int edge value") { - dov_int_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced copy construction") { - dov_sourced g1; - dov_sourced g2 = g1; - REQUIRE(g2.size() == 0); - } - - SECTION("sourced move construction") { - dov_sourced g1; - dov_sourced g2 = std::move(g1); - REQUIRE(g2.size() == 0); - } -} //================================================================================================== // 10. Const Correctness Tests @@ -617,8 +559,6 @@ TEST_CASE("dov various template instantiations compile", "[dov][compilation]") { [[maybe_unused]] dov_void_void_int g5; [[maybe_unused]] dov_int_int_int g6; [[maybe_unused]] dov_string_string_string g7; - [[maybe_unused]] dov_sourced g8; - [[maybe_unused]] dov_int_sourced g9; REQUIRE(true); // Just ensuring compilation } @@ -840,22 +780,6 @@ TEST_CASE("dov initializer_list constructor with all value types", "[dov][constr } } -TEST_CASE("dov initializer_list constructor with sourced edges", "[dov][construction][initializer_list][sourced]") { - using G = dov_sourced; - - SECTION("construct sourced graph with initializer list") { - G g({{0, 1}, {1, 2}, {2, 0}}); - REQUIRE(g.size() == 3); - - // Verify sourced edges have source_id - auto& v0 = g[0]; - auto edges0 = v0.edges(); - REQUIRE(std::ranges::distance(edges0) == 1); - auto e0 = edges0.begin(); - REQUIRE(e0->source_id() == 0); - REQUIRE(e0->target_id() == 1); - } -} TEST_CASE("dov initializer_list complex graph patterns", "[dov][construction][initializer_list]") { using G = dov_int_void_void; @@ -945,8 +869,7 @@ TEST_CASE("dov load_vertices", "[dynamic_graph][dov][load_vertices]") { } SECTION("with custom projection from struct") { - using G2 = dynamic_graph>; + using G2 = dynamic_graph>; using vertex_data2 = copyable_vertex_t; struct Person { @@ -1083,8 +1006,7 @@ TEST_CASE("dov load_edges", "[dynamic_graph][dov][load_edges]") { } SECTION("with custom projection") { - using G2 = dynamic_graph>; + using G2 = dynamic_graph>; using vertex_data2 = copyable_vertex_t; using edge_data2 = copyable_edge_t; diff --git a/tests/container/dynamic_graph/test_dynamic_graph_integration.cpp b/tests/container/dynamic_graph/test_dynamic_graph_integration.cpp index 2cbb192..79139a4 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_integration.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_integration.cpp @@ -78,26 +78,22 @@ using vov_string = dynamic_graph>; + false, vov_graph_traits>; using vofl_string = dynamic_graph>; + false, vofl_graph_traits>; using mos_string = dynamic_graph>; + false, mos_graph_traits>; using mol_string = dynamic_graph>; + false, mol_graph_traits>; //================================================================================================== // Helper: Count all edges diff --git a/tests/container/dynamic_graph/test_dynamic_graph_mixed_types.cpp b/tests/container/dynamic_graph/test_dynamic_graph_mixed_types.cpp index 4245808..1da0a29 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_mixed_types.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_mixed_types.cpp @@ -42,7 +42,7 @@ size_t count_vertices(const G& g) { return count; } -// Generic has_edge function +// Generic has_edges function template bool has_edge_generic(const G& g, const VId& uid, const VId& vid) { for (auto&& u : vertices(g)) { diff --git a/tests/container/dynamic_graph/test_dynamic_graph_mod.cpp b/tests/container/dynamic_graph/test_dynamic_graph_mod.cpp index ff5e697..1622ac5 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_mod.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_mod.cpp @@ -62,11 +62,6 @@ using mod_str_int_int_int = dynamic_graph>; // Sourced edge variants (store source vertex ID in edge) -using mod_sourced = dynamic_graph>; -using mod_int_sourced = - dynamic_graph>; -using mod_str_sourced = - dynamic_graph>; //================================================================================================== // 2. Traits Verification Tests @@ -204,22 +199,6 @@ TEST_CASE("mod construction with string vertex IDs", "[dynamic_graph][mod][const } } -TEST_CASE("mod construction sourced", "[dynamic_graph][mod][construction][sourced]") { - SECTION("sourced edge construction with uint32_t IDs") { - mod_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced with edge value construction") { - mod_int_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced edge construction with string IDs") { - mod_str_sourced g; - REQUIRE(g.size() == 0); - } -} //================================================================================================== // 4. Basic Properties Tests @@ -272,15 +251,9 @@ TEST_CASE("mod type aliases", "[dynamic_graph][mod][types]") { SECTION("graph type aliases are correct") { using G = mod_int_int_int; static_assert(std::same_as); // GV - static_assert(G::sourced == false); REQUIRE(true); } - SECTION("sourced graph type aliases are correct") { - using G = mod_sourced; - static_assert(G::sourced == true); - REQUIRE(true); - } SECTION("string key graph type aliases are correct") { using G = mod_str_int_int_int; @@ -403,11 +376,6 @@ TEST_CASE("mod initializer_list construction string IDs", "[dynamic_graph][mod][ REQUIRE(g.size() == 5); } - SECTION("sourced edges with string IDs") { - using G = mod_str_sourced; - G g({{"alice", "bob"}, {"bob", "charlie"}}); - REQUIRE(g.size() == 3); - } } //================================================================================================== @@ -628,12 +596,9 @@ TEST_CASE("mod template instantiation", "[dynamic_graph][mod][compilation]") { [[maybe_unused]] mod_int_int_void g4; [[maybe_unused]] mod_void_void_int g5; [[maybe_unused]] mod_int_int_int g6; - [[maybe_unused]] mod_sourced g7; - [[maybe_unused]] mod_int_sourced g8; [[maybe_unused]] mod_str_void_void_void g9; [[maybe_unused]] mod_str_int_void_void g10; [[maybe_unused]] mod_str_int_int_int g11; - [[maybe_unused]] mod_str_sourced g12; REQUIRE(true); } diff --git a/tests/container/dynamic_graph/test_dynamic_graph_mofl.cpp b/tests/container/dynamic_graph/test_dynamic_graph_mofl.cpp index 2062e49..439f1b0 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_mofl.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_mofl.cpp @@ -53,13 +53,7 @@ using mofl_str_void_int_void = using mofl_str_int_int_int = dynamic_graph>; -using mofl_sourced = - dynamic_graph>; -using mofl_int_sourced = - dynamic_graph>; -using mofl_str_sourced = - dynamic_graph>; //================================================================================================== // 1. Traits Verification Tests @@ -91,13 +85,6 @@ TEST_CASE("mofl traits verification", "[dynamic_graph][mofl][traits]") { REQUIRE(true); } - SECTION("sourced flag is preserved") { - using traits_unsourced = mofl_graph_traits; - using traits_sourced = mofl_graph_traits; - static_assert(traits_unsourced::sourced == false); - static_assert(traits_sourced::sourced == true); - REQUIRE(true); - } SECTION("vertex_id_type for uint32_t") { using traits = mofl_graph_traits; @@ -234,22 +221,6 @@ TEST_CASE("mofl construction with string vertex IDs", "[dynamic_graph][mofl][con } } -TEST_CASE("mofl construction sourced", "[dynamic_graph][mofl][construction][sourced]") { - SECTION("sourced edge construction with uint32_t IDs") { - mofl_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced with edge value construction") { - mofl_int_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced edge construction with string IDs") { - mofl_str_sourced g; - REQUIRE(g.size() == 0); - } -} //================================================================================================== // 4. Basic Properties Tests @@ -302,15 +273,9 @@ TEST_CASE("mofl type aliases", "[dynamic_graph][mofl][types]") { SECTION("graph type aliases are correct") { using G = mofl_int_int_int; static_assert(std::same_as); // GV - static_assert(G::sourced == false); REQUIRE(true); } - SECTION("sourced graph type aliases are correct") { - using G = mofl_sourced; - static_assert(G::sourced == true); - REQUIRE(true); - } SECTION("string key graph type aliases are correct") { using G = mofl_str_int_int_int; @@ -433,11 +398,6 @@ TEST_CASE("mofl initializer_list construction string IDs", "[dynamic_graph][mofl REQUIRE(g.size() == 5); } - SECTION("sourced edges with string IDs") { - using G = mofl_str_sourced; - G g({{"alice", "bob"}, {"bob", "charlie"}}); - REQUIRE(g.size() == 3); - } } //================================================================================================== @@ -645,12 +605,9 @@ TEST_CASE("mofl template instantiation", "[dynamic_graph][mofl][compilation]") { [[maybe_unused]] mofl_int_int_void g4; [[maybe_unused]] mofl_void_void_int g5; [[maybe_unused]] mofl_int_int_int g6; - [[maybe_unused]] mofl_sourced g7; - [[maybe_unused]] mofl_int_sourced g8; [[maybe_unused]] mofl_str_void_void_void g9; [[maybe_unused]] mofl_str_int_void_void g10; [[maybe_unused]] mofl_str_int_int_int g11; - [[maybe_unused]] mofl_str_sourced g12; REQUIRE(true); } diff --git a/tests/container/dynamic_graph/test_dynamic_graph_mol.cpp b/tests/container/dynamic_graph/test_dynamic_graph_mol.cpp index e6ad4b3..aac8359 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_mol.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_mol.cpp @@ -55,12 +55,7 @@ using mol_str_void_int_void = using mol_str_int_int_int = dynamic_graph>; -using mol_sourced = dynamic_graph>; -using mol_int_sourced = - dynamic_graph>; -using mol_str_sourced = - dynamic_graph>; //================================================================================================== // 1. Traits Verification Tests @@ -91,13 +86,6 @@ TEST_CASE("mol traits verification", "[dynamic_graph][mol][traits]") { REQUIRE(true); } - SECTION("sourced flag is preserved") { - using traits_unsourced = mol_graph_traits; - using traits_sourced = mol_graph_traits; - static_assert(traits_unsourced::sourced == false); - static_assert(traits_sourced::sourced == true); - REQUIRE(true); - } SECTION("vertex_id_type for uint32_t") { using traits = mol_graph_traits; @@ -242,22 +230,6 @@ TEST_CASE("mol construction with string vertex IDs", "[dynamic_graph][mol][const } } -TEST_CASE("mol construction sourced", "[dynamic_graph][mol][construction][sourced]") { - SECTION("sourced edge construction with uint32_t IDs") { - mol_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced with edge value construction") { - mol_int_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced edge construction with string IDs") { - mol_str_sourced g; - REQUIRE(g.size() == 0); - } -} //================================================================================================== // 4. Basic Properties Tests @@ -310,15 +282,9 @@ TEST_CASE("mol type aliases", "[dynamic_graph][mol][types]") { SECTION("graph type aliases are correct") { using G = mol_int_int_int; static_assert(std::same_as); // GV - static_assert(G::sourced == false); REQUIRE(true); } - SECTION("sourced graph type aliases are correct") { - using G = mol_sourced; - static_assert(G::sourced == true); - REQUIRE(true); - } SECTION("string key graph type aliases are correct") { using G = mol_str_int_int_int; @@ -441,11 +407,6 @@ TEST_CASE("mol initializer_list construction string IDs", "[dynamic_graph][mol][ REQUIRE(g.size() == 5); } - SECTION("sourced edges with string IDs") { - using G = mol_str_sourced; - G g({{"alice", "bob"}, {"bob", "charlie"}}); - REQUIRE(g.size() == 3); - } } //================================================================================================== @@ -666,12 +627,9 @@ TEST_CASE("mol template instantiation", "[dynamic_graph][mol][compilation]") { [[maybe_unused]] mol_int_int_void g4; [[maybe_unused]] mol_void_void_int g5; [[maybe_unused]] mol_int_int_int g6; - [[maybe_unused]] mol_sourced g7; - [[maybe_unused]] mol_int_sourced g8; [[maybe_unused]] mol_str_void_void_void g9; [[maybe_unused]] mol_str_int_void_void g10; [[maybe_unused]] mol_str_int_int_int g11; - [[maybe_unused]] mol_str_sourced g12; REQUIRE(true); } diff --git a/tests/container/dynamic_graph/test_dynamic_graph_mom.cpp b/tests/container/dynamic_graph/test_dynamic_graph_mom.cpp index 13d2dc3..628f3c5 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_mom.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_mom.cpp @@ -53,13 +53,7 @@ using mos_str_void_int_void = using mos_str_int_int_int = dynamic_graph>; -using mos_sourced = - dynamic_graph>; -using mos_int_sourced = - dynamic_graph>; -using mos_str_sourced = - dynamic_graph>; // Edge and vertex data types for loading using edge_void = copyable_edge_t; @@ -104,13 +98,6 @@ TEST_CASE("mom traits verification", "[dynamic_graph][mom][traits]") { REQUIRE(true); } - SECTION("sourced flag is preserved") { - using traits_unsourced = mom_graph_traits; - using traits_sourced = mom_graph_traits; - static_assert(traits_unsourced::sourced == false); - static_assert(traits_sourced::sourced == true); - REQUIRE(true); - } SECTION("vertex_id_type for uint32_t") { using traits = mom_graph_traits; @@ -245,22 +232,6 @@ TEST_CASE("mom construction with string vertex IDs", "[dynamic_graph][mom][const } } -TEST_CASE("mom construction sourced", "[dynamic_graph][mom][construction][sourced]") { - SECTION("sourced edge construction with uint32_t IDs") { - mos_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced with edge value construction") { - mos_int_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced edge construction with string IDs") { - mos_str_sourced g; - REQUIRE(g.size() == 0); - } -} //================================================================================================== // 4. Basic Properties Tests @@ -410,19 +381,6 @@ TEST_CASE("mom edge deduplication", "[dynamic_graph][mom][set][deduplication]") REQUIRE(count_all_edges(g) == 1); } - SECTION("sourced edges - deduplication by (source_id, target_id)") { - mos_sourced g; - std::vector ee = { - {0, 1}, - {0, 1}, // Duplicates - {1, 0}, - {1, 0} // Different direction, also duplicates - }; - g.load_edges(ee, std::identity{}); - - // Should have exactly 2 unique edges (0->1 and 1->0) - REQUIRE(count_all_edges(g) == 2); - } } //================================================================================================== @@ -448,21 +406,6 @@ TEST_CASE("mom edges are sorted by target_id", "[dynamic_graph][mom][set][sorted REQUIRE(target_ids == std::vector{1, 2, 3, 5, 8}); } - SECTION("sourced edges sorted by target_id") { - mos_sourced g; - std::vector ee = {{0, 7}, {0, 3}, {0, 9}, {0, 1}}; - g.load_edges(ee, std::identity{}); - - auto it = g.try_find_vertex(0); - REQUIRE(it != g.end()); - - std::vector target_ids; - for (const auto& edge : it->second.edges()) { - target_ids.push_back(edge.second.target_id()); - } - - REQUIRE(target_ids == std::vector{1, 3, 7, 9}); - } } //================================================================================================== @@ -492,11 +435,6 @@ TEST_CASE("mom initializer_list construction string IDs", "[dynamic_graph][mom][ REQUIRE(g.size() == 5); } - SECTION("sourced edges with string IDs") { - using G = mos_str_sourced; - G g({{"alice", "bob"}, {"bob", "charlie"}}); - REQUIRE(g.size() == 3); - } SECTION("string deduplication") { using G = mos_str_void_void_void; @@ -1012,12 +950,9 @@ TEST_CASE("mom template instantiation", "[dynamic_graph][mom][compilation]") { [[maybe_unused]] mos_int_int_void g4; [[maybe_unused]] mos_void_void_int g5; [[maybe_unused]] mos_int_int_int g6; - [[maybe_unused]] mos_sourced g7; - [[maybe_unused]] mos_int_sourced g8; [[maybe_unused]] mos_str_void_void_void g9; [[maybe_unused]] mos_str_int_void_void g10; [[maybe_unused]] mos_str_int_int_int g11; - [[maybe_unused]] mos_str_sourced g12; REQUIRE(true); } @@ -1090,40 +1025,3 @@ TEST_CASE("mom edge bidirectional iteration", "[dynamic_graph][mom][set][iterati // 20. Sourced Edge Tests //================================================================================================== -TEST_CASE("mom sourced edges", "[dynamic_graph][mom][sourced]") { - SECTION("source_id access") { - mos_sourced g({{0, 1}, {0, 2}, {1, 0}}); - - auto it0 = g.try_find_vertex(0); - REQUIRE(it0 != g.end()); - for (const auto& edge : it0->second.edges()) { - REQUIRE(edge.second.source_id() == 0); - } - - auto it1 = g.try_find_vertex(1); - REQUIRE(it1 != g.end()); - for (const auto& edge : it1->second.edges()) { - REQUIRE(edge.second.source_id() == 1); - } - } - - SECTION("sourced edge with values") { - mos_int_sourced g; - std::vector ee = {{0, 1, 100}, {1, 0, 200}}; - g.load_edges(ee, std::identity{}); - - auto it0 = g.try_find_vertex(0); - REQUIRE(it0 != g.end()); - auto e0 = it0->second.edges().begin(); - REQUIRE(e0->second.source_id() == 0); - REQUIRE(e0->second.target_id() == 1); - REQUIRE(e0->second.value() == 100); - - auto it1 = g.try_find_vertex(1); - REQUIRE(it1 != g.end()); - auto e1 = it1->second.edges().begin(); - REQUIRE(e1->second.source_id() == 1); - REQUIRE(e1->second.target_id() == 0); - REQUIRE(e1->second.value() == 200); - } -} diff --git a/tests/container/dynamic_graph/test_dynamic_graph_mos.cpp b/tests/container/dynamic_graph/test_dynamic_graph_mos.cpp index 609e612..e389e7e 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_mos.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_mos.cpp @@ -52,12 +52,7 @@ using mos_str_void_int_void = using mos_str_int_int_int = dynamic_graph>; -using mos_sourced = dynamic_graph>; -using mos_int_sourced = - dynamic_graph>; -using mos_str_sourced = - dynamic_graph>; // Edge and vertex data types for loading using edge_void = copyable_edge_t; @@ -102,13 +97,6 @@ TEST_CASE("mos traits verification", "[dynamic_graph][mos][traits]") { REQUIRE(true); } - SECTION("sourced flag is preserved") { - using traits_unsourced = mos_graph_traits; - using traits_sourced = mos_graph_traits; - static_assert(traits_unsourced::sourced == false); - static_assert(traits_sourced::sourced == true); - REQUIRE(true); - } SECTION("vertex_id_type for uint32_t") { using traits = mos_graph_traits; @@ -243,22 +231,6 @@ TEST_CASE("mos construction with string vertex IDs", "[dynamic_graph][mos][const } } -TEST_CASE("mos construction sourced", "[dynamic_graph][mos][construction][sourced]") { - SECTION("sourced edge construction with uint32_t IDs") { - mos_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced with edge value construction") { - mos_int_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced edge construction with string IDs") { - mos_str_sourced g; - REQUIRE(g.size() == 0); - } -} //================================================================================================== // 4. Basic Properties Tests @@ -408,19 +380,6 @@ TEST_CASE("mos edge deduplication", "[dynamic_graph][mos][set][deduplication]") REQUIRE(count_all_edges(g) == 1); } - SECTION("sourced edges - deduplication by (source_id, target_id)") { - mos_sourced g; - std::vector ee = { - {0, 1}, - {0, 1}, // Duplicates - {1, 0}, - {1, 0} // Different direction, also duplicates - }; - g.load_edges(ee, std::identity{}); - - // Should have exactly 2 unique edges (0->1 and 1->0) - REQUIRE(count_all_edges(g) == 2); - } } //================================================================================================== @@ -446,21 +405,6 @@ TEST_CASE("mos edges are sorted by target_id", "[dynamic_graph][mos][set][sorted REQUIRE(target_ids == std::vector{1, 2, 3, 5, 8}); } - SECTION("sourced edges sorted by target_id") { - mos_sourced g; - std::vector ee = {{0, 7}, {0, 3}, {0, 9}, {0, 1}}; - g.load_edges(ee, std::identity{}); - - auto it = g.try_find_vertex(0); - REQUIRE(it != g.end()); - - std::vector target_ids; - for (const auto& edge : it->second.edges()) { - target_ids.push_back(edge.target_id()); - } - - REQUIRE(target_ids == std::vector{1, 3, 7, 9}); - } } //================================================================================================== @@ -490,11 +434,6 @@ TEST_CASE("mos initializer_list construction string IDs", "[dynamic_graph][mos][ REQUIRE(g.size() == 5); } - SECTION("sourced edges with string IDs") { - using G = mos_str_sourced; - G g({{"alice", "bob"}, {"bob", "charlie"}}); - REQUIRE(g.size() == 3); - } SECTION("string deduplication") { using G = mos_str_void_void_void; @@ -1010,12 +949,9 @@ TEST_CASE("mos template instantiation", "[dynamic_graph][mos][compilation]") { [[maybe_unused]] mos_int_int_void g4; [[maybe_unused]] mos_void_void_int g5; [[maybe_unused]] mos_int_int_int g6; - [[maybe_unused]] mos_sourced g7; - [[maybe_unused]] mos_int_sourced g8; [[maybe_unused]] mos_str_void_void_void g9; [[maybe_unused]] mos_str_int_void_void g10; [[maybe_unused]] mos_str_int_int_int g11; - [[maybe_unused]] mos_str_sourced g12; REQUIRE(true); } @@ -1088,40 +1024,3 @@ TEST_CASE("mos edge bidirectional iteration", "[dynamic_graph][mos][set][iterati // 20. Sourced Edge Tests //================================================================================================== -TEST_CASE("mos sourced edges", "[dynamic_graph][mos][sourced]") { - SECTION("source_id access") { - mos_sourced g({{0, 1}, {0, 2}, {1, 0}}); - - auto it0 = g.try_find_vertex(0); - REQUIRE(it0 != g.end()); - for (const auto& edge : it0->second.edges()) { - REQUIRE(edge.source_id() == 0); - } - - auto it1 = g.try_find_vertex(1); - REQUIRE(it1 != g.end()); - for (const auto& edge : it1->second.edges()) { - REQUIRE(edge.source_id() == 1); - } - } - - SECTION("sourced edge with values") { - mos_int_sourced g; - std::vector ee = {{0, 1, 100}, {1, 0, 200}}; - g.load_edges(ee, std::identity{}); - - auto it0 = g.try_find_vertex(0); - REQUIRE(it0 != g.end()); - auto e0 = it0->second.edges().begin(); - REQUIRE(e0->source_id() == 0); - REQUIRE(e0->target_id() == 1); - REQUIRE(e0->value() == 100); - - auto it1 = g.try_find_vertex(1); - REQUIRE(it1 != g.end()); - auto e1 = it1->second.edges().begin(); - REQUIRE(e1->source_id() == 1); - REQUIRE(e1->target_id() == 0); - REQUIRE(e1->value() == 200); - } -} diff --git a/tests/container/dynamic_graph/test_dynamic_graph_mous.cpp b/tests/container/dynamic_graph/test_dynamic_graph_mous.cpp index 16a1237..e490b22 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_mous.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_mous.cpp @@ -54,13 +54,7 @@ using mous_str_void_int_void = using mous_str_int_int_int = dynamic_graph>; -using mous_sourced = - dynamic_graph>; -using mous_int_sourced = - dynamic_graph>; -using mous_str_sourced = - dynamic_graph>; // Edge and vertex data types for loading using edge_void = copyable_edge_t; @@ -105,13 +99,6 @@ TEST_CASE("mous traits verification", "[dynamic_graph][mous][traits]") { REQUIRE(true); } - SECTION("sourced flag is preserved") { - using traits_unsourced = mous_graph_traits; - using traits_sourced = mous_graph_traits; - static_assert(traits_unsourced::sourced == false); - static_assert(traits_sourced::sourced == true); - REQUIRE(true); - } SECTION("vertex_id_type for uint32_t") { using traits = mous_graph_traits; @@ -246,22 +233,6 @@ TEST_CASE("mous construction with string vertex IDs", "[dynamic_graph][mous][con } } -TEST_CASE("mous construction sourced", "[dynamic_graph][mous][construction][sourced]") { - SECTION("sourced edge construction with uint32_t IDs") { - mous_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced with edge value construction") { - mous_int_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced edge construction with string IDs") { - mous_str_sourced g; - REQUIRE(g.size() == 0); - } -} //================================================================================================== // 4. Basic Properties Tests @@ -411,19 +382,6 @@ TEST_CASE("mous edge deduplication", "[dynamic_graph][mous][unordered_set][dedup REQUIRE(count_all_edges(g) == 1); } - SECTION("sourced edges - deduplication by (source_id, target_id)") { - mous_sourced g; - std::vector ee = { - {0, 1}, - {0, 1}, // Duplicates - {1, 0}, - {1, 0} // Different direction, also duplicates - }; - g.load_edges(ee, std::identity{}); - - // Should have exactly 2 unique edges (0->1 and 1->0) - REQUIRE(count_all_edges(g) == 2); - } } //================================================================================================== @@ -450,22 +408,6 @@ TEST_CASE("mous edges are unordered by target_id", "[dynamic_graph][mous][unorde REQUIRE(target_ids == std::vector{1, 2, 3, 5, 8}); } - SECTION("sourced edges unordered by target_id") { - mous_sourced g; - std::vector ee = {{0, 7}, {0, 3}, {0, 9}, {0, 1}}; - g.load_edges(ee, std::identity{}); - - auto it = g.try_find_vertex(0); - REQUIRE(it != g.end()); - - std::vector target_ids; - for (const auto& edge : it->second.edges()) { - target_ids.push_back(edge.target_id()); - } - std::ranges::sort(target_ids); - - REQUIRE(target_ids == std::vector{1, 3, 7, 9}); - } } //================================================================================================== @@ -495,11 +437,6 @@ TEST_CASE("mous initializer_list construction string IDs", "[dynamic_graph][mous REQUIRE(g.size() == 5); } - SECTION("sourced edges with string IDs") { - using G = mous_str_sourced; - G g({{"alice", "bob"}, {"bob", "charlie"}}); - REQUIRE(g.size() == 3); - } SECTION("string deduplication") { using G = mous_str_void_void_void; @@ -1015,12 +952,9 @@ TEST_CASE("mous template instantiation", "[dynamic_graph][mous][compilation]") { [[maybe_unused]] mous_int_int_void g4; [[maybe_unused]] mous_void_void_int g5; [[maybe_unused]] mous_int_int_int g6; - [[maybe_unused]] mous_sourced g7; - [[maybe_unused]] mous_int_sourced g8; [[maybe_unused]] mous_str_void_void_void g9; [[maybe_unused]] mous_str_int_void_void g10; [[maybe_unused]] mous_str_int_int_int g11; - [[maybe_unused]] mous_str_sourced g12; REQUIRE(true); } @@ -1096,40 +1030,3 @@ TEST_CASE("mous edge bidirectional iteration", "[dynamic_graph][mous][unordered_ // 20. Sourced Edge Tests //================================================================================================== -TEST_CASE("mous sourced edges", "[dynamic_graph][mous][sourced]") { - SECTION("source_id access") { - mous_sourced g({{0, 1}, {0, 2}, {1, 0}}); - - auto it0 = g.try_find_vertex(0); - REQUIRE(it0 != g.end()); - for (const auto& edge : it0->second.edges()) { - REQUIRE(edge.source_id() == 0); - } - - auto it1 = g.try_find_vertex(1); - REQUIRE(it1 != g.end()); - for (const auto& edge : it1->second.edges()) { - REQUIRE(edge.source_id() == 1); - } - } - - SECTION("sourced edge with values") { - mous_int_sourced g; - std::vector ee = {{0, 1, 100}, {1, 0, 200}}; - g.load_edges(ee, std::identity{}); - - auto it0 = g.try_find_vertex(0); - REQUIRE(it0 != g.end()); - auto e0 = it0->second.edges().begin(); - REQUIRE(e0->source_id() == 0); - REQUIRE(e0->target_id() == 1); - REQUIRE(e0->value() == 100); - - auto it1 = g.try_find_vertex(1); - REQUIRE(it1 != g.end()); - auto e1 = it1->second.edges().begin(); - REQUIRE(e1->source_id() == 1); - REQUIRE(e1->target_id() == 0); - REQUIRE(e1->value() == 200); - } -} diff --git a/tests/container/dynamic_graph/test_dynamic_graph_mov.cpp b/tests/container/dynamic_graph/test_dynamic_graph_mov.cpp index 1ea0e86..0092961 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_mov.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_mov.cpp @@ -60,12 +60,7 @@ using mov_str_void_int_void = using mov_str_int_int_int = dynamic_graph>; -using mov_sourced = dynamic_graph>; -using mov_int_sourced = - dynamic_graph>; -using mov_str_sourced = - dynamic_graph>; //================================================================================================== // 1. Traits Verification Tests @@ -98,13 +93,6 @@ TEST_CASE("mov traits verification", "[dynamic_graph][mov][traits]") { REQUIRE(true); } - SECTION("sourced flag is preserved") { - using traits_unsourced = mov_graph_traits; - using traits_sourced = mov_graph_traits; - static_assert(traits_unsourced::sourced == false); - static_assert(traits_sourced::sourced == true); - REQUIRE(true); - } SECTION("vertex_id_type for uint32_t") { using traits = mov_graph_traits; @@ -248,22 +236,6 @@ TEST_CASE("mov construction with string vertex IDs", "[dynamic_graph][mov][const } } -TEST_CASE("mov construction sourced", "[dynamic_graph][mov][construction][sourced]") { - SECTION("sourced edge construction with uint32_t IDs") { - mov_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced with edge value construction") { - mov_int_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced edge construction with string IDs") { - mov_str_sourced g; - REQUIRE(g.size() == 0); - } -} //================================================================================================== // 4. Basic Properties Tests @@ -316,15 +288,9 @@ TEST_CASE("mov type aliases", "[dynamic_graph][mov][types]") { SECTION("graph type aliases are correct") { using G = mov_int_int_int; static_assert(std::same_as); // GV - static_assert(G::sourced == false); REQUIRE(true); } - SECTION("sourced graph type aliases are correct") { - using G = mov_sourced; - static_assert(G::sourced == true); - REQUIRE(true); - } SECTION("string key graph type aliases are correct") { using G = mov_str_int_int_int; @@ -447,11 +413,6 @@ TEST_CASE("mov initializer_list construction string IDs", "[dynamic_graph][mov][ REQUIRE(g.size() == 5); } - SECTION("sourced edges with string IDs") { - using G = mov_str_sourced; - G g({{"alice", "bob"}, {"bob", "charlie"}}); - REQUIRE(g.size() == 3); - } } //================================================================================================== @@ -672,12 +633,9 @@ TEST_CASE("mov template instantiation", "[dynamic_graph][mov][compilation]") { [[maybe_unused]] mov_int_int_void g4; [[maybe_unused]] mov_void_void_int g5; [[maybe_unused]] mov_int_int_int g6; - [[maybe_unused]] mov_sourced g7; - [[maybe_unused]] mov_int_sourced g8; [[maybe_unused]] mov_str_void_void_void g9; [[maybe_unused]] mov_str_int_void_void g10; [[maybe_unused]] mov_str_int_int_int g11; - [[maybe_unused]] mov_str_sourced g12; REQUIRE(true); } diff --git a/tests/container/dynamic_graph/test_dynamic_graph_nonintegral_ids.cpp b/tests/container/dynamic_graph/test_dynamic_graph_nonintegral_ids.cpp index 60a0c80..51820c9 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_nonintegral_ids.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_nonintegral_ids.cpp @@ -64,15 +64,11 @@ using mous_string = dynamic_graph>; using mos_string_ev = dynamic_graph>; -using mos_string_sourced = - dynamic_graph>; // Double ID graphs using mos_double = dynamic_graph>; using mous_double = dynamic_graph>; using mos_double_ev = dynamic_graph>; -using mos_double_sourced = - dynamic_graph>; // PersonId (compound type) graphs using mos_person = @@ -240,22 +236,6 @@ TEST_CASE("string ID edge cases - long strings", "[nonintegral][string][performa } } -TEST_CASE("string ID - sourced edges", "[nonintegral][string][sourced]") { - SECTION("sourced graph with string IDs") { - mos_string_sourced g({{"alice", "bob"}, {"bob", "charlie"}}); - REQUIRE(g.size() == 3); - - auto alice = find_vertex(g, "alice"); - REQUIRE(alice != vertices(g).end()); - - auto edge_rng = edges(g, *alice); - auto edge = *std::ranges::begin(edge_rng); - - // Verify source_id returns the correct string - REQUIRE(source_id(g, edge) == "alice"); - REQUIRE(target_id(g, edge) == "bob"); - } -} //================================================================================================== // PART 2: Double/Floating-Point Vertex IDs @@ -436,18 +416,6 @@ TEST_CASE("double ID - unordered_map", "[nonintegral][double][unordered]") { } } -TEST_CASE("double ID - sourced edges", "[nonintegral][double][sourced]") { - SECTION("source_id returns double") { - mos_double_sourced g({{1.0, 2.0}, {2.0, 3.0}}); - - auto v = find_vertex(g, 1.0); - auto edge_rng = edges(g, *v); - auto edge = *std::ranges::begin(edge_rng); - - REQUIRE(source_id(g, edge) == 1.0); - REQUIRE(target_id(g, edge) == 2.0); - } -} //================================================================================================== // PART 3: Compound/Custom Type Vertex IDs (PersonId) diff --git a/tests/container/dynamic_graph/test_dynamic_graph_traversal_helpers.cpp b/tests/container/dynamic_graph/test_dynamic_graph_traversal_helpers.cpp index 9886149..b76f692 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_traversal_helpers.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_traversal_helpers.cpp @@ -142,7 +142,7 @@ size_t count_self_loops(const G& g) { } // ============================================================================ -// Test Cases: has_edge +// Test Cases: has_edges // ============================================================================ TEST_CASE("generic_has_edge - empty graph (vov)", "[6.3.2][generic_has_edge][traversal]") { diff --git a/tests/container/dynamic_graph/test_dynamic_graph_type_erasure.cpp b/tests/container/dynamic_graph/test_dynamic_graph_type_erasure.cpp index a16cf7c..e44985f 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_type_erasure.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_type_erasure.cpp @@ -21,7 +21,7 @@ class graph_view { virtual size_t num_vertices() const = 0; virtual size_t num_edges() const = 0; - virtual bool has_edge(const VId& u, const VId& v) const = 0; + virtual bool has_edges(const VId& u, const VId& v) const = 0; virtual std::vector get_vertex_ids() const = 0; virtual std::vector> get_edges() const = 0; }; @@ -51,7 +51,7 @@ class graph_wrapper : public graph_view::vertex_ return count; } - bool has_edge(const VId& uid, const VId& vid) const override { + bool has_edges(const VId& uid, const VId& vid) const override { for (auto&& u : vertices(graph_)) { if (vertex_id(graph_, u) == uid) { for (auto&& e : edges(graph_, u)) { @@ -129,20 +129,20 @@ TEST_CASE("graph_view wraps graph with edges", "[6.3.5][type-erasure][edges]") { REQUIRE(view->num_vertices() == 3); REQUIRE(view->num_edges() == 3); - REQUIRE(view->has_edge(0, 1)); - REQUIRE(view->has_edge(1, 2)); - REQUIRE(view->has_edge(0, 2)); - REQUIRE_FALSE(view->has_edge(2, 0)); + REQUIRE(view->has_edges(0, 1)); + REQUIRE(view->has_edges(1, 2)); + REQUIRE(view->has_edges(0, 2)); + REQUIRE_FALSE(view->has_edges(2, 0)); } -TEST_CASE("graph_view has_edge on non-existent edge", "[6.3.5][type-erasure][has-edge]") { +TEST_CASE("graph_view has_edges on non-existent edge", "[6.3.5][type-erasure][has-edge]") { vov_void g({{0, 1}}); auto view = make_graph_view(g); - REQUIRE(view->has_edge(0, 1)); - REQUIRE_FALSE(view->has_edge(1, 0)); - REQUIRE_FALSE(view->has_edge(0, 2)); + REQUIRE(view->has_edges(0, 1)); + REQUIRE_FALSE(view->has_edges(1, 0)); + REQUIRE_FALSE(view->has_edges(0, 2)); } TEST_CASE("graph_view get_vertex_ids returns all vertices", "[6.3.5][type-erasure][vertices]") { @@ -176,7 +176,7 @@ TEST_CASE("graph_view with self-loop", "[6.3.5][type-erasure][self-loop]") { REQUIRE(view->num_vertices() == 1); REQUIRE(view->num_edges() == 1); - REQUIRE(view->has_edge(0, 0)); + REQUIRE(view->has_edges(0, 0)); } TEST_CASE("graph_view wraps mos graph", "[6.3.5][type-erasure][mos]") { @@ -186,9 +186,9 @@ TEST_CASE("graph_view wraps mos graph", "[6.3.5][type-erasure][mos]") { REQUIRE(view->num_vertices() == 3); REQUIRE(view->num_edges() == 2); - REQUIRE(view->has_edge(10, 20)); - REQUIRE(view->has_edge(20, 30)); - REQUIRE_FALSE(view->has_edge(10, 30)); + REQUIRE(view->has_edges(10, 20)); + REQUIRE(view->has_edges(20, 30)); + REQUIRE_FALSE(view->has_edges(10, 30)); } TEST_CASE("graph_view wraps dofl graph", "[6.3.5][type-erasure][dofl]") { @@ -254,9 +254,9 @@ TEST_CASE("graph_view handles cycle", "[6.3.5][type-erasure][cycle]") { REQUIRE(view->num_vertices() == 3); REQUIRE(view->num_edges() == 3); - REQUIRE(view->has_edge(0, 1)); - REQUIRE(view->has_edge(1, 2)); - REQUIRE(view->has_edge(2, 0)); + REQUIRE(view->has_edges(0, 1)); + REQUIRE(view->has_edges(1, 2)); + REQUIRE(view->has_edges(2, 0)); } TEST_CASE("graph_view with disconnected components", "[6.3.5][type-erasure][disconnected]") { @@ -266,10 +266,10 @@ TEST_CASE("graph_view with disconnected components", "[6.3.5][type-erasure][disc REQUIRE(view->num_vertices() == 4); REQUIRE(view->num_edges() == 2); - REQUIRE(view->has_edge(0, 1)); - REQUIRE(view->has_edge(2, 3)); - REQUIRE_FALSE(view->has_edge(0, 2)); - REQUIRE_FALSE(view->has_edge(1, 3)); + REQUIRE(view->has_edges(0, 1)); + REQUIRE(view->has_edges(2, 3)); + REQUIRE_FALSE(view->has_edges(0, 2)); + REQUIRE_FALSE(view->has_edges(1, 3)); } TEST_CASE("graph_view polymorphic function call", "[6.3.5][type-erasure][function]") { @@ -325,8 +325,8 @@ TEST_CASE("graph_view multiple self-loops", "[6.3.5][type-erasure][multi-self-lo REQUIRE(view->num_vertices() == 2); REQUIRE(view->num_edges() == 2); - REQUIRE(view->has_edge(0, 0)); - REQUIRE(view->has_edge(1, 1)); + REQUIRE(view->has_edges(0, 0)); + REQUIRE(view->has_edges(1, 1)); } TEST_CASE("graph_view with star topology", "[6.3.5][type-erasure][star]") { @@ -336,8 +336,8 @@ TEST_CASE("graph_view with star topology", "[6.3.5][type-erasure][star]") { REQUIRE(view->num_vertices() == 5); REQUIRE(view->num_edges() == 4); - REQUIRE(view->has_edge(0, 1)); - REQUIRE(view->has_edge(0, 2)); - REQUIRE(view->has_edge(0, 3)); - REQUIRE(view->has_edge(0, 4)); + REQUIRE(view->has_edges(0, 1)); + REQUIRE(view->has_edges(0, 2)); + REQUIRE(view->has_edges(0, 3)); + REQUIRE(view->has_edges(0, 4)); } diff --git a/tests/container/dynamic_graph/test_dynamic_graph_uod.cpp b/tests/container/dynamic_graph/test_dynamic_graph_uod.cpp index 4fa54f1..db89a0c 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_uod.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_uod.cpp @@ -58,12 +58,7 @@ using uod_str_void_int_void = using uod_str_int_int_int = dynamic_graph>; -using uod_sourced = dynamic_graph>; -using uod_int_sourced = - dynamic_graph>; -using uod_str_sourced = - dynamic_graph>; //================================================================================================== // 1. Traits Verification Tests @@ -105,13 +100,6 @@ TEST_CASE("uod traits verification", "[dynamic_graph][uod][traits]") { REQUIRE(true); } - SECTION("sourced flag is preserved") { - using traits_unsourced = uod_graph_traits; - using traits_sourced = uod_graph_traits; - static_assert(traits_unsourced::sourced == false); - static_assert(traits_sourced::sourced == true); - REQUIRE(true); - } SECTION("vertex_id_type for uint32_t") { using traits = uod_graph_traits; @@ -245,22 +233,6 @@ TEST_CASE("uod construction with string vertex IDs", "[dynamic_graph][uod][const } } -TEST_CASE("uod construction sourced", "[dynamic_graph][uod][construction][sourced]") { - SECTION("sourced edge construction with uint32_t IDs") { - uod_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced with edge value construction") { - uod_int_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced edge construction with string IDs") { - uod_str_sourced g; - REQUIRE(g.size() == 0); - } -} //================================================================================================== // 4. Basic Properties Tests @@ -313,15 +285,9 @@ TEST_CASE("uod type aliases", "[dynamic_graph][uod][types]") { SECTION("graph type aliases are correct") { using G = uod_int_int_int; static_assert(std::same_as); // GV - static_assert(G::sourced == false); REQUIRE(true); } - SECTION("sourced graph type aliases are correct") { - using G = uod_sourced; - static_assert(G::sourced == true); - REQUIRE(true); - } SECTION("string key graph type aliases are correct") { using G = uod_str_int_int_int; @@ -444,11 +410,6 @@ TEST_CASE("uod initializer_list construction string IDs", "[dynamic_graph][uod][ REQUIRE(g.size() == 5); } - SECTION("sourced edges with string IDs") { - using G = uod_str_sourced; - G g({{"alice", "bob"}, {"bob", "charlie"}}); - REQUIRE(g.size() == 3); - } } //================================================================================================== @@ -676,12 +637,9 @@ TEST_CASE("uod template instantiation", "[dynamic_graph][uod][compilation]") { [[maybe_unused]] uod_int_int_void g4; [[maybe_unused]] uod_void_void_int g5; [[maybe_unused]] uod_int_int_int g6; - [[maybe_unused]] uod_sourced g7; - [[maybe_unused]] uod_int_sourced g8; [[maybe_unused]] uod_str_void_void_void g9; [[maybe_unused]] uod_str_int_void_void g10; [[maybe_unused]] uod_str_int_int_int g11; - [[maybe_unused]] uod_str_sourced g12; REQUIRE(true); } diff --git a/tests/container/dynamic_graph/test_dynamic_graph_uofl.cpp b/tests/container/dynamic_graph/test_dynamic_graph_uofl.cpp index 26245d4..d8148c8 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_uofl.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_uofl.cpp @@ -59,13 +59,7 @@ using uofl_str_void_int_void = using uofl_str_int_int_int = dynamic_graph>; -using uofl_sourced = - dynamic_graph>; -using uofl_int_sourced = - dynamic_graph>; -using uofl_str_sourced = - dynamic_graph>; //================================================================================================== // 1. Traits Verification Tests @@ -99,13 +93,6 @@ TEST_CASE("uofl traits verification", "[dynamic_graph][uofl][traits]") { REQUIRE(true); } - SECTION("sourced flag is preserved") { - using traits_unsourced = uofl_graph_traits; - using traits_sourced = uofl_graph_traits; - static_assert(traits_unsourced::sourced == false); - static_assert(traits_sourced::sourced == true); - REQUIRE(true); - } SECTION("vertex_id_type for uint32_t") { using traits = uofl_graph_traits; @@ -240,22 +227,6 @@ TEST_CASE("uofl construction with string vertex IDs", "[dynamic_graph][uofl][con } } -TEST_CASE("uofl construction sourced", "[dynamic_graph][uofl][construction][sourced]") { - SECTION("sourced edge construction with uint32_t IDs") { - uofl_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced with edge value construction") { - uofl_int_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced edge construction with string IDs") { - uofl_str_sourced g; - REQUIRE(g.size() == 0); - } -} //================================================================================================== // 4. Basic Properties Tests @@ -308,15 +279,9 @@ TEST_CASE("uofl type aliases", "[dynamic_graph][uofl][types]") { SECTION("graph type aliases are correct") { using G = uofl_int_int_int; static_assert(std::same_as); // GV - static_assert(G::sourced == false); REQUIRE(true); } - SECTION("sourced graph type aliases are correct") { - using G = uofl_sourced; - static_assert(G::sourced == true); - REQUIRE(true); - } SECTION("string key graph type aliases are correct") { using G = uofl_str_int_int_int; @@ -439,11 +404,6 @@ TEST_CASE("uofl initializer_list construction string IDs", "[dynamic_graph][uofl REQUIRE(g.size() == 5); } - SECTION("sourced edges with string IDs") { - using G = uofl_str_sourced; - G g({{"alice", "bob"}, {"bob", "charlie"}}); - REQUIRE(g.size() == 3); - } } //================================================================================================== @@ -671,12 +631,9 @@ TEST_CASE("uofl template instantiation", "[dynamic_graph][uofl][compilation]") { [[maybe_unused]] uofl_int_int_void g4; [[maybe_unused]] uofl_void_void_int g5; [[maybe_unused]] uofl_int_int_int g6; - [[maybe_unused]] uofl_sourced g7; - [[maybe_unused]] uofl_int_sourced g8; [[maybe_unused]] uofl_str_void_void_void g9; [[maybe_unused]] uofl_str_int_void_void g10; [[maybe_unused]] uofl_str_int_int_int g11; - [[maybe_unused]] uofl_str_sourced g12; REQUIRE(true); } diff --git a/tests/container/dynamic_graph/test_dynamic_graph_uol.cpp b/tests/container/dynamic_graph/test_dynamic_graph_uol.cpp index 3679c3c..e14c045 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_uol.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_uol.cpp @@ -58,12 +58,7 @@ using uol_str_void_int_void = using uol_str_int_int_int = dynamic_graph>; -using uol_sourced = dynamic_graph>; -using uol_int_sourced = - dynamic_graph>; -using uol_str_sourced = - dynamic_graph>; //================================================================================================== // 1. Traits Verification Tests @@ -103,13 +98,6 @@ TEST_CASE("uol traits verification", "[dynamic_graph][uol][traits]") { REQUIRE(true); } - SECTION("sourced flag is preserved") { - using traits_unsourced = uol_graph_traits; - using traits_sourced = uol_graph_traits; - static_assert(traits_unsourced::sourced == false); - static_assert(traits_sourced::sourced == true); - REQUIRE(true); - } SECTION("vertex_id_type for uint32_t") { using traits = uol_graph_traits; @@ -243,22 +231,6 @@ TEST_CASE("uol construction with string vertex IDs", "[dynamic_graph][uol][const } } -TEST_CASE("uol construction sourced", "[dynamic_graph][uol][construction][sourced]") { - SECTION("sourced edge construction with uint32_t IDs") { - uol_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced with edge value construction") { - uol_int_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced edge construction with string IDs") { - uol_str_sourced g; - REQUIRE(g.size() == 0); - } -} //================================================================================================== // 4. Basic Properties Tests @@ -311,15 +283,9 @@ TEST_CASE("uol type aliases", "[dynamic_graph][uol][types]") { SECTION("graph type aliases are correct") { using G = uol_int_int_int; static_assert(std::same_as); // GV - static_assert(G::sourced == false); REQUIRE(true); } - SECTION("sourced graph type aliases are correct") { - using G = uol_sourced; - static_assert(G::sourced == true); - REQUIRE(true); - } SECTION("string key graph type aliases are correct") { using G = uol_str_int_int_int; @@ -442,11 +408,6 @@ TEST_CASE("uol initializer_list construction string IDs", "[dynamic_graph][uol][ REQUIRE(g.size() == 5); } - SECTION("sourced edges with string IDs") { - using G = uol_str_sourced; - G g({{"alice", "bob"}, {"bob", "charlie"}}); - REQUIRE(g.size() == 3); - } } //================================================================================================== @@ -674,12 +635,9 @@ TEST_CASE("uol template instantiation", "[dynamic_graph][uol][compilation]") { [[maybe_unused]] uol_int_int_void g4; [[maybe_unused]] uol_void_void_int g5; [[maybe_unused]] uol_int_int_int g6; - [[maybe_unused]] uol_sourced g7; - [[maybe_unused]] uol_int_sourced g8; [[maybe_unused]] uol_str_void_void_void g9; [[maybe_unused]] uol_str_int_void_void g10; [[maybe_unused]] uol_str_int_int_int g11; - [[maybe_unused]] uol_str_sourced g12; REQUIRE(true); } diff --git a/tests/container/dynamic_graph/test_dynamic_graph_uos.cpp b/tests/container/dynamic_graph/test_dynamic_graph_uos.cpp index bf252db..19b7e36 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_uos.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_uos.cpp @@ -53,12 +53,7 @@ using uos_str_void_int_void = using uos_str_int_int_int = dynamic_graph>; -using uos_sourced = dynamic_graph>; -using uos_int_sourced = - dynamic_graph>; -using uos_str_sourced = - dynamic_graph>; // Edge and vertex data types for loading using edge_void = copyable_edge_t; @@ -103,13 +98,6 @@ TEST_CASE("uos traits verification", "[dynamic_graph][uos][traits]") { REQUIRE(true); } - SECTION("sourced flag is preserved") { - using traits_unsourced = uos_graph_traits; - using traits_sourced = uos_graph_traits; - static_assert(traits_unsourced::sourced == false); - static_assert(traits_sourced::sourced == true); - REQUIRE(true); - } SECTION("vertex_id_type for uint32_t") { using traits = uos_graph_traits; @@ -247,22 +235,6 @@ TEST_CASE("uos construction with string vertex IDs", "[dynamic_graph][uos][const } } -TEST_CASE("uos construction sourced", "[dynamic_graph][uos][construction][sourced]") { - SECTION("sourced edge construction with uint32_t IDs") { - uos_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced with edge value construction") { - uos_int_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced edge construction with string IDs") { - uos_str_sourced g; - REQUIRE(g.size() == 0); - } -} //================================================================================================== // 4. Basic Properties Tests @@ -412,19 +384,6 @@ TEST_CASE("uos edge deduplication", "[dynamic_graph][uos][set][deduplication]") REQUIRE(count_all_edges(g) == 1); } - SECTION("sourced edges - deduplication by (source_id, target_id)") { - uos_sourced g; - std::vector ee = { - {0, 1}, - {0, 1}, // Duplicates - {1, 0}, - {1, 0} // Different direction, also duplicates - }; - g.load_edges(ee, std::identity{}); - - // Should have exactly 2 unique edges (0->1 and 1->0) - REQUIRE(count_all_edges(g) == 2); - } } //================================================================================================== @@ -450,21 +409,6 @@ TEST_CASE("uos edges are sorted by target_id", "[dynamic_graph][uos][set][sorted REQUIRE(target_ids == std::vector{1, 2, 3, 5, 8}); } - SECTION("sourced edges sorted by target_id") { - uos_sourced g; - std::vector ee = {{0, 7}, {0, 3}, {0, 9}, {0, 1}}; - g.load_edges(ee, std::identity{}); - - auto it = g.try_find_vertex(0); - REQUIRE(it != g.end()); - - std::vector target_ids; - for (const auto& edge : it->second.edges()) { - target_ids.push_back(edge.target_id()); - } - - REQUIRE(target_ids == std::vector{1, 3, 7, 9}); - } } //================================================================================================== @@ -494,11 +438,6 @@ TEST_CASE("uos initializer_list construction string IDs", "[dynamic_graph][uos][ REQUIRE(g.size() == 5); } - SECTION("sourced edges with string IDs") { - using G = uos_str_sourced; - G g({{"alice", "bob"}, {"bob", "charlie"}}); - REQUIRE(g.size() == 3); - } SECTION("string deduplication") { using G = uos_str_void_void_void; @@ -1020,12 +959,9 @@ TEST_CASE("uos template instantiation", "[dynamic_graph][uos][compilation]") { [[maybe_unused]] uos_int_int_void g4; [[maybe_unused]] uos_void_void_int g5; [[maybe_unused]] uos_int_int_int g6; - [[maybe_unused]] uos_sourced g7; - [[maybe_unused]] uos_int_sourced g8; [[maybe_unused]] uos_str_void_void_void g9; [[maybe_unused]] uos_str_int_void_void g10; [[maybe_unused]] uos_str_int_int_int g11; - [[maybe_unused]] uos_str_sourced g12; REQUIRE(true); } @@ -1098,40 +1034,3 @@ TEST_CASE("uos edge bidirectional iteration", "[dynamic_graph][uos][set][iterati // 20. Sourced Edge Tests //================================================================================================== -TEST_CASE("uos sourced edges", "[dynamic_graph][uos][sourced]") { - SECTION("source_id access") { - uos_sourced g({{0, 1}, {0, 2}, {1, 0}}); - - auto it0 = g.try_find_vertex(0); - REQUIRE(it0 != g.end()); - for (const auto& edge : it0->second.edges()) { - REQUIRE(edge.source_id() == 0); - } - - auto it1 = g.try_find_vertex(1); - REQUIRE(it1 != g.end()); - for (const auto& edge : it1->second.edges()) { - REQUIRE(edge.source_id() == 1); - } - } - - SECTION("sourced edge with values") { - uos_int_sourced g; - std::vector ee = {{0, 1, 100}, {1, 0, 200}}; - g.load_edges(ee, std::identity{}); - - auto it0 = g.try_find_vertex(0); - REQUIRE(it0 != g.end()); - auto e0 = it0->second.edges().begin(); - REQUIRE(e0->source_id() == 0); - REQUIRE(e0->target_id() == 1); - REQUIRE(e0->value() == 100); - - auto it1 = g.try_find_vertex(1); - REQUIRE(it1 != g.end()); - auto e1 = it1->second.edges().begin(); - REQUIRE(e1->source_id() == 1); - REQUIRE(e1->target_id() == 0); - REQUIRE(e1->value() == 200); - } -} diff --git a/tests/container/dynamic_graph/test_dynamic_graph_uous.cpp b/tests/container/dynamic_graph/test_dynamic_graph_uous.cpp index 1128e72..e5db30f 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_uous.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_uous.cpp @@ -54,13 +54,7 @@ using uous_str_void_int_void = using uous_str_int_int_int = dynamic_graph>; -using uous_sourced = - dynamic_graph>; -using uous_int_sourced = - dynamic_graph>; -using uous_str_sourced = - dynamic_graph>; // Edge and vertex data types for loading using edge_void = copyable_edge_t; @@ -105,13 +99,6 @@ TEST_CASE("uous traits verification", "[dynamic_graph][uous][traits]") { REQUIRE(true); } - SECTION("sourced flag is preserved") { - using traits_unsourced = uous_graph_traits; - using traits_sourced = uous_graph_traits; - static_assert(traits_unsourced::sourced == false); - static_assert(traits_sourced::sourced == true); - REQUIRE(true); - } SECTION("vertex_id_type for uint32_t") { using traits = uous_graph_traits; @@ -247,22 +234,6 @@ TEST_CASE("uous construction with string vertex IDs", "[dynamic_graph][uous][con } } -TEST_CASE("uous construction sourced", "[dynamic_graph][uous][construction][sourced]") { - SECTION("sourced edge construction with uint32_t IDs") { - uous_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced with edge value construction") { - uous_int_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced edge construction with string IDs") { - uous_str_sourced g; - REQUIRE(g.size() == 0); - } -} //================================================================================================== // 4. Basic Properties Tests @@ -412,19 +383,6 @@ TEST_CASE("uous edge deduplication", "[dynamic_graph][uous][unordered_set][dedup REQUIRE(count_all_edges(g) == 1); } - SECTION("sourced edges - deduplication by (source_id, target_id)") { - uous_sourced g; - std::vector ee = { - {0, 1}, - {0, 1}, // Duplicates - {1, 0}, - {1, 0} // Different direction, also duplicates - }; - g.load_edges(ee, std::identity{}); - - // Should have exactly 2 unique edges (0->1 and 1->0) - REQUIRE(count_all_edges(g) == 2); - } } //================================================================================================== @@ -451,22 +409,6 @@ TEST_CASE("uous edges are unordered by target_id", "[dynamic_graph][uous][unorde REQUIRE(target_ids == std::vector{1, 2, 3, 5, 8}); } - SECTION("sourced edges unordered by target_id") { - uous_sourced g; - std::vector ee = {{0, 7}, {0, 3}, {0, 9}, {0, 1}}; - g.load_edges(ee, std::identity{}); - - auto it = g.try_find_vertex(0); - REQUIRE(it != g.end()); - - std::vector target_ids; - for (const auto& edge : it->second.edges()) { - target_ids.push_back(edge.target_id()); - } - std::ranges::sort(target_ids); - - REQUIRE(target_ids == std::vector{1, 3, 7, 9}); - } } //================================================================================================== @@ -496,11 +438,6 @@ TEST_CASE("uous initializer_list construction string IDs", "[dynamic_graph][uous REQUIRE(g.size() == 5); } - SECTION("sourced edges with string IDs") { - using G = uous_str_sourced; - G g({{"alice", "bob"}, {"bob", "charlie"}}); - REQUIRE(g.size() == 3); - } SECTION("string deduplication") { using G = uous_str_void_void_void; @@ -1016,12 +953,9 @@ TEST_CASE("uous template instantiation", "[dynamic_graph][uous][compilation]") { [[maybe_unused]] uous_int_int_void g4; [[maybe_unused]] uous_void_void_int g5; [[maybe_unused]] uous_int_int_int g6; - [[maybe_unused]] uous_sourced g7; - [[maybe_unused]] uous_int_sourced g8; [[maybe_unused]] uous_str_void_void_void g9; [[maybe_unused]] uous_str_int_void_void g10; [[maybe_unused]] uous_str_int_int_int g11; - [[maybe_unused]] uous_str_sourced g12; REQUIRE(true); } @@ -1097,40 +1031,3 @@ TEST_CASE("uous edge forward iteration only", "[dynamic_graph][uous][unordered_s // 20. Sourced Edge Tests //================================================================================================== -TEST_CASE("uous sourced edges", "[dynamic_graph][uous][sourced]") { - SECTION("source_id access") { - uous_sourced g({{0, 1}, {0, 2}, {1, 0}}); - - auto it0 = g.try_find_vertex(0); - REQUIRE(it0 != g.end()); - for (const auto& edge : it0->second.edges()) { - REQUIRE(edge.source_id() == 0); - } - - auto it1 = g.try_find_vertex(1); - REQUIRE(it1 != g.end()); - for (const auto& edge : it1->second.edges()) { - REQUIRE(edge.source_id() == 1); - } - } - - SECTION("sourced edge with values") { - uous_int_sourced g; - std::vector ee = {{0, 1, 100}, {1, 0, 200}}; - g.load_edges(ee, std::identity{}); - - auto it0 = g.try_find_vertex(0); - REQUIRE(it0 != g.end()); - auto e0 = it0->second.edges().begin(); - REQUIRE(e0->source_id() == 0); - REQUIRE(e0->target_id() == 1); - REQUIRE(e0->value() == 100); - - auto it1 = g.try_find_vertex(1); - REQUIRE(it1 != g.end()); - auto e1 = it1->second.edges().begin(); - REQUIRE(e1->source_id() == 1); - REQUIRE(e1->target_id() == 0); - REQUIRE(e1->value() == 200); - } -} diff --git a/tests/container/dynamic_graph/test_dynamic_graph_uov.cpp b/tests/container/dynamic_graph/test_dynamic_graph_uov.cpp index 7e70ae9..0bf8c75 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_uov.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_uov.cpp @@ -58,12 +58,7 @@ using uov_str_void_int_void = using uov_str_int_int_int = dynamic_graph>; -using uov_sourced = dynamic_graph>; -using uov_int_sourced = - dynamic_graph>; -using uov_str_sourced = - dynamic_graph>; //================================================================================================== // 1. Traits Verification Tests @@ -104,13 +99,6 @@ TEST_CASE("uov traits verification", "[dynamic_graph][uov][traits]") { REQUIRE(true); } - SECTION("sourced flag is preserved") { - using traits_unsourced = uov_graph_traits; - using traits_sourced = uov_graph_traits; - static_assert(traits_unsourced::sourced == false); - static_assert(traits_sourced::sourced == true); - REQUIRE(true); - } SECTION("vertex_id_type for uint32_t") { using traits = uov_graph_traits; @@ -244,22 +232,6 @@ TEST_CASE("uov construction with string vertex IDs", "[dynamic_graph][uov][const } } -TEST_CASE("uov construction sourced", "[dynamic_graph][uov][construction][sourced]") { - SECTION("sourced edge construction with uint32_t IDs") { - uov_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced with edge value construction") { - uov_int_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced edge construction with string IDs") { - uov_str_sourced g; - REQUIRE(g.size() == 0); - } -} //================================================================================================== // 4. Basic Properties Tests @@ -312,15 +284,9 @@ TEST_CASE("uov type aliases", "[dynamic_graph][uov][types]") { SECTION("graph type aliases are correct") { using G = uov_int_int_int; static_assert(std::same_as); // GV - static_assert(G::sourced == false); REQUIRE(true); } - SECTION("sourced graph type aliases are correct") { - using G = uov_sourced; - static_assert(G::sourced == true); - REQUIRE(true); - } SECTION("string key graph type aliases are correct") { using G = uov_str_int_int_int; @@ -443,11 +409,6 @@ TEST_CASE("uov initializer_list construction string IDs", "[dynamic_graph][uov][ REQUIRE(g.size() == 5); } - SECTION("sourced edges with string IDs") { - using G = uov_str_sourced; - G g({{"alice", "bob"}, {"bob", "charlie"}}); - REQUIRE(g.size() == 3); - } } //================================================================================================== @@ -675,12 +636,9 @@ TEST_CASE("uov template instantiation", "[dynamic_graph][uov][compilation]") { [[maybe_unused]] uov_int_int_void g4; [[maybe_unused]] uov_void_void_int g5; [[maybe_unused]] uov_int_int_int g6; - [[maybe_unused]] uov_sourced g7; - [[maybe_unused]] uov_int_sourced g8; [[maybe_unused]] uov_str_void_void_void g9; [[maybe_unused]] uov_str_int_void_void g10; [[maybe_unused]] uov_str_int_int_int g11; - [[maybe_unused]] uov_str_sourced g12; REQUIRE(true); } diff --git a/tests/container/dynamic_graph/test_dynamic_graph_vod.cpp b/tests/container/dynamic_graph/test_dynamic_graph_vod.cpp index 828db12..4f03444 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_vod.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_vod.cpp @@ -48,12 +48,8 @@ using vod_string_string_string = std::string, std::string, uint32_t, - false, - vod_graph_traits>; + false, vod_graph_traits>; -using vod_sourced = dynamic_graph>; -using vod_int_sourced = - dynamic_graph>; //================================================================================================== // 1. Construction Tests @@ -141,17 +137,6 @@ TEST_CASE("vod copy and move construction", "[vod][construction]") { } } -TEST_CASE("vod sourced construction", "[vod][construction][sourced]") { - SECTION("sourced edge construction") { - vod_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced with edge value construction") { - vod_int_sourced g; - REQUIRE(g.size() == 0); - } -} //================================================================================================== // 2. Basic Properties Tests @@ -292,13 +277,8 @@ TEST_CASE("vod_graph_traits", "[vod][traits]") { STATIC_REQUIRE(std::is_same_v); STATIC_REQUIRE(std::is_same_v); STATIC_REQUIRE(std::is_same_v); - STATIC_REQUIRE(traits::sourced == false); } - SECTION("sourced = true") { - using traits = vod_graph_traits; - STATIC_REQUIRE(traits::sourced == true); - } SECTION("vertex_id_type variations") { using traits_u64 = vod_graph_traits; @@ -423,22 +403,19 @@ TEST_CASE("vod value types", "[vod][value_types]") { } SECTION("with string edge value type") { - using graph_t = dynamic_graph>; + using graph_t = dynamic_graph>; graph_t g; REQUIRE(g.size() == 0); } SECTION("with string vertex value type") { - using graph_t = dynamic_graph>; + using graph_t = dynamic_graph>; graph_t g; REQUIRE(g.size() == 0); } SECTION("with string graph value type") { - using graph_t = dynamic_graph>; + using graph_t = dynamic_graph>; graph_t g(std::string("test")); REQUIRE(g.graph_value() == "test"); } @@ -491,41 +468,6 @@ TEST_CASE("vod vertex ID types", "[vod][vertex_id]") { // 9. Sourced Edge Tests //================================================================================================== -TEST_CASE("vod sourced edges", "[vod][sourced]") { - SECTION("sourced=false by default") { - vod_void_void_void g; - using traits = vod_graph_traits; - STATIC_REQUIRE(traits::sourced == false); - } - - SECTION("sourced=true explicit") { - vod_sourced g; - using traits = vod_graph_traits; - STATIC_REQUIRE(traits::sourced == true); - } - - SECTION("sourced with void values") { - vod_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced with int edge value") { - vod_int_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced copy construction") { - vod_sourced g1; - vod_sourced g2 = g1; - REQUIRE(g2.size() == 0); - } - - SECTION("sourced move construction") { - vod_sourced g1; - vod_sourced g2 = std::move(g1); - REQUIRE(g2.size() == 0); - } -} //================================================================================================== // 10. Const Correctness Tests @@ -623,8 +565,6 @@ TEST_CASE("vod various template instantiations compile", "[vod][compilation]") { [[maybe_unused]] vod_void_void_int g5; [[maybe_unused]] vod_int_int_int g6; [[maybe_unused]] vod_string_string_string g7; - [[maybe_unused]] vod_sourced g8; - [[maybe_unused]] vod_int_sourced g9; REQUIRE(true); // Just ensuring compilation } @@ -846,22 +786,6 @@ TEST_CASE("vod initializer_list constructor with all value types", "[vod][constr } } -TEST_CASE("vod initializer_list constructor with sourced edges", "[vod][construction][initializer_list][sourced]") { - using G = vod_sourced; - - SECTION("construct sourced graph with initializer list") { - G g({{0, 1}, {1, 2}, {2, 0}}); - REQUIRE(g.size() == 3); - - // Verify sourced edges have source_id - auto& v0 = g[0]; - auto edges0 = v0.edges(); - REQUIRE(std::ranges::distance(edges0) == 1); - auto e0 = edges0.begin(); - REQUIRE(e0->source_id() == 0); - REQUIRE(e0->target_id() == 1); - } -} TEST_CASE("vod initializer_list complex graph patterns", "[vod][construction][initializer_list]") { using G = vod_int_void_void; @@ -951,8 +875,7 @@ TEST_CASE("vod load_vertices", "[dynamic_graph][vod][load_vertices]") { } SECTION("with custom projection from struct") { - using G2 = dynamic_graph>; + using G2 = dynamic_graph>; using vertex_data2 = copyable_vertex_t; struct Person { @@ -1089,8 +1012,7 @@ TEST_CASE("vod load_edges", "[dynamic_graph][vod][load_edges]") { } SECTION("with custom projection") { - using G2 = dynamic_graph>; + using G2 = dynamic_graph>; using vertex_data2 = copyable_vertex_t; using edge_data2 = copyable_edge_t; diff --git a/tests/container/dynamic_graph/test_dynamic_graph_vofl.cpp b/tests/container/dynamic_graph/test_dynamic_graph_vofl.cpp index d61fbdb..6769f2a 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_vofl.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_vofl.cpp @@ -39,13 +39,8 @@ using vofl_string_string_string = std::string, std::string, uint32_t, - false, - vofl_graph_traits>; + false, vofl_graph_traits>; -using vofl_sourced = - dynamic_graph>; -using vofl_int_sourced = - dynamic_graph>; //================================================================================================== // 1. Construction Tests (40 tests) @@ -133,17 +128,6 @@ TEST_CASE("vofl construction", "[dynamic_graph][vofl][construction]") { } } -TEST_CASE("vofl construction sourced", "[dynamic_graph][vofl][construction][sourced]") { - SECTION("sourced edge construction") { - vofl_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced with edge value construction") { - vofl_int_sourced g; - REQUIRE(g.size() == 0); - } -} //================================================================================================== // 2. Basic Properties Tests (20 tests) @@ -409,12 +393,10 @@ TEST_CASE("vofl traits", "[dynamic_graph][vofl][traits]") { STATIC_REQUIRE(std::is_same_v); STATIC_REQUIRE(std::is_same_v); STATIC_REQUIRE(std::is_same_v); - STATIC_REQUIRE(traits::sourced == false); } SECTION("vofl_graph_traits sourced = true") { using traits = vofl_graph_traits; - STATIC_REQUIRE(traits::sourced == true); } SECTION("vertex_id_type variations") { @@ -541,22 +523,19 @@ TEST_CASE("vofl value_types", "[dynamic_graph][vofl][value_types]") { } SECTION("with string edge value type") { - using graph_t = dynamic_graph>; + using graph_t = dynamic_graph>; graph_t g; REQUIRE(g.size() == 0); } SECTION("with string vertex value type") { - using graph_t = dynamic_graph>; + using graph_t = dynamic_graph>; graph_t g; REQUIRE(g.size() == 0); } SECTION("with string graph value type") { - using graph_t = dynamic_graph>; + using graph_t = dynamic_graph>; graph_t g(std::string("test")); REQUIRE(g.graph_value() == "test"); } @@ -610,41 +589,6 @@ TEST_CASE("vofl vertex_id", "[dynamic_graph][vofl][vertex_id]") { // 9. Sourced Edge Tests (15 tests) //================================================================================================== -TEST_CASE("vofl sourced", "[dynamic_graph][vofl][sourced]") { - SECTION("sourced=false by default") { - vofl_void_void_void g; - using traits = vofl_graph_traits; - STATIC_REQUIRE(traits::sourced == false); - } - - SECTION("sourced=true explicit") { - vofl_sourced g; - using traits = vofl_graph_traits; - STATIC_REQUIRE(traits::sourced == true); - } - - SECTION("sourced with void values") { - vofl_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced with int edge value") { - vofl_int_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced copy construction") { - vofl_sourced g1; - vofl_sourced g2 = g1; - REQUIRE(g2.size() == 0); - } - - SECTION("sourced move construction") { - vofl_sourced g1; - vofl_sourced g2 = std::move(g1); - REQUIRE(g2.size() == 0); - } -} //================================================================================================== // 10. Const Correctness Tests (15 tests) @@ -742,8 +686,6 @@ TEST_CASE("vofl various template instantiations compile", "[dynamic_graph][vofl] [[maybe_unused]] vofl_void_void_int g5; [[maybe_unused]] vofl_int_int_int g6; [[maybe_unused]] vofl_string_string_string g7; - [[maybe_unused]] vofl_sourced g8; - [[maybe_unused]] vofl_int_sourced g9; REQUIRE(true); // Just ensuring compilation } @@ -1012,22 +954,6 @@ TEST_CASE("vofl construction initializer_list", "[dynamic_graph][vofl][construct } } -TEST_CASE("vofl construction initializer_list sourced", - "[dynamic_graph][vofl][construction][initializer_list][sourced]") { - SECTION("construct sourced graph with initializer list") { - using G = vofl_sourced; - G g({{0, 1}, {1, 2}, {2, 0}}); - REQUIRE(g.size() == 3); - - // Verify sourced edges have source_id - auto& v0 = g[0]; - auto edges0 = v0.edges(); - REQUIRE(std::ranges::distance(edges0) == 1); - auto e0 = edges0.begin(); - REQUIRE(e0->source_id() == 0); - REQUIRE(e0->target_id() == 1); - } -} //================================================================================================== //================================================================================================== @@ -1069,8 +995,7 @@ TEST_CASE("vofl load_vertices", "[dynamic_graph][vofl][load_vertices]") { } SECTION("custom projection - load with projection from struct") { - using G = dynamic_graph>; + using G = dynamic_graph>; using vertex_data = copyable_vertex_t; struct Person { uint32_t id; @@ -1215,8 +1140,7 @@ TEST_CASE("vofl load_edges", "[dynamic_graph][vofl][load_edges]") { } SECTION("custom projection - load with projection from custom struct") { - using G = dynamic_graph>; + using G = dynamic_graph>; using vertex_data = copyable_vertex_t; using edge_data = copyable_edge_t; struct Edge { diff --git a/tests/container/dynamic_graph/test_dynamic_graph_vol.cpp b/tests/container/dynamic_graph/test_dynamic_graph_vol.cpp index c096ea4..b4c7e4f 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_vol.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_vol.cpp @@ -40,12 +40,8 @@ using vol_string_string_string = std::string, std::string, uint32_t, - false, - vol_graph_traits>; + false, vol_graph_traits>; -using vol_sourced = dynamic_graph>; -using vol_int_sourced = - dynamic_graph>; //================================================================================================== // 1. Construction Tests @@ -133,17 +129,6 @@ TEST_CASE("vol copy and move construction", "[vol][construction]") { } } -TEST_CASE("vol sourced construction", "[vol][construction][sourced]") { - SECTION("sourced edge construction") { - vol_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced with edge value construction") { - vol_int_sourced g; - REQUIRE(g.size() == 0); - } -} //================================================================================================== // 2. Basic Properties Tests @@ -284,13 +269,8 @@ TEST_CASE("vol_graph_traits", "[vol][traits]") { STATIC_REQUIRE(std::is_same_v); STATIC_REQUIRE(std::is_same_v); STATIC_REQUIRE(std::is_same_v); - STATIC_REQUIRE(traits::sourced == false); } - SECTION("sourced = true") { - using traits = vol_graph_traits; - STATIC_REQUIRE(traits::sourced == true); - } SECTION("vertex_id_type variations") { using traits_u64 = vol_graph_traits; @@ -415,22 +395,19 @@ TEST_CASE("vol value types", "[vol][value_types]") { } SECTION("with string edge value type") { - using graph_t = dynamic_graph>; + using graph_t = dynamic_graph>; graph_t g; REQUIRE(g.size() == 0); } SECTION("with string vertex value type") { - using graph_t = dynamic_graph>; + using graph_t = dynamic_graph>; graph_t g; REQUIRE(g.size() == 0); } SECTION("with string graph value type") { - using graph_t = dynamic_graph>; + using graph_t = dynamic_graph>; graph_t g(std::string("test")); REQUIRE(g.graph_value() == "test"); } @@ -483,41 +460,6 @@ TEST_CASE("vol vertex ID types", "[vol][vertex_id]") { // 9. Sourced Edge Tests //================================================================================================== -TEST_CASE("vol sourced edges", "[vol][sourced]") { - SECTION("sourced=false by default") { - vol_void_void_void g; - using traits = vol_graph_traits; - STATIC_REQUIRE(traits::sourced == false); - } - - SECTION("sourced=true explicit") { - vol_sourced g; - using traits = vol_graph_traits; - STATIC_REQUIRE(traits::sourced == true); - } - - SECTION("sourced with void values") { - vol_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced with int edge value") { - vol_int_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced copy construction") { - vol_sourced g1; - vol_sourced g2 = g1; - REQUIRE(g2.size() == 0); - } - - SECTION("sourced move construction") { - vol_sourced g1; - vol_sourced g2 = std::move(g1); - REQUIRE(g2.size() == 0); - } -} //================================================================================================== // 10. Const Correctness Tests @@ -615,8 +557,6 @@ TEST_CASE("vol various template instantiations compile", "[vol][compilation]") { [[maybe_unused]] vol_void_void_int g5; [[maybe_unused]] vol_int_int_int g6; [[maybe_unused]] vol_string_string_string g7; - [[maybe_unused]] vol_sourced g8; - [[maybe_unused]] vol_int_sourced g9; REQUIRE(true); // Just ensuring compilation } @@ -840,22 +780,6 @@ TEST_CASE("vol initializer_list constructor with all value types", "[vol][constr } } -TEST_CASE("vol initializer_list constructor with sourced edges", "[vol][construction][initializer_list][sourced]") { - using G = vol_sourced; - - SECTION("construct sourced graph with initializer list") { - G g({{0, 1}, {1, 2}, {2, 0}}); - REQUIRE(g.size() == 3); - - // Verify sourced edges have source_id - auto& v0 = g[0]; - auto edges0 = v0.edges(); - REQUIRE(std::ranges::distance(edges0) == 1); - auto e0 = edges0.begin(); - REQUIRE(e0->source_id() == 0); - REQUIRE(e0->target_id() == 1); - } -} TEST_CASE("vol initializer_list complex graph patterns", "[vol][construction][initializer_list]") { using G = vol_int_void_void; @@ -945,8 +869,7 @@ TEST_CASE("vol load_vertices", "[dynamic_graph][vol][load_vertices]") { } SECTION("load with custom projection from struct") { - using G2 = dynamic_graph>; + using G2 = dynamic_graph>; using vertex_data2 = copyable_vertex_t; struct Person { @@ -1080,8 +1003,7 @@ TEST_CASE("vol load_edges", "[dynamic_graph][vol][load_edges]") { } SECTION("load with custom projection from struct") { - using G3 = dynamic_graph>; + using G3 = dynamic_graph>; using vertex_data3 = copyable_vertex_t; using edge_data3 = copyable_edge_t; diff --git a/tests/container/dynamic_graph/test_dynamic_graph_vom.cpp b/tests/container/dynamic_graph/test_dynamic_graph_vom.cpp index 45c4371..b334e5b 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_vom.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_vom.cpp @@ -45,13 +45,8 @@ using vos_string_string_string = std::string, std::string, uint32_t, - false, - vom_graph_traits>; + false, vom_graph_traits>; -using vos_sourced = - dynamic_graph>; -using vos_int_sourced = - dynamic_graph>; // Edge and vertex data types for loading using edge_void = copyable_edge_t; @@ -219,19 +214,6 @@ TEST_CASE("vom edge deduplication", "[vom][set][deduplication]") { REQUIRE(v0.edges().begin()->second.value() == 100); } - SECTION("sourced edges - deduplication by (source_id, target_id)") { - vos_sourced g; - std::vector ee = { - {0, 1}, - {0, 1}, // Duplicates - {1, 0}, - {1, 0} // Different direction, also duplicates - }; - g.load_edges(ee, std::identity{}); - - // Should have exactly 2 unique edges (0->1 and 1->0) - REQUIRE(count_all_edges(g) == 2); - } } //================================================================================================== @@ -255,21 +237,6 @@ TEST_CASE("vom edges are sorted by target_id", "[vom][set][sorted]") { REQUIRE(target_ids == std::vector{1, 2, 3, 5, 8}); } - SECTION("sourced edges sorted by target_id (source is same per vertex)") { - vos_sourced g; - std::vector ee = {{0, 7}, {0, 3}, {0, 9}, {0, 1}}; - g.load_edges(ee, std::identity{}); - - auto& v0 = g[0]; - std::vector target_ids; - for (const auto& edge : v0.edges()) { - target_ids.push_back(edge.second.target_id()); - } - - // Note: For sourced edges, comparison is (source_id, target_id) - // Since source_id is same (0) for all edges from v0, they sort by target_id - REQUIRE(target_ids == std::vector{1, 3, 7, 9}); - } } //================================================================================================== @@ -430,39 +397,6 @@ TEST_CASE("vom edge values", "[vom][edge][value]") { // 9. Sourced Edge Tests //================================================================================================== -TEST_CASE("vom sourced edges", "[vom][sourced]") { - SECTION("source_id access") { - vos_sourced g({{0, 1}, {0, 2}, {1, 0}}); - - auto& v0 = g[0]; - for (const auto& edge : v0.edges()) { - REQUIRE(edge.second.source_id() == 0); - } - - auto& v1 = g[1]; - for (const auto& edge : v1.edges()) { - REQUIRE(edge.second.source_id() == 1); - } - } - - SECTION("sourced edge with values") { - vos_int_sourced g; - std::vector ee = {{0, 1, 100}, {1, 0, 200}}; - g.load_edges(ee, std::identity{}); - - auto& v0 = g[0]; - auto it0 = v0.edges().begin(); - REQUIRE(it0->second.source_id() == 0); - REQUIRE(it0->second.target_id() == 1); - REQUIRE(it0->second.value() == 100); - - auto& v1 = g[1]; - auto it1 = v1.edges().begin(); - REQUIRE(it1->second.source_id() == 1); - REQUIRE(it1->second.target_id() == 0); - REQUIRE(it1->second.value() == 200); - } -} //================================================================================================== // 10. Self-Loop Tests @@ -674,13 +608,6 @@ TEST_CASE("vom type traits", "[vom][traits]") { static_assert(requires { typename edges_t::key_type; }); } - SECTION("sourced trait") { - using traits_unsourced = vom_graph_traits; - using traits_sourced = vom_graph_traits; - - static_assert(traits_unsourced::sourced == false); - static_assert(traits_sourced::sourced == true); - } } //================================================================================================== diff --git a/tests/container/dynamic_graph/test_dynamic_graph_vos.cpp b/tests/container/dynamic_graph/test_dynamic_graph_vos.cpp index ea2c6cd..d7c66e5 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_vos.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_vos.cpp @@ -44,12 +44,8 @@ using vos_string_string_string = std::string, std::string, uint32_t, - false, - vos_graph_traits>; + false, vos_graph_traits>; -using vos_sourced = dynamic_graph>; -using vos_int_sourced = - dynamic_graph>; // Edge and vertex data types for loading using edge_void = copyable_edge_t; @@ -217,19 +213,6 @@ TEST_CASE("vos edge deduplication", "[vos][set][deduplication]") { REQUIRE(v0.edges().begin()->value() == 100); } - SECTION("sourced edges - deduplication by (source_id, target_id)") { - vos_sourced g; - std::vector ee = { - {0, 1}, - {0, 1}, // Duplicates - {1, 0}, - {1, 0} // Different direction, also duplicates - }; - g.load_edges(ee, std::identity{}); - - // Should have exactly 2 unique edges (0->1 and 1->0) - REQUIRE(count_all_edges(g) == 2); - } } //================================================================================================== @@ -253,21 +236,6 @@ TEST_CASE("vos edges are sorted by target_id", "[vos][set][sorted]") { REQUIRE(target_ids == std::vector{1, 2, 3, 5, 8}); } - SECTION("sourced edges sorted by target_id (source is same per vertex)") { - vos_sourced g; - std::vector ee = {{0, 7}, {0, 3}, {0, 9}, {0, 1}}; - g.load_edges(ee, std::identity{}); - - auto& v0 = g[0]; - std::vector target_ids; - for (const auto& edge : v0.edges()) { - target_ids.push_back(edge.target_id()); - } - - // Note: For sourced edges, comparison is (source_id, target_id) - // Since source_id is same (0) for all edges from v0, they sort by target_id - REQUIRE(target_ids == std::vector{1, 3, 7, 9}); - } } //================================================================================================== @@ -428,39 +396,6 @@ TEST_CASE("vos edge values", "[vos][edge][value]") { // 9. Sourced Edge Tests //================================================================================================== -TEST_CASE("vos sourced edges", "[vos][sourced]") { - SECTION("source_id access") { - vos_sourced g({{0, 1}, {0, 2}, {1, 0}}); - - auto& v0 = g[0]; - for (const auto& edge : v0.edges()) { - REQUIRE(edge.source_id() == 0); - } - - auto& v1 = g[1]; - for (const auto& edge : v1.edges()) { - REQUIRE(edge.source_id() == 1); - } - } - - SECTION("sourced edge with values") { - vos_int_sourced g; - std::vector ee = {{0, 1, 100}, {1, 0, 200}}; - g.load_edges(ee, std::identity{}); - - auto& v0 = g[0]; - auto it0 = v0.edges().begin(); - REQUIRE(it0->source_id() == 0); - REQUIRE(it0->target_id() == 1); - REQUIRE(it0->value() == 100); - - auto& v1 = g[1]; - auto it1 = v1.edges().begin(); - REQUIRE(it1->source_id() == 1); - REQUIRE(it1->target_id() == 0); - REQUIRE(it1->value() == 200); - } -} //================================================================================================== // 10. Self-Loop Tests @@ -672,13 +607,6 @@ TEST_CASE("vos type traits", "[vos][traits]") { static_assert(requires { typename edges_t::key_type; }); } - SECTION("sourced trait") { - using traits_unsourced = vos_graph_traits; - using traits_sourced = vos_graph_traits; - - static_assert(traits_unsourced::sourced == false); - static_assert(traits_sourced::sourced == true); - } } //================================================================================================== diff --git a/tests/container/dynamic_graph/test_dynamic_graph_voum.cpp b/tests/container/dynamic_graph/test_dynamic_graph_voum.cpp index 77488fe..33449be 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_voum.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_voum.cpp @@ -45,13 +45,8 @@ using voum_string_string_string = std::string, std::string, uint32_t, - false, - voum_graph_traits>; + false, voum_graph_traits>; -using voum_sourced = - dynamic_graph>; -using voum_int_sourced = - dynamic_graph>; // Edge and vertex data types for loading using edge_void = copyable_edge_t; @@ -218,19 +213,6 @@ TEST_CASE("voum edge deduplication", "[voum][unordered_map][deduplication]") { REQUIRE(v0.edges().begin()->second.value() == 100); } - SECTION("sourced edges - deduplication by (source_id, target_id)") { - voum_sourced g; - std::vector ee = { - {0, 1}, - {0, 1}, // Duplicates - {1, 0}, - {1, 0} // Different direction, also duplicates - }; - g.load_edges(ee, std::identity{}); - - // Should have exactly 2 unique edges (0->1 and 1->0) - REQUIRE(count_all_edges(g) == 2); - } } //================================================================================================== @@ -253,19 +235,6 @@ TEST_CASE("voum edges are unordered", "[voum][unordered_map][unordered]") { REQUIRE(target_ids == std::set{1, 2, 3, 5, 8}); } - SECTION("sourced edges - all target_ids present") { - voum_sourced g; - std::vector ee = {{0, 7}, {0, 3}, {0, 9}, {0, 1}}; - g.load_edges(ee, std::identity{}); - - auto& v0 = g[0]; - std::set target_ids; - for (const auto& edge : v0.edges()) { - target_ids.insert(edge.second.target_id()); - } - - REQUIRE(target_ids == std::set{1, 3, 7, 9}); - } } //================================================================================================== @@ -415,41 +384,6 @@ TEST_CASE("voum edge values", "[voum][edge][value]") { // 9. Sourced Edge Tests //================================================================================================== -TEST_CASE("voum sourced edges", "[voum][sourced]") { - SECTION("source_id access") { - voum_sourced g({{0, 1}, {0, 2}, {1, 0}}); - - auto& v0 = g[0]; - for (const auto& edge : v0.edges()) { - REQUIRE(edge.second.source_id() == 0); - } - - auto& v1 = g[1]; - for (const auto& edge : v1.edges()) { - REQUIRE(edge.second.source_id() == 1); - } - } - - SECTION("sourced edge with values") { - voum_int_sourced g; - std::vector ee = {{0, 1, 100}, {1, 0, 200}}; - g.load_edges(ee, std::identity{}); - - // Verify edges from vertex 0 - auto& v0 = g[0]; - auto it0 = v0.edges().begin(); - REQUIRE(it0->second.source_id() == 0); - REQUIRE(it0->second.target_id() == 1); - REQUIRE(it0->second.value() == 100); - - // Verify edges from vertex 1 - auto& v1 = g[1]; - auto it1 = v1.edges().begin(); - REQUIRE(it1->second.source_id() == 1); - REQUIRE(it1->second.target_id() == 0); - REQUIRE(it1->second.value() == 200); - } -} //================================================================================================== // 10. Self-Loop Tests @@ -667,13 +601,6 @@ TEST_CASE("voum type traits", "[voum][traits]") { static_assert(requires { typename edges_t::hasher; }); } - SECTION("sourced trait") { - using traits_unsourced = voum_graph_traits; - using traits_sourced = voum_graph_traits; - - static_assert(traits_unsourced::sourced == false); - static_assert(traits_sourced::sourced == true); - } } //================================================================================================== diff --git a/tests/container/dynamic_graph/test_dynamic_graph_vous.cpp b/tests/container/dynamic_graph/test_dynamic_graph_vous.cpp index 2fa7530..05e2ba3 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_vous.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_vous.cpp @@ -49,13 +49,8 @@ using vous_string_string_string = std::string, std::string, uint32_t, - false, - vous_graph_traits>; + false, vous_graph_traits>; -using vous_sourced = - dynamic_graph>; -using vous_int_sourced = - dynamic_graph>; // Edge and vertex data types for loading using edge_void = copyable_edge_t; @@ -296,34 +291,6 @@ TEST_CASE("vous value access", "[vous][values]") { // 6. Sourced Edge Tests //================================================================================================== -TEST_CASE("vous sourced edges", "[vous][sourced]") { - SECTION("source_id access") { - vous_sourced g; - std::vector ee = {{0, 1}, {1, 2}, {0, 2}}; - g.load_edges(ee, std::identity{}); - - auto& v0 = g[0]; - for (auto& e : v0.edges()) { - REQUIRE(e.source_id() == 0); - } - - auto& v1 = g[1]; - for (auto& e : v1.edges()) { - REQUIRE(e.source_id() == 1); - } - } - - SECTION("sourced edge deduplication") { - vous_int_sourced g; - // Multiple edges from 0 to 1 with different values - std::vector ee = {{0, 1, 100}, {0, 1, 200}, {0, 1, 300}}; - g.load_edges(ee, std::identity{}); - - auto& v0 = g[0]; - // unordered_set deduplicates by (source_id, target_id) pair - REQUIRE(std::ranges::distance(v0.edges()) == 1); - } -} //================================================================================================== // 7. Unordered Set Specific Behavior diff --git a/tests/container/dynamic_graph/test_dynamic_graph_vov.cpp b/tests/container/dynamic_graph/test_dynamic_graph_vov.cpp index 5abee77..94f1345 100644 --- a/tests/container/dynamic_graph/test_dynamic_graph_vov.cpp +++ b/tests/container/dynamic_graph/test_dynamic_graph_vov.cpp @@ -41,12 +41,8 @@ using vov_string_string_string = std::string, std::string, uint32_t, - false, - vov_graph_traits>; + false, vov_graph_traits>; -using vov_sourced = dynamic_graph>; -using vov_int_sourced = - dynamic_graph>; //================================================================================================== // 1. Construction Tests @@ -134,17 +130,6 @@ TEST_CASE("vov copy and move construction", "[vov][construction]") { } } -TEST_CASE("vov sourced construction", "[vov][construction][sourced]") { - SECTION("sourced edge construction") { - vov_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced with edge value construction") { - vov_int_sourced g; - REQUIRE(g.size() == 0); - } -} //================================================================================================== // 2. Basic Properties Tests @@ -285,13 +270,8 @@ TEST_CASE("vov_graph_traits", "[vov][traits]") { STATIC_REQUIRE(std::is_same_v); STATIC_REQUIRE(std::is_same_v); STATIC_REQUIRE(std::is_same_v); - STATIC_REQUIRE(traits::sourced == false); } - SECTION("sourced = true") { - using traits = vov_graph_traits; - STATIC_REQUIRE(traits::sourced == true); - } SECTION("vertex_id_type variations") { using traits_u64 = vov_graph_traits; @@ -416,22 +396,19 @@ TEST_CASE("vov value types", "[vov][value_types]") { } SECTION("with string edge value type") { - using graph_t = dynamic_graph>; + using graph_t = dynamic_graph>; graph_t g; REQUIRE(g.size() == 0); } SECTION("with string vertex value type") { - using graph_t = dynamic_graph>; + using graph_t = dynamic_graph>; graph_t g; REQUIRE(g.size() == 0); } SECTION("with string graph value type") { - using graph_t = dynamic_graph>; + using graph_t = dynamic_graph>; graph_t g(std::string("test")); REQUIRE(g.graph_value() == "test"); } @@ -484,41 +461,6 @@ TEST_CASE("vov vertex ID types", "[vov][vertex_id]") { // 9. Sourced Edge Tests //================================================================================================== -TEST_CASE("vov sourced edges", "[vov][sourced]") { - SECTION("sourced=false by default") { - vov_void_void_void g; - using traits = vov_graph_traits; - STATIC_REQUIRE(traits::sourced == false); - } - - SECTION("sourced=true explicit") { - vov_sourced g; - using traits = vov_graph_traits; - STATIC_REQUIRE(traits::sourced == true); - } - - SECTION("sourced with void values") { - vov_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced with int edge value") { - vov_int_sourced g; - REQUIRE(g.size() == 0); - } - - SECTION("sourced copy construction") { - vov_sourced g1; - vov_sourced g2 = g1; - REQUIRE(g2.size() == 0); - } - - SECTION("sourced move construction") { - vov_sourced g1; - vov_sourced g2 = std::move(g1); - REQUIRE(g2.size() == 0); - } -} //================================================================================================== // 10. Const Correctness Tests @@ -616,8 +558,6 @@ TEST_CASE("vov various template instantiations compile", "[vov][compilation]") { [[maybe_unused]] vov_void_void_int g5; [[maybe_unused]] vov_int_int_int g6; [[maybe_unused]] vov_string_string_string g7; - [[maybe_unused]] vov_sourced g8; - [[maybe_unused]] vov_int_sourced g9; REQUIRE(true); // Just ensuring compilation } @@ -839,22 +779,6 @@ TEST_CASE("vov initializer_list constructor with all value types", "[vov][constr } } -TEST_CASE("vov initializer_list constructor with sourced edges", "[vov][construction][initializer_list][sourced]") { - using G = vov_sourced; - - SECTION("construct sourced graph with initializer list") { - G g({{0, 1}, {1, 2}, {2, 0}}); - REQUIRE(g.size() == 3); - - // Verify sourced edges have source_id - auto& v0 = g[0]; - auto edges0 = v0.edges(); - REQUIRE(std::ranges::distance(edges0) == 1); - auto e0 = edges0.begin(); - REQUIRE(e0->source_id() == 0); - REQUIRE(e0->target_id() == 1); - } -} TEST_CASE("vov initializer_list complex graph patterns", "[vov][construction][initializer_list]") { using G = vov_int_void_void; @@ -944,8 +868,7 @@ TEST_CASE("vov load_vertices", "[dynamic_graph][vov][load_vertices]") { } SECTION("with custom projection from struct") { - using G2 = dynamic_graph>; + using G2 = dynamic_graph>; using vertex_data2 = copyable_vertex_t; struct Person { @@ -1082,8 +1005,7 @@ TEST_CASE("vov load_edges", "[dynamic_graph][vov][load_edges]") { } SECTION("with custom projection") { - using G2 = dynamic_graph>; + using G2 = dynamic_graph>; using vertex_data2 = copyable_vertex_t; using edge_data2 = copyable_edge_t; diff --git a/tests/container/undirected_adjacency_list/test_undirected_adjacency_list_cpo.cpp b/tests/container/undirected_adjacency_list/test_undirected_adjacency_list_cpo.cpp index b408ebe..5c425dd 100644 --- a/tests/container/undirected_adjacency_list/test_undirected_adjacency_list_cpo.cpp +++ b/tests/container/undirected_adjacency_list/test_undirected_adjacency_list_cpo.cpp @@ -25,7 +25,7 @@ using graph::adj_list::find_vertex; using graph::adj_list::vertex_id; using graph::adj_list::num_edges; using graph::adj_list::num_vertices; -using graph::adj_list::has_edge; +using graph::adj_list::has_edges; using graph::adj_list::graph_value; using graph::adj_list::edges; using graph::adj_list::target; @@ -147,15 +147,15 @@ TEST_CASE("num_edges CPO", "[undirected_adjacency_list][cpo][num_edges]") { REQUIRE(num_edges(g) == 2); } -TEST_CASE("has_edge CPO", "[undirected_adjacency_list][cpo][has_edge]") { +TEST_CASE("has_edges CPO", "[undirected_adjacency_list][cpo][has_edges]") { IntGraph g(0); g.create_vertex(); g.create_vertex(); - REQUIRE_FALSE(has_edge(g)); + REQUIRE_FALSE(has_edges(g)); g.create_edge(0, 1, 100); - REQUIRE(has_edge(g)); + REQUIRE(has_edges(g)); } TEST_CASE("graph_value CPO", "[undirected_adjacency_list][cpo][graph_value]") { @@ -579,7 +579,7 @@ TEST_CASE("CPO with empty graph", "[undirected_adjacency_list][cpo][empty]") { SECTION("num_edges on empty graph") { REQUIRE(num_edges(g) == 0); } - SECTION("has_edge on empty graph") { REQUIRE_FALSE(has_edge(g)); } + SECTION("has_edges on empty graph") { REQUIRE_FALSE(has_edges(g)); } SECTION("find_vertex on empty graph returns end") { auto it = find_vertex(g, 0u); @@ -600,7 +600,7 @@ TEST_CASE("CPO const correctness", "[undirected_adjacency_list][cpo][const]") { SECTION("all read CPOs work on const graph") { REQUIRE(num_vertices(cg) == 2); REQUIRE(num_edges(cg) == 1); - REQUIRE(has_edge(cg)); + REQUIRE(has_edges(cg)); auto verts = vertices(cg); REQUIRE(std::distance(verts.begin(), verts.end()) == 2); diff --git a/tests/container/undirected_adjacency_list/test_undirected_bidirectional.cpp b/tests/container/undirected_adjacency_list/test_undirected_bidirectional.cpp new file mode 100644 index 0000000..4c2a232 --- /dev/null +++ b/tests/container/undirected_adjacency_list/test_undirected_bidirectional.cpp @@ -0,0 +1,387 @@ +/** + * @file test_undirected_bidirectional.cpp + * @brief Integration tests for incoming-edge support in undirected_adjacency_list + * + * Verifies that undirected_adjacency_list models bidirectional_adjacency_list + * by providing in_edges() ADL friends that return the same edge ranges as + * edges(). Tests in_edges, in_degree, find_in_edge, and contains_in_edge CPOs. + */ + +#include +#include +#include +#include +#include +#include + +using graph::container::undirected_adjacency_list; + +// Graph type aliases +using IntGraph = undirected_adjacency_list; + +// Bring CPOs into scope (use aliases where available per project convention) +using graph::adj_list::vertices; +using graph::adj_list::find_vertex; +using graph::adj_list::vertex_id; +using graph::adj_list::edges; +using graph::adj_list::degree; +using graph::adj_list::target_id; +using graph::adj_list::source_id; +using graph::adj_list::in_edges; +using graph::adj_list::in_degree; +using graph::adj_list::find_in_edge; +using graph::adj_list::contains_in_edge; +using graph::adj_list::find_vertex_edge; +using graph::adj_list::contains_edge; +using graph::adj_list::edge_value; + +// ============================================================================= +// Concept satisfaction +// ============================================================================= + +TEST_CASE("undirected_adjacency_list models bidirectional_adjacency_list", + "[undirected_adjacency_list][bidirectional][concept]") { + static_assert(graph::adj_list::bidirectional_adjacency_list, + "undirected_adjacency_list must model bidirectional_adjacency_list"); + static_assert(graph::adj_list::index_bidirectional_adjacency_list, + "undirected_adjacency_list must model index_bidirectional_adjacency_list"); + + // Also verify via graph:: namespace re-exports + static_assert(graph::bidirectional_adjacency_list); + static_assert(graph::index_bidirectional_adjacency_list); +} + +// ============================================================================= +// in_edges CPO +// ============================================================================= + +TEST_CASE("in_edges returns same edges as edges for undirected graph", + "[undirected_adjacency_list][bidirectional][in_edges]") { + IntGraph g(0); + g.create_vertex(10); // 0 + g.create_vertex(20); // 1 + g.create_vertex(30); // 2 + g.create_edge(0, 1, 100); + g.create_edge(0, 2, 200); + g.create_edge(1, 2, 300); + + SECTION("in_edges and edges produce identical target sets per vertex") { + for (auto v : vertices(g)) { + std::set out_targets, in_targets; + for (auto e : edges(g, v)) { + out_targets.insert(target_id(g, e)); + } + for (auto ie : in_edges(g, v)) { + in_targets.insert(target_id(g, ie)); + } + REQUIRE(out_targets == in_targets); + } + } + + SECTION("in_edges count matches edges count per vertex") { + for (auto v : vertices(g)) { + size_t out_count = 0, in_count = 0; + for ([[maybe_unused]] auto e : edges(g, v)) + ++out_count; + for ([[maybe_unused]] auto ie : in_edges(g, v)) + ++in_count; + REQUIRE(out_count == in_count); + } + } +} + +TEST_CASE("in_edges by vertex id", + "[undirected_adjacency_list][bidirectional][in_edges]") { + IntGraph g(0); + g.create_vertex(10); // 0 + g.create_vertex(20); // 1 + g.create_vertex(30); // 2 + g.create_edge(0, 1, 100); + g.create_edge(0, 2, 200); + g.create_edge(1, 2, 300); + + // in_edges(g, uid) should work via the CPO default tier + size_t count = 0; + for ([[maybe_unused]] auto ie : in_edges(g, 0u)) { + ++count; + } + REQUIRE(count == 2); // vertex 0 has edges to 1 and 2 +} + +TEST_CASE("in_edges on const graph", + "[undirected_adjacency_list][bidirectional][in_edges]") { + IntGraph g(0); + g.create_vertex(10); + g.create_vertex(20); + g.create_edge(0, 1, 100); + + const IntGraph& cg = g; + auto v = *vertices(cg).begin(); + + size_t count = 0; + for ([[maybe_unused]] auto ie : in_edges(cg, v)) { + ++count; + } + REQUIRE(count == 1); +} + +TEST_CASE("in_edges on vertex with no edges", + "[undirected_adjacency_list][bidirectional][in_edges]") { + IntGraph g(0); + g.create_vertex(10); // isolated vertex + + auto v = *vertices(g).begin(); + auto range = in_edges(g, v); + + size_t count = 0; + for ([[maybe_unused]] auto ie : range) { + ++count; + } + REQUIRE(count == 0); +} + +// ============================================================================= +// in_degree CPO +// ============================================================================= + +TEST_CASE("in_degree equals degree for undirected graph", + "[undirected_adjacency_list][bidirectional][in_degree]") { + IntGraph g(0); + g.create_vertex(10); // 0 + g.create_vertex(20); // 1 + g.create_vertex(30); // 2 + g.create_vertex(40); // 3 (isolated) + g.create_edge(0, 1, 100); + g.create_edge(0, 2, 200); + g.create_edge(1, 2, 300); + + SECTION("in_degree matches degree via vertex descriptor") { + for (auto v : vertices(g)) { + REQUIRE(in_degree(g, v) == degree(g, v)); + } + } + + SECTION("in_degree matches degree via vertex id") { + for (unsigned int uid = 0; uid < 4; ++uid) { + REQUIRE(in_degree(g, uid) == degree(g, uid)); + } + } + + SECTION("in_degree of isolated vertex is 0") { + REQUIRE(in_degree(g, 3u) == 0); + } + + SECTION("in_degree specific values") { + REQUIRE(in_degree(g, 0u) == 2); // edges to 1 and 2 + REQUIRE(in_degree(g, 1u) == 2); // edges to 0 and 2 + REQUIRE(in_degree(g, 2u) == 2); // edges to 0 and 1 + REQUIRE(in_degree(g, 3u) == 0); // isolated + } +} + +// ============================================================================= +// find_in_edge CPO +// ============================================================================= + +TEST_CASE("find_in_edge works on undirected graph", + "[undirected_adjacency_list][bidirectional][find_in_edge]") { + IntGraph g(0); + g.create_vertex(10); // 0 + g.create_vertex(20); // 1 + g.create_vertex(30); // 2 + g.create_edge(0, 1, 100); + g.create_edge(0, 2, 200); + g.create_edge(1, 2, 300); + + SECTION("find_in_edge with two vertex ids - edge exists") { + // find_in_edge(g, uid, vid) default: find_vertex_edge(g, vid, uid) + // = search edges(g, vid) for target_id == uid + // So find_in_edge(g, 0, 1) = find_vertex_edge(g, 1, 0) + // = search edges from vertex 1 for target_id == 0 -> finds edge 1-0 + auto e = find_in_edge(g, 0u, 1u); + // The edge is found from vertex 1's perspective: source_id == 1, target_id == 0 + REQUIRE(source_id(g, e) == 1); + REQUIRE(target_id(g, e) == 0); + REQUIRE(edge_value(g, *e.value()) == 100); + } + + SECTION("find_in_edge with two vertex ids - other direction") { + auto e = find_in_edge(g, 1u, 0u); + // find_in_edge(g, 1, 0) = find_vertex_edge(g, 0, 1) + // = search edges from vertex 0 for target_id == 1 + REQUIRE(source_id(g, e) == 0); + REQUIRE(target_id(g, e) == 1); + REQUIRE(edge_value(g, *e.value()) == 100); + } + + SECTION("find_in_edge symmetry") { + // For undirected graphs, find_in_edge(g, u, v) and find_in_edge(g, v, u) + // should both find the same underlying edge (with different source/target perspectives) + auto e1 = find_in_edge(g, 0u, 2u); + auto e2 = find_in_edge(g, 2u, 0u); + REQUIRE(edge_value(g, *e1.value()) == 200); + REQUIRE(edge_value(g, *e2.value()) == 200); + } + + SECTION("find_in_edge with descriptor and vid") { + auto v0 = *vertices(g).begin(); + auto e = find_in_edge(g, v0, 1u); + REQUIRE(edge_value(g, *e.value()) == 100); + } + + SECTION("find_in_edge with two descriptors") { + auto it = vertices(g).begin(); + auto v0 = *it; + ++it; + auto v1 = *it; + auto e = find_in_edge(g, v0, v1); + REQUIRE(edge_value(g, *e.value()) == 100); + } + + SECTION("find_in_edge on const graph") { + const IntGraph& cg = g; + auto e = find_in_edge(cg, 1u, 2u); + REQUIRE(edge_value(cg, *e.value()) == 300); + } +} + +// ============================================================================= +// contains_in_edge CPO +// ============================================================================= + +TEST_CASE("contains_in_edge works on undirected graph", + "[undirected_adjacency_list][bidirectional][contains_in_edge]") { + IntGraph g(0); + g.create_vertex(10); // 0 + g.create_vertex(20); // 1 + g.create_vertex(30); // 2 + g.create_edge(0, 1, 100); + g.create_edge(0, 2, 200); + // No edge between 1 and 2 + + SECTION("contains_in_edge with two vertex ids - edge exists") { + // contains_in_edge(g, uid, vid) default: search edges(g, vid) for target_id == uid + // So contains_in_edge(g, 0, 1) searches edges from vertex 1 for target_id == 0 + // Vertex 1 has an edge to vertex 0, so this is true. + REQUIRE(contains_in_edge(g, 0u, 1u) == true); + REQUIRE(contains_in_edge(g, 1u, 0u) == true); + REQUIRE(contains_in_edge(g, 0u, 2u) == true); + REQUIRE(contains_in_edge(g, 2u, 0u) == true); + } + + SECTION("contains_in_edge with two vertex ids - edge does not exist") { + // No edge between 1 and 2 + REQUIRE(contains_in_edge(g, 1u, 2u) == false); + REQUIRE(contains_in_edge(g, 2u, 1u) == false); + } + + SECTION("contains_in_edge matches contains_edge for undirected") { + // For undirected graphs, contains_in_edge should agree with contains_edge + REQUIRE(contains_in_edge(g, 0u, 1u) == contains_edge(g, 0u, 1u)); + REQUIRE(contains_in_edge(g, 0u, 2u) == contains_edge(g, 0u, 2u)); + REQUIRE(contains_in_edge(g, 1u, 2u) == contains_edge(g, 1u, 2u)); + } + + SECTION("contains_in_edge with two vertex descriptors") { + auto it = vertices(g).begin(); + auto v0 = *it; + ++it; + auto v1 = *it; + ++it; + auto v2 = *it; + + REQUIRE(contains_in_edge(g, v0, v1) == true); + REQUIRE(contains_in_edge(g, v0, v2) == true); + REQUIRE(contains_in_edge(g, v1, v2) == false); + } + + SECTION("contains_in_edge on const graph") { + const IntGraph& cg = g; + REQUIRE(contains_in_edge(cg, 0u, 1u) == true); + REQUIRE(contains_in_edge(cg, 1u, 2u) == false); + } +} + +// ============================================================================= +// Integration: Edge symmetry in undirected graphs +// ============================================================================= + +TEST_CASE("undirected graph edge symmetry with in_edges", + "[undirected_adjacency_list][bidirectional][integration]") { + IntGraph g(0); + // Triangle: 0--1, 1--2, 2--0 + g.create_vertex(10); + g.create_vertex(20); + g.create_vertex(30); + g.create_edge(0, 1, 12); + g.create_edge(1, 2, 23); + g.create_edge(2, 0, 31); + + SECTION("total in_edges iteration matches total edges iteration") { + size_t total_out = 0, total_in = 0; + for (auto v : vertices(g)) { + for ([[maybe_unused]] auto e : edges(g, v)) + ++total_out; + for ([[maybe_unused]] auto ie : in_edges(g, v)) + ++total_in; + } + // Each edge counted twice from each endpoint + REQUIRE(total_out == 6); + REQUIRE(total_in == 6); + REQUIRE(total_out == total_in); + } + + SECTION("in_degree sum equals degree sum") { + size_t deg_sum = 0, in_deg_sum = 0; + for (auto v : vertices(g)) { + deg_sum += degree(g, v); + in_deg_sum += in_degree(g, v); + } + REQUIRE(deg_sum == in_deg_sum); + REQUIRE(deg_sum == 6); // 2*|E| for undirected + } +} + +TEST_CASE("undirected graph star topology - in_edges correctness", + "[undirected_adjacency_list][bidirectional][integration]") { + IntGraph g(0); + // Star graph: vertex 0 connected to 1..4 + for (int i = 0; i < 5; ++i) + g.create_vertex(i * 10); + g.create_edge(0, 1, 1); + g.create_edge(0, 2, 2); + g.create_edge(0, 3, 3); + g.create_edge(0, 4, 4); + + SECTION("hub vertex has same in_degree and degree") { + REQUIRE(in_degree(g, 0u) == 4); + REQUIRE(degree(g, 0u) == 4); + } + + SECTION("leaf vertices have in_degree 1") { + for (unsigned int uid = 1; uid <= 4; ++uid) { + REQUIRE(in_degree(g, uid) == 1); + REQUIRE(degree(g, uid) == 1); + } + } + + SECTION("in_edges from hub has all neighbors") { + std::set neighbors; + for (auto ie : in_edges(g, 0u)) { + neighbors.insert(target_id(g, ie)); + } + REQUIRE(neighbors == std::set{1, 2, 3, 4}); + } + + SECTION("contains_in_edge hub to each leaf") { + for (unsigned int uid = 1; uid <= 4; ++uid) { + REQUIRE(contains_in_edge(g, 0u, uid) == true); + REQUIRE(contains_in_edge(g, uid, 0u) == true); + } + } + + SECTION("contains_in_edge between non-adjacent leaves") { + REQUIRE(contains_in_edge(g, 1u, 2u) == false); + REQUIRE(contains_in_edge(g, 3u, 4u) == false); + } +} diff --git a/tests/views/CMakeLists.txt b/tests/views/CMakeLists.txt index acc1bc8..27dcc43 100644 --- a/tests/views/CMakeLists.txt +++ b/tests/views/CMakeLists.txt @@ -9,8 +9,10 @@ add_executable(graph3_views_tests test_basic_vertexlist.cpp test_incidence.cpp test_basic_incidence.cpp + test_in_incidence.cpp test_neighbors.cpp test_basic_neighbors.cpp + test_in_neighbors.cpp test_edgelist.cpp test_basic_edgelist.cpp test_basic_views.cpp @@ -22,6 +24,8 @@ add_executable(graph3_views_tests test_unified_header.cpp test_graph_hpp_includes_views.cpp test_edge_cases.cpp + test_transpose.cpp + test_reverse_traversal.cpp ) target_link_libraries(graph3_views_tests diff --git a/tests/views/test_adaptors.cpp b/tests/views/test_adaptors.cpp index 7e758f0..4da5b0a 100644 --- a/tests/views/test_adaptors.cpp +++ b/tests/views/test_adaptors.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include using namespace graph; @@ -1172,4 +1173,173 @@ TEST_CASE("basic_edgelist adaptor - direct call compatibility", "[adaptors][basi auto count2 = std::ranges::distance(view2); REQUIRE(count1 == count2); REQUIRE(count1 == 3); +} + +//============================================================================= +// Incoming-edge adaptor tests (Phase 6) +// Use undirected_adjacency_list as bidirectional graph +//============================================================================= + +#include + +using graph::container::undirected_adjacency_list; +using BiGraph = undirected_adjacency_list; + +// Triangle graph: 0--1 (w=100), 0--2 (w=200), 1--2 (w=300) +static BiGraph make_bi_graph() { + BiGraph g(0); + g.create_vertex(10); // 0 + g.create_vertex(20); // 1 + g.create_vertex(30); // 2 + g.create_edge(0, 1, 100); + g.create_edge(0, 2, 200); + g.create_edge(1, 2, 300); + return g; +} + +// -- out_incidence pipe -- + +TEST_CASE("pipe: g | out_incidence(uid)", "[adaptors][out_incidence]") { + auto bg = make_bi_graph(); + auto view = bg | out_incidence(0u); + REQUIRE(size(view) == 2); // vertex 0 has 2 edges + + // Also verify it matches the free-function factory + auto ref = graph::views::out_incidence(bg, 0u); + REQUIRE(size(ref) == size(view)); +} + +TEST_CASE("pipe: g | out_incidence(uid, evf)", "[adaptors][out_incidence]") { + auto bg = make_bi_graph(); + auto evf = [](const auto& g, auto e) { return adj_list::edge_value(g, e); }; + auto view = bg | out_incidence(0u, evf); + REQUIRE(size(view) == 2); + + std::set values; + for (auto [tid, e, val] : view) { + values.insert(val); + } + REQUIRE(values == std::set{100, 200}); +} + +// -- in_incidence pipe -- + +TEST_CASE("pipe: g | in_incidence(uid)", "[adaptors][in_incidence]") { + auto bg = make_bi_graph(); + auto view = bg | in_incidence(0u); + REQUIRE(size(view) == 2); // undirected: in_degree == degree + + // Must match the free-function factory + auto ref = graph::views::in_incidence(bg, 0u); + REQUIRE(size(ref) == size(view)); +} + +TEST_CASE("pipe: g | in_incidence(uid, evf)", "[adaptors][in_incidence]") { + auto bg = make_bi_graph(); + auto evf = [](const auto& g, auto e) { return adj_list::edge_value(g, e); }; + auto view = bg | in_incidence(0u, evf); + REQUIRE(size(view) == 2); + + std::set values; + for (auto [tid, e, val] : view) { + values.insert(val); + } + REQUIRE(values == std::set{100, 200}); +} + +// -- basic_in_incidence pipe -- + +TEST_CASE("pipe: g | basic_in_incidence(uid)", "[adaptors][basic_in_incidence]") { + auto bg = make_bi_graph(); + auto view = bg | basic_in_incidence(0u); + REQUIRE(size(view) == 2); +} + +TEST_CASE("pipe: g | basic_in_incidence(uid, evf)", "[adaptors][basic_in_incidence]") { + auto bg = make_bi_graph(); + auto evf = [](const auto& g, auto e) { return adj_list::edge_value(g, e); }; + auto view = bg | basic_in_incidence(0u, evf); + REQUIRE(size(view) == 2); +} + +// -- out_neighbors pipe -- + +TEST_CASE("pipe: g | out_neighbors(uid)", "[adaptors][out_neighbors]") { + auto bg = make_bi_graph(); + auto view = bg | out_neighbors(0u); + REQUIRE(size(view) == 2); + + auto ref = graph::views::out_neighbors(bg, 0u); + REQUIRE(size(ref) == size(view)); +} + +// -- in_neighbors pipe -- + +TEST_CASE("pipe: g | in_neighbors(uid)", "[adaptors][in_neighbors]") { + auto bg = make_bi_graph(); + auto view = bg | in_neighbors(0u); + REQUIRE(size(view) == 2); + + auto ref = graph::views::in_neighbors(bg, 0u); + REQUIRE(size(ref) == size(view)); +} + +TEST_CASE("pipe: g | in_neighbors(uid, vvf)", "[adaptors][in_neighbors]") { + auto bg = make_bi_graph(); + auto vvf = [](const auto&, auto v) { return static_cast(v.vertex_id()); }; + auto view = bg | in_neighbors(0u, vvf); + REQUIRE(size(view) == 2); +} + +// -- basic_in_neighbors pipe -- + +TEST_CASE("pipe: g | basic_in_neighbors(uid)", "[adaptors][basic_in_neighbors]") { + auto bg = make_bi_graph(); + auto view = bg | basic_in_neighbors(0u); + REQUIRE(size(view) == 2); +} + +TEST_CASE("pipe: g | basic_in_neighbors(uid, vvf)", "[adaptors][basic_in_neighbors]") { + auto bg = make_bi_graph(); + auto vvf = [](const auto&, auto v) { return static_cast(v.vertex_id()); }; + auto view = bg | basic_in_neighbors(0u, vvf); + REQUIRE(size(view) == 2); +} + +// -- basic_out_incidence / basic_out_neighbors pipe -- + +TEST_CASE("pipe: g | basic_out_incidence(uid)", "[adaptors][basic_out_incidence]") { + auto bg = make_bi_graph(); + auto view = bg | basic_out_incidence(0u); + REQUIRE(size(view) == 2); +} + +TEST_CASE("pipe: g | basic_out_neighbors(uid)", "[adaptors][basic_out_neighbors]") { + auto bg = make_bi_graph(); + auto view = bg | basic_out_neighbors(0u); + REQUIRE(size(view) == 2); +} + +// -- Alias verification -- + +TEST_CASE("pipe: incidence/neighbors aliases point to out_* objects", "[adaptors][aliases]") { + // The short names must be references to the out_* objects + REQUIRE(&incidence == &out_incidence); + REQUIRE(&neighbors == &out_neighbors); + REQUIRE(&basic_incidence == &basic_out_incidence); + REQUIRE(&basic_neighbors == &basic_out_neighbors); +} + +// -- Direct call for incoming adaptors -- + +TEST_CASE("in_incidence adaptor - direct call", "[adaptors][in_incidence]") { + auto bg = make_bi_graph(); + auto view = in_incidence(bg, 0u); + REQUIRE(size(view) == 2); +} + +TEST_CASE("in_neighbors adaptor - direct call", "[adaptors][in_neighbors]") { + auto bg = make_bi_graph(); + auto view = in_neighbors(bg, 0u); + REQUIRE(size(view) == 2); } \ No newline at end of file diff --git a/tests/views/test_in_incidence.cpp b/tests/views/test_in_incidence.cpp new file mode 100644 index 0000000..59151e2 --- /dev/null +++ b/tests/views/test_in_incidence.cpp @@ -0,0 +1,258 @@ +/** + * @file test_in_incidence.cpp + * @brief Tests for incoming / outgoing incidence view factory functions. + * + * Verifies that the accessor-parameterized incidence views compile and + * iterate correctly: + * - out_incidence(g, u) — explicit outgoing (equivalent to incidence(g, u)) + * - in_incidence(g, u) — incoming via in_edge_accessor + * - basic_out_incidence(g, uid) + * - basic_in_incidence(g, uid) + * + * Uses undirected_adjacency_list as the only currently available + * bidirectional_adjacency_list container. For undirected graphs + * in_edges(g,u)==edges(g,u), so incoming views iterate the same edge + * list but report source_id as the neighbor. + */ + +#include +#include +#include +#include +#include +#include + +using graph::container::undirected_adjacency_list; +using Graph = undirected_adjacency_list; + +namespace adj = graph::adj_list; +namespace view = graph::views; + +// --------------------------------------------------------------------------- +// Helper: build a small triangle graph +// 0 --100-- 1 +// | / | +// 200 300 (no 1-2 direct; we do 0-2 and 1-2) +// | / +// 2 +// Edges: (0,1,100), (0,2,200), (1,2,300) +// --------------------------------------------------------------------------- +static Graph make_triangle() { + Graph g(0); + g.create_vertex(10); // 0 + g.create_vertex(20); // 1 + g.create_vertex(30); // 2 + g.create_edge(0, 1, 100); + g.create_edge(0, 2, 200); + g.create_edge(1, 2, 300); + return g; +} + +// ============================================================================= +// Concept checks +// ============================================================================= + +TEST_CASE("in_incidence - concept prerequisites", "[in_incidence][concept]") { + static_assert(adj::bidirectional_adjacency_list); + static_assert(adj::index_bidirectional_adjacency_list); +} + +// ============================================================================= +// out_incidence — must match incidence() +// ============================================================================= + +TEST_CASE("out_incidence matches incidence", "[in_incidence][out]") { + auto g = make_triangle(); + auto v0 = *adj::find_vertex(g, 0u); + + SECTION("no EVF — same edge count and target_ids") { + auto ref = view::incidence(g, v0); + auto test = view::out_incidence(g, v0); + + REQUIRE(ref.size() == test.size()); + + std::vector ref_ids, test_ids; + for (auto [tid, e] : ref) + ref_ids.push_back(tid); + for (auto [tid, e] : test) + test_ids.push_back(tid); + + REQUIRE(ref_ids == test_ids); + } + + SECTION("with EVF") { + auto evf = [](const auto& g, auto e) { return adj::edge_value(g, e); }; + auto ref = view::incidence(g, v0, evf); + auto test = view::out_incidence(g, v0, evf); + + REQUIRE(ref.size() == test.size()); + + std::vector ref_vals, test_vals; + for (auto [tid, e, val] : ref) + ref_vals.push_back(val); + for (auto [tid, e, val] : test) + test_vals.push_back(val); + + REQUIRE(ref_vals == test_vals); + } + + SECTION("from vertex id") { + auto ref = view::incidence(g, 0u); + auto test = view::out_incidence(g, 0u); + + REQUIRE(ref.size() == test.size()); + } + + SECTION("with EVF from vertex id") { + auto evf = [](const auto& g, auto e) { return adj::edge_value(g, e); }; + auto ref = view::incidence(g, 0u, evf); + auto test = view::out_incidence(g, 0u, evf); + + REQUIRE(ref.size() == test.size()); + } +} + +// ============================================================================= +// basic_out_incidence — must match basic_incidence() +// ============================================================================= + +TEST_CASE("basic_out_incidence matches basic_incidence", "[in_incidence][basic_out]") { + auto g = make_triangle(); + + SECTION("no EVF") { + auto ref = view::basic_incidence(g, 0u); + auto test = view::basic_out_incidence(g, 0u); + + REQUIRE(ref.size() == test.size()); + + std::vector ref_ids, test_ids; + for (auto ei : ref) + ref_ids.push_back(ei.target_id); + for (auto ei : test) + test_ids.push_back(ei.target_id); + + REQUIRE(ref_ids == test_ids); + } + + SECTION("with EVF") { + auto evf = [](const auto& g, auto e) { return adj::edge_value(g, e); }; + auto ref = view::basic_incidence(g, 0u, evf); + auto test = view::basic_out_incidence(g, 0u, evf); + + REQUIRE(ref.size() == test.size()); + } +} + +// ============================================================================= +// in_incidence — incoming edges +// ============================================================================= + +TEST_CASE("in_incidence iterates in_edges", "[in_incidence][in]") { + auto g = make_triangle(); + auto v0 = *adj::find_vertex(g, 0u); + + SECTION("edge count matches in_degree") { + auto iview = view::in_incidence(g, v0); + REQUIRE(iview.size() == adj::in_degree(g, v0)); + } + + SECTION("edge count matches degree for undirected graph") { + // For undirected graphs, in_degree == degree + for (auto v : adj::vertices(g)) { + auto iview = view::in_incidence(g, v); + REQUIRE(iview.size() == adj::degree(g, v)); + } + } + + SECTION("no EVF — structured binding") { + auto iview = view::in_incidence(g, v0); + std::size_t count = 0; + for (auto [tid, e] : iview) { + // For undirected, source_id (used as neighbor_id) == iterating vertex id + (void)tid; + (void)e; + ++count; + } + REQUIRE(count == 2); // vertex 0 has 2 edges + } + + SECTION("with EVF") { + auto evf = [](const auto& g, auto e) { return adj::edge_value(g, e); }; + auto iview = view::in_incidence(g, v0, evf); + REQUIRE(iview.size() == 2); + + std::set values; + for (auto [tid, e, val] : iview) { + values.insert(val); + } + // Edge values from vertex 0: 100 (to 1) and 200 (to 2) + REQUIRE(values == std::set{100, 200}); + } + + SECTION("from vertex id") { + auto iview = view::in_incidence(g, 1u); + REQUIRE(iview.size() == adj::in_degree(g, *adj::find_vertex(g, 1u))); + } + + SECTION("with EVF from vertex id") { + auto evf = [](const auto& g, auto e) { return adj::edge_value(g, e); }; + auto iview = view::in_incidence(g, 1u, evf); + REQUIRE(iview.size() == 2); // vertex 1: edges to 0 and 2 + } +} + +// ============================================================================= +// basic_in_incidence +// ============================================================================= + +TEST_CASE("basic_in_incidence", "[in_incidence][basic_in]") { + auto g = make_triangle(); + + SECTION("no EVF — iteration count") { + auto bview = view::basic_in_incidence(g, 0u); + REQUIRE(bview.size() == 2); + } + + SECTION("with EVF — values accessible") { + auto evf = [](const auto& g, auto e) { return adj::edge_value(g, e); }; + auto bview = view::basic_in_incidence(g, 0u, evf); + REQUIRE(bview.size() == 2); + + std::set values; + for (auto ei : bview) { + values.insert(ei.value); + } + REQUIRE(values == std::set{100, 200}); + } +} + +// ============================================================================= +// in_incidence — empty vertex +// ============================================================================= + +TEST_CASE("in_incidence - isolated vertex", "[in_incidence][empty]") { + Graph g(0); + g.create_vertex(10); // 0 — no edges + g.create_vertex(20); // 1 + g.create_edge(1, 1, 99); // self-loop on 1 (just so the graph isn't trivial) + + auto v0 = *adj::find_vertex(g, 0u); + auto iview = view::in_incidence(g, v0); + + REQUIRE(iview.begin() == iview.end()); + REQUIRE(iview.size() == 0); +} + +// ============================================================================= +// in_incidence on const graph +// ============================================================================= + +TEST_CASE("in_incidence - const graph", "[in_incidence][const]") { + auto g = make_triangle(); + const auto& cg = g; + + auto v0 = *adj::find_vertex(cg, 0u); + auto iview = view::in_incidence(cg, v0); + + REQUIRE(iview.size() == 2); +} diff --git a/tests/views/test_in_neighbors.cpp b/tests/views/test_in_neighbors.cpp new file mode 100644 index 0000000..a513ae3 --- /dev/null +++ b/tests/views/test_in_neighbors.cpp @@ -0,0 +1,253 @@ +/** + * @file test_in_neighbors.cpp + * @brief Tests for incoming / outgoing neighbors view factory functions. + * + * Verifies that the accessor-parameterized neighbors views compile and + * iterate correctly: + * - out_neighbors(g, u) — explicit outgoing (equivalent to neighbors(g, u)) + * - in_neighbors(g, u) — incoming via in_edge_accessor + * - basic_out_neighbors(g, uid) + * - basic_in_neighbors(g, uid) + * + * Uses undirected_adjacency_list as the only currently available + * bidirectional_adjacency_list container. + */ + +#include +#include +#include +#include +#include +#include + +using graph::container::undirected_adjacency_list; +using Graph = undirected_adjacency_list; + +namespace adj = graph::adj_list; +namespace view = graph::views; + +// --------------------------------------------------------------------------- +// Helper: build a small triangle graph +// 0 --100-- 1 +// | / +// 200 300 +// | / +// 2 +// Edges: (0,1,100), (0,2,200), (1,2,300) +// --------------------------------------------------------------------------- +static Graph make_triangle() { + Graph g(0); + g.create_vertex(10); // 0 + g.create_vertex(20); // 1 + g.create_vertex(30); // 2 + g.create_edge(0, 1, 100); + g.create_edge(0, 2, 200); + g.create_edge(1, 2, 300); + return g; +} + +// ============================================================================= +// Concept checks +// ============================================================================= + +TEST_CASE("in_neighbors - concept prerequisites", "[in_neighbors][concept]") { + static_assert(adj::bidirectional_adjacency_list); + static_assert(adj::index_bidirectional_adjacency_list); +} + +// ============================================================================= +// out_neighbors — must match neighbors() +// ============================================================================= + +TEST_CASE("out_neighbors matches neighbors", "[in_neighbors][out]") { + auto g = make_triangle(); + auto v0 = *adj::find_vertex(g, 0u); + + SECTION("no VVF — same count and target_ids") { + auto ref = view::neighbors(g, v0); + auto test = view::out_neighbors(g, v0); + + REQUIRE(ref.size() == test.size()); + + std::vector ref_ids, test_ids; + for (auto [tid, v] : ref) + ref_ids.push_back(tid); + for (auto [tid, v] : test) + test_ids.push_back(tid); + + REQUIRE(ref_ids == test_ids); + } + + SECTION("with VVF") { + auto vvf = [](const auto&, auto v) { return v.vertex_id() * 10; }; + auto ref = view::neighbors(g, v0, vvf); + auto test = view::out_neighbors(g, v0, vvf); + + REQUIRE(ref.size() == test.size()); + + std::vector ref_vals, test_vals; + for (auto [tid, v, val] : ref) + ref_vals.push_back(val); + for (auto [tid, v, val] : test) + test_vals.push_back(val); + + REQUIRE(ref_vals == test_vals); + } + + SECTION("from vertex id") { + auto ref = view::neighbors(g, 0u); + auto test = view::out_neighbors(g, 0u); + REQUIRE(ref.size() == test.size()); + } + + SECTION("with VVF from vertex id") { + auto vvf = [](const auto&, auto v) { return v.vertex_id() * 10; }; + auto ref = view::neighbors(g, 0u, vvf); + auto test = view::out_neighbors(g, 0u, vvf); + REQUIRE(ref.size() == test.size()); + } +} + +// ============================================================================= +// basic_out_neighbors — must match basic_neighbors() +// ============================================================================= + +TEST_CASE("basic_out_neighbors matches basic_neighbors", "[in_neighbors][basic_out]") { + auto g = make_triangle(); + + SECTION("no VVF") { + auto ref = view::basic_neighbors(g, 0u); + auto test = view::basic_out_neighbors(g, 0u); + + REQUIRE(ref.size() == test.size()); + + std::vector ref_ids, test_ids; + for (auto ni : ref) + ref_ids.push_back(ni.target_id); + for (auto ni : test) + test_ids.push_back(ni.target_id); + + REQUIRE(ref_ids == test_ids); + } + + SECTION("with VVF") { + auto vvf = [](const auto&, auto v) { return v.vertex_id() * 10; }; + auto ref = view::basic_neighbors(g, 0u, vvf); + auto test = view::basic_out_neighbors(g, 0u, vvf); + REQUIRE(ref.size() == test.size()); + } +} + +// ============================================================================= +// in_neighbors — incoming neighbors +// ============================================================================= + +TEST_CASE("in_neighbors iterates in_edges", "[in_neighbors][in]") { + auto g = make_triangle(); + auto v0 = *adj::find_vertex(g, 0u); + + SECTION("neighbor count matches in_degree") { + auto nview = view::in_neighbors(g, v0); + REQUIRE(nview.size() == adj::in_degree(g, v0)); + } + + SECTION("neighbor count matches degree for undirected graph") { + for (auto v : adj::vertices(g)) { + auto nview = view::in_neighbors(g, v); + REQUIRE(nview.size() == adj::degree(g, v)); + } + } + + SECTION("no VVF — structured binding") { + auto nview = view::in_neighbors(g, v0); + std::size_t count = 0; + for (auto [tid, v] : nview) { + (void)tid; + (void)v; + ++count; + } + REQUIRE(count == 2); // vertex 0 has 2 edges + } + + SECTION("with VVF") { + auto vvf = [](const auto&, auto v) { return static_cast(v.vertex_id() * 100); }; + auto nview = view::in_neighbors(g, v0, vvf); + REQUIRE(nview.size() == 2); + + std::size_t count = 0; + for (auto [tid, v, val] : nview) { + (void)tid; + (void)val; + ++count; + } + REQUIRE(count == 2); + } + + SECTION("from vertex id") { + auto nview = view::in_neighbors(g, 1u); + REQUIRE(nview.size() == adj::in_degree(g, *adj::find_vertex(g, 1u))); + } + + SECTION("with VVF from vertex id") { + auto vvf = [](const auto&, auto v) { return static_cast(v.vertex_id()); }; + auto nview = view::in_neighbors(g, 1u, vvf); + REQUIRE(nview.size() == 2); // vertex 1: edges to 0, 2 + } +} + +// ============================================================================= +// basic_in_neighbors +// ============================================================================= + +TEST_CASE("basic_in_neighbors", "[in_neighbors][basic_in]") { + auto g = make_triangle(); + + SECTION("no VVF — iteration count") { + auto bview = view::basic_in_neighbors(g, 0u); + REQUIRE(bview.size() == 2); + } + + SECTION("with VVF") { + auto vvf = [](const auto&, auto v) { return static_cast(v.vertex_id()); }; + auto bview = view::basic_in_neighbors(g, 0u, vvf); + REQUIRE(bview.size() == 2); + + std::size_t count = 0; + for (auto ni : bview) { + (void)ni; + ++count; + } + REQUIRE(count == 2); + } +} + +// ============================================================================= +// in_neighbors — isolated vertex +// ============================================================================= + +TEST_CASE("in_neighbors - isolated vertex", "[in_neighbors][empty]") { + Graph g(0); + g.create_vertex(10); + g.create_vertex(20); + g.create_edge(1, 1, 99); // self-loop on 1 + + auto v0 = *adj::find_vertex(g, 0u); + auto nview = view::in_neighbors(g, v0); + + REQUIRE(nview.begin() == nview.end()); + REQUIRE(nview.size() == 0); +} + +// ============================================================================= +// in_neighbors on const graph +// ============================================================================= + +TEST_CASE("in_neighbors - const graph", "[in_neighbors][const]") { + auto g = make_triangle(); + const auto& cg = g; + + auto v0 = *adj::find_vertex(cg, 0u); + auto nview = view::in_neighbors(cg, v0); + + REQUIRE(nview.size() == 2); +} diff --git a/tests/views/test_reverse_traversal.cpp b/tests/views/test_reverse_traversal.cpp new file mode 100644 index 0000000..1068730 --- /dev/null +++ b/tests/views/test_reverse_traversal.cpp @@ -0,0 +1,451 @@ +/** + * @file test_reverse_traversal.cpp + * @brief Tests for Accessor-parameterized BFS/DFS/topological-sort views. + * + * Phase 7: Verifies that: + * - Forward traversal with default (out_edge_accessor) still works + * - Reverse traversal with in_edge_accessor follows incoming edges + * - Existing call sites remain source-compatible + * - topological_sort with Accessor produces correct orderings + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace graph; +using namespace graph::views; +using namespace graph::adj_list; + +// Non-uniform bidirectional traits: in_edge_type = dynamic_in_edge (has source_id()) +// so that bidirectional_adjacency_list concept is satisfied. +template +struct vov_bidir_graph_traits { + using edge_value_type = EV; + using vertex_value_type = VV; + using graph_value_type = GV; + using vertex_id_type = VId; + static constexpr bool bidirectional = true; + + using edge_type = container::dynamic_out_edge; + using in_edge_type = container::dynamic_in_edge; + using vertex_type = container::dynamic_vertex; + using graph_type = container::dynamic_graph; + + using edges_type = std::vector; + using in_edges_type = std::vector; + using vertices_type = std::vector; +}; + +// Bidirectional graph type with void edge values using non-uniform bidir traits +using bidir_graph = container::dynamic_graph>; + +// ============================================================================= +// Helper: build a small directed bidirectional graph +// +// 0 -> 1 -> 3 +// | ^ +// v | +// 2 --------+ +// +// Edges: 0->1, 0->2, 1->3, 2->3 +// ============================================================================= +static bidir_graph make_diamond() { + return bidir_graph({{0, 1}, {0, 2}, {1, 3}, {2, 3}}); +} + +// ============================================================================= +// Helper: build a chain 0 -> 1 -> 2 -> 3 +// ============================================================================= +static bidir_graph make_chain() { + return bidir_graph({{0, 1}, {1, 2}, {2, 3}}); +} + +// ============================================================================= +// BFS — Forward (default accessor) +// ============================================================================= + +TEST_CASE("vertices_bfs - default accessor on bidir graph", "[bfs][accessor][forward]") { + auto g = make_diamond(); + + std::vector visited; + for (auto [v] : vertices_bfs(g, uint32_t{0})) { + visited.push_back(vertex_id(g, v)); + } + + REQUIRE(visited.size() == 4); + REQUIRE(visited[0] == 0); + // Level 1: {1, 2} in some order + std::set level1(visited.begin() + 1, visited.begin() + 3); + REQUIRE(level1 == std::set{1, 2}); + // Level 2: {3} + REQUIRE(visited[3] == 3); +} + +// ============================================================================= +// BFS — Reverse (in_edge_accessor) +// ============================================================================= + +TEST_CASE("vertices_bfs - reverse accessor from sink", "[bfs][accessor][reverse]") { + auto g = make_diamond(); + + // BFS backwards from vertex 3 (the sink) using in_edge_accessor + std::vector visited; + for (auto [v] : vertices_bfs(g, uint32_t{3})) { + visited.push_back(vertex_id(g, v)); + } + + // From 3 following incoming edges: 3 -> {1, 2} -> {0} + REQUIRE(visited.size() == 4); + REQUIRE(visited[0] == 3); + std::set level1(visited.begin() + 1, visited.begin() + 3); + REQUIRE(level1 == std::set{1, 2}); + REQUIRE(visited[3] == 0); +} + +TEST_CASE("edges_bfs - reverse accessor from sink", "[bfs][accessor][reverse]") { + auto g = make_diamond(); + + std::vector source_ids; + for (auto [uv] : edges_bfs(g, uint32_t{3})) { + source_ids.push_back(source_id(g, uv)); + } + + // From 3 following incoming edges: edges arriving at 3 are from 1 and 2 + // Then from 1 and 2, incoming edges arrive from 0 + REQUIRE(source_ids.size() >= 2); // At least the edges into 3 +} + +TEST_CASE("vertices_bfs - reverse on chain", "[bfs][accessor][reverse]") { + auto g = make_chain(); // 0->1->2->3 + + // Reverse BFS from 3 should visit 3, 2, 1, 0 + std::vector visited; + for (auto [v] : vertices_bfs(g, uint32_t{3})) { + visited.push_back(vertex_id(g, v)); + } + + REQUIRE(visited.size() == 4); + REQUIRE(visited == std::vector{3, 2, 1, 0}); +} + +// ============================================================================= +// BFS — Forward with value function + Accessor +// ============================================================================= + +TEST_CASE("vertices_bfs - reverse with value function", "[bfs][accessor][reverse]") { + auto g = make_chain(); + auto vvf = [](const auto& gg, auto v) { return vertex_id(gg, v) * 10; }; + + std::vector ids; + std::vector vals; + for (auto [v, val] : vertices_bfs(g, uint32_t{3}, vvf)) { + ids.push_back(vertex_id(g, v)); + vals.push_back(val); + } + + REQUIRE(ids == std::vector{3, 2, 1, 0}); + REQUIRE(vals == std::vector{30, 20, 10, 0}); +} + +// ============================================================================= +// DFS — Forward (default accessor) +// ============================================================================= + +TEST_CASE("vertices_dfs - default accessor on bidir graph", "[dfs][accessor][forward]") { + auto g = make_chain(); // 0->1->2->3 + + std::vector visited; + for (auto [v] : vertices_dfs(g, uint32_t{0})) { + visited.push_back(vertex_id(g, v)); + } + + // DFS from 0 along the chain should visit all 4 + REQUIRE(visited.size() == 4); + REQUIRE(visited[0] == 0); + // DFS follows the single path: 0, 1, 2, 3 + REQUIRE(visited == std::vector{0, 1, 2, 3}); +} + +// ============================================================================= +// DFS — Reverse (in_edge_accessor) +// ============================================================================= + +TEST_CASE("vertices_dfs - reverse accessor from sink", "[dfs][accessor][reverse]") { + auto g = make_chain(); // 0->1->2->3 + + // Reverse DFS from 3 should visit 3, 2, 1, 0 + std::vector visited; + for (auto [v] : vertices_dfs(g, uint32_t{3})) { + visited.push_back(vertex_id(g, v)); + } + + REQUIRE(visited.size() == 4); + REQUIRE(visited == std::vector{3, 2, 1, 0}); +} + +TEST_CASE("edges_dfs - reverse accessor from sink", "[dfs][accessor][reverse]") { + auto g = make_chain(); + + std::vector neighbor_ids; + for (auto [uv] : edges_dfs(g, uint32_t{3})) { + neighbor_ids.push_back(source_id(g, uv)); + } + + // From 3 backwards: edges are 3<-2, 2<-1, 1<-0 + REQUIRE(neighbor_ids.size() == 3); +} + +TEST_CASE("vertices_dfs - reverse with value function", "[dfs][accessor][reverse]") { + auto g = make_chain(); + auto vvf = [](const auto& gg, auto v) { return vertex_id(gg, v) + 100; }; + + std::vector ids; + std::vector vals; + for (auto [v, val] : vertices_dfs(g, uint32_t{3}, vvf)) { + ids.push_back(vertex_id(g, v)); + vals.push_back(val); + } + + REQUIRE(ids == std::vector{3, 2, 1, 0}); + REQUIRE(vals == std::vector{103, 102, 101, 100}); +} + +// ============================================================================= +// DFS — Reverse on diamond +// ============================================================================= + +TEST_CASE("vertices_dfs - reverse on diamond from sink", "[dfs][accessor][reverse]") { + auto g = make_diamond(); // 0->1, 0->2, 1->3, 2->3 + + std::vector visited; + for (auto [v] : vertices_dfs(g, uint32_t{3})) { + visited.push_back(vertex_id(g, v)); + } + + // From 3, incoming edges lead to 1 and 2; from those, incoming edge leads to 0 + REQUIRE(visited.size() == 4); + REQUIRE(visited[0] == 3); + // All vertices reachable + std::set all(visited.begin(), visited.end()); + REQUIRE(all == std::set{0, 1, 2, 3}); +} + +// ============================================================================= +// Topological Sort — Forward (default accessor) +// ============================================================================= + +TEST_CASE("vertices_topological_sort - default accessor", "[topo][accessor][forward]") { + auto g = make_diamond(); // 0->1, 0->2, 1->3, 2->3 + + std::vector order; + for (auto [v] : vertices_topological_sort(g)) { + order.push_back(vertex_id(g, v)); + } + + REQUIRE(order.size() == 4); + // 0 must precede 1, 2; both 1 and 2 must precede 3 + auto pos = [&](uint32_t id) { return std::find(order.begin(), order.end(), id) - order.begin(); }; + REQUIRE(pos(0) < pos(1)); + REQUIRE(pos(0) < pos(2)); + REQUIRE(pos(1) < pos(3)); + REQUIRE(pos(2) < pos(3)); +} + +// ============================================================================= +// Topological Sort — Reverse (in_edge_accessor) +// ============================================================================= + +TEST_CASE("vertices_topological_sort - reverse accessor", "[topo][accessor][reverse]") { + auto g = make_diamond(); // 0->1, 0->2, 1->3, 2->3 + + // Reverse topo sort: follow incoming edges in DFS. + // The reversed dependency graph has edges 1->0, 2->0, 3->1, 3->2. + // Topological order of the reversed graph: 3 before {1,2}, {1,2} before 0 + std::vector order; + for (auto [v] : vertices_topological_sort(g)) { + order.push_back(vertex_id(g, v)); + } + + REQUIRE(order.size() == 4); + auto pos = [&](uint32_t id) { return std::find(order.begin(), order.end(), id) - order.begin(); }; + // In reverse topo: 3 before 1 and 2; 1 and 2 before 0 + REQUIRE(pos(3) < pos(1)); + REQUIRE(pos(3) < pos(2)); + REQUIRE(pos(1) < pos(0)); + REQUIRE(pos(2) < pos(0)); +} + +TEST_CASE("edges_topological_sort - reverse accessor", "[topo][accessor][reverse]") { + auto g = make_diamond(); + + std::set source_vertices; + for (auto [uv] : edges_topological_sort(g)) { + source_vertices.insert(source_id(g, uv)); + } + + // All vertices with incoming edges (in the reversed direction = original out-edges) + // should appear as sources + REQUIRE(!source_vertices.empty()); + REQUIRE(source_vertices.size() <= 4); +} + +// ============================================================================= +// Topological Sort — Safe variants with Accessor +// ============================================================================= + +TEST_CASE("vertices_topological_sort_safe - reverse accessor", "[topo][accessor][reverse][safe]") { + auto g = make_diamond(); + + auto result = vertices_topological_sort_safe(g); + REQUIRE(result.has_value()); + + std::vector order; + for (auto [v] : *result) { + order.push_back(vertex_id(g, v)); + } + + REQUIRE(order.size() == 4); +} + +TEST_CASE("vertices_topological_sort_safe - reverse with VVF", "[topo][accessor][reverse][safe]") { + auto g = make_diamond(); + auto vvf = [](const auto& gg, auto v) { return vertex_id(gg, v); }; + + auto result = vertices_topological_sort_safe(g, vvf); + REQUIRE(result.has_value()); + + std::vector order; + for (auto [v, val] : *result) { + order.push_back(val); + } + + REQUIRE(order.size() == 4); +} + +TEST_CASE("edges_topological_sort_safe - reverse accessor", "[topo][accessor][reverse][safe]") { + auto g = make_diamond(); + + auto result = edges_topological_sort_safe(g); + REQUIRE(result.has_value()); + + size_t count = 0; + for (auto [uv] : *result) { + ++count; + } + REQUIRE(count > 0); +} + +// ============================================================================= +// Source compatibility — existing call sites still compile +// ============================================================================= + +TEST_CASE("source compatibility - existing BFS calls unchanged", "[accessor][compat]") { + // Simple vector> graph — not bidirectional, uses default out_edge_accessor + using Graph = std::vector>; + Graph g = {{1, 2}, {3}, {3}, {}}; + + // These must still compile and work without specifying Accessor: + std::vector visited; + for (auto [v] : vertices_bfs(g, 0)) { + visited.push_back(static_cast(vertex_id(g, v))); + } + REQUIRE(visited.size() == 4); + + size_t edge_count = 0; + for (auto [uv] : edges_bfs(g, 0)) { + ++edge_count; + } + REQUIRE(edge_count == 3); +} + +TEST_CASE("source compatibility - existing DFS calls unchanged", "[accessor][compat]") { + using Graph = std::vector>; + Graph g = {{1, 2}, {3}, {3}, {}}; + + std::vector visited; + for (auto [v] : vertices_dfs(g, 0)) { + visited.push_back(static_cast(vertex_id(g, v))); + } + REQUIRE(visited.size() == 4); + + size_t edge_count = 0; + for (auto [uv] : edges_dfs(g, 0)) { + ++edge_count; + } + REQUIRE(edge_count == 3); +} + +TEST_CASE("source compatibility - existing topo sort calls unchanged", "[accessor][compat]") { + using Graph = std::vector>; + Graph g = {{1, 2}, {3}, {3}, {}}; + + std::vector visited; + for (auto [v] : vertices_topological_sort(g)) { + visited.push_back(static_cast(vertex_id(g, v))); + } + REQUIRE(visited.size() == 4); + + auto result = vertices_topological_sort_safe(g); + REQUIRE(result.has_value()); +} + +// ============================================================================= +// Explicit out_edge_accessor matches default behavior +// ============================================================================= + +TEST_CASE("explicit out_edge_accessor matches default BFS", "[accessor][explicit]") { + auto g = make_chain(); + + std::vector default_order, explicit_order; + + for (auto [v] : vertices_bfs(g, uint32_t{0})) { + default_order.push_back(vertex_id(g, v)); + } + + for (auto [v] : vertices_bfs(g, uint32_t{0})) { + explicit_order.push_back(vertex_id(g, v)); + } + + REQUIRE(default_order == explicit_order); +} + +TEST_CASE("explicit out_edge_accessor matches default DFS", "[accessor][explicit]") { + auto g = make_chain(); + + std::vector default_order, explicit_order; + + for (auto [v] : vertices_dfs(g, uint32_t{0})) { + default_order.push_back(vertex_id(g, v)); + } + + for (auto [v] : vertices_dfs(g, uint32_t{0})) { + explicit_order.push_back(vertex_id(g, v)); + } + + REQUIRE(default_order == explicit_order); +} + +TEST_CASE("explicit out_edge_accessor matches default topo sort", "[accessor][explicit]") { + auto g = make_diamond(); + + std::vector default_order, explicit_order; + + for (auto [v] : vertices_topological_sort(g)) { + default_order.push_back(vertex_id(g, v)); + } + + for (auto [v] : vertices_topological_sort(g)) { + explicit_order.push_back(vertex_id(g, v)); + } + + REQUIRE(default_order == explicit_order); +} diff --git a/tests/views/test_transpose.cpp b/tests/views/test_transpose.cpp new file mode 100644 index 0000000..44cc442 --- /dev/null +++ b/tests/views/test_transpose.cpp @@ -0,0 +1,239 @@ +/** + * @file test_transpose.cpp + * @brief Tests for transpose_view graph adaptor. + * + * Verifies that transpose_view correctly swaps edge directions: + * - edges(tv, v) returns in_edges of the underlying graph + * - in_edges(tv, v) returns edges of the underlying graph + * - target_id(tv, e) returns source_id of the underlying edge + * - source_id(tv, e) returns target_id of the underlying edge + * - Vertex-level CPOs (vertices, num_vertices, vertex_id) forward unchanged + * + * Uses bidirectional dynamic_graph containers (vov with Bidirectional=true). + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace adj = graph::adj_list; +namespace view = graph::views; +using namespace graph::container; + +// Non-uniform bidirectional traits: in_edge_type = dynamic_in_edge (has source_id()) +// so that bidirectional_adjacency_list concept is satisfied. +template +struct vov_bidir_graph_traits { + using edge_value_type = EV; + using vertex_value_type = VV; + using graph_value_type = GV; + using vertex_id_type = VId; + static constexpr bool bidirectional = true; + + using edge_type = dynamic_out_edge; + using in_edge_type = dynamic_in_edge; + using vertex_type = dynamic_vertex; + using graph_type = dynamic_graph; + + using edges_type = std::vector; + using in_edges_type = std::vector; + using vertices_type = std::vector; +}; + +// Bidirectional vov graph (random-access edges — full transpose_view support) +using bidir_vov = dynamic_graph>; + +// Bidirectional vov with void edge values +using bidir_vov_void = dynamic_graph>; + +// ============================================================================= +// Helper: build a small directed graph for transpose testing +// +// 0 --→ 1 --→ 2 +// | ↑ +// +---→ 3 ---+ +// +// Edges: (0,1), (0,3), (1,2), (3,2) +// Transpose edges: (1,0), (3,0), (2,1), (2,3) +// ============================================================================= + +static bidir_vov_void make_dag() { + return bidir_vov_void({{0, 1}, {0, 3}, {1, 2}, {3, 2}}); +} + +// ============================================================================= +// Vertex CPO forwarding +// ============================================================================= + +TEST_CASE("transpose_view - vertex CPOs forward unchanged", "[views][transpose]") { + auto g = make_dag(); + auto tv = view::transpose(g); + + CHECK(adj_list::num_vertices(tv) == adj_list::num_vertices(g)); + + // Verify vertices range produces same vertex ids + std::vector g_ids, tv_ids; + for (auto&& [uid, u] : view::vertexlist(g)) + g_ids.push_back(uid); + for (auto&& [uid, u] : view::vertexlist(tv)) + tv_ids.push_back(uid); + + REQUIRE(g_ids == tv_ids); +} + +// ============================================================================= +// Edge direction swapping +// ============================================================================= + +TEST_CASE("transpose_view - edges returns in_edges of underlying", "[views][transpose]") { + auto g = make_dag(); + auto tv = view::transpose(g); + + // Vertex 2 has in-edges from 1 and 3 in the original graph. + // So edges(tv, v2) should yield those two edges. + auto v2 = *adj_list::find_vertex(tv, uint32_t(2)); + std::set edge_sources; + for (auto&& ie : adj_list::edges(tv, v2)) { + // target_id on transpose edge = source_id on underlying in-edge + edge_sources.insert(adj_list::target_id(tv, ie)); + } + CHECK(edge_sources == std::set{1, 3}); +} + +TEST_CASE("transpose_view - in_edges returns edges of underlying", "[views][transpose]") { + auto g = make_dag(); + auto tv = view::transpose(g); + + // Vertex 0 has out-edges to 1 and 3 in the original graph. + // So in_edges(tv, v0) should yield those two edges. + auto v0 = *adj_list::find_vertex(tv, uint32_t(0)); + std::set edge_targets; + for (auto&& ie : adj_list::in_edges(tv, v0)) { + edge_targets.insert(adj_list::source_id(tv, ie)); + } + // source_id on transpose's in-edges = target_id on underlying = 1, 3 + CHECK(edge_targets == std::set{1, 3}); +} + +TEST_CASE("transpose_view - degree is swapped", "[views][transpose]") { + auto g = make_dag(); + auto tv = view::transpose(g); + + // Vertex 0: out-degree=2, in-degree=0 in original + // Transpose: degree=0, in_degree=2 + auto v0 = *adj_list::find_vertex(tv, uint32_t(0)); + CHECK(adj_list::degree(tv, v0) == 0); + CHECK(adj_list::in_degree(tv, v0) == 2); + + // Vertex 2: out-degree=0, in-degree=2 in original + // Transpose: degree=2, in_degree=0 + auto v2 = *adj_list::find_vertex(tv, uint32_t(2)); + CHECK(adj_list::degree(tv, v2) == 2); + CHECK(adj_list::in_degree(tv, v2) == 0); +} + +// ============================================================================= +// Double transpose is identity +// ============================================================================= + +TEST_CASE("transpose_view - double transpose is identity", "[views][transpose]") { + auto g = make_dag(); + auto tv = view::transpose(g); + auto ttv = view::transpose(tv); + + // edges(ttv, v0) should be same as edges(g, v0) + auto v0 = *adj_list::find_vertex(g, uint32_t(0)); + + std::set g_targets, ttv_targets; + for (auto&& e : adj_list::edges(g, v0)) { + g_targets.insert(adj_list::target_id(g, e)); + } + + auto tv0 = *adj_list::find_vertex(ttv, uint32_t(0)); + for (auto&& e : adj_list::edges(ttv, tv0)) { + ttv_targets.insert(adj_list::target_id(ttv, e)); + } + + CHECK(g_targets == ttv_targets); +} + +// ============================================================================= +// Whole-graph edge collection in transpose +// ============================================================================= + +TEST_CASE("transpose_view - all transposed edges correct", "[views][transpose]") { + auto g = make_dag(); + auto tv = view::transpose(g); + + // Original edges: (0,1), (0,3), (1,2), (3,2) + // Transposed: edges(tv, v) should give: 1→0, 3→0, 2→1, 2→3 + // Collect as (source, target) pairs in traverse order + std::set> transposed_edges; + + for (auto&& [uid, u] : view::vertexlist(tv)) { + for (auto&& e : adj_list::edges(tv, u)) { + auto tid = adj_list::target_id(tv, e); + transposed_edges.emplace(static_cast(uid), static_cast(tid)); + } + } + + std::set> expected{ + {1, 0}, {2, 1}, {2, 3}, {3, 0}}; + + CHECK(transposed_edges == expected); +} + +// ============================================================================= +// Edge value forwarding +// ============================================================================= + +TEST_CASE("transpose_view - edge_value preserved", "[views][transpose]") { + // Use weighted bidirectional graph + bidir_vov g({{0, 1, 10}, {1, 2, 20}, {2, 0, 30}}); + auto tv = view::transpose(g); + + // Collect edge values from vertex 0's edges in transpose + // Vertex 0 has in-edges: (2→0, weight=30) + // So edges(tv, v0) should have one edge with value 30 + auto v0 = *adj_list::find_vertex(tv, uint32_t(0)); + + std::vector values; + for (auto&& e : adj_list::edges(tv, v0)) { + values.push_back(adj_list::edge_value(tv, e)); + } + + REQUIRE(values.size() == 1); + CHECK(values[0] == 30); +} + +// ============================================================================= +// Empty graph +// ============================================================================= + +TEST_CASE("transpose_view - empty graph", "[views][transpose]") { + bidir_vov_void g; + g.resize_vertices(0); // ensure graph is valid but empty + auto tv = view::transpose(g); + + CHECK(adj_list::num_vertices(tv) == 0); +} + +// ============================================================================= +// Single vertex, no edges +// ============================================================================= + +TEST_CASE("transpose_view - single vertex no edges", "[views][transpose]") { + bidir_vov_void g; + g.resize_vertices(1); + auto tv = view::transpose(g); + + CHECK(adj_list::num_vertices(tv) == 1); +}