Slices and Multilayer Workflows¶
Slices and multilayer coordinates solve different problems.
- slices are named contexts over one graph
- layers are explicit structural coordinates in a multilayer space
This notebook shows both, using the current manager-first API.
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
G = an.AnnNet(directed=True)
G.add_vertices_bulk(['TP53', 'MDM2', 'BAX', 'ATM'])
G.add_edge('ATM', 'TP53', edge_id='e_damage')
G.add_edge('TP53', 'BAX', edge_id='e_apoptosis')
G.add_edge('TP53', 'MDM2', edge_id='e_feedback')
'e_feedback'
Slice manager¶
Use G.slices for named graph contexts. Slice membership is an overlay over the same structural graph.
G.slices.add('baseline', note='untreated')
G.slices.add('treated', note='DNA damage')
G.slices.active = 'baseline'
G.add_edge_to_slice('baseline', 'e_feedback')
G.add_edge_to_slice('treated', 'e_damage')
G.add_edge_to_slice('treated', 'e_apoptosis')
print('active:', G.slices.active)
print('all slices:', G.slices.list(include_default=True))
print('treated info:', G.slices.info('treated'))
print('treated edges:', G.slices.edges('treated'))
active: baseline
all slices: ['default', 'baseline', 'treated']
treated info: {'vertices': set(), 'edges': {'e_damage', 'e_apoptosis'}, 'attributes': {'note': 'DNA damage'}}
treated edges: {'e_damage', 'e_apoptosis'}
Declare aspects before materializing full layer state¶
G.layers.set_aspects([...]) declares the dimensions of the multilayer graph. It does not force you to materialize every elementary layer immediately.
M = an.AnnNet(directed=True)
M.layers.set_aspects(['condition'])
print('aspects:', M.layers.aspects())
print('elementary layers right after declaration:', M.layers.elementary_layers())
aspects: ['condition']
elementary layers right after declaration: {'condition': ['_']}
Materialize elementary layers explicitly¶
Use G.layers.set_elementary_layers(...) when you want concrete elementary layer values in the registry.
M.layers.set_elementary_layers({'condition': ['baseline', 'treated']})
print('elementary layers:', M.layers.elementary_layers())
elementary layers: {'condition': ['baseline', 'treated']}
Add multilayer vertices¶
Once the graph is layered, explicit structural placement matters.
For vertices, omitting layer= uses the placeholder coordinate and emits a warning. For edges, bare endpoints are not accepted once the graph is multilayered; you must use explicit supra-node tuples.
for vid in ['TP53', 'MDM2', 'BAX', 'ATM']:
M.add_vertex(vid, layer=('baseline',), role='gene')
M.add_vertex(vid, layer=('treated',), role='gene')
M.layers.set_vertex_layer_attrs('TP53', ('treated',), activity='high', status='activated')
print('TP53 layers:', M.layers.vertex_layers('TP53'))
print('TP53 treated attrs:', M.layers.vertex_layer_attrs('TP53', ('treated',)))
TP53 layers: [('treated',), ('baseline',)]
TP53 treated attrs: {'activity': 'high', 'status': 'activated'}
Add multilayer edges through add_edge(...)¶
This is the key point: multilayer binary edges are still created through add_edge(...). The difference is that endpoints are explicit supra-nodes.
The edge role is inferred from the endpoints:
- same layer, different vertices -> intra-layer
- different layers, different vertices -> inter-layer
- same vertex across layers -> coupling
M.add_edge(('ATM', ('treated',)), ('TP53', ('treated',)), edge_id='e_intra', weight=2.0)
M.add_edge(('ATM', ('baseline',)), ('TP53', ('treated',)), edge_id='e_inter', weight=0.5)
M.add_edge(('TP53', ('baseline',)), ('TP53', ('treated',)), edge_id='e_coupling', weight=1.0)
print(M.edges_view())
shape: (3, 11) ┌────────────┬──────────┬──────────┬───────────┬───┬───────────┬───────────┬───────────┬───────────┐ │ edge_id ┆ kind ┆ directed ┆ global_we ┆ … ┆ head ┆ tail ┆ members ┆ effective │ │ --- ┆ --- ┆ --- ┆ ight ┆ ┆ --- ┆ --- ┆ --- ┆ _weight │ │ str ┆ str ┆ bool ┆ --- ┆ ┆ list[str] ┆ list[str] ┆ list[str] ┆ --- │ │ ┆ ┆ ┆ f64 ┆ ┆ ┆ ┆ ┆ f64 │ ╞════════════╪══════════╪══════════╪═══════════╪═══╪═══════════╪═══════════╪═══════════╪═══════════╡ │ e_intra ┆ intra ┆ true ┆ 2.0 ┆ … ┆ null ┆ null ┆ null ┆ 2.0 │ │ e_inter ┆ inter ┆ true ┆ 0.5 ┆ … ┆ null ┆ null ┆ null ┆ 0.5 │ │ e_coupling ┆ coupling ┆ true ┆ 1.0 ┆ … ┆ null ┆ null ┆ null ┆ 1.0 │ └────────────┴──────────┴──────────┴───────────┴───┴───────────┴───────────┴───────────┴───────────┘
Layer-derived operators¶
The layers manager exposes algebra on layer tuples and supra-matrix constructions.
print('realized layers:', M.layers.layers())
print('baseline vertex set:', M.layers.vertex_set(('baseline',)))
print(
'baseline edge set:',
M.layers.edge_set(('baseline',), include_inter=True, include_coupling=True),
)
SI = M.layers.supra_incidence(
[('baseline',), ('treated',)], include_inter=True, include_coupling=True
)
if isinstance(SI, tuple):
supra_incidence_matrix = SI[0]
print('supra incidence extra return items:', len(SI) - 1)
else:
supra_incidence_matrix = SI
print('supra incidence shape:', supra_incidence_matrix.shape)
realized layers: [('baseline',), ('treated',)]
baseline vertex set: {'MDM2', 'TP53', 'BAX', 'ATM'}
baseline edge set: {'e_coupling', 'e_inter'}
supra incidence extra return items: 2
supra incidence shape: (0, 0)