Skip to content

Commit f855971

Browse files
committed
fixing ubuntu build, updated docs
1 parent 3305b9e commit f855971

8 files changed

Lines changed: 106 additions & 51 deletions

File tree

.github/workflows/release.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ jobs:
3434
# macOS: Build Universal2 wheels on Intel runner (macos-13)
3535
CIBW_ARCHS_MACOS: "universal2"
3636
CIBW_TEST_COMMAND: "python -c \"import netgraph_core; print(netgraph_core.__version__)\""
37+
CIBW_BEFORE_ALL_LINUX: "yum install -y libatomic || apt-get update && apt-get install -y libatomic1 || apk add libatomic"
3738

3839
- uses: actions/upload-artifact@v4
3940
with:

CHANGELOG.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [0.1.0] - 2025-11-23
9+
10+
### Added
11+
12+
- **Core Library**: Initial release of C++ implementation for graph algorithms and flow tracking.
13+
- **Graph Structures**:
14+
- `StrictMultiDiGraph`: Immutable directed multigraph using CSR (Compressed Sparse Row) adjacency.
15+
- `FlowGraph`: Manages flow state, per-flow edge allocations, and residual capacities.
16+
- **Algorithms**:
17+
- Shortest paths (Dijkstra variant returning a DAG for ECMP; supports node/edge masking and residual-aware tie-breaking).
18+
- K-Shortest paths (Yen's algorithm).
19+
- Max-flow (Successive Shortest Path with ECMP/WCMP placement; supports capacity-aware (TE) and cost-only (IP) routing modes).
20+
- Sensitivity analysis (identifies bottlenecks).
21+
- **Flow Policy**:
22+
- **Modeling**: Unified configuration for IP routing (cost-based ECMP) and Traffic Engineering (capacity-aware TE).
23+
- **Placement**: `Proportional` (WCMP) and `EqualBalanced` (ECMP) strategies.
24+
- **Lifecycle**: Manages demand placement, static/dynamic path selection, and re-optimization.
25+
- **Constraints**: Enforces limits on path cost, stretch factor, and flow counts.
26+
- **Python Bindings**:
27+
- Python 3.9+ support via pybind11.
28+
- NumPy integration using zero-copy views where applicable.
29+
- Releases GIL during long-running graph algorithms.
30+
- **Testing**:
31+
- Python and C++ test suites.

README.md

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,56 @@
11
# NetGraph-Core
22

3-
C++ implementation of graph algorithms for network flow analysis and traffic engineering with Python bindings.
3+
C++ graph engine for network flow analysis, traffic engineering simulation, and capacity planning.
44

5-
## Features
5+
## Overview
66

7-
- **Algorithms:** Shortest paths (Dijkstra), K-shortest paths (Yen), max-flow (successive shortest paths)
8-
- **Graph representation:** Immutable directed multigraph with CSR adjacency
9-
- **Flow placement:** Tunable policies (proportional to capacity, equal-balanced across paths)
10-
- **Python bindings:** NumPy integration, GIL released during computation
11-
- **Deterministic:** Reproducible edge ordering by (cost, src, dst)
7+
NetGraph-Core provides a specialized graph implementation for networking problems. Key design priorities:
8+
9+
- **Determinism**: Guaranteed reproducible edge ordering by (cost, src, dst).
10+
- **Flow Modeling**: Native support for multi-commodity flow state, residual tracking, and ECMP/WCMP placement.
11+
- **Performance**: Immutable CSR (Compressed Sparse Row) adjacency and zero-copy NumPy views.
12+
13+
## Core Features
14+
15+
### 1. Graph Representations
16+
17+
- **`StrictMultiDiGraph`**: Immutable directed multigraph using CSR adjacency. Supports parallel edges (multi-graph), essential for network topologies.
18+
- **`FlowGraph`**: Topology overlay managing mutable flow state, per-flow edge allocations, and residual capacities.
19+
20+
### 2. Network Algorithms
21+
22+
- **Shortest Paths (SPF)**:
23+
- Modified Dijkstra returns a **Predecessor DAG** to capture all equal-cost paths.
24+
- Supports **ECMP** (Equal-Cost Multi-Path) routing.
25+
- Features **node/edge masking** and **residual-aware tie-breaking**.
26+
27+
- **K-Shortest Paths (KSP)**:
28+
- Yen's algorithm returning DAG-wrapped paths.
29+
- Configurable constraints on cost factors (e.g., paths within 1.5x of optimal).
30+
31+
- **Max-Flow**:
32+
- **Algorithm**: Iterative augmentation using Successive Shortest Path on residual graphs, pushing flow across full ECMP/WCMP DAGs at each step.
33+
- **Traffic Engineering (TE) Mode**: Routing adapts to residual capacity (progressive fill).
34+
- **IP Routing Mode**: Cost-only routing (ECMP/WCMP) ignoring capacity constraints.
35+
36+
- **Analysis**:
37+
- **Sensitivity Analysis**: Identifies bottleneck edges where capacity relaxation increases total flow.
38+
- **Min-Cut**: Computes minimum cuts on residual graphs.
39+
40+
### 3. Flow Policy Engine
41+
42+
Unified configuration object (`FlowPolicy`) that models diverse routing behaviors:
43+
44+
- **Modeling**: Unified configuration for **IP Routing** (static costs) and **Traffic Engineering** (dynamic residuals).
45+
- **Placement Strategies**:
46+
- `EqualBalanced`: **ECMP** (equal splitting) - equal distribution across next-hops and parallel edges.
47+
- `Proportional`: **WCMP** (weighted splitting) - distribution proportional to residual capacity.
48+
- **Lifecycle Management**: Handles demand placement, re-optimization of existing flows, and constraints (path cost, stretch factor, flow counts).
49+
50+
### 4. Python Integration
51+
52+
- **Zero-Copy**: Exposes C++ internal buffers to Python as read-only NumPy arrays (float64/int64).
53+
- **Concurrency**: Releases the Python GIL during graph algorithms to enable threading.
1254

1355
## Installation
1456

include/netgraph/core/flow_graph.hpp

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* FlowGraph manages per-flow edge deltas over FlowState. */
1+
/* FlowGraph manages per-flow edge allocations over FlowState. */
22
#pragma once
33

44
#include <cstdint>
@@ -14,7 +14,7 @@
1414

1515
namespace netgraph::core {
1616

17-
// FlowGraph manages per-flow edge deltas over a StrictMultiDiGraph.
17+
// FlowGraph manages per-flow edge allocations over a StrictMultiDiGraph.
1818
// Composes FlowState for residual/aggregate edge flow management.
1919
class FlowGraph {
2020
public:
@@ -29,12 +29,12 @@ class FlowGraph {
2929
// Access underlying graph (const)
3030
[[nodiscard]] const StrictMultiDiGraph& graph() const noexcept { return *g_; }
3131

32-
// Apply placement and record per-edge deltas for this flow. Returns placed amount.
32+
// Apply placement and record per-edge allocations for this flow. Returns placed amount.
3333
[[nodiscard]] Flow place(const FlowIndex& idx, NodeId src, NodeId dst,
3434
const PredDAG& dag, Flow amount,
3535
FlowPlacement placement);
3636

37-
// Remove a specific flow, reverting its edge deltas from the ledger.
37+
// Remove a specific flow, reverting its edge allocations from the ledger.
3838
void remove(const FlowIndex& idx);
3939

4040
// Remove all flows belonging to a given flowClass.
@@ -55,9 +55,6 @@ class FlowGraph {
5555
FlowState fs_;
5656
// Per-flow ledger: stores only edges with non-zero flow
5757
std::unordered_map<FlowIndex, std::vector<std::pair<EdgeId, Flow>>, FlowIndexHash> ledger_;
58-
// Cached EdgeId -> (src,dst) maps for faster path reconstruction
59-
std::vector<NodeId> src_of_;
60-
std::vector<NodeId> dst_of_;
6158
};
6259

6360
} // namespace netgraph::core

include/netgraph/core/flow_state.hpp

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ class FlowState {
5555
const PredDAG& dag,
5656
Flow requested_flow,
5757
FlowPlacement placement,
58-
// Optional trace collector to record per-edge deltas applied by this call
58+
// Optional trace collector to record per-edge allocations applied by this call
5959
std::vector<std::pair<EdgeId, Flow>>* trace = nullptr);
6060

6161
// Convenience: run repeated placements until exhaustion (or single tier when
@@ -76,16 +76,17 @@ class FlowState {
7676
std::span<const bool> node_mask = {},
7777
std::span<const bool> edge_mask = {});
7878

79-
// Compute min-cut with respect to current residual state, starting reachability
80-
// from source s on the residual graph (forward arcs: residual>MIN; reverse arcs:
81-
// positive flow). Honors optional masks.
79+
// Compute min-cut (edges crossing from reachable set S to unreachable set T)
80+
// based on reachability in the current residual graph.
81+
// Reachability starts from source s; forward arcs allowed if residual > kMinCap,
82+
// reverse arcs allowed if flow > kMinFlow.
8283
[[nodiscard]] MinCut compute_min_cut(NodeId src,
8384
std::span<const bool> node_mask = {},
8485
std::span<const bool> edge_mask = {}) const;
8586

86-
// Apply or revert a set of edge flow deltas directly.
87+
// Apply or revert a set of edge flow allocations directly.
8788
// When add==true, treats each (eid, flow) as additional placed flow on the edge.
88-
// When add==false, removes placed flow (reverts deltas), clamping to [0, capacity].
89+
// When add==false, removes placed flow (reverts allocations), clamping to [0, capacity].
8990
void apply_deltas(std::span<const std::pair<EdgeId, Flow>> deltas, bool add) noexcept;
9091

9192
private:

pyproject.toml

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
11
[build-system]
2-
requires = [
3-
"scikit-build-core>=0.10",
4-
"pybind11>=3",
5-
"numpy>=1.22",
6-
"cmake>=3.23",
7-
"ninja",
8-
]
2+
requires = ["scikit-build-core>=0.10", "pybind11>=3", "numpy>=1.22"]
93
build-backend = "scikit_build_core.build"
104

115
[project]
@@ -36,12 +30,6 @@ classifiers = [
3630
]
3731
dependencies = ["numpy>=1.22"]
3832

39-
[tool.setuptools]
40-
packages = []
41-
42-
[tool.setuptools.package-data]
43-
"netgraph_core" = ["py.typed"]
44-
4533
[project.optional-dependencies]
4634
dev = [
4735
"pytest>=8",
@@ -61,8 +49,7 @@ Homepage = "https://github.com/networmix/NetGraph-Core"
6149
[tool.scikit-build]
6250
wheel.expand-macos-universal-tags = true
6351
build-dir = "build/{wheel_tag}"
64-
cmake.minimum-version = "3.23"
65-
52+
cmake.version = ">=3.23"
6653
# Include Python sources from the "python/" directory
6754
wheel.packages = ["python/netgraph_core"]
6855

@@ -71,9 +58,7 @@ wheel.packages = ["python/netgraph_core"]
7158

7259
[tool.pytest.ini_options]
7360
addopts = "-q"
74-
markers = [
75-
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
76-
]
61+
markers = ["slow: marks tests as slow (deselect with '-m \"not slow\"')"]
7762

7863
[tool.ruff]
7964
line-length = 88

src/flow_graph.cpp

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
FlowGraph — authoritative flow ledger layered over FlowState.
33
4-
Tracks per-flow edge deltas to support exact removal and path inspection,
4+
Tracks per-flow edge allocations to support exact removal and path inspection,
55
while delegating residual and aggregate flow management to FlowState.
66
*/
77
#include "netgraph/core/flow_graph.hpp"
@@ -13,11 +13,6 @@ namespace netgraph::core {
1313

1414
FlowGraph::FlowGraph(const StrictMultiDiGraph& g)
1515
: g_(&g), fs_(g) {
16-
// Precompute EdgeId -> (src,dst) directly from edge views
17-
auto sv = g_->edge_src_view();
18-
auto dv = g_->edge_dst_view();
19-
src_of_.assign(sv.begin(), sv.end());
20-
dst_of_.assign(dv.begin(), dv.end());
2116
}
2217

2318
Flow FlowGraph::place(const FlowIndex& idx, NodeId src, NodeId dst,
@@ -31,7 +26,7 @@ Flow FlowGraph::place(const FlowIndex& idx, NodeId src, NodeId dst,
3126
auto& bucket = ledger_[idx];
3227

3328
// Delegate placement to FlowState, which returns the actual placed flow and
34-
// populates bucket with per-edge deltas (EdgeId, Flow) pairs.
29+
// populates bucket with per-edge allocations (EdgeId, Flow) pairs.
3530
Flow placed = fs_.place_on_dag(src, dst, dag, amount, placement, &bucket);
3631

3732
// Coalesce and filter: merge duplicate EdgeIds. Keep any positive totals
@@ -63,7 +58,7 @@ void FlowGraph::remove(const FlowIndex& idx) {
6358
auto it = ledger_.find(idx);
6459
if (it == ledger_.end()) return; // flow not found
6560
const auto& deltas = it->second;
66-
// Revert this flow's deltas from the FlowState by subtracting them.
61+
// Revert this flow's allocations from the FlowState by subtracting them.
6762
if (!deltas.empty()) {
6863
fs_.apply_deltas(deltas, /*add=*/false); // false = subtract
6964
}
@@ -93,16 +88,16 @@ std::vector<EdgeId> FlowGraph::get_flow_path(const FlowIndex& idx) const {
9388
if (it == ledger_.end()) return {};
9489
const auto& deltas = it->second;
9590

96-
// Reconstruct a simple path from the flow's edge deltas.
91+
// Reconstruct a simple path from the flow's edge allocations.
9792
// This only succeeds if the flow forms a single path (not a DAG).
9893

9994
// Step 1: Build adjacency map from edges with positive flow.
10095
std::unordered_map<NodeId, std::vector<std::pair<NodeId, EdgeId>>> adj;
101-
// Build adjacency list from deltas using cached src/dst.
96+
// Build adjacency list from allocations using cached src/dst.
10297
for (auto const& pr : deltas) {
10398
if (pr.second < kMinFlow) continue;
10499
auto eid = static_cast<std::size_t>(pr.first);
105-
NodeId u = src_of_[eid]; NodeId v = dst_of_[eid];
100+
NodeId u = g_->edge_src_view()[eid]; NodeId v = g_->edge_dst_view()[eid];
106101
adj[u].emplace_back(v, pr.first);
107102
}
108103

src/max_flow.cpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
Uses FlowState to track residuals and place flow along SPF predecessor DAGs.
55
Accumulates total flow, optional per-edge flows, cost distribution, and
66
derives a min-cut from the final residual graph.
7+
8+
Algorithm: Iterative augmentation using Successive Shortest Path on residual graphs,
9+
pushing flow across full ECMP/WCMP DAGs at each step.
710
*/
811
#include "netgraph/core/max_flow.hpp"
912
#include "netgraph/core/shortest_paths.hpp"
@@ -104,8 +107,8 @@ calc_max_flow(const StrictMultiDiGraph& g, NodeId src, NodeId dst,
104107
summary.flows.reserve(cost_dist.size());
105108
for (auto const& pr : cost_dist) { summary.costs.push_back(pr.first); summary.flows.push_back(pr.second); }
106109
}
107-
// Min-cut extraction (proportional/equal-balanced): compute reachability in final residual graph
108-
// Residual graph has forward residual = residual[e], reverse residual = flow[e] (capacity - residual)
110+
// Min-Cut: Set of edges crossing from the reachable set (S) to the unreachable set (T).
111+
// Computed by finding all nodes reachable from src in the residual graph.
109112
if (total >= kMinFlow) {
110113
auto mc = fs.compute_min_cut(src, node_mask, edge_mask);
111114
summary.min_cut = mc;

0 commit comments

Comments
 (0)