Graph model and matrices¶
Import a network, learn what its identities and relationships mean, and read its numerical representations. This notebook covers binary edges, direction, parallel evidence, hyperedges, coefficients, and the matrix mappings used in computation. Small constructed examples make the less common shapes visible.
Requires annnet[polars]. The guide data are synthetic
teaching data: put the CSV files in a data/ directory beside this notebook.
Start with the quickstart if this is your first use of
annnet.
import annnet as an
import numpy as np
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'
anet.summary()
AnnNet summary: 10 node(s), 11 edge(s), 10 placement(s) directed default: True slices (2): default (0n/0e), prior (10n/11e) attrs: edges: ['sign', 'confidence', 'evidence'] edges (computed): binary=11; directed=11, undirected=0
Identities and relationships¶
In the tutorial data, egfr_grb2 and egfr_grb2_alt both connect EGFR to GRB2.
They remain separate edges because they have different IDs. This is useful
when two evidence records or mechanisms share endpoints: an analysis can select
or annotate each one independently.
Supply the source's edge-ID column when it provides suitable identifiers.
Without one, from_edge_frame derives IDs from each row's content, including
the imported annotations and context. Reordering identical rows preserves their
derived IDs; changing a recorded value can change the ID. Row-derived IDs are
therefore unsuitable as permanent identifiers for records whose contents change.
anet.N addresses ordinary nodes and anet.E addresses structural edges.
anet.E.at(edge_id) returns an edge record containing its endpoints, kind,
direction, and weight. In a flat graph an endpoint is a node ID. In a layered
graph it is a placement, (node_id, layer_coordinate).
| Relationship | Meaning |
|---|---|
| Directed binary edge | One source connected to one target. |
| Undirected binary edge | A connection with no direction. |
| Parallel edges | Distinct edge IDs connecting the same endpoints. |
| Self-loop | The same entity is the source and target. |
| Undirected hyperedge | One relationship involving a set of members. |
| Directed hyperedge | One relationship connecting a source group to a target group. |
Direction can be specified per edge. The graph's directed setting supplies a
default, so one container can mix directed and undirected relationships.
anet.attrs.table('edges', derived=True).filter(
(pl.col('source') == 'EGFR') & (pl.col('target') == 'GRB2')
).select('edge_id', 'source', 'target', 'confidence')
| edge_id | source | target | confidence |
|---|---|---|---|
| str | str | str | f64 |
| "egfr_grb2" | "EGFR" | "GRB2" | 0.95 |
| "egfr_grb2_alt" | "EGFR" | "GRB2" | 0.65 |
Both rows survive import. Repeated endpoints do not imply a duplicate identity.
For incremental imports, an.add_edges_from_frame provides explicit ID-conflict
policies: error, skip, replace, and rename. With error, ID conflicts
are checked before inserting the batch. Preserve stable source IDs when results
must still refer to records after their annotations change.
When a pair of endpoints is insufficient¶
A complex or a reaction may involve several participants in one relation. Representing it as independent pairs loses the fact that those participants belong together. A hyperedge retains that grouping:
reaction_anet = an.Graph()
reaction_anet.add_edges(['A', 'B'], ['C'], edge_id='reaction', directed=True)
'reaction'
Here the first group is the source and the second is the target. Some legacy
records call these groups head and tail, respectively. Prefer source and
target when describing direction; the legacy names can be confused with other
hypergraph conventions.
If another relation needs to refer to the reaction itself, the edge can be
registered with as_entity=True. An edge entity may also be a placeholder with
an identity but no participants yet. It can be an endpoint, while the placeholder
itself contributes no structural edge column. This is a specialized modeling
feature; ordinary interaction tables do not require it.
reaction_anet.attrs.backend = 'polars'
reaction_anet.attrs.table('edges', derived=True, layout='incidences')
| edge_id | position | entity_id | entity_kind | layer | layer_id | role | coefficient | kind | directed |
|---|---|---|---|---|---|---|---|---|---|
| str | i64 | str | str | null | null | str | f64 | str | bool |
| "reaction" | 0 | "A" | "node" | null | null | "source" | 1.0 | "hyper" | true |
| "reaction" | 1 | "B" | "node" | null | null | "source" | 1.0 | "hyper" | true |
| "reaction" | 2 | "C" | "node" | null | null | "target" | -1.0 | "hyper" | true |
Each row identifies a participant and its role. The hyperedge is one relation
with three participants. Its source group is {A, B} and its target is {C};
projecting it into pairs would require a decision about that grouping.
Structure and numerical representations¶
annnet stores each edge's participants, their roles, and coefficients. Sparse matrices are built from that structure when requested. Several matrices serve different purposes:
| Property | Rows × columns | Contents |
|---|---|---|
anet.B |
Entities × binary/node-edge relations | Signed coefficient incidence for those edge kinds. |
anet.H |
Entities × hyperedges | Unsigned membership, one contribution per participant role. |
anet.S |
Entities × all structural edges | Stored incidence coefficients, including hyperedges. |
anet.A |
Entities × entities | Weighted adjacency; hyperedges and one-sided edges are excluded by default. |
anet.L |
Entities × entities | D - A, where D contains row sums of anet.A. |
For an ordinary directed edge of weight w, incidence contributes +w at
the source and -w at the target. An undirected edge contributes +w on both
sides. Explicit participant coefficients can override these values. The stored
coefficient sign alone is therefore insufficient to infer endpoint roles.
A directed self-loop has two roles on the same entity. Its two incidence contributions cancel, while its adjacency entry remains on the diagonal. A zero column or a column summing to zero is not a reliable self-loop test; read the endpoints. Ordinary directed edges also have column sum zero.
Weights are already present in coefficient incidence. Do not multiply by a
second weight matrix and assume the result equals anet.A or anet.L. In particular,
anet.L uses the adjacency definition above, not the product of the unsigned
incidence matrix with its transpose.
Use anet.matrices.signed() or another named matrix method when you need the
identity-to-position mappings alongside the matrix. Positions describe that
materialization and can change after edits. Keep IDs in annotations and saved
results.
print('Binary incidence:', reaction_anet.B.shape)
print('Hyperedge membership:', reaction_anet.H.toarray())
print('Coefficient incidence:', reaction_anet.S.toarray())
Binary incidence: (3, 0) Hyperedge membership: [[1.] [1.] [1.]] Coefficient incidence: [[-1.] [ 1.] [ 1.]]
Participant coefficients¶
A reaction-style model often uses negative coefficients for consumed species
and positive coefficients for produced species. This differs from the default
incidence sign convention above. Set coefficients explicitly and continue to
read roles from the edge record. The coefficient matrix is derived state;
use set_edge_coeffs rather than modifying a returned sparse matrix.
reaction_anet.set_edge_coeffs('reaction', {'A': -1.0, 'B': -2.0, 'C': 3.0})
S = reaction_anet.matrices.signed()
print('Rows:', S.entity_of_row)
print('Columns:', S.edge_of_column)
print(S.matrix.toarray())
print('Source group:', sorted(reaction_anet.E.at('reaction').source))
Rows: (('C', ('_',)), ('B', ('_',)), ('A', ('_',)))
Columns: ['reaction']
[[ 3.]
[-2.]
[-1.]]
Source group: ['A', 'B']
For rates $v$, the product $Sv$ is the per-entity balance under the coefficients you supplied. A zero balance for an internal participant can express steady state; a column summing to zero does not prove chemical mass conservation. That requires the species' composition and a chemically balanced model.
rate = np.array([2.0])
print('Balance at rate 2:', S.matrix @ rate)
Balance at rate 2: [ 6. -4. -2.]
Self-loops retain structure even when incidence cancels¶
Add a directed self-loop and use the matrix's own identity maps to inspect it. The loop has a zero coefficient column and a nonzero adjacency entry.
reaction_anet.add_edges('A', 'A', edge_id='loop', weight=2.0, directed=True)
S = reaction_anet.matrices.signed()
A = reaction_anet.matrices.adjacency()
loop_column = S.column_of_edge['loop']
a_row = A.row_of_entity[('A', ('_',))]
print('Loop coefficient column:', S.matrix[:, [loop_column]].toarray().ravel())
print('Loop adjacency entry:', A.matrix[a_row, a_row])
Loop coefficient column: [0. 0. 0.] Loop adjacency entry: 2.0
An edge as an endpoint¶
A placeholder edge entity allows another relation to refer to a named object before its participants are defined. It occupies entity space but contributes no structural incidence column of its own.
reaction_anet.add_edges(edge_id='regulatory_event', as_entity=True)
reaction_anet.add_edges('regulatory_event', 'C', edge_id='event_to_c', as_entity=True)
reaction_anet.E.at('event_to_c')
EdgeView(edge_id='event_to_c', kind='node_edge', source=frozenset({'regulatory_event'}), target=frozenset({'C'}), members=frozenset({'regulatory_event', 'C'}), weight=1.0, directed=True)
Inspect local connectivity¶
On the imported binary network, traversal helpers answer questions without a
backend conversion. These queries concern connectivity; an inhibitory sign
does not reverse the direction.
print('EGFR successors:', sorted(anet.successors('EGFR')))
print('ERK predecessors:', sorted(anet.predecessors('ERK')))
EGFR successors: ['AKT', 'GRB2'] ERK predecessors: ['DUSP6', 'MEK']
For mixed or hypergraph data, check the role semantics of the traversal you need before treating its result as a conventional simple-graph neighborhood. Continue with Annotations, selections, and views to query the imported network by its evidence and work with a selected graph.