π§ Work in progress! π§
NetGraph is a tool for network modeling and analysis. It consists of two main parts:
- A lower level library providing graph data structures and algorithms for network modeling and analysis.
- A set of higher level abstractions like network and workflow that can comprise a complete network analysis scenario.
The lower level lib provides the following main primitives:
-
StrictMultiDiGraph
Specialized multi-digraph with addressable edges and strict checks on duplicate nodes/edges. -
Path
Represents a single path between two nodes in the graph. -
PathBundle
A collection of equal-cost paths between two nodes. -
Demand
Models a network demand from a source node to a destination node with a specified traffic volume. -
Flow
Represent placement of a Demand volume along one or more paths (via a PathBundle) in a graph. -
FlowPolicy
Governs how Demands are split into Flows, enforcing routing/TE constraints (e.g., shortest paths, multipath, capacity limits).
NetGraph can be used in two ways:
Prerequisites:
- Docker installed on your machine.
Steps:
-
Clone the repository:
git clone https://github.com/networmix/NetGraph
-
Build the Docker image:
cd NetGraph ./run.sh build
-
Start the container with JupyterLab server:
./run.sh start
-
Open the JupyterLab URL in your browser:
http://127.0.0.1:8788/
-
Jupyter will show the content of
notebooks
directory and you can start using the provided notebooks or create your own.
Note: Docker will mount the content of NetGraph
directory into the /root/env
directory inside container, so any changes made to the code in the NetGraph
directory will be reflected in the container and vice versa.
The ngraph
package is installed in the container in editable mode, so you can make changes to the code and see the changes reflected immediately in JupyterLab.
To exit the JupyterLab server, press Ctrl+C
in the terminal where the server is running. To stop the remaining Docker container, run:
./run.sh stop
Prerequisites:
- Python 3.8 or higher installed on your machine.
Note: Don't forget to use a virtual environment (e.g., venv
) to avoid conflicts with other Python packages. See Python Virtual Environments for more information.
Steps:
-
Install the package using pip:
pip install ngraph
-
Use the package in your Python code:
from ngraph.lib.graph import StrictMultiDiGraph from ngraph.lib.algorithms.max_flow import calc_max_flow # Create a graph g = StrictMultiDiGraph() g.add_node("A") g.add_node("B") g.add_node("C") g.add_edge("A", "B", cost=1, capacity=1) g.add_edge("A", "B", cost=1, capacity=1) g.add_edge("B", "C", cost=1, capacity=2) g.add_edge("A", "C", cost=2, capacity=3) # Calculate MaxFlow between the source and destination nodes max_flow = calc_max_flow(g, "A", "C") print(max_flow)
"""
Tests max flow calculations on a graph with parallel edges.
Graph topology (costs/capacities):
[1,1] & [1,2] [1,1] & [1,2]
A βββββββββββββββββββΊ B ββββββββββββββΊ C
β β²
β [2,3] β [2,3]
βββββββββββββββββββββΊ D ββββββββββββββββ
Edges:
- AβB: two parallel edges with (cost=1, capacity=1) and (cost=1, capacity=2)
- BβC: two parallel edges with (cost=1, capacity=1) and (cost=1, capacity=2)
- AβD: (cost=2, capacity=3)
- DβC: (cost=2, capacity=3)
The test computes:
- The true maximum flow (expected flow: 6.0)
- The flow along the shortest paths (expected flow: 3.0)
- Flow placement using an equal-balanced strategy on the shortest paths (expected flow: 2.0)
"""
from ngraph.lib.graph import StrictMultiDiGraph
from ngraph.lib.algorithms.max_flow import calc_max_flow
from ngraph.lib.algorithms.base import FlowPlacement
g = StrictMultiDiGraph()
for node in ("A", "B", "C", "D"):
g.add_node(node)
# Create parallel edges between AβB and BβC
g.add_edge("A", "B", key=0, cost=1, capacity=1)
g.add_edge("A", "B", key=1, cost=1, capacity=2)
g.add_edge("B", "C", key=2, cost=1, capacity=1)
g.add_edge("B", "C", key=3, cost=1, capacity=2)
# Create an alternative path AβDβC
g.add_edge("A", "D", key=4, cost=2, capacity=3)
g.add_edge("D", "C", key=5, cost=2, capacity=3)
# 1. The true maximum flow
max_flow_prop = calc_max_flow(g, "A", "C")
assert max_flow_prop == 6.0, f"Expected 6.0, got {max_flow_prop}"
# 2. The flow along the shortest paths
max_flow_sp = calc_max_flow(g, "A", "C", shortest_path=True)
assert max_flow_sp == 3.0, f"Expected 3.0, got {max_flow_sp}"
# 3. Flow placement using an equal-balanced strategy on the shortest paths
max_flow_eq = calc_max_flow(
g, "A", "C", shortest_path=True, flow_placement=FlowPlacement.EQUAL_BALANCED
)
assert max_flow_eq == 2.0, f"Expected 2.0, got {max_flow_eq}"
"""
Demonstrates traffic engineering by placing two demands on a network.
Graph topology (costs/capacities):
[15]
A βββββββ B
\ /
[5] \ / [15]
\ /
C
- Each link is bidirectional:
AβB: capacity 15, BβC: capacity 15, and AβC: capacity 5.
- We place a demand of volume 20 from AβC and a second demand of volume 20 from CβA.
- Each demand uses its own FlowPolicy, so the policy's global flow accounting does not overlap.
- The test verifies that each demand is fully placed at 20 units.
"""
from ngraph.lib.graph import StrictMultiDiGraph
from ngraph.lib.algorithms.flow_init import init_flow_graph
from ngraph.lib.flow_policy import FlowPolicyConfig, get_flow_policy
from ngraph.lib.demand import Demand
# Build the graph.
g = StrictMultiDiGraph()
for node in ("A", "B", "C"):
g.add_node(node)
# Create bidirectional edges with distinct labels (for clarity).
g.add_edge("A", "B", key=0, cost=1, capacity=15, label="1")
g.add_edge("B", "A", key=1, cost=1, capacity=15, label="1")
g.add_edge("B", "C", key=2, cost=1, capacity=15, label="2")
g.add_edge("C", "B", key=3, cost=1, capacity=15, label="2")
g.add_edge("A", "C", key=4, cost=1, capacity=5, label="3")
g.add_edge("C", "A", key=5, cost=1, capacity=5, label="3")
# Initialize flow-related structures (e.g., to track placed flows in the graph).
flow_graph = init_flow_graph(g)
# Demand from AβC (volume 20).
demand_ac = Demand("A", "C", 20)
flow_policy_ac = get_flow_policy(FlowPolicyConfig.TE_UCMP_UNLIM)
demand_ac.place(flow_graph, flow_policy_ac)
assert demand_ac.placed_demand == 20, (
f"Demand from {demand_ac.src_node} to {demand_ac.dst_node} "
f"expected to be fully placed."
)
# Demand from CβA (volume 20), using a separate FlowPolicy instance.
demand_ca = Demand("C", "A", 20)
flow_policy_ca = get_flow_policy(FlowPolicyConfig.TE_UCMP_UNLIM)
demand_ca.place(flow_graph, flow_policy_ca)
assert demand_ca.placed_demand == 20, (
f"Demand from {demand_ca.src_node} to {demand_ca.dst_node} "
f"expected to be fully placed."
)