An open API service indexing awesome lists of open source software.

https://github.com/mik1810/nxpp

nxpp is a modular C++20 graph library built on top of Boost Graph Library, offering a practical NetworkX-inspired API for graph construction, traversal, shortest paths, components, flows, and generators, with a generated single-header release asset also available.
https://github.com/mik1810/nxpp

algorithms boost boost-graph-library c-plus-plus cpp20 graph graph-algorithms graph-library header-only networkx

Last synced: 3 months ago
JSON representation

nxpp is a modular C++20 graph library built on top of Boost Graph Library, offering a practical NetworkX-inspired API for graph construction, traversal, shortest paths, components, flows, and generators, with a generated single-header release asset also available.

Awesome Lists containing this project

README

          

# nxpp — NetworkX-inspired graph utilities for modern C++

Current release: `v0.7.13` (April 3, 2026)


nxpp logo

`nxpp` is a **header-only C++20** library built on top of the **Boost Graph Library (BGL)**.

It does **not** try to reimplement all of NetworkX.
It aims to provide a **small, practical, C++-friendly graph API** that feels closer to NetworkX in day-to-day use while still delegating most graph work to Boost.

The core abstraction is:

```cpp
nxpp::Graph
```

with public aliases such as:

- `nxpp::WeightedGraphInt`
- `nxpp::WeightedGraphStr`
- `nxpp::WeightedDiGraph`
- `nxpp::WeightedDiGraphInt`
- `nxpp::GraphInt`
- `nxpp::GraphStr`
- `nxpp::DiGraph`
- `nxpp::DiGraphInt`
- `nxpp::MultiGraph`
- `nxpp::MultiDiGraph`
- `nxpp::UnweightedGraphInt`
- `nxpp::UnweightedDiGraphInt`

---

## Project status

`nxpp` is now released as `v0.7.13`.

> [!WARNING]
> The public API is still settling.
> The biggest open work is no longer the critical multigraph block or the first
> formal-test block, but the next round of documentation cleanup, generated-docs
> scaffolding, packaging polish, and broader CI / usage clarity.

Today, the project is strongest as:

- a **header-only wrapper over BGL**
- a **NetworkX-inspired convenience layer**
- a library that often returns **materialized C++ results**
- a project with a **snippet-based parity / regression harness**
- a codebase that now also has a **real assertion-based test suite with CI**

The most important open issue groups right now are:

- documentation/generated-docs/testing-story cleanup: `#28`
- API safety / attribute-system follow-up: `#25`, `#27`
- packaging / CI follow-up: none immediately blocking

Detailed API tables now live in [`docs/API_REFERENCE.md`](docs/API_REFERENCE.md).
Runtime validation errors now use a more consistent `X failed: ...` wording
across the public surface.

---

## Read this before using `nxpp`

### 1. Complexity in this README means **public-call complexity**

This README documents the cost of calling an `nxpp` function, not only the underlying graph algorithm.

That matters because `nxpp` often does extra work around the Boost call itself, for example:

- NodeID ↔ descriptor translation
- `std::any`-based attribute lookups
- result materialization into containers
- cleanup of metadata on destructive operations
- descriptor remapping after `remove_node()`

So the complexity tables below describe the cost of the **full `nxpp` function call**.

Where `nxpp` now materializes ordered map-like public results, those notes also
count the real `O(log n)` insertion/lookup behavior of the underlying
`std::map` storage instead of treating hash-table behavior as expected `O(1)`.
Some other results now use indexed wrappers that keep linear materialization
cost while still giving `O(log n)` key lookup. The detailed API reference calls
out which helpers use ordered maps and which use indexed wrappers.

See [`docs/COMPLEXITY.md`](docs/COMPLEXITY.md) for the fuller distinction
between Boost's algorithmic complexity and `nxpp`'s full public-call cost.

### 2. Multigraph APIs are still the weakest part of the public surface

For simple graphs, calls such as `has_edge(u, v)`, `get_edge_weight(u, v)`, `get_edge_attr(u, v, key)`, `G[u][v]`, and `remove_edge(u, v)` are straightforward.

For multigraphs, the current implementation still addresses edges through `(u, v)` in several places, which is not a stable public way to identify one specific parallel edge.
In particular:

- `has_edge(u, v)` means "at least one parallel edge exists"
- `remove_edge(u, v)` removes all parallel edges between `u` and `v`
- `get_edge_weight(u, v)`, `get_edge_attr(u, v, key)`, and `G[u][v]` resolve through a single edge returned by `boost::edge(u, v, g)`, so they do not identify a specific parallel edge in a stable public way
- `add_edge(..., attrs)` on multigraphs attaches attributes through that same `(u, v)` resolution path, so attribute assignment is also not yet a precise per-parallel-edge API

Treat multigraph mutation / lookup as a **known caveat**, not as a fully polished API.

For precise multigraph work, prefer the explicit `edge_id` API:

- create a specific edge with `add_edge_with_id(...)`
- enumerate parallel edges with `edge_ids(u, v)`
- inspect a specific edge with `get_edge_endpoints(edge_id)`, `get_edge_weight(edge_id)`, `get_edge_attr(edge_id, key)`
- mutate a specific edge with `set_edge_weight(edge_id, ...)`, `set_edge_attr(edge_id, ...)`, and `remove_edge(edge_id)`

### 3. `remove_node()` is intentionally not cheap

`nxpp` now supports custom `OutEdgeSelector` and `VertexSelector` values on `Graph<...>`,
but destructive updates still require wrapper-side bookkeeping.

In particular, `remove_node()` must also:

- clear incident edge metadata
- erase node metadata
- rebuild the NodeID <-> descriptor map
- rebuild the maintained vertex-index map used by wrapper algorithms

In practice, this is still an **`O(V + E)` public operation**.

---

## What `nxpp` is and what it is not

`nxpp` is best understood as:

- a **BGL-backed convenience layer**
- a **partial NetworkX-inspired API**, not full parity
- a library that prefers **materialized C++ containers** over lazy iterator-heavy public APIs
- a place for a few **C++-oriented utility wrappers** that are easier to consume than raw Boost output

It is **not**:

- a drop-in replacement for NetworkX
- a full abstraction over all BGL concepts
- a zero-cost wrapper in every public function
- a fully stabilized graph framework yet

---

## Project documents

These repository files should have clearly separated roles:

- [`README.md`](README.md): user-facing overview, quick start, caveats, and document navigation
- [`TODO.md`](TODO.md): compact local priority index; GitHub Issues are the full backlog source of truth
- [`CHANGELOG.md`](CHANGELOG.md): dated change history
- [`SESSION.md`](SESSION.md): historical development log
- [`docs/README.md`](docs/README.md): index for longer-form and future generated documentation
- [`docs/API_REFERENCE.md`](docs/API_REFERENCE.md): detailed public API tables and technical reference
- [`docs/API_ARCHITECTURE.md`](docs/API_ARCHITECTURE.md): public API placement policy for graph methods and namespace-scope helpers
- [`docs/GRAPH_CONFIGURATION.md`](docs/GRAPH_CONFIGURATION.md): supported BGL configurability surface and advanced-knob policy
- [`docs/COMPLEXITY.md`](docs/COMPLEXITY.md): complexity policy and Boost-vs-`nxpp` cost model
- [`docs/EXTERNAL_USAGE.md`](docs/EXTERNAL_USAGE.md): how to consume `nxpp` from another project today
- [`docs/TEST.md`](docs/TEST.md): testing layers, test commands, and verification scope

The external-consumption story now also includes a minimal root
[`CMakeLists.txt`](CMakeLists.txt) for vendored `add_subdirectory(...)` usage,
in addition to the plain compiler examples documented in
[`docs/EXTERNAL_USAGE.md`](docs/EXTERNAL_USAGE.md).

If these files disagree, `README.md` should describe the **current user-facing reality**, while `TODO.md` should describe what is still open.

---

## API architecture policy

The intended public shape of `nxpp` is:

- **graph methods** as the primary API for graph ownership, mutation, local lookups, traversals, algorithms, and attribute/proxy access on an existing graph
- **free functions under `nxpp::`** only where namespace-scope factories/generators are the cleaner fit

In other words:

- `G.add_edge(...)`, `G.remove_node(...)`, `G.neighbors(...)`, `G.get_edge_attr(...)`, `G.bfs_edges(...)`, and `G.dijkstra_shortest_paths(...)` belong on the graph object
- calls such as `nxpp::complete_graph(...)`, `nxpp::path_graph(...)`, and `nxpp::erdos_renyi_graph(...)` remain free functions because they construct and return graphs
- old free-function forms for existing-graph operations are deprecated and should not be treated as canonical entry points

See [`docs/API_ARCHITECTURE.md`](docs/API_ARCHITECTURE.md) for the fuller rule and future-addition placement policy.

---

## Graph configuration policy

`nxpp` intentionally exposes a narrow graph-configuration surface.

Today, the supported public knobs are:

- `NodeID`
- `EdgeWeight`
- `Directed`
- `Multi`
- `Weighted`
- `OutEdgeSelector`
- `VertexSelector`

The existing aliases such as `WeightedDiGraphInt`, `Graph`, and `MultiGraph` still use the standard `boost::vecS` / `boost::vecS` backend.
Advanced users can override selectors by instantiating `Graph<...>` directly.

Today, selector customization is broader than the default aliases, but it is still not "anything goes":

- `OutEdgeSelector` can be changed for advanced use cases such as `boost::listS` or `boost::setS`
- `VertexSelector` can also be changed for advanced use cases such as `boost::listS` or `boost::setS`
- `Multi=true` is rejected with `boost::setS` because `setS` suppresses parallel edges by design

`NodeID` now has an explicit compile-time contract too:

- it must be copy-constructible
- it must support equality comparison
- it must be orderable through `std::less`

That is the contract used by the wrapper-owned translation maps, ordered result
wrappers, and on-demand shortest-path reconstruction.

The free graph generators (`complete_graph`, `path_graph`, and
`erdos_renyi_graph`) have one extra requirement on top of that:

- `NodeID` must be constructible from `std::size_t`, because those helpers
synthesize node IDs `0..n-1`

So `Graph` remains a valid graph type, but the numeric generator
helpers are intentionally aimed at integer-like node IDs unless you provide a
custom generator path yourself.

For example:

```cpp
nxpp::WeightedDiGraphInt G_default;
nxpp::Graph G_custom;
```

See [`docs/GRAPH_CONFIGURATION.md`](docs/GRAPH_CONFIGURATION.md) for the full policy, rationale, and scoped future extension path.

---

## Current scope

The current public surface covers these areas:

1. **Graph construction and mutation**
2. **Node and edge attributes through proxies and checked accessors**
3. **Traversal wrappers**
4. **Shortest paths**
5. **Connected / strongly connected components**
6. **Topological sort and minimum spanning tree helpers**
7. **Maximum flow, minimum cut, and min-cost flow wrappers**
8. **Graph generators and extra utilities**

The most important implemented functionality currently includes:

- graph aliases for directed / undirected / multi / unweighted variants
- `add_node`, `add_edge`, `remove_edge`, `remove_node`, `clear`
- `neighbors`, `successors`, `predecessors`
- `has_*_attr`, `get_*_attr`, `try_get_*_attr`
- `bfs_edges`, `dfs_edges`, `bfs_tree`, `dfs_tree`
- `breadth_first_search`, `depth_first_search`, `bfs_visit`, `dfs_visit`
- `shortest_path`, `dijkstra_path`, `bellman_ford_path`, `dag_shortest_paths`
- `dijkstra_shortest_paths`, `bellman_ford_shortest_paths`
- `floyd_warshall_all_pairs_shortest_paths`
- `connected_components`, `connected_component_groups`
- `strongly_connected_components`, `strongly_connected_component_map`, `strongly_connected_component_roots`
- `topological_sort`, `kruskal_minimum_spanning_tree`, `prim_minimum_spanning_tree`
- `maximum_flow`, `minimum_cut`, `max_flow_min_cost`, `cycle_canceling`
- `complete_graph`, `path_graph`, `erdos_renyi_graph`
- `degree_centrality`, `two_sat_satisfiable`

The canonical umbrella header is `include/nxpp.hpp`.

The repo now also has a semantic include layout under `include/nxpp/`:

- `nxpp/graph.hpp`
- `nxpp/attributes.hpp`
- `nxpp/traversal.hpp`
- `nxpp/shortest_paths.hpp`
- `nxpp/components.hpp`
- `nxpp/spanning_tree.hpp`
- `nxpp/flow.hpp`
- `nxpp/generators.hpp`
- `nxpp/multigraph.hpp`
- `nxpp/sat.hpp`

That split is now real for the main public surface:

- `nxpp/graph.hpp` owns the core `Graph` type, its base structural operations,
and the public graph aliases
- `nxpp/attributes.hpp` owns the attribute-aware overloads and checked
attribute accessors
- `nxpp/multigraph.hpp` owns the precise `edge_id`-based multigraph API
- `nxpp/traversal.hpp` owns the traversal visitors, deprecated BFS/DFS wrappers,
and the `Graph` traversal method definitions
- `nxpp/shortest_paths.hpp` owns the shortest-path helpers, deprecated shortest-path wrappers,
and the `Graph` shortest-path method definitions
- `nxpp/components.hpp` owns the component helpers, deprecated component wrappers,
and the `Graph` component method definitions
- `nxpp/spanning_tree.hpp` owns topological sort, minimum-spanning-tree helpers,
deprecated wrappers, and the matching `Graph` method definitions
- `nxpp/flow.hpp` owns flow/cut/min-cost helpers, deprecated wrappers,
supporting result types, and the matching `Graph` method definitions
- `nxpp/generators.hpp` owns `complete_graph`, `path_graph`, and
`erdos_renyi_graph`
- `nxpp/sat.hpp` owns the 2-SAT helpers

`include/nxpp.hpp` is the real umbrella include for the modular public surface.

That means both of these forms currently work:

```cpp
#include "include/nxpp/flow.hpp"
```

```cpp
#include "include/nxpp.hpp"
```

The repository also now has a small helper script for rebuilding a distributable
single header from the umbrella rooted at `include/nxpp.hpp`:

```bash
bash scripts/build_single_header.sh
```

That command now generates a standalone `dist/nxpp.hpp` by expanding local
`nxpp` headers recursively while leaving standard-library and Boost includes
alone. The modular headers remain the source of truth; the generated file is a
distribution artifact and is intentionally not versioned in git.

The repo now keeps three aligned showcase entry points:

- `main_boost.cpp` for the raw Boost version
- `main_nxpp.cpp` for the `nxpp` wrapper version
- `main.py` as the NetworkX/Python companion to the two C++ demos

The assertion-based test suite now has a dedicated runner:

```bash
bash scripts/run_tests.sh
```

Use [`docs/TEST.md`](docs/TEST.md) as the source of truth for the testing
story. It now separates:

- showcase programs
- snippet parity / regression
- formal assertion-based tests
- single-header validation
- large-graph raw-Boost comparison

There is also an opt-in large-graph comparison runner:

```bash
bash scripts/run_large_graph_compare.sh
```

That path compiles `tests/test_large_graph_compare.cpp`, generates
deterministic larger graph instances, and compares `nxpp` against raw Boost for:

- BFS depth results
- DFS tree edges
- connected-component partitions
- strongly connected-component partitions
- Dijkstra distance maps
- Bellman-Ford distance maps
- DAG shortest-path distance maps
- reachable negative-cycle detection behavior
- post-`remove_node()` graph state on large graphs
- large multigraph mutation and cleanup behavior
- large attribute preservation and cleanup after repeated mutations
- large combined weighted-graph mutation sequences with raw-Boost comparison
- large Floyd-Warshall all-pairs agreement with a raw-Boost all-pairs baseline
- large maximum-flow / minimum-cut agreement with Boost
- large successive-shortest-path min-cost-flow agreement with Boost

It is intentionally kept outside `bash scripts/run_tests.sh` so the default
formal suite stays fast while the larger comparison path remains available when
you want extra confidence on wrapper behavior at scale.

The large-graph driver now also re-runs representative BFS, connected-component,
strongly-connected-component, Dijkstra, and `remove_node()` comparisons across
multiple fixed seeds instead of relying on one seed per scenario, and it
includes a non-default `boost::listS` / `boost::listS` selector regression so
the comparison path exercises more of the supported wrapper configuration
surface than the default aliases alone.

and a dedicated GitHub Actions workflow:

- `.github/workflows/tests.yml`

There is also a dedicated workflow for the generated standalone header:

- `.github/workflows/single-header.yml`
- `.github/workflows/large-graph-compare.yml`
- `.github/workflows/release.yml`

The formal-test workflow is intentionally narrow: it installs Boost, runs the
formal test suite, and publishes the test output as a Markdown job summary.

The standalone-header workflow is equally narrow in scope:

- runs `bash scripts/build_single_header.sh`
- smoke-tests the generated `dist/nxpp.hpp`
- uploads the generated header as a workflow artifact

The large-graph comparison workflow is dedicated to the new scale-oriented
verification path:

- runs `bash scripts/run_large_graph_compare.sh` against the modular headers
- builds `dist/nxpp.hpp`
- reruns the same large-graph comparison against the generated single header
- publishes both outputs as a separate Markdown job summary

The release workflow is now fully automated from pushes to `main`:

- reads the top version from `RELEASE_NOTES.md` and `CHANGELOG.md`
- fails if those top versions do not match
- skips publication if the matching `vX.Y.Z` tag already exists
- builds `dist/nxpp.hpp`
- runs `bash scripts/run_tests.sh`
- runs `bash scripts/run_single_header_tests.sh` against `dist/nxpp.hpp`
- creates and pushes the matching `vX.Y.Z` tag only after the release checks pass
- creates the GitHub release from `RELEASE_NOTES.md`
- uploads the tested file as `nxpp.hpp`

`workflow_dispatch` remains available as a manual fallback, but it follows the
same self-contained path: read the top version, skip if the tag already exists,
otherwise build, test, tag, and publish in the same run.

This means the top version in `RELEASE_NOTES.md` and `CHANGELOG.md` should now
be treated as the concrete next release that the release workflow will publish
on the next push to `main`.

These are showcase demos, not formal tests or parity harnesses.

The `*_nxpp.cpp` snippets now also follow the semantic-header split instead of
pulling in the full umbrella header by default. For example:

- traversal snippets include `nxpp/traversal.hpp`
- shortest-path snippets include `nxpp/shortest_paths.hpp`
- component snippets include `nxpp/components.hpp`
- flow snippets include `nxpp/flow.hpp`
- the topological-sort snippet includes `nxpp/topological_sort.hpp`

This keeps the snippet layer closer to the real modular include story and makes
compile-time comparisons more meaningful.

The intended split is:

- `main_*` files are showcase demos
- `snippet/` is the curated example-plus-parity layer
- `tests/` is the formal regression suite
- `test_large_graph_compare.cpp` is the separate large deterministic raw-Boost comparison path

There is now also a minimal assertion-based test entry point:

- `tests/test_core.cpp`
- `tests/test_attributes.cpp`
- `tests/test_edge_cases.cpp`
- `tests/test_flow.cpp`
- `tests/test_remove_node.cpp`
- `tests/test_multigraph.cpp`
- `tests/test_large_graph_compare.cpp`
- `scripts/run_tests.sh`
- `scripts/run_large_graph_compare.sh`

Where it stays readable, the test files now mirror the semantic-header split too:

- `tests/test_attributes.cpp` includes `nxpp/attributes.hpp`
- `tests/test_multigraph.cpp` includes `nxpp/multigraph.hpp`
- `tests/test_flow.cpp` includes `nxpp/flow.hpp`
- `tests/test_edge_cases.cpp` includes only the graph/traversal/shortest-path/components headers it actually exercises

That test binary is intentionally small for now, but it gives the project a real
formal test path in addition to snippets, showcases, and benchmarks.

The large-graph comparison path is intentionally separate:

- it reuses deterministic generated graphs instead of committed fixtures
- it compares `nxpp` against raw Boost implementations directly
- it is meant as scale-oriented verification, not as a benchmark claim
- it stays opt-in so the normal test path remains quick to run locally and in CI

For benchmark runs that should produce both CSV data and a ready-to-read report,
you can use:

```bash
python3 scripts/run_benchmark_report.py --all --iterations 3 --jobs "$(nproc)"
```

That command:

- runs the serial benchmark first and the parallel benchmark second
- moves any previous benchmark CSVs, `benchmark/BENCHMARK.md`, and `benchmark/imgs/` into `backups/benchmark//`
- generates exactly two timestamped CSV files: one serial and one parallel
- writes the report to `benchmark/BENCHMARK.md` and the plots to `benchmark/imgs/`

The multigraph-specific part of the suite now checks the exact behavior that the
recent critical issue work stabilized:

- distinct `edge_id` values for parallel edges
- precise single-edge removal with `remove_edge(edge_id)`
- all-edge removal with `remove_edge(u, v)`
- per-edge attribute separation across parallel edges
- consistent endpoint lookup after partial multigraph removal

The attribute-specific part of the suite now also checks failure behavior:

- missing node/edge attribute lookups that should throw
- type mismatches that should throw
- `try_get_*_attr(...)` returning empty on missing keys or wrong types
- numeric edge-attribute lookup rejecting non-numeric stored values

The edge-case part of the suite now checks common boundary conditions:

- empty graphs returning empty collections and component views
- singleton graphs producing empty neighbor/traversal results
- missing-node operations throwing in a defined way
- disconnected shortest paths preserving unreachable distances and missing paths
- disconnected component grouping staying stable across separate subgraphs

The flow-specific part of the suite now checks the wrapper layer around the
small reference flow networks already used in the snippets:

- `maximum_flow(...)`
- `minimum_cut(...)`
- `push_relabel_maximum_flow(...)`
- `cycle_canceling(...)`
- `successive_shortest_path_nonnegative_weights(...)`
- `max_flow_min_cost(...)` and `max_flow_min_cost_successive_shortest_path(...)`

The `remove_node()` regression part of the suite now checks the descriptor-remap
area directly:

- node and edge views after removing a middle vertex
- cleanup of node and incident edge attributes
- traversal and shortest-path behavior after internal remapping
- component views after node removal
- multigraph cleanup of incident `edge_id` state

String attributes passed as `"..."` are normalized internally, so typed reads like
`get_node_attr(...)` and `get_edge_attr(...)` work without
having to spell `std::string(...)` at the call site.

---

## Build and requirements

`nxpp` is a modular library in the repository, and releases also ship a generated
single-header distribution asset. It depends on **Boost Graph Library**.

### Minimal support matrix

This matrix is intentionally conservative and reflects what the repository
currently verifies directly:

| Area | Current status |
|---|---|
| Language level | C++20 required |
| Standard library / toolchain used in repo scripts | local scripts are still primarily `g++`-oriented |
| Continuous integration | GitHub Actions on `ubuntu-latest` and `macos-latest` |
| Boost dependency | `libboost-graph-dev` on Ubuntu CI |
| Standalone single header | Generated in CI and tested on Ubuntu |
| macOS | Covered by the formal test workflow with Homebrew Boost |
| Windows | Expected to work with vcpkg Boost, but not covered by CI yet |
| Clang | Covered by the formal test workflow on Ubuntu and macOS |

In practice, the repository now has a stronger verified story on Linux and
macOS, with both `g++` and `clang++` covered in the formal test workflow, while
Windows still remains an expected but not yet CI-backed path.

### Minimal local build

```bash
g++ -std=c++20 -Wall -Wextra -pedantic -O3 main_boost.cpp -o main_boost
g++ -std=c++20 -Wall -Wextra -pedantic -O3 main_nxpp.cpp -o main_nxpp
python3 main.py
bash scripts/run_tests.sh
```

### Install Boost Graph Library

**Ubuntu / Debian**

```bash
sudo apt-get install libboost-graph-dev
```

**macOS (Homebrew)**

```bash
brew install boost
```

**Windows (vcpkg)**

```bash
vcpkg install boost-graph
```

The header performs a compile-time include check and fails early if BGL headers are missing.

### External usage

Today there are two supported ways to consume `nxpp` from another project:

1. the modular header tree under `include/`
2. the tested single-header release asset `nxpp.hpp`

For the modular layout, add the repo's `include/` directory to your compiler
include path and write includes like:

```cpp
#include
```

or, when you only need one area:

```cpp
#include
```

Minimal modular compile example:

```bash
g++ -std=c++20 -I/path/to/nxpp/include app.cpp -o app
```

For the single-header path, use the tested `nxpp.hpp` asset attached to a GitHub
release, place it in a vendor/include directory, and compile with that
directory on your include path:

```cpp
#include
```

```bash
g++ -std=c++20 -I/path/to/vendor/include app.cpp -o app
```

In both cases, `nxpp` is still header-only and still depends on **Boost Graph
Library** headers being available to the compiler.

See [`docs/EXTERNAL_USAGE.md`](docs/EXTERNAL_USAGE.md) for the fuller external
consumer story, including the difference between the repo-local generated
`dist/nxpp.hpp` artifact and the tested release asset.

---

## Quick start

The old graph-weights / direct-path style example is no longer representative enough.
A more up-to-date "small but real" example is:

```cpp
#include "include/nxpp.hpp"
#include
#include

int main() {
auto G = nxpp::DiGraph();

G.add_edge("Milan", "Rome", 5.0, {
{"capacity", 8},
{"company", "Trenitalia"}
});
G.add_edge("Rome", "Naples", 2.5);
G.add_edge("Milan", "Florence", 2.0);
G.add_edge("Florence", "Naples", 4.0);

G.node("Rome")["population"] = 2800000;

auto routes = G.dijkstra_shortest_paths("Milan");
auto dist_to_naples = routes.distance.at("Naples");
auto path_to_naples = routes.path_to("Naples");

auto operator_name =
G.get_edge_attr("Milan", "Rome", "company");

std::cout << "Distance Milan -> Naples: " << dist_to_naples << "\n";
std::cout << "Rail operator on Milan -> Rome: " << operator_name << "\n";
std::cout << "Path:";
for (const auto& node : path_to_naples) {
std::cout << " " << node;
}
std::cout << "\n";
}
```

This shows the three main ideas of the library:

- graph mutation through a small wrapper API
- checked node / edge attribute access
- richer result wrappers such as `dijkstra_shortest_paths()` with on-demand path reconstruction and indexed component views

---

## Internal model in one minute

`nxpp` keeps these synchronized structures:

- `id_to_bgl`: `NodeID -> vertex_descriptor`
- `bgl_to_id`: index-ordered `NodeID` storage used for normalized wrapper results
- a maintained wrapper index property on each vertex, exposed as `vertex_index_map` for BGL algorithms

This lets the public API use `std::string`, `int`, or other orderable node IDs while running algorithms on a normal BGL graph internally.

### Consequence

The backend now uses:

```cpp
boost::adjacency_list
```

The default aliases still resolve to `boost::vecS` / `boost::vecS`,
but direct `Graph<...>` instantiation can now use non-default selectors.

That flexibility means `nxpp` cannot rely on descriptor-as-index assumptions anymore,
so the wrapper maintains its own vertex index normalization layer.
That is why some public-call costs differ from the textbook complexity of the wrapped algorithm alone.

---

## Further documentation

The root README is intentionally kept focused on overview, quick start, caveats, and repository navigation.

For deeper technical detail, use:

- [`docs/API_REFERENCE.md`](docs/API_REFERENCE.md): detailed API tables, aliases, proxy syntax, result wrappers, and algorithm reference
- [`docs/API_ARCHITECTURE.md`](docs/API_ARCHITECTURE.md): policy for graph methods vs namespace-scope helpers
- [`docs/GRAPH_CONFIGURATION.md`](docs/GRAPH_CONFIGURATION.md): graph selector/configuration policy
- [`docs/README.md`](docs/README.md): docs index

The most important topics moved out of the README are:

- public API tables and complexity notes
- alias/reference detail
- traversal / shortest-path / flow reference tables
- proxy syntax detail
- result-wrapper reference
- repository layout / parity harness detail
- deeper caveat and trade-off sections

---

## What is still clearly open

The most important open work visible from the repository today is:

- a real assertion-based test suite
- stronger edge-case coverage
- packaging / install story beyond manual header inclusion
- clearer documentation generation and source-of-truth discipline
- a minimal compiler/platform support matrix in the README
- eventual API stabilization

See [`TODO.md`](TODO.md) for the open list.

---

## License

This project is licensed under the MIT License. See [`LICENSE`](LICENSE).