Skip to content

LayeredSpec dataclass

LayeredSpec(
    input_nodes: tuple[str, ...],
    output_nodes: tuple[str, ...],
    hidden_nodes: tuple[str, ...],
    layer_nodes: tuple[tuple[str, ...], ...],
    layer_dims: tuple[int, ...],
    layer_widths: tuple[tuple[int, ...], ...],
    hops: tuple[Hop, ...],
    skips: tuple[Skip, ...],
)

Frozen blueprint from parse_layered.

Depth-ranked wiring for one knowledge-primed network: every node sits at a layer, and one Hop per layer after the first holds every edge entering it, skips included, as packed indices. Build one PackedLinear per hop, and use the name tuples to label tensors. It is structure only — no nn.Module, no parameters — and there is no stored mask. AdjacencySpec is the shared-state packed alternative.

Parameters:

Name Type Description Default

input_nodes

tuple[str, ...]

In-degree 0 names, alphabetical. This is the feature-axis order align_inputs returns as a column index. The gathered width is layer_dims[0].

required

output_nodes

tuple[str, ...]

Out-degree 0 names, alphabetical. A terminal node below maximum depth belongs here too, so this is not the same tuple as layer_nodes[-1].

required

hidden_nodes

tuple[str, ...]

Names that are neither input nor output, alphabetical.

required

layer_nodes

tuple[tuple[str, ...], ...]

layer_nodes[i] is the names at depth i, alphabetical. Index 0 is the input layer. Depth is longest path from the inputs unless parse_layered(..., ranks=) assigned compact ranks, and there are always at least two layers. One name per node, not per unit.

required

layer_dims

tuple[int, ...]

Unit count of each layer: layer_dims[i] is sum(layer_widths[i]). Equal to len(layer_nodes[i]) only when every node at that depth has width 1.

required

layer_widths

tuple[tuple[int, ...], ...]

layer_widths[i][j] is the width of layer_nodes[i][j]. Default parse yields 1 for every node.

required

hops

tuple[Hop, ...]

One hop per layer after the first: len(hops) == len(layer_nodes) - 1 and hops[i].target_layer == i + 1. hops[i] holds every edge entering layer i + 1, adjacent and skip alike, as packed indices over the concatenated source layers. hops[0] always reads layer 0 only.

required

skips

tuple[Skip, ...]

Original edges with depth gap greater than 1, as metadata. Each one is already a block of packed unit pairs in hops[target_layer - 1]; this list only says which edges span layers, and is empty when none do. Use edge_location to find the packed slots of a named edge, skip or adjacent.

required
See Also

parse_layered : Builds this spec from a source / target edgelist. AdjacencySpec : Packed sibling layout, for cycles, self-loops, or one shared state vector instead of depths. align_inputs : Column index that puts named features in input_nodes order; length is layer_dims[0]. gather_hop_inputs : Assembles one hop's input from the layer tensors produced so far. scatter_hop_outputs : Splits a transposed hop's output back onto those source layers. PackedLinear : Consumes hops[i].source_index / target_index as one layer. PackedLinear.transpose : Tied decode of that hop. MaskedLinear : Consumes hops[i].to_mask() as one layer.

Notes

Fields cannot be reassigned and sequences are tuples, so the structure itself is fixed. There is no stored mask tensor and no densifying mask property. to_mask() on each hop allocates a fresh dense rectangle; mutating that tensor does not change this spec. MaskedLinear(hop.to_mask()) clones the rectangle into a non-persistent buffer, so a layer built earlier keeps its own connectivity.

Because a hop carries every parent of its target, the per-row degree PackedLinear and MaskedLinear initialize from is the real fan-in of that unit, skips included.

to_edgelist(), to_dict() with from_dict(), and fingerprint are the supported interchange. Widths and ranks live on to_dict(), not on the edgelist: reparse of to_edgelist() without widths= / ranks= is width 1 and longest-path. to_dict() omits "ranks" when compacted layers equal longest-path on the same edges. Pickle and torch.save of the dataclass are not. edge_location finds packed slots of a named edge; it is not a constraint. A hard freeze of those slots is torch.where inside constraint=. node_units and hop_units map a named node to its contiguous unit slice on a layer tensor or a hop source axis.

Examples:

Inspect layers, a hop, and a skip after parsing:

>>> 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[0].source_layers, spec.hops[0].to_mask().tolist()
((0,), [[1.0]])
>>> spec.hops[1].source_layers, spec.hops[1].to_mask().tolist()
((0, 1), [[1.0, 1.0]])
>>> spec.skips[0].source, spec.skips[0].target
('A', 'C')
>>> spec.skips[0].source_layer, spec.skips[0].target_layer
(0, 2)
>>> spec.edge_location("A", "C")
(1, (0,))

Attributes

fingerprint property

fingerprint: str

SHA-256 hex digest of the canonical to_dict() JSON.

The payload is json.dumps(self.to_dict(), sort_keys=True, separators=(",", ":"), ensure_ascii=False) encoded as UTF-8. The result is 64 lowercase hex characters. It is not Python hash().

Returns:

Type Description
str

Hex digest of the tagged spec dict.

Examples:

>>> import pandas as pd
>>> import kpnn2
>>> edgelist = pd.DataFrame(
...     {
...         "source": ["A", "H"],
...         "target": ["H", "C"],
...     }
... )
>>> spec = kpnn2.parse_layered(edgelist)
>>> len(spec.fingerprint)
64
>>> (
...     spec.fingerprint
...     == kpnn2.parse_layered(spec.to_edgelist()).fingerprint
... )
True

Methods:

to_edgelist

to_edgelist() -> pd.DataFrame

Return this spec's edges as a two-column table.

Columns are exactly source then target. Rows follow the packed hop edges in canonical order: sorted lexicographically by (source, target), one row per original edge, names as strings. Extra columns from the DataFrame that was parsed are not reproduced.

parse_layered on this table reconstructs the same node lists and named edges when every node has width 1 and ranking is longest-path. Packed hop indices and layer_dims match in that case. Otherwise pass widths= and/or ranks=, or use from_dict(), which reads those keys from the tagged dict. Skip tuple order follows these sorted rows rather than the original parse input order; the skip set matches.

Returns:

Type Description
DataFrame

One row per original edge.

Examples:

Unsorted input comes back sorted:

>>> import pandas as pd
>>> import kpnn2
>>> edgelist = pd.DataFrame(
...     {
...         "source": ["H", "A", "A"],
...         "target": ["C", "C", "H"],
...     }
... )
>>> spec = kpnn2.parse_layered(edgelist)
>>> table = spec.to_edgelist()
>>> list(table.columns)
['source', 'target']
>>> table["source"].tolist()
['A', 'A', 'H']
>>> table["target"].tolist()
['C', 'H', 'C']

edge_location

edge_location(
    source: object, target: object
) -> tuple[int, tuple[int, ...]]

Return packed weight slots of one named edge.

source and target are matched after str(...), same as parse. This is identity into the hop that carries the edge, not a constraint.

Parameters:

Name Type Description Default

source

str

Source node name. Non-strings are converted with str(...).

required

target

str

Target node name. Non-strings are converted with str(...).

required

Returns:

Name Type Description
hop_index int

Index i such that the named edge is in hops[i].

packed_indices tuple of int

Indices into hops[i].source_index / target_index and the corresponding PackedLinear.weight. Length is k_source * k_target (1 at default width). Order is the stored order: named edges canonical lexicographic by (source name, target name); within one named edge, target-unit outer, source-unit inner.

Raises:

Type Description
Kpnn2Error

If the pair is missing, a name is empty, or a name is not a node. The message names the pair as {source} -> {target}.

Notes

Width greater than 1 does not change the named edge. It returns several packed indices, one per unit pair of the block. Mixed signs and frozen values are caller PyTorch on these slots. A hard freeze is torch.where inside constraint=, not a gradient hook.

Examples:

A chain plus a skip. The skip is on the later hop:

>>> import pandas as pd
>>> import kpnn2
>>> edgelist = pd.DataFrame(
...     {
...         "source": ["A", "H", "A"],
...         "target": ["H", "C", "C"],
...     }
... )
>>> spec = kpnn2.parse_layered(edgelist)
>>> spec.edge_location("A", "H")
(0, (0,))
>>> spec.edge_location("A", "C")
(1, (0,))

node_units

node_units(name: object) -> tuple[int, slice]

Return the layer and unit slice of one named node.

name is matched after str(...), same as parse. The slice indexes the last axis of that layer's tensor (saved[layer]). It is always a slice, including at width 1.

Parameters:

Name Type Description Default

name

str

Node name. Non-strings are converted with str(...).

required

Returns:

Name Type Description
layer int

Depth of the node: index into layer_nodes and the key of a saved-layer dict.

units slice

Contiguous columns on that layer's last axis. Length is the node's width.

Raises:

Type Description
Kpnn2Error

If name is empty or is not a node.

Notes

This is identity into the unit axis, not a dropout module and not a head helper. Index as saved[layer][..., units].

Examples:

>>> import pandas as pd
>>> import kpnn2
>>> edgelist = pd.DataFrame(
...     {
...         "source": ["A", "H"],
...         "target": ["H", "C"],
...     }
... )
>>> spec = kpnn2.parse_layered(
...     edgelist,
...     widths={"H": 3},
... )
>>> layer, units = spec.node_units("H")
>>> layer, units.start, units.stop
(1, 0, 3)

hop_units

hop_units(hop: Hop, name: object) -> slice

Return the unit slice of one named node on a hop axis.

The axis is the concatenated source of hop, the same tensor gather_hop_inputs returns. name is matched after str(...), same as parse. hop must compare equal to one entry of hops.

Parameters:

Name Type Description Default

hop

Hop

A hop from spec.hops.

required

name

str

Node name. Non-strings are converted with str(...).

required

Returns:

Type Description
slice

Contiguous columns on hop.in_features. Length is the node's width. Index as sources[..., units].

Raises:

Type Description
Kpnn2Error

If hop is not a Hop, does not match an entry of hops, name is empty, name is not a node, or the node is not a source of this hop.

Notes

column_offsets locates a whole source layer on this axis. This method locates one named node, widths included. A target-layer name is not on the source axis.

Examples:

>>> import pandas as pd
>>> import kpnn2
>>> edgelist = pd.DataFrame(
...     {
...         "source": ["A", "H", "A"],
...         "target": ["H", "C", "C"],
...     }
... )
>>> spec = kpnn2.parse_layered(edgelist)
>>> hop = spec.hops[1]
>>> spec.hop_units(hop, "A").start
0
>>> spec.hop_units(hop, "H").start
1

to_dict

to_dict() -> dict

Return this spec as a JSON-safe tagged dict.

Keys are kpnn2_spec (integer 1), layout ("layered"), and edges (list of [source, target] lists in the same order as to_edgelist() rows). When any node has width other than 1, a "widths" object of those names is included. All-1 graphs omit "widths". When compacted layers differ from longest-path on the same edges, a "ranks" object maps every node to its compacted 0-based layer index. A ranks= parse that matches longest-path omits "ranks". The returned dict is new on every call.

Returns:

Type Description
dict

Tagged edge list plus layout.

Examples:

>>> import pandas as pd
>>> import kpnn2
>>> edgelist = pd.DataFrame(
...     {
...         "source": ["A", "H"],
...         "target": ["H", "C"],
...     }
... )
>>> spec = kpnn2.parse_layered(edgelist)
>>> payload = spec.to_dict()
>>> payload["kpnn2_spec"]
1
>>> payload["layout"]
'layered'
>>> payload["edges"]
[['A', 'H'], ['H', 'C']]

from_dict classmethod

from_dict(payload: dict) -> LayeredSpec

Rebuild a LayeredSpec from to_dict() output.

Calls parse_layered on a DataFrame built from payload["edges"], passing payload["widths"] and payload["ranks"] when present. Hops and packed indices are not assembled by hand. Extra unknown keys are ignored. An absent or empty "widths" object is width 1. Absent "ranks" is longest-path.

Parameters:

Name Type Description Default

payload

dict

A dict with kpnn2_spec, layout, and edges. layout must be "layered". Optional "widths" is a node-name-to-int object. Optional "ranks" is a node-name-to-int object of compacted (or user) depths.

required

Returns:

Type Description
LayeredSpec

The parsed spec.

Raises:

Type Description
Kpnn2Error

If payload is not a dict; kpnn2_spec is missing or not 1; layout is missing, not a known layout, or is "adjacency"; edges is missing or not a sequence of two nonempty names; "widths" is present and not a mapping of positive ints; or "ranks" is present and not a mapping of non-negative ints covering every node.

Examples:

>>> import pandas as pd
>>> import kpnn2
>>> edgelist = pd.DataFrame(
...     {
...         "source": ["A", "H"],
...         "target": ["H", "C"],
...     }
... )
>>> spec = kpnn2.parse_layered(edgelist)
>>> roundtrip = kpnn2.LayeredSpec.from_dict(spec.to_dict())
>>> roundtrip.layer_nodes == spec.layer_nodes
True