AnnNet Demo¶
In [26]:
Copied!
# ## 1. Setup & Imports
import sys
import os
sys.path.insert(0, os.path.abspath(".."))
from annnet.core.graph import AnnNet
# ## 1. Setup & Imports
import sys
import os
sys.path.insert(0, os.path.abspath(".."))
from annnet.core.graph import AnnNet
In [2]:
Copied!
# ## 2. Basic AnnNet Construction
# Create a directed graph with pre-allocated capacity
G = AnnNet(directed=True, n=100, e=200, description = "a demo graph to show AnnNet's API and features")
# Add vertices with attributes
G.add_vertex("alice", age=30, role="engineer")
G.add_vertex("bob", age=25, role="designer")
G.add_vertex("carol", age=35, role="manager")
G.add_vertex("dave", age=28, role="engineer")
# Add edges with weights and attributes
G.add_edge("alice", "bob", weight=1.0, relation="colleague")
G.add_edge("bob", "carol", weight=2.0, relation="reports_to")
G.add_edge("carol", "dave", weight=1.5, relation="manages")
G.add_edge("alice", "carol", weight=0.8, relation="collaborates")
print(f"Vertices: {G.number_of_vertices()}")
print(f"Edges: {G.number_of_edges()}")
print(f"Vertex list: {G.vertices()}")
print(f"Edge list: {G.edges()}")
# ## 2. Basic AnnNet Construction
# Create a directed graph with pre-allocated capacity
G = AnnNet(directed=True, n=100, e=200, description = "a demo graph to show AnnNet's API and features")
# Add vertices with attributes
G.add_vertex("alice", age=30, role="engineer")
G.add_vertex("bob", age=25, role="designer")
G.add_vertex("carol", age=35, role="manager")
G.add_vertex("dave", age=28, role="engineer")
# Add edges with weights and attributes
G.add_edge("alice", "bob", weight=1.0, relation="colleague")
G.add_edge("bob", "carol", weight=2.0, relation="reports_to")
G.add_edge("carol", "dave", weight=1.5, relation="manages")
G.add_edge("alice", "carol", weight=0.8, relation="collaborates")
print(f"Vertices: {G.number_of_vertices()}")
print(f"Edges: {G.number_of_edges()}")
print(f"Vertex list: {G.vertices()}")
print(f"Edge list: {G.edges()}")
Vertices: 4 Edges: 4 Vertex list: ['alice', 'bob', 'carol', 'dave'] Edge list: ['edge_0', 'edge_1', 'edge_2', 'edge_3']
In [3]:
Copied!
# ## 3. Bulk Operations (Fast Path)
# Bulk vertex insertion - much faster than looping
vertices_bulk = [
("user_1", {"score": 10, "active": True}),
("user_2", {"score": 20, "active": False}),
("user_3", {"score": 15, "active": True}),
]
G.add_vertices_bulk(vertices_bulk)
# Bulk edge insertion
edges_bulk = [
{"source": "user_1", "target": "user_2", "weight": 1.0},
{"source": "user_2", "target": "user_3", "weight": 2.0},
("user_3", "user_1", 0.5), # tuple format: (src, tgt, weight)
("alice", "user_1"), # default weight = 1.0
]
G.add_edges_bulk(edges_bulk)
print(f"After bulk ops: {G.number_of_vertices()} vertices, {G.number_of_edges()} edges")
# ## 3. Bulk Operations (Fast Path)
# Bulk vertex insertion - much faster than looping
vertices_bulk = [
("user_1", {"score": 10, "active": True}),
("user_2", {"score": 20, "active": False}),
("user_3", {"score": 15, "active": True}),
]
G.add_vertices_bulk(vertices_bulk)
# Bulk edge insertion
edges_bulk = [
{"source": "user_1", "target": "user_2", "weight": 1.0},
{"source": "user_2", "target": "user_3", "weight": 2.0},
("user_3", "user_1", 0.5), # tuple format: (src, tgt, weight)
("alice", "user_1"), # default weight = 1.0
]
G.add_edges_bulk(edges_bulk)
print(f"After bulk ops: {G.number_of_vertices()} vertices, {G.number_of_edges()} edges")
After bulk ops: 7 vertices, 8 edges
In [4]:
Copied!
# ## 4. Undirected & Mixed Graphs
# Create undirected graph
G_undir = AnnNet(directed=False)
G_undir.add_vertex("a")
G_undir.add_vertex("b")
G_undir.add_edge("a", "b", weight=1.0)
# Mixed: per-edge direction override
G_mixed = AnnNet(directed=True)
G_mixed.add_vertex("x")
G_mixed.add_vertex("y")
G_mixed.add_vertex("z")
G_mixed.add_edge("x", "y", edge_directed=True) # directed
G_mixed.add_edge("y", "z", edge_directed=False) # undirected despite graph default
print(f"Directed edges: {G_mixed.get_directed_edges()}")
print(f"Undirected edges: {G_mixed.get_undirected_edges()}")
# ## 4. Undirected & Mixed Graphs
# Create undirected graph
G_undir = AnnNet(directed=False)
G_undir.add_vertex("a")
G_undir.add_vertex("b")
G_undir.add_edge("a", "b", weight=1.0)
# Mixed: per-edge direction override
G_mixed = AnnNet(directed=True)
G_mixed.add_vertex("x")
G_mixed.add_vertex("y")
G_mixed.add_vertex("z")
G_mixed.add_edge("x", "y", edge_directed=True) # directed
G_mixed.add_edge("y", "z", edge_directed=False) # undirected despite graph default
print(f"Directed edges: {G_mixed.get_directed_edges()}")
print(f"Undirected edges: {G_mixed.get_undirected_edges()}")
Directed edges: ['edge_0'] Undirected edges: ['edge_1']
In [5]:
Copied!
# ## 5. Slices (Subgraph Views)
# Slices partition your graph into logical subsets
G.add_slice("team_alpha", department="engineering")
G.add_slice("team_beta", department="design")
# Set active slice - new elements go here by default
G.set_active_slice("team_alpha")
G.add_vertex("eng_1", level="senior")
G.add_edge("eng_1", "alice")
G.set_active_slice("team_beta")
G.add_vertex("des_1", level="junior")
G.add_edge("des_1", "bob")
# Query slice contents
print(f"team_alpha vertices: {G.get_slice_vertices('team_alpha')}")
print(f"team_beta vertices: {G.get_slice_vertices('team_beta')}")
print(f"All slices: {G.list_slices()}")
# ## 5. Slices (Subgraph Views)
# Slices partition your graph into logical subsets
G.add_slice("team_alpha", department="engineering")
G.add_slice("team_beta", department="design")
# Set active slice - new elements go here by default
G.set_active_slice("team_alpha")
G.add_vertex("eng_1", level="senior")
G.add_edge("eng_1", "alice")
G.set_active_slice("team_beta")
G.add_vertex("des_1", level="junior")
G.add_edge("des_1", "bob")
# Query slice contents
print(f"team_alpha vertices: {G.get_slice_vertices('team_alpha')}")
print(f"team_beta vertices: {G.get_slice_vertices('team_beta')}")
print(f"All slices: {G.list_slices()}")
team_alpha vertices: {'eng_1', 'alice'}
team_beta vertices: {'des_1', 'bob'}
All slices: ['team_alpha', 'team_beta']
In [6]:
Copied!
# ## 6. Slice Set Operations
# Union of slices
union_result = G.slice_union(["team_alpha", "team_beta"])
print(f"Union vertices: {union_result['vertices']}")
# Intersection
G.add_vertex("shared_member", slice="team_alpha")
# Also add to team_beta
G._slices["team_beta"]["vertices"].add("shared_member")
intersection = G.slice_intersection(["team_alpha", "team_beta"])
print(f"Intersection: {intersection}")
# Create new slice from operation result
G.create_slice_from_operation("combined_teams", union_result, source="union")
print(f"Combined team has {len(G.get_slice_vertices('combined_teams'))} vertices")
# ## 6. Slice Set Operations
# Union of slices
union_result = G.slice_union(["team_alpha", "team_beta"])
print(f"Union vertices: {union_result['vertices']}")
# Intersection
G.add_vertex("shared_member", slice="team_alpha")
# Also add to team_beta
G._slices["team_beta"]["vertices"].add("shared_member")
intersection = G.slice_intersection(["team_alpha", "team_beta"])
print(f"Intersection: {intersection}")
# Create new slice from operation result
G.create_slice_from_operation("combined_teams", union_result, source="union")
print(f"Combined team has {len(G.get_slice_vertices('combined_teams'))} vertices")
Union vertices: {'des_1', 'eng_1', 'alice', 'bob'}
Intersection: {'vertices': {'shared_member'}, 'edges': set()}
Combined team has 4 vertices
In [7]:
Copied!
# ## 7. Hyperedges (k-ary Relations)
H = AnnNet(directed=False)
# Add vertices for a collaboration network
for i in range(6):
H.add_vertex(f"person_{i}")
# Undirected hyperedge: a meeting with multiple participants
H.add_hyperedge(
members=["person_0", "person_1", "person_2"],
edge_id="meeting_1",
weight=1.0,
topic="planning"
)
# Directed hyperedge: authors -> reviewers (head -> tail)
H.add_hyperedge(
head=["person_3", "person_4"], # authors
tail=["person_5"], # reviewer
edge_id="review_1",
weight=2.0
)
print(f"Hyperedge definitions: {H.hyperedge_definitions}")
# ## 7. Hyperedges (k-ary Relations)
H = AnnNet(directed=False)
# Add vertices for a collaboration network
for i in range(6):
H.add_vertex(f"person_{i}")
# Undirected hyperedge: a meeting with multiple participants
H.add_hyperedge(
members=["person_0", "person_1", "person_2"],
edge_id="meeting_1",
weight=1.0,
topic="planning"
)
# Directed hyperedge: authors -> reviewers (head -> tail)
H.add_hyperedge(
head=["person_3", "person_4"], # authors
tail=["person_5"], # reviewer
edge_id="review_1",
weight=2.0
)
print(f"Hyperedge definitions: {H.hyperedge_definitions}")
Hyperedge definitions: {'meeting_1': {'directed': False, 'members': {'person_0', 'person_1', 'person_2'}}, 'review_1': {'directed': True, 'head': {'person_3', 'person_4'}, 'tail': {'person_5'}}}
In [8]:
Copied!
# ## 8. Polars-Backed Attribute Tables
# Vertex attributes as Polars DF (DataFrame)
print("=== Vertex Attributes ===")
print(G.vertices_view())
# Edge attributes
print("\n=== Edge Attributes ===")
print(G.edges_view(include_weight=True, include_directed=True))
# Slice attributes
print("\n=== Slice Attributes ===")
print(G.slices_view())
# Direct attribute access
print(f"\nAlice's age: {G.get_attr_vertex('alice', 'age')}")
# Bulk attribute update
G.set_vertex_attrs("alice", department="R&D", years_exp=5)
print(f"Alice updated: {G.get_vertex_attrs('alice')}")
# ## 8. Polars-Backed Attribute Tables
# Vertex attributes as Polars DF (DataFrame)
print("=== Vertex Attributes ===")
print(G.vertices_view())
# Edge attributes
print("\n=== Edge Attributes ===")
print(G.edges_view(include_weight=True, include_directed=True))
# Slice attributes
print("\n=== Slice Attributes ===")
print(G.slices_view())
# Direct attribute access
print(f"\nAlice's age: {G.get_attr_vertex('alice', 'age')}")
# Bulk attribute update
G.set_vertex_attrs("alice", department="R&D", years_exp=5)
print(f"Alice updated: {G.get_vertex_attrs('alice')}")
=== Vertex Attributes ===
shape: (10, 6)
┌───────────────┬──────┬──────────┬────────┬───────┬────────┐
│ vertex_id ┆ age ┆ role ┆ active ┆ score ┆ level │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ i64 ┆ str ┆ bool ┆ i64 ┆ str │
╞═══════════════╪══════╪══════════╪════════╪═══════╪════════╡
│ alice ┆ 30 ┆ engineer ┆ null ┆ null ┆ null │
│ bob ┆ 25 ┆ designer ┆ null ┆ null ┆ null │
│ carol ┆ 35 ┆ manager ┆ null ┆ null ┆ null │
│ dave ┆ 28 ┆ engineer ┆ null ┆ null ┆ null │
│ user_1 ┆ null ┆ null ┆ true ┆ 10 ┆ null │
│ user_2 ┆ null ┆ null ┆ false ┆ 20 ┆ null │
│ user_3 ┆ null ┆ null ┆ true ┆ 15 ┆ null │
│ eng_1 ┆ null ┆ null ┆ null ┆ null ┆ senior │
│ des_1 ┆ null ┆ null ┆ null ┆ null ┆ junior │
│ shared_member ┆ null ┆ null ┆ null ┆ null ┆ null │
└───────────────┴──────┴──────────┴────────┴───────┴────────┘
=== Edge Attributes ===
shape: (10, 12)
┌─────────┬────────┬──────────┬─────────────┬───┬───────────┬───────────┬─────────────┬────────────┐
│ edge_id ┆ kind ┆ directed ┆ global_weig ┆ … ┆ tail ┆ members ┆ relation ┆ effective_ │
│ --- ┆ --- ┆ --- ┆ ht ┆ ┆ --- ┆ --- ┆ --- ┆ weight │
│ str ┆ str ┆ bool ┆ --- ┆ ┆ list[str] ┆ list[str] ┆ str ┆ --- │
│ ┆ ┆ ┆ f64 ┆ ┆ ┆ ┆ ┆ f64 │
╞═════════╪════════╪══════════╪═════════════╪═══╪═══════════╪═══════════╪═════════════╪════════════╡
│ edge_0 ┆ binary ┆ true ┆ 1.0 ┆ … ┆ null ┆ null ┆ colleague ┆ 1.0 │
│ edge_1 ┆ binary ┆ true ┆ 2.0 ┆ … ┆ null ┆ null ┆ reports_to ┆ 2.0 │
│ edge_2 ┆ binary ┆ true ┆ 1.5 ┆ … ┆ null ┆ null ┆ manages ┆ 1.5 │
│ edge_3 ┆ binary ┆ true ┆ 0.8 ┆ … ┆ null ┆ null ┆ collaborate ┆ 0.8 │
│ ┆ ┆ ┆ ┆ ┆ ┆ ┆ s ┆ │
│ edge_4 ┆ binary ┆ true ┆ 1.0 ┆ … ┆ null ┆ null ┆ null ┆ 1.0 │
│ edge_5 ┆ binary ┆ true ┆ 2.0 ┆ … ┆ null ┆ null ┆ null ┆ 2.0 │
│ edge_6 ┆ binary ┆ true ┆ 0.5 ┆ … ┆ null ┆ null ┆ null ┆ 0.5 │
│ edge_7 ┆ binary ┆ true ┆ 1.0 ┆ … ┆ null ┆ null ┆ null ┆ 1.0 │
│ edge_8 ┆ binary ┆ true ┆ 1.0 ┆ … ┆ null ┆ null ┆ null ┆ 1.0 │
│ edge_9 ┆ binary ┆ true ┆ 1.0 ┆ … ┆ null ┆ null ┆ null ┆ 1.0 │
└─────────┴────────┴──────────┴─────────────┴───┴───────────┴───────────┴─────────────┴────────────┘
=== Slice Attributes ===
shape: (2, 2)
┌────────────┬─────────────┐
│ slice_id ┆ department │
│ --- ┆ --- │
│ str ┆ str │
╞════════════╪═════════════╡
│ team_alpha ┆ engineering │
│ team_beta ┆ design │
└────────────┴─────────────┘
Alice's age: 30
Alice updated: {'vertex_id': 'alice', 'age': 30, 'role': 'engineer', 'active': None, 'score': None, 'level': None, 'department': 'R&D', 'years_exp': 5}
In [9]:
Copied!
# ## 9. Per-Slice Edge Weights
# Same edge can have different weights in different slices
G.add_slice("context_a")
G.add_slice("context_b")
# Add edge to both slices
eid = G.add_edge("alice", "bob", weight=1.0, slice="context_a")
G._slices["context_b"]["edges"].add(eid)
# Override weight in context_b
G.set_edge_slice_attrs("context_b", eid, weight=5.0)
print(f"Weight in context_a: {G.get_effective_edge_weight(eid, slice='context_a')}")
print(f"Weight in context_b: {G.get_effective_edge_weight(eid, slice='context_b')}")
# ## 9. Per-Slice Edge Weights
# Same edge can have different weights in different slices
G.add_slice("context_a")
G.add_slice("context_b")
# Add edge to both slices
eid = G.add_edge("alice", "bob", weight=1.0, slice="context_a")
G._slices["context_b"]["edges"].add(eid)
# Override weight in context_b
G.set_edge_slice_attrs("context_b", eid, weight=5.0)
print(f"Weight in context_a: {G.get_effective_edge_weight(eid, slice='context_a')}")
print(f"Weight in context_b: {G.get_effective_edge_weight(eid, slice='context_b')}")
Weight in context_a: 1.0 Weight in context_b: 5.0
In [10]:
Copied!
# ## 10. Multilayer Networks (Kivelä Framework)
ML = AnnNet()
# Define multi-aspect structure
ML.set_aspects(
aspects=["time", "relation"],
elem_layers={
"time": ["t1", "t2", "t3"],
"relation": ["friendship", "work"]
}
)
# Add vertices with layer presence
for v in ["alice", "bob", "carol"]:
ML.add_vertex(v)
# Declare presence in specific layers (vertex-layer pairs)
ML.add_presence("alice", ("t1", "friendship"))
ML.add_presence("alice", ("t2", "friendship"))
ML.add_presence("bob", ("t1", "friendship"))
ML.add_presence("bob", ("t1", "work"))
ML.add_presence("carol", ("t2", "work"))
# Intra-layer edge (same layer)
ML.add_intra_edge_nl("alice", "bob", ("t1", "friendship"), weight=1.0)
# Inter-layer edge (across layers)
ML.add_inter_edge_nl("alice", ("t1", "friendship"), "alice", ("t2", "friendship"), weight=0.5)
# Coupling edge (same vertex across layers)
ML.add_coupling_edge_nl("bob", ("t1", "friendship"), ("t1", "work"), weight=0.8)
print(f"Edge kinds: {ML.edge_kind}")
print(f"Edge layers: {ML.edge_layers}")
# ## 10. Multilayer Networks (Kivelä Framework)
ML = AnnNet()
# Define multi-aspect structure
ML.set_aspects(
aspects=["time", "relation"],
elem_layers={
"time": ["t1", "t2", "t3"],
"relation": ["friendship", "work"]
}
)
# Add vertices with layer presence
for v in ["alice", "bob", "carol"]:
ML.add_vertex(v)
# Declare presence in specific layers (vertex-layer pairs)
ML.add_presence("alice", ("t1", "friendship"))
ML.add_presence("alice", ("t2", "friendship"))
ML.add_presence("bob", ("t1", "friendship"))
ML.add_presence("bob", ("t1", "work"))
ML.add_presence("carol", ("t2", "work"))
# Intra-layer edge (same layer)
ML.add_intra_edge_nl("alice", "bob", ("t1", "friendship"), weight=1.0)
# Inter-layer edge (across layers)
ML.add_inter_edge_nl("alice", ("t1", "friendship"), "alice", ("t2", "friendship"), weight=0.5)
# Coupling edge (same vertex across layers)
ML.add_coupling_edge_nl("bob", ("t1", "friendship"), ("t1", "work"), weight=0.8)
print(f"Edge kinds: {ML.edge_kind}")
print(f"Edge layers: {ML.edge_layers}")
Edge kinds: {'alice--bob@t1.friendship': 'intra', 'alice--alice==t1.friendship~t2.friendship': 'inter', 'bob--bob==t1.friendship~t1.work': 'coupling'}
Edge layers: {'alice--bob@t1.friendship': ('t1', 'friendship'), 'alice--alice==t1.friendship~t2.friendship': (('t1', 'friendship'), ('t2', 'friendship')), 'bob--bob==t1.friendship~t1.work': (('t1', 'friendship'), ('t1', 'work'))}
In [11]:
Copied!
# ## 11. Layer Algebra & Views
# Get vertices in a specific layer
layer_verts = ML.layer_vertex_set(("t1", "friendship"))
print(f"Vertices in (t1, friendship): {layer_verts}")
# Layer union
union = ML.layer_union([("t1", "friendship"), ("t1", "work")])
print(f"Union of t1 layers: {union}")
# Aspects view
print("\n=== Aspects ===")
print(ML.aspects_view())
# Layers view
print("\n=== Layers ===")
print(ML.layers_view())
# ## 11. Layer Algebra & Views
# Get vertices in a specific layer
layer_verts = ML.layer_vertex_set(("t1", "friendship"))
print(f"Vertices in (t1, friendship): {layer_verts}")
# Layer union
union = ML.layer_union([("t1", "friendship"), ("t1", "work")])
print(f"Union of t1 layers: {union}")
# Aspects view
print("\n=== Aspects ===")
print(ML.aspects_view())
# Layers view
print("\n=== Layers ===")
print(ML.layers_view())
Vertices in (t1, friendship): {'alice', 'bob'}
Union of t1 layers: {'vertices': {'alice', 'bob'}, 'edges': {'alice--bob@t1.friendship'}}
=== Aspects ===
shape: (2, 2)
┌──────────┬────────────────────────┐
│ aspect ┆ elem_layers │
│ --- ┆ --- │
│ str ┆ list[str] │
╞══════════╪════════════════════════╡
│ time ┆ ["t1", "t2", "t3"] │
│ relation ┆ ["friendship", "work"] │
└──────────┴────────────────────────┘
=== Layers ===
shape: (6, 4)
┌──────────────────────┬───────────────┬──────┬────────────┐
│ layer_tuple ┆ layer_id ┆ time ┆ relation │
│ --- ┆ --- ┆ --- ┆ --- │
│ list[str] ┆ str ┆ str ┆ str │
╞══════════════════════╪═══════════════╪══════╪════════════╡
│ ["t1", "friendship"] ┆ t1×friendship ┆ t1 ┆ friendship │
│ ["t1", "work"] ┆ t1×work ┆ t1 ┆ work │
│ ["t2", "friendship"] ┆ t2×friendship ┆ t2 ┆ friendship │
│ ["t2", "work"] ┆ t2×work ┆ t2 ┆ work │
│ ["t3", "friendship"] ┆ t3×friendship ┆ t3 ┆ friendship │
│ ["t3", "work"] ┆ t3×work ┆ t3 ┆ work │
└──────────────────────┴───────────────┴──────┴────────────┘
In [12]:
Copied!
# ## 12. Supra-Adjacency & Spectral Methods
# Build vertex-layer index for matrix operations
n_rows = ML.ensure_vertex_layer_index()
print(f"Supra-adjacency size: {n_rows}x{n_rows}")
# Supra-adjacency matrix (CSR - Compressed Sparse Row)
A_supra = ML.supra_adjacency()
print(f"Supra-adjacency nnz (non-zero entries): {A_supra.nnz}")
# Supra-Laplacian
L = ML.supra_laplacian(kind="comb") # combinatorial
print(f"Laplacian shape: {L.shape}")
# Algebraic connectivity (Fiedler value)
if n_rows >= 2:
lambda2, fiedler = ML.algebraic_connectivity()
print(f"Algebraic connectivity: {lambda2:.4f}\n")
L.toarray()
# ## 12. Supra-Adjacency & Spectral Methods
# Build vertex-layer index for matrix operations
n_rows = ML.ensure_vertex_layer_index()
print(f"Supra-adjacency size: {n_rows}x{n_rows}")
# Supra-adjacency matrix (CSR - Compressed Sparse Row)
A_supra = ML.supra_adjacency()
print(f"Supra-adjacency nnz (non-zero entries): {A_supra.nnz}")
# Supra-Laplacian
L = ML.supra_laplacian(kind="comb") # combinatorial
print(f"Laplacian shape: {L.shape}")
# Algebraic connectivity (Fiedler value)
if n_rows >= 2:
lambda2, fiedler = ML.algebraic_connectivity()
print(f"Algebraic connectivity: {lambda2:.4f}\n")
L.toarray()
Supra-adjacency size: 5x5 Supra-adjacency nnz (non-zero entries): 6 Laplacian shape: (5, 5) Algebraic connectivity: 0.0000
Out[12]:
array([[ 1.5, -0.5, -1. , 0. , 0. ],
[-0.5, 0.5, 0. , 0. , 0. ],
[-1. , 0. , 1.8, -0.8, 0. ],
[ 0. , 0. , -0.8, 0.8, 0. ],
[ 0. , 0. , 0. , 0. , 0. ]])
In [13]:
Copied!
# ## 13. Tensor View & Flattening
# 4-index tensor representation (u, layer_a, v, layer_b, weight)
tensor = ML.adjacency_tensor_view()
print(f"Tensor vertices: {tensor['vertices']}")
print(f"Tensor layers: {tensor['layers']}")
print(f"Number of entries: {len(tensor['w'])}")
# Round-trip: tensor -> supra -> tensor
A_from_tensor = ML.flatten_to_supra(tensor)
tensor_back = ML.unflatten_from_supra(A_from_tensor)
# ## 13. Tensor View & Flattening
# 4-index tensor representation (u, layer_a, v, layer_b, weight)
tensor = ML.adjacency_tensor_view()
print(f"Tensor vertices: {tensor['vertices']}")
print(f"Tensor layers: {tensor['layers']}")
print(f"Number of entries: {len(tensor['w'])}")
# Round-trip: tensor -> supra -> tensor
A_from_tensor = ML.flatten_to_supra(tensor)
tensor_back = ML.unflatten_from_supra(A_from_tensor)
Tensor vertices: ['alice', 'bob', 'carol']
Tensor layers: [('t1', 'friendship'), ('t2', 'friendship'), ('t1', 'work'), ('t2', 'work')]
Number of entries: 6
In [14]:
Copied!
# ## 14. Random Walks & Diffusion
import numpy as np
# Transition matrix (row-stochastic)
P = ML.transition_matrix()
print(f"Transition matrix shape: {P.shape}")
# One step of random walk
if n_rows > 0:
p0 = np.zeros(n_rows)
p0[0] = 1.0 # start at first vertex-layer
p1 = ML.random_walk_step(p0)
print(f"After 1 step, distribution sum: {p1.sum():.4f}")
# Diffusion step
if n_rows > 0:
x0 = np.random.randn(n_rows)
x1 = ML.diffusion_step(x0, tau=0.1, kind="comb")
print(f"Diffusion step complete, ||x1|| = {np.linalg.norm(x1):.4f}")
# ## 14. Random Walks & Diffusion
import numpy as np
# Transition matrix (row-stochastic)
P = ML.transition_matrix()
print(f"Transition matrix shape: {P.shape}")
# One step of random walk
if n_rows > 0:
p0 = np.zeros(n_rows)
p0[0] = 1.0 # start at first vertex-layer
p1 = ML.random_walk_step(p0)
print(f"After 1 step, distribution sum: {p1.sum():.4f}")
# Diffusion step
if n_rows > 0:
x0 = np.random.randn(n_rows)
x1 = ML.diffusion_step(x0, tau=0.1, kind="comb")
print(f"Diffusion step complete, ||x1|| = {np.linalg.norm(x1):.4f}")
Transition matrix shape: (5, 5) After 1 step, distribution sum: 1.0000 Diffusion step complete, ||x1|| = 1.9721
In [15]:
Copied!
# ## 15. Coupling Regime Analysis
# Sweep coupling strength and measure algebraic connectivity
scales = [0.1, 0.5, 1.0, 2.0, 5.0]
if ML.number_of_edges() > 0:
results = ML.sweep_coupling_regime(scales, metric="algebraic_connectivity")
for s, r in zip(scales, results):
print(f"omega={s}: lambda_2={r:.4f}")
# ## 15. Coupling Regime Analysis
# Sweep coupling strength and measure algebraic connectivity
scales = [0.1, 0.5, 1.0, 2.0, 5.0]
if ML.number_of_edges() > 0:
results = ML.sweep_coupling_regime(scales, metric="algebraic_connectivity")
for s, r in zip(scales, results):
print(f"omega={s}: lambda_2={r:.4f}")
omega=0.1: lambda_2=0.0000 omega=0.5: lambda_2=-0.0000 omega=1.0: lambda_2=0.0000 omega=2.0: lambda_2=0.0000 omega=5.0: lambda_2=0.0000
In [16]:
Copied!
# ## 16. AnnNet Traversal
# Neighbors (all adjacent vertices)
print(f"Alice's neighbors: {G.neighbors('alice')}")
# Directed traversal
print(f"Out-neighbors of alice: {G.out_neighbors('alice')}")
print(f"In-neighbors of alice: {G.in_neighbors('alice')}")
# Synonyms
print(f"Successors: {G.successors('alice')}")
print(f"Predecessors: {G.predecessors('alice')}")
# Degree
print(f"Degree of alice: {G.degree('alice')}")
# ## 16. AnnNet Traversal
# Neighbors (all adjacent vertices)
print(f"Alice's neighbors: {G.neighbors('alice')}")
# Directed traversal
print(f"Out-neighbors of alice: {G.out_neighbors('alice')}")
print(f"In-neighbors of alice: {G.in_neighbors('alice')}")
# Synonyms
print(f"Successors: {G.successors('alice')}")
print(f"Predecessors: {G.predecessors('alice')}")
# Degree
print(f"Degree of alice: {G.degree('alice')}")
Alice's neighbors: ['user_1', 'carol', 'bob'] Out-neighbors of alice: ['user_1', 'carol', 'bob'] In-neighbors of alice: ['eng_1'] Successors: ['user_1', 'carol', 'bob'] Predecessors: ['eng_1'] Degree of alice: 5
In [17]:
Copied!
# ## 17. Subgraphs
# Vertex-induced subgraph
sub_v = G.subgraph(["alice", "bob", "carol"])
print(f"Vertex subgraph: {sub_v.number_of_vertices()} V, {sub_v.number_of_edges()} E")
# Edge-induced subgraph
edges_to_keep = G.edges()[:2]
sub_e = G.edge_subgraph(edges_to_keep)
print(f"Edge subgraph: {sub_e.number_of_vertices()} V, {sub_e.number_of_edges()} E")
# Combined filter
sub_both = G.extract_subgraph(vertices=["alice", "bob"], edges=edges_to_keep)
# ## 17. Subgraphs
# Vertex-induced subgraph
sub_v = G.subgraph(["alice", "bob", "carol"])
print(f"Vertex subgraph: {sub_v.number_of_vertices()} V, {sub_v.number_of_edges()} E")
# Edge-induced subgraph
edges_to_keep = G.edges()[:2]
sub_e = G.edge_subgraph(edges_to_keep)
print(f"Edge subgraph: {sub_e.number_of_vertices()} V, {sub_e.number_of_edges()} E")
# Combined filter
sub_both = G.extract_subgraph(vertices=["alice", "bob"], edges=edges_to_keep)
Vertex subgraph: 3 V, 4 E Edge subgraph: 3 V, 2 E
In [18]:
Copied!
# ## 18. AnnNet Views (Lazy Filtering)
# Create a lazy view without copying data
view = G.view(vertices=["alice", "bob", "carol"])
print(f"View type: {type(view)}")
# Views support filtering by slices too
view_slice = G.view(slices=["team_alpha"])
view_slice
# ## 18. AnnNet Views (Lazy Filtering)
# Create a lazy view without copying data
view = G.view(vertices=["alice", "bob", "carol"])
print(f"View type: {type(view)}")
# Views support filtering by slices too
view_slice = G.view(slices=["team_alpha"])
view_slice
View type: <class 'annnet.core._GraphView.GraphView'>
Out[18]:
GraphView(vertices=3, edges=1)
In [19]:
Copied!
# ## 19. History & Versioning
# AnnNet automatically logs mutations
print(f"Current version: {G._version}")
print(f"History length: {len(G._history)}")
# View history as DataFrame
hist_df = G.history(as_df=True)
print(f"\nHistory columns: {hist_df.columns}")
# Manual markers
G.mark("checkpoint_1")
# Snapshots for diffing
snap1 = G.snapshot(label="before_changes")
G.add_vertex("new_vertex_for_diff")
snap2 = G.snapshot(label="after_changes")
# Diff snapshots
diff = G.diff("before_changes", "after_changes")
print(f"\nDiff added vertices: {diff.vertices_added}")
# Export history
# G.export_history("graph_history.parquet")
# ## 19. History & Versioning
# AnnNet automatically logs mutations
print(f"Current version: {G._version}")
print(f"History length: {len(G._history)}")
# View history as DataFrame
hist_df = G.history(as_df=True)
print(f"\nHistory columns: {hist_df.columns}")
# Manual markers
G.mark("checkpoint_1")
# Snapshots for diffing
snap1 = G.snapshot(label="before_changes")
G.add_vertex("new_vertex_for_diff")
snap2 = G.snapshot(label="after_changes")
# Diff snapshots
diff = G.diff("before_changes", "after_changes")
print(f"\nDiff added vertices: {diff.vertices_added}")
# Export history
# G.export_history("graph_history.parquet")
Current version: 22
History length: 22
History columns: ['version', 'ts_utc', 'mono_ns', 'op', 'vertex_id', 'slice', 'attributes', 'result', 'edge_id', 'attrs', 'source', 'target', 'weight', 'edge_type', 'propagate', 'slice_weight', 'directed', 'edge_directed', 'slice_id']
Diff added vertices: {'new_vertex_for_diff'}
In [28]:
Copied!
# ## 20. Interop (Lazy Proxy) with NetworkX, igraph and graph-tool
# The .nx property provides lazy NetworkX proxies
# Algorithms are called on the converted graph transparently
# Example: G.nx.louvain_communities(G)
Glv = G.nx.louvain_communities(G)
print(f"louvain_communities: {Glv}")
# ## 20. Interop (Lazy Proxy) with NetworkX, igraph and graph-tool
# The .nx property provides lazy NetworkX proxies
# Algorithms are called on the converted graph transparently
# Example: G.nx.louvain_communities(G)
Glv = G.nx.louvain_communities(G)
print(f"louvain_communities: {Glv}")
louvain_communities: [{8, 0, 1, 7}, {2, 3}, {4, 5, 6}, {9}, {10}, {11, 12}]
/mnt/c/Users/pc/Desktop/annnet/annnet/core/lazy_proxies/nx_lazyproxy.py:1993: RuntimeWarning: AnnNet-NX conversion is lossy: multiple slices flattened into single NX graph. nxG = self._convert_to_nx(
In [21]:
Copied!
# ## 23. Composite Vertex Keys
# Define composite key for vertex lookup
K = AnnNet()
K.set_vertex_key("type", "name")
# Add vertices with key attributes
K.add_vertex("v1", type="person", name="Alice")
K.add_vertex("v2", type="person", name="Bob")
K.add_vertex("v3", type="org", name="Acme")
# Lookup by composite key
vid = K.get_or_create_vertex_by_attrs(type="person", name="Alice")
print(f"Found vertex: {vid}")
# Edges can reference vertices by attribute dict
K.add_edge(
source={"type": "person", "name": "Alice"},
target={"type": "org", "name": "Acme"},
relation="works_at"
)
# ## 23. Composite Vertex Keys
# Define composite key for vertex lookup
K = AnnNet()
K.set_vertex_key("type", "name")
# Add vertices with key attributes
K.add_vertex("v1", type="person", name="Alice")
K.add_vertex("v2", type="person", name="Bob")
K.add_vertex("v3", type="org", name="Acme")
# Lookup by composite key
vid = K.get_or_create_vertex_by_attrs(type="person", name="Alice")
print(f"Found vertex: {vid}")
# Edges can reference vertices by attribute dict
K.add_edge(
source={"type": "person", "name": "Alice"},
target={"type": "org", "name": "Acme"},
relation="works_at"
)
Found vertex: cid:type='person'|name='Alice'
Out[21]:
'edge_0'
In [22]:
Copied!
# ## 24. Matrix Access & Incidence
# Raw incidence matrix (DOK - Dictionary of Keys, or CSR)
M = G._matrix.tocsr()
print(f"Incidence matrix shape: {M.shape}")
print(f"Non-zero entries: {M.nnz}")
# Get as dense array
dense = G.vertex_incidence_matrix(values=True, sparse=False)
print(f"Dense incidence shape: {dense.shape}")
# Incidence as Python dict
inc_dict = G.get_vertex_incidence_matrix_as_lists(values=False)
print(f"Incidence for alice: edges {inc_dict.get('alice', [])}")
# ## 24. Matrix Access & Incidence
# Raw incidence matrix (DOK - Dictionary of Keys, or CSR)
M = G._matrix.tocsr()
print(f"Incidence matrix shape: {M.shape}")
print(f"Non-zero entries: {M.nnz}")
# Get as dense array
dense = G.vertex_incidence_matrix(values=True, sparse=False)
print(f"Dense incidence shape: {dense.shape}")
# Incidence as Python dict
inc_dict = G.get_vertex_incidence_matrix_as_lists(values=False)
print(f"Incidence for alice: edges {inc_dict.get('alice', [])}")
Incidence matrix shape: (100, 200) Non-zero entries: 22 Dense incidence shape: (100, 200) Incidence for alice: edges [0, 3, 7, 8, 10]
In [23]:
Copied!
# ## 25. Adjacency Matrix
# Standard adjacency matrix (vertices x vertices)
A = G.cache.adjacency
print(f"Adjacency matrix shape: {A.shape}")
# ## 25. Adjacency Matrix
# Standard adjacency matrix (vertices x vertices)
A = G.cache.adjacency
print(f"Adjacency matrix shape: {A.shape}")
Adjacency matrix shape: (100, 100)
In [39]:
Copied!
# ## 26. Statistics & Metrics
# Slice statistics
stats = G.slice_statistics()
print(f"Slice stats: {stats}")
# Memory usage estimate
mem = G.memory_usage()
print(f"Estimated memory: {mem / 1024:.2f} KB")
# Audit attributes for consistency
audit = G.audit_attributes()
print(f"Audit result: {audit}")
# ## 26. Statistics & Metrics
# Slice statistics
stats = G.slice_statistics()
print(f"Slice stats: {stats}")
# Memory usage estimate
mem = G.memory_usage()
print(f"Estimated memory: {mem / 1024:.2f} KB")
# Audit attributes for consistency
audit = G.audit_attributes()
print(f"Audit result: {audit}")
Slice stats: {'team_alpha': {'vertices': 3, 'edges': 1, 'attributes': {'department': 'engineering'}}, 'team_beta': {'vertices': 4, 'edges': 1, 'attributes': {'department': 'design'}}, 'combined_teams': {'vertices': 4, 'edges': 2, 'attributes': {'source': 'union'}}, 'context_a': {'vertices': 2, 'edges': 1, 'attributes': {}}, 'context_b': {'vertices': 0, 'edges': 1, 'attributes': {}}}
Estimated memory: 3.96 KB
Audit result: {'extra_vertex_rows': [], 'extra_edge_rows': [], 'missing_vertex_rows': [], 'missing_edge_rows': ['edge_6', 'edge_5', 'edge_8', 'edge_4', 'edge_10', 'edge_9', 'edge_7'], 'invalid_edge_slice_rows': []}
In [43]:
Copied!
# ## 27. AnnNet Copy
# Deep copy
G_copy = G.copy(history=False)
print(f"Copy has {G_copy.number_of_vertices()} vertices")
# Copy preserves all structure
assert G_copy.V == G.V
assert G_copy.E == G.E
# ## 27. AnnNet Copy
# Deep copy
G_copy = G.copy(history=False)
print(f"Copy has {G_copy.number_of_vertices()} vertices")
# Copy preserves all structure
assert G_copy.V == G.V
assert G_copy.E == G.E
Copy has 11 vertices
In [47]:
Copied!
# ## 28. Edge Presence Across Slices
# Find which slices contain an edge
presence = G.edge_presence_across_slices(source="alice", target="bob")
print(f"alice->bob present in slices: {presence}")
# By edge ID
edge_ids = G.get_edge_ids("alice", "bob")
if edge_ids:
slices = G.edge_presence_across_slices(edge_id=edge_ids[0])
print(f"Edge {edge_ids[0]} in slices: {slices}")
# ## 28. Edge Presence Across Slices
# Find which slices contain an edge
presence = G.edge_presence_across_slices(source="alice", target="bob")
print(f"alice->bob present in slices: {presence}")
# By edge ID
edge_ids = G.get_edge_ids("alice", "bob")
if edge_ids:
slices = G.edge_presence_across_slices(edge_id=edge_ids[0])
print(f"Edge {edge_ids[0]} in slices: {slices}")
alice->bob present in slices: {'context_a': ['edge_10'], 'context_b': ['edge_10']}
Edge edge_0 in slices: []
In [48]:
Copied!
# ## 29. Serialization (I/O)
# Save to .annnet format (lossless)
G.write("my_graph.annnet")
# Load back
G_loaded = AnnNet.read("my_graph.annnet")
# ## 29. Serialization (I/O)
# Save to .annnet format (lossless)
G.write("my_graph.annnet")
# Load back
G_loaded = AnnNet.read("my_graph.annnet")
In [50]:
Copied!
assert G.V == G_loaded.V
assert G.E == G_loaded.E
assert G.V == G_loaded.V
assert G.E == G_loaded.E
In [59]:
Copied!
# ## 30. AnnData-like API
# The library provides AnnData-inspired accessors for bioinformatics workflows
# X() - sparse incidence matrix
X = G.X()
print(f"X shape: {X.shape}")
# obs - vertex attributes (observations)
obs = G.obs
print(f"obs columns: {obs.columns}")
# var - edge attributes (variables)
var = G.var
print(f"var columns: {var.columns}")
# uns - unstructured metadata
uns = G.uns
print(f"uns: {uns}")
# ## 30. AnnData-like API
# The library provides AnnData-inspired accessors for bioinformatics workflows
# X() - sparse incidence matrix
X = G.X()
print(f"X shape: {X.shape}")
# obs - vertex attributes (observations)
obs = G.obs
print(f"obs columns: {obs.columns}")
# var - edge attributes (variables)
var = G.var
print(f"var columns: {var.columns}")
# uns - unstructured metadata
uns = G.uns
print(f"uns: {uns}")
X shape: (100, 200)
obs columns: ['vertex_id', 'age', 'role', 'active', 'score', 'level', 'department', 'years_exp']
var columns: ['edge_id', 'relation']
uns: {}
In [60]:
Copied!
# ## 31. Manager APIs
# Slice manager
slices_mgr = G.slices
print(f"Slice manager: {type(slices_mgr)}")
# Layer manager (for multilayer)
layers_mgr = G.layers
print(f"Layer manager: {type(layers_mgr)}")
# Index manager (entity<->row, edge<->col)
idx_mgr = G.idx
print(f"Index manager: {type(idx_mgr)}")
# Cache manager (CSR/CSC materialization)
cache_mgr = G.cache
print(f"Cache manager: {type(cache_mgr)}")
# ## 31. Manager APIs
# Slice manager
slices_mgr = G.slices
print(f"Slice manager: {type(slices_mgr)}")
# Layer manager (for multilayer)
layers_mgr = G.layers
print(f"Layer manager: {type(layers_mgr)}")
# Index manager (entity<->row, edge<->col)
idx_mgr = G.idx
print(f"Index manager: {type(idx_mgr)}")
# Cache manager (CSR/CSC materialization)
cache_mgr = G.cache
print(f"Cache manager: {type(cache_mgr)}")
Slice manager: <class 'annnet.core._SliceManager.SliceManager'> Layer manager: <class 'annnet.core._LayerManager.LayerManager'> Index manager: <class 'annnet.core._IndexManager.IndexManager'> Cache manager: <class 'annnet.core._CacheManager.CacheManager'>
In [61]:
Copied!
# ## 32. Edge Entities (Vertex-Edge Hybrids)
# Edge entities allow edges to connect to other edges
VE = AnnNet()
VE.add_vertex("a")
VE.add_vertex("b")
# Add an edge entity (can be source/target of other edges)
VE.add_edge_entity("edge_node_1")
# Connect vertex to edge entity
VE.add_edge("a", "edge_node_1", edge_type="vertex_edge")
VE.add_edge("edge_node_1", "b", edge_type="vertex_edge")
print(f"Entity types: {VE.entity_types}")
# ## 32. Edge Entities (Vertex-Edge Hybrids)
# Edge entities allow edges to connect to other edges
VE = AnnNet()
VE.add_vertex("a")
VE.add_vertex("b")
# Add an edge entity (can be source/target of other edges)
VE.add_edge_entity("edge_node_1")
# Connect vertex to edge entity
VE.add_edge("a", "edge_node_1", edge_type="vertex_edge")
VE.add_edge("edge_node_1", "b", edge_type="vertex_edge")
print(f"Entity types: {VE.entity_types}")
Entity types: {'a': 'vertex', 'b': 'vertex', 'edge_node_1': 'edge'}
In [24]:
Copied!
# ## 33. Flexible Direction Edges
# Edges can have flexible direction based on attribute thresholds
# Useful for flow networks where direction depends on state
# Helper to read signs
def signs(G, eid):
s,t,_ = G.edge_definitions[eid]
col = G.edge_to_idx[eid]
si = G.entity_to_idx[s]; ti = G.entity_to_idx[t]
M = G._matrix
return M.get((si,col),0), M.get((ti,col),0)
# Edge-scope policy
e = G.add_edge("u", "v",
flexible={"var":"capacity", "threshold":0.7, "scope":"edge", "above":"s->t", "tie": "undirected"},
capacity=0.5, weight=1.0)
print("init:", signs(G,e)) # expect (-1, +1)
G.set_edge_attrs(e, capacity=0.9)
print("after:", signs(G,e)) # expect (+1, -1)
# ## 33. Flexible Direction Edges
# Edges can have flexible direction based on attribute thresholds
# Useful for flow networks where direction depends on state
# Helper to read signs
def signs(G, eid):
s,t,_ = G.edge_definitions[eid]
col = G.edge_to_idx[eid]
si = G.entity_to_idx[s]; ti = G.entity_to_idx[t]
M = G._matrix
return M.get((si,col),0), M.get((ti,col),0)
# Edge-scope policy
e = G.add_edge("u", "v",
flexible={"var":"capacity", "threshold":0.7, "scope":"edge", "above":"s->t", "tie": "undirected"},
capacity=0.5, weight=1.0)
print("init:", signs(G,e)) # expect (-1, +1)
G.set_edge_attrs(e, capacity=0.9)
print("after:", signs(G,e)) # expect (+1, -1)
init: (np.float32(-1.0), np.float32(1.0)) after: (np.float32(1.0), np.float32(-1.0))
In [25]:
Copied!
# ## 34. Stoichiometry (SBML Support)
# For biochemical networks, set per-vertex coefficients in hyperedges
BIO = AnnNet()
for species in ["A", "B", "C", "D"]:
BIO.add_vertex(species)
# Reaction: 2A + B -> C + 3D
BIO.add_hyperedge(
head=["A", "B"],
tail=["C", "D"],
edge_id="reaction_1"
)
# Set stoichiometric coefficients
BIO.set_hyperedge_coeffs("reaction_1", {"A": -2, "B": -1, "C": 1, "D": 3})
print(f"Reaction matrix column: {BIO._matrix.getcol(0).toarray().flatten()}")
# ## 34. Stoichiometry (SBML Support)
# For biochemical networks, set per-vertex coefficients in hyperedges
BIO = AnnNet()
for species in ["A", "B", "C", "D"]:
BIO.add_vertex(species)
# Reaction: 2A + B -> C + 3D
BIO.add_hyperedge(
head=["A", "B"],
tail=["C", "D"],
edge_id="reaction_1"
)
# Set stoichiometric coefficients
BIO.set_hyperedge_coeffs("reaction_1", {"A": -2, "B": -1, "C": 1, "D": 3})
print(f"Reaction matrix column: {BIO._matrix.getcol(0).toarray().flatten()}")
Reaction matrix column: [-2. -1. 1. 3. 0. 0. 0. 0.]
Summary¶
This library provides:
- Sparse incidence matrix backend (DOK/CSR)
- Polars DataFrames for attributes
- Slices for graph partitioning
- Multilayer networks (Kivelä framework)
- Hyperedges (k-ary relations)
- adapters for format conversion:
networkx_adapter.py- NetworkX graphsigraph_adapter.py- igraph graphsgraphtool_adapter.py- graph-tool graphsGraphML_adapter.py- GraphML formatjson_adapter.py- JSON serializationdataframe_adapter.py- DataFrames (Narwhals)GraphDir_Parquet_adapter.py- Parquet directory formatSIF_adapter.py- Simple Interaction Formatcx2_adapter.py- CX2 format (Cytoscape)SBML_adapter.py- Systems Biology Markup Language (WIP)sbml_cobra_adapter.py- COBRA metabolic models (WIP)
- Lazy proxies with NetworkX, igraph, graph-tool
- History/versioning with snapshots and diffs
- Spectral methods (Laplacian, random walks, algebraic connectivity)
- AnnData-like API for bioinformatics
In [ ]:
Copied!