Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
## [Unreleased]

### Added
- **Container mutation API** — BGL-style member functions for incrementally building and editing graphs:
- **`dynamic_graph`**: `add_vertex()` / `add_vertex(val)` (sequential), `add_vertex(id)` / `add_vertex(id, val)` (associative, returns `bool`), `add_edge(u, v[, val])` (throws `std::out_of_range` if either endpoint is missing; maintains in-edges when bidirectional), `remove_edge(u, v)` (returns count removed), `remove_vertex(u)` (sequential renumbers higher ids; associative keeps stable keys).
- **`undirected_adjacency_list`**: `remove_edge(uid, vid)` (returns count removed), `remove_vertex(uid)` (O(V+E), renumbers higher ids). Both throw `std::out_of_range` on invalid ids.
- **Composable visitor toolkit** (`algorithm/visitor_factory.hpp`, included via `algorithms.hpp` umbrella) — BGL-style event-visitor combinators built on the existing duck-typed `on_*` callback model, without changing any traversal algorithm:
- **Layer 1 — single-event adaptors** (`on_discover_vertex(f)`, `on_tree_edge(f)`, …): wrap a callable so it fires for exactly one traversal event. Generated for all 14 event names (5 vertex + 9 edge) via a macro. Analogous to BGL event tags.
- **Layer 2 — `composite_visitor` / `make_visitor(...)`**: fan a single traversal out to multiple sub-visitors. Each event method is constrained by a fold over the child pack, so a composite exposes `on_X` only when at least one child handles it — keeping `has_on_*` / `valid_visitor` detection accurate and preserving zero-overhead event skipping. Bridges descriptor- and id-form children automatically via `vertex_id()`.
Expand Down Expand Up @@ -59,6 +62,7 @@
- `germany_routes_example.cpp` — builds a Germany routes graph, traverses it, and runs Dijkstra twice (segment count and km distance)

### Changed
- **`undirected_adjacency_list` mutation API renamed** to match `dynamic_graph` and BGL conventions: `create_vertex` → `add_vertex`, `create_edge` → `add_edge`, `erase_edge` → `remove_edge`. The old member names were removed (no backward-compatible aliases); update call sites accordingly.
- **`edge_descriptor` simplified to iterator-only storage** — removed the `conditional_t<random_access_iterator, size_t, EdgeIter>` dual-storage path; edges always store the iterator directly since edges always have physical containers. Eliminates 38 `if constexpr` branches across 6 files (~500 lines removed).
- **`compressed_graph::vertices(g)` returns `iota_view`** — simplified to `std::ranges::iota_view<size_t, size_t>(0, num_vertices())`, which the `vertices` CPO wraps automatically via `_wrap_if_needed`.
- **`vertex_descriptor_view` CTAD deduction guides** — updated from `Container::iterator`/`const_iterator` to `std::ranges::iterator_t<>` for compatibility with views like `iota_view`.
Expand Down
121 changes: 96 additions & 25 deletions agents/bgl_migration_strategy.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ graph-v3 is a ground-up C++20 redesign targeting ISO standardization (P3126–P3
- No `subgraph` hierarchy with descriptor mapping
- No DIMACS or METIS I/O
- Graph generators partially implemented (Erdős-Rényi G(n,p), Barabási-Albert, 2D grid, path available; Watts-Strogatz, R-MAT, complete graph still missing)
- `dynamic_graph` lacks individual mutation (`add_vertex`, `add_edge`, `remove_vertex`, `remove_edge`); `undirected_adjacency_list` provides `create_vertex` / `create_edge` / `erase_edge` but no `erase_vertex`
- Both `dynamic_graph` and `undirected_adjacency_list` now support full mutation (`add_vertex`, `add_edge`, `remove_edge`, `remove_vertex`); no major mutation gaps remain
- No `adjacency_matrix` container
- No `copy_graph` utility with cross-type and property mapping support
- No `labeled_graph` adaptor (string labels → vertex mapping)
Expand Down Expand Up @@ -138,11 +138,11 @@ These are behavioral analogues, not strict one-to-one translations. In particula
| `BidirectionalGraph` | `bidirectional_adjacency_list<G>` | `in_edges(g, u)` available |
| `AdjacencyGraph` | Part of `adjacency_list<G>` | `neighbors(g, u)` view |
| `VertexListGraph` | `vertex_range<G>` | `vertices(g)` returns range |
| `EdgeListGraph` | `edgelist(g)` view | Not a concept — a view function |
| `EdgeListGraph` | `basic_sourced_edgelist<EL>` / `basic_sourced_index_edgelist<EL>` | Concepts for an edgelist (range of edges); `edgelist(g)` is a *view* that produces such a range from an adjacency list |
| `VertexAndEdgeListGraph` | `adjacency_list<G>` | Combined by default |
| `AdjacencyMatrix` | ❌ Not available | No O(1) edge lookup concept |
| `MutableGraph` | ❌ No unified concept | `dynamic_graph` is batch-load oriented; `undirected_adjacency_list` supports create-vertex/create-edge and edge erasure |
| `PropertyGraph` | ❌ No formal concept | CPOs `vertex_value`/`edge_value` serve the role |
| `MutableGraph` | ❌ No unified concept | No mutating CPOs are defined in the current design |
| `PropertyGraph` | ❌ No formal concept | CPOs `vertex_value`/`edge_value` and their matching `vertex_value_t<G>/edge_value_t<G>` serve the role |
| `ColorValue` | ❌ Internal only | Hidden inside algorithms |

### Key Concept Differences
Expand All @@ -162,8 +162,8 @@ These are behavioral analogues, not strict one-to-one translations. In particula
| `graph_traits<G>::vertex_descriptor` | `vertex_t<G>` (descriptor-like handle); use `vertex_id_t<G>` when you specifically need the key/index |
| `graph_traits<G>::edge_descriptor` | `edge_t<G>` |
| `graph_traits<G>::vertex_iterator` | `vertex_iterator_t<G>` |
| `graph_traits<G>::out_edge_iterator` | `iterator_t<vertex_edge_range_t<G>>` |
| `graph_traits<G>::in_edge_iterator` | `iterator_t<in_edge_range_t<G>>` |
| `graph_traits<G>::out_edge_iterator` | `out_edge_iterator_t<G>` |
| `graph_traits<G>::in_edge_iterator` | `in_edge_iterator_t<G>` |
| `graph_traits<G>::adjacency_iterator` | (use `neighbors(g, u)` view) |
| `graph_traits<G>::directed_category` | (implicit in type: `dynamic_graph` vs `undirected_adjacency_list`) |
| `graph_traits<G>::traversal_category` | (replaced by concept constraints) |
Expand Down Expand Up @@ -717,39 +717,58 @@ To achieve full BGL parity, the following generators are still needed:
```cpp
typedef boost::adjacency_list<vecS, vecS, directedS,
no_property, property<edge_weight_t, double>> Graph;
typedef graph_traits<Graph>::vertex_descriptor Vertex;
typedef graph_traits<Graph>::edge_descriptor Edge;
typedef graph_traits<Graph>::vertex_descriptor Vertex; // integral (size_t) for vecS
typedef graph_traits<Graph>::edge_descriptor Edge;
typedef graph_traits<Graph>::vertices_size_type VId; // same as vertex_descriptor for vecS
```

**graph-v3:**

Define a graph based on `dynamic_graph`. This is similar to the graph used by BGL, but it's
not the only graph type supported.
```cpp
#include <graph/container/traits/vov_graph_traits.hpp>
using Graph = graph::container::vov_graph<double>; // vecS vertices, vecS edges, EV=double
using Vertex = vertex_t<Graph>;
using Edge = edge_t<Graph>;
using VId = graph::vertex_id_t<Graph>; // integral in this example, but may be non-integral
```

Graphs may also be defined using standard containers. The following has the same characteristics
as the `vov_graph` above. Construction and mutation functions (e.g. add/remove for vertices and
edges) are unique to a graph type.
```cpp
using Graph = graph::dynamic_graph<double>; // EV=double, VV=void
using VId = graph::vertex_id_t<Graph>;
using Graph = vector<<vector<pair<size_t,double>>>; // vecS vertices, vecS edges, EV=double
using Vertex = vertex_t<Graph>;
using Edge = edge_t<Graph>;
using VId = graph::vertex_id_t<Graph>; // integral in this example, but may be non-integral
```

User-defined graphs can easily be used by by overriding 3-7 CPO functions for the graph.

#### Pattern 2: Graph Construction with add_edge

**BGL:**
```cpp
Graph g(5);
Graph g(5); // 5 vertices, no edges
add_edge(0, 1, 10.0, g);
add_edge(1, 2, 20.0, g);
add_edge(2, 3, 30.0, g);
```

**graph-v3:**
```cpp
Graph g({{0,1,10.0}, {0,2,20.0}, {2,3,30.0}});
// Or with undirected_adjacency_list for individual mutation:
undirected_adjacency_list<double> g;
g.create_vertex(); g.create_vertex(); // ...
g.create_edge(0, 1, 10.0);
Graph g(5); // 5 vertices, no edges
g.add_edge(0, 1, 10.0);
g.add_edge(1, 2, 20.0);
g.add_edge(2, 3, 30.0);
```
* Note that add_edge is a member function of g. There are no mutating CPOs at this time.

> ⚠️ **Migration friction:** `dynamic_graph` does not support incremental `add_edge`. Code that builds graphs edge-by-edge must either:
> 1. Collect edges into a vector first, then construct
> 2. Use `undirected_adjacency_list` (undirected only)
> 3. Wait for mutation API to be added to `dynamic_graph`
Or with an _Initializer list_
```cpp
Graph g({{0,1,10.0}, {1,2,20.0}, {2,3,30.0}});
```

#### Pattern 3: Vertex/Edge Iteration

Expand All @@ -765,9 +784,20 @@ for (tie(vi, vi_end) = vertices(g); vi != vi_end; ++vi) {
```

**graph-v3:**

Using views:
```cpp
for (auto&& [uid, u] : vertexlist(g)) {
for (auto&& [tid, uv] : incidence(g, u)) {
for (auto&& [vid, uv] : incidence(g, u)) {
double w = edge_value(g, uv);
}
}
```

Using lower-level CPO functions directly:
```cpp
for (auto u : vertices(g)) {
for (auto uv : out_edges(g, u)) {
double w = edge_value(g, uv);
}
}
Expand All @@ -787,11 +817,16 @@ dijkstra_shortest_paths(g, s,
**graph-v3:**
```cpp
vector<VId> pred(num_vertices(g));
vector<double> dist(num_vertices(g), numeric_limits<double>::max());
vector<double> dist(num_vertices(g));
init_shortest_paths(g, dist, pred); // dist -> infinite_distance, pred -> self-id
dijkstra_shortest_paths(g, {s}, container_value_fn(dist), container_value_fn(pred),
[](const auto& g, const auto& uv) { return edge_value(g, uv); });
```

> **Note:** BGL's full `dijkstra_shortest_paths` initializes the distance and predecessor
> maps internally. graph-v3 does not auto-initialize, so the caller must call
> `init_shortest_paths(g, dist, pred)` first (or pre-fill `dist` with `infinite_distance<double>()`).

#### Pattern 5: Custom BFS Visitor

**BGL:**
Expand Down Expand Up @@ -821,7 +856,6 @@ dijkstra_shortest_paths(fg, s, ...);

**graph-v3:**
```cpp
#include <graph/adaptors/filtered_graph.hpp>
auto ep = [&](auto&& uv) { return edge_value(g, uv) > 5.0; };
auto fg = graph::adaptors::filtered_graph(g, graph::adaptors::keep_all{}, ep);
dijkstra_shortest_paths(fg, {s}, dist_fn, pred_fn, weight_fn);
Expand Down Expand Up @@ -888,6 +922,15 @@ There is **no `graph_traits` to specialise**. A CPO is satisfied in one of three
2. A free function found by ADL in the namespace of `G` (e.g. `vertices(g)` in namespace `boost`).
3. A built-in default that works when `G` matches a recognised pattern (e.g. random-access range of inner ranges of integers).

> **Note:** This refers to *adapting* an external graph type — there is no trait to
> specialise to teach graph-v3 about your type; you satisfy the CPOs instead. This is
> distinct from `dynamic_graph`, the container used in many examples, which *does* take a
> traits template parameter that selects the container types for vertices and edges.
> Pre-defined combinations are provided as aliases in their own headers (e.g. `vov_graph`
> = vector-of-vector). Because those traits constrain containers via *concepts* (not
> concrete standard-library types), containers from other libraries can be used as long
> as they model the required concepts.

For BGL types you will use **option 2**: add free functions in `namespace boost`. ADL will find them because `boost::adjacency_list` lives in that namespace.

### 12.2 Recommended Layout
Expand All @@ -908,6 +951,16 @@ All adapter functions go inside `namespace boost { ... }` so ADL picks them up.

### 12.3 Minimal Adapter for `boost::adjacency_list`

> **Note:** graph-v3 already ships a complete, production-ready BGL adaptor — prefer it
> over hand-rolling your own. Include `<graph/adaptors/bgl/graph_adaptor.hpp>` (and
> `<graph/adaptors/bgl/property_bridge.hpp>` for property maps). It supports `vecS`/`listS`/`setS`
> storage, `directedS`/`undirectedS`/`bidirectionalS`, `in_edges`, and BGL property-map bridging.
> See [docs/user-guide/bgl-adaptor.md](../docs/user-guide/bgl-adaptor.md) and
> [examples/bgl_adaptor_example.cpp](../examples/bgl_adaptor_example.cpp).
>
> The minimal example below is kept for reference: it shows *how* CPO-based adaptation works
> and serves as a template for adapting other third-party graph types.

The example below adapts `boost::adjacency_list<vecS, vecS, directedS, VBundle, EBundle>` — the most common BGL configuration. It is the analogue of graph-v3's `vov_graph_traits`-backed `dynamic_graph`.

```cpp
Expand Down Expand Up @@ -1031,6 +1084,25 @@ graph::dijkstra_shortest_paths(g, {source}, distances, predecessors, weight);

**Exterior property maps.** Pass them as ordinary lambdas — graph-v3 algorithms accept any invocable taking an edge or vertex descriptor.

**No bundled properties.** When the BGL `adjacency_list` carries no bundled properties, you don't
need `vertex_value` / `edge_value` at all. Instead, define a separate function object per property
whose call interface takes the graph plus a descriptor — `prop(g, u)` for vertex properties or
`prop(g, uv)` for edge properties — and hand each one to the algorithms that consume it:

```cpp
// Pull values from an interior or exterior BGL property map.
auto weight = [wmap = ::boost::get(::boost::edge_weight, g)](const auto& g, auto uv) -> double {
return ::boost::get(wmap, uv); // prop(g, uv)
};
auto vcolor = [cmap = ::boost::get(::boost::vertex_color, g)](const auto& g, auto u) {
return ::boost::get(cmap, u); // prop(g, u)
};

graph::dijkstra_shortest_paths(g, {source}, distances, predecessors, weight);
```

Each property is an independent callable, so you only define the ones a given algorithm requires.

### 12.6 Other BGL Container Configurations

| BGL `OutEdgeS` / `VertexS` | Vertex descriptor | Adapter notes |
Expand Down Expand Up @@ -1086,8 +1158,7 @@ These items block migration for the largest number of BGL users:

| Item | Type | Effort | Rationale |
|------|------|--------|-----------|
| **Individual mutation on directed graph** | Container | High | `add_vertex()`, `add_edge()`, `remove_vertex()`, `remove_edge()` on `dynamic_graph`. Most BGL code builds graphs incrementally. |
| **`erase_vertex` on `undirected_adjacency_list`** | Container | Medium | Currently only `erase_edge` is supported on the mutation-friendly container |
| **`adjacency_matrix` container** | Container | High | BGL users relying on matrix storage have no direct equivalent |
| **A\* Search** | Algorithm | Medium | Heavily used in pathfinding, robotics, game AI |
| **`copy_graph` utility** | Utility | Low | Cross-type graph copy with property mapping |
| **Betweenness Centrality** | Algorithm | Medium | Core network analysis metric |
Expand Down
18 changes: 10 additions & 8 deletions docs/migration-from-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,11 @@ class undirected_adjacency_list;
| 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 |
| `add_vertex()` | O(1) amortized |
| `add_edge(u, v)` | O(1) |
| `remove_edge(pos)` | O(1) — unlinks from both vertices' lists |
| `remove_edge(uid, vid)` | O(degree(uid)) |
| `remove_vertex(uid)` | O(V + E) — renumbers higher vertex ids |
| `degree(v)` | O(1) — cached per vertex |
| Iterate edges from vertex | O(degree) |
| Iterate all edges | O(V + E) |
Expand All @@ -88,8 +90,8 @@ class undirected_adjacency_list;
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()`.
- Vertex iterators: invalidated by `add_vertex()` if reallocation occurs, by `remove_vertex()`, and by `clear()`.
- Vertex iterators: **not** invalidated by `add_edge()` or `remove_edge()`.
**Basic usage:**
Expand All @@ -100,9 +102,9 @@ using namespace graph::container;
// Edge value = int (weight), vertex value = std::string (name)
undirected_adjacency_list<int, std::string> g;
auto u = g.create_vertex("Alice");
auto v = g.create_vertex("Bob");
auto e = g.create_edge(u, v, 42); // weight 42
auto u = g.add_vertex("Alice");
auto v = g.add_vertex("Bob");
auto e = g.add_edge(u, v, 42); // weight 42
// Iterate incident edges of u
for (auto&& [uid, vid, uv] : edges(g, *u)) {
Expand Down
Loading
Loading