Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
57f3204
Implement incoming edges Phases 1-3 and rename concepts/aliases for c…
pratzl Feb 22, 2026
054d6cc
Swap primary/alias: out_edges, out_degree, find_out_edge are now primary
pratzl Feb 22, 2026
d53310d
Rename contains_edge -> contains_out_edge (primary), contains_edge is…
pratzl Feb 22, 2026
64cc2d1
Rename has_edge(g) CPO to has_edges(g)
pratzl Feb 22, 2026
80a4e1a
Phase 4: undirected_adjacency_list incoming-edge support
pratzl Feb 22, 2026
0951379
Update Phase 5-6 plan: out_* primary naming for views and adaptors
pratzl Feb 22, 2026
428c1f4
Update Phase 5 plan: single-header approach for views
pratzl Feb 22, 2026
94abc34
Restructure Phases 5-7: edge accessor parameterization
pratzl Feb 22, 2026
90f6157
Phase 5: Edge accessor + parameterized incidence/neighbors views
pratzl Feb 22, 2026
ecef002
Phase 6: Pipe-syntax adaptors for incoming views + alias renames
pratzl Feb 22, 2026
3f994fc
Phase 8: dynamic_graph bidirectional support with direction tags
pratzl Feb 22, 2026
7911ba1
Phase 9: transpose_view + kosaraju bidirectional overload
pratzl Feb 22, 2026
50b8e15
Phases 7 & 10: Accessor-parameterized search views + documentation
pratzl Feb 23, 2026
e96800e
docs: update migration guide and coverage report for bidirectional/un…
pratzl Feb 23, 2026
4c6862d
Minor update to migration guide for v2.1.0 release.
pratzl Feb 23, 2026
4b933c8
docs: update metrics and implementation matrix (2026-02-23)
pratzl Feb 23, 2026
026df37
refine dynamic_edge_refactor_strategy: comprehensive review fixes\n\n…
pratzl Feb 23, 2026
ded9bd9
add phased implementation plan for dynamic_edge refactor\n\n6-phase p…
pratzl Feb 23, 2026
9a39591
docs: apply final review patches to dynamic_edge_refactor_plan
pratzl Feb 23, 2026
715020f
feat: add dynamic_in_edge class and detection-idiom aliases (Phase 1)
pratzl Feb 23, 2026
46ba10d
feat: wire in_edge_type into bidir vertex base and load_edges (Phase 2)
pratzl Feb 23, 2026
f560d86
refactor: rename dynamic_edge → dynamic_out_edge with deprecated alia…
pratzl Feb 23, 2026
0b57cec
docs: mark Phase 3 COMPLETE in refactor plan
pratzl Feb 23, 2026
89e1018
refactor: remove Sourced from edge classes, collapse dynamic_out_edge…
pratzl Feb 23, 2026
08e6ef6
Phase 4b: Remove bool Sourced from vertex and graph classes
pratzl Feb 23, 2026
67a0e1f
Phase 4c: Remove bool Sourced from all 27 traits files
pratzl Feb 23, 2026
7b06988
Phase 4d: Simplify load_edges — remove Sourced branches and make_in_edge
pratzl Feb 23, 2026
01a980a
Phase 4f/4g: Compile gate — fix examples, verify library headers compile
pratzl Feb 23, 2026
cd8b5a5
docs: update status tracker — Phase 4a-4g all COMPLETE
pratzl Feb 23, 2026
9fd9c1f
Phase 5.1: Update graph_test_types.hpp — remove Sourced, add bidir al…
pratzl Feb 23, 2026
880ae00
test: update all tests for Sourced removal and add bidir uniform/non-…
pratzl Feb 23, 2026
74c731d
chore: update status tracker for Phase 5 completion (5.8 count fix, 5…
pratzl Feb 23, 2026
686d897
chore: fix stale dynamic_edge comment in edge_descriptor.hpp (Phase 6.1)
pratzl Feb 23, 2026
ee50199
docs: update Doxygen comments in dynamic_graph.hpp — remove Sourced/d…
pratzl Feb 23, 2026
e8a426b
docs: fix stale Sourced references in migration-from-v2.md (Phase 6.3)
pratzl Feb 23, 2026
42e6414
docs: cleanup Doxygen, update user docs, archive strategy (Phase 6)
pratzl Feb 23, 2026
25fc136
chore: record Phase 6.6 commit hash in status tracker
pratzl Feb 23, 2026
3ccfd6a
test: add coverage for uncovered dijkstra_shortest_paths.hpp lines (e…
pratzl Feb 23, 2026
57f3efb
docs: update coverage.md — dijkstra 100% line coverage, overall 96.3%
pratzl Feb 23, 2026
b8223e9
docs: fix Quick Example in README — add init_shortest_paths, weight f…
pratzl Feb 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<G>`, `in_edge_iterator_t<G>`, `in_edge_t<G>` 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
Expand Down
24 changes: 18 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.**

</td>
</tr></table>
Expand All @@ -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)

---

Expand All @@ -23,16 +23,16 @@
- **Works with your containers** — `std::vector<std::vector<int>>` or `std::map<int, std::vector<int>>` 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 <vector>
Expand All @@ -50,13 +50,25 @@ int main() {
std::vector<double> distance(graph::num_vertices(g));
std::vector<uint32_t> 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
```

<!--
A full example would be useful, but dijkstra_clrs_example uses a reference version of Dijkstra, not the
library's version.
Expand Down Expand Up @@ -181,4 +193,4 @@ Distributed under the [Boost Software License 1.0](LICENSE).
---

<!-- Status counts pinned to docs/status/metrics.md -->
**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
518 changes: 518 additions & 0 deletions agents/archive/dynamic_edge_refactor_strategy.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<G>` — i.e. the exact range type returned by
**Return type:** `in_edge_range_t<G>` — 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.

Expand All @@ -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
Expand All @@ -173,33 +175,41 @@ matches `source_id(g, ie)` against the target vertex (the in-neighbor). The defa

| Alias | Definition |
|---|---|
| `in_vertex_edge_range_t<G>` | `decltype(in_edges(g, vertex_t<G>))` |
| `in_vertex_edge_iterator_t<G>` | `ranges::iterator_t<in_vertex_edge_range_t<G>>` |
| `in_edge_t<G>` | `ranges::range_value_t<in_vertex_edge_range_t<G>>` |
| `in_edge_range_t<G>` | `decltype(in_edges(g, vertex_t<G>))` |
| `in_edge_iterator_t<G>` | `ranges::iterator_t<in_edge_range_t<G>>` |
| `in_edge_t<G>` | `ranges::range_value_t<in_edge_range_t<G>>` |

`in_edge_t<G>` is **independently deduced** from the `in_edges()` return range.
It is commonly the same type as `edge_t<G>` but this is not required. See
It is commonly the same type as `out_edge_t<G>` 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<G>` | `decltype(out_edges(g, vertex_t<G>))` |
| `out_edge_iterator_t<G>` | `ranges::iterator_t<out_edge_range_t<G>>` |
| `out_edge_t<G>` | `ranges::range_value_t<out_edge_range_t<G>>` |

Convenience aliases:

| Alias | Definition |
|---|---|
| `out_vertex_edge_range_t<G>` | Same as `vertex_edge_range_t<G>` |
| `out_vertex_edge_iterator_t<G>` | Same as `vertex_edge_iterator_t<G>` |
| `out_edge_t<G>` | Same as `edge_t<G>` |
| `vertex_edge_range_t<G>` | Same as `out_edge_range_t<G>` |
| `vertex_edge_iterator_t<G>` | Same as `out_edge_iterator_t<G>` |
| `edge_t<G>` | Same as `out_edge_t<G>` |

---

## 6. New Concepts

**File:** `include/graph/adj_list/adjacency_list_concepts.hpp`

### 6.1 `in_vertex_edge_range<R, G>`
### 6.1 `in_edge_range<R, G>`

```cpp
template <class R, class G>
concept in_vertex_edge_range =
concept in_edge_range =
std::ranges::forward_range<R>;
```

Expand All @@ -210,7 +220,7 @@ not satisfy the full `edge<G, E>` concept. The minimum requirement is that
`source_id(g, ie)` can extract the neighbor vertex ID (see §6.2).

When `in_edge_t<G>` and `edge_t<G>` 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<G>`

Expand All @@ -219,7 +229,7 @@ template <class G>
concept bidirectional_adjacency_list =
adjacency_list<G> &&
requires(G& g, vertex_t<G> u, in_edge_t<G> ie) {
{ in_edges(g, u) } -> in_vertex_edge_range<G>;
{ in_edges(g, u) } -> in_edge_range<G>;
{ source_id(g, ie) } -> std::convertible_to<vertex_id_t<G>>;
// Note: no edge_value() requirement on incoming edges
};
Expand Down Expand Up @@ -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;

Expand All @@ -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;
```

Expand All @@ -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 |
Expand All @@ -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
Expand Down Expand Up @@ -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) | — |
Expand Down Expand Up @@ -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"
Expand All @@ -759,7 +769,7 @@ directionality.

```cpp
template <class G> using edge_t = ranges::range_value_t<vertex_edge_range_t<G>>; // from edges(g,u)
template <class G> using in_edge_t = ranges::range_value_t<in_vertex_edge_range_t<G>>; // from in_edges(g,u)
template <class G> using in_edge_t = ranges::range_value_t<in_edge_range_t<G>>; // from in_edges(g,u)
```

They are commonly the same type, but this is **not required**. Scenarios where
Expand Down
Loading
Loading