parse_adjacency ¶
parse_adjacency(
edgelist: DataFrame,
*,
widths: Mapping[str, int] | None = None,
) -> AdjacencySpec
Parse a source/target edgelist into an AdjacencySpec.
Prior-knowledge edges, such as genes into pathways, become one
alphabetical state vector plus packed source/target index
tuples, ready for PackedLinear or packed attention. Reach
for it when the graph has cycles or self-loops, or when a
shared-state loop is the update; parse_layered ranks a DAG
into one packed hop per layer instead. Nothing is ranked here,
and no (n, n) tensor is allocated. Every node is a single
unit, one neuron, unless widths makes it a block of
several.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
|
DataFrame
|
Edge table with required columns |
required |
|
mapping of str to int or numpy integer
|
Number of units per named node. A unit is one neuron: it
holds one number per sample. A node of width Widths change what the network can compute. A node of width 1 squeezes everything its parents send into one number, and every node it feeds sees that same number. So wider nodes matter where one number per node is too little:
The price is Omitted names, |
None
|
Returns:
| Type | Description |
|---|---|
AdjacencySpec
|
Frozen structure: every node name alphabetically with its
width, the packed unit pairs of every edge, and the unit
positions of the input and output nodes in that state
vector. Packed order is canonical, lexicographic by
|
Raises:
| Type | Description |
|---|---|
Kpnn2Error
|
If |
Warns:
| Type | Description |
|---|---|
UserWarning
|
If |
See Also
parse_layered : Rank a DAG into one incoming mask per layer; rejects cycles and self-loops. PackedLinear.from_adjacency : One trainable weight per packed edge, for large node counts. PackedMultiheadAttention.from_adjacency : Score only the packed pairs.
Notes
Self-loops are allowed here and rejected by parse_layered.
That is the only edgelist rule the two parsers disagree on;
every other validation is shared, so the messages match. A
self-loop takes its node out of both the input and the output
set, so an edgelist of only A -> A raises for having no
input node, as does a pure ring such as A -> B, B -> A.
Isolated nodes cannot appear: the node set is the union of
source and target.
The layout is your choice, not a property of the graph. A DAG
is valid input to both parsers, and neither inspects the graph
to decide which spec to return. A dense square would carry
1.0 at [target_index[i], source_index[i]], the
nn.Linear.weight orientation the layered hops also use
after to_mask(); spec.to_mask() materializes it.
Nothing here builds an nn.Module, unrolls time, or
re-injects inputs between steps: that update stays in user
forward() code.
Examples:
A feedback edge b -> a, which parse_layered would
reject, packed alongside the forward edges:
>>> import pandas as pd
>>> import kpnn2
>>> edgelist = pd.DataFrame(
... {
... "source": ["x", "a", "b", "a"],
... "target": ["a", "b", "a", "y"],
... }
... )
>>> spec = kpnn2.parse_adjacency(edgelist)
>>> spec.nodes
('a', 'b', 'x', 'y')
>>> spec.input_nodes, spec.input_index
(('x',), (2,))
>>> spec.source_index, spec.target_index
((0, 0, 1, 2), (1, 3, 0, 0))
>>> spec.to_mask()[0, 1].item()
1.0
A node can own several units, as in parse_layered. Its
named edges become blocks of unit pairs:
>>> wide = kpnn2.parse_adjacency(
... edgelist,
... widths={"a": 2},
... )
>>> wide.node_widths, wide.state_dim
((2, 1, 1, 1), 5)
>>> wide.node_units("a")
slice(0, 2, None)
>>> wide.edge_location("a", "b")
(0, 1)
x moves from unit 2 to unit 3, because a now takes two:
>>> wide.input_index
(3,)