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.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
|
DataFrame
|
Edge table with required columns |
required |
|
mapping of str to int
|
Units per named node, for a node backed by several
neurons (DCell-style), exactly as in |
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 |
See Also
parse_layered : Rank a DAG into one incoming mask per layer; rejects cycles and self-loops. PackedLinear : One trainable weight per packed edge, for large node counts. PackedMultiheadAttention : 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)
>>> wide.input_index
(3,)