Computation and storage¶
Which nodes lie on shortest paths through the imported network? Project the prior to NetworkX, compute betweenness, attach the result by node ID, and save both a native graph and an exchange file for Cytoscape.
Requires annnet[polars,storage,networkx] and the guide data
in a data/ directory beside this notebook. The result describes this teaching
topology, not a biological ranking.
from pathlib import Path
from tempfile import TemporaryDirectory
import annnet as an
import networkx as nx
import polars as pl
interactions = pl.read_csv('data/interactions.csv')
anet = an.from_edge_frame(interactions, edge_id='edge_id', sign='effect', slice='prior')
anet.attrs.backend = 'polars'
What an export represents¶
A backend graph is a projection of one annnet context. Binary graph libraries cannot directly express every hyperedge, node-layer value, or named slice. Choose the algorithm input before you interpret its output:
| Hyperedge mode | Representation and consequence |
|---|---|
skip |
Omits the relation from computation. |
reify / star |
Adds a relation node; paths and node statistics include it. |
expand / clique |
Adds pairwise connections; the pairs alone lose the original grouping. |
Adapters have defaults: the NetworkX and
igraph adapters use skip. With explicit
participant coefficients, expansion fails unless you accept the loss with
coefficients='drop'. Also decide whether to keep parallel edges, how to
aggregate them if not, and how to represent mixed direction or layers. An
export can support reconstruction and still be the wrong input for an
algorithm.
Make the algorithm input explicit¶
Select the prior slice and state the hyperedge policy. This dataset contains
only binary edges, so skip removes none. If you later introduce hyperedges,
reconsider the projection. Keep the returned manifest for re-import.
nx_graph, manifest = an.to_nx(anet, slice='prior', hyperedges='skip')
print(type(nx_graph).__name__, nx_graph.number_of_nodes(), nx_graph.number_of_edges())
MultiDiGraph 10 11
The projected graph preserves the parallel evidence records. Different algorithms have different support for multigraphs and directed graphs; establish that the representation fits the method before interpreting its result.
Here weight=None counts hops. Neither confidence nor an inhibitory sign is a
path length. We leave both out of this calculation deliberately.
scores = nx.betweenness_centrality(nx_graph, weight=None)
anet.attrs.update('nodes', {node: {'betweenness': value} for node, value in scores.items()})
anet.uns['analysis'] = {
'method': 'networkx.betweenness_centrality',
'networkx_version': nx.__version__,
'slice': 'prior',
'weight': None,
'hyperedges': 'skip',
}
anet.attrs.table('nodes').sort('betweenness', descending=True)
| node_id | betweenness |
|---|---|
| str | f64 |
| "RAS" | 0.166667 |
| "RAF" | 0.166667 |
| "SOS1" | 0.138889 |
| "MEK" | 0.138889 |
| "GRB2" | 0.083333 |
| "ERK" | 0.083333 |
| "AKT" | 0.013889 |
| "EGFR" | 0.0 |
| "DUSP6" | 0.0 |
| "FOXO3" | 0.0 |
A higher score means a node participates in more shortest paths under these settings. It does not estimate activity or establish importance in an experiment. The update joins scores by node ID, while the graph metadata records how they were obtained.
Use a cached accessor when convenient¶
anet.nx combines projection and dispatch. For this call, explicitly select the
same slice. Clear the accessor after edits before requesting a new projection:
current automatic invalidation does not cover every removal or attribute change.
Fresh an.to_nx calls avoid that cache.
anet.nx.clear()
via_accessor = anet.nx.betweenness_centrality(
anet, weight=None, _nx_slice='prior', _nx_hyperedge='skip'
)
via_accessor == scores
True
Save the annotated analysis and export for Cytoscape¶
The native object is the analysis artifact. CX2/)
is an exchange format for Cytoscape; reify makes
hyperedges explicit as relation nodes for that consumer. annnet writes the
reconstruction metadata into the CX2 output.
The example writes into a temporary directory, so you can run it again. To keep the files, use your own analysis directory.
with TemporaryDirectory() as directory:
directory = Path(directory)
anet.write(directory / 'analysis.annnet')
an.to_cx2(anet, directory / 'analysis.cx2', hyperedges='reify')
restored = an.read(directory / 'analysis.annnet')
print('Files:', sorted(path.name for path in directory.iterdir()))
print('Analysis metadata:', restored.uns['analysis'])
restored.attrs.table('nodes').sort('betweenness', descending=True)
Files: ['analysis.annnet', 'analysis.cx2']
Analysis metadata: {'method': 'networkx.betweenness_centrality', 'networkx_version': '3.5', 'slice': 'prior', 'weight': None, 'hyperedges': 'skip'}
| node_id | betweenness |
|---|---|
| str | f64 |
| "RAS" | 0.166667 |
| "RAF" | 0.166667 |
| "SOS1" | 0.138889 |
| "MEK" | 0.138889 |
| "GRB2" | 0.083333 |
| "ERK" | 0.083333 |
| "AKT" | 0.013889 |
| "EGFR" | 0.0 |
| "DUSP6" | 0.0 |
| "FOXO3" | 0.0 |
For a backend round trip, keep both values returned by the adapter and use
an.from_nx(nx_graph, manifest). For GraphML, CSV, and other formats that use a
sidecar, keep the companion file as well. An external format alone may not
retain every annotation, slice, or richer relationship.
Persistence and reconstruction channels¶
Use the native .annnet format to resume work in annnet. JSON/NDJSON,
Parquet, and CX2 also keep reconstruction
extensions inside their output. Runtime graph adapters return a separate
manifest; GraphML, GEXF,
SIF, CSV, and Excel can write a sidecar file. Keep that metadata with the export.
Sidecars contain a checksum, and annnet refuses a sidecar that does not match
the main file.
SBML and PyTorch Geometric serve domain-specific tasks and have different limits. The format comparison in the API reference shows what each route can keep.
The native writer materializes attached measurement arrays only for existing
node-layer placements. Use attached='error' to refuse that operation, or
attached='drop' to leave out the backings. Save an external assay separately
when it contains values outside those placements. The writer can reconstruct
matrices from structure records; matrix=True also saves the matrix, which
makes loading faster and the file larger.
History, snapshots, and reproducibility¶
History records some mutating calls, not every edit. Current hooks can miss removals, attribute updates, and slice creation. Snapshots compare sets of node, edge, and slice IDs. They do not capture a full revision or support replay.
The next example demonstrates the distinction: an attribute-only edit changes the graph while leaving a membership diff empty; removing an edge changes that diff. Retain the input files, analysis code, parameters, package versions, and saved graph alongside any history export.
before = anet.history.snapshot('before_annotation')
anet.attrs.update('nodes', {'ERK': {'note': 'reviewed'}})
print('Empty diff after the attribute edit:', anet.history.diff(before).is_empty())
anet.remove_edges('egfr_grb2_alt')
print(anet.history.diff(before).summary())
Empty diff after the attribute edit: True Diff: before_annotation - current Nodes: +0 added, 0 removed Edges: +0 added, 1 removed slices: +0 added, 0 removed
anet.history.mark('review_complete')
with TemporaryDirectory() as directory:
event_count = anet.history.export(str(Path(directory) / 'history.ndjson'))
print('Exported events:', event_count)
Exported events: 3
A marker gives the event log a useful checkpoint label, but does not fill gaps in mutation coverage. A fresh adapter conversion or an explicit accessor clear is also needed before recomputing on a graph edited after an earlier projection.
API and related topics¶
Backend accessors · Format comparison · Native format · History API