Skip to content

parse_layered

parse_layered(
    edgelist: DataFrame,
    *,
    widths: Mapping[str, int] | None = None,
    ranks: Mapping[str, int] | None = None
) -> LayeredSpec

Parse a source/target edgelist into a LayeredSpec.

Prior-knowledge edges, such as genes into pathways, become depth-ranked layers plus one packed hop per layer, ready for a PackedLinear stack. Reach for it when a DAG should become one hop per layer; parse_adjacency is the shared-state packed alternative, which also allows cycles. Depth is longest path from inputs unless ranks assigns compact layers, names sort alphabetically within a layer, and no hop mask is allocated. Optional widths lets a named node own several units. Optional ranks places nodes at official ontology levels instead of longest-path hops.

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. Names are converted with str(...); extra columns are ignored. The frame is read, never modified.

required

widths

mapping of str to int

Units per named node. Omitted names, None, and an empty mapping are width 1. Keys are matched after str(...). Unknown names raise Kpnn2Error. Values must be positive ints; bool, 0, and negatives are rejected.

None

ranks

mapping of str to int

User depth per named node. None or omitted is longest-path ranking, identical to today's default. Keys are matched after str(...). Every graph node must be present; unknown names raise Kpnn2Error. Values are non-negative ints; bool and non-ints are rejected. Unique values are compacted to layers 0 .. L-1 with no empty layers, so numbers need not be 0-based or consecutive. All in-degree-0 nodes must share the minimum rank, and no non-input may use it. Every named edge must be strictly forward after compacting; same-rank edges are illegal. A node may have only skip parents (no parent at the previous compact layer).

None

Returns:

Type Description
LayeredSpec

Frozen structure: layers, one Hop per layer after the first, and skips metadata. skips is empty when no edge spans more than one layer; hops never is, because a valid edgelist always yields at least two layers.

Raises:

Type Description
Kpnn2Error

If edgelist is not a DataFrame; source or target is absent, missing, or an empty name; the table has no rows; a (source, target) pair is duplicated; any edge is a self-loop; the graph has a cycle; there is no in-degree-0 node or no out-degree-0 node; widths names an unknown node or is not a mapping of positive ints; or ranks is incomplete, names an unknown node, is not a mapping of non-negative ints, places inputs off the minimum rank, places a non-input at that minimum, or contains a non-forward edge. Each message names the offending pairs or nodes, sorted.

See Also

parse_adjacency : Pack the same table into one state vector; allows cycles and self-loops. Has no ranks argument. PackedLinear : Apply one hop from its packed indices. gather_hop_inputs : Build one hop's input from the saved layer tensors.

Notes

Every named edge belongs to exactly one hop, the one of its target layer, whether its depth gap is 1 or larger. Packed indices are in unit space: named edge A -> B expands into a (k_B, k_A) block of live pairs. At default width 1 that is one pair per named edge. A hop whose target has parents further back reads several layers, and its source columns are those layers concatenated in ascending order, so a skip edge is an ordinary weight rather than a dummy neuron or a second mechanism; skips only reports it. Under longest-path ranking a hop always reads the previous layer; with ranks it may omit that layer when every parent is a skip. Terminals below maximum depth (early outputs) are allowed, and isolated nodes cannot appear, since the node set is the union of source and target. to_dict() stores compacted ranks only when they differ from longest-path on the same edges.

Examples:

A chain A -> H -> C plus the skip A -> C. The hop into C reads both earlier layers, so the skip is a packed pair of that hop:

>>> import pandas as pd
>>> import kpnn2
>>> edgelist = pd.DataFrame(
...     {"source": ["A", "H", "A"], "target": ["H", "C", "C"]}
... )
>>> spec = kpnn2.parse_layered(edgelist)
>>> spec.layer_nodes
(('A',), ('H',), ('C',))
>>> spec.hops[1].source_nodes
('A', 'H')
>>> spec.hops[1].source_index, spec.hops[1].target_index
((0, 1), (0, 0))
>>> spec.hops[1].to_mask().tolist()
[[1.0, 1.0]]
>>> spec.skips[0].source, spec.skips[0].target
('A', 'C')

A hidden node of width 2 owns two units; the named edge A -> H becomes a 2-by-1 block:

>>> spec = kpnn2.parse_layered(
...     edgelist,
...     widths={"H": 2},
... )
>>> spec.layer_dims
(1, 2, 1)
>>> spec.layer_widths
((1,), (2,), (1,))
>>> spec.hops[0].to_mask().tolist()
[[1.0], [1.0]]

Two siblings at the same official level share a layer even when longest-path depths differ:

>>> siblings = pd.DataFrame(
...     {
...         "source": ["A", "A", "Mid"],
...         "target": ["Short", "Mid", "Long"],
...     }
... )
>>> spec = kpnn2.parse_layered(
...     siblings,
...     ranks={
...         "A": 0,
...         "Mid": 1,
...         "Short": 2,
...         "Long": 2,
...     },
... )
>>> spec.layer_nodes
(('A',), ('Mid',), ('Long', 'Short'))