Backend lazy proxies — run igraph & NetworkX algorithms straight off an AnnNet¶
AnnNet is the source of truth; igraph, NetworkX, and graph-tool are algorithm libraries you
borrow on demand. G.ig, G.nx, and G.gt are lazy proxies: no backend graph exists
until you call an algorithm, then AnnNet converts once, caches the result (keyed on
the graph version), and rebuilds transparently if you mutate the graph.
So you call G.ig.betweenness() or G.nx.pagerank(G) by gene symbol and get an
answer — never holding a backend object yourself. The network below is the real
DoRothEA TF→target graph (~5k genes, ~15k signed interactions), fetched live from
OmniPath, so the notebook runs anywhere with no data files.
1. Build the graph¶
AnnNet reaches no knowledge base of its own, so the fetch and the build are two
steps. omnipath.interactions.Dorothea fetches the DoRothEA regulatory network
live from OmniPath, and omnipath_client.to_annnet turns that table into a
graph. The result is a directed, signed graph carrying real edge annotations
(is_stimulation, is_inhibition).
import annnet as an
import omnipath as op
import omnipath_client as oc
dorothea = op.interactions.Dorothea.get(genesymbols=True)
G = oc.to_annnet(
dorothea,
source_col='source_genesymbol',
target_col='target_genesymbol',
directed_col='is_directed',
edge_attr_cols=['is_stimulation', 'is_inhibition'],
)
print('nodes :', G.nv)
print('edges :', G.ne)
print('directed :', G.directed)
print('edge cols:', list(G.var.columns))
--------------------------------------------------------------------------- ModuleNotFoundError Traceback (most recent call last) Cell In[1], line 2 1 import annnet as an ----> 2 import omnipath as op 3 import omnipath_client as oc 5 dorothea = op.interactions.Dorothea.get(genesymbols=True) ModuleNotFoundError: No module named 'omnipath'
2. The proxy is lazy¶
Nothing converts until you ask. .backend() materialises and caches the backend
graph; call it again and you get the same object back.
ig_graph = G.ig.backend() # first call: convert AnnNet -> igraph
ig_graph_again = G.ig.backend() # second call: served from cache
print(type(ig_graph).__module__, type(ig_graph).__name__)
print('vcount / ecount :', ig_graph.vcount(), ig_graph.ecount())
print('same cached object :', ig_graph is ig_graph_again)
nx_graph = G.nx.backend()
print(type(nx_graph).__name__, '->', nx_graph.number_of_nodes(), 'nodes,',
nx_graph.number_of_edges(), 'edges')
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[2], line 1 ----> 1 ig_graph = G.ig.backend() # first call: convert AnnNet -> igraph 2 ig_graph_again = G.ig.backend() # second call: served from cache 4 print(type(ig_graph).__module__, type(ig_graph).__name__) NameError: name 'G' is not defined
3. igraph algorithms, driven from the AnnNet¶
Any igraph method or top-level function is reachable as G.ig.<name>(...). igraph
returns results by node index, so pair them with G.ig.peek_nodes() to read
them back as gene symbols.
# Betweenness flags regulatory bottlenecks. igraph returns a plain list keyed by
# node index; peek_nodes gives the matching gene symbols, in the same order.
bt = G.ig.betweenness()
names = G.ig.peek_nodes(G.nv)
top_bt = sorted(zip(names, bt), key=lambda s: -s[1])[:8]
print('Top betweenness hubs (master regulators):')
for gene, score in top_bt:
print(f' {gene:8s} {score:12.1f}')
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[3], line 3 1 # Betweenness flags regulatory bottlenecks. igraph returns a plain list keyed by 2 # node index; peek_nodes gives the matching gene symbols, in the same order. ----> 3 bt = G.ig.betweenness() 4 names = G.ig.peek_nodes(G.nv) 5 top_bt = sorted(zip(names, bt), key=lambda s: -s[1])[:8] NameError: name 'G' is not defined
# Community detection wants an undirected view: pass _ig_directed=False and the
# proxy caches a separate undirected conversion under its own key.
comm = G.ig.community_multilevel(_ig_directed=False)
print('communities :', len(comm))
print('modularity :', round(comm.modularity, 3))
print('largest 3 :', sorted((len(c) for c in comm), reverse=True)[:3])
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[4], line 3 1 # Community detection wants an undirected view: pass _ig_directed=False and the 2 # proxy caches a separate undirected conversion under its own key. ----> 3 comm = G.ig.community_multilevel(_ig_directed=False) 4 print('communities :', len(comm)) 5 print('modularity :', round(comm.modularity, 3)) NameError: name 'G' is not defined
4. Address nodes by gene symbol¶
Node IDs are gene symbols, so pass them straight in — the proxy maps them to
backend indices and maps results back. (Different labels? Point at the column with
_ig_label_field= / _nx_label_field=.)
# Shortest directed path length, MYC -> CDKN1A.
d = G.ig.distances(source='MYC', target='CDKN1A', mode='out')
print('igraph MYC -> CDKN1A hops:', d[0][0])
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[5], line 2 1 # Shortest directed path length, MYC -> CDKN1A. ----> 2 d = G.ig.distances(source='MYC', target='CDKN1A', mode='out') 3 print('igraph MYC -> CDKN1A hops:', d[0][0]) NameError: name 'G' is not defined
5. The same graph through NetworkX¶
G.nx.<name>(...) reaches any NetworkX algorithm; pass G where it expects a
graph. NetworkX outputs come back already labelled with gene symbols.
# Named shortest path — nodes come back as gene symbols.
path = G.nx.shortest_path(G, source='MYC', target='CDKN1A')
print('NetworkX MYC -> CDKN1A path:', path)
# PageRank as a {gene: score} dict.
pr = G.nx.pagerank(G)
print('\nTop PageRank genes:')
for gene, score in sorted(pr.items(), key=lambda kv: -kv[1])[:5]:
print(f' {gene:8s} {score:.5f}')
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[6], line 2 1 # Named shortest path — nodes come back as gene symbols. ----> 2 path = G.nx.shortest_path(G, source='MYC', target='CDKN1A') 3 print('NetworkX MYC -> CDKN1A path:', path) 5 # PageRank as a {gene: score} dict. NameError: name 'G' is not defined
6. And again through graph-tool¶
G.gt is the third lazy proxy. graph-tool groups its algorithms into namespaces,
so you call G.gt.<namespace>.<algo>(...) — e.g. G.gt.centrality.pagerank. Like
igraph, it hands back native graph-tool objects (property maps); read them back
through the graph's id vertex property. graph-tool ships via conda, not pip, so
this section self-skips when it isn't installed.
import importlib.util
if importlib.util.find_spec('graph_tool') is None:
print('graph-tool not installed — skipping (conda install -c conda-forge graph-tool).')
else:
gt_graph = G.gt.backend()
print(type(gt_graph).__module__, '->',
gt_graph.num_vertices(), 'vertices,', gt_graph.num_edges(), 'edges')
# Named shortest directed distance, MYC -> CDKN1A (addressed by gene symbol).
dist = G.gt.topology.shortest_distance(G, source='MYC', target='CDKN1A')
print('graph-tool MYC -> CDKN1A hops:', int(dist))
# PageRank -> a graph-tool VertexPropertyMap; map it back via the 'id' property.
pr = G.gt.centrality.pagerank(G)
ids = gt_graph.vp['id']
top_pr = sorted(((ids[v], pr[v]) for v in gt_graph.vertices()), key=lambda s: -s[1])[:5]
print('Top PageRank genes (graph-tool):')
for gene, score in top_pr:
print(f' {gene:8s} {score:.5f}')
graph-tool not installed — skipping (conda install -c conda-forge graph-tool).
7. Mutating the AnnNet invalidates the cache¶
The cache is keyed on the graph's version counter. Any structural edit bumps it, so the next proxy call reconverts — you never manage staleness by hand.
before = G.ig.backend()
G.add_nodes(['__PROBE__']) # structural mutation -> version bump
after = G.ig.backend()
print('same object after mutation :', before is after) # False: rebuilt
print('vcount before / after :', before.vcount(), '/', after.vcount())
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[8], line 1 ----> 1 before = G.ig.backend() 2 G.add_nodes(['__PROBE__']) # structural mutation -> version bump 3 after = G.ig.backend() NameError: name 'G' is not defined
8. Two things to know¶
- Lossy conversions warn. Hyperedges and multiple layers/slices have no plain
igraph/NetworkX equivalent; converting a graph that has them emits a
RuntimeWarningnaming what was dropped. The AnnNet is untouched — the backend view is just a projection. - Take collections, not scalar counts. The proxy maps integer outputs back to
node IDs, so a function returning a plain count has its int mapped to a node.
Use the collection-returning variant and
len()it yourself.
# DON'T rely on scalar-int returns through the proxy:
mapped = G.nx.number_weakly_connected_components(G)
print('number_weakly_connected_components ->', repr(mapped), ' # <- int mapped to a node id!')
# DO take the collection and measure it:
n_wcc = len(list(G.nx.weakly_connected_components(G)))
print('len(weakly_connected_components) ->', n_wcc, ' # correct')
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[9], line 2 1 # DON'T rely on scalar-int returns through the proxy: ----> 2 mapped = G.nx.number_weakly_connected_components(G) 3 print('number_weakly_connected_components ->', repr(mapped), ' # <- int mapped to a node id!') 5 # DO take the collection and measure it: NameError: name 'G' is not defined
Recap¶
G.ig/G.nx/G.gtare lazy — no backend graph until an algorithm is called.- The conversion is cached per graph version and reused across calls; mutating the AnnNet invalidates it transparently.
- Address nodes by their AnnNet IDs (or a label column). NetworkX maps outputs
back to them; igraph returns by index (pair it with
peek_nodes); graph-tool returns native property maps (read back via theidnode property). - AnnNet stays the single source of truth; igraph, NetworkX, and graph-tool are disposable, on-demand compute backends.