Skip to content

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

edgelist

DataFrame

Edge table with required columns source and target, one row per directed edge in the direction of computation. Node names are text: a str is kept, and an integer is converted with str(...), so 1 and "1" name the same node. A float or bool is converted too but warns, since 1.0 becomes "1.0"; any other value raises. Extra columns are ignored. The frame is read, never modified.

required

widths

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 k is a block of k units, a contiguous slice of the state vector (node_units), so it can carry k numbers instead of one. An edge A -> B then connects every unit of A to every unit of B, its (k_B, k_A) block of unit pairs, which is k_B * k_A weights instead of one.

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:

  • a hidden node that combines many parents and should pass on more than one summary of them
  • a node in a feedback loop that should carry more than one number from one update to the next
  • capacity that grows with the node, such as a width set by its number of parents
  • an output that is several numbers, such as class scores or the parameters of a distribution

The price is k_B * k_A weights per edge, and a score per node has to combine the scores of its units. A wide node also shifts every node after it in the state vector, so read positions from node_units, unit_index, input_index, and output_index instead of counting nodes.

Omitted names, None, and an empty mapping are width 1. Keys are matched after str(...). Unknown names raise Kpnn2Error. Values must be positive ints or numpy integers, stored as int; bool, numpy.bool_, floats, 0, and negatives are rejected. Input nodes must stay width 1: each reads one feature column, so separate measurements belong on separate input nodes.

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 (source name, target name), then target-unit outer, source-unit inner. hidden_nodes is empty when every node is an input or an output; the other tuples never are.

Raises:

Type Description
Kpnn2Error

If edgelist is not a DataFrame; source or target is absent, missing, or an empty name, or holds a value that is not a str, integer, float, or bool (a list, a tensor, bytes, any other object); the table has no rows; two node names differ only by leading or trailing whitespace ('A' and 'A '); a (source, target) pair is duplicated; there is no in-degree-0 node or no out-degree-0 node; or widths is not a mapping, names an unknown node, holds a value that is not a positive integer (bool included), or gives an input node a width other than 1. Each message names the offending pairs or nodes, sorted.

Warns:

Type Description
UserWarning

If source or target holds a float or a bool, converted to a name such as "1.0" or "True". Convert the column to the intended strings first to silence it.

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,)