Quickstart: Build and Inspect an AnnNet¶
This notebook introduces the current core API.
The main design points to keep in mind are:
AnnNetstores topology as an incidence matrix- vertices and edges are created through a small number of canonical methods
add_edge(...)is the central structural constructor: binary edges, hyperedges, stoichiometric hyperedges, multilayer supra-node edges, and edge-entities all pass through this method
This notebook focuses on understanding add_edge(...) well, because once that method is clear the rest of the graph API is much easier to reason about.
import sys
from pathlib import Path
repo_root = Path.cwd()
if not (repo_root / 'annnet').exists():
for parent in repo_root.parents:
if (parent / 'annnet').exists():
repo_root = parent
break
if str(repo_root) not in sys.path:
sys.path.insert(0, str(repo_root))
import annnet as an
Create a graph¶
directed=None means the graph does not force a global default. Individual edges can still be directed or undirected.
G = an.AnnNet(directed=None, study='toy signaling network')
G
<annnet.core.graph.AnnNet at 0x75537d116120>
Add vertices¶
Use add_vertex(...) for one vertex and add_vertices_bulk(...) for many. The two methods are meant to share the same semantics; the bulk version is just the batched path.
G.add_vertex('EGFR', family='RTK', layer=None)
G.add_vertex('GRB2', family='adapter')
G.add_vertices_bulk(
[
('SOS1', {'family': 'exchange_factor'}),
{'vertex_id': 'RAS', 'family': 'small_gtpase'},
'RAF1',
]
)
print('vertices:', G.vertices())
print('nv:', G.nv)
G.obs
vertices: ['EGFR', 'GRB2', 'SOS1', 'RAS', 'RAF1'] nv: 5
| vertex_id | family |
|---|---|
| str | str |
| "EGFR" | "RTK" |
| "GRB2" | "adapter" |
| "SOS1" | "exchange_factor" |
| "RAS" | "small_gtpase" |
| "RAF1" | null |
add_edge(...) for ordinary binary edges¶
For a binary edge, src and tgt are single endpoints.
Important parameters here:
edge_id: stable edge identifierdirected: edge-level directionalityweight: default incidence magnitudeas_entity: whether the edge should also get its own entity row and become connectable as an endpoint later
Directed binary edges write a positive coefficient at the source row and a negative coefficient at the target row. Undirected binary edges write positive coefficients at both rows.
G.add_edge('EGFR', 'GRB2', edge_id='e_activation', directed=True, weight=2.0, relation='activates')
G.add_edge('GRB2', 'SOS1', edge_id='e_binding', directed=False, relation='binds')
print('edges:', G.edges())
print('ne:', G.ne)
print('edge list:', G.edge_list())
G.var
edges: ['e_activation', 'e_binding']
ne: 2
edge list: [('EGFR', 'GRB2', 'e_activation', 2.0), ('GRB2', 'SOS1', 'e_binding', 1.0)]
| edge_id | relation |
|---|---|
| str | str |
| "e_activation" | "activates" |
| "e_binding" | "binds" |
add_edge(...) for hyperedges¶
Hyperedges use collection-valued endpoints.
There are two main shapes:
- undirected hyperedge:
src=[...],tgt=None - directed hyperedge:
src=[head members],tgt=[tail members]
Internally the edge is still one incidence column. The difference is the pattern of coefficients written into that column.
G.add_edge(
src=['EGFR', 'GRB2', 'SOS1'], edge_id='h_complex', directed=False, process='complex assembly'
)
G.add_edge(
src=['RAS'], tgt=['RAF1'], edge_id='h_signal_transfer', directed=True, process='signal transfer'
)
G.edges_view()
| edge_id | kind | directed | global_weight | source | target | edge_type | head | tail | members | relation | process | effective_weight |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| str | str | bool | f64 | str | str | str | list[str] | list[str] | list[str] | str | str | f64 |
| "e_activation" | "binary" | true | 2.0 | "EGFR" | "GRB2" | "binary" | null | null | null | "activates" | null | 2.0 |
| "e_binding" | "binary" | false | 1.0 | "GRB2" | "SOS1" | "binary" | null | null | null | "binds" | null | 1.0 |
| "h_complex" | "hyper" | false | 1.0 | "EGFR|GRB2|SOS1" | null | null | null | null | ["EGFR", "GRB2", "SOS1"] | null | "complex assembly" | 1.0 |
| "h_signal_transfer" | "hyper" | true | 1.0 | "RAS" | "RAF1" | null | ["RAS"] | ["RAF1"] | null | null | "signal transfer" | 1.0 |
add_edge(...) for stoichiometric incidence¶
If src and tgt are dictionaries, the values are interpreted as explicit incidence coefficients.
This is the strongest form of the API because it writes the column directly instead of using one global weight.
Use this when the edge is really an incidence pattern, not just a binary relation.
G.add_edge(
src={'EGFR': 2.0, 'GRB2': 1.0},
tgt={'SOS1': 1.0},
edge_id='h_stoich',
directed=True,
process='stoichiometric transfer',
)
print('edge ids between EGFR and GRB2:', G.get_edge_ids('EGFR', 'GRB2'))
print(G.edges_view())
edge ids between EGFR and GRB2: ['e_activation'] shape: (5, 13) ┌────────────┬────────┬──────────┬────────────┬───┬────────────┬───────────┬───────────┬───────────┐ │ edge_id ┆ kind ┆ directed ┆ global_wei ┆ … ┆ members ┆ relation ┆ process ┆ effective │ │ --- ┆ --- ┆ --- ┆ ght ┆ ┆ --- ┆ --- ┆ --- ┆ _weight │ │ str ┆ str ┆ bool ┆ --- ┆ ┆ list[str] ┆ str ┆ str ┆ --- │ │ ┆ ┆ ┆ f64 ┆ ┆ ┆ ┆ ┆ f64 │ ╞════════════╪════════╪══════════╪════════════╪═══╪════════════╪═══════════╪═══════════╪═══════════╡ │ e_activati ┆ binary ┆ true ┆ 2.0 ┆ … ┆ null ┆ activates ┆ null ┆ 2.0 │ │ on ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ │ │ e_binding ┆ binary ┆ false ┆ 1.0 ┆ … ┆ null ┆ binds ┆ null ┆ 1.0 │ │ h_complex ┆ hyper ┆ false ┆ 1.0 ┆ … ┆ ["EGFR", ┆ null ┆ complex ┆ 1.0 │ │ ┆ ┆ ┆ ┆ ┆ "GRB2", ┆ ┆ assembly ┆ │ │ ┆ ┆ ┆ ┆ ┆ "SOS1"] ┆ ┆ ┆ │ │ h_signal_t ┆ hyper ┆ true ┆ 1.0 ┆ … ┆ null ┆ null ┆ signal ┆ 1.0 │ │ ransfer ┆ ┆ ┆ ┆ ┆ ┆ ┆ transfer ┆ │ │ h_stoich ┆ hyper ┆ true ┆ 1.0 ┆ … ┆ null ┆ null ┆ stoichiom ┆ 1.0 │ │ ┆ ┆ ┆ ┆ ┆ ┆ ┆ etric ┆ │ │ ┆ ┆ ┆ ┆ ┆ ┆ ┆ transfer ┆ │ └────────────┴────────┴──────────┴────────────┴───┴────────────┴───────────┴───────────┴───────────┘
add_edge(..., as_entity=True) for connectable edges¶
An edge can also live in the entity row space. That makes vertex→edge and edge→edge constructions possible.
There are two cases:
- create a normal structural edge and also register it as an entity
- create an edge-entity placeholder with no structural incidence yet
G.add_edge('SOS1', 'RAS', edge_id='e_bridge', directed=True, as_entity=True, relation='bridge')
G.add_edge(edge_id='meta_edge', as_entity=True, role='placeholder edge-entity')
G.add_edge('meta_edge', 'RAF1', edge_id='e_meta_to_vertex', directed=True)
print('entity kind of e_bridge:', G.idx.entity_type('e_bridge'))
print('entity kind of meta_edge:', G.idx.entity_type('meta_edge'))
entity kind of e_bridge: edge entity kind of meta_edge: edge
Matrix and indexing view¶
AnnNet is easiest to understand once you look at the matrix and the row/column translation side by side.
X = G.X()
print(type(X).__name__)
print('shape:', X.shape)
print('entity -> row:', {eid: G.idx.entity_to_row(eid) for eid in ['EGFR', 'GRB2', 'e_bridge']})
print('edge -> col:', {eid: G.idx.edge_to_col(eid) for eid in G.edges()[:4]})
dok_matrix
shape: (8, 8)
entity -> row: {'EGFR': 0, 'GRB2': 1, 'e_bridge': 5}
edge -> col: {'e_activation': 0, 'e_binding': 1, 'h_complex': 2, 'h_signal_transfer': 3}
Views and summaries¶
The graph-facing read helpers are intentionally lightweight. Use them to inspect what was built before moving to slices, multilayer state, or adapters.
print('shape:', G.shape)
print('graph attrs:', G.uns)
print('vertices view:')
print(G.vertices_view())
print('edges view:')
print(G.edges_view())
shape: (5, 7)
graph attrs: {'study': 'toy signaling network'}
vertices view:
shape: (5, 2)
┌───────────┬─────────────────┐
│ vertex_id ┆ family │
│ --- ┆ --- │
│ str ┆ str │
╞═══════════╪═════════════════╡
│ EGFR ┆ RTK │
│ GRB2 ┆ adapter │
│ SOS1 ┆ exchange_factor │
│ RAS ┆ small_gtpase │
│ RAF1 ┆ null │
└───────────┴─────────────────┘
edges view:
shape: (7, 14)
┌─────────────┬────────┬──────────┬─────────────┬───┬───────────┬─────────────┬──────┬─────────────┐
│ edge_id ┆ kind ┆ directed ┆ global_weig ┆ … ┆ relation ┆ process ┆ role ┆ effective_w │
│ --- ┆ --- ┆ --- ┆ ht ┆ ┆ --- ┆ --- ┆ --- ┆ eight │
│ str ┆ str ┆ bool ┆ --- ┆ ┆ str ┆ str ┆ str ┆ --- │
│ ┆ ┆ ┆ f64 ┆ ┆ ┆ ┆ ┆ f64 │
╞═════════════╪════════╪══════════╪═════════════╪═══╪═══════════╪═════════════╪══════╪═════════════╡
│ e_activatio ┆ binary ┆ true ┆ 2.0 ┆ … ┆ activates ┆ null ┆ null ┆ 2.0 │
│ n ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ │
│ e_binding ┆ binary ┆ false ┆ 1.0 ┆ … ┆ binds ┆ null ┆ null ┆ 1.0 │
│ h_complex ┆ hyper ┆ false ┆ 1.0 ┆ … ┆ null ┆ complex ┆ null ┆ 1.0 │
│ ┆ ┆ ┆ ┆ ┆ ┆ assembly ┆ ┆ │
│ h_signal_tr ┆ hyper ┆ true ┆ 1.0 ┆ … ┆ null ┆ signal ┆ null ┆ 1.0 │
│ ansfer ┆ ┆ ┆ ┆ ┆ ┆ transfer ┆ ┆ │
│ h_stoich ┆ hyper ┆ true ┆ 1.0 ┆ … ┆ null ┆ stoichiomet ┆ null ┆ 1.0 │
│ ┆ ┆ ┆ ┆ ┆ ┆ ric ┆ ┆ │
│ ┆ ┆ ┆ ┆ ┆ ┆ transfer ┆ ┆ │
│ e_bridge ┆ binary ┆ true ┆ 1.0 ┆ … ┆ bridge ┆ null ┆ null ┆ 1.0 │
│ e_meta_to_v ┆ binary ┆ true ┆ 1.0 ┆ … ┆ null ┆ null ┆ null ┆ 1.0 │
│ ertex ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ │
└─────────────┴────────┴──────────┴─────────────┴───┴───────────┴─────────────┴──────┴─────────────┘